glueby 1.2.2 → 1.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d9a8e99610f76e6d765a52a645ead25eb8cf71343ba259ed25e5ec2fbdb0d5e5
4
- data.tar.gz: 0ee19c244501e854cbe59f6957e5e0c8a25bca403a9fa58d0367ad95de3f0a38
3
+ metadata.gz: 41286c658b45bd9914f4c46f27e8e0f8696b61a8d763b5fcc2b0117d0e91668d
4
+ data.tar.gz: 3237815c98b14f3fc126e5531c8ff229d468fe77832bcca4b22828ffd386c7bd
5
5
  SHA512:
6
- metadata.gz: 47dfd21bdd71bf99379f448816a58b3cef5fd093352d0f7a53806a85db5b2509765fa4f2f4813621a36fe0edc98c7491aca162f445ab9bb0708e3b3ca68756cc
7
- data.tar.gz: a8cd80a311e74759b6bf1351974888e787ddeb084a435dd6a13aadc4c1fb2a65d0be5d993ee0303e6e08d67bfecd7e09cd0763ecb92d0a19146838544479ee85
6
+ metadata.gz: dad03d9f436a2720b854450fb95e8fab7fedde3d721f5db3bf0c69c1aa7b818ebfc359cda0c1057f4d98075295459f55f5261c69a4cc99a49f752bba3683e95c
7
+ data.tar.gz: eb8f0724b6b7b849a343b14e5630480584df5b6f408509dcdeebcca711055f8bcd3a62c188130b75118a7f93e18e015aeb2afd60784d4382cd27741b010f2c06
@@ -7,19 +7,15 @@
7
7
 
8
8
  name: Ruby
9
9
 
10
- on:
11
- push:
12
- branches: [master, v1.1, v1.2]
13
- pull_request:
14
- branches: [master, v1.1, v1.2]
10
+ on: [push,pull_request]
15
11
 
16
12
  jobs:
17
13
  test:
18
14
  runs-on: ubuntu-latest
19
15
  strategy:
20
16
  matrix:
21
- ruby-version: ["2.7", "3.0", "3.1", "3.2"]
22
-
17
+ ruby-version: ["3.0", "3.1", "3.2", "3.3"]
18
+ permissions: write-all
23
19
  steps:
24
20
  - run: docker pull tapyrus/tapyrusd:v0.5.2
25
21
  - uses: actions/checkout@v2
@@ -31,4 +27,4 @@ jobs:
31
27
  ruby-version: ${{ matrix.ruby-version }}
32
28
  bundler-cache: true # runs 'bundle install' and caches installed gems automatically
33
29
  - name: Run tests
34
- run: bundle exec rake
30
+ run: bundle exec rake
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- ruby-3.2.2
1
+ ruby-3.3.0
data/Gemfile CHANGED
@@ -6,3 +6,5 @@ gemspec
6
6
  gem "rake", "~> 12.0"
7
7
  gem "rspec", "~> 3.0"
8
8
  gem "docker-api", "~> 2.1.0"
9
+
10
+ gem "simplecov", require: false
data/glueby.gemspec CHANGED
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.description = %q{A Ruby library of smart contracts that can be used on Tapyrus.}
11
11
  spec.homepage = "https://github.com/chaintope/glueby"
12
12
  spec.license = "MIT"
13
- spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 3.0.0")
14
14
 
15
15
 
16
16
  spec.metadata["homepage_uri"] = spec.homepage
@@ -28,6 +28,7 @@ Gem::Specification.new do |spec|
28
28
 
29
29
  spec.add_runtime_dependency 'tapyrus', '>= 0.3.1'
30
30
  spec.add_runtime_dependency 'activerecord', '~> 7.0.0'
31
+ spec.add_runtime_dependency 'kaminari'
31
32
  spec.add_development_dependency 'sqlite3'
32
33
  spec.add_development_dependency 'mysql2'
33
34
  spec.add_development_dependency 'rails', '~> 7.0.0'
@@ -13,6 +13,7 @@ class CreateTimestamp < ActiveRecord::Migration<%= migration_version %>
13
13
  t.string :payment_base
14
14
  t.bigint :prev_id
15
15
  t.boolean :latest, null: false, default: true
16
+ t.string :version, null: false, default: "1"
16
17
  end
17
18
 
18
19
  add_index :glueby_timestamps, [:prev_id], unique: true
@@ -4,6 +4,7 @@ class CreateUtxo < ActiveRecord::Migration<%= migration_version %>
4
4
  t.string :txid
5
5
  t.integer :index
6
6
  t.bigint :value
7
+ t.string :color_id, index: true
7
8
  t.string :script_pubkey
8
9
  t.string :label, index: true
9
10
  t.integer :status
@@ -4,5 +4,22 @@ require 'active_record'
4
4
  module Glueby
5
5
  module AR
6
6
  autoload :SystemInformation, 'glueby/active_record/system_information'
