coinbase-sdk 0.0.4 → 0.0.6
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 +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
|