sequence-sdk 1.0.3 → 1.0.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.
@@ -2,32 +2,43 @@ require_relative './client_module'
2
2
  require_relative './response_object'
3
3
  require_relative './query'
4
4
 
5
-
6
5
  module Sequence
7
6
  # A summation of contract amounts. Contracts are selected using a filter, and
8
7
  # their values are summed using the common values of one or more contract
9
8
  # fields.
10
9
  class Balance < ResponseObject
11
-
12
10
  # @!attribute [r] amount
13
- # Summation of contract amounts.
11
+ # Summation of contract amounts.
14
12
  # @return [Integer]
15
13
  attrib :amount
16
14
 
17
15
  # @!attribute [r] sum_by
18
- # List of parameters along which contract amounts were summed.
16
+ # List of parameters along which contract amounts were summed.
19
17
  # @return [Hash<String => String>]
20
18
  attrib :sum_by
21
19
 
22
20
  class ClientModule < Sequence::ClientModule
23
21
  # Executes a query, returning an enumerable over individual balances.
24
- # @param [Hash] opts Options hash
25
- # @option opts [String] filter A filter expression.
26
- # @option opts [Array<String|Integer>] filter_params A list of values that will be interpolated into the filter expression.
27
- # @option opts [Array<String>] sum_by A list of fields along which contract values will be summed.
28
- # @option opts [Integer] timestamp A millisecond Unix timestamp. Indicates that the query should be run over the state of the ledger at a given point in time.
22
+ # @param [Hash] opts
23
+ # Options hash
24
+ # @option opts [String] filter
25
+ # A filter expression.
26
+ # @option opts [Array<String|Integer>] filter_params
27
+ # A list of values that will be interpolated into the filter expression.
28
+ # @option opts [Array<String>] sum_by
29
+ # A list of fields along which contract values will be summed.
30
+ # @option opts [Integer>] page_size
31
+ # The number of items to return in the result set.
29
32
  # @return [Query]
30
33
  def query(opts = {})
34
+ validate_inclusion_of!(
35
+ opts,
36
+ :filter,
37
+ :filter_params,
38
+ :sum_by,
39
+ :page_size,
40
+ :after,
41
+ )
31
42
  Query.new(client, opts)
32
43
  end
33
44
  end
@@ -1,4 +1,5 @@
1
1
  require_relative './account'
2
+ require_relative './action'
2
3
  require_relative './asset'
3
4
  require_relative './balance'
4
5
  require_relative './contract'
@@ -9,26 +10,30 @@ require_relative './transaction'
9
10
 
10
11
  module Sequence
11
12
  class Client
12
-
13
- # @param [Hash] opts Options hash
14
- # @option opts [String] url The ledger API URL.
15
- # @option opts [String] access_token The ledger access token.
13
+ # @param [Hash] opts
14
+ # Options hash
15
+ # @option opts [String] ledger
16
+ # Ledger name.
17
+ # @option opts [String] credential
18
+ # API credential secret.
16
19
  # @return [Query]
17
20
  def initialize(opts = {})
18
- if !opts.key?(:ledger) || opts[:ledger].empty?
19
- raise ArgumentError.new('missing :ledger parameter')
21
+ if opts[:ledger].nil? || opts[:ledger].empty?
22
+ raise ArgumentError, ':ledger must be provided'
20
23
  end
21
- if !opts.key?(:credential) || opts[:credential].empty?
22
- raise ArgumentError.new('missing :credential parameter')
24
+ if opts[:credential].nil? || opts[:credential].empty?
25
+ raise ArgumentError, ':credential must be provided'
23
26
  end
24
27
 
25
28
  @opts = opts
26
29
  end
27
30
 
31
+ # @private
28
32
  def opts
29
33
  @opts.dup
30
34
  end
31
35
 
36
+ # @private
32
37
  # @return [Session]
33
38
  def session
34
39
  @session ||= Session.new(@opts)
@@ -44,6 +49,11 @@ module Sequence
44
49
  @assets ||= Asset::ClientModule.new(self)
45
50
  end
46
51
 
52
+ # @return [Action::ClientModule]
53
+ def actions
54
+ @actions ||= Action::ClientModule.new(self)
55
+ end
56
+
47
57
  # @return [Balance::ClientModule]
48
58
  def balances
49
59
  @balances ||= Balance::ClientModule.new(self)
@@ -64,6 +74,7 @@ module Sequence
64
74
  @contracts ||= Contract::ClientModule.new(self)
