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 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