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.

Defined Under Namespace

Modules: ServerSignerStatus Classes: Data

Constant Summary collapse

MAX_ADDRESSES =

The maximum number of addresses in a Wallet.

20
PAGE_LIMIT =

The maximum number of wallets to fetch in a single page.

100

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(model, seed: nil) ⇒ Wallet

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

Parameters:

  • model (Coinbase::Client::Wallet)

    The underlying Wallet object

  • seed (String) (defaults to: nil)

    (Optional) The seed to use for the Wallet. Expects a 32-byte hexadecimal with no 0x prefix. If nil, a new seed will be generated. If the empty string, no seed is generated, and the Wallet will be instantiated without a seed and its corresponding private keys. with the Wallet. If not provided, the Wallet will derive the first default address.

Raises:

  • (ArgumentError)


145
146
147
148
149
150
151
152
153
# File 'lib/coinbase/wallet.rb', line 145

def initialize(model, seed: nil)
  raise ArgumentError, 'model must be a Wallet' unless model.is_a?(Coinbase::Client::Wallet)

  @model = model

  return if Coinbase.use_server_signer?

  @master = master_node(seed)
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

Parameters:

  • network_id (String) (defaults to: 'base-sepolia')

    (Optional) the ID of the blockchain network. Defaults to ‘base-sepolia’.

  • interval_seconds (Integer) (defaults to: 0.2)

    The interval at which to poll the CDPService for the Wallet to

  • timeout_seconds (Integer) (defaults to: 20)

    The maximum amount of time to wait for the ServerSigner to

Returns:



75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/coinbase/wallet.rb', line 75

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: Coinbase.normalize_network(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

.fetch(wallet_id) ⇒ Coinbase::Wallet

Fetches a Wallet by its ID. The returned wallet can be immediately used for signing operations if backed by a server signer. If the wallet is not backed by a server signer, the wallet’s seed will need to be set before it can be used for signing operations.

Parameters:

  • wallet_id (String)

    The ID of the Wallet to fetch

Returns:



60
61
62
63
64
65
66
# File 'lib/coinbase/wallet.rb', line 60

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

  new(model, seed: '')
end

.import(data) ⇒ Coinbase::Wallet

Imports a Wallet from previously exported wallet data.

Parameters:

Returns:

Raises:

  • (ArgumentError)


34
35
36
37
38
39
40
41
42
# File 'lib/coinbase/wallet.rb', line 34

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

  new(model, seed: data.seed)
end

.listEnumerable<Coinbase::Wallet>

Enumerates the wallets for the requesting user. The result is an enumerator that lazily fetches from the server, and can be iterated over, converted to an array, etc…

Returns:



48
49
50
51
52
# File 'lib/coinbase/wallet.rb', line 48

def list
  Coinbase::Pagination.enumerate(lambda(&method(:fetch_wallets_page))) do |wallet|
    Coinbase::Wallet.new(wallet, seed: '')
  end
end

Instance Method Details

#address(address_id) ⇒ Address

Returns the Address with the given ID.

Parameters:

  • address_id (String)

    The ID of the Address to retrieve

Returns:



251
252
253
# File 'lib/coinbase/wallet.rb', line 251

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

#addressesArray<Coinbase::WalletAddress>

Returns the addresses belonging to the Wallet.

Returns:



157
158
159
160
161
162
163
164
165
166
167
168
# File 'lib/coinbase/wallet.rb', line 157

def addresses
  @addresses ||= begin
    address_list = Coinbase.call_api do
      addresses_api.list_addresses(@model.id, { limit: MAX_ADDRESSES })
    end

    # Build the WalletAddress objects, injecting the key if available.
    address_list.data.each_with_index.map do |address_model, index|
      build_wallet_address(address_model, index)
    end
  end
end

#balance(asset_id) ⇒ BigDecimal

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

Parameters:

  • asset_id (Symbol)

    The ID of the Asset to retrieve the balance for

Returns:

  • (BigDecimal)

    The balance of the Asset



268
269
270
271
272
273
274
275
276
# File 'lib/coinbase/wallet.rb', line 268

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.

Returns:

  • (BalanceMap)

    The list of balances. The key is the Asset ID, and the value is the balance.



257
258
259
260
261
262
263
# File 'lib/coinbase/wallet.rb', line 257

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.

Returns:

  • (Boolean)

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



324
325
326
# File 'lib/coinbase/wallet.rb', line 324

def can_sign?
  !@master.nil?
end

#create_addressAddress

Creates a new Address in the Wallet.

Returns:



212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
# File 'lib/coinbase/wallet.rb', line 212

def create_address
  opts = { create_address_request: {} }

  unless Coinbase.use_server_signer?
    # The index for the next address is the number of addresses already registered.
    private_key_index = addresses.count

    key = derive_key(private_key_index)

    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 default_address.nil?

  # Cache the address in our memoized list
  address = WalletAddress.new(address_model, key)
  @addresses << address
  address
end

#default_addressAddress

Returns the default address of the Wallet.

Returns:

  • (Address)

    The default address



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

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

#exportData

Exports the Wallet’s data to a Data object.

Returns:

  • (Data)

    The Wallet data



302
303
304
305
306
307
308
309
# File 'lib/coinbase/wallet.rb', line 302

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.

Returns:

Raises:



316
317
318
319
320
# File 'lib/coinbase/wallet.rb', line 316

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.

Returns:

  • (String)

    The Wallet ID



172
173
174
# File 'lib/coinbase/wallet.rb', line 172

def id
  @model.id
end

#inspectString

Same as to_s.

Returns:

  • (String)

    a String representation of the Wallet



418
419
420
# File 'lib/coinbase/wallet.rb', line 418

def inspect
  to_s
end

#load_seed(file_path) ⇒ String

Loads the seed of the Wallet from the given file.

Parameters:

  • file_path (String)

    The path of the file to load the seed from

Returns:

  • (String)

    A string indicating the success of the operation

Raises:

  • (ArgumentError)


373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/coinbase/wallet.rb', line 373

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.

Returns:

  • (Symbol)

    The Network ID



178
179
180
# File 'lib/coinbase/wallet.rb', line 178

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.

Parameters:

  • file_path (String)

    The path of the file to save the seed to

  • encrypt (bool) (defaults to: false)

    (Optional) Whether the seed information persisted to the local file system should be

Returns:

  • (String)

    A string indicating the success of the operation



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
367
368
# File 'lib/coinbase/wallet.rb', line 336

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.

Parameters:

  • seed (String)

    The seed to set. Expects a 32-byte hexadecimal with no 0x prefix.

Raises:

  • (ArgumentError)


190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
# File 'lib/coinbase/wallet.rb', line 190

def seed=(seed)
  raise ArgumentError, 'Seed must not be empty' if seed.nil? || seed.empty?
  raise StandardError, 'Seed is already set' unless @master.nil?

  @master = master_node(seed)

  # If the addresses are not loaded the keys will be set on them whenever they are loaded.
  return if @addresses.nil?

  # If addresses are already loaded, set the keys on each address.
  addresses.each_with_index.each do |address, index|
    key = derive_key(index)

    # If we derive a key the derived address must match the address from the API.
    raise StandardError, 'Seed does not match wallet' unless address.id == key.address.to_s

    address.key = key
  end
end

#server_signer_statusSymbol

Returns the ServerSigner Status of the Wallet.

Returns:

  • (Symbol)

    The ServerSigner Status



184
185
186
# File 'lib/coinbase/wallet.rb', line 184

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

#to_sString

Returns a String representation of the Wallet.

Returns:

  • (String)

    a String representation of the Wallet



411
412
413
414
# File 'lib/coinbase/wallet.rb', line 411

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

Parameters:

  • amount (Integer, Float, BigDecimal)

    The amount of the Asset to send.

  • from_asset_id (Symbol)

    The ID of the Asset to trade from. For Ether, :eth, :gwei, and :wei are supported.

  • to_asset_id (Symbol)

    The ID of the Asset to trade to. For Ether, :eth, :gwei, and :wei are supported. default address. If a String, interprets it as the address ID.

Returns:



296
297
298
# File 'lib/coinbase/wallet.rb', line 296

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.

Parameters:

  • amount (Integer, Float, BigDecimal)

    The amount of the Asset to send

  • asset_id (Symbol)

    The ID of the Asset to send

  • destination (Wallet | Address | String)

    The destination of the transfer. If a Wallet, sends to the Wallet’s default address. If a String, interprets it as the address ID.

Returns:



285
286
287
# File 'lib/coinbase/wallet.rb', line 285

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