65
75
  end
66
76
 
77
+ # @private
67
78
  # @return [Stats::ClientModule]
68
79
  def stats
69
80
  @stats ||= Stats::ClientModule.new(self)
@@ -73,6 +84,5 @@ module Sequence
73
84
  def dev_utils
74
85
  @dev_utils ||= DevUtils::ClientModule.new(self)
75
86
  end
76
-
77
87
  end
78
88
  end
@@ -1,13 +1,15 @@
1
+ require_relative './validations'
2
+
1
3
  module Sequence
2
4
  # Base class for ledger client components.
3
- # @abstract
5
+ # @private
4
6
  class ClientModule
7
+ include Sequence::Validations
5
8
 
6
9
  attr_reader :client
7
10
 
8
11
  def initialize(client)
9
12
  @client = client
10
13
  end
11
-
12
14
  end
13
15
  end
@@ -6,68 +6,83 @@ module Sequence
6
6
  # An entry in the ledger that contains value that can be spent.
7
7
  class Contract < ResponseObject
8
8
  # @!attribute [r] id
9
- # A unique ID.
9
+ # A unique ID.
10
10
  # @return [String]
11
11
  attrib :id
12
12
 
13
13
  # @!attribute [r] type
14
- # The type of the contract. Currently, this is always "account".
14
+ # The type of the contract. Currently, this is always "account".
15
15
  # @return [String]
16
16
  attrib :type
17
17
 
18
18
  # @!attribute [r] transaction_id
19
- # The ID of the transaction in which the contract appears.
19
+ # The ID of the transaction in which the contract appears.
20
20
  # @return [String]
21
21
  attrib :transaction_id
22
22
 
23
23
  # @!attribute [r] asset_id
24
- # The ID of the asset held by the contract.
24
+ # The ID of the asset held by the contract.
25
25
  # @return [String]
26
26
  attrib :asset_id
27
27
 
28
28
  # @!attribute [r] asset_alias
29
- # The alias of the asset held by the contract.
29
+ # The alias of the asset held by the contract.
30
30
  # @return [String]
31
31
  attrib :asset_alias
32
32
 
33
33
  # @!attribute [r] asset_tags
34
- # The tags of the asset held by the contract.
34
+ # The tags of the asset held by the contract.
35
35
  # @return [Hash]
36
36
  attrib :asset_tags
37
37
 
38
38
  # @!attribute [r] amount
39
- # The number of units of the asset held by the contract.
39
+ # The number of units of the asset held by the contract.
40
40
  # @return [Integer]
41
41
  attrib :amount
42
42
 
43
43
  # @!attribute [r] account_id
44
- # The ID of the account controlling the contract.
44
+ # The ID of the account controlling the contract.
45
45
  # @return [String]
46
46
  attrib :account_id
47
47
 
48
48
  # @!attribute [r] account_alias
49
- # The alias of the account controlling the contract.
49
+ # The alias of the account controlling the contract.
50
50
  # @return [String]
51
51
  attrib :account_alias
52
52
 
53
53
  # @!attribute [r] account_tags
54
- # The tags of the account controlling the contract.
54
+ # The tags of the account controlling the contract.
55
55
  # @return [Hash]
56
56
  attrib :account_tags
57
57
 
58
58
  # @!attribute [r] reference_data
59
- # User-specified key-value data embedded in the contract.
59
+ # User-specified key-value data embedded in the contract.
60
60
  # @return [Hash]
61
61
  attrib :reference_data
62
62
 
63
63
  class ClientModule < Sequence::ClientModule
64
64
  # Executes a query, returning an enumerable over individual contracts.
65
- # @param [Hash] opts Options hash
66
- # @option opts [String] filter A filter expression.
67
- # @option opts [Array<String|Integer>] filter_params A list of values that will be interpolated into the filter expression.
68
- # @option opts [Integer] timestamp A millisecond Unix timestamp. Indicates that the query should be run over the state of the ledger at a given point in time.
65
+ # @param [Hash] opts
66
+ # Options hash
67
+ # @option opts [String] filter
68
+ # A filter expression.
69
+ # @option opts [Array<String|Integer>] filter_params
70
+ # A list of values that will be interpolated into the filter expression.
71
+ # @option opts [Integer] timestamp
72
+ # A millisecond Unix timestamp. Indicates that the query should be run
73
+ # over the state of the ledger at a given point in time.
74
+ # @option opts [Integer>] page_size
75
+ # The number of items to return in the result set.
69
76
  # @return [Query]
