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

Implement Glueby::Wallet#tokens #196

Merged
merged 10 commits into from
Dec 5, 2023
1 change: 1 addition & 0 deletions glueby.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Gem::Specification.new do |spec|

spec.add_runtime_dependency 'tapyrus', '>= 0.3.1'
spec.add_runtime_dependency 'activerecord', '~> 7.0.0'
spec.add_runtime_dependency 'kaminari'
spec.add_development_dependency 'sqlite3'
spec.add_development_dependency 'mysql2'
spec.add_development_dependency 'rails', '~> 7.0.0'
Expand Down
1 change: 1 addition & 0 deletions lib/generators/glueby/contract/templates/utxo_table.rb.erb
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ class CreateUtxo < ActiveRecord::Migration<%= migration_version %>
t.string :txid
t.integer :index
t.bigint :value
t.string :color_id, index: true
t.string :script_pubkey
t.string :label, index: true
t.integer :status
Expand Down
2 changes: 1 addition & 1 deletion lib/glueby.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ module Glueby

module GluebyLogger
def logger
if defined?(Rails)
if defined?(Rails) && Rails.logger
Rails.logger
else
Logger.new(STDOUT)
Expand Down
13 changes: 5 additions & 8 deletions lib/glueby/contract/tx_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -206,8 +206,8 @@ def create_multi_transfer_tx(color_id:, sender:, receivers:, fee_estimator: FeeE
tx = Tapyrus::Tx.new

amount = receivers.reduce(0) { |sum, r| sum + r[:amount].to_i }
utxos = sender.internal_wallet.list_unspent(only_finalized)
sum_token, outputs = collect_colored_outputs(utxos, color_id, amount)
utxos = sender.internal_wallet.list_unspent(only_finalized, color_id: color_id)
sum_token, outputs = collect_colored_outputs(utxos, amount)
fill_input(tx, outputs)

receivers.each do |r|
Expand Down Expand Up @@ -247,8 +247,8 @@ def create_multi_transfer_tx(color_id:, sender:, receivers:, fee_estimator: FeeE
def create_burn_tx(color_id:, sender:, amount: 0, fee_estimator: FeeEstimator::Fixed.new, only_finalized: true)
tx = Tapyrus::Tx.new

utxos = sender.internal_wallet.list_unspent(only_finalized)
sum_token, outputs = collect_colored_outputs(utxos, color_id, amount)
utxos = sender.internal_wallet.list_unspent(only_finalized, color_id: color_id)
sum_token, outputs = collect_colored_outputs(utxos, amount)
fill_input(tx, outputs)

fill_change_token(tx, sender, sum_token - amount, color_id) if amount.positive?
Expand Down Expand Up @@ -319,12 +319,9 @@ def fill_change_token(tx, wallet, change, color_id)
# Returns the set of utxos that satisfies the specified amount and has the specified color_id.
# if amount is not specified or 0, return all utxos with color_id
# @param results [Array] response of Glueby::Internal::Wallet#list_unspent
# @param color_id [Tapyrus::Color::ColorIdentifier] color identifier
# @param amount [Integer]
def collect_colored_outputs(results, color_id, amount = 0)
def collect_colored_outputs(results, amount = 0)
results = results.inject([0, []]) do |sum, output|
next sum unless output[:color_id] == color_id.to_hex

new_sum = sum[0] + output[:amount]
new_outputs = sum[1] << output
return [new_sum, new_outputs] if new_sum >= amount && amount.positive?
Expand Down
33 changes: 24 additions & 9 deletions lib/glueby/internal/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,9 +93,25 @@ def balance(only_finalized = true)
# @param label [String] This label is used to filtered the UTXOs with labeled if a key or Utxo is labeled.
# - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
# - If label=:all, all UTXOs will be returned.
def list_unspent(only_finalized = true, label = :unlabeled)
# @param color_id [Tapyrus::Color::ColorIdentifier] The color identifier associated with UTXO.
# It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
# If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
# @param page [Integer] The page parameter is responsible for specifying the current page being viewed within the paginated results. default is 1.
# @param per [Integer] The per parameter is used to determine the number of items to display per page. default is 25.
def list_unspent_with_count(only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
wallet_adapter.list_unspent_with_count(id, only_finalized, label, color_id: color_id, page: page, per: per)
end

# @param only_finalized [Boolean] The flag to get a UTXO with status only finalized
# @param label [String] This label is used to filtered the UTXOs with labeled if a key or Utxo is labeled.
# - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
# - If label=:all, all UTXOs will be returned.
# @param color_id [Tapyrus::Color::ColorIdentifier] The color identifier associated with UTXO.
# It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
# If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
def list_unspent(only_finalized = true, label = :unlabeled, color_id: nil )
label = :unlabeled unless label
wallet_adapter.list_unspent(id, only_finalized, label)
wallet_adapter.list_unspent(id, only_finalized, label, color_id: color_id)
end

def lock_unspent(utxo)
Expand Down Expand Up @@ -171,8 +187,7 @@ def collect_uncolored_outputs(
lock_utxos = false,
excludes = []
)
collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
next false unless output[:color_id].nil?
collect_utxos(amount, label, Tapyrus::Color::ColorIdentifier.default, only_finalized, shuffle, lock_utxos, excludes) do |output|
next yield(output) if block_given?

true
Expand Down Expand Up @@ -206,8 +221,7 @@ def collect_colored_outputs(
lock_utxos = false,
excludes = []
)
collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
next false unless output[:color_id] == color_id.to_hex
collect_utxos(amount, label, color_id, only_finalized, shuffle, lock_utxos, excludes) do |output|
next yield(output) if block_given?

true
Expand Down Expand Up @@ -261,7 +275,7 @@ def fill_uncolored_inputs(
while current_amount - fee < target_amount
sum, utxos = collect_uncolored_outputs(
fee + target_amount - current_amount,
nil, nil, true, true,
nil, false, true, true,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

only_finalized が nil になってたんですね、、nil でも false でも挙動は変わりませんが、boolean を想定している引数だと思うので、nil は変ですね。

provided_utxos,
&block
)
Expand All @@ -288,15 +302,16 @@ def wallet_adapter
def collect_utxos(
amount,
label,
only_finalized,
color_id,
only_finalized = true,
shuffle = true,
lock_utxos = false,
excludes = []
)
collect_all = amount.nil?

raise Glueby::ArgumentError, 'amount must be positive' unless collect_all || amount.positive?
utxos = list_unspent(only_finalized, label)
utxos = list_unspent(only_finalized, label, color_id: color_id)
utxos = utxos.shuffle if shuffle

r = utxos.inject([0, []]) do |(sum, outputs), output|
Expand Down
20 changes: 19 additions & 1 deletion lib/glueby/internal/wallet/abstract_wallet_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,21 @@ def balance(wallet_id, only_finalized = true)
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
end

# Returns all tokens with specified color_id
#
# @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
# @param [Boolean] only_finalized - includes only finalized UTXO value if it
# is true. Default is true.
# @param [Tapyrus::Color::ColorIdentifier] color_id The color identifier associated with UTXO.
# It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
# If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
# @param [Integer] page - The page parameter is responsible for specifying the current page being viewed within the paginated results. default is 1.
# @param [Integer] per - The per parameter is used to determine the number of items to display per page. default is 25.
# @return [Array<Utxo>] The array of the utxos with specified color_id
def list_unspent_with_count(wallet_id, only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
end

# Returns the UTXOs that the wallet has.
# If label is specified, return UTXOs filtered with label
#
Expand All @@ -74,6 +89,9 @@ def balance(wallet_id, only_finalized = true)
# @param [String] label - Label for filtering UTXOs
# - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
# - If label=:all, it will return all utxos
# @param [Tapyrus::Color::ColorIdentifier] color_id - The color identifier.
# It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
# If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
# @return [Array of UTXO]
#
# ## The UTXO structure
Expand All @@ -84,7 +102,7 @@ def balance(wallet_id, only_finalized = true)
# - finalized: [Boolean] Whether the UTXO is finalized
# - color_id: [String] Color id of the UTXO. If it is TPC UTXO, color_id is nil.
# - script_pubkey: [String] Script pubkey of the UTXO
def list_unspent(wallet_id, only_finalized = true, label = nil)
def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
raise NotImplementedError, "You must implement #{self.class}##{__method__}"
end

Expand Down
1 change: 1 addition & 0 deletions lib/glueby/internal/wallet/active_record/utxo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ def self.create_or_update_for_outputs(tx, status: :finalized)
utxo = Utxo.find_or_initialize_by(txid: tx.txid, index: index)
utxo.update!(
label: key.label,
color_id: output.script_pubkey.color_id&.to_hex,
script_pubkey: output.script_pubkey.to_hex,
value: output.value,
status: status,
Expand Down
65 changes: 43 additions & 22 deletions lib/glueby/internal/wallet/active_record_wallet_adapter.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# frozen_string_literal: true

require 'securerandom'
require 'kaminari'

module Glueby
module Internal
Expand Down Expand Up @@ -89,29 +90,18 @@ def balance(wallet_id, only_finalized = true)
utxos.sum(&:value)
end

def list_unspent(wallet_id, only_finalized = true, label = nil)
wallet = AR::Wallet.find_by(wallet_id: wallet_id)
utxos = wallet.utxos.where(locked_at: nil)
utxos = utxos.where(status: :finalized) if only_finalized
if [:unlabeled, nil].include?(label)
utxos = utxos.where(label: nil)
elsif label && (label != :all)
utxos = utxos.where(label: label)
else
utxos
end
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

utxos.map do |utxo|
{
txid: utxo.txid,
vout: utxo.index,
script_pubkey: utxo.script_pubkey,
color_id: utxo.color_id,
amount: utxo.value,
finalized: utxo.status == 'finalized',
label: utxo.label
}
end
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

def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
Expand Down Expand Up @@ -245,6 +235,37 @@ def create_pay_to_contract_private_key(wallet_id, payment_base, contents)
key_type: Tapyrus::Key::TYPES[:compressed]
)
end

def list_unspent_internal(wallet_id, color_id = nil, only_finalized = true, label = nil)
wallet = AR::Wallet.find_by(wallet_id: wallet_id)
utxos = wallet.utxos.where(locked_at: nil)
utxos = utxos.where(color_id: color_id.to_hex) if color_id && !color_id.default?
utxos = utxos.where(color_id: nil) if color_id && color_id.default?
utxos = utxos.where(status: :finalized) if only_finalized
utxos = utxos.order(:id)
utxos = if [:unlabeled, nil].include?(label)
utxos = utxos.where(label: nil)
elsif label && (label != :all)
utxos = utxos.where(label: label)
else
utxos
end
utxos
end

def utxos_to_h(utxos)
utxos.map do |utxo|
{
txid: utxo.txid,
vout: utxo.index,
script_pubkey: utxo.script_pubkey,
color_id: utxo.color_id,
amount: utxo.value,
finalized: utxo.status == 'finalized',
label: utxo.label
}
end
end
end
end
end
Expand Down
9 changes: 8 additions & 1 deletion lib/glueby/internal/wallet/tapyrus_core_wallet_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ def balance(wallet_id, only_finalized = true)

# If label=nil, it will return unlabeled utxos to protect labeled utxos for specific purpose
# If label=:all, it will return all utxos
def list_unspent(wallet_id, only_finalized = true, label = nil)
def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
perform_as(wallet_id) do |client|
min_conf = only_finalized ? 1 : 0
res = client.listunspent(min_conf)
Expand All @@ -97,6 +97,13 @@ def list_unspent(wallet_id, only_finalized = true, label = nil)
res
end

if color_id
res = res.filter do |i|
script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
script.cp2pkh? || script.cp2sh? && color_id == Tapyrus::Color::ColorIdentifier.parse_from_payload(script.chunks[0].pushed_data)
end
end

res.map do |i|
script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
color_id = if script.cp2pkh? || script.cp2sh?
Expand Down
4 changes: 4 additions & 0 deletions lib/glueby/wallet.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ def balances(only_finalized = true)
end
end

def token_utxos(color_id = nil, only_finalized = true, page = 1, per = 25)
@internal_wallet.list_unspent_with_count(only_finalized, nil, color_id: color_id, page: page, per: per)
end

private

def initialize(internal_wallet)
Expand Down
14 changes: 13 additions & 1 deletion spec/glueby/contract/token_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,20 @@
end
after { Glueby.configuration.disable_utxo_provider! }
end

before do
allow(internal_wallet).to receive(:list_unspent).and_return(unspents)
allow(internal_wallet).to receive(:list_unspent).and_return([])
allow(internal_wallet).to receive(:list_unspent).with(true, nil, color_id: Tapyrus::Color::ColorIdentifier.default).and_return(unspents.select{ |i| !i[:color_id] && i[:finalized] })
allow(internal_wallet).to receive(:list_unspent).with(false, nil, color_id: Tapyrus::Color::ColorIdentifier.default).and_return(unspents.select{ |i| !i[:color_id] })
[
"c3eb2b846463430b7be9962843a97ee522e3dc0994a0f5e2fc0aa82e20e67fe893",
"c2dbbebb191128de429084246fa3215f7ccc36d6abde62984eb5a42b1f2253a016",
"c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3",
].each do |color_id_hex|
color_id = Tapyrus::Color::ColorIdentifier.parse_from_payload(color_id_hex.htb)
allow(internal_wallet).to receive(:list_unspent).with(true, nil, color_id: color_id).and_return(unspents.select{ |i| i[:color_id] == color_id_hex && i[:finalized] })
allow(internal_wallet).to receive(:list_unspent).with(false, nil, color_id: color_id).and_return(unspents.select{ |i| i[:color_id] == color_id_hex })
end
allow(Glueby::Internal::RPC).to receive(:client).and_return(rpc)
allow(rpc).to receive(:sendrawtransaction).and_return('')
end
Expand Down
22 changes: 18 additions & 4 deletions spec/glueby/contract/tx_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -385,7 +385,11 @@ class TxBuilderMock
let(:receiver_address) { wallet.internal_wallet.receive_address }
let(:script_pubkey) { Tapyrus::Script.parse_from_addr(receiver_address).add_color(color_id).to_hex }
let(:amount) { 100_001 }


before do
allow(internal_wallet).to receive(:list_unspent).with(true, color_id: color_id)
.and_return(unspents.select { |utxo| utxo[:color_id] == 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' })
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

メモ

このテストでは、もともと list_unspent はスタブしていたが、今回のへ変更で list_unspent に color_id を指定することで、特定のcolor id の utxo だけを返すようにできるので、それを使用する想定でスタブの記述を上書きしたという事ですね。

少し理解に時間がかかったので他のレビュアーのためにメモとして残します。

end
it { expect(subject.inputs.size).to eq 3 }
it { expect(subject.inputs[0].out_point.txid).to eq '100c4dc65ea4af8abb9e345b3d4cdcc548bb5e1cdb1cb3042c840e147da72fa2' }
it { expect(subject.inputs[0].out_point.index).to eq 0 }
Expand Down Expand Up @@ -423,6 +427,11 @@ class TxBuilderMock
]
end

before do
allow(internal_wallet).to receive(:list_unspent).with(true, color_id: color_id)
.and_return(unspents.select { |utxo| utxo[:color_id] == 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' })
end

it { expect(subject.inputs.size).to eq 3 }
it { expect(subject.inputs[0].out_point.txid).to eq '100c4dc65ea4af8abb9e345b3d4cdcc548bb5e1cdb1cb3042c840e147da72fa2' }
it { expect(subject.inputs[0].out_point.index).to eq 0 }
Expand Down Expand Up @@ -479,6 +488,12 @@ class TxBuilderMock
let(:amount) { 50_000 }
let(:fee_estimator) { Glueby::Contract::FeeEstimator::Fixed.new }


before do
allow(internal_wallet).to receive(:list_unspent).with(true, color_id: color_id)
.and_return(unspents.select { |utxo| utxo[:color_id] == 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' })
end

it { expect(subject.inputs.size).to eq 2 }
it { expect(subject.inputs[0].out_point.txid).to eq '100c4dc65ea4af8abb9e345b3d4cdcc548bb5e1cdb1cb3042c840e147da72fa2' }
it { expect(subject.inputs[0].out_point.index).to eq 0 }
Expand Down Expand Up @@ -641,11 +656,10 @@ class TxBuilderMock
end

describe '#collect_colored_outputs' do
subject { mock.collect_colored_outputs(results, color_id, amount) }
subject { mock.collect_colored_outputs(results, amount) }

let(:results) { unspents }
let(:results) { unspents.select { |utxo| utxo[:color_id] == 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' } }
let(:amount) { 50_000 }
let(:color_id) { Tapyrus::Color::ColorIdentifier.parse_from_payload('c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3'.htb) }

it { expect(subject[0]).to eq 100_000 }
it { expect(subject[1].size).to eq 1 }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,15 @@
end
end

describe '#list_unspent_with_count' do
it 'does not implemented' do
expect { adapter.list_unspent_with_count("wallet_id") }.to raise_error(NotImplementedError)
end
end

describe '#list_unspent' do
it 'does not implemented' do
expect { adapter.list_unspent("wallet_id") }.to raise_error(NotImplementedError)
expect { adapter.list_unspent("wallet_id", nil) }.to raise_error(NotImplementedError)
end
end

Expand Down
Loading
Loading