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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3390d577fe288a6397d9f1c2844729023abc68b3147584211794d1d0eed294b2
4
- data.tar.gz: 01ce41b3d2a9be6ea02196e14639a379955b584744435a622d7091840426358e
3
+ metadata.gz: d7c9a8f80386d8ae7e790f4cd5198968433c692417c2de7fd87d8d934d966e6a
4
+ data.tar.gz: d31590ea3a3a2d469f4f0fc5fe59232696c7d090a4ab70415d823fed3a9228b1
5
5
  SHA512:
6
- metadata.gz: c740fe9977ea1dd2ae41ce0f2c4001eec2142d3adafe2739a2ec7deed9ba60f31419ad1f63c8d63b91cbc565146f855f5041f4171e2e324bda8770f1eae7d12b
7
- data.tar.gz: 563d52017e9767190f0bd09d1fdf0dfe17903dfb55820b53ca24be6eabd2e4cc8d3896a6bf4148ee6f2fcdbec89fecba08ec7b012a5d2258bf6fd354bbac28d0
6
+ metadata.gz: 45d62b035a05bdcdf5f93bf696e67e490a7f87df4b4e64b43503eb7e91a0b7dcfe7d4b8556119a0db9021ed32c8f2bf98684470bf58dae383d78fb44e67f8324
7
+ data.tar.gz: 919b5641f08bd77ada6f39b0eac25e89a4aa8b66d27c7b32bdbfa2efe26b701551612d7b997750185124f6de3252a5013390982032c771a26103d3e2dc367b66
data/.gitignore CHANGED
@@ -14,3 +14,4 @@
14
14
  *.a
15
15
  mkmf.log
16
16
  /spec/config.yml
17
+ .vscode
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.1.6.0
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/stellar/ruby-stellar-sdk.svg)](https://travis-ci.org/stellar/ruby-stellar-sdk)
4
- [![Code Climate](https://codeclimate.com/github/stellar/ruby-stellar-sdk/badges/gpa.svg)](https://codeclimate.com/github/stellar/ruby-stellar-sdk)
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/stellar/ruby-stellar-base)):
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/stellar/ruby-stellar-lib/fork )
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
@@ -4,9 +4,9 @@ require 'contracts'
4
4
  module Stellar
5
5
 
6
6
  autoload :Account
7
- autoload :AccountInfo
8
7
  autoload :Amount
9
8
  autoload :Client
9
+ autoload :SEP10
10
10
 
11
11
  module Horizon
12
12
  extend ActiveSupport::Autoload
@@ -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(account)
56
- account_id = account.address
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