diff --git a/lib/glueby/internal/wallet.rb b/lib/glueby/internal/wallet.rb index 66a11be8..dec78f68 100644 --- a/lib/glueby/internal/wallet.rb +++ b/lib/glueby/internal/wallet.rb @@ -293,6 +293,10 @@ def fill_uncolored_inputs( [tx, fee, current_amount, provided_utxos] end + def import_private_key(key, label = nil) + wallet_adapter.import_private_key(id, key, label) + end + private def wallet_adapter diff --git a/lib/glueby/internal/wallet/abstract_wallet_adapter.rb b/lib/glueby/internal/wallet/abstract_wallet_adapter.rb index 9300e8cb..74fd15d9 100644 --- a/lib/glueby/internal/wallet/abstract_wallet_adapter.rb +++ b/lib/glueby/internal/wallet/abstract_wallet_adapter.rb @@ -186,6 +186,19 @@ def get_addresses(wallet_id, label = nil) raise NotImplementedError, "You must implement #{self.class}##{__method__}" end + # Import a private key to the wallet + # + # CAUTION: Wallet Adapter doesn't rescan blockchain for the imported privatekey. + # So the UTXOs in processed blocks are not counted on the Glueby::BlockSyncer. + # + # @param [String] wallet_id - The private key is imported to. + # @param [Tapyrus::Key] key - The private key that is imported to the wallet. + # @param [String?] label - The label to filter the addresses. + # @raise [Errors::PrivateKeyAlreadyImported] If the private key already imported to the wallet. + def import_private_key(wallet_id, key, label = nil) + raise NotImplementedError, "You must implement #{self.class}##{__method__}" + end + # Create and returns pay to contract address # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method. # @param [String] contents - The data to be used for generating pay-to-contract address diff --git a/lib/glueby/internal/wallet/active_record_wallet_adapter.rb b/lib/glueby/internal/wallet/active_record_wallet_adapter.rb index 7ec891a2..16d32c4f 100644 --- a/lib/glueby/internal/wallet/active_record_wallet_adapter.rb +++ b/lib/glueby/internal/wallet/active_record_wallet_adapter.rb @@ -143,6 +143,30 @@ def get_addresses(wallet_id, label = nil) keys.map(&:address) end + def import_private_key(wallet_id, key, label = nil) + raise Glueby::ArgumentError, 'key should be a Tapyrus::Key' unless key.is_a?(Tapyrus::Key) + raise Glueby::ArgumentError, 'label should be a String' if label && !label.is_a?(String) + + wallet = AR::Wallet.find_by(wallet_id: wallet_id) + ar_key = AR::Key.find_by( + wallet: wallet, + private_key: key.priv_key, + public_key: key.pubkey + ) + if ar_key + raise Errors::PrivateKeyAlreadyImported, "Key(pubkey: #{key.pubkey}) already imported in the wallet(#{wallet_id})" + else + AR::Key.create!( + wallet: wallet, + private_key: key.priv_key, + public_key: key.pubkey, + label: label, + purpose: :receive + ) + end + nil + end + def get_addresses_info(addresses) unless addresses.is_a?(Array) addresses = [addresses] diff --git a/lib/glueby/internal/wallet/errors.rb b/lib/glueby/internal/wallet/errors.rb index 70d2e048..0b13e97f 100644 --- a/lib/glueby/internal/wallet/errors.rb +++ b/lib/glueby/internal/wallet/errors.rb @@ -9,6 +9,7 @@ class WalletAlreadyCreated < Error; end class WalletNotFound < Error; end class InvalidSighashType < Error; end class InvalidSigner < Error; end + class PrivateKeyAlreadyImported < Error; end end end end diff --git a/lib/glueby/wallet.rb b/lib/glueby/wallet.rb index fc8f0aff..f4b2f851 100644 --- a/lib/glueby/wallet.rb +++ b/lib/glueby/wallet.rb @@ -35,6 +35,10 @@ def id @internal_wallet.id end + def import_private_key(key, label = nil) + @internal_wallet.import_private_key(key, label) + end + # @return [HashMap] hash of balances which key is color_id or empty string, and value is amount def balances(only_finalized = true) utxos = @internal_wallet.list_unspent(only_finalized) diff --git a/spec/glueby/internal/wallet/active_record_wallet_adapter_spec.rb b/spec/glueby/internal/wallet/active_record_wallet_adapter_spec.rb index c9c6246a..1f0dc519 100644 --- a/spec/glueby/internal/wallet/active_record_wallet_adapter_spec.rb +++ b/spec/glueby/internal/wallet/active_record_wallet_adapter_spec.rb @@ -333,6 +333,65 @@ end end + describe '#import_private_key' do + let(:key) { Tapyrus::Key.generate } + subject { adapter.import_private_key(wallet.wallet_id, key) } + + it 'create a new Key record' do + expect { subject }.to change { Glueby::Internal::Wallet::AR::Key.count }.by(1) + + ar_key = Glueby::Internal::Wallet::AR::Key.where(public_key: key.pubkey).first + expect(ar_key.private_key).to eq(key.priv_key) + expect(ar_key.script_pubkey).to eq(Tapyrus::Script.to_p2pkh(Tapyrus.hash160(key.pubkey)).to_hex) + expect(ar_key.label).to be_nil + expect(ar_key.purpose).to eq('receive') + end + + context 'key is not an instance of Tapyrus::Key' do + let(:key) { 'invalid key' } + + it 'create a new Key record' do + expect { subject }.to raise_error(Glueby::ArgumentError, 'key should be a Tapyrus::Key') + end + end + + context 'specify label' do + let(:label) { 'test_label' } + subject { adapter.import_private_key(wallet.wallet_id, key, label) } + + it 'the new Key record has label' do + subject + ar_key = Glueby::Internal::Wallet::AR::Key.where(public_key: key.pubkey).first + expect(ar_key.label).to eq('test_label') + end + + context 'label is not an instance of String' do + let(:label) { 123 } + it 'create a new Key record' do + expect { subject }.to raise_error(Glueby::ArgumentError, 'label should be a String') + end + end + end + + context 'the private key is already in the wallet' do + before do + Glueby::Internal::Wallet::AR::Key.create!( + wallet: wallet, + private_key: key.priv_key, + public_key: key.pubkey, + purpose: :receive + ) + end + + it 'create a new Key record' do + expect { subject }.to raise_error( + Glueby::Internal::Wallet::Errors::PrivateKeyAlreadyImported, + "Key(pubkey: #{key.pubkey}) already imported in the wallet(#{wallet.wallet_id})" + ) + end + end + end + describe '#get_addresses_info' do subject { adapter.get_addresses_info(addresses) } diff --git a/spec/glueby/wallet_spec.rb b/spec/glueby/wallet_spec.rb index d9578f9c..3105bcfb 100644 --- a/spec/glueby/wallet_spec.rb +++ b/spec/glueby/wallet_spec.rb @@ -75,6 +75,10 @@ def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil) def token_utxos(wallet_id, color_id, only_finalized, per, page) [] end + + def import_private_key(wallet_id, key, label = nil) + true + end end before { Glueby::Internal::Wallet.wallet_adapter = TestWalletAdapter.new } @@ -160,4 +164,13 @@ def token_utxos(wallet_id, color_id, only_finalized, per, page) expect(Glueby::Internal::Wallet.wallet_adapter).to have_received(:list_unspent_with_count).with("wallet_id:1", only_finalized, nil, color_id: color_id, page: 1, per: 25) end end + + describe '#import_private_key' do + let(:key) { Tapyrus::Key.generate } + let(:wallet) { Glueby::Wallet.create } + + it 'returns true' do + expect(wallet.import_private_key(key, 'test-label')).to eq(true) + end + end end \ No newline at end of file