glueby 0.2.0 → 0.4.2
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 +4 -4
- data/.github/workflows/ruby.yml +35 -0
- data/Gemfile +1 -0
- data/README.md +224 -16
- data/glueby.gemspec +2 -2
- data/lib/generators/glueby/contract/block_syncer_generator.rb +26 -0
- data/lib/generators/glueby/contract/reissuable_token_generator.rb +26 -0
- data/lib/generators/glueby/contract/templates/initializer.rb.erb +4 -2
- data/lib/generators/glueby/contract/templates/key_table.rb.erb +4 -3
- data/lib/generators/glueby/contract/templates/reissuable_token_table.rb.erb +10 -0
- data/lib/generators/glueby/contract/templates/system_information_table.rb.erb +12 -0
- data/lib/generators/glueby/contract/templates/timestamp_table.rb.erb +1 -1
- data/lib/generators/glueby/contract/templates/utxo_table.rb.erb +3 -2
- data/lib/generators/glueby/contract/templates/wallet_table.rb.erb +2 -2
- data/lib/glueby.rb +28 -11
- data/lib/glueby/active_record.rb +8 -0
- data/lib/glueby/active_record/system_information.rb +15 -0
- data/lib/glueby/block_syncer.rb +98 -0
- data/lib/glueby/configuration.rb +62 -0
- data/lib/glueby/contract.rb +2 -2
- data/lib/glueby/contract/active_record.rb +1 -0
- data/lib/glueby/contract/active_record/reissuable_token.rb +26 -0
- data/lib/glueby/contract/fee_estimator.rb +38 -0
- data/lib/glueby/contract/payment.rb +10 -6
- data/lib/glueby/contract/timestamp.rb +8 -6
- data/lib/glueby/contract/timestamp/syncer.rb +13 -0
- data/lib/glueby/contract/token.rb +78 -26
- data/lib/glueby/contract/tx_builder.rb +23 -20
- data/lib/glueby/fee_provider.rb +73 -0
- data/lib/glueby/fee_provider/tasks.rb +141 -0
- data/lib/glueby/generator/migrate_generator.rb +1 -1
- data/lib/glueby/internal/wallet.rb +56 -13
- data/lib/glueby/internal/wallet/abstract_wallet_adapter.rb +25 -6
- data/lib/glueby/internal/wallet/active_record/utxo.rb +1 -0
- data/lib/glueby/internal/wallet/active_record/wallet.rb +15 -5
- data/lib/glueby/internal/wallet/active_record_wallet_adapter.rb +25 -8
- data/lib/glueby/internal/wallet/active_record_wallet_adapter/syncer.rb +14 -0
- data/lib/glueby/internal/wallet/errors.rb +3 -0
- data/lib/glueby/internal/wallet/tapyrus_core_wallet_adapter.rb +42 -14
- data/lib/glueby/railtie.rb +14 -0
- data/lib/glueby/version.rb +1 -1
- data/lib/glueby/wallet.rb +3 -2
- data/lib/tasks/glueby/block_syncer.rake +29 -0
- data/lib/tasks/glueby/contract/timestamp.rake +4 -26
- data/lib/tasks/glueby/fee_provider.rake +18 -0
- metadata +26 -11
- data/.travis.yml +0 -7
- data/lib/glueby/contract/fee_provider.rb +0 -21
- data/lib/tasks/glueby/contract/wallet_adapter.rake +0 -42
| @@ -0,0 +1,13 @@ | |
| 1 | 
            +
            module Glueby
         | 
| 2 | 
            +
              module Contract
         | 
| 3 | 
            +
                class Timestamp
         | 
| 4 | 
            +
                  class Syncer
         | 
| 5 | 
            +
                    def block_sync(block)
         | 
| 6 | 
            +
                      Glueby::Contract::AR::Timestamp
         | 
| 7 | 
            +
                        .where(txid: block.transactions.map(&:txid), status: :unconfirmed)
         | 
| 8 | 
            +
                        .update_all(status: :confirmed)
         | 
| 9 | 
            +
                    end
         | 
| 10 | 
            +
                  end
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
            end
         | 
| @@ -1,4 +1,5 @@ | |
| 1 1 | 
             
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'active_record'
         | 
| 2 3 |  | 
| 3 4 | 
             
            module Glueby
         | 
| 4 5 | 
             
              module Contract
         | 
| @@ -13,13 +14,17 @@ module Glueby | |
| 13 14 | 
             
                # alice = Glueby::Wallet.create
         | 
| 14 15 | 
             
                # bob = Glueby::Wallet.create
         | 
| 15 16 | 
             
                #
         | 
| 17 | 
            +
                # Use `Glueby::Internal::Wallet#receive_address` to generate the address of bob
         | 
| 18 | 
            +
                # bob.internal_wallet.receive_address
         | 
| 19 | 
            +
                # => '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a'
         | 
| 20 | 
            +
                #
         | 
| 16 21 | 
             
                # Issue
         | 
| 17 22 | 
             
                # token = Token.issue!(issuer: alice, amount: 100)
         | 
| 18 23 | 
             
                # token.amount(wallet: alice)
         | 
| 19 24 | 
             
                # => 100
         | 
| 20 25 | 
             
                #
         | 
| 21 26 | 
             
                # Send
         | 
