stellar-sdk 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.gitignore +1 -0
- data/.travis.yml +5 -4
- data/CHANGELOG.md +5 -0
- data/README.md +8 -7
- data/examples/07_sep10.rb +125 -0
- data/lib/stellar-sdk.rb +1 -1
- data/lib/stellar/client.rb +72 -4
- data/lib/stellar/sep10.rb +384 -0
- data/lib/stellar/version.rb +1 -1
- data/ruby-stellar-sdk.gemspec +6 -7
- data/spec/fixtures/vcr_cassettes/Stellar_Client/_load_account/loads_successfully.yml +226 -0
- data/spec/fixtures/vcr_cassettes/Stellar_Client/_send_payment/memo/accepts_the_memo_attribute.yml +239 -230
- data/spec/fixtures/vcr_cassettes/Stellar_Client/_send_payment/using_a_payment_channel/sends_a_payment_account_through_a_channel_account.yml +198 -135
- data/spec/fixtures/vcr_cassettes/Stellar_Client/_send_payment/using_a_payment_channel/sends_a_payment_when_the_channel_is_the_same_as_from_.yml +166 -110
- data/spec/lib/stellar/client_spec.rb +18 -1
- data/spec/lib/stellar/sep10_spec.rb +1148 -0
- metadata +25 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7c9a8f80386d8ae7e790f4cd5198968433c692417c2de7fd87d8d934d966e6a
|
4
|
+
data.tar.gz: d31590ea3a3a2d469f4f0fc5fe59232696c7d090a4ab70415d823fed3a9228b1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 45d62b035a05bdcdf5f93bf696e67e490a7f87df4b4e64b43503eb7e91a0b7dcfe7d4b8556119a0db9021ed32c8f2bf98684470bf58dae383d78fb44e67f8324
|
7
|
+
data.tar.gz: 919b5641f08bd77ada6f39b0eac25e89a4aa8b66d27c7b32bdbfa2efe26b701551612d7b997750185124f6de3252a5013390982032c771a26103d3e2dc367b66
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
@@ -3,14 +3,15 @@ rvm:
|
|
3
3
|
- 2.4.1
|
4
4
|
- 2.5.1
|
5
5
|
- 2.6.1
|
6
|
-
- jruby-9.
|
6
|
+
- jruby-9.2.5.0
|
7
7
|
cache: bundler
|
8
|
+
addons:
|
9
|
+
apt:
|
10
|
+
packages:
|
11
|
+
- libsodium-dev
|
8
12
|
before_install:
|
9
13
|
- gem update --system
|
10
14
|
- gem install bundler -v 2.0
|
11
|
-
- sudo add-apt-repository -y ppa:chris-lea/libsodium
|
12
|
-
- sudo apt-get -y update
|
13
|
-
- sudo apt-get install -y libsodium-dev
|
14
15
|
script: LD_LIBRARY_PATH=lib bundle exec rake travis
|
15
16
|
before_script:
|
16
17
|
- cp spec/config.yml.sample spec/config.yml
|
data/CHANGELOG.md
CHANGED
@@ -4,6 +4,11 @@ All notable changes to this project will be documented in this file.
|
|
4
4
|
The format is based on [Keep a Changelog](http://keepachangelog.com/)
|
5
5
|
and this project adheres to [Semantic Versioning](http://semver.org/).
|
6
6
|
|
7
|
+
## [0.8.0](https://github.com/stellar/ruby-stellar-sdk/compare/v0.7.0...v0.8.0)
|
8
|
+
### Added
|
9
|
+
- SEP-10 Multisig Support [#69](https://github.com/stellar/ruby-stellar-sdk/pull/69)
|
10
|
+
- `X-Client-Name` and `X-Client-Version` headers
|
11
|
+
|
7
12
|
## [0.7.0] - 2019-04-26
|
8
13
|
### Added
|
9
14
|
- Friendbot support
|
data/README.md
CHANGED
@@ -1,9 +1,7 @@
|
|
1
1
|
# Ruby Stellar
|
2
2
|
|
3
|
-
[](https://travis-ci.org/bloom-solutions/ruby-stellar-sdk)
|
4
|
+
[](https://codeclimate.com/github/bloom-solutions/ruby-stellar-sdk)
|
7
5
|
|
8
6
|
This library helps you to integrate your application into the [Stellar network](http://stellar.org).
|
9
7
|
|
@@ -45,7 +43,7 @@ client.send_payment({
|
|
45
43
|
})
|
46
44
|
```
|
47
45
|
|
48
|
-
Be sure to set the network when submitting to the public network (more information in [stellar-base](https://www.github.com/
|
46
|
+
Be sure to set the network when submitting to the public network (more information in [stellar-base](https://www.github.com/bloom-solutions/ruby-stellar-base)):
|
49
47
|
|
50
48
|
```ruby
|
51
49
|
Stellar.default_network = Stellar::Networks::PUBLIC
|
@@ -53,14 +51,17 @@ Stellar.default_network = Stellar::Networks::PUBLIC
|
|
53
51
|
|
54
52
|
## Development
|
55
53
|
|
54
|
+
- Install and activate [rvm](https://rvm.io/rvm/install)
|
55
|
+
- Ensure your `bundler` version is up-to-date: `gem install bundler`
|
56
|
+
- Run `bundle install`
|
56
57
|
- Copy `spec/config.yml.sample` to `spec/config.yml`
|
57
58
|
- Replace anything in `spec/config.yml` especially if you will re-record specs
|
58
|
-
- `rspec spec`
|
59
|
+
- `bundle exec rspec spec`
|
59
60
|
|
60
61
|
## Contributing
|
61
62
|
|
62
63
|
1. Sign the [Contributor License Agreement](https://docs.google.com/forms/d/1g7EF6PERciwn7zfmfke5Sir2n10yddGGSXyZsq98tVY/viewform?usp=send_form)
|
63
|
-
2. Fork it ( https://github.com/
|
64
|
+
2. Fork it ( https://github.com/bloom-solutions/ruby-stellar-lib/fork )
|
64
65
|
2. Create your feature branch (`git checkout -b my-new-feature`)
|
65
66
|
3. Commit your changes (`git commit -am 'Add some feature'`)
|
66
67
|
4. Push to the branch (`git push origin my-new-feature`)
|
@@ -0,0 +1,125 @@
|
|
1
|
+
require 'stellar-sdk'
|
2
|
+
require 'hyperclient'
|
3
|
+
|
4
|
+
$client = Stellar::Client.default_testnet
|
5
|
+
$client_master_kp = Stellar::KeyPair.random
|
6
|
+
$client_signer_kp1 = Stellar::KeyPair.random
|
7
|
+
$client_signer_kp2 = Stellar::KeyPair.random
|
8
|
+
$server_kp = Stellar::KeyPair.random
|
9
|
+
|
10
|
+
def setup_multisig
|
11
|
+
# create funded account
|
12
|
+
# On mainet there is no friendbot, use Stellar::Client.create_account instead
|
13
|
+
account = Stellar::Account.from_seed($client_master_kp.seed)
|
14
|
+
$client.friendbot(account)
|
15
|
+
|
16
|
+
# get account sequence number for next transaction
|
17
|
+
sequence_number = $client.account_info(account).sequence.to_i + 1
|
18
|
+
|
19
|
+
# build the non-master signers to be added to the account
|
20
|
+
signer1 = Stellar::Signer.new(
|
21
|
+
key: Stellar::SignerKey.ed25519($client_signer_kp1),
|
22
|
+
weight: 1
|
23
|
+
)
|
24
|
+
signer2 = Stellar::Signer.new(
|
25
|
+
key: Stellar::SignerKey.ed25519($client_signer_kp2),
|
26
|
+
weight: 1
|
27
|
+
)
|
28
|
+
|
29
|
+
# Stellar::Transaction only has method to construct single-operation
|
30
|
+
# transactions, so we have to add an operation to add an additional signer.
|
31
|
+
tx = Stellar::Transaction.set_options({
|
32
|
+
account: $client_master_kp,
|
33
|
+
sequence: sequence_number,
|
34
|
+
signer: signer1,
|
35
|
+
low_threshold: 1,
|
36
|
+
med_threshold: 2,
|
37
|
+
high_threshold: 3,
|
38
|
+
fee: 100 * 3
|
39
|
+
})
|
40
|
+
tx.operations << Stellar::Operation.set_options({ signer: signer2 })
|
41
|
+
|
42
|
+
envelope_xdr = tx.to_envelope($client_master_kp).to_xdr(:base64)
|
43
|
+
begin
|
44
|
+
$client.horizon.transactions._post(tx: envelope_xdr)
|
45
|
+
rescue Faraday::ClientError => e
|
46
|
+
p e.response
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
|
51
|
+
# This function walks throught the steps both the wallet and server would take
|
52
|
+
# during a SEP-10 challenge verification.
|
53
|
+
def example_verify_challenge_tx_threshold
|
54
|
+
# 1. The wallet makes a GET request to /auth,
|
55
|
+
# 2. The server receives the request, and returns the challenge xdr.
|
56
|
+
envelope_xdr = Stellar::SEP10.build_challenge_tx(
|
57
|
+
server: $server_kp,
|
58
|
+
client: $client_master_kp,
|
59
|
+
anchor_name: "SDF",
|
60
|
+
timeout: 600
|
61
|
+
)
|
62
|
+
# 3. The wallet recieves the challenge xdr and collects enough signatures from
|
63
|
+
# the accounts signers to reach the medium threshold on the account.
|
64
|
+
# `envelope.signatures` already contains the server's signature, so the wallet
|
65
|
+
# adds to the list.
|
66
|
+
envelope = Stellar::TransactionEnvelope.from_xdr(envelope_xdr, "base64")
|
67
|
+
envelope.signatures += [
|
68
|
+
envelope.tx.sign_decorated($client_master_kp),
|
69
|
+
envelope.tx.sign_decorated($client_signer_kp1),
|
70
|
+
envelope.tx.sign_decorated($client_signer_kp2)
|
71
|
+
]
|
72
|
+
envelope_xdr = envelope.to_xdr(:base64)
|
73
|
+
|
74
|
+
# 4. The wallet makes a POST request to /auth containing the signed challenge
|
75
|
+
# 5. The server verifies the challenge transaction
|
76
|
+
envelope, client_master_address = Stellar::SEP10.read_challenge_tx(
|
77
|
+
challenge_xdr: envelope_xdr,
|
78
|
+
server: $server_kp
|
79
|
+
)
|
80
|
+
account = Stellar::Account.from_address(client_master_address)
|
81
|
+
begin
|
82
|
+
# Get all signers and thresholds for account
|
83
|
+
info = $client.account_info(account)
|
84
|
+
rescue Faraday::ResourceNotFound
|
85
|
+
# The account doesn't exist yet.
|
86
|
+
# In this situation, all the server can do is verify that the client master
|
87
|
+
# keypair has signed the transaction.
|
88
|
+
begin
|
89
|
+
Stellar::SEP10.verify_challenge_tx(
|
90
|
+
challenge_xdr: envelope_xdr, server: $server_kp
|
91
|
+
)
|
92
|
+
rescue Stellar::InvalidSep10ChallengeError => e
|
93
|
+
puts "You should handle possible exceptions:"
|
94
|
+
puts e
|
95
|
+
else
|
96
|
+
puts "Challenge verified by client master key signature"
|
97
|
+
end
|
98
|
+
else
|
99
|
+
# The account exists, so the server should check if the signatures reach the
|
100
|
+
# medium threshold on the account
|
101
|
+
begin
|
102
|
+
signers_found = Stellar::SEP10.verify_challenge_tx_threshold(
|
103
|
+
challenge_xdr: envelope_xdr,
|
104
|
+
server: $server_kp,
|
105
|
+
threshold: info.thresholds["med_threshold"],
|
106
|
+
signers: Set.new(info.signers)
|
107
|
+
)
|
108
|
+
rescue Stellar::InvalidSep10ChallengeError => e
|
109
|
+
puts "You should handle possible exceptions:"
|
110
|
+
puts e
|
111
|
+
else
|
112
|
+
puts "Challenge signatures and threshold verified!"
|
113
|
+
total_weight = 0
|
114
|
+
signers_found.each do |signer|
|
115
|
+
total_weight += signer['weight']
|
116
|
+
puts "signer: #{signer['key']}, weight: #{signer['weight']}"
|
117
|
+
end
|
118
|
+
puts "Account medium threshold: #{info.thresholds["med_threshold"]}, total signature(s) weight: #{total_weight}"
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
# Comment out setup_multisig to execute Stellar::Account.verify_challenge_transaction
|
124
|
+
setup_multisig
|
125
|
+
example_verify_challenge_tx_threshold
|
data/lib/stellar-sdk.rb
CHANGED
data/lib/stellar/client.rb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
require 'hyperclient'
|
2
2
|
require "active_support/core_ext/object/blank"
|
3
|
+
require 'securerandom'
|
3
4
|
|
4
5
|
module Stellar
|
6
|
+
class InvalidSep10ChallengeError < StandardError; end
|
7
|
+
|
5
8
|
class Client
|
6
9
|
include Contracts
|
7
10
|
C = Contracts
|
@@ -46,14 +49,20 @@ module Stellar
|
|
46
49
|
conn.adapter :excon
|
47
50
|
end
|
48
51
|
client.headers = {
|
49
|
-
'Accept' => 'application/hal+json,application/problem+json,application/json'
|
52
|
+
'Accept' => 'application/hal+json,application/problem+json,application/json',
|
53
|
+
"X-Client-Name" => "ruby-stellar-sdk",
|
54
|
+
"X-Client-Version" => VERSION,
|
50
55
|
}
|
51
56
|
end
|
52
57
|
end
|
53
58
|
|
54
|
-
Contract Stellar::Account => Any
|
55
|
-
def account_info(
|
56
|
-
|
59
|
+
Contract Or[Stellar::Account, String] => Any
|
60
|
+
def account_info(account_or_address)
|
61
|
+
if account_or_address.is_a?(Stellar::Account)
|
62
|
+
account_id = account_or_address.address
|
63
|
+
else
|
64
|
+
account_id = account_or_address
|
65
|
+
end
|
57
66
|
@horizon.account(account_id:account_id)._get
|
58
67
|
end
|
59
68
|
|
@@ -187,5 +196,64 @@ module Stellar
|
|
187
196
|
horizon.transactions._post(tx: envelope_base64)
|
188
197
|
end
|
189
198
|
|
199
|
+
Contract(C::KeywordArgs[
|
200
|
+
server: Stellar::KeyPair,
|
201
|
+
client: Stellar::KeyPair,
|
202
|
+
anchor_name: String,
|
203
|
+
timeout: C::Optional[Integer]
|
204
|
+
] => String)
|
205
|
+
# DEPRECATED: this function has been moved Stellar::SEP10.build_challenge_tx and
|
206
|
+
# will be removed in the next major version release.
|
207
|
+
#
|
208
|
+
# A wrapper function for Stellar::SEP10::build_challenge_tx.
|
209
|
+
#
|
210
|
+
# @param server [Stellar::KeyPair] Keypair for server's signing account.
|
211
|
+
# @param client [Stellar::KeyPair] Keypair for the account whishing to authenticate with the server.
|
212
|
+
# @param anchor_name [String] Anchor's name to be used in the manage_data key.
|
213
|
+
# @param timeout [Integer] Challenge duration (default to 5 minutes).
|
214
|
+
#
|
215
|
+
# @return [String] A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction.
|
216
|
+
def build_challenge_tx(server:, client:, anchor_name:, timeout: 300)
|
217
|
+
Stellar::SEP10.build_challenge_tx(
|
218
|
+
server: server, client: client, anchor_name: anchor_name, timeout: timeout
|
219
|
+
)
|
220
|
+
end
|
221
|
+
|
222
|
+
Contract(C::KeywordArgs[
|
223
|
+
challenge: String,
|
224
|
+
server: Stellar::KeyPair
|
225
|
+
] => C::Bool)
|
226
|
+
# DEPRECATED: this function has been moved to Stellar::SEP10::read_challenge_tx and
|
227
|
+
# will be removed in the next major version release.
|
228
|
+
#
|
229
|
+
# A wrapper function for Stellar::SEP10.verify_challenge_transaction
|
230
|
+
#
|
231
|
+
# @param challenge [String] SEP0010 transaction challenge in base64.
|
232
|
+
# @param server [Stellar::KeyPair] Stellar::KeyPair for server where the challenge was generated.
|
233
|
+
#
|
234
|
+
# @return [Boolean]
|
235
|
+
def verify_challenge_tx(challenge:, server:)
|
236
|
+
Stellar::SEP10.verify_challenge_tx(challenge_tx: challenge, server: server)
|
237
|
+
true
|
238
|
+
end
|
239
|
+
|
240
|
+
Contract(C::KeywordArgs[
|
241
|
+
transaction_envelope: Stellar::TransactionEnvelope,
|
242
|
+
keypair: Stellar::KeyPair
|
243
|
+
] => C::Bool)
|
244
|
+
# DEPRECATED: this function has been moved to Stellar::SEP10::verify_tx_signed_by and
|
245
|
+
# will be removed in the next major version release.
|
246
|
+
#
|
247
|
+
# @param transaction_envelope [Stellar::TransactionEnvelope]
|
248
|
+
# @param keypair [Stellar::KeyPair]
|
249
|
+
#
|
250
|
+
# @return [Boolean]
|
251
|
+
#
|
252
|
+
def verify_tx_signed_by(transaction_envelope:, keypair:)
|
253
|
+
Stellar::SEP10.verify_tx_signed_by(
|
254
|
+
tx_envelope: transaction_envelope, keypair: keypair
|
255
|
+
)
|
256
|
+
end
|
257
|
+
|
190
258
|
end
|
191
259
|
end
|
@@ -0,0 +1,384 @@
|
|
1
|
+
module Stellar
|
2
|
+
class InvalidSep10ChallengeError < StandardError; end
|
3
|
+
|
4
|
+
class SEP10
|
5
|
+
include Contracts
|
6
|
+
C = Contracts
|
7
|
+
|
8
|
+
Contract(C::KeywordArgs[
|
9
|
+
server: Stellar::KeyPair,
|
10
|
+
client: Stellar::KeyPair,
|
11
|
+
anchor_name: String,
|
12
|
+
timeout: C::Optional[Integer]
|
13
|
+
] => String)
|
14
|
+
#
|
15
|
+
# Helper method to create a valid {SEP0010}[https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md]
|
16
|
+
# challenge transaction which you can use for Stellar Web Authentication.
|
17
|
+
#
|
18
|
+
# @param server [Stellar::KeyPair] Keypair for server's signing account.
|
19
|
+
# @param client [Stellar::KeyPair] Keypair for the account whishing to authenticate with the server.
|
20
|
+
# @param anchor_name [String] Anchor's name to be used in the manage_data key.
|
21
|
+
# @param timeout [Integer] Challenge duration (default to 5 minutes).
|
22
|
+
#
|
23
|
+
# @return [String] A base64 encoded string of the raw TransactionEnvelope xdr struct for the transaction.
|
24
|
+
#
|
25
|
+
# = Example
|
26
|
+
#
|
27
|
+
# Stellar::SEP10.build_challenge_tx(server: server, client: user, anchor_name: anchor, timeout: timeout)
|
28
|
+
#
|
29
|
+
# @see {SEP0010: Stellar Web Authentication}[https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0010.md]
|
30
|
+
def self.build_challenge_tx(server:, client:, anchor_name:, timeout: 300)
|
31
|
+
# The value must be 64 bytes long. It contains a 48 byte
|
32
|
+
# cryptographic-quality random string encoded using base64 (for a total of
|
33
|
+
# 64 bytes after encoding).
|
34
|
+
value = SecureRandom.base64(48)
|
35
|
+
|
36
|
+
tx = Stellar::Transaction.manage_data({
|
37
|
+
account: server,
|
38
|
+
sequence: 0,
|
39
|
+
name: "#{anchor_name} auth",
|
40
|
+
value: value,
|
41
|
+
source_account: client
|
42
|
+
})
|
43
|
+
|
44
|
+
now = Time.now.to_i
|
45
|
+
tx.time_bounds = Stellar::TimeBounds.new(
|
46
|
+
min_time: now,
|
47
|
+
max_time: now + timeout
|
48
|
+
)
|
49
|
+
|
50
|
+
tx.to_envelope(server).to_xdr(:base64)
|
51
|
+
end
|
52
|
+
|
53
|
+
|
54
|
+
Contract(C::KeywordArgs[
|
55
|
+
challenge_xdr: String,
|
56
|
+
server: Stellar::KeyPair
|
57
|
+
] => [Stellar::TransactionEnvelope, String])
|
58
|
+
# Reads a SEP 10 challenge transaction and returns the decoded transaction envelope and client account ID contained within.
|
59
|
+
#
|
60
|
+
# It also verifies that transaction is signed by the server.
|
61
|
+
#
|
62
|
+
# It does not verify that the transaction has been signed by the client or
|
63
|
+
# that any signatures other than the servers on the transaction are valid. Use
|
64
|
+
# one of the following functions to completely verify the transaction:
|
65
|
+
# - Stellar::SEP10.verify_challenge_tx_threshold
|
66
|
+
# - Stellar::SEP10.verify_challenge_tx_signers
|
67
|
+
#
|
68
|
+
# @param challenge_xdr [String] SEP0010 transaction challenge in base64.
|
69
|
+
# @param server [Stellar::KeyPair] keypair for server where the challenge was generated.
|
70
|
+
#
|
71
|
+
# @return [Stellar::TransactionEnvelope, String]
|
72
|
+
#
|
73
|
+
# = Example
|
74
|
+
#
|
75
|
+
# sep10 = Stellar::SEP10
|
76
|
+
# challenge = sep10.build_challenge_tx(server: server, client: user, anchor_name: anchor, timeout: timeout)
|
77
|
+
# envelope, client_address = sep10.read_challenge_tx(challenge: challenge, server: server)
|
78
|
+
#
|
79
|
+
def self.read_challenge_tx(challenge_xdr:, server:)
|
80
|
+
envelope = Stellar::TransactionEnvelope.from_xdr(challenge_xdr, "base64")
|
81
|
+
transaction = envelope.tx
|
82
|
+
|
83
|
+
if transaction.seq_num != 0
|
84
|
+
raise InvalidSep10ChallengeError.new(
|
85
|
+
"The transaction sequence number should be zero"
|
86
|
+
)
|
87
|
+
end
|
88
|
+
|
89
|
+
if transaction.source_account != server.public_key
|
90
|
+
raise InvalidSep10ChallengeError.new(
|
91
|
+
"The transaction source account is not equal to the server's account"
|
92
|
+
)
|
93
|
+
end
|
94
|
+
|
95
|
+
if transaction.operations.size != 1
|
96
|
+
raise InvalidSep10ChallengeError.new(
|
97
|
+
"The transaction should contain only one operation"
|
98
|
+
)
|
99
|
+
end
|
100
|
+
|
101
|
+
operation = transaction.operations.first
|
102
|
+
client_account_id = operation.source_account
|
103
|
+
|
104
|
+
if client_account_id.nil?
|
105
|
+
raise InvalidSep10ChallengeError.new(
|
106
|
+
"The transaction's operation should contain a source account"
|
107
|
+
)
|
108
|
+
end
|
109
|
+
|
110
|
+
if operation.body.arm != :manage_data_op
|
111
|
+
raise InvalidSep10ChallengeError.new(
|
112
|
+
"The transaction's operation should be manageData"
|
113
|
+
)
|
114
|
+
end
|
115
|
+
|
116
|
+
if operation.body.value.data_value.unpack("m")[0].size != 48
|
117
|
+
raise InvalidSep10ChallengeError.new(
|
118
|
+
"The transaction's operation value should be a 64 bytes base64 random string"
|
119
|
+
)
|
120
|
+
end
|
121
|
+
|
122
|
+
if !verify_tx_signed_by(tx_envelope: envelope, keypair: server)
|
123
|
+
raise InvalidSep10ChallengeError.new(
|
124
|
+
"The transaction is not signed by the server"
|
125
|
+
)
|
126
|
+
end
|
127
|
+
|
128
|
+
time_bounds = transaction.time_bounds
|
129
|
+
now = Time.now.to_i
|
130
|
+
|
131
|
+
if time_bounds.nil? || !now.between?(time_bounds.min_time, time_bounds.max_time)
|
132
|
+
raise InvalidSep10ChallengeError.new("The transaction has expired")
|
133
|
+
end
|
134
|
+
|
135
|
+
# Mirror the return type of the other SDK's and return a string
|
136
|
+
client_kp = Stellar::KeyPair.from_public_key(client_account_id.ed25519!)
|
137
|
+
|
138
|
+
return envelope, client_kp.address
|
139
|
+
end
|
140
|
+
|
141
|
+
|
142
|
+
Contract(C::KeywordArgs[
|
143
|
+
challenge_xdr: String,
|
144
|
+
server: Stellar::KeyPair,
|
145
|
+
signers: SetOf[String]
|
146
|
+
] => C::SetOf[String])
|
147
|
+
# Verifies that for a SEP 10 challenge transaction all signatures on the transaction are accounted for.
|
148
|
+
#
|
149
|
+
# A transaction is verified if it is signed by the server account, and all other signatures match a signer
|
150
|
+
# that has been provided as an argument. Additional signers can be provided that do not have a signature,
|
151
|
+
# but all signatures must be matched to a signer for verification to succeed.
|
152
|
+
#
|
153
|
+
# If verification succeeds a list of signers that were found is returned, excluding the server account ID.
|
154
|
+
#
|
155
|
+
# @param challenge_xdr [String] SEP0010 transaction challenge transaction in base64.
|
156
|
+
# @param server [Stellar::Keypair] keypair for server's account.
|
157
|
+
# @param signers [SetOf[String]] The signers of client account.
|
158
|
+
#
|
159
|
+
# @return [SetOf[String]]
|
160
|
+
#
|
161
|
+
# Raises a InvalidSep10ChallengeError if:
|
162
|
+
# - The transaction is invalid according to Stellar::SEP10.read_challenge_tx.
|
163
|
+
# - One or more signatures in the transaction are not identifiable as the server account or one of the
|
164
|
+
# signers provided in the arguments.
|
165
|
+
def self.verify_challenge_tx_signers(
|
166
|
+
challenge_xdr:,
|
167
|
+
server:,
|
168
|
+
signers:
|
169
|
+
)
|
170
|
+
if signers.empty?
|
171
|
+
raise InvalidSep10ChallengeError.new("No signers provided.")
|
172
|
+
end
|
173
|
+
|
174
|
+
te, _ = read_challenge_tx(
|
175
|
+
challenge_xdr: challenge_xdr, server: server
|
176
|
+
)
|
177
|
+
|
178
|
+
# deduplicate signers and ignore non-G addresses
|
179
|
+
client_signers = Set.new
|
180
|
+
signers.each do |signer|
|
181
|
+
# ignore server kp if passed
|
182
|
+
if signer == server.address
|
183
|
+
next
|
184
|
+
end
|
185
|
+
begin
|
186
|
+
Stellar::Util::StrKey.check_decode(:account_id, signer)
|
187
|
+
rescue
|
188
|
+
next
|
189
|
+
else
|
190
|
+
client_signers.add(signer)
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
if client_signers.empty?
|
195
|
+
raise InvalidSep10ChallengeError.new("At least one signer with a G... address must be provied")
|
196
|
+
end
|
197
|
+
|
198
|
+
# verify all signatures in one pass
|
199
|
+
all_signers = client_signers + Set[server.address]
|
200
|
+
signers_found = verify_tx_signatures(
|
201
|
+
tx_envelope: te, signers: all_signers
|
202
|
+
)
|
203
|
+
|
204
|
+
# ensure server signed transaction and remove it
|
205
|
+
if !signers_found.delete?(server.address)
|
206
|
+
raise InvalidSep10ChallengeError.new("Transaction not signed by server: #{server.address}")
|
207
|
+
end
|
208
|
+
|
209
|
+
# Confirm we matched signatures to the client signers.
|
210
|
+
if signers_found.empty?
|
211
|
+
raise InvalidSep10ChallengeError.new("Transaction not signed by any client signer.")
|
212
|
+
end
|
213
|
+
|
214
|
+
# Confirm all signatures were consumed by a signer.
|
215
|
+
if signers_found.length != te.signatures.length - 1
|
216
|
+
raise InvalidSep10ChallengeError.new("Transaction has unrecognized signatures.")
|
217
|
+
end
|
218
|
+
|
219
|
+
return signers_found
|
220
|
+
|
221
|
+
end
|
222
|
+
|
223
|
+
Contract(C::KeywordArgs[
|
224
|
+
challenge_xdr: String,
|
225
|
+
server: Stellar::KeyPair,
|
226
|
+
threshold: Integer,
|
227
|
+
signers: SetOf[::Hash],
|
228
|
+
] => C::SetOf[::Hash])
|
229
|
+
# Verifies that for a SEP 10 challenge transaction all signatures on the transaction
|
230
|
+
# are accounted for and that the signatures meet a threshold on an account. A
|
231
|
+
# transaction is verified if it is signed by the server account, and all other
|
232
|
+
# signatures match a signer that has been provided as an argument, and those
|
233
|
+
# signatures meet a threshold on the account.
|
234
|
+
#
|
235
|
+
# @param challenge_xdr [String] SEP0010 transaction challenge transaction in base64.
|
236
|
+
# @param server [Stellar::KeyPair] keypair for server's account.
|
237
|
+
# @param threshold [Integer] The medThreshold on the client account.
|
238
|
+
# @param signers [SetOf[::Hash]]The signers of client account.
|
239
|
+
#
|
240
|
+
# @return [SetOf[::Hash]]
|
241
|
+
#
|
242
|
+
# Raises a InvalidSep10ChallengeError if:
|
243
|
+
# - The transaction is invalid according to Stellar::SEP10.read_challenge_transaction.
|
244
|
+
# - One or more signatures in the transaction are not identifiable as the server
|
245
|
+
# account or one of the signers provided in the arguments.
|
246
|
+
# - The signatures are all valid but do not meet the threshold.
|
247
|
+
def self.verify_challenge_tx_threshold(
|
248
|
+
challenge_xdr:,
|
249
|
+
server:,
|
250
|
+
threshold:,
|
251
|
+
signers:
|
252
|
+
)
|
253
|
+
signer_str_set = signers.map { |s| s['key'] }.to_set
|
254
|
+
signer_strs_found = verify_challenge_tx_signers(
|
255
|
+
challenge_xdr: challenge_xdr,
|
256
|
+
server: server,
|
257
|
+
signers: signer_str_set
|
258
|
+
)
|
259
|
+
|
260
|
+
weight = 0
|
261
|
+
signers_found = Set.new
|
262
|
+
signers.each do |s|
|
263
|
+
if !signer_strs_found.include?(s['key'])
|
264
|
+
next
|
265
|
+
end
|
266
|
+
signer_strs_found.delete(s['key'])
|
267
|
+
signers_found.add(s)
|
268
|
+
weight += s['weight']
|
269
|
+
end
|
270
|
+
|
271
|
+
if weight < threshold
|
272
|
+
raise InvalidSep10ChallengeError.new(
|
273
|
+
"signers with weight #{weight} do not meet threshold #{threshold}."
|
274
|
+
)
|
275
|
+
end
|
276
|
+
|
277
|
+
return signers_found
|
278
|
+
end
|
279
|
+
|
280
|
+
Contract(C::KeywordArgs[
|
281
|
+
challenge_xdr: String,
|
282
|
+
server: Stellar::KeyPair
|
283
|
+
] => nil)
|
284
|
+
# DEPRECATED: Use verify_challenge_tx_signers instead.
|
285
|
+
# This function does not support multiple client signatures.
|
286
|
+
#
|
287
|
+
# Verifies if a transaction is a valid per SEP-10 challenge transaction, if the validation
|
288
|
+
# fails, an exception will be thrown.
|
289
|
+
#
|
290
|
+
# This function performs the following checks:
|
291
|
+
# 1. verify that transaction sequenceNumber is equal to zero;
|
292
|
+
# 2. verify that transaction source account is equal to the server's signing key;
|
293
|
+
# 3. verify that transaction has time bounds set, and that current time is between the minimum and maximum bounds;
|
294
|
+
# 4. verify that transaction contains a single Manage Data operation and it's source account is not null;
|
295
|
+
# 5. verify that transaction envelope has a correct signature by server's signing key;
|
296
|
+
# 6. verify that transaction envelope has a correct signature by the operation's source account;
|
297
|
+
#
|
298
|
+
# @param challenge_xdr [String] SEP0010 transaction challenge transaction in base64.
|
299
|
+
# @param server [Stellar::KeyPair] keypair for server's account.
|
300
|
+
#
|
301
|
+
# Raises a InvalidSep10ChallengeError if the validation fails
|
302
|
+
def self.verify_challenge_tx(
|
303
|
+
challenge_xdr: String, server: Stellar::KeyPair
|
304
|
+
)
|
305
|
+
transaction_envelope, client_address = read_challenge_tx(
|
306
|
+
challenge_xdr: challenge_xdr, server: server
|
307
|
+
)
|
308
|
+
client_keypair = Stellar::KeyPair.from_address(client_address)
|
309
|
+
if !verify_tx_signed_by(tx_envelope: transaction_envelope, keypair: client_keypair)
|
310
|
+
raise InvalidSep10ChallengeError.new(
|
311
|
+
"Transaction not signed by client: %s" % [client_keypair.address]
|
312
|
+
)
|
313
|
+
end
|
314
|
+
end
|
315
|
+
|
316
|
+
Contract(C::KeywordArgs[
|
317
|
+
tx_envelope: Stellar::TransactionEnvelope,
|
318
|
+
signers: SetOf[String]
|
319
|
+
] => SetOf[String])
|
320
|
+
# Verifies every signer passed matches a signature on the transaction exactly once,
|
321
|
+
# returning a list of unique signers that were found to have signed the transaction.
|
322
|
+
#
|
323
|
+
# @param tx_envelope [Stellar::TransactionEnvelope] SEP0010 transaction challenge transaction envelope.
|
324
|
+
# @param signers [SetOf[String]] The signers of client account.
|
325
|
+
#
|
326
|
+
# @return [SetOf[String]]
|
327
|
+
def self.verify_tx_signatures(
|
328
|
+
tx_envelope:,
|
329
|
+
signers:
|
330
|
+
)
|
331
|
+
signatures = tx_envelope.signatures
|
332
|
+
if signatures.empty?
|
333
|
+
raise InvalidSep10ChallengeError.new("Transaction has no signatures.")
|
334
|
+
end
|
335
|
+
|
336
|
+
tx_hash = tx_envelope.tx.hash
|
337
|
+
signatures_used = Set.new
|
338
|
+
signers_found = Set.new
|
339
|
+
signers.each do |signer|
|
340
|
+
kp = Stellar::KeyPair.from_address(signer)
|
341
|
+
tx_envelope.signatures.each_with_index do |sig, i|
|
342
|
+
if signatures_used.include?(i)
|
343
|
+
next
|
344
|
+
end
|
345
|
+
if sig.hint != kp.signature_hint
|
346
|
+
next
|
347
|
+
end
|
348
|
+
if kp.verify(sig.signature, tx_hash)
|
349
|
+
signatures_used.add(i)
|
350
|
+
signers_found.add(signer)
|
351
|
+
end
|
352
|
+
end
|
353
|
+
end
|
354
|
+
|
355
|
+
return signers_found
|
356
|
+
end
|
357
|
+
|
358
|
+
Contract(C::KeywordArgs[
|
359
|
+
tx_envelope: Stellar::TransactionEnvelope,
|
360
|
+
keypair: Stellar::KeyPair
|
361
|
+
] => C::Bool)
|
362
|
+
# Verifies if a Stellar::TransactionEnvelope was signed by the given Stellar::KeyPair
|
363
|
+
#
|
364
|
+
# @param tx_envelope [Stellar::TransactionEnvelope]
|
365
|
+
# @param keypair [Stellar::KeyPair]
|
366
|
+
#
|
367
|
+
# @return [Boolean]
|
368
|
+
#
|
369
|
+
# = Example
|
370
|
+
#
|
371
|
+
# Stellar::SEP10.verify_tx_signed_by(tx_envelope: envelope, keypair: keypair)
|
372
|
+
#
|
373
|
+
def self.verify_tx_signed_by(tx_envelope:, keypair:)
|
374
|
+
tx_hash = tx_envelope.tx.hash
|
375
|
+
tx_envelope.signatures.any? do |sig|
|
376
|
+
if sig.hint != keypair.signature_hint
|
377
|
+
next
|
378
|
+
end
|
379
|
+
keypair.verify(sig.signature, tx_hash)
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
end
|
384
|
+
end
|