Class: Coinbase::Wallet

Inherits:
Object
  • Object
show all
Defined in:
lib/coinbase/wallet.rb

Overview

A representation of a Wallet. Wallets come with a single default Address, but can expand to have a set of Addresses, each of which can hold a balance of one or more Assets. Wallets can create new Addresses, list their addresses, list their balances, and transfer Assets to other Addresses. Wallets should be created through User#create_wallet or User#import_wallet.

Defined Under Namespace

Modules: ServerSignerStatus Classes: Data

Constant Summary collapse

MAX_ADDRESSES =

The maximum number of addresses in a Wallet.

20

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, seed: nil, address_models: []) ⇒ Wallet

Returns a new Wallet object. Do not use this method directly. Instead, use User#create_wallet or User#import_wallet.



125
126
127
128
129
130
131
132
133
134
135
136
137
# File 'lib/coinbase/wallet.rb', line 125

def initialize(model, seed: nil, address_models: [])
  validate_seed_and_address_models(seed, address_models) unless Coinbase.use_server_signer?

  @model = model
  @addresses = []

  unless Coinbase.use_server_signer?
    @master = master_node(seed)
    @private_key_index = 0
  end

  derive_addresses(address_models)
end

Instance Attribute Details

#addressesObject (readonly)

Returns the value of attribute addresses.



15
16
17
# File 'lib/coinbase/wallet.rb', line 15

def addresses
  @addresses
end

#modelObject (readonly)

Returns the value of attribute model.



15
16
17
# File 'lib/coinbase/wallet.rb', line 15

def model
  @model
end

Class Method Details

.create(network_id: 'base-sepolia', interval_seconds: 0.2, timeout_seconds: 20) ⇒ Coinbase::Wallet

Creates a new Wallet on the specified Network and generate a default address for it. have an active seed, if using a ServerSigner, in seconds create a seed for the Wallet, in seconds



56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/coinbase/wallet.rb', line 56

def create(network_id: 'base-sepolia', interval_seconds: 0.2, timeout_seconds: 20)
  model = Coinbase.call_api do
    wallets_api.create_wallet(
      create_wallet_request: {
        wallet: {
          network_id: network_id,
          use_server_signer: Coinbase.use_server_signer?
        }
      }
    )
  end

  wallet = new(model)

  # When used with a ServerSigner, the Signer must first register
  # with the Wallet before addresses can be created.
  wait_for_signer(wallet.id, interval_seconds, timeout_seconds) if Coinbase.use_server_signer?

  wallet.create_address
  wallet
end

.import(data) ⇒ Coinbase::Wallet

Imports a Wallet from previously exported wallet data.

Raises:

  • (ArgumentError)


35
36
37
38
39
40
41
42
43
44
45
46
47
# File 'lib/coinbase/wallet.rb', line 35

def import(data)
  raise ArgumentError, 'data must be a Coinbase::Wallet::Data object' unless data.is_a?(Data)

  model = Coinbase.call_api do
    wallets_api.get_wallet(data.wallet_id)
  end

  address_list = Coinbase.call_api do
    addresses_api.list_addresses(model.id, { limit: MAX_ADDRESSES })
  end

  new(model, seed: data.seed, address_models: address_list.data)
end

Instance Method Details

#address(address_id) ⇒ Address

Returns the Address with the given ID.



210
211
212
# File 'lib/coinbase/wallet.rb', line 210

def address(address_id)
  @addresses.find { |address| address.id == address_id }
end

#balance(asset_id) ⇒ BigDecimal

Returns the balance of the provided Asset. Balances are aggregated across all Addresses in the Wallet.



227
228
229
230
231
232
233
234
235
# File 'lib/coinbase/wallet.rb', line 227

def balance(asset_id)
  response = Coinbase.call_api do
    wallets_api.get_wallet_balance(id, Coinbase::Asset.primary_denomination(asset_id).to_s)
  end

  return BigDecimal('0') if response.nil?

  Coinbase::Balance.from_model_and_asset_id(response, asset_id).amount
