pinetwork 0.1.2 → 0.1.4

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: 19ca8427c38f5d1a3cfef85fe1a3e026fb0b0f354b056c106d3d980e5182229e
4
- data.tar.gz: a76c9c5b067472f207d10d0d9bd52b92149b0d2192af5b13198bef6bf469e23e
3
+ metadata.gz: 9cab0d9673b76e3b49f2ccb041f917b9574ca85dedb1c5d4529fd19ada191b2b
4
+ data.tar.gz: ee2bf09fd869205f4377f7ad72c79897282b419d3551dc72f88facbd8d33ca0c
5
5
  SHA512:
6
- metadata.gz: 3de9acdecb5e03c5256be0a6a65d4341f3037fb64eb73262577a5dea5c61beae7a42ba7dc02596985683144242da6e7273b1ba6f37cf02019a09a294a2a8fae2
7
- data.tar.gz: 9828e6a0cd9fe3a8de440bef58589ac275a51119c94f2adf67874282134db21042f0081a6e1ab23519445be11ea257635bf9a91494fe44448c3fb10b93b2a1ea
6
+ metadata.gz: 9f3279cb6518e666ef5ee59de68af1f98d147f1e159c2af1d9c775915f0d22624a640b2f36654c48f8f9a729e4ace10db4f9bedff721769eb220f82a67ca420a
7
+ data.tar.gz: f6d002eae9fd88cfb6e6d86958558c6f01dbb69b365cd7799ebdbe601d9d6242fb30ded18353f51f7d7f1bd43992e68d763f7687d6453f094f9ccbfe384d58e5
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require 'rake/testtask'
2
+
3
+ Rake::TestTask.new do |t|
4
+ t.libs << "test"
5
+ t.test_files = FileList["test/*.rb"]
6
+ end
7
+
8
+ desc "Run tests"
9
+ task default: :test
data/lib/errors.rb ADDED
@@ -0,0 +1,45 @@
1
+ class ::PiNetwork
2
+ module Errors
3
+ class APIRequestError < StandardError
4
+ attr_reader :response_body
5
+ attr_reader :response_status
6
+
7
+ def initialize(message, response_status, response_body)
8
+ super(message)
9
+ @response_status = response_status
10
+ @response_body = response_body
11
+ end
12
+ end
13
+
14
+ class PaymentNotFoundError < StandardError
15
+ attr_reader :payment_id
16
+
17
+ def initialize(message, payment_id)
18
+ super(message)
19
+ @payment_id = payment_id
20
+ end
21
+ end
22
+
23
+ class TxidAlreadyLinkedError < StandardError
24
+ attr_reader :payment_id
25
+ attr_reader :txid
26
+
27
+ def initialize(message, payment_id, txid)
28
+ super(message)
29
+ @payment_id = payment_id
30
+ @txid = txid
31
+ end
32
+ end
33
+
34
+ class TxSubmissionError < StandardError
35
+ attr_reader :tx_error_code
36
+ attr_reader :op_error_codes
37
+
38
+ def initialize(tx_error_code, op_error_codes)
39
+ super(message)
40
+ @tx_error_code = tx_error_code
41
+ @op_error_codes = op_error_codes
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,5 +1,4 @@
1
- gem_dir = Gem::Specification.find_by_name("pi_network").gem_dir
2
- require "#{gem_dir}/lib/errors"
1
+ require_relative 'errors'
3
2
 
4
3
  class PiNetwork
5
4
  require 'faraday'
@@ -12,13 +11,21 @@ class PiNetwork
12
11
  attr_reader :base_url
13
12
  attr_reader :from_address
14
13
 
15
- def initialize(api_key, wallet_private_key, options = {})
14
+ BASE_URL = "https://api.minepi.com".freeze
15
+ MAINNET_HOST = "api.mainnet.minepi.com".freeze
16
+ TESTNET_HOST = "api.testnet.minepi.com".freeze
17
+
18
+ def initialize(api_key:, wallet_private_key:, faraday: Faraday.new, options: {})
16
19
  validate_private_seed_format!(wallet_private_key)
17
20
  @api_key = api_key
18
21
  @account = load_account(wallet_private_key)
19
- @base_url = options[:base_url] || "https://api.minepi.com"
22
+ @base_url = options[:base_url] || BASE_URL
23
+ @mainnet_host = options[:mainnet_host] || MAINNET_HOST
24
+ @testnet_host = options[:testnet_host] || TESTNET_HOST
25
+ @faraday = faraday
20
26
 
21
27
  @open_payments = {}
28
+ @open_payments_mutex = Mutex.new
22
29
  end
23
30
 
24
31
  def get_payment(payment_id)
