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
|