Class: Glueby::Internal::Wallet::ActiveRecordWalletAdapter

Inherits:
AbstractWalletAdapter show all
Defined in:
lib/glueby/internal/wallet/active_record_wallet_adapter.rb,
lib/glueby/internal/wallet/active_record_wallet_adapter/syncer.rb

Overview

ActiveRecordWalletAdapter

This class represents a wallet adapter that use Active Record to manage wallet and utxos. To use this wallet adapter, you should do the following steps:

(1) Generate migrations for wallets, keys, utxos tables. The generator ‘glueby:contract:wallet_adapter` is available for migration. “` rails g glueby:contract:wallet_adapter “` this generator generates 3 files to create tables used by ActiveRecordWalletAdatper. then, migrate them “` $ rails db:migrate “`

(2) Add configuration for activerecord “‘ config = ’activerecord’, schema: ‘http’, host: ‘127.0.0.1’, port: 12381, user: ‘user’, password: ‘pass’ Glueby::Wallet.configure(config) “‘

(3) Generate wallet and receive address “‘ alice_wallet = Glueby::Wallet.create address = alice_wallet.internal_wallet.receive_address “` `Glueby::Internal::Wallet#receive_address` returns Base58 encoded Tapyrus address like ’1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a’.

(4) Send TPC to created address and import into wallet. In general, ActiveRecordWalletAdapter handle only utxos generated by glueby. So, to start to use wallet, some external utxo should be imported into the wallet first. This step should be done by external transaction without Glueby::Wallet, such as ‘sendtoaddress’ or ‘generatetoaddress’ RPC command of Tapyrus Core “‘ $ tapyrus-cli sendtoaddress 1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a 1 1740af9f65ffd8537bdd06ccfa911bf1f4d6833222807d29c99d72b47838917d “`

then, import into wallet by rake task ‘glueby:contract:wallet_adapter:import_tx` or `glueby:contract:wallet_adapter:import_block` “` $ rails “glueby:contract:wallet_adapter:import_tx”“ “`

(5) You are ready to use ActiveRecordWalletAdatper, check ‘Glueby::Internal::Wallet#list_unspent` or `Glueby::Wallet#balances` “` alice_wallet = Glueby::Wallet.create alice_wallet.balances “`

Direct Known Subclasses

MySQLWalletAdapter

Defined Under Namespace

Classes: Syncer

Instance Method Summary collapse

Methods inherited from AbstractWalletAdapter

#lock_unspent

Instance Method Details

#balance(wallet_id, only_finalized = true) ⇒ Object



86
87
88
89
90
91
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 86

def balance(wallet_id, only_finalized = true)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  utxos = wallet.utxos.where(locked_at: nil)
  utxos = utxos.where(status: :finalized) if only_finalized
  utxos.sum(&:value)
end

#broadcast(wallet_id, tx, &block) ⇒ Object



112
113
114
115
116
117
118
119
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 112

def broadcast(wallet_id, tx, &block)
  ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
    AR::Utxo.destroy_for_inputs(tx)
    AR::Utxo.create_or_update_for_outputs(tx, status: :broadcasted)
    block.call(tx) if block
    Glueby::Internal::RPC.client.sendrawtransaction(tx.to_hex)
  end
end

#change_address(wallet_id) ⇒ Object



127
128
129
130
131
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 127

def change_address(wallet_id)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  key = wallet.keys.create(purpose: :change)
  key.address
end

#create_pay_to_contract_address(wallet_id, contents) ⇒ Object



176
177
178
179
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 176

def create_pay_to_contract_address(wallet_id, contents)
  pubkey = create_pubkey(wallet_id)
  [pay_to_contract_key(wallet_id, pubkey, contents).to_p2pkh, pubkey.pubkey]
end

#create_pubkey(wallet_id) ⇒ Object



133
134
135
136
137
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 133

def create_pubkey(wallet_id)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  key = wallet.keys.create(purpose: :receive)
  Tapyrus::Key.new(pubkey: key.public_key, key_type: Tapyrus::Key::TYPES[:compressed])
end

#create_wallet(wallet_id = nil) ⇒ Object



61
62
63
64
65
66
67
68
69
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 61

def create_wallet(wallet_id = nil)
  wallet_id = SecureRandom.hex(16) unless wallet_id
  begin
    AR::Wallet.create!(wallet_id: wallet_id)
  rescue ActiveRecord::RecordInvalid => _
    raise Errors::WalletAlreadyCreated, "wallet_id '#{wallet_id}' is already exists"
  end
  wallet_id
end

#delete_wallet(wallet_id) ⇒ Object



71
72
73
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 71

def delete_wallet(wallet_id)
  AR::Wallet.destroy_by(wallet_id: wallet_id)
end

#get_addresses(wallet_id, label = nil) ⇒ Object