@@ -42,7 +49,7 @@ class PiNetwork
42
49
  payment: payment_data,
43
50
  }
44
51
 
45
- response = Faraday.post(
52
+ response = @faraday.post(
46
53
  base_url + "/v2/payments",
47
54
  request_body.to_json,
48
55
  http_headers,
@@ -51,58 +58,68 @@ class PiNetwork
51
58
  parsed_response = handle_http_response(response, "An unknown error occurred while creating a payment")
52
59
 
53
60
  identifier = parsed_response["identifier"]
54
- @open_payments[identifier] = parsed_response
61
+ @open_payments_mutex.synchronize do
62
+ @open_payments[identifier] = parsed_response
63
+ end
55
64
 
56
65
  return identifier
57
66
  end
58
67
 
59
68
  def submit_payment(payment_id)
60
- payment = @open_payments[payment_id]
69
+ @open_payments_mutex.synchronize do
70
+ payment = @open_payments[payment_id]
61
71
 
62
- if payment.nil? || payment["identifier"] != payment_id
63
- payment = get_payment(payment_id)
64
- txid = payment["transaction"]["txid"]
65
- raise Errors::TxidAlreadyLinkedError.new("This payment already has a linked txid", payment_id, txid) if txid.present?
66
- end
72
+ if payment.nil? || payment["identifier"] != payment_id
73
+ payment = get_payment(payment_id)
74
+ txid = payment["transaction"]&.dig("txid")
75
+ raise Errors::TxidAlreadyLinkedError.new("This payment already has a linked txid", payment_id, txid) if txid.present?
76
+ end
67
77
 
68
- set_horizon_client(payment["network"])
69
- @from_address = payment["from_address"]
78
+ set_horizon_client(payment["network"])
79
+ @from_address = payment["from_address"]
70
80
 
71
- transaction_data = {
72
- amount: payment["amount"],
73
- identifier: payment["identifier"],
74
- recipient: payment["to_address"]
75
- }
81
+ transaction_data = {
82
+ amount: payment["amount"],
83
+ identifier: payment["identifier"],
84
+ recipient: payment["to_address"]
85
+ }
76
86
 
77
- transaction = build_a2u_transaction(transaction_data)
78
- txid = submit_transaction(transaction)
87
+ transaction = build_a2u_transaction(transaction_data)
88
+ txid = submit_transaction(transaction)
79
89
 
80
- @open_payments.delete(payment_id)
90
+ @open_payments.delete(payment_id)
81
91
 
82
- return txid
92
+ return txid
93
+ end
83
94
  end
84
95
 
85
- def complete_payment(identifier, txid)
96
+ def complete_payment(payment_id, txid)
86
97
  body = {"txid": txid}
87
98
 
88
99
  response = Faraday.post(
89
- base_url + "/v2/payments/#{identifier}/complete",
100
+ base_url + "/v2/payments/#{payment_id}/complete",
90
101
  body.to_json,
91
102
  http_headers
92
103
  )
93
104
 
94
- @open_payments.delete(identifier)
105
+ @open_payments_mutex.synchronize do
106
+ @open_payments.delete(payment_id)
107
+ end
108
+
95
109
  handle_http_response(response, "An unknown error occurred while completing the payment")
96
110
  end
97
111
 
98
- def cancel_payment(identifier)
112
+ def cancel_payment(payment_id)
99
113
  response = Faraday.post(
100
- base_url + "/v2/payments/#{identifier}/cancel",
114
+ base_url + "/v2/payments/#{payment_id}/cancel",
101
115
  {}.to_json,
102
116
  http_headers,
103
117
  )
104
118
 
105
- @open_payments.delete(identifier)
119
+ @open_payments_mutex.synchronize do
120
+ @open_payments.delete(payment_id)
121
+ end
122
+
106
123
  handle_http_response(response, "An unknown error occurred while cancelling the payment")
107
124
  end
108
125
 
@@ -130,7 +147,7 @@ class PiNetwork
130
147
 
131
148
  def handle_http_response(response, unknown_error_message = "An unknown error occurred while making an API request")
132
149
  unless response.status == 200
133
- error_message = JSON.parse(response.body).dig("error_message") rescue unknown_err_message
150
+ error_message = extract_error_message(response.body, unknown_error_message)
134
151
  raise Errors::APIRequestError.new(error_message, response.status, response.body)
135
152
  end
136
153
 
@@ -144,8 +161,9 @@ class PiNetwork
144
161
  end
145
162
 
146
163
  def set_horizon_client(network)
147
- host = network == "Pi Network" ? "api.mainnet.minepi.com" : "api.testnet.minepi.com"
148
- horizon = network == "Pi Network" ? "https://api.mainnet.minepi.com" : "https://api.testnet.minepi.com"
164
+ host = (network.starts_with? "Pi Network") ? @mainnet_host : @testnet_host
165
+ horizon = "https://#{host}"
166
+
149
167
  client = Stellar::Client.new(host: host, horizon: horizon)
150
168
  Stellar::default_network = network
151
169
 
@@ -186,8 +204,13 @@ class PiNetwork
186
204
 
187
205
  def submit_transaction(transaction)
188
206
  envelope = transaction.to_envelope(self.account.keypair)
189
- response = self.client.submit_transaction(tx_envelope: envelope)
190
- txid = response._response.body["id"]
207
+ begin
208
+ response = self.client.submit_transaction(tx_envelope: envelope)
209
+ txid = response._response.body["id"]
210
+ rescue => error
211
+ result_codes = error.response&.dig(:body, "extras", "result_codes")
212
+ raise Errors::TxSubmissionError.new(result_codes&.dig("transaction"), result_codes&.dig("operations"))
213
+ end
191
214
  end
192
215
 
193
216
  def validate_payment_data!(data, options = {})
@@ -200,7 +223,11 @@ class PiNetwork
200
223
  end
201
224
 
202
225
  def validate_private_seed_format!(seed)
203
- raise StandardError.new("Private Seed should start with \"S\"") unless seed.upcase.starts_with?("S")
226
+ raise StandardError.new("Private Seed should start with \"S\"") unless seed.upcase.start_with?("S")
204
227
  raise StandardError.new("Private Seed should be 56 characters") unless seed.length == 56
205
228
  end
229
+
230
+ def extract_error_message(response_body, default_message)
231
+ JSON.parse(response_body).dig("error_message") rescue default_message
232
+ end
206
233
  end
@@ -0,0 +1,35 @@
1
+ require 'minitest/autorun'
2
+ require_relative '../lib/pinetwork'
3
+
4
+ class A2UConcurrencyTest < Minitest::Test
5
+ def test_concurrent_create_payment
6
+ total_threads = 10000
7
+ api_key = "api-key"
8
+ wallet_private_key = "SC2L62EYF7LYF43L4OOSKUKDESRAFJZW3UW6RFZ57UY25VAMHTL2BFER"
9
+
10
+ threads = []
11
+
12
+ faraday_stub = Minitest::Mock.new
13
+ pi = PiNetwork.new(api_key: api_key, wallet_private_key: wallet_private_key, faraday: faraday_stub)
14
+
15
+ total_threads.times do
16
+ threads << Thread.new do
17
+ faraday_response = Faraday::Response.new(
18
+ status: 200,
19
+ body: {identifier: SecureRandom.alphanumeric(12)}.to_json,
20
+ response_headers: {}
21
+ )
22
+ faraday_stub.expect(:post, faraday_response) do |url|
23
+ url == "https://api.minepi.com/v2/payments"
24
+ end
25
+
26
+ payment_data = { amount: 1, memo: "test", metadata: {"info": "test"}, uid: "test-uid" }
27
+ payment_id = pi.create_payment(payment_data)
28
+ end
29
+ end
30
+ threads.each(&:join)
31
+
32
+ open_payments_after = pi.instance_variable_get(:@open_payments)
33
+ assert_equal(total_threads, open_payments_after.values.uniq.count, "open_payments got corrupted!")
34
+ end
35
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: pinetwork
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pi Core Team
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2023-01-25 00:00:00.000000000 Z
11
+ date: 2024-02-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: stellar-sdk
@@ -44,13 +44,16 @@ executables: []
44
44
  extensions: []
45
45
  extra_rdoc_files: []
46
46
  files:
47
- - lib/pi_network.rb
47
+ - Rakefile
48
+ - lib/errors.rb
49
+ - lib/pinetwork.rb
50
+ - test/a2u_concurrency_test.rb
48
51
  homepage: https://github.com/pi-apps/pi-ruby
49
52
  licenses:
50
53
  - PiOS
51
54
  metadata:
52
55
  documentation_uri: https://github.com/pi-apps/pi-ruby
53
- post_install_message:
56
+ post_install_message:
54
57
  rdoc_options: []
55
58
  require_paths:
56
59
  - lib
@@ -65,8 +68,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
65
68
  - !ruby/object:Gem::Version
66
69
  version: '0'
67
70
  requirements: []
68
- rubygems_version: 3.0.3.1
69
- signing_key:
71
+ rubygems_version: 3.4.10
72
+ signing_key:
70
73
  specification_version: 4
71
74
  summary: Pi Network Ruby
72
75
  test_files: []