diff --git a/lib/generators/glueby/contract/reissuable_token_generator.rb b/lib/generators/glueby/contract/reissuable_token_generator.rb new file mode 100644 index 00000000..b4a11898 --- /dev/null +++ b/lib/generators/glueby/contract/reissuable_token_generator.rb @@ -0,0 +1,26 @@ +module Glueby + module Contract + class ReissuableTokenGenerator < Rails::Generators::Base + include ::Rails::Generators::Migration + include Glueby::Generator::MigrateGenerator + extend Glueby::Generator::MigrateGenerator::ClassMethod + + source_root File.expand_path('templates', __dir__) + + def create_migration_file + migration_dir = File.expand_path("db/migrate") + + if self.class.migration_exists?(migration_dir, "create_reissuable_token") + ::Kernel.warn "Migration already exists: create_reissuable_token" + else + migration_template( + "reissuable_token_table.rb.erb", + "db/migrate/create_reissuable_token.rb", + migration_version: migration_version, + table_options: table_options, + ) + end + end + end + end +end diff --git a/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb b/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb new file mode 100644 index 00000000..5e6a8e8e --- /dev/null +++ b/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb @@ -0,0 +1,10 @@ +class CreateReissuableToken < ActiveRecord::Migration<%= migration_version %> + def change + create_table :reissuable_tokens<%= table_options %> do |t| + t.string :color_id, null: false + t.string :script_pubkey, null: false + t.timestamps + end + add_index :reissuable_tokens, [:color_id], unique: true + end +end \ No newline at end of file diff --git a/lib/glueby/contract/active_record.rb b/lib/glueby/contract/active_record.rb index ecf0251d..f2fd5b3e 100644 --- a/lib/glueby/contract/active_record.rb +++ b/lib/glueby/contract/active_record.rb @@ -3,6 +3,7 @@ module Glueby module Contract module AR + autoload :ReissuableToken, 'glueby/contract/active_record/reissuable_token' autoload :Timestamp, 'glueby/contract/active_record/timestamp' end end diff --git a/lib/glueby/contract/active_record/reissuable_token.rb b/lib/glueby/contract/active_record/reissuable_token.rb new file mode 100644 index 00000000..eddfc2dd --- /dev/null +++ b/lib/glueby/contract/active_record/reissuable_token.rb @@ -0,0 +1,26 @@ +module Glueby + module Contract + module AR + class ReissuableToken < ::ActiveRecord::Base + + # Get the script_pubkey corresponding to the color_id in Tapyrus::Script format + # @param [String] color_id + # @return [Tapyrus::Script] + def self.script_pubkey(color_id) + script_pubkey = Glueby::Contract::AR::ReissuableToken.where(color_id: color_id).pluck(:script_pubkey).first + if script_pubkey + Tapyrus::Script.parse_from_payload(script_pubkey.htb) + end + end + + # Check if the color_id is already stored + # @param [String] color_id + # @return [Boolean] + def self.saved?(color_id) + Glueby::Contract::AR::ReissuableToken.where(color_id: color_id).exists? + end + + end + end + end +end diff --git a/lib/glueby/contract/token.rb b/lib/glueby/contract/token.rb index 6f56e472..56380915 100644 --- a/lib/glueby/contract/token.rb +++ b/lib/glueby/contract/token.rb @@ -73,7 +73,10 @@ def issue!(issuer:, token_type: Tapyrus::Color::TokenTypes::REISSUABLE, amount: raise Glueby::Contract::Errors::UnsupportedTokenType end txs.each { |tx| issuer.internal_wallet.broadcast(tx) } - [new(color_id: color_id, script_pubkey: script_pubkey), txs] + if token_type == Tapyrus::Color::TokenTypes::REISSUABLE + Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex) + end + [new(color_id: color_id), txs] end private @@ -116,13 +119,16 @@ def issue_nft_token(issuer:) def reissue!(issuer:, amount:) raise Glueby::Contract::Errors::InvalidAmount unless amount.positive? raise Glueby::Contract::Errors::InvalidTokenType unless token_type == Tapyrus::Color::TokenTypes::REISSUABLE - raise Glueby::Contract::Errors::UnknownScriptPubkey unless @script_pubkey - estimated_fee = FixedFeeEstimator.new.fee(Tapyrus::Tx.new) - funding_tx = create_funding_tx(wallet: issuer, amount: estimated_fee, script: @script_pubkey) - tx = create_reissue_tx(funding_tx: funding_tx, issuer: issuer, amount: amount, color_id: color_id) - [funding_tx, tx].each { |tx| issuer.internal_wallet.broadcast(tx) } - [color_id, tx] + if script_pubkey + estimated_fee = FixedFeeEstimator.new.fee(Tapyrus::Tx.new) + funding_tx = create_funding_tx(wallet: issuer, amount: estimated_fee, script: @script_pubkey) + tx = create_reissue_tx(funding_tx: funding_tx, issuer: issuer, amount: amount, color_id: color_id) + [funding_tx, tx].each { |tx| issuer.internal_wallet.broadcast(tx) } + [color_id, tx] + else + raise Glueby::Contract::Errors::UnknownScriptPubkey + end end # Send the token to other wallet @@ -173,12 +179,19 @@ def token_type color_id.type end + # Return the script_pubkey of the token from ActiveRecord + # @return [String] script_pubkey + def script_pubkey + @script_pubkey ||= Glueby::Contract::AR::ReissuableToken.script_pubkey(@color_id.to_hex) + end + # Return serialized payload # @return [String] payload def to_payload payload = +'' payload << @color_id.to_payload - payload << @script_pubkey.to_payload if @script_pubkey + payload << @script_pubkey.to_payload if script_pubkey + payload end # Restore token from payload @@ -187,14 +200,19 @@ def to_payload def self.parse_from_payload(payload) color_id, script_pubkey = payload.unpack('a33a*') color_id = Tapyrus::Color::ColorIdentifier.parse_from_payload(color_id) - script_pubkey = Tapyrus::Script.parse_from_payload(script_pubkey) if script_pubkey - new(color_id: color_id, script_pubkey: script_pubkey) + if color_id.type == Tapyrus::Color::TokenTypes::REISSUABLE + raise ArgumentError, 'script_pubkey should not be empty' if script_pubkey.empty? + script_pubkey = Tapyrus::Script.parse_from_payload(script_pubkey) + Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex) + end + new(color_id: color_id) end - def initialize(color_id:, script_pubkey:nil) + # Generate Token Instance + # @param color_id [String] + def initialize(color_id:) @color_id = color_id - @script_pubkey = script_pubkey end end end -end +end \ No newline at end of file diff --git a/spec/glueby/contract/token_spec.rb b/spec/glueby/contract/token_spec.rb index a4a3ee95..00411d25 100644 --- a/spec/glueby/contract/token_spec.rb +++ b/spec/glueby/contract/token_spec.rb @@ -1,6 +1,6 @@ # frozen_string_literal: true -RSpec.describe 'Glueby::Contract::Token' do +RSpec.describe 'Glueby::Contract::Token', active_record: true do let(:wallet) { TestWallet.new(internal_wallet) } let(:internal_wallet) { TestInternalWallet.new } let(:unspents) do @@ -69,12 +69,28 @@ let(:token_type) { Tapyrus::Color::TokenTypes::REISSUABLE } let(:amount) { 1_000 } - it { - expect {subject}.not_to raise_error - expect(subject[0].color_id.type).to eq Tapyrus::Color::TokenTypes::REISSUABLE - expect(subject[0].color_id.valid?).to be true - expect(subject[1][1].valid?).to be true - } + context 'reissuable token' do + it do + expect {subject}.not_to raise_error + expect(subject[0].color_id.type).to eq Tapyrus::Color::TokenTypes::REISSUABLE + expect(subject[0].color_id.valid?).to be true + expect(subject[1][1].valid?).to be true + expect(Glueby::Contract::AR::ReissuableToken.count).to eq 1 + expect(subject[0].color_id.to_hex).to eq Glueby::Contract::AR::ReissuableToken.find(1).color_id + expect(subject[1][0].outputs.first.script_pubkey.to_hex).to eq Glueby::Contract::AR::ReissuableToken.find_by(color_id: subject[0].color_id.to_hex).script_pubkey + end + end + + context 'non reissuable token' do + let(:token_type) { Tapyrus::Color::TokenTypes::NON_REISSUABLE } + it do + expect {subject}.not_to raise_error + expect(subject[0].color_id.type).to eq Tapyrus::Color::TokenTypes::NON_REISSUABLE + expect(subject[0].color_id.valid?).to be true + expect(subject[1][0].valid?).to be true + expect(Glueby::Contract::AR::ReissuableToken.count).to eq 0 + end + end context 'invalid amount' do let(:amount) { 0 } @@ -272,7 +288,7 @@ context 'with no script pubkey' do let(:token) { Glueby::Contract::Token.parse_from_payload('c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3'.htb) } - it { is_expected.to eq 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' } + it { expect { subject }.to raise_error(ArgumentError, 'script_pubkey should not be empty') } end end @@ -281,12 +297,18 @@ let(:token) { Glueby::Contract::Token.parse_from_payload('c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb376a914234113b860822e68f9715d1957af28b8f5117ee288ac'.htb) } - it { is_expected.to eq 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb376a914234113b860822e68f9715d1957af28b8f5117ee288ac' } + it do + expect(subject).to eq 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb376a914234113b860822e68f9715d1957af28b8f5117ee288ac' + expect(Glueby::Contract::AR::ReissuableToken.count).to eq 1 + end context 'with no script pubkey' do let(:token) { Glueby::Contract::Token.parse_from_payload('c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3'.htb) } - it { is_expected.to eq 'c150ad685ec8638543b2356cb1071cf834fb1c84f5fa3a71699c3ed7167dfcdbb3' } + it do + expect{ subject } .to raise_error(ArgumentError, 'script_pubkey should not be empty') + expect(Glueby::Contract::AR::ReissuableToken.count).to eq 0 + end end end end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index 6a07667f..73993fe3 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -84,6 +84,13 @@ def setup_database end connection.add_index :system_informations, [:info_key], unique: true Glueby::AR::SystemInformation.create(info_key: 'synced_block_number', info_value: '0') + + connection.create_table :reissuable_tokens, force: true do |t| + t.string :color_id, null: false + t.string :script_pubkey, null: false + t.timestamps + end + connection.add_index :reissuable_tokens, [:color_id], unique: true end def teardown_database @@ -93,6 +100,7 @@ def teardown_database connection.drop_table :keys, if_exists: true connection.drop_table :timestamps, if_exists: true connection.drop_table :system_informations, if_exists: true + connection.drop_table :reissuable_tokens, if_exists: true end class TestWallet