139
140
141
142
143
144
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 139

def get_addresses(wallet_id, label = nil)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  keys = wallet.keys
  keys = keys.where(label: label) if label
  keys.map(&:address)
end

#get_addresses_info(addresses) ⇒ Object



146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 146

def get_addresses_info(addresses)
  unless addresses.is_a?(Array)
    addresses = [addresses]
  end

  script_pubkeys = addresses.map do |address|
    Tapyrus::Script.parse_from_addr(address).to_hex
  rescue ::ArgumentError => e
    raise Glueby::ArgumentError, "\"#{address}\" is invalid address. #{e.message}"
  rescue RuntimeError => e
    if e.message == 'Invalid version bytes.' || e.message == 'Invalid address.'
      raise Glueby::ArgumentError, "\"#{address}\" is invalid address. #{e.message}"
    else
      raise e
    end
  end

  keys = AR::Key.where(script_pubkey: script_pubkeys)
  keys.map do |key|
    {
      address: key.address,
      public_key: key.public_key,
      wallet_id: key.wallet.wallet_id,
      label: key.label,
      purpose: key.purpose,
      script_pubkey: key.script_pubkey
    }
  end
end

#has_address?(wallet_id, address) ⇒ Boolean

Returns:

  • (Boolean)


200
201
202
203
204
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 200

def has_address?(wallet_id, address)
  script_pubkey = Tapyrus::Script.parse_from_addr(address)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  wallet.keys.exists?(script_pubkey: script_pubkey.to_hex)
end

#list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil) ⇒ Object



102
103
104
105
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 102

def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
  utxos = list_unspent_internal(wallet_id, color_id, only_finalized, label)
  utxos_to_h(utxos)
end

#list_unspent_with_count(wallet_id, only_finalized = true, label = nil, color_id: nil, page: 1, per: 25) ⇒ Object



93
94
95
96
97
98
99
100
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 93

def list_unspent_with_count(wallet_id, only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
  utxos = list_unspent_internal(wallet_id, color_id, only_finalized, label)
  utxos = utxos.page(page).per(per) if per > 0
  {
    count: utxos.total_count,
    outputs: utxos_to_h(utxos)
  }
end

#load_wallet(wallet_id) ⇒ Object



75
76
77
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 75

def load_wallet(wallet_id)
  raise Errors::WalletNotFound, "Wallet #{wallet_id} does not found" unless AR::Wallet.where(wallet_id: wallet_id).exists?
end

#pay_to_contract_key(wallet_id, pubkey, contents) ⇒ Object



181
182
183
184
185
186
187
188
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 181

def pay_to_contract_key(wallet_id, pubkey, contents)
  # Calculate P + H(P || contents)G
  group = ECDSA::Group::Secp256k1
  p = pubkey.to_point # P
  commitment = create_pay_to_contract_commitment(pubkey, contents)
  point = p + group.generator.multiply_by_scalar(commitment) # P + H(P || contents)G
  Tapyrus::Key.new(pubkey: point.to_hex(true), key_type: Tapyrus::Key::TYPES[:compressed])
end

#receive_address(wallet_id, label = nil) ⇒ Object



121
122
123
124
125
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 121

def receive_address(wallet_id, label = nil)
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  key = wallet.keys.create(purpose: :receive, label: label)
  key.address
end

#sign_to_pay_to_contract_address(wallet_id, tx, utxo, payment_base, contents) ⇒ Object



190
191
192
193
194
195
196
197
198
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 190

def sign_to_pay_to_contract_address(wallet_id, tx, utxo, payment_base, contents)
  key = create_pay_to_contract_private_key(wallet_id, payment_base, contents)
  sighash = tx.sighash_for_input(utxo[:vout], Tapyrus::Script.parse_from_payload(utxo[:script_pubkey].htb))

  sig = key.sign(sighash, algo: :schnorr) + [Tapyrus::SIGHASH_TYPE[:all]].pack('C')
  script_sig = Tapyrus::Script.parse_from_payload(Tapyrus::Script.pack_pushdata(sig) + Tapyrus::Script.pack_pushdata(key.pubkey.htb))
  tx.inputs[utxo[:vout]].script_sig = script_sig
  tx
end

#sign_tx(wallet_id, tx, prevtxs = [], sighashtype: ) ⇒ Object



107
108
109
110
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 107

def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
  wallet = AR::Wallet.find_by(wallet_id: wallet_id)
  wallet.sign(tx, prevtxs, sighashtype: sighashtype)
end

#unload_wallet(wallet_id) ⇒ Object



79
80
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 79

def unload_wallet(wallet_id)
end

#walletsObject



82
83
84
# File 'lib/glueby/internal/wallet/active_record_wallet_adapter.rb', line 82

def wallets
  AR::Wallet.all.map(&:wallet_id).sort
end