7
+
8
+ def transaction(isolation: nil)
9
+ options = { joinable: false, requires_new: true }
10
+
11
+ adapter_type = ActiveRecord::Base.connection.adapter_name.downcase
12
+
13
+ # SQLite3 does not support transaction isolation level READ COMMITTED.
14
+ if isolation == :read_committed && adapter_type == 'mysql2'
15
+ options[:isolation] = :read_committed
16
+ end
17
+
18
+ ActiveRecord::Base.transaction(**options) do
19
+ yield
20
+ end
21
+ end
22
+
23
+ module_function :transaction
7
24
  end
8
25
  end
@@ -24,7 +24,7 @@ module Glueby
24
24
  end
25
25
 
26
26
  # Specify wallet adapter.
27
- # @param [Symbol] adapter - The adapter type :activerecord or :core is currently supported.
27
+ # @param [Symbol] adapter - The adapter type :activerecord, :core, or :mysql is currently supported.
28
28
  def wallet_adapter=(adapter)
29
29
  case adapter
30
30
  when :core
@@ -9,16 +9,25 @@ module Glueby
9
9
 
10
10
  attr_reader :tx
11
11
 
12
- belongs_to :prev, class_name: 'Glueby::Contract::AR::Timestamp', optional: true
12
+ belongs_to :prev, class_name: 'Glueby::Contract::AR::Timestamp', inverse_of: :next, optional: true
13
+ has_one :next, class_name: 'Glueby::Contract::AR::Timestamp', foreign_key: 'prev_id'
14
+
15
+ validates :version, presence: true, inclusion: { in: ['1', '2'] }
16
+
17
+ def next_id
18
+ self.next&.id
19
+ end
13
20
 
14
21
  validate :validate_prev
15
22
 
16
23
  class << self
17
- def digest_content(content, digest)
24
+ def digest_content(content, digest, hex = false)
18
25
  case digest&.downcase
19
26
  when :sha256
27
+ content = hex ? content.htb : content
20
28
  Tapyrus.sha256(content).bth
21
29
  when :double_sha256
30
+ content = hex ? content.htb : content
22
31
  Tapyrus.double_sha256(content).bth
23
32
  when :none
24
33
  content
@@ -33,17 +42,23 @@ module Glueby
33
42
  # - content
34
43
  # - prefix(optional)
35
44
  # - timestamp_type(optional)
45
+ # - version [String] Version of the timestamp recording method.
46
+ # The format in which the timestamp is recorded differs depending on the version.
47
+ # Version "1" treats the specified content and prefix as a binary string.
48
+ # Version "2" treats the specified content and prefix as a hexadecimal string with the string set to prefix and content.
36
49
  # @raise [Glueby::ArgumentError] If the timestamp_type is not in :simple or :trackable
37
50
  def initialize(attributes = nil)
38
51
  # Set content_hash from :content attribute
39
- content_hash = Timestamp.digest_content(attributes[:content], attributes[:digest] || :sha256)
52
+ hex = attributes[:version] == '1' ? false : true
53
+ content_hash = Timestamp.digest_content(attributes[:content], attributes[:digest] || :sha256, hex)
40
54
  super(
41
55
  wallet_id: attributes[:wallet_id],
42
56
  content_hash: content_hash,
43
- prefix: attributes[:prefix] ? attributes[:prefix] : '',
57
+ prefix: attributes[:prefix] || '',
44
58
  status: :init,
45
59
  timestamp_type: attributes[:timestamp_type] || :simple,
46
- prev_id: attributes[:prev_id]
60
+ prev_id: attributes[:prev_id],
61
+ version: attributes[:version]
47
62
  )
48
63
  rescue ::ArgumentError => e
49
64
  raise Glueby::ArgumentError, e.message
@@ -109,6 +124,7 @@ module Glueby
109
124
 
110
125
  if update_trackable?
111
126
  prev.latest = false
127
+ prev.next = self
112
128
  prev.save!
113
129
  end
114
130
  end
@@ -124,6 +140,12 @@ module Glueby
124
140
  raise Errors::FailedToBroadcast, "failed to broadcast (id=#{id}, reason=#{e.message})"
125
141
  end
126
142
 
143
+ def hex?
144
+ version != '1'
145
+ end
146
+
147
+ alias_method :hex, :hex?
148
+
127
149
  private
128
150
 
129
151
  def wallet
@@ -134,14 +156,18 @@ module Glueby
134
156
  builder = builder_class.new(wallet, fee_estimator)
135
157
 
136
158
  if builder.instance_of?(Contract::Timestamp::TxBuilder::UpdatingTrackable)
159
+ prev_prefix = prev.hex ? prev.prefix&.htb : prev.prefix
160
+ prev_content_hash = prev.hex ? prev.content_hash&.htb : prev.content_hash
137
161
  builder.set_prev_timestamp_info(
138
162
  timestamp_utxo: prev.utxo,
139
163
  payment_base: prev.payment_base,
140
- prefix: prev.prefix,
141
- data: prev.content_hash
164
+ prefix: prev_prefix,
165
+ data: prev_content_hash
142
166
  )
143
167
  end
144
168
 
169
+ prefix = hex ? self.prefix&.htb : self.prefix
170
+ content_hash = hex ? self.content_hash&.htb : self.content_hash
145
171
  tx = builder.set_data(prefix, content_hash)
146
172
  .set_inputs(utxo_provider)
147
173
  .build
