coinbase-sdk 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
Files changed (77) hide show
  1. checksums.yaml +4 -4
  2. data/lib/coinbase/address/external_address.rb +173 -0
  3. data/lib/coinbase/address/wallet_address.rb +219 -0
  4. data/lib/coinbase/address.rb +32 -219
  5. data/lib/coinbase/asset.rb +1 -1
  6. data/lib/coinbase/authenticator.rb +2 -0
  7. data/lib/coinbase/client/api/addresses_api.rb +1 -1
  8. data/lib/coinbase/client/api/assets_api.rb +1 -1
  9. data/lib/coinbase/client/api/external_addresses_api.rb +1 -1
  10. data/lib/coinbase/client/api/server_signers_api.rb +1 -1
  11. data/lib/coinbase/client/api/stake_api.rb +1 -1
  12. data/lib/coinbase/client/api/trades_api.rb +1 -1
  13. data/lib/coinbase/client/api/transfers_api.rb +1 -1
  14. data/lib/coinbase/client/api/users_api.rb +1 -1
  15. data/lib/coinbase/client/api/wallets_api.rb +1 -1
  16. data/lib/coinbase/client/api_client.rb +3 -3
  17. data/lib/coinbase/client/api_error.rb +1 -1
  18. data/lib/coinbase/client/configuration.rb +1 -1
  19. data/lib/coinbase/client/models/address.rb +1 -1
  20. data/lib/coinbase/client/models/address_balance_list.rb +1 -1
  21. data/lib/coinbase/client/models/address_list.rb +1 -1
  22. data/lib/coinbase/client/models/asset.rb +1 -1
  23. data/lib/coinbase/client/models/balance.rb +1 -1
  24. data/lib/coinbase/client/models/broadcast_trade_request.rb +1 -1
  25. data/lib/coinbase/client/models/broadcast_transfer_request.rb +1 -1
  26. data/lib/coinbase/client/models/build_staking_operation_request.rb +1 -1
  27. data/lib/coinbase/client/models/create_address_request.rb +1 -1
  28. data/lib/coinbase/client/models/create_server_signer_request.rb +22 -5
  29. data/lib/coinbase/client/models/create_trade_request.rb +1 -1
  30. data/lib/coinbase/client/models/create_transfer_request.rb +1 -1
  31. data/lib/coinbase/client/models/create_wallet_request.rb +1 -1
  32. data/lib/coinbase/client/models/create_wallet_request_wallet.rb +1 -1
  33. data/lib/coinbase/client/models/error.rb +1 -1
  34. data/lib/coinbase/client/models/faucet_transaction.rb +23 -5
  35. data/lib/coinbase/client/models/feature.rb +1 -1
  36. data/lib/coinbase/client/models/fetch_staking_rewards200_response.rb +1 -1
  37. data/lib/coinbase/client/models/fetch_staking_rewards_request.rb +2 -15
  38. data/lib/coinbase/client/models/get_staking_context_request.rb +1 -1
  39. data/lib/coinbase/client/models/partial_eth_staking_context.rb +4 -7
  40. data/lib/coinbase/client/models/seed_creation_event.rb +1 -1
  41. data/lib/coinbase/client/models/seed_creation_event_result.rb +1 -1
  42. data/lib/coinbase/client/models/server_signer.rb +22 -5
  43. data/lib/coinbase/client/models/server_signer_event.rb +1 -1
  44. data/lib/coinbase/client/models/server_signer_event_event.rb +1 -1
  45. data/lib/coinbase/client/models/server_signer_event_list.rb +1 -1
  46. data/lib/coinbase/client/models/server_signer_list.rb +1 -1
  47. data/lib/coinbase/client/models/signature_creation_event.rb +1 -1
  48. data/lib/coinbase/client/models/signature_creation_event_result.rb +1 -1
  49. data/lib/coinbase/client/models/staking_context.rb +1 -1
  50. data/lib/coinbase/client/models/staking_context_context.rb +1 -1
  51. data/lib/coinbase/client/models/staking_operation.rb +15 -12
  52. data/lib/coinbase/client/models/staking_reward.rb +21 -5
  53. data/lib/coinbase/client/models/staking_reward_format.rb +40 -0
  54. data/lib/coinbase/client/models/trade.rb +1 -1
  55. data/lib/coinbase/client/models/trade_list.rb +1 -1
  56. data/lib/coinbase/client/models/transaction.rb +1 -1
  57. data/lib/coinbase/client/models/transaction_type.rb +1 -1
  58. data/lib/coinbase/client/models/transfer.rb +1 -1
  59. data/lib/coinbase/client/models/transfer_list.rb +1 -1
  60. data/lib/coinbase/client/models/user.rb +1 -1
  61. data/lib/coinbase/client/models/wallet.rb +1 -1
  62. data/lib/coinbase/client/models/wallet_list.rb +1 -1
  63. data/lib/coinbase/client/version.rb +1 -1
  64. data/lib/coinbase/client.rb +2 -1
  65. data/lib/coinbase/errors.rb +7 -0
  66. data/lib/coinbase/faucet_transaction.rb +5 -4
  67. data/lib/coinbase/pagination.rb +26 -0
  68. data/lib/coinbase/staking_operation.rb +29 -0
  69. data/lib/coinbase/staking_reward.rb +79 -0
  70. data/lib/coinbase/transaction.rb +6 -0
  71. data/lib/coinbase/user.rb +5 -51
  72. data/lib/coinbase/wallet.rb +95 -100
  73. data/lib/coinbase.rb +11 -0
  74. metadata +8 -19
  75. data/lib/coinbase/client/api/balances_api.rb +0 -97
  76. data/lib/coinbase/client/api/transfer_api.rb +0 -114
  77. data/lib/coinbase/client/models/request_faucet_funds200_response.rb +0 -222
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c52613ebb145609813f96508c0eff76721ee6afebf3f3207394dad4c91b51635
4
- data.tar.gz: d7f86c6cfbc02b6dd8866717cc9a89439b81da9ae8838dc34855dd10bf9f3fa9
3
+ metadata.gz: df442398edde68b8c97794f9b1f0a2bfd33304f02459962fe13a437c348aba37
4
+ data.tar.gz: e8fcd68e07c0d0012985c5046705d53137b3f7c6f1717f0ac2546f161cfadb0a
5
5
  SHA512:
