stellar-sdk 0.7.0 → 0.8.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
-
[![Build Status](https://travis-ci.org/
|
4
|
-
[![Code Climate](https://codeclimate.com/github/
|
5
|
-
|
6
|
-
*STATUS: this library is very early and incomplete. The examples provided do not work, yet*
|
3
|
+
[![Build Status](https://travis-ci.org/bloom-solutions/ruby-stellar-sdk.svg)](https://travis-ci.org/bloom-solutions/ruby-stellar-sdk)
|
4
|
+
[![Code Climate](https://codeclimate.com/github/bloom-solutions/ruby-stellar-sdk/badges/gpa.svg)](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
|