| 22 | 
            -
                # token.transfer!(sender: alice,  | 
| 27 | 
            +
                # token.transfer!(sender: alice, receiver_address: '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a', amount: 1)
         | 
| 23 28 | 
             
                # token.amount(wallet: alice)
         | 
| 24 29 | 
             
                # => 99
         | 
| 25 30 | 
             
                # token.amount(wallet: bob)
         | 
| @@ -51,14 +56,14 @@ module Glueby | |
| 51 56 | 
             
                    # @param issuer [Glueby::Wallet]
         | 
| 52 57 | 
             
                    # @param token_type [TokenTypes]
         | 
| 53 58 | 
             
                    # @param amount [Integer]
         | 
| 54 | 
            -
                    # @return [ | 
| 59 | 
            +
                    # @return [Array<token, Array<tx>>] Tuple of tx array and token object
         | 
| 55 60 | 
             
                    # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
         | 
| 56 61 | 
             
                    # @raise [InvalidAmount] if amount is not positive integer.
         | 
| 57 62 | 
             
                    # @raise [UnspportedTokenType] if token is not supported.
         | 
| 58 63 | 
             
                    def issue!(issuer:, token_type: Tapyrus::Color::TokenTypes::REISSUABLE, amount: 1)
         | 
| 59 64 | 
             
                      raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
         | 
| 60 65 |  | 
| 61 | 
            -
                      txs, color_id | 
| 66 | 
            +
                      txs, color_id = case token_type
         | 
| 62 67 | 
             
                                     when Tapyrus::Color::TokenTypes::REISSUABLE
         | 
| 63 68 | 
             
                                       issue_reissuable_token(issuer: issuer, amount: amount)
         | 
| 64 69 | 
             
                                     when Tapyrus::Color::TokenTypes::NON_REISSUABLE
         | 
| @@ -68,33 +73,44 @@ module Glueby | |
| 68 73 | 
             
                                     else
         | 
| 69 74 | 
             
                                       raise Glueby::Contract::Errors::UnsupportedTokenType
         | 
| 70 75 | 
             
                                     end
         | 
| 71 | 
            -
             | 
| 72 | 
            -
                      new(color_id: color_id,  | 
| 76 | 
            +
             | 
| 77 | 
            +
                      [new(color_id: color_id), txs]
         | 
| 73 78 | 
             
                    end
         | 
| 74 79 |  | 
| 75 80 | 
             
                    private
         | 
| 76 81 |  | 
| 77 82 | 
             
                    def issue_reissuable_token(issuer:, amount:)
         | 
| 78 | 
            -
                       | 
| 79 | 
            -
                      funding_tx = create_funding_tx(wallet: issuer, amount: estimated_fee)
         | 
| 80 | 
            -
                      tx = create_issue_tx_for_reissuable_token(funding_tx: funding_tx, issuer: issuer, amount: amount)
         | 
| 83 | 
            +
                      funding_tx = create_funding_tx(wallet: issuer)
         | 
| 81 84 | 
             
                      script_pubkey = funding_tx.outputs.first.script_pubkey
         | 
| 82 85 | 
             
                      color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
         | 
| 83 | 
            -
             | 
| 86 | 
            +
             | 
| 87 | 
            +
                      ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
         | 
| 88 | 
            +
                        # Store the script_pubkey for reissue the token.
         | 
| 89 | 
            +
                        Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex)
         | 
| 90 | 
            +
             | 
| 91 | 
            +
                        funding_tx = issuer.internal_wallet.broadcast(funding_tx)
         | 
| 92 | 
            +
                        tx = create_issue_tx_for_reissuable_token(funding_tx: funding_tx, issuer: issuer, amount: amount)
         | 
| 93 | 
            +
                        tx = issuer.internal_wallet.broadcast(tx)
         | 
| 94 | 
            +
                        [[funding_tx, tx], color_id]
         | 
| 95 | 
            +
                      end
         | 
| 84 96 | 
             
                    end
         | 
| 85 97 |  | 
| 86 98 | 
             
                    def issue_non_reissuable_token(issuer:, amount:)
         | 
| 87 99 | 
             
                      tx = create_issue_tx_for_non_reissuable_token(issuer: issuer, amount: amount)
         | 
| 100 | 
            +
                      tx = issuer.internal_wallet.broadcast(tx)
         | 
| 101 | 
            +
             | 
| 88 102 | 
             
                      out_point = tx.inputs.first.out_point
         | 
| 89 103 | 
             
                      color_id = Tapyrus::Color::ColorIdentifier.non_reissuable(out_point)
         | 
