coinbase-sdk 0.0.4 → 0.0.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/coinbase/address.rb +79 -42
- data/lib/coinbase/authenticator.rb +1 -1
- data/lib/coinbase/balance.rb +3 -5
- data/lib/coinbase/balance_map.rb +4 -4
- data/lib/coinbase/client/api/server_signers_api.rb +419 -0
- data/lib/coinbase/client/api/trades_api.rb +342 -0
- data/lib/coinbase/client/models/broadcast_trade_request.rb +222 -0
- data/lib/coinbase/client/models/create_address_request.rb +0 -14
- data/lib/coinbase/client/models/create_server_signer_request.rb +239 -0
- data/lib/coinbase/client/models/create_trade_request.rb +256 -0
- data/lib/coinbase/client/models/create_wallet_request.rb +1 -1
- data/lib/coinbase/client/models/create_wallet_request_wallet.rb +233 -0
- data/lib/coinbase/client/models/seed_creation_event.rb +240 -0
- data/lib/coinbase/client/models/seed_creation_event_result.rb +274 -0
- data/lib/coinbase/client/models/server_signer.rb +235 -0
- data/lib/coinbase/client/models/server_signer_event.rb +239 -0
- data/lib/coinbase/client/models/server_signer_event_event.rb +105 -0
- data/lib/coinbase/client/models/server_signer_event_list.rb +275 -0
- data/lib/coinbase/client/models/signature_creation_event.rb +363 -0
- data/lib/coinbase/client/models/signature_creation_event_result.rb +329 -0
- data/lib/coinbase/client/models/trade.rb +356 -0
- data/lib/coinbase/client/models/trade_list.rb +275 -0
- data/lib/coinbase/client/models/transaction.rb +294 -0
- data/lib/coinbase/client/models/transaction_type.rb +39 -0
- data/lib/coinbase/client/models/wallet.rb +55 -4
- data/lib/coinbase/client.rb +18 -0
- data/lib/coinbase/middleware.rb +4 -1
- data/lib/coinbase/transfer.rb +21 -21
- data/lib/coinbase/user.rb +43 -104
- data/lib/coinbase/wallet.rb +312 -58
- data/lib/coinbase.rb +16 -4
- metadata +48 -2
data/lib/coinbase/user.rb
CHANGED
@@ -20,21 +20,14 @@ module Coinbase
|
|
20
20
|
end
|
21
21
|
|
22
22
|
# Creates a new Wallet belonging to the User.
|
23
|
+
# @param network_id [String] (Optional) the ID of the blockchain network. Defaults to 'base-sepolia'.
|
23
24
|
# @return [Coinbase::Wallet] the new Wallet
|
24
|
-
def create_wallet
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
network_id: 'base-sepolia'
|
29
|
-
}
|
30
|
-
}
|
31
|
-
opts = { create_wallet_request: create_wallet_request }
|
32
|
-
|
33
|
-
model = Coinbase.call_api do
|
34
|
-
wallets_api.create_wallet(opts)
|
35
|
-
end
|
25
|
+
def create_wallet(create_wallet_options = {})
|
26
|
+
# For ruby 2.7 compatibility we cannot pass in keyword args when the create wallet
|
27
|
+
# options is empty
|
28
|
+
return Wallet.create if create_wallet_options.empty?
|
36
29
|
|
37
|
-
Wallet.
|
30
|
+
Wallet.create(**create_wallet_options)
|
38
31
|
end
|
39
32
|
|
40
33
|
# Imports a Wallet belonging to the User.
|
@@ -44,90 +37,53 @@ module Coinbase
|
|
44
37
|
Wallet.import(data)
|
45
38
|
end
|
46
39
|
|
47
|
-
# Lists the
|
48
|
-
# @
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
40
|
+
# Lists the Wallets belonging to the User.
|
41
|
+
# @param page_size [Integer] (Optional) the number of Wallets to return per page. Defaults to 10
|
42
|
+
# @param next_page_token [String] (Optional) the token for the next page of Wallets
|
43
|
+
# @return [Array<Coinbase::Wallet, String>] the Wallets belonging to the User and the pagination token, if
|
44
|
+
# any.
|
45
|
+
def wallets(page_size: 10, next_page_token: nil)
|
46
|
+
opts = {
|
47
|
+
limit: page_size
|
48
|
+
}
|
53
49
|
|
54
|
-
|
55
|
-
end
|
50
|
+
opts[:page] = next_page_token unless next_page_token.nil?
|
56
51
|
|
57
|
-
|
58
|
-
|
59
|
-
# only be used for development purposes. If you call save_wallet_locally! twice with wallets containing the same
|
60
|
-
# wallet_id, the backup will be overwritten during the second attempt.
|
61
|
-
# The default backup_file is `seeds.json` in the root folder. It can be configured by changing
|
62
|
-
# Coinbase.configuration.backup_file_path.
|
63
|
-
#
|
64
|
-
# @param wallet [Coinbase::Wallet] The wallet model to save.
|
65
|
-
# @param encrypt [bool] (Optional) Boolean representing whether the backup persisted to local file system should be
|
66
|
-
# encrypted or not. Data is unencrypted by default.
|
67
|
-
# @return [Coinbase::Wallet] the saved wallet.
|
68
|
-
def save_wallet_locally!(wallet, encrypt: false)
|
69
|
-
existing_seeds_in_store = existing_seeds
|
70
|
-
data = wallet.export
|
71
|
-
seed_to_store = data.seed
|
72
|
-
auth_tag = ''
|
73
|
-
iv = ''
|
74
|
-
if encrypt
|
75
|
-
shared_secret = store_encryption_key
|
76
|
-
cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
|
77
|
-
cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
|
78
|
-
iv = cipher.random_iv
|
79
|
-
cipher.iv = iv
|
80
|
-
cipher.auth_data = ''
|
81
|
-
encrypted_data = cipher.update(data.seed) + cipher.final
|
82
|
-
auth_tag = cipher.auth_tag.unpack1('H*')
|
83
|
-
iv = iv.unpack1('H*')
|
84
|
-
seed_to_store = encrypted_data.unpack1('H*')
|
52
|
+
wallet_list = Coinbase.call_api do
|
53
|
+
wallets_api.list_wallets(opts)
|
85
54
|
end
|
86
55
|
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
56
|
+
# A map from wallet_id to address models.
|
57
|
+
address_model_map = {}
|
58
|
+
|
59
|
+
wallet_list.data.each do |wallet_model|
|
60
|
+
addresses_list = Coinbase.call_api do
|
61
|
+
addresses_api.list_addresses(wallet_model.id, { limit: Coinbase::Wallet::MAX_ADDRESSES })
|
62
|
+
end
|
63
|
+
|
64
|
+
address_model_map[wallet_model.id] = addresses_list.data
|
65
|
+
end
|
93
66
|
|
94
|
-
|
95
|
-
|
67
|
+
wallets = wallet_list.data.map do |wallet_model|
|
68
|
+
Wallet.new(wallet_model, seed: '', address_models: address_model_map[wallet_model.id])
|
96
69
|
end
|
97
|
-
|
70
|
+
|
71
|
+
[wallets, wallet_list.next_page]
|
98
72
|
end
|
99
73
|
|
100
|
-
#
|
101
|
-
# @
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
existing_seeds_in_store.each do |wallet_id, seed_data|
|
108
|
-
seed = seed_data['seed']
|
109
|
-
raise ArgumentError, 'Malformed backup data' if seed.nil? || seed == ''
|
110
|
-
|
111
|
-
if seed_data['encrypted']
|
112
|
-
shared_secret = store_encryption_key
|
113
|
-
raise ArgumentError, 'Malformed encrypted seed data' if seed_data['iv'] == '' ||
|
114
|
-
seed_data['auth_tag'] == ''
|
115
|
-
|
116
|
-
cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
|
117
|
-
cipher.key = OpenSSL::Digest.digest('SHA256', shared_secret)
|
118
|
-
iv = [seed_data['iv']].pack('H*')
|
119
|
-
cipher.iv = iv
|
120
|
-
auth_tag = [seed_data['auth_tag']].pack('H*')
|
121
|
-
cipher.auth_tag = auth_tag
|
122
|
-
cipher.auth_data = ''
|
123
|
-
hex_decoded_data = [seed_data['seed']].pack('H*')
|
124
|
-
seed = cipher.update(hex_decoded_data) + cipher.final
|
125
|
-
end
|
74
|
+
# Returns the Wallet with the given ID.
|
75
|
+
# @param wallet_id [String] the ID of the Wallet
|
76
|
+
# @return [Coinbase::Wallet] the unhydrated Wallet
|
77
|
+
def wallet(wallet_id)
|
78
|
+
wallet_model = Coinbase.call_api do
|
79
|
+
wallets_api.get_wallet(wallet_id)
|
80
|
+
end
|
126
81
|
|
127
|
-
|
128
|
-
|
82
|
+
addresses_list = Coinbase.call_api do
|
83
|
+
addresses_api.list_addresses(wallet_model.id, { limit: Coinbase::Wallet::MAX_ADDRESSES })
|
129
84
|
end
|
130
|
-
|
85
|
+
|
86
|
+
Wallet.new(wallet_model, seed: '', address_models: addresses_list.data)
|
131
87
|
end
|
132
88
|
|
133
89
|
# Returns a string representation of the User.
|
@@ -151,22 +107,5 @@ module Coinbase
|
|
151
107
|
def wallets_api
|
152
108
|
@wallets_api ||= Coinbase::Client::WalletsApi.new(Coinbase.configuration.api_client)
|
153
109
|
end
|
154
|
-
|
155
|
-
def existing_seeds
|
156
|
-
existing_seed_data = '{}'
|
157
|
-
file_path = Coinbase.configuration.backup_file_path
|
158
|
-
existing_seed_data = File.read(file_path) if File.exist?(file_path)
|
159
|
-
output = JSON.parse(existing_seed_data)
|
160
|
-
|
161
|
-
raise ArgumentError, 'Malformed backup data' unless output.is_a?(Hash)
|
162
|
-
|
163
|
-
output
|
164
|
-
end
|
165
|
-
|
166
|
-
def store_encryption_key
|
167
|
-
pk = OpenSSL::PKey.read(Coinbase.configuration.api_key_private_key)
|
168
|
-
public_key = pk.public_key # use own public key to generate the shared secret.
|
169
|
-
pk.dh_compute_key(public_key)
|
170
|
-
end
|
171
110
|
end
|
172
111
|
end
|
data/lib/coinbase/wallet.rb
CHANGED
@@ -12,7 +12,21 @@ module Coinbase
|
|
12
12
|
# list their balances, and transfer Assets to other Addresses. Wallets should be created through User#create_wallet or
|
13
13
|
# User#import_wallet.
|
14
14
|
class Wallet
|
15
|
-
attr_reader :addresses
|
15
|
+
attr_reader :addresses, :model
|
16
|
+
|
17
|
+
# The maximum number of addresses in a Wallet.
|
18
|
+
MAX_ADDRESSES = 20
|
19
|
+
|
20
|
+
# A representation of ServerSigner status in a Wallet.
|
21
|
+
module ServerSignerStatus
|
22
|
+
# The Wallet is awaiting seed creation by the ServerSigner. At this point,
|
23
|
+
# the Wallet cannot create addresses or sign transactions.
|
24
|
+
PENDING = 'pending_seed_creation'
|
25
|
+
|
26
|
+
# The Wallet has an associated seed created by the ServerSigner. It is ready
|
27
|
+
# to create addresses and sign transactions.
|
28
|
+
ACTIVE = 'active_seed'
|
29
|
+
end
|
16
30
|
|
17
31
|
class << self
|
18
32
|
# Imports a Wallet from previously exported wallet data.
|
@@ -25,16 +39,70 @@ module Coinbase
|
|
25
39
|
wallets_api.get_wallet(data.wallet_id)
|
26
40
|
end
|
27
41
|
|
28
|
-
|
29
|
-
|
30
|
-
addresses_api.list_addresses(model.id).total_count
|
42
|
+
address_list = Coinbase.call_api do
|
43
|
+
addresses_api.list_addresses(model.id, { limit: MAX_ADDRESSES })
|
31
44
|
end
|
32
45
|
|
33
|
-
new(model, seed: data.seed,
|
46
|
+
new(model, seed: data.seed, address_models: address_list.data)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Creates a new Wallet on the specified Network and generate a default address for it.
|
50
|
+
# @param network_id [String] (Optional) the ID of the blockchain network. Defaults to 'base-sepolia'.
|
51
|
+
# @param interval_seconds [Integer] The interval at which to poll the CDPService for the Wallet to
|
52
|
+
# have an active seed, if using a ServerSigner, in seconds
|
53
|
+
# @param timeout_seconds [Integer] The maximum amount of time to wait for the ServerSigner to
|
54
|
+
# create a seed for the Wallet, in seconds
|
55
|
+
# @return [Coinbase::Wallet] the new Wallet
|
56
|
+
def create(network_id: 'base-sepolia', interval_seconds: 0.2, timeout_seconds: 20)
|
57
|
+
model = Coinbase.call_api do
|
58
|
+
wallets_api.create_wallet(
|
59
|
+
create_wallet_request: {
|
60
|
+
wallet: {
|
61
|
+
network_id: network_id,
|
62
|
+
use_server_signer: Coinbase.use_server_signer?
|
63
|
+
}
|
64
|
+
}
|
65
|
+
)
|
66
|
+
end
|
67
|
+
|
68
|
+
wallet = new(model)
|
69
|
+
|
70
|
+
# When used with a ServerSigner, the Signer must first register
|
71
|
+
# with the Wallet before addresses can be created.
|
72
|
+
wait_for_signer(wallet.id, interval_seconds, timeout_seconds) if Coinbase.use_server_signer?
|
73
|
+
|
74
|
+
wallet.create_address
|
75
|
+
wallet
|
34
76
|
end
|
35
77
|
|
36
78
|
private
|
37
79
|
|
80
|
+
# Wait_for_signer waits until the ServerSigner has created a seed for the Wallet.
|
81
|
+
# Timeout::Error if the ServerSigner takes longer than the given timeout to create the seed.
|
82
|
+
# @param wallet_id [string] The ID of the Wallet that is awaiting seed creation.
|
83
|
+
# @param interval_seconds [Integer] The interval at which to poll the CDPService, in seconds
|
84
|
+
# @param timeout_seconds [Integer] The maximum amount of time to wait for the Signer to create a seed, in seconds
|
85
|
+
# @return [Wallet] The completed Wallet object that is ready to create addresses.
|
86
|
+
def wait_for_signer(wallet_id, interval_seconds, timeout_seconds)
|
87
|
+
start_time = Time.now
|
88
|
+
|
89
|
+
loop do
|
90
|
+
model = Coinbase.call_api do
|
91
|
+
wallets_api.get_wallet(wallet_id)
|
92
|
+
end
|
93
|
+
|
94
|
+
return self if model.server_signer_status == ServerSignerStatus::ACTIVE
|
95
|
+
|
96
|
+
if Time.now - start_time > timeout_seconds
|
97
|
+
raise Timeout::Error, 'Wallet creation timed out. Check status of your Server-Signer'
|
98
|
+
end
|
99
|
+
|
100
|
+
self.sleep interval_seconds
|
101
|
+
end
|
102
|
+
|
103
|
+
self
|
104
|
+
end
|
105
|
+
|
38
106
|
# TODO: Memoize these objects in a thread-safe way at the top-level.
|
39
107
|
def addresses_api
|
40
108
|
Coinbase::Client::AddressesApi.new(Coinbase.configuration.api_client)
|
@@ -49,31 +117,23 @@ module Coinbase
|
|
49
117
|
# User#import_wallet.
|
50
118
|
# @param model [Coinbase::Client::Wallet] The underlying Wallet object
|
51
119
|
# @param seed [String] (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal with no 0x prefix.
|
52
|
-
# If
|
53
|
-
#
|
120
|
+
# If nil, a new seed will be generated. If the empty string, no seed is generated, and the Wallet will be
|
121
|
+
# instantiated without a seed and its corresponding private keys.
|
122
|
+
# @param address_models [Array<Coinbase::Client::Address>] (Optional) The models of the addresses already registered
|
123
|
+
# with the Wallet. If not provided, the Wallet will derive the first default address.
|
54
124
|
# @param client [Jimson::Client] (Optional) The JSON RPC client to use for interacting with the Network
|
55
|
-
def initialize(model, seed: nil,
|
56
|
-
|
125
|
+
def initialize(model, seed: nil, address_models: [])
|
126
|
+
validate_seed_and_address_models(seed, address_models) unless Coinbase.use_server_signer?
|
57
127
|
|
58
128
|
@model = model
|
59
|
-
|
60
|
-
@master = seed.nil? ? MoneyTree::Master.new : MoneyTree::Master.new(seed_hex: seed)
|
61
|
-
|
62
|
-
# TODO: Make Network an argument to the constructor.
|
63
|
-
@network_id = :base_sepolia
|
64
129
|
@addresses = []
|
65
130
|
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
if address_count.positive?
|
71
|
-
address_count.times { derive_address }
|
72
|
-
else
|
73
|
-
create_address
|
74
|
-
# Update the model to reflect the new default address.
|
75
|
-
update_model
|
131
|
+
unless Coinbase.use_server_signer?
|
132
|
+
@master = master_node(seed)
|
133
|
+
@private_key_index = 0
|
76
134
|
end
|
135
|
+
|
136
|
+
derive_addresses(address_models)
|
77
137
|
end
|
78
138
|
|
79
139
|
# Returns the Wallet ID.
|
@@ -88,30 +148,60 @@ module Coinbase
|
|
88
148
|
Coinbase.to_sym(@model.network_id)
|
89
149
|
end
|
90
150
|
|
151
|
+
# Returns the ServerSigner Status of the Wallet.
|
152
|
+
# @return [Symbol] The ServerSigner Status
|
153
|
+
def server_signer_status
|
154
|
+
Coinbase.to_sym(@model.server_signer_status)
|
155
|
+
end
|
156
|
+
|
157
|
+
# Sets the seed of the Wallet. This seed is used to derive keys and sign transactions.
|
158
|
+
# @param seed [String] The seed to set. Expects a 32-byte hexadecimal with no 0x prefix.
|
159
|
+
def seed=(seed)
|
160
|
+
raise ArgumentError, 'Seed must be 32 bytes' if seed.length != 64
|
161
|
+
raise 'Seed is already set' unless @master.nil?
|
162
|
+
raise 'Cannot set seed for Wallet with non-zero private key index' if @private_key_index.positive?
|
163
|
+
|
164
|
+
@master = MoneyTree::Master.new(seed_hex: seed)
|
165
|
+
|
166
|
+
@addresses.each do
|
167
|
+
key = derive_key
|
168
|
+
a = address(key.address.to_s)
|
169
|
+
raise "Seed does not match wallet; cannot find address #{key.address}" if a.nil?
|
170
|
+
|
171
|
+
a.key = key
|
172
|
+
end
|
173
|
+
end
|
174
|
+
|
91
175
|
# Creates a new Address in the Wallet.
|
92
176
|
# @return [Address] The new Address
|
93
177
|
def create_address
|
94
|
-
|
95
|
-
attestation = create_attestation(key)
|
96
|
-
public_key = key.public_key.compressed.unpack1('H*')
|
178
|
+
opts = { create_address_request: {} }
|
97
179
|
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
180
|
+
unless Coinbase.use_server_signer?
|
181
|
+
key = derive_key
|
182
|
+
|
183
|
+
opts = {
|
184
|
+
create_address_request: {
|
185
|
+
public_key: key.public_key.compressed.unpack1('H*'),
|
186
|
+
attestation: create_attestation(key)
|
187
|
+
}
|
102
188
|
}
|
103
|
-
|
189
|
+
end
|
190
|
+
|
104
191
|
address_model = Coinbase.call_api do
|
105
192
|
addresses_api.create_address(id, opts)
|
106
193
|
end
|
107
194
|
|
195
|
+
# Auto-reload wallet to set default address on first address creation.
|
196
|
+
reload if addresses.empty?
|
197
|
+
|
108
198
|
cache_address(address_model, key)
|
109
199
|
end
|
110
200
|
|
111
201
|
# Returns the default address of the Wallet.
|
112
202
|
# @return [Address] The default address
|
113
203
|
def default_address
|
114
|
-
address(@model.default_address
|
204
|
+
address(@model.default_address&.address_id)
|
115
205
|
end
|
116
206
|
|
117
207
|
# Returns the Address with the given ID.
|
@@ -144,30 +234,25 @@ module Coinbase
|
|
144
234
|
Coinbase::Balance.from_model_and_asset_id(response, asset_id).amount
|
145
235
|
end
|
146
236
|
|
147
|
-
# Transfers the given amount of the given Asset to the
|
148
|
-
# Currently only the default_address is used to source the Transfer.
|
237
|
+
# Transfers the given amount of the given Asset to the specified address or wallet.
|
238
|
+
# Only same-network Transfers are supported. Currently only the default_address is used to source the Transfer.
|
149
239
|
# @param amount [Integer, Float, BigDecimal] The amount of the Asset to send
|
150
240
|
# @param asset_id [Symbol] The ID of the Asset to send
|
151
241
|
# @param destination [Wallet | Address | String] The destination of the transfer. If a Wallet, sends to the Wallet's
|
152
242
|
# default address. If a String, interprets it as the address ID.
|
153
243
|
# @return [Transfer] The hash of the Transfer transaction.
|
154
244
|
def transfer(amount, asset_id, destination)
|
155
|
-
if destination.is_a?(Wallet)
|
156
|
-
raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
|
157
|
-
|
158
|
-
destination = destination.default_address.id
|
159
|
-
elsif destination.is_a?(Address)
|
160
|
-
raise ArgumentError, 'Transfer must be on the same Network' if destination.network_id != @network_id
|
161
|
-
|
162
|
-
destination = destination.id
|
163
|
-
end
|
164
|
-
|
165
245
|
default_address.transfer(amount, asset_id, destination)
|
166
246
|
end
|
167
247
|
|
168
248
|
# Exports the Wallet's data to a Data object.
|
169
249
|
# @return [Data] The Wallet data
|
170
250
|
def export
|
251
|
+
# TODO: Improve this check by relying on the backend data to decide whether a wallet is server-signer backed.
|
252
|
+
raise 'Cannot export data for Server-Signer backed Wallet' if Coinbase.use_server_signer?
|
253
|
+
|
254
|
+
raise 'Cannot export Wallet without loaded seed' if @master.nil?
|
255
|
+
|
171
256
|
Data.new(wallet_id: id, seed: @master.seed_hex)
|
172
257
|
end
|
173
258
|
|
@@ -182,11 +267,98 @@ module Coinbase
|
|
182
267
|
end
|
183
268
|
end
|
184
269
|
|
270
|
+
# Returns whether the Wallet has a seed with which to derive keys and sign transactions.
|
271
|
+
# @return [Boolean] Whether the Wallet has a seed with which to derive keys and sign transactions.
|
272
|
+
def can_sign?
|
273
|
+
!@master.nil?
|
274
|
+
end
|
275
|
+
|
276
|
+
# Saves the seed of the Wallet to the given file. Wallets whose seeds are saved this way can be
|
277
|
+
# rehydrated using load_seed. A single file can be used for multiple Wallet seeds.
|
278
|
+
# This is an insecure method of storing Wallet seeds and should only be used for development purposes.
|
279
|
+
#
|
280
|
+
# @param file_path [String] The path of the file to save the seed to
|
281
|
+
# @param encrypt [bool] (Optional) Whether the seed information persisted to the local file system should be
|
282
|
+
# encrypted or not. Data is unencrypted by default.
|
283
|
+
# @return [String] A string indicating the success of the operation
|
284
|
+
def save_seed!(file_path, encrypt: false)
|
285
|
+
raise 'Wallet does not have seed loaded' if @master.nil?
|
286
|
+
|
287
|
+
existing_seeds_in_store = existing_seeds(file_path)
|
288
|
+
|
289
|
+
seed_to_store = @master.seed_hex
|
290
|
+
auth_tag = ''
|
291
|
+
iv = ''
|
292
|
+
if encrypt
|
293
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
|
294
|
+
cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
|
295
|
+
iv = cipher.random_iv
|
296
|
+
cipher.iv = iv
|
297
|
+
cipher.auth_data = ''
|
298
|
+
encrypted_data = cipher.update(@master.seed_hex) + cipher.final
|
299
|
+
auth_tag = cipher.auth_tag.unpack1('H*')
|
300
|
+
iv = iv.unpack1('H*')
|
301
|
+
seed_to_store = encrypted_data.unpack1('H*')
|
302
|
+
end
|
303
|
+
|
304
|
+
existing_seeds_in_store[id] = {
|
305
|
+
seed: seed_to_store,
|
306
|
+
encrypted: encrypt,
|
307
|
+
auth_tag: auth_tag,
|
308
|
+
iv: iv
|
309
|
+
}
|
310
|
+
|
311
|
+
File.open(file_path, 'w') do |file|
|
312
|
+
file.write(JSON.pretty_generate(existing_seeds_in_store))
|
313
|
+
end
|
314
|
+
|
315
|
+
"Successfully saved seed for wallet #{id} to #{file_path}."
|
316
|
+
end
|
317
|
+
|
318
|
+
# Loads the seed of the Wallet from the given file.
|
319
|
+
# @param file_path [String] The path of the file to load the seed from
|
320
|
+
# @return [String] A string indicating the success of the operation
|
321
|
+
def load_seed(file_path)
|
322
|
+
raise 'Wallet already has seed loaded' unless @master.nil?
|
323
|
+
|
324
|
+
existing_seeds_in_store = existing_seeds(file_path)
|
325
|
+
|
326
|
+
raise ArgumentError, "File #{file_path} does not contain seed data" if existing_seeds_in_store == {}
|
327
|
+
|
328
|
+
if existing_seeds_in_store[id].nil?
|
329
|
+
raise ArgumentError, "File #{file_path} does not contain seed data for wallet #{id}"
|
330
|
+
end
|
331
|
+
|
332
|
+
seed_data = existing_seeds_in_store[id]
|
333
|
+
local_seed = seed_data['seed']
|
334
|
+
|
335
|
+
raise ArgumentError, 'Seed data is malformed' if local_seed.nil? || local_seed == ''
|
336
|
+
|
337
|
+
if seed_data['encrypted']
|
338
|
+
raise ArgumentError, 'Encrypted seed data is malformed' if seed_data['iv'] == '' ||
|
339
|
+
seed_data['auth_tag'] == ''
|
340
|
+
|
341
|
+
cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
|
342
|
+
cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
|
343
|
+
iv = [seed_data['iv']].pack('H*')
|
344
|
+
cipher.iv = iv
|
345
|
+
auth_tag = [seed_data['auth_tag']].pack('H*')
|
346
|
+
cipher.auth_tag = auth_tag
|
347
|
+
cipher.auth_data = ''
|
348
|
+
hex_decoded_data = [seed_data['seed']].pack('H*')
|
349
|
+
local_seed = cipher.update(hex_decoded_data) + cipher.final
|
350
|
+
end
|
351
|
+
|
352
|
+
self.seed = local_seed
|
353
|
+
|
354
|
+
"Successfully loaded seed for wallet #{id} from #{file_path}."
|
355
|
+
end
|
356
|
+
|
185
357
|
# Returns a String representation of the Wallet.
|
186
358
|
# @return [String] a String representation of the Wallet
|
187
359
|
def to_s
|
188
360
|
"Coinbase::Wallet{wallet_id: '#{id}', network_id: '#{network_id}', " \
|
189
|
-
"default_address: '#{default_address
|
361
|
+
"default_address: '#{@model.default_address&.address_id}'}"
|
190
362
|
end
|
191
363
|
|
192
364
|
# Same as to_s.
|
@@ -223,14 +395,55 @@ module Coinbase
|
|
223
395
|
|
224
396
|
private
|
225
397
|
|
398
|
+
# Reloads the Wallet with the latest data.
|
399
|
+
def reload
|
400
|
+
@model = Coinbase.call_api do
|
401
|
+
wallets_api.get_wallet(id)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def master_node(seed)
|
406
|
+
return MoneyTree::Master.new if seed.nil?
|
407
|
+
return nil if seed.empty?
|
408
|
+
|
409
|
+
MoneyTree::Master.new(seed_hex: seed)
|
410
|
+
end
|
411
|
+
|
412
|
+
def address_path_prefix
|
413
|
+
# TODO: Add support for other networks.
|
414
|
+
@address_path_prefix ||= case network_id.to_s.split('_').first
|
415
|
+
when 'base'
|
416
|
+
"m/44'/60'/0'/0"
|
417
|
+
else
|
418
|
+
raise ArgumentError, "Unsupported network ID: #{network_id}"
|
419
|
+
end
|
420
|
+
end
|
421
|
+
|
422
|
+
# Derives the registered Addresses in the Wallet.
|
423
|
+
# @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
|
424
|
+
# Wallet
|
425
|
+
def derive_addresses(address_models)
|
426
|
+
return unless address_models.any?
|
427
|
+
|
428
|
+
# Create a map tracking which addresses are already registered with the Wallet.
|
429
|
+
address_map = build_address_map(address_models)
|
430
|
+
|
431
|
+
address_models.each do |address_model|
|
432
|
+
# Derive the addresses using the provided models.
|
433
|
+
derive_address(address_map, address_model)
|
434
|
+
end
|
435
|
+
end
|
436
|
+
|
226
437
|
# Derives an already registered Address in the Wallet.
|
438
|
+
# @param address_map [Hash<String, Boolean>] The map of registered Address IDs
|
439
|
+
# @param address_model [Coinbase::Client::Address] The Address model
|
227
440
|
# @return [Address] The new Address
|
228
|
-
def derive_address
|
229
|
-
key = derive_key
|
441
|
+
def derive_address(address_map, address_model)
|
442
|
+
key = @master.nil? ? nil : derive_key
|
230
443
|
|
231
|
-
|
232
|
-
|
233
|
-
|
444
|
+
unless key.nil?
|
445
|
+
address_from_key = key.address.to_s
|
446
|
+
raise 'Invalid address' if address_map[address_from_key].nil?
|
234
447
|
end
|
235
448
|
|
236
449
|
cache_address(address_model, key)
|
@@ -239,8 +452,11 @@ module Coinbase
|
|
239
452
|
# Derives a key for an already registered Address in the Wallet.
|
240
453
|
# @return [Eth::Key] The new key
|
241
454
|
def derive_key
|
242
|
-
|
455
|
+
raise 'Cannot derive key for Wallet without seed loaded' if @master.nil?
|
456
|
+
|
457
|
+
path = "#{address_path_prefix}/#{@private_key_index}"
|
243
458
|
private_key = @master.node_for_path(path).private_key.to_hex
|
459
|
+
@private_key_index += 1
|
244
460
|
Eth::Key.new(priv: private_key)
|
245
461
|
end
|
246
462
|
|
@@ -251,10 +467,22 @@ module Coinbase
|
|
251
467
|
def cache_address(address_model, key)
|
252
468
|
address = Address.new(address_model, key)
|
253
469
|
@addresses << address
|
254
|
-
@address_index += 1
|
255
470
|
address
|
256
471
|
end
|
257
472
|
|
473
|
+
# Builds a Hash of the registered Addresses.
|
474
|
+
# @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
|
475
|
+
# Wallet
|
476
|
+
# @return [Hash<String, Boolean>] The Hash of registered Addresses
|
477
|
+
def build_address_map(address_models)
|
478
|
+
address_map = {}
|
479
|
+
address_models.each do |address_model|
|
480
|
+
address_map[address_model.address_id] = true
|
481
|
+
end
|
482
|
+
|
483
|
+
address_map
|
484
|
+
end
|
485
|
+
|
258
486
|
# Creates an attestation for the Address currently being created.
|
259
487
|
# @param key [Eth::Key] The private key of the Address
|
260
488
|
# @return [String] The attestation
|
@@ -279,11 +507,37 @@ module Coinbase
|
|
279
507
|
new_signature_bytes.pack('C*').unpack1('H*')
|
280
508
|
end
|
281
509
|
|
282
|
-
#
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
510
|
+
# Validates the seed and address models passed to the constructor.
|
511
|
+
# @param seed [String] The seed to use for the Wallet
|
512
|
+
# @param address_models [Array<Coinbase::Client::Address>] The models of the addresses already registered with the
|
513
|
+
# Wallet
|
514
|
+
def validate_seed_and_address_models(seed, address_models)
|
515
|
+
raise ArgumentError, 'Seed must be 32 bytes' if !seed.nil? && !seed.empty? && seed.length != 64
|
516
|
+
|
517
|
+
raise ArgumentError, 'Seed must be present if address_models are provided' if seed.nil? && address_models.any?
|
518
|
+
|
519
|
+
return unless !seed.nil? && seed.empty? && address_models.empty?
|
520
|
+
|
521
|
+
raise ArgumentError, 'Seed must be empty if address_models are not provided'
|
522
|
+
end
|
523
|
+
|
524
|
+
# Loads the Hash of Wallet seeds from the given file.
|
525
|
+
# @param file_path [String] The path of the file to load the seed from
|
526
|
+
# @return [Hash<String, Hash>] The Hash of from Wallet IDs to seed data
|
527
|
+
def existing_seeds(file_path)
|
528
|
+
existing_seed_data = '{}'
|
529
|
+
existing_seed_data = File.read(file_path) if File.exist?(file_path)
|
530
|
+
existing_seeds = JSON.parse(existing_seed_data)
|
531
|
+
raise ArgumentError, "#{file_path} is malformed, must be a valid JSON object" unless existing_seeds.is_a?(Hash)
|
532
|
+
|
533
|
+
existing_seeds
|
534
|
+
end
|
535
|
+
|
536
|
+
# Returns the shared secret to use for encrypting the seed.
|
537
|
+
def encryption_key
|
538
|
+
pk = OpenSSL::PKey.read(Coinbase.configuration.api_key_private_key)
|
539
|
+
public_key = pk.public_key # use own public key to generate the shared secret.
|
540
|
+
pk.dh_compute_key(public_key)
|
287
541
|
end
|
288
542
|
|
289
543
|
def addresses_api
|