end

#balancesBalanceMap

Returns the list of balances of this Wallet. Balances are aggregated across all Addresses in the Wallet.



216
217
218
219
220
221
222
# File 'lib/coinbase/wallet.rb', line 216

def balances
  response = Coinbase.call_api do
    wallets_api.list_wallet_balances(id)
  end

  Coinbase::BalanceMap.from_balances(response.data)
end

#can_sign?Boolean

Returns whether the Wallet has a seed with which to derive keys and sign transactions.



283
284
285
# File 'lib/coinbase/wallet.rb', line 283

def can_sign?
  !@master.nil?
end

#create_addressAddress

Creates a new Address in the Wallet.



177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
# File 'lib/coinbase/wallet.rb', line 177

def create_address
  opts = { create_address_request: {} }

  unless Coinbase.use_server_signer?
    key = derive_key

    opts = {
      create_address_request: {
        public_key: key.public_key.compressed.unpack1('H*'),
        attestation: create_attestation(key)
      }
    }
  end

  address_model = Coinbase.call_api do
    addresses_api.create_address(id, opts)
  end

  # Auto-reload wallet to set default address on first address creation.
  reload if addresses.empty?

  cache_address(address_model, key)
end

#default_addressAddress

Returns the default address of the Wallet.



203
204
205
# File 'lib/coinbase/wallet.rb', line 203

def default_address
  address(@model.default_address&.address_id)
end

#exportData

Exports the Wallet’s data to a Data object.



261
262
263
264
265
266
267
268
# File 'lib/coinbase/wallet.rb', line 261

def export
  # TODO: Improve this check by relying on the backend data to decide whether a wallet is server-signer backed.
  raise 'Cannot export data for Server-Signer backed Wallet' if Coinbase.use_server_signer?

  raise 'Cannot export Wallet without loaded seed' if @master.nil?

  Data.new(wallet_id: id, seed: @master.seed_hex)
end

#faucetCoinbase::FaucetTransaction

Requests funds from the faucet for the Wallet’s default address and returns the faucet transaction. This is only supported on testnet networks.

Raises:



275
276
277
278
279
# File 'lib/coinbase/wallet.rb', line 275

def faucet
  Coinbase.call_api do
    Coinbase::FaucetTransaction.new(addresses_api.request_faucet_funds(id, default_address.id))
  end
end

#idString

Returns the Wallet ID.



141
142
143
# File 'lib/coinbase/wallet.rb', line 141

def id
  @model.id
end

#inspectString

Same as to_s.



377
378
379
# File 'lib/coinbase/wallet.rb', line 377

def inspect
  to_s
end

#load_seed(file_path) ⇒ String

Loads the seed of the Wallet from the given file.

Raises:

  • (ArgumentError)


332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
# File 'lib/coinbase/wallet.rb', line 332

def load_seed(file_path)
  raise 'Wallet already has seed loaded' unless @master.nil?

  existing_seeds_in_store = existing_seeds(file_path)

  raise ArgumentError, "File #{file_path} does not contain seed data" if existing_seeds_in_store == {}

  if existing_seeds_in_store[id].nil?
    raise ArgumentError, "File #{file_path} does not contain seed data for wallet #{id}"
  end

  seed_data = existing_seeds_in_store[id]
  local_seed = seed_data['seed']

  raise ArgumentError, 'Seed data is malformed' if local_seed.nil? || local_seed == ''

  if seed_data['encrypted']
    raise ArgumentError, 'Encrypted seed data is malformed' if seed_data['iv'] == '' ||
                                                               seed_data['auth_tag'] == ''

    cipher = OpenSSL::Cipher.new('aes-256-gcm').decrypt
    cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
    iv = [seed_data['iv']].pack('H*')
    cipher.iv = iv
    auth_tag = [seed_data['auth_tag']].pack('H*')
    cipher.auth_tag = auth_tag
    cipher.auth_data = ''
    hex_decoded_data = [seed_data['seed']].pack('H*')
    local_seed = cipher.update(hex_decoded_data) + cipher.final
  end

  self.seed = local_seed

  "Successfully loaded seed for wallet #{id} from #{file_path}."
end

#network_idSymbol

Returns the Network ID of the Wallet.



147
148
149
# File 'lib/coinbase/wallet.rb', line 147

def network_id
  Coinbase.to_sym(@model.network_id)
end

#save_seed!(file_path, encrypt: false) ⇒ String

Saves the seed of the Wallet to the given file. Wallets whose seeds are saved this way can be rehydrated using load_seed. A single file can be used for multiple Wallet seeds. This is an insecure method of storing Wallet seeds and should only be used for development purposes.

encrypted or not. Data is unencrypted by default.



295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
# File 'lib/coinbase/wallet.rb', line 295

def save_seed!(file_path, encrypt: false)
  raise 'Wallet does not have seed loaded' if @master.nil?

  existing_seeds_in_store = existing_seeds(file_path)

  seed_to_store = @master.seed_hex
  auth_tag = ''
  iv = ''
  if encrypt
    cipher = OpenSSL::Cipher.new('aes-256-gcm').encrypt
    cipher.key = OpenSSL::Digest.digest('SHA256', encryption_key)
    iv = cipher.random_iv
    cipher.iv = iv
    cipher.auth_data = ''
    encrypted_data = cipher.update(@master.seed_hex) + cipher.final
    auth_tag = cipher.auth_tag.unpack1('H*')
    iv = iv.unpack1('H*')
    seed_to_store = encrypted_data.unpack1('H*')
  end

  existing_seeds_in_store[id] = {
    seed: seed_to_store,
    encrypted: encrypt,
    auth_tag: auth_tag,
    iv: iv
  }

  File.open(file_path, 'w') do |file|
    file.write(JSON.pretty_generate(existing_seeds_in_store))
  end

  "Successfully saved seed for wallet #{id} to #{file_path}."
end

#seed=(seed) ⇒ Object

Sets the seed of the Wallet. This seed is used to derive keys and sign transactions.

Raises:

  • (ArgumentError)


159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
# File 'lib/coinbase/wallet.rb', line 159

def seed=(seed)
  raise ArgumentError, 'Seed must be 32 bytes' if seed.length != 64
  raise 'Seed is already set' unless @master.nil?
  raise 'Cannot set seed for Wallet with non-zero private key index' if @private_key_index.positive?

  @master = MoneyTree::Master.new(seed_hex: seed)

  @addresses.each do
    key = derive_key
    a = address(key.address.to_s)
    raise "Seed does not match wallet; cannot find address #{key.address}" if a.nil?

    a.key = key
  end
end

#server_signer_statusSymbol

Returns the ServerSigner Status of the Wallet.



153
154
155
# File 'lib/coinbase/wallet.rb', line 153

def server_signer_status
  Coinbase.to_sym(@model.server_signer_status)
end

#to_sString

Returns a String representation of the Wallet.



370
371
372
373
# File 'lib/coinbase/wallet.rb', line 370

def to_s
  "Coinbase::Wallet{wallet_id: '#{id}', network_id: '#{network_id}', " \
    "default_address: '#{@model.default_address&.address_id}'}"
end

#trade(amount, from_asset_id, to_asset_id) ⇒ Coinbase::Trade

Trades the given amount of the given Asset for another Asset. Currently only the default_address is used to source the Trade



255
256
257
# File 'lib/coinbase/wallet.rb', line 255

def trade(amount, from_asset_id, to_asset_id)
  default_address.trade(amount, from_asset_id, to_asset_id)
end

#transfer(amount, asset_id, destination) ⇒ Coinbase::Transfer

Transfers the given amount of the given Asset to the specified address or wallet. Only same-network Transfers are supported. Currently only the default_address is used to source the Transfer.



244
245
246
# File 'lib/coinbase/wallet.rb', line 244

def transfer(amount, asset_id, destination)
  default_address.transfer(amount, asset_id, destination)
end