@@ -15,6 +15,7 @@ module Glueby
15
15
  class UnsupportedTokenType < ArgumentError; end
16
16
  class UnknownScriptPubkey < ArgumentError; end
17
17
  class UnsupportedDigestType < ArgumentError; end
18
+ class UnsupportedTimestampVersion < ArgumentError; end
18
19
  class PrevTimestampNotFound < ArgumentError; end
19
20
  class PrevTimestampIsNotTrackable < ArgumentError; end
20
21
  class UnnecessaryPrevTimestamp < ArgumentError; end
@@ -7,6 +7,51 @@ module Glueby
7
7
  # * 1 output to send the change TPC back to the wallet.
8
8
  #
9
9
  # Storing timestamp transaction to the blockchain enables everyone to verify that the data existed at that time and a user signed it.
10
+ #
11
+ # == Versioning Timestamp
12
+ #
13
+ # The timestamp has an attribute called "version".
14
+ # Version indicates how the timestamp is recorded in the blockchain. Currently, only versions 1 and 2 are supported, and each is recorded in the following manner
15
+ # * Version 1: The first version of the blockchain is used to record timestamps.
16
+ # treats the content and prefix received as parameters as a binary string
17
+ # * Version 2:.
18
+ # treats the specified content and prefix as a hexadecimal string with the string set to prefix and content.
19
+ #
20
+ # For example, when recording a simple type of timestamp, the difference between the content recorded by version 1 and version 2 is as follows
21
+ # Version 1:
22
+ # Save the timestamp as follows:
23
+ #
24
+ # Glueby::Contract::Timestamp.new(
25
+ # wallet: wallet,
26
+ # content: "1234",
27
+ # prefix: "071222",
28
+ # digest: :none,
29
+ # version: "1"
30
+ # )
31
+ #
32
+ # The output script of the recorded transaction will include OP_RETURN and will look like this:
33
+ #
34
+ # OP_RETURN 30373132323231323334
35
+ #
36
+ # Note that prefix: "071222" and content: "1234" are interpreted as ASCII strings and their hexadecimal representation "3037313232323132323334" is recorded in the actual blockchain, respectively.
37
+ #
38
+ # Version 2:
39
+ #
40
+ # To save the timestamp in version 2, simply change the version of the previous example to "2".
41
+ #
42
+ # Glueby::Contract::Timestamp.new(
43
+ # wallet: wallet,
44
+ # content: "1234",
45
+ # prefix: "071222",
46
+ # digest: :none,
47
+ # version: "2"
48
+ # )
49
+ #
50
+ # The output script will look like this:
51
+ #
52
+ # OP_RETURN 0712221234
53
+ #
54
+ # In this case, prefix: "071222" and content: "1234" are treated as a hexadecimal string and recorded directly in the blockchain.
10
55
  class Timestamp
11
56
  P2C_DEFAULT_VALUE = 1_000
12
57
 
@@ -29,6 +74,10 @@ module Glueby
29
74
  # - :simple
30
75
  # - :trackable
31
76
  # @param [Integer] prev_timestamp_id The id column of glueby_timestamps that will be updated by the timestamp that will be created
77
+ # @param [String] version Version of the timestamp recording method.
78
+ # The format in which the timestamp is recorded differs depending on the version.
79
+ # Version "1" treats the specified content and prefix as a binary string.
80
+ # Version "2" treats the specified content and prefix as a hexadecimal string with the string set to prefix and content.
32
81
  # @raise [Glueby::Contract::Errors::UnsupportedDigestType] if digest is unsupported
33
82
  # @raise [Glueby::Contract::Errors::InvalidTimestampType] if timestamp_type is unsupported
34
83
  def initialize(
@@ -39,7 +88,8 @@ module Glueby
39
88
  digest: :sha256,
40
89
  utxo_provider: nil,
41
90
  timestamp_type: :simple,
42
- prev_timestamp_id: nil
91
+ prev_timestamp_id: nil,
92
+ version:
43
93
  )
44
94
  @wallet = wallet
45
95
  @content = content
@@ -51,6 +101,8 @@ module Glueby
51
101
  raise Glueby::Contract::Errors::InvalidTimestampType, "#{timestamp_type} is invalid type, supported types are :simple, and :trackable." unless [:simple, :trackable].include?(timestamp_type)
52
102
  @timestamp_type = timestamp_type
53
103
  @prev_timestamp_id = prev_timestamp_id
104
+ raise Glueby::Contract::Errors::UnsupportedTimestampVersion, "#{version} is unsupported, supported versions are '1' and '2'." unless ['1', '2'].include?(version)
105
+ @version = version
54
106
  end
55
107
 
56
108
  # broadcast to Tapyrus Core
@@ -66,7 +118,8 @@ module Glueby
66
118
  content: @content,
67
119
  timestamp_type: @timestamp_type,
68
120
  digest: @digest,
69
- prev_id: @prev_timestamp_id
121
+ prev_id: @prev_timestamp_id,
122
+ version: @version
70
123
  )
71
124
  @ar.save_with_broadcast!(fee_estimator: @fee_estimator, utxo_provider: @utxo_provider)
72
125
  @ar.txid
@@ -79,7 +79,6 @@ module Glueby
79
79
  else
