Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[PSDK-102] Implement wallet hydration #47

Merged
merged 7 commits into from
May 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions lib/coinbase/address.rb
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,14 @@ def id
@model.address_id
end

# Sets the private key backing the Address. This key is used to sign transactions.
# @param key [Eth::Key] The key backing the Address
def key=(key)
raise 'Private key is already set' unless @key.nil?

@key = key
end

# Returns the balances of the Address.
# @return [BalanceMap] The balances of the Address, keyed by asset ID. Ether balances are denominated
# in ETH.
Expand Down
26 changes: 22 additions & 4 deletions lib/coinbase/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ def initialize(model, seed: nil, address_models: [])

# TODO: Adjust derivation path prefix based on network protocol.
@address_path_prefix = "m/44'/60'/0'/0"
@address_index = 0
@private_key_index = 0

if address_models.any?
derive_addresses(address_models)
Expand All @@ -99,6 +99,24 @@ def network_id
Coinbase.to_sym(@model.network_id)
end

# Sets the seed of the Wallet. This seed is used to derive keys and sign transactions.
# @param seed [String] The seed to set. Expects a 32-byte hexadecimal with no 0x prefix.
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

# Creates a new Address in the Wallet.
# @return [Address] The new Address
def create_address
Expand All @@ -122,7 +140,7 @@ def create_address
# Returns the default address of the Wallet.
# @return [Address] The default address
def default_address
address(@model.default_address.address_id)
address(@model.default_address&.address_id)
end

# Returns the Address with the given ID.
Expand Down Expand Up @@ -275,8 +293,9 @@ def derive_address(address_map, address_model)
def derive_key
raise 'Cannot derive key for Wallet without seed loaded' if @master.nil?

path = "#{@address_path_prefix}/#{@address_index}"
path = "#{@address_path_prefix}/#{@private_key_index}"
private_key = @master.node_for_path(path).private_key.to_hex
@private_key_index += 1
Eth::Key.new(priv: private_key)
end

Expand All @@ -287,7 +306,6 @@ def derive_key
def cache_address(address_model, key)
address = Address.new(address_model, key)
@addresses << address
@address_index += 1
address
end

Expand Down
12 changes: 12 additions & 0 deletions spec/unit/coinbase/address_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,18 @@
end
end

describe '#key=' do
let(:unhydrated_address) { described_class.new(model, nil) }

it 'sets the key' do
expect { unhydrated_address.key = key }.not_to raise_error
end

it 'raises an error if the key is already set' do
expect { address.key = key }.to raise_error('Private key is already set')
end
end

describe '#balance' do
let(:response) do
Coinbase::Client::Balance.new(
Expand Down
30 changes: 30 additions & 0 deletions spec/unit/coinbase/wallet_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,36 @@
end
end

describe '#seed=' do
let(:seedless_wallet) do
described_class.new(model_with_default_address, seed: '', address_models: [address_model1])
end

it 'sets the seed' do
seedless_wallet.seed = '86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe'
expect(seedless_wallet.can_sign?).to be true
expect(seedless_wallet.default_address.can_sign?).to be true
end

it 'raises an error for an invalid seed' do
expect do
seedless_wallet.seed = 'invalid seed'
end.to raise_error(ArgumentError, 'Seed must be 32 bytes')
end

it 'raises an error if the wallet is already hydrated' do
expect do
wallet.seed = '86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbe'
end.to raise_error('Seed is already set')
end

it 'raises an error if it is the wrong seed' do
expect do
seedless_wallet.seed = '86fc9fba421dcc6ad42747f14132c3cd975bd9fb1454df84ce5ea554f2542fbf'
end.to raise_error(/Seed does not match wallet/)
end
end

describe '#create_address' do
it 'creates a new address' do
expect(wallet.addresses.length).to eq(1)
Expand Down
Loading