70
77
  def query(opts = {})
78
+ validate_inclusion_of!(
79
+ opts,
80
+ :filter,
81
+ :filter_params,
82
+ :timestamp,
83
+ :page_size,
84
+ :after,
85
+ )
71
86
  Query.new(client, opts)
72
87
  end
73
88
  end
@@ -8,7 +8,7 @@ module Sequence
8
8
  class ClientModule < Sequence::ClientModule
9
9
  # Deletes all data in the ledger.
10
10
  def reset
11
- client.session.request('/reset', client_token: SecureRandom.uuid)
11
+ client.session.request('/reset')
12
12
  end
13
13
  end
14
14
  end
@@ -34,23 +34,23 @@ module Sequence
34
34
  # an error code, a message, and an optional detail field that provides
35
35
  # additional context for the error.
36
36
  class APIError < BaseError
37
- RETRIABLE_STATUS_CODES = [
38
- 408, # Request Timeout
39
- 429, # Too Many Requests
40
- 500, # Internal Server Error
41
- 502, # Bad Gateway
42
- 503, # Service Unavailable
43
- 504, # Gateway Timeout
44
- 509, # Bandwidth Limit Exceeded
45
- ]
46
-
47
- attr_accessor :code, :chain_message, :detail, :data, :temporary, :request_id, :response
37
+ attr_accessor(
38
+ :chain_message,
39
+ :code,
40
+ :data,
41
+ :detail,
42
+ :request_id,
43
+ :response,
44
+ :retriable,
45
+ :temporary,
46
+ )
48
47
 
49
48
  def initialize(body, response)
50
49
  self.code = body['code']
51
50
  self.chain_message = body['message']
52
51
  self.detail = body['detail']
53
- self.temporary = body['temporary']
52
+ self.retriable = body['retriable']
53
+ self.temporary = body['retriable']
54
54
 
55
55
  self.response = response
56
56
  self.request_id = response['Chain-Request-ID'] if response
@@ -59,7 +59,7 @@ module Sequence
59
59
  end
60
60
 
61
61
  def retriable?
62
- temporary || (response && RETRIABLE_STATUS_CODES.include?(Integer(response.code)))
62
+ self.retriable
63
63
  end
64
64
 
65
65
  def self.format_error_message(code, message, detail, request_id)
@@ -2,14 +2,15 @@ require 'net/http'
2
2
  require 'net/https'
3
3
  require 'openssl'
4
4
  require 'thread'
5
+ require 'securerandom'
5
6
 
6
7
  module Sequence
8
+ # @private
7
9
  class HttpWrapper
8
-
9
10
  # Parameters to the retry exponential backoff function.
10
- MAX_RETRIES = 10
11
11
  RETRY_BASE_DELAY_MS = 40
12
- RETRY_MAX_DELAY_MS = 4000
12
+ RETRY_MAX_DELAY_MS = 20_000
13
+ RETRY_TIMEOUT_SECS = 120 # 2 minutes
13
14
 
14
15
  NETWORK_ERRORS = [
15
16
  InvalidRequestIDError,
@@ -22,46 +23,76 @@ module Sequence
22
23
  Errno::ETIMEDOUT,
23
24
  Errno::EHOSTUNREACH,
24
25
  Errno::ECONNREFUSED,
25
- ]
26
+ ].freeze
26
27
 
27
28
  attr_accessor :dis_macaroon
28
29
 
29
- def initialize(host, macaroon, opts={})
30
+ def initialize(host, macaroon, opts = {})
30
31
  @mutex = Mutex.new
31
32
  @host = URI(host)
32
33
  @macaroon = macaroon
33
34
  @dis_macaroon = nil
34
35
  @opts = opts
35
- @http = setup_http
36
+ @connection = setup_connection
36
37
  end
37
38
 
38
- def post(url, body)
39
+ def post(id, url, body)
40
+ start_time = Process.clock_gettime(Process::CLOCK_MONOTONIC)
39
41
  attempts = 0
42
+ idempotency_key = SecureRandom.uuid
40
43
  begin
41
44
  attempts += 1
42
45
  # If this is a retry and not the first attempt, sleep before making the
43
46
  # retry request.
44
47
  sleep(backoff_delay(attempts)) if attempts > 1
45
48
 
49
+ attempt_id = "#{id}/#{attempts}"
46
50
  @mutex.synchronize do