80
80
  raise Glueby::Contract::Errors::UnsupportedTokenType
81
81
  end
82
-
83
82
  [new(color_id: color_id), txs]
84
83
  end
85
84
 
@@ -87,27 +86,8 @@ module Glueby
87
86
  Glueby::AR::SystemInformation.use_only_finalized_utxo?
88
87
  end
89
88
 
90
- # Sign to pay-to-contract output.
91
- #
92
- # @param issuer [Glueby::Walelt] Issuer of the token
93
- # @param tx [Tapyrus::Tx] The transaction to be signed with metadata
94
- # @param funding_tx [Tapyrus::Tx] The funding transaction that has pay-to-contract output in its first output
95
- # @param payment_base [String] The public key used to generate pay to contract public key
96
- # @param metadata [String] Data that represents token metadata
97
- # @return [Tapyrus::Tx] signed tx
98
- def sign_to_p2c_output(issuer, tx, funding_tx, payment_base, metadata)
99
- utxo = { txid: funding_tx.txid, vout: 0, script_pubkey: funding_tx.outputs[0].script_pubkey.to_hex }
100
- issuer.internal_wallet.sign_to_pay_to_contract_address(tx, utxo, payment_base, metadata)
101
- end
102
-
103
89
  private
104
90
 
105
- def create_p2c_address(wallet, metadata)
106
- p2c_address, payment_base = wallet.internal_wallet.create_pay_to_contract_address(metadata)
107
- script = Tapyrus::Script.parse_from_addr(p2c_address)
108
- [script, p2c_address, payment_base]
109
- end
110
-
111
91
  def issue_reissuable_token(issuer:, amount:, split: 1, fee_estimator:, metadata: nil)