6
- metadata.gz: 6fa544e18af4ad42457ec8615e7a6e652fb15e1ae00cfa14924dcb86c53aab1ba90b21a8e1b7b29f193b1e0039d030cba1f45631b8dac23b287617105b88478b
7
- data.tar.gz: 3625e897b9681e208cd921e8118275bd012996c17fb62fc235eac8624e6742ce18cff8d8177a1d379bfb5fa71ebec3db02a70d2bb683aeb067ccac7f89345945
6
+ metadata.gz: e777794208415ac5797c445db30fb7e0b6e2fe5f53c1dc22323ba2411a7648d6ee971b94e594806d9738018e41888d04c5ae036457883f51e4eaa76de7875920
7
+ data.tar.gz: 8b5b812b286509fa1962b1f9c5098955de0d8a19d3f233b2205c9c85c8bb1c4869566915db7ca15dd2020e713f515b80499532b87a45f1e7ba7b80419c35c3e5
@@ -0,0 +1,173 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+
5
+ module Coinbase
6
+ # A representation of a blockchain Address that do not belong to a Coinbase::Wallet.
7
+ # External addresses can be used to fetch balances, request funds from the faucet, etc...,
8
+ # but cannot be used to sign transactions.
9
+ class ExternalAddress < Address
10
+ # Builds a stake operation for the supplied asset. The stake operation
11
+ # may take a few minutes to complete in the case when infrastructure is spun up.
12
+ # @param amount [Integer,String,BigDecimal] The amount of the asset to stake
13
+ # @param asset_id [Symbol] The asset to stake
14
+ # @param mode [Symbol] The staking mode. Defaults to :default.
15
+ # @param options [Hash] Additional options for the stake operation
16
+ # @return [Coinbase::StakingOperation] The stake operation
17
+ def build_stake_operation(amount, asset_id, mode: :default, options: {})
18
+ validate_can_stake!(amount, asset_id, mode, options)
19
+
20
+ build_staking_operation(amount, asset_id, 'stake', mode: mode, options: options)
21
+ end
22
+
23
+ # Builds an unstake operation for the supplied asset.
24
+ # @param amount [Integer,String,BigDecimal] The amount of the asset to unstake
25
+ # @param asset_id [Symbol] The asset to unstake
26
+ # @param mode [Symbol] The staking mode. Defaults to :default.
27
+ # @param options [Hash] Additional options for the unstake operation
28
+ # @return [Coinbase::StakingOperation] The unstake operation
29
+ def build_unstake_operation(amount, asset_id, mode: :default, options: {})
30
+ validate_can_unstake!(amount, asset_id, mode, options)
31
+
32
+ build_staking_operation(amount, asset_id, 'unstake', mode: mode, options: options)
33
+ end
34
+
35
+ # Builds an claim_stake operation for the supplied asset.
36
+ # @param amount [Integer,String,BigDecimal] The amount of the asset to claim
37
+ # @param asset_id [Symbol] The asset to claim
38
+ # @param mode [Symbol] The staking mode. Defaults to :default.
39
+ # @param options [Hash] Additional options for the claim_stake operation
40
+ # @return [Coinbase::StakingOperation] The claim_stake operation
41
+ def build_claim_stake_operation(amount, asset_id, mode: :default, options: {})
42
+ validate_can_claim_stake!(amount, asset_id, mode, options)
43
+
44
+ build_staking_operation(amount, asset_id, 'claim_stake', mode: mode, options: options)
45
+ end
46
+
47
+ # Retreives the balances used for staking for the supplied asset.
48
+ # @param asset_id [Symbol] The asset to retrieve staking balances for
49
+ # @param mode [Symbol] The staking mode. Defaults to :default.
50
+ # @param options [Hash] Additional options for the staking operation
51
+ # @return [Hash] The staking balances
52
+ # @return [BigDecimal] :stakeable_balance The amount of the asset that can be staked
53
+ # @return [BigDecimal] :unstakeable_balance The amount of the asset that is currently staked and cannot be unstaked
54
+ # @return [BigDecimal] :claimable_balance The amount of the asset that can be claimed
55
+ def staking_balances(asset_id, mode: :default, options: {})
56
+ context_model = Coinbase.call_api do
57
+ stake_api.get_staking_context(
58
+ {
59
+ asset_id: asset_id,
60
+ network_id: Coinbase.normalize_network(network_id),
61
+ address_id: id,
62
+ options: {
63
+ mode: mode
64
+ }.merge(options)
65
+ }
66
+ )
67
+ end.context
68
+
69
+ {
70
+ stakeable_balance: Coinbase::Balance.from_model_and_asset_id(
71
+ context_model.stakeable_balance,
72
+ asset_id
73
+ ).amount,
74
+ unstakeable_balance: Coinbase::Balance.from_model_and_asset_id(
75
+ context_model.unstakeable_balance,
76
+ asset_id
77
+ ).amount,
78
+ claimable_balance: Coinbase::Balance.from_model_and_asset_id(
79
+ context_model.claimable_balance,
80
+ asset_id
81
+ ).amount
82
+ }
83
+ end
84
+
85
+ # Retreives the stakeable balance for the supplied asset.
86
+ # @param asset_id [Symbol] The asset to retrieve the stakeable balance for
87
+ # @param mode [Symbol] The staking mode. Defaults to :default.
88
+ # @param options [Hash] Additional options for the staking operation
89
+ # @return [BigDecimal] The stakeable balance
90
+ def stakeable_balance(asset_id, mode: :default, options: {})
91
+ staking_balances(asset_id, mode: mode, options: options)[:stakeable_balance]
92
+ end
93
+
94
+ # Retreives the unstakeable balance for the supplied asset.
95
+ # @param asset_id [Symbol] The asset to retrieve the unstakeable balance for
96
+ # @param mode [Symbol] The staking mode. Defaults to :default.
97
+ # @param options [Hash] Additional options for the staking operation
98
+ # @return [BigDecimal] The unstakeable balance
99
+ def unstakeable_balance(asset_id, mode: :default, options: {})
100
+ staking_balances(asset_id, mode: mode, options: options)[:unstakeable_balance]
101
+ end
102
+
103
+ # Retreives the claimable balance for the supplied asset.
104
+ # @param asset_id [Symbol] The asset to retrieve the claimable balance for
105
+ # @param mode [Symbol] The staking mode. Defaults to :default.
106
+ # @param options [Hash] Additional options for the staking operation
107
+ # @return [BigDecimal] The claimable balance
108
+ def claimable_balance(asset_id, mode: :default, options: {})
109
+ staking_balances(asset_id, mode: mode, options: options)[:claimable_balance]
110
+ end
111
+
112
+ # Lists the staking rewards for the address.
113
+ # @param asset_id [Symbol] The asset to retrieve staking rewards for
114
+ # @param start_time [Time] The start time for the rewards. Defaults to 1 month ago.
115
+ # @param end_time [Time] The end time for the rewards. Defaults to the current time.
116
+ # @param format [Symbol] The format to return the rewards in. Defaults to :usd.
117
+ # @return [Enumerable<Coinbase::StakingReward>] The staking rewards
118
+ def staking_rewards(asset_id, start_time: DateTime.now.prev_month(1), end_time: DateTime.now, format: :usd)
119
+ StakingReward.list(
120
+ network_id,
121
+ asset_id,
122
+ [id],
123
+ start_time: start_time,
124
+ end_time: end_time,
125
+ format: format
126
+ )
127
+ end
128
+
129
+ private
130
+
131
+ def validate_can_stake!(amount, asset_id, mode, options)
132
+ stakeable_balance = stakeable_balance(asset_id, mode: mode, options: options)
133
+
134
+ raise InsufficientFundsError.new(amount, stakeable_balance) unless stakeable_balance >= amount
135
+ end
136
+
137
+ def validate_can_unstake!(amount, asset_id, mode, options)
138
+ unstakeable_balance = unstakeable_balance(asset_id, mode: mode, options: options)
139
+
140
+ raise InsufficientFundsError.new(amount, unstakeable_balance) unless unstakeable_balance >= amount
141
+ end
142
+
143
+ def validate_can_claim_stake!(amount, asset_id, mode, options)
144
+ claimable_balance = claimable_balance(asset_id, mode: mode, options: options)
145
+
146
+ raise InsufficientFundsError.new(amount, claimable_balance) unless claimable_balance >= amount
147
+ end
148
+
149
+ def build_staking_operation(amount, asset_id, action, mode: :default, options: {})
150
+ operation_model = Coinbase.call_api do
151
+ asset = Coinbase::Asset.fetch(network_id, asset_id)
152
+ stake_api.build_staking_operation(
153
+ {
154
+ asset_id: asset.primary_denomination.to_s,
155
+ address_id: id,
156
+ action: action,
157
+ network_id: Coinbase.normalize_network(network_id),
158
+ options: {
159
+ amount: asset.to_atomic_amount(amount).to_i.to_s,
160
+ mode: mode
161
+ }.merge(options)
162
+ }
163
+ )
164
+ end
165
+
166
+ StakingOperation.new(operation_model)
167
+ end
168
+
169
+ def stake_api
170
+ @stake_api ||= Coinbase::Client::StakeApi.new(Coinbase.configuration.api_client)
171
+ end
172
+ end
173
+ end
@@ -0,0 +1,219 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+ require 'eth'
5
+
6
+ module Coinbase
7
+ # A representation of a blockchain Address that belongs to a Coinbase::Wallet.
8
+ # Addresses are used to send and receive Assets, and should be created using
9
+ # Wallet#create_address. Addresses require an Eth::Key to sign transaction data.
10
+ class WalletAddress < Address
11
+ PAGE_LIMIT = 100
12
+
13
+ # Returns a new Address object. Do not use this method directly. Instead, use Wallet#create_address, or use
14
+ # the Wallet's default_address.
15
+ # @param model [Coinbase::Client::Address] The underlying Address object
16
+ # @param key [Eth::Key] The key backing the Address. Can be nil.
17
+ def initialize(model, key)
18
+ @model = model
19
+ @key = key
20
+
21
+ super(model.network_id, model.address_id)
22
+ end
23
+
24
+ # Returns the Wallet ID of the Address.
25
+ # @return [String] The Wallet ID
26
+ def wallet_id
27
+ @model.wallet_id
28
+ end
29
+
30
+ # Sets the private key backing the Address. This key is used to sign transactions.
31
+ # @param key [Eth::Key] The key backing the Address
32
+ def key=(key)
33
+ raise 'Private key is already set' unless @key.nil?
34
+
35
+ @key = key
36
+ end
37
+
38
+ # Transfers the given amount of the given Asset to the specified address or wallet.
39
+ # Only same-network Transfers are supported.
40
+ # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send.
41
+ # @param asset_id [Symbol] The ID of the Asset to send. For Ether, :eth, :gwei, and :wei are supported.
42
+ # @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
43
+ # default address. If a String, interprets it as the address ID.
44
+ # @return [Coinbase::Transfer] The Transfer object.
45
+ def transfer(amount, asset_id, destination)
46
+ asset = Asset.fetch(network_id, asset_id)
47
+
48
+ destination_address, destination_network = destination_address_and_network(destination)
49
+
50
+ validate_can_transfer!(amount, asset, destination_network)
51
+
52
+ transfer = create_transfer(amount, asset, destination_address)
53
+
54
+ # If a server signer is managing keys, it will sign and broadcast the underlying transfer transaction out of band.
55
+ return transfer if Coinbase.use_server_signer?
56
+
57
+ broadcast_transfer(transfer, transfer.transaction.sign(@key))
58
+ end
59
+
60
+ # Trades the given amount of the given Asset for another Asset.
61
+ # Only same-network Trades are supported.
62
+ # @param amount [Integer, Float, BigDecimal] The amount of the Asset to send.
63
+ # @param from_asset_id [Symbol] The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.
64
+ # @param to_asset_id [Symbol] The ID of the Asset to trade to. For Ether, :eth, :gwei, and :wei are supported.
65
+ # @return [Coinbase::Trade] The Trade object.
66
+ def trade(amount, from_asset_id, to_asset_id)
67
+ from_asset = Asset.fetch(network_id, from_asset_id)
68
+ to_asset = Asset.fetch(network_id, to_asset_id)
69
+
70
+ validate_can_trade!(amount, from_asset)
71
+
72
+ trade = create_trade(amount, from_asset, to_asset)
73
+
74
+ # NOTE: Trading does not yet support server signers at this point.
75
+
76
+ payloads = { signed_payload: trade.transaction.sign(@key) }
77
+
78
+ payloads[:approve_tx_signed_payload] = trade.approve_transaction.sign(@key) unless trade.approve_transaction.nil?
79
+
80
+ broadcast_trade(trade, **payloads)
81
+ end
82
+
83
+ # Returns whether the Address has a private key backing it to sign transactions.
84
+ # @return [Boolean] Whether the Address has a private key backing it to sign transactions.
85
+ def can_sign?
86
+ !@key.nil?
87
+ end
88
+
89
+ # Exports the Address's private key to a hex string.
90
+ # @return [String] The Address's private key as a hex String
91
+ def export
92
+ raise 'Cannot export key without private key loaded' if @key.nil?
93
+
94
+ @key.private_hex
95
+ end
96
+
97
+ # Enumerates the transfers associated with the address.
98
+ # The result is an enumerator that lazily fetches from the server, and can be iterated over,
99
+ # converted to an array, etc...
100
+ # @return [Enumerable<Coinbase::Transfer>] Enumerator that returns the address's transfers
101
+ def transfers
102
+ Coinbase::Pagination.enumerate(lambda(&method(:fetch_transfers_page))) do |transfer|
103
+ Coinbase::Transfer.new(transfer)
104
+ end
105
+ end
106
+
107
+ # Enumerates the trades associated with the address.
108
+ # The result is an enumerator that lazily fetches from the server, and can be iterated over,
109
+ # converted to an array, etc...
110
+ # @return [Enumerable<Coinbase::Trade>] Enumerator that returns the address's trades
111
+ def trades
112
+ Coinbase::Pagination.enumerate(lambda(&method(:fetch_trades_page))) do |trade|
113
+ Coinbase::Trade.new(trade)
114
+ end
115
+ end
116
+
117
+ # Returns a String representation of the WalletAddress.
118
+ # @return [String] a String representation of the WalletAddress
119
+ def to_s
120
+ "Coinbase::Address{id: '#{id}', network_id: '#{network_id}', wallet_id: '#{wallet_id}'}"
121
+ end
122
+
123
+ private
124
+
125
+ def fetch_transfers_page(page)
126
+ transfers_api.list_transfers(wallet_id, id, { limit: PAGE_LIMIT, page: page })
127
+ end
128
+
129
+ def fetch_trades_page(page)
130
+ trades_api.list_trades(wallet_id, id, { limit: PAGE_LIMIT, page: page })
131
+ end
132
+
133
+ def transfers_api
134
+ @transfers_api ||= Coinbase::Client::TransfersApi.new(Coinbase.configuration.api_client)
135
+ end
136
+
137
+ def trades_api
138
+ @trades_api ||= Coinbase::Client::TradesApi.new(Coinbase.configuration.api_client)
139
+ end
140
+
141
+ def destination_address_and_network(destination)
142
+ return [destination.default_address.id, destination.network_id] if destination.is_a?(Wallet)
143
+ return [destination.id, destination.network_id] if destination.is_a?(Address)
144
+
145
+ [destination, network_id]
146
+ end
147
+
148
+ def validate_can_transfer!(amount, asset, destination_network_id)
149
+ raise 'Cannot transfer from address without private key loaded' unless can_sign? || Coinbase.use_server_signer?
150
+
151
+ raise ArgumentError, 'Transfer must be on the same Network' unless destination_network_id == network_id
152
+
153
+ current_balance = balance(asset.asset_id)
154
+
155
+ return unless current_balance < amount
156
+
157
+ raise ArgumentError, "Insufficient funds: #{amount} requested, but only #{current_balance} available"
158
+ end
159
+
160
+ def create_transfer(amount, asset, destination)
161
+ create_transfer_request = {
162
+ amount: asset.to_atomic_amount(amount).to_i.to_s,
163
+ network_id: Coinbase.normalize_network(network_id),
164
+ asset_id: asset.primary_denomination.to_s,
165
+ destination: destination
166
+ }
167
+
168
+ transfer_model = Coinbase.call_api do
169
+ transfers_api.create_transfer(wallet_id, id, create_transfer_request)
170
+ end
171
+
172
+ Coinbase::Transfer.new(transfer_model)
173
+ end
174
+
175
+ def broadcast_transfer(transfer, signed_payload)
176
+ transfer_model = Coinbase.call_api do
177
+ transfers_api.broadcast_transfer(wallet_id, id, transfer.id, { signed_payload: signed_payload })
178
+ end
179
+
180
+ Coinbase::Transfer.new(transfer_model)
181
+ end
182
+
183
+ def validate_can_trade!(amount, from_asset)
184
+ raise 'Cannot trade from address without private key loaded' unless can_sign?
185
+
186
+ current_balance = balance(from_asset.asset_id)
187
+
188
+ return unless current_balance < amount
189
+
190
+ raise ArgumentError, "Insufficient funds: #{amount} requested, but only #{current_balance} available"
191
+ end
192
+
193
+ def create_trade(amount, from_asset, to_asset)
194
+ create_trade_request = {
195
+ amount: from_asset.to_atomic_amount(amount).to_i.to_s,
196
+ from_asset_id: from_asset.primary_denomination.to_s,
197
+ to_asset_id: to_asset.primary_denomination.to_s
198
+ }
199
+
200
+ trade_model = Coinbase.call_api do
201
+ trades_api.create_trade(wallet_id, id, create_trade_request)
202
+ end
203
+
204
+ Coinbase::Trade.new(trade_model)
205
+ end
206
+
207
+ def broadcast_trade(trade, signed_payload:, approve_tx_signed_payload: nil)
208
+ req = { signed_payload: signed_payload }
209
+
210
+ req[:approve_transaction_signed_payload] = approve_tx_signed_payload unless approve_tx_signed_payload.nil?
211
+
212
+ trade_model = Coinbase.call_api do
213
+ trades_api.broadcast_trade(wallet_id, id, trade.id, req)
214
+ end
215
+
216
+ Coinbase::Trade.new(trade_model)
217
+ end
218
+ end
219
+ end