47
51
  req = Net::HTTP::Post.new(url)
48
52
  req.body = JSON.dump(body)
49
53
  req['Accept'] = 'application/json'
50
54
  req['Content-Type'] = 'application/json'
55
+ req['Id'] = attempt_id
56
+ req['Idempotency-Key'] = idempotency_key
51
57
  req['User-Agent'] = 'chain-sdk-ruby/' + Sequence::VERSION
52
- if @macaroon != nil && @dis_macaroon != nil
58
+ if !@macaroon.nil? && !@dis_macaroon.nil?
53
59
  req['Macaroon'] = @macaroon
54
60
  req['Discharge-Macaroon'] = @dis_macaroon
55
61
  end
56
- return @http.request(req)
62
+ unless @connection.started?
63
+ @connection.start
64
+ end
65
+ response = @connection.request(req)
66
+
67
+ if block_given?
68
+ yield response
69
+ end
70
+
71
+ # We must parse any APIErrors here so that
72
+ # the retry logic can handle them.
73
+ status = Integer(response.code)
74
+ parsed_body = nil
75
+ if status != 204 # No Content
76
+ begin
77
+ parsed_body = JSON.parse(response.body)
78
+ rescue JSON::JSONError
79
+ raise JSONError.new(attempt_id, response)
80
+ end
81
+ end
82
+ if status / 100 != 2
83
+ klass = status == 401 ? UnauthorizedError : APIError
84
+ raise klass.new(parsed_body, response)
85
+ end
86
+
87
+ { parsed_body: parsed_body, response: response }
57
88
  end
58
89
  rescue *NETWORK_ERRORS => e
59
- raise e if attempts > MAX_RETRIES
90
+ raise e if elapsed_secs(start_time) > RETRY_TIMEOUT_SECS
60
91
  retry
61
92
  rescue APIError => e
62
- raise e if attempts > MAX_RETRIES
63
- retry if e.retriable?
64
- raise e
93
+ raise e unless e.retriable?
94
+ raise e if elapsed_secs(start_time) > RETRY_TIMEOUT_SECS
95
+ retry
65
96
  end
66
97
  end
67
98
 
@@ -69,14 +100,18 @@ module Sequence
69
100
 
70
101
  MILLIS_TO_SEC = 0.001
71
102
 
103
+ def elapsed_secs(start_time)
104
+ Process.clock_gettime(Process::CLOCK_MONOTONIC) - start_time
105
+ end
106
+
72
107
  def backoff_delay(attempt)
73
- max = RETRY_BASE_DELAY_MS * 2**(attempt-1)
108
+ max = RETRY_BASE_DELAY_MS * 2**(attempt - 1)
74
109
  max = [max, RETRY_MAX_DELAY_MS].min
75
110
  millis = rand(max) + 1
76
111
  millis * MILLIS_TO_SEC
77
112
  end
78
113
 
79
- def setup_http
114
+ def setup_connection
80
115
  args = [@host.hostname, @host.port]
81
116
 
82
117
  # Proxy configuration
@@ -87,25 +122,24 @@ module Sequence
87
122
  end
88
123
  end
89
124
 
90
- http = Net::HTTP.new(*args)
91
- http.set_debug_output($stdout) if ENV['DEBUG']
125
+ connection = Net::HTTP.new(*args)
126
+ connection.set_debug_output($stdout) if ENV['DEBUG']
92
127
 
93
128
  # TLS configuration
94
- http.use_ssl = true
95
- http.verify_mode = OpenSSL::SSL::VERIFY_PEER
129
+ connection.use_ssl = true
130
+ connection.verify_mode = OpenSSL::SSL::VERIFY_PEER
96
131
  [:ca_file, :cert, :key].each do |k|
97
132
  next unless @opts.key?(:ssl_params) && @opts[:ssl_params].key?(k)
98
- http.send("#{k}=", @opts[:ssl_params][k])
133
+ connection.send("#{k}=", @opts[:ssl_params][k])
99
134
  end
100
135
 
101
136
  # Timeout configuration
102
137
  [:open_timeout, :read_timeout, :ssl_timeout].each do |k|
103
138
  next unless @opts.key?(k)
104
- http.send "#{k}=", @opts[k]
139
+ connection.send "#{k}=", @opts[k]
105
140
  end
106
141
 
107
- return http
142
+ connection
108
143
  end
109
-
110
144
  end
111
145
  end