112
92
  txb = Internal::ContractBuilder.new(
113
93
  sender_wallet: issuer.internal_wallet,
@@ -135,7 +115,7 @@ module Glueby
135
115
  script_pubkey = funding_tx.outputs.first.script_pubkey
136
116
  color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
137
117
 
138
- ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
118
+ Glueby::AR.transaction(isolation: :read_committed) do
139
119
  # Store the script_pubkey for reissue the token.
140
120
  Glueby::Contract::AR::ReissuableToken.create!(
141
121
  color_id: color_id.to_hex,
@@ -226,7 +206,7 @@ module Glueby
226
206
  .build
227
207
  end
228
208
 
229
- ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
209
+ Glueby::AR.transaction(isolation: :read_committed) do
230
210
  if metadata
231
211
  p2c_utxo = txb.p2c_utxos.first
232
212
  Glueby::Contract::AR::TokenMetadata.create!(
@@ -245,14 +225,6 @@ module Glueby
245
225
  end
246
226
  end
247
227
  end
248
-
249
- # Add dummy inputs and outputs to tx for issue non-reissuable transaction and nft transaction
250
- def dummy_issue_tx_from_out_point
251
- tx = Tapyrus::Tx.new
252
- receiver_colored_script = Tapyrus::Script.parse_from_payload('21c20000000000000000000000000000000000000000000000000000000000000000bc76a914000000000000000000000000000000000000000088ac'.htb)
253
- tx.outputs << Tapyrus::TxOut.new(value: 0, script_pubkey: receiver_colored_script)
254
- FeeEstimator.dummy_tx(tx)
255
- end
256
228
  end
257
229
 
258
230
  attr_reader :color_id
@@ -354,7 +326,10 @@ module Glueby
354
326
  txb.pay(r[:address], r[:amount].to_i, color_id)
355
327
  end
356
328
 
357
- tx = sender.internal_wallet.broadcast(txb.build)
329
+ tx = nil
330
+ Glueby::AR.transaction(isolation: :read_committed) do
331
+ tx = sender.internal_wallet.broadcast(txb.build)
332
+ end
358
333
  [color_id, tx]
359
334
  end
360
335
 
@@ -373,7 +348,7 @@ module Glueby
373
348
  raise Glueby::Contract::Errors::InsufficientTokens unless balance
374
349
  raise Glueby::Contract::Errors::InsufficientTokens if balance < amount
375
350
 
376
- tx = Internal::ContractBuilder
351
+ builder = Internal::ContractBuilder
377
352
  .new(
378
353
  sender_wallet: sender.internal_wallet,
379
354
  fee_estimator: fee_estimator,
@@ -382,9 +357,11 @@ module Glueby
382
357
  )
383
358
  .burn(amount, color_id)
384
359
  .change_address(sender.internal_wallet.receive_address, color_id)
385
- .build
386
360
 
387
- sender.internal_wallet.broadcast(tx)
361
+ Glueby::AR.transaction(isolation: :read_committed) do
362
+ tx = builder.build
363
+ sender.internal_wallet.broadcast(tx)
364
+ end
388
365
  end
389
366
 
390
367
  # Return balance of token in the specified wallet.
@@ -206,8 +206,8 @@ module Glueby
206
206
  tx = Tapyrus::Tx.new
207
207
 
208
208
  amount = receivers.reduce(0) { |sum, r| sum + r[:amount].to_i }
209
- utxos = sender.internal_wallet.list_unspent(only_finalized)
210
- sum_token, outputs = collect_colored_outputs(utxos, color_id, amount)
209
+ utxos = sender.internal_wallet.list_unspent(only_finalized, color_id: color_id)
210
+ sum_token, outputs = collect_colored_outputs(utxos, amount)
211
211
  fill_input(tx, outputs)
212
212
 
213
213
  receivers.each do |r|
@@ -247,8 +247,8 @@ module Glueby
247
247
  def create_burn_tx(color_id:, sender:, amount: 0, fee_estimator: FeeEstimator::Fixed.new, only_finalized: true)
248
248
  tx = Tapyrus::Tx.new
249
249
 
250
- utxos = sender.internal_wallet.list_unspent(only_finalized)
251
- sum_token, outputs = collect_colored_outputs(utxos, color_id, amount)
250
+ utxos = sender.internal_wallet.list_unspent(only_finalized, color_id: color_id)
251
+ sum_token, outputs = collect_colored_outputs(utxos, amount)
252
252
  fill_input(tx, outputs)
253
253
 
254
254
  fill_change_token(tx, sender, sum_token - amount, color_id) if amount.positive?
@@ -319,12 +319,9 @@ module Glueby
319
319
  # Returns the set of utxos that satisfies the specified amount and has the specified color_id.
320
320
  # if amount is not specified or 0, return all utxos with color_id
321
321
  # @param results [Array] response of Glueby::Internal::Wallet#list_unspent
322
- # @param color_id [Tapyrus::Color::ColorIdentifier] color identifier
323
322
  # @param amount [Integer]
324
- def collect_colored_outputs(results, color_id, amount = 0)
323
+ def collect_colored_outputs(results, amount = 0)
325
324
  results = results.inject([0, []]) do |sum, output|
326
- next sum unless output[:color_id] == color_id.to_hex
327
-
328
325
  new_sum = sum[0] + output[:amount]
329
326
  new_outputs = sum[1] << output
330
327
  return [new_sum, new_outputs] if new_sum >= amount && amount.positive?
@@ -90,16 +90,6 @@ module Glueby
90
90
 
91
91
  private
92
92
 
93
- def check_wallet_amount!
94
- if tpc_amount < fee_provider.fixed_fee
95
- raise InsufficientTPC, <<~MESSAGE
96
- FeeProvider has insufficient TPC to create fee outputs to fill the UTXO pool.
97
- 1. Please replenishment TPC which is for paying fee to FeeProvider. FeeProvider needs #{fee_provider.utxo_pool_size * fee_provider.fixed_fee} tapyrus at least. FeeProvider wallet's address is '#{wallet.receive_address}'
98
- 2. Then create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'
99
- MESSAGE
100
- end
101
- end
102
-
103
93
  def tpc_amount
104
94
  wallet.balance(false)
105
95
  end
@@ -130,26 +130,26 @@ module Glueby
130
130
  )
131
131
  tx, index = nil
132
132
 
133
- if Glueby.configuration.use_utxo_provider? || utxo_provider
134
- utxo_provider ||= UtxoProvider.new
135
- script_pubkey = Tapyrus::Script.parse_from_addr(address)
136
- tx, index = utxo_provider.get_utxo(script_pubkey, amount)
137
- else
138
- fee_estimator ||= @fee_estimator
139
- txb = Tapyrus::TxBuilder.new
140
- fee = fee_estimator.fee(Contract::FeeEstimator.dummy_tx(txb.build))
141
- _sum, utxos = sender_wallet
142
- .collect_uncolored_outputs(fee + amount, nil, only_finalized)
143
- utxos.each { |utxo| txb.add_utxo(to_tapyrusrb_utxo_hash(utxo)) }
144
- tx = txb.pay(address, amount)
145
- .change_address(sender_wallet.change_address)
146
- .fee(fee)
147
- .build
148
- sender_wallet.sign_tx(tx)
149
- index = 0
150
- end
133
+ Glueby::AR.transaction(isolation: :read_committed) do
134
+ if Glueby.configuration.use_utxo_provider? || utxo_provider
135
+ utxo_provider ||= UtxoProvider.new
136
+ script_pubkey = Tapyrus::Script.parse_from_addr(address)
137
+ tx, index = utxo_provider.get_utxo(script_pubkey, amount)
138
+ else
139
+ fee_estimator ||= @fee_estimator
140
+ txb = Tapyrus::TxBuilder.new
141
+ fee = fee_estimator.fee(Contract::FeeEstimator.dummy_tx(txb.build))
142
+ _sum, utxos = sender_wallet
143
+ .collect_uncolored_outputs(fee + amount, nil, only_finalized)
144
+ utxos.each { |utxo| txb.add_utxo(to_tapyrusrb_utxo_hash(utxo)) }
145
+ tx = txb.pay(address, amount)
146
+ .change_address(sender_wallet.change_address)
147
+ .fee(fee)
148
+ .build
149
+ sender_wallet.sign_tx(tx)
150
+ index = 0
151
+ end
151
152
 
152
- ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
153
153
  # Here needs to use the return tx from Internal::Wallet#broadcast because the txid
154
154
  # is changed if you enable FeeProvider.
155
155
  tx = sender_wallet.broadcast(tx)
@@ -357,7 +357,7 @@ module Glueby
357
357
  end
358
358
 
359
359
  def get_fee_estimator(fee_estimator_name)
360
- Glueby::Contract::FeeEstimator.get_const("#{fee_estimator_name.capitalize}", false).new
360
+ Glueby::Contract::FeeEstimator.const_get(fee_estimator_name.capitalize, false).new
361
361
  end
362
362
 
363
363
  def valid_fee_estimator?(fee_estimator)
@@ -65,6 +65,21 @@ module Glueby
65
65
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
66
66
  end
67
67
 
68
+ # Returns all tokens with specified color_id
69
+ #
70
+ # @param [String] wallet_id - The wallet id that is offered by `create_wallet()` method.
71
+ # @param [Boolean] only_finalized - includes only finalized UTXO value if it
72
+ # is true. Default is true.
73
+ # @param [Tapyrus::Color::ColorIdentifier] color_id The color identifier associated with UTXO.
74
+ # It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
75
+ # If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
76
+ # @param [Integer] page - The page parameter is responsible for specifying the current page being viewed within the paginated results. default is 1.
77
+ # @param [Integer] per - The per parameter is used to determine the number of items to display per page. default is 25.
78
+ # @return [Array<Utxo>] The array of the utxos with specified color_id
79
+ def list_unspent_with_count(wallet_id, only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
80
+ raise NotImplementedError, "You must implement #{self.class}##{__method__}"
81
+ end
82
+
68
83
  # Returns the UTXOs that the wallet has.
69
84
  # If label is specified, return UTXOs filtered with label
70
85
  #
@@ -74,6 +89,9 @@ module Glueby
74
89
  # @param [String] label - Label for filtering UTXOs
75
90
  # - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
76
91
  # - If label=:all, it will return all utxos
92
+ # @param [Tapyrus::Color::ColorIdentifier] color_id - The color identifier.
93
+ # It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
94
+ # If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
77
95
  # @return [Array of UTXO]
78
96
  #
79
97
  # ## The UTXO structure
@@ -84,7 +102,7 @@ module Glueby
84
102
  # - finalized: [Boolean] Whether the UTXO is finalized
85
103
  # - color_id: [String] Color id of the UTXO. If it is TPC UTXO, color_id is nil.
86
104
  # - script_pubkey: [String] Script pubkey of the UTXO
87
- def list_unspent(wallet_id, only_finalized = true, label = nil)
105
+ def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
88
106
  raise NotImplementedError, "You must implement #{self.class}##{__method__}"
89
107
  end
90
108
 
@@ -139,7 +157,7 @@ module Glueby
139
157
 
140
158
  # Returns information for the addresses
141
159
  #
142
- # @param [String] address - The p2pkh address to get information about
160
+ # @param [String, Array<String>] addresses - The p2pkh address to get information about
143
161
  # @return [Array<Hash>] The array of hash instance which has keys wallet_id, label and purpose.
144
162
  # Returns blank array if the key correspond with the address is not exist.
145
163
  def get_addresses_info(addresses)
@@ -38,6 +38,7 @@ module Glueby
38
38
  utxo = Utxo.find_or_initialize_by(txid: tx.txid, index: index)
39
39
  utxo.update!(
40
40
  label: key.label,
41
+ color_id: output.script_pubkey.color_id&.to_hex,
41
42
  script_pubkey: output.script_pubkey.to_hex,
42
43
  value: output.value,
43
44
  status: status,
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'securerandom'
4
+ require 'kaminari'
4
5
 
5
6
  module Glueby
6
7
  module Internal
@@ -89,29 +90,18 @@ module Glueby
89
90
  utxos.sum(&:value)
90
91
  end
91
92
 
92
- def list_unspent(wallet_id, only_finalized = true, label = nil)
93
- wallet = AR::Wallet.find_by(wallet_id: wallet_id)
94
- utxos = wallet.utxos.where(locked_at: nil)
95
- utxos = utxos.where(status: :finalized) if only_finalized
96
- if [:unlabeled, nil].include?(label)
97
- utxos = utxos.where(label: nil)
98
- elsif label && (label != :all)
99
- utxos = utxos.where(label: label)
100
- else
101
- utxos
102
- end
93
+ def list_unspent_with_count(wallet_id, only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
94
+ utxos = list_unspent_internal(wallet_id, color_id, only_finalized, label)
95
+ utxos = utxos.page(page).per(per) if per > 0
96
+ {
97
+ count: utxos.total_count,
98
+ outputs: utxos_to_h(utxos)
99
+ }
100
+ end
103
101
 
104
- utxos.map do |utxo|
105
- {
106
- txid: utxo.txid,
107
- vout: utxo.index,
108
- script_pubkey: utxo.script_pubkey,
109
- color_id: utxo.color_id,
110
- amount: utxo.value,
111
- finalized: utxo.status == 'finalized',
112
- label: utxo.label
113
- }
114
- end
102
+ def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
103
+ utxos = list_unspent_internal(wallet_id, color_id, only_finalized, label)
104
+ utxos_to_h(utxos)
115
105
  end
116
106
 
117
107
  def sign_tx(wallet_id, tx, prevtxs = [], sighashtype: Tapyrus::SIGHASH_TYPE[:all])
@@ -120,7 +110,7 @@ module Glueby
120
110
  end
121
111
 
122
112
  def broadcast(wallet_id, tx, &block)
123
- ::ActiveRecord::Base.transaction do
113
+ ::ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
124
114
  AR::Utxo.destroy_for_inputs(tx)
125
115
  AR::Utxo.create_or_update_for_outputs(tx, status: :broadcasted)
126
116
  block.call(tx) if block
@@ -245,6 +235,37 @@ module Glueby
245
235
  key_type: Tapyrus::Key::TYPES[:compressed]
246
236
  )
247
237
  end
238
+
239
+ def list_unspent_internal(wallet_id, color_id = nil, only_finalized = true, label = nil)
240
+ wallet = AR::Wallet.find_by(wallet_id: wallet_id)
241
+ utxos = wallet.utxos.where(locked_at: nil)
242
+ utxos = utxos.where(color_id: color_id.to_hex) if color_id && !color_id.default?
243
+ utxos = utxos.where(color_id: nil) if color_id && color_id.default?
244
+ utxos = utxos.where(status: :finalized) if only_finalized
245
+ utxos = utxos.order(:id)
246
+ utxos = if [:unlabeled, nil].include?(label)
247
+ utxos = utxos.where(label: nil)
248
+ elsif label && (label != :all)
249
+ utxos = utxos.where(label: label)
250
+ else
251
+ utxos
252
+ end
253
+ utxos
254
+ end
255
+
256
+ def utxos_to_h(utxos)
257
+ utxos.map do |utxo|
258
+ {
259
+ txid: utxo.txid,
260
+ vout: utxo.index,
261
+ script_pubkey: utxo.script_pubkey,
262
+ color_id: utxo.color_id,
263
+ amount: utxo.value,
264
+ finalized: utxo.status == 'finalized',
265
+ label: utxo.label
266
+ }
267
+ end
268
+ end
248
269
  end
249
270
  end
250
271
  end
@@ -84,7 +84,7 @@ module Glueby
84
84
 
85
85
  # If label=nil, it will return unlabeled utxos to protect labeled utxos for specific purpose
86
86
  # If label=:all, it will return all utxos
87
- def list_unspent(wallet_id, only_finalized = true, label = nil)
87
+ def list_unspent(wallet_id, only_finalized = true, label = nil, color_id: nil)
88
88
  perform_as(wallet_id) do |client|
89
89
  min_conf = only_finalized ? 1 : 0
90
90
  res = client.listunspent(min_conf)
@@ -97,6 +97,13 @@ module Glueby
97
97
  res
98
98
  end
99
99
 
100
+ if color_id
101
+ res = res.filter do |i|
102
+ script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
103
+ script.cp2pkh? || script.cp2sh? && color_id == Tapyrus::Color::ColorIdentifier.parse_from_payload(script.chunks[0].pushed_data)
104
+ end
105
+ end
106
+
100
107
  res.map do |i|
101
108
  script = Tapyrus::Script.parse_from_payload(i['scriptPubKey'].htb)
102
109
  color_id = if script.cp2pkh? || script.cp2sh?
@@ -177,7 +184,7 @@ module Glueby
177
184
  type = case sighashtype & (~(Tapyrus::SIGHASH_TYPE[:anyonecanpay]))
178
185
  when Tapyrus::SIGHASH_TYPE[:all] then 'ALL'
179
186
  when Tapyrus::SIGHASH_TYPE[:none] then 'NONE'
180
- when Tapyrus::SIGHASH_TYPE[:single] then 'SIGNLE'
187
+ when Tapyrus::SIGHASH_TYPE[:single] then 'SINGLE'
181
188
  else
182
189
  raise Errors::InvalidSighashType, "Invalid sighash type '#{sighashtype}'"
183
190
  end
@@ -93,9 +93,25 @@ module Glueby
93
93
  # @param label [String] This label is used to filtered the UTXOs with labeled if a key or Utxo is labeled.
94
94
  # - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
95
95
  # - If label=:all, all UTXOs will be returned.
96
- def list_unspent(only_finalized = true, label = :unlabeled)
96
+ # @param color_id [Tapyrus::Color::ColorIdentifier] The color identifier associated with UTXO.
97
+ # It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
98
+ # If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
99
+ # @param page [Integer] The page parameter is responsible for specifying the current page being viewed within the paginated results. default is 1.
100
+ # @param per [Integer] The per parameter is used to determine the number of items to display per page. default is 25.
101
+ def list_unspent_with_count(only_finalized = true, label = nil, color_id: nil, page: 1, per: 25)
102
+ wallet_adapter.list_unspent_with_count(id, only_finalized, label, color_id: color_id, page: page, per: per)
103
+ end
104
+
105
+ # @param only_finalized [Boolean] The flag to get a UTXO with status only finalized
106
+ # @param label [String] This label is used to filtered the UTXOs with labeled if a key or Utxo is labeled.
107
+ # - If label is nil or :unlabeled, only unlabeled UTXOs will be returned.
108
+ # - If label=:all, all UTXOs will be returned.
109
+ # @param color_id [Tapyrus::Color::ColorIdentifier] The color identifier associated with UTXO.
110
+ # It will return only UTXOs with specified color_id. If color_id is nil, it will return all UTXOs.
111
+ # If Tapyrus::Color::ColorIdentifier.default is specified, it will return uncolored UTXOs(i.e. TPC)
112
+ def list_unspent(only_finalized = true, label = :unlabeled, color_id: nil )
97
113
  label = :unlabeled unless label
98
- wallet_adapter.list_unspent(id, only_finalized, label)
114
+ wallet_adapter.list_unspent(id, only_finalized, label, color_id: color_id)
99
115
  end
100
116
 
101
117
  def lock_unspent(utxo)
@@ -171,8 +187,7 @@ module Glueby
171
187
  lock_utxos = false,
172
188
  excludes = []
173
189
  )
174
- collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
175
- next false unless output[:color_id].nil?
190
+ collect_utxos(amount, label, Tapyrus::Color::ColorIdentifier.default, only_finalized, shuffle, lock_utxos, excludes) do |output|
176
191
  next yield(output) if block_given?
177
192
 
178
193
  true
@@ -206,8 +221,7 @@ module Glueby
206
221
  lock_utxos = false,
207
222
  excludes = []
208
223
  )
209
- collect_utxos(amount, label, only_finalized, shuffle, lock_utxos, excludes) do |output|
210
- next false unless output[:color_id] == color_id.to_hex
224
+ collect_utxos(amount, label, color_id, only_finalized, shuffle, lock_utxos, excludes) do |output|
211
225
  next yield(output) if block_given?
212
226
 
213
227
  true
@@ -261,7 +275,7 @@ module Glueby
261
275
  while current_amount - fee < target_amount
262
276
  sum, utxos = collect_uncolored_outputs(
263
277
  fee + target_amount - current_amount,
264
- nil, nil, true, true,
278
+ nil, false, true, true,
265
279
  provided_utxos,
266
280
  &block
267
281
  )
@@ -288,7 +302,8 @@ module Glueby
288
302
  def collect_utxos(
289
303
  amount,
290
304
  label,
291
- only_finalized,
305
+ color_id,
306
+ only_finalized = true,
292
307
  shuffle = true,
293
308
  lock_utxos = false,
294
309
  excludes = []
@@ -296,7 +311,7 @@ module Glueby
296
311
  collect_all = amount.nil?
297
312
 
298
313
  raise Glueby::ArgumentError, 'amount must be positive' unless collect_all || amount.positive?
299
- utxos = list_unspent(only_finalized, label)
314
+ utxos = list_unspent(only_finalized, label, color_id: color_id)
300
315
  utxos = utxos.shuffle if shuffle
301
316
 
302
317
  r = utxos.inject([0, []]) do |(sum, outputs), output|
@@ -1,3 +1,3 @@
1
1
  module Glueby
2
- VERSION = "1.2.2"
2
+ VERSION = "1.3.0"
3
3
  end
data/lib/glueby/wallet.rb CHANGED
@@ -46,6 +46,10 @@ module Glueby
46
46
  end
47
47
  end
48
48
 
49
+ def token_utxos(color_id = nil, only_finalized = true, page = 1, per = 25)
50
+ @internal_wallet.list_unspent_with_count(only_finalized, nil, color_id: color_id, page: page, per: per)
51
+ end
52
+
49
53
  private
50
54
 
51
55
  def initialize(internal_wallet)
data/lib/glueby.rb CHANGED
@@ -19,7 +19,7 @@ module Glueby
19
19
 
20
20
  module GluebyLogger
21
21
  def logger
22
- if defined?(Rails)
22
+ if defined?(Rails) && Rails.logger
23
23
  Rails.logger
24
24
  else
25
25
  Logger.new(STDOUT)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: glueby
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.2
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - azuchi
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-09-27 00:00:00.000000000 Z
11
+ date: 2024-01-17 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: tapyrus
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: 7.0.0
41
+ - !ruby/object:Gem::Dependency
42
+ name: kaminari
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: sqlite3
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -173,7 +187,7 @@ metadata:
173
187
  homepage_uri: https://github.com/chaintope/glueby
174
188
  source_code_uri: https://github.com/chaintope/glueby
175
189
  changelog_uri: https://github.com/chaintope/glueby
176
- post_install_message:
190
+ post_install_message:
177
191
  rdoc_options: []
178
192
  require_paths:
179
193
  - lib
@@ -181,15 +195,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
181
195
  requirements:
182
196
  - - ">="
183
197
  - !ruby/object:Gem::Version
184
- version: 2.7.0
198
+ version: 3.0.0
185
199
  required_rubygems_version: !ruby/object:Gem::Requirement
186
200
  requirements:
187
201
  - - ">="
188
202
  - !ruby/object:Gem::Version
189
203
  version: '0'
190
204
  requirements: []
191
- rubygems_version: 3.4.10
192
- signing_key:
205
+ rubygems_version: 3.5.3
206
+ signing_key:
193
207
  specification_version: 4
194
208
  summary: A Ruby library of smart contracts that can be used on Tapyrus.
195
209
  test_files: []