| 90 | 
            -
                      [[tx], color_id | 
| 104 | 
            +
                      [[tx], color_id]
         | 
| 91 105 | 
             
                    end
         | 
| 92 106 |  | 
| 93 107 | 
             
                    def issue_nft_token(issuer:)
         | 
| 94 108 | 
             
                      tx = create_issue_tx_for_nft_token(issuer: issuer)
         | 
| 109 | 
            +
                      tx = issuer.internal_wallet.broadcast(tx)
         | 
| 110 | 
            +
             | 
| 95 111 | 
             
                      out_point = tx.inputs.first.out_point
         | 
| 96 112 | 
             
                      color_id = Tapyrus::Color::ColorIdentifier.nft(out_point)
         | 
| 97 | 
            -
                      [[tx], color_id | 
| 113 | 
            +
                      [[tx], color_id]
         | 
| 98 114 | 
             
                    end
         | 
| 99 115 | 
             
                  end
         | 
| 100 116 |  | 
| @@ -104,6 +120,7 @@ module Glueby | |
| 104 120 | 
             
                  # A wallet can issue the token only when it is REISSUABLE token.
         | 
| 105 121 | 
             
                  # @param issuer [Glueby::Wallet]
         | 
| 106 122 | 
             
                  # @param amount [Integer]
         | 
| 123 | 
            +
                  # @return [Array<String, tx>] Tuple of color_id and tx object
         | 
| 107 124 | 
             
                  # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
         | 
| 108 125 | 
             
                  # @raise [InvalidAmount] if amount is not positive integer.
         | 
| 109 126 | 
             
                  # @raise [InvalidTokenType] if token is not reissuable.
         | 
| @@ -111,28 +128,34 @@ module Glueby | |
| 111 128 | 
             
                  def reissue!(issuer:, amount:)
         | 
| 112 129 | 
             
                    raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
         | 
| 113 130 | 
             
                    raise Glueby::Contract::Errors::InvalidTokenType unless token_type == Tapyrus::Color::TokenTypes::REISSUABLE
         | 
| 114 | 
            -
                    raise Glueby::Contract::Errors::UnknownScriptPubkey unless @script_pubkey
         | 
| 115 131 |  | 
| 116 | 
            -
                     | 
| 117 | 
            -
             | 
| 118 | 
            -
             | 
| 119 | 
            -
             | 
| 132 | 
            +
                    if validate_reissuer(wallet: issuer)
         | 
| 133 | 
            +
                      funding_tx = create_funding_tx(wallet: issuer, script: @script_pubkey)
         | 
| 134 | 
            +
                      funding_tx = issuer.internal_wallet.broadcast(funding_tx)
         | 
| 135 | 
            +
                      tx = create_reissue_tx(funding_tx: funding_tx, issuer: issuer, amount: amount, color_id: color_id)
         | 
| 136 | 
            +
                      tx = issuer.internal_wallet.broadcast(tx)
         | 
| 137 | 
            +
             | 
| 138 | 
            +
                      [color_id, tx]
         | 
| 139 | 
            +
                    else
         | 
| 140 | 
            +
                      raise Glueby::Contract::Errors::UnknownScriptPubkey
         | 
| 141 | 
            +
                    end
         | 
| 120 142 | 
             
                  end
         | 
| 121 143 |  | 
| 122 144 | 
             
                  # Send the token to other wallet
         | 
| 123 145 | 
             
                  #
         | 
| 124 146 | 
             
                  # @param sender [Glueby::Wallet] wallet to send this token
         | 
| 125 | 
            -
                  # @param  | 
| 147 | 
            +
                  # @param receiver_address [String] address to receive this token
         | 
| 126 148 | 
             
                  # @param amount [Integer]
         | 
| 127 | 
            -
                  # @return [ | 
| 149 | 
            +
                  # @return [Array<String, tx>] Tuple of color_id and tx object
         | 
| 128 150 | 
             
                  # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
         | 
| 129 151 | 
             
                  # @raise [InsufficientTokens] if wallet does not have enough token to send.
         | 
| 130 152 | 
             
                  # @raise [InvalidAmount] if amount is not positive integer.
         | 
| 131 | 
            -
                  def transfer!(sender:,  | 
| 153 | 
            +
                  def transfer!(sender:, receiver_address:, amount: 1)
         | 
| 132 154 | 
             
                    raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
         | 
| 133 155 |  | 
| 134 | 
            -
                    tx = create_transfer_tx(color_id: color_id, sender: sender,  | 
| 156 | 
            +
                    tx = create_transfer_tx(color_id: color_id, sender: sender, receiver_address: receiver_address, amount: amount)
         | 
| 135 157 | 
             
                    sender.internal_wallet.broadcast(tx)
         | 
| 158 | 
            +
                    [color_id, tx]
         | 
| 136 159 | 
             
                  end
         | 
| 137 160 |  | 
| 138 161 | 
             
                  # Burn token
         | 
| @@ -166,12 +189,19 @@ module Glueby | |
| 166 189 | 
             
                    color_id.type
         | 
| 167 190 | 
             
                  end
         | 
| 168 191 |  | 
| 192 | 
            +
                  # Return the script_pubkey of the token from ActiveRecord
         | 
| 193 | 
            +
                  # @return [String] script_pubkey
         | 
| 194 | 
            +
                  def script_pubkey
         | 
| 195 | 
            +
                    @script_pubkey ||= Glueby::Contract::AR::ReissuableToken.script_pubkey(@color_id.to_hex)
         | 
| 196 | 
            +
                  end
         | 
| 197 | 
            +
             | 
| 169 198 | 
             
                  # Return serialized payload
         | 
| 170 199 | 
             
                  # @return [String] payload
         | 
| 171 200 | 
             
                  def to_payload
         | 
| 172 201 | 
             
                    payload = +''
         | 
| 173 202 | 
             
                    payload << @color_id.to_payload
         | 
| 174 | 
            -
                    payload << @script_pubkey.to_payload if  | 
| 203 | 
            +
                    payload << @script_pubkey.to_payload if script_pubkey
         | 
| 204 | 
            +
                    payload
         | 
| 175 205 | 
             
                  end
         | 
| 176 206 |  | 
| 177 207 | 
             
                  # Restore token from payload
         | 
| @@ -180,14 +210,36 @@ module Glueby | |
| 180 210 | 
             
                  def self.parse_from_payload(payload)
         | 
| 181 211 | 
             
                    color_id, script_pubkey = payload.unpack('a33a*')
         | 
| 182 212 | 
             
                    color_id = Tapyrus::Color::ColorIdentifier.parse_from_payload(color_id)
         | 
| 183 | 
            -
                     | 
| 184 | 
            -
             | 
| 213 | 
            +
                    if color_id.type == Tapyrus::Color::TokenTypes::REISSUABLE
         | 
| 214 | 
            +
                      raise ArgumentError, 'script_pubkey should not be empty' if script_pubkey.empty?
         | 
| 215 | 
            +
                      script_pubkey = Tapyrus::Script.parse_from_payload(script_pubkey)
         | 
| 216 | 
            +
                      Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex)
         | 
| 217 | 
            +
                    end
         | 
| 218 | 
            +
                    new(color_id: color_id)
         | 
| 185 219 | 
             
                  end
         | 
| 186 220 |  | 
| 187 | 
            -
                   | 
| 221 | 
            +
                  # Generate Token Instance
         | 
| 222 | 
            +
                  # @param color_id [String]
         | 
| 223 | 
            +
                  def initialize(color_id:)
         | 
| 188 224 | 
             
                    @color_id = color_id
         | 
| 189 | 
            -
             | 
| 225 | 
            +
                  end
         | 
| 226 | 
            +
             | 
| 227 | 
            +
                  private
         | 
| 228 | 
            +
             | 
| 229 | 
            +
                  # Verify that wallet is the issuer of the reissuable token
         | 
| 230 | 
            +
                  # reutrn [Boolean]
         | 
| 231 | 
            +
                  def validate_reissuer(wallet:)
         | 
| 232 | 
            +
                    addresses = wallet.internal_wallet.get_addresses
         | 
| 233 | 
            +
                    addresses.each do |address|
         | 
| 234 | 
            +
                      decoded_address = Tapyrus.decode_base58_address(address)
         | 
| 235 | 
            +
                      pubkey_hash_from_address = decoded_address[0]
         | 
| 236 | 
            +
                      pubkey_hash_from_script = Tapyrus::Script.parse_from_payload(script_pubkey.chunks[2])
         | 
| 237 | 
            +
                      if pubkey_hash_from_address == pubkey_hash_from_script.to_s
         | 
| 238 | 
            +
                        return true
         | 
| 239 | 
            +
                      end
         | 
| 240 | 
            +
                    end
         | 
| 241 | 
            +
                    false
         | 
| 190 242 | 
             
                  end
         | 
| 191 243 | 
             
                end
         | 
| 192 244 | 
             
              end
         | 
| 193 | 
            -
            end
         | 
| 245 | 
            +
            end
         | 
| @@ -3,26 +3,29 @@ | |
| 3 3 | 
             
            module Glueby
         | 
| 4 4 | 
             
              module Contract
         | 
| 5 5 | 
             
                module TxBuilder
         | 
| 6 | 
            +
                  # The amount of output in funding tx for Reissuable token.
         | 
| 7 | 
            +
                  FUNDING_TX_AMOUNT = 10_000
         | 
| 8 | 
            +
             | 
| 6 9 | 
             
                  def receive_address(wallet:)
         | 
| 7 10 | 
             
                    wallet.receive_address
         | 
| 8 11 | 
             
                  end
         | 
| 9 12 |  | 
| 10 13 | 
             
                  # Create new public key, and new transaction that sends TPC to it
         | 
| 11 | 
            -
                  def create_funding_tx(wallet:,  | 
| 14 | 
            +
                  def create_funding_tx(wallet:, script: nil, fee_estimator: FixedFeeEstimator.new)
         | 
| 12 15 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 13 | 
            -
                    fee =  | 
| 16 | 
            +
                    fee = fee_estimator.fee(dummy_tx(tx))
         | 
| 14 17 |  | 
| 15 | 
            -
                    sum, outputs = wallet.internal_wallet.collect_uncolored_outputs(fee +  | 
| 18 | 
            +
                    sum, outputs = wallet.internal_wallet.collect_uncolored_outputs(fee + FUNDING_TX_AMOUNT)
         | 
| 16 19 | 
             
                    fill_input(tx, outputs)
         | 
| 17 20 |  | 
| 18 21 | 
             
                    receiver_script = script ? script : Tapyrus::Script.parse_from_addr(wallet.internal_wallet.receive_address)
         | 
| 19 | 
            -
                    tx.outputs << Tapyrus::TxOut.new(value:  | 
| 22 | 
            +
                    tx.outputs << Tapyrus::TxOut.new(value: FUNDING_TX_AMOUNT, script_pubkey: receiver_script)
         | 
| 20 23 |  | 
| 21 | 
            -
                    fill_change_tpc(tx, wallet, sum - fee -  | 
| 24 | 
            +
                    fill_change_tpc(tx, wallet, sum - fee - FUNDING_TX_AMOUNT)
         | 
| 22 25 | 
             
                    wallet.internal_wallet.sign_tx(tx)
         | 
| 23 26 | 
             
                  end
         | 
| 24 27 |  | 
| 25 | 
            -
                  def create_issue_tx_for_reissuable_token(funding_tx:, issuer:, amount:,  | 
| 28 | 
            +
                  def create_issue_tx_for_reissuable_token(funding_tx:, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
         | 
| 26 29 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 27 30 |  | 
| 28 31 | 
             
                    out_point = Tapyrus::OutPoint.from_txid(funding_tx.txid, 0)
         | 
| @@ -34,7 +37,7 @@ module Glueby | |
| 34 37 | 
             
                    receiver_colored_script = receiver_script.add_color(color_id)
         | 
| 35 38 | 
             
                    tx.outputs << Tapyrus::TxOut.new(value: amount, script_pubkey: receiver_colored_script)
         | 
| 36 39 |  | 
| 37 | 
            -
                    fee =  | 
| 40 | 
            +
                    fee = fee_estimator.fee(dummy_tx(tx))
         | 
| 38 41 | 
             
                    fill_change_tpc(tx, issuer, output.value - fee)
         | 
| 39 42 | 
             
                    prev_txs = [{
         | 
| 40 43 | 
             
                      txid: funding_tx.txid,
         | 
| @@ -45,18 +48,18 @@ module Glueby | |
| 45 48 | 
             
                    issuer.internal_wallet.sign_tx(tx, prev_txs)
         | 
| 46 49 | 
             
                  end
         | 
| 47 50 |  | 
| 48 | 
            -
                  def create_issue_tx_for_non_reissuable_token(issuer:, amount:,  | 
| 49 | 
            -
                    create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NON_REISSUABLE, issuer: issuer, amount: amount,  | 
| 51 | 
            +
                  def create_issue_tx_for_non_reissuable_token(issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
         | 
| 52 | 
            +
                    create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NON_REISSUABLE, issuer: issuer, amount: amount, fee_estimator: fee_estimator)
         | 
| 50 53 | 
             
                  end
         | 
| 51 54 |  | 
| 52 | 
            -
                  def create_issue_tx_for_nft_token(issuer:,  | 
| 53 | 
            -
                    create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NFT, issuer: issuer, amount: 1,  | 
| 55 | 
            +
                  def create_issue_tx_for_nft_token(issuer:, fee_estimator: FixedFeeEstimator.new)
         | 
| 56 | 
            +
                    create_issue_tx_from_out_point(token_type: Tapyrus::Color::TokenTypes::NFT, issuer: issuer, amount: 1, fee_estimator: fee_estimator)
         | 
| 54 57 | 
             
                  end
         | 
| 55 58 |  | 
| 56 | 
            -
                  def create_issue_tx_from_out_point(token_type:, issuer:, amount:,  | 
| 59 | 
            +
                  def create_issue_tx_from_out_point(token_type:, issuer:, amount:, fee_estimator: FixedFeeEstimator.new)
         | 
| 57 60 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 58 61 |  | 
| 59 | 
            -
                    fee =  | 
| 62 | 
            +
                    fee = fee_estimator.fee(dummy_issue_tx_from_out_point)
         | 
| 60 63 | 
             
                    sum, outputs = issuer.internal_wallet.collect_uncolored_outputs(fee)
         | 
| 61 64 | 
             
                    fill_input(tx, outputs)
         | 
| 62 65 |  | 
| @@ -78,7 +81,7 @@ module Glueby | |
| 78 81 | 
             
                    issuer.internal_wallet.sign_tx(tx)
         | 
| 79 82 | 
             
                  end
         | 
| 80 83 |  | 
| 81 | 
            -
                  def create_reissue_tx(funding_tx:, issuer:, amount:, color_id:,  | 
| 84 | 
            +
                  def create_reissue_tx(funding_tx:, issuer:, amount:, color_id:, fee_estimator: FixedFeeEstimator.new)
         | 
| 82 85 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 83 86 |  | 
| 84 87 | 
             
                    out_point = Tapyrus::OutPoint.from_txid(funding_tx.txid, 0)
         | 
| @@ -89,7 +92,7 @@ module Glueby | |
| 89 92 | 
             
                    receiver_colored_script = receiver_script.add_color(color_id)
         | 
| 90 93 | 
             
                    tx.outputs << Tapyrus::TxOut.new(value: amount, script_pubkey: receiver_colored_script)
         | 
| 91 94 |  | 
| 92 | 
            -
                    fee =  | 
| 95 | 
            +
                    fee = fee_estimator.fee(dummy_tx(tx))
         | 
| 93 96 | 
             
                    fill_change_tpc(tx, issuer, output.value - fee)
         | 
| 94 97 | 
             
                    prev_txs = [{
         | 
| 95 98 | 
             
                      txid: funding_tx.txid,
         | 
| @@ -100,20 +103,20 @@ module Glueby | |
| 100 103 | 
             
                    issuer.internal_wallet.sign_tx(tx, prev_txs)
         | 
| 101 104 | 
             
                  end
         | 
| 102 105 |  | 
| 103 | 
            -
                  def create_transfer_tx(color_id:, sender:,  | 
| 106 | 
            +
                  def create_transfer_tx(color_id:, sender:, receiver_address:, amount:, fee_estimator: FixedFeeEstimator.new)
         | 
| 104 107 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 105 108 |  | 
| 106 109 | 
             
                    utxos = sender.internal_wallet.list_unspent
         | 
| 107 110 | 
             
                    sum_token, outputs = collect_colored_outputs(utxos, color_id, amount)
         | 
| 108 111 | 
             
                    fill_input(tx, outputs)
         | 
| 109 112 |  | 
| 110 | 
            -
                    receiver_script = Tapyrus::Script.parse_from_addr( | 
| 113 | 
            +
                    receiver_script = Tapyrus::Script.parse_from_addr(receiver_address)
         | 
| 111 114 | 
             
                    receiver_colored_script = receiver_script.add_color(color_id)
         | 
| 112 115 | 
             
                    tx.outputs << Tapyrus::TxOut.new(value: amount, script_pubkey: receiver_colored_script)
         | 
| 113 116 |  | 
| 114 117 | 
             
                    fill_change_token(tx, sender, sum_token - amount, color_id)
         | 
| 115 118 |  | 
| 116 | 
            -
                    fee =  | 
| 119 | 
            +
                    fee = fee_estimator.fee(dummy_tx(tx))
         | 
| 117 120 | 
             
                    sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee)
         | 
| 118 121 | 
             
                    fill_input(tx, outputs)
         | 
| 119 122 |  | 
| @@ -121,7 +124,7 @@ module Glueby | |
| 121 124 | 
             
                    sender.internal_wallet.sign_tx(tx)
         | 
| 122 125 | 
             
                  end
         | 
| 123 126 |  | 
| 124 | 
            -
                  def create_burn_tx(color_id:, sender:, amount: 0,  | 
| 127 | 
            +
                  def create_burn_tx(color_id:, sender:, amount: 0, fee_estimator: FixedFeeEstimator.new)
         | 
| 125 128 | 
             
                    tx = Tapyrus::Tx.new
         | 
| 126 129 |  | 
| 127 130 | 
             
                    utxos = sender.internal_wallet.list_unspent
         | 
| @@ -130,7 +133,7 @@ module Glueby | |
| 130 133 |  | 
| 131 134 | 
             
                    fill_change_token(tx, sender, sum_token - amount, color_id) if amount.positive?
         | 
| 132 135 |  | 
| 133 | 
            -
                    fee =  | 
| 136 | 
            +
                    fee = fee_estimator.fee(dummy_tx(tx))
         | 
| 134 137 |  | 
| 135 138 | 
             
                    dust = 600 # in case that the wallet has output which has just fee amount.
         | 
| 136 139 | 
             
                    sum_tpc, outputs = sender.internal_wallet.collect_uncolored_outputs(fee + dust)
         | 
| @@ -0,0 +1,73 @@ | |
| 1 | 
            +
            module Glueby
         | 
| 2 | 
            +
              class FeeProvider
         | 
| 3 | 
            +
             | 
| 4 | 
            +
                autoload :Tasks, 'glueby/fee_provider/tasks'
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                class NoUtxosInUtxoPool < StandardError; end
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                WALLET_ID = 'FEE_PROVIDER_WALLET'
         | 
| 9 | 
            +
                DEFAULT_FIXED_FEE = 1000
         | 
| 10 | 
            +
                DEFAULT_UTXO_POOL_SIZE = 20
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                attr_reader :fixed_fee, :utxo_pool_size, :wallet,
         | 
| 13 | 
            +
             | 
| 14 | 
            +
                class << self
         | 
| 15 | 
            +
                  attr_reader :config
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  # @param [Hash] config
         | 
| 18 | 
            +
                  # @option config [Integer] :fixed_fee The amount of fee which FeeProvider would provide to the txs
         | 
| 19 | 
            +
                  # @option opts [Integer] :utxo_pool_size The number of UTXOs in UTXO pool that is managed by FeeProvider
         | 
| 20 | 
            +
                  def configure(config)
         | 
| 21 | 
            +
                    @config = config
         | 
| 22 | 
            +
                  end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  def provide(tx)
         | 
| 25 | 
            +
                    new.provide(tx)
         | 
| 26 | 
            +
                  end
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                def initialize
         | 
| 30 | 
            +
                  @wallet = begin
         | 
| 31 | 
            +
                              Internal::Wallet.load(WALLET_ID)
         | 
| 32 | 
            +
                            rescue Internal::Wallet::Errors::WalletNotFound => _
         | 
| 33 | 
            +
                              Internal::Wallet.create(WALLET_ID)
         | 
| 34 | 
            +
                            end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                  @fixed_fee = (FeeProvider.config && FeeProvider.config[:fixed_fee]) || DEFAULT_FIXED_FEE
         | 
| 37 | 
            +
                  @utxo_pool_size = (FeeProvider.config && FeeProvider.config[:utxo_pool_size]) || DEFAULT_UTXO_POOL_SIZE
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # Provide an input for fee to the tx.
         | 
| 41 | 
            +
                # @param [Tapyrus::Tx] tx - The tx that is provided fee as a input. It should be signed with ANYONECANPAY flag.
         | 
| 42 | 
            +
                # @return [Tapyrus::Tx]
         | 
| 43 | 
            +
                # @raise [ArgumentError] If the signatures that the tx inputs has don't have ANYONECANPAY flag.
         | 
| 44 | 
            +
                # @raise [Glueby::FeeProvider::NoUtxosInUtxoPool] If there are no UTXOs for paying fee in FeeProvider's UTXO pool
         | 
| 45 | 
            +
                def provide(tx)
         | 
| 46 | 
            +
                  tx.inputs.each do |txin|
         | 
| 47 | 
            +
                    sig = get_signature(txin.script_sig)
         | 
| 48 | 
            +
                    unless sig[-1].unpack1('C') & Tapyrus::SIGHASH_TYPE[:anyonecanpay] == Tapyrus::SIGHASH_TYPE[:anyonecanpay]
         | 
| 49 | 
            +
                      raise ArgumentError, 'All the signatures that the tx inputs has should have ANYONECANPAY flag.'
         | 
| 50 | 
            +
                    end
         | 
| 51 | 
            +
                  end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                  utxo = utxo_for_fee
         | 
| 54 | 
            +
                  out_point = Tapyrus::OutPoint.new(utxo[:txid].rhex, utxo[:vout])
         | 
| 55 | 
            +
                  tx.inputs << Tapyrus::TxIn.new(out_point: out_point)
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                  wallet.sign_tx(tx, for_fee_provider_input: true)
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
             | 
| 60 | 
            +
                private
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                def utxo_for_fee
         | 
| 63 | 
            +
                  utxo = wallet.list_unspent.select { |o| !o[:color_id] && o[:amount] == fixed_fee }.sample
         | 
| 64 | 
            +
                  raise NoUtxosInUtxoPool, 'No UTXOs in Fee Provider UTXO pool. UTXOs should be created with "glueby:fee_provider:manage_utxo_pool" rake task' unless utxo
         | 
| 65 | 
            +
                  utxo
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
                # Get Signature from P2PKH or CP2PKH script sig
         | 
| 69 | 
            +
                def get_signature(script_sig)
         | 
| 70 | 
            +
                  script_sig.chunks.first.pushed_data
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
            end
         | 
| @@ -0,0 +1,141 @@ | |
| 1 | 
            +
            module Glueby
         | 
| 2 | 
            +
              class FeeProvider
         | 
| 3 | 
            +
                class Tasks
         | 
| 4 | 
            +
                  attr_reader :fee_provider
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                  STATUS = {
         | 
| 7 | 
            +
                    # FeeProvider is ready to pay fees.
         | 
| 8 | 
            +
                    ready: 'Ready',
         | 
| 9 | 
            +
                    # FeeProvider is ready to pay fees, but it doesn't have enough amount to fill the UTXO pool by UTXOs which is for paying fees.
         | 
| 10 | 
            +
                    insufficient_amount: 'Insufficient Amount',
         | 
| 11 | 
            +
                    # FeeProvider is not ready to pay fees. It has no UTXOs for paying fee and amounts.
         | 
| 12 | 
            +
                    not_ready: 'Not Ready'
         | 
| 13 | 
            +
                  }
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  def initialize
         | 
| 16 | 
            +
                    @fee_provider = Glueby::FeeProvider.new
         | 
| 17 | 
            +
                  end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  # Create UTXOs for paying fee from TPC amount of the wallet FeeProvider has. Then show the status.
         | 
| 20 | 
            +
                  #
         | 
| 21 | 
            +
                  # About the UTXO Pool
         | 
| 22 | 
            +
                  # FeeProvider have the UTXO pool. the pool is manged to keep some number of UTXOs that have fixed fee value. The
         | 
| 23 | 
            +
                  # value is configurable by :fixed_fee. This method do the management to the pool.
         | 
| 24 | 
            +
                  def manage_utxo_pool
         | 
| 25 | 
            +
                    txb = Tapyrus::TxBuilder.new
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                    sum, utxos = collect_outputs
         | 
| 28 | 
            +
                    return if utxos.empty?
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                    utxos.each { |utxo| txb.add_utxo(utxo) }
         | 
| 31 | 
            +
                    address = wallet.receive_address
         | 
| 32 | 
            +
             | 
| 33 | 
            +
                    shortage = [fee_provider.utxo_pool_size - current_utxo_pool_size, 0].max
         | 
| 34 | 
            +
                    can_create = (sum - fee_provider.fixed_fee) / fee_provider.fixed_fee
         | 
| 35 | 
            +
                    fee_outputs_count_to_be_created = [shortage, can_create].min
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                    return if fee_outputs_count_to_be_created == 0
         | 
| 38 | 
            +
             | 
| 39 | 
            +
                    fee_outputs_count_to_be_created.times do
         | 
| 40 | 
            +
                      txb.pay(address, fee_provider.fixed_fee)
         | 
| 41 | 
            +
                    end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
                    tx = txb.change_address(address)
         | 
| 44 | 
            +
                            .fee(fee_provider.fixed_fee)
         | 
| 45 | 
            +
                            .build
         | 
| 46 | 
            +
                    tx = wallet.sign_tx(tx)
         | 
| 47 | 
            +
                    wallet.broadcast(tx, without_fee_provider: true)
         | 
| 48 | 
            +
                  ensure
         | 
| 49 | 
            +
                    status
         | 
| 50 | 
            +
                  end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                  # Show the status of the UTXO pool
         | 
| 53 | 
            +
                  def status
         | 
| 54 | 
            +
                    status = :ready
         | 
| 55 | 
            +
             | 
| 56 | 
            +
                    if current_utxo_pool_size < fee_provider.utxo_pool_size
         | 
| 57 | 
            +
                      if tpc_amount < value_to_fill_utxo_pool
         | 
| 58 | 
            +
                        status = :insufficient_amount
         | 
| 59 | 
            +
                        message = <<~MESSAGE
         | 
| 60 | 
            +
                        1. Please replenishment TPC which is for paying fee to FeeProvider. 
         | 
| 61 | 
            +
                           FeeProvider needs #{value_to_fill_utxo_pool} tapyrus at least for paying 20 transaction fees. 
         | 
| 62 | 
            +
                           FeeProvider wallet's address is '#{wallet.receive_address}'
         | 
| 63 | 
            +
                        2. Then create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'
         | 
| 64 | 
            +
                        MESSAGE
         | 
| 65 | 
            +
                      else
         | 
| 66 | 
            +
                        message = "Please create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'\n"
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    status = :not_ready if current_utxo_pool_size == 0
         | 
| 71 | 
            +
             | 
| 72 | 
            +
                    puts <<~EOS
         | 
| 73 | 
            +
                    Status: #{STATUS[status]}
         | 
| 74 | 
            +
                    TPC amount: #{delimit(tpc_amount)}
         | 
| 75 | 
            +
                    UTXO pool size: #{delimit(current_utxo_pool_size)}
         | 
| 76 | 
            +
                    #{"\n" if message}#{message}
         | 
| 77 | 
            +
                    Configuration:
         | 
| 78 | 
            +
                      fixed_fee = #{delimit(fee_provider.fixed_fee)}
         | 
| 79 | 
            +
                      utxo_pool_size = #{delimit(fee_provider.utxo_pool_size)}
         | 
| 80 | 
            +
                    EOS
         | 
| 81 | 
            +
                  end
         | 
| 82 | 
            +
             | 
| 83 | 
            +
                  # Show the address of Fee Provider 
         | 
| 84 | 
            +
                  def address
         | 
| 85 | 
            +
                    puts wallet.receive_address
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  private
         | 
| 89 | 
            +
             | 
| 90 | 
            +
                  def check_wallet_amount!
         | 
| 91 | 
            +
                    if tpc_amount < fee_provider.fixed_fee
         | 
| 92 | 
            +
                      raise InsufficientTPC, <<~MESSAGE
         | 
| 93 | 
            +
                        FeeProvider has insufficient TPC to create fee outputs to fill the UTXO pool.
         | 
| 94 | 
            +
                        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}'
         | 
| 95 | 
            +
                        2. Then create UTXOs for paying in UTXO pool with 'rake glueby:fee_provider:manage_utxo_pool'
         | 
| 96 | 
            +
                      MESSAGE
         | 
| 97 | 
            +
                    end
         | 
| 98 | 
            +
                  end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                  def tpc_amount
         | 
| 101 | 
            +
                    wallet.balance(false)
         | 
| 102 | 
            +
                  end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                  def collect_outputs
         | 
| 105 | 
            +
                    wallet.list_unspent.inject([0, []]) do |sum, output|
         | 
| 106 | 
            +
                      next sum if output[:color_id] || output[:amount] == fee_provider.fixed_fee
         | 
| 107 | 
            +
             | 
| 108 | 
            +
                      new_sum = sum[0] + output[:amount]
         | 
| 109 | 
            +
                      new_outputs = sum[1] << {
         | 
| 110 | 
            +
                        txid: output[:txid],
         | 
| 111 | 
            +
                        script_pubkey: output[:script_pubkey],
         | 
| 112 | 
            +
                        value: output[:amount],
         | 
| 113 | 
            +
                        index: output[:vout] ,
         | 
| 114 | 
            +
                        finalized: output[:finalized]
         | 
| 115 | 
            +
                      }
         | 
| 116 | 
            +
                      return [new_sum, new_outputs] if new_sum >= value_to_fill_utxo_pool
         | 
| 117 | 
            +
             | 
| 118 | 
            +
                      [new_sum, new_outputs]
         | 
| 119 | 
            +
                    end
         | 
| 120 | 
            +
                  end
         | 
| 121 | 
            +
             | 
| 122 | 
            +
                  def current_utxo_pool_size
         | 
| 123 | 
            +
                    wallet
         | 
| 124 | 
            +
                      .list_unspent(false)
         | 
| 125 | 
            +
                      .count { |o| !o[:color_id] && o[:amount] == fee_provider.fixed_fee }
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
             | 
| 128 | 
            +
                  def value_to_fill_utxo_pool
         | 
| 129 | 
            +
                    fee_provider.fixed_fee * (fee_provider.utxo_pool_size + 1) #  +1 is for paying fee
         | 
| 130 | 
            +
                  end
         | 
| 131 | 
            +
             | 
| 132 | 
            +
                  def wallet
         | 
| 133 | 
            +
                    fee_provider.wallet
         | 
| 134 | 
            +
                  end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                  def delimit(num)
         | 
| 137 | 
            +
                    num.to_s.reverse.scan(/.{1,3}/).join('_').reverse
         | 
| 138 | 
            +
                  end
         | 
| 139 | 
            +
                end
         | 
| 140 | 
            +
              end
         | 
| 141 | 
            +
            end
         |