glueby 0.4.4 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,245 +1,271 @@
1
- # frozen_string_literal: true
2
- require 'active_record'
3
-
4
- module Glueby
5
- module Contract
6
- # This class represents custom token issued by application user.
7
- # Application users can
8
- # - issue their own tokens.
9
- # - send to other users.
10
- # - make the tokens disable.
11
- #
12
- # Examples:
13
- #
14
- # alice = Glueby::Wallet.create
15
- # bob = Glueby::Wallet.create
16
- #
17
- # Use `Glueby::Internal::Wallet#receive_address` to generate the address of bob
18
- # bob.internal_wallet.receive_address
19
- # => '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a'
20
- #
21
- # Issue
22
- # token = Token.issue!(issuer: alice, amount: 100)
23
- # token.amount(wallet: alice)
24
- # => 100
25
- #
26
- # Send
27
- # token.transfer!(sender: alice, receiver_address: '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a', amount: 1)
28
- # token.amount(wallet: alice)
29
- # => 99
30
- # token.amount(wallet: bob)
31
- # => 1
32
- #
33
- # Burn
34
- # token.burn!(sender: alice, amount: 10)
35
- # token.amount(wallet: alice)
36
- # => 89
37
- # token.burn!(sender: alice)
38
- # token.amount(wallet: alice)
39
- # => 0
40
- #
41
- # Reissue
42
- # token.reissue!(issuer: alice, amount: 100)
43
- # token.amount(wallet: alice)
44
- # => 100
45
- #
46
- class Token
47
- include Glueby::Contract::TxBuilder
48
- extend Glueby::Contract::TxBuilder
49
-
50
- class << self
51
- # Issue new token with specified amount and token type.
52
- # REISSUABLE token can be reissued with #reissue! method, and
53
- # NON_REISSUABLE and NFT token can not.
54
- # Amount is set to 1 when the token type is NFT
55
- #
56
- # @param issuer [Glueby::Wallet]
57
- # @param token_type [TokenTypes]
58
- # @param amount [Integer]
59
- # @return [Array<token, Array<tx>>] Tuple of tx array and token object
60
- # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
61
- # @raise [InvalidAmount] if amount is not positive integer.
62
- # @raise [UnspportedTokenType] if token is not supported.
63
- def issue!(issuer:, token_type: Tapyrus::Color::TokenTypes::REISSUABLE, amount: 1)
64
- raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
65
-
66
- txs, color_id = case token_type
67
- when Tapyrus::Color::TokenTypes::REISSUABLE
68
- issue_reissuable_token(issuer: issuer, amount: amount)
69
- when Tapyrus::Color::TokenTypes::NON_REISSUABLE
70
- issue_non_reissuable_token(issuer: issuer, amount: amount)
71
- when Tapyrus::Color::TokenTypes::NFT
72
- issue_nft_token(issuer: issuer)
73
- else
74
- raise Glueby::Contract::Errors::UnsupportedTokenType
75
- end
76
-
77
- [new(color_id: color_id), txs]
78
- end
79
-
80
- private
81
-
82
- def issue_reissuable_token(issuer:, amount:)
83
- funding_tx = create_funding_tx(wallet: issuer)
84
- script_pubkey = funding_tx.outputs.first.script_pubkey
85
- color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
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
96
- end
97
-
98
- def issue_non_reissuable_token(issuer:, amount:)
99
- tx = create_issue_tx_for_non_reissuable_token(issuer: issuer, amount: amount)
100
- tx = issuer.internal_wallet.broadcast(tx)
101
-
102
- out_point = tx.inputs.first.out_point
103
- color_id = Tapyrus::Color::ColorIdentifier.non_reissuable(out_point)
104
- [[tx], color_id]
105
- end
106
-
107
- def issue_nft_token(issuer:)
108
- tx = create_issue_tx_for_nft_token(issuer: issuer)
109
- tx = issuer.internal_wallet.broadcast(tx)
110
-
111
- out_point = tx.inputs.first.out_point
112
- color_id = Tapyrus::Color::ColorIdentifier.nft(out_point)
113
- [[tx], color_id]
114
- end
115
- end
116
-
117
- attr_reader :color_id
118
-
119
- # Re-issue the token with specified amount.
120
- # A wallet can issue the token only when it is REISSUABLE token.
121
- # @param issuer [Glueby::Wallet]
122
- # @param amount [Integer]
123
- # @return [Array<String, tx>] Tuple of color_id and tx object
124
- # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
125
- # @raise [InvalidAmount] if amount is not positive integer.
126
- # @raise [InvalidTokenType] if token is not reissuable.
127
- # @raise [UnknownScriptPubkey] when token is reissuable but it doesn't know script pubkey to issue token.
128
- def reissue!(issuer:, amount:)
129
- raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
130
- raise Glueby::Contract::Errors::InvalidTokenType unless token_type == Tapyrus::Color::TokenTypes::REISSUABLE
131
-
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
142
- end
143
-
144
- # Send the token to other wallet
145
- #
146
- # @param sender [Glueby::Wallet] wallet to send this token
147
- # @param receiver_address [String] address to receive this token
148
- # @param amount [Integer]
149
- # @return [Array<String, tx>] Tuple of color_id and tx object
150
- # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
151
- # @raise [InsufficientTokens] if wallet does not have enough token to send.
152
- # @raise [InvalidAmount] if amount is not positive integer.
153
- def transfer!(sender:, receiver_address:, amount: 1)
154
- raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
155
-
156
- tx = create_transfer_tx(color_id: color_id, sender: sender, receiver_address: receiver_address, amount: amount)
157
- sender.internal_wallet.broadcast(tx)
158
- [color_id, tx]
159
- end
160
-
161
- # Burn token
162
- # If amount is not specified or 0, burn all token associated with the wallet.
163
- #
164
- # @param sender [Glueby::Wallet] wallet to send this token
165
- # @param amount [Integer]
166
- # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
167
- # @raise [InsufficientTokens] if wallet does not have enough token to send transaction.
168
- # @raise [InvalidAmount] if amount is not positive integer.
169
- def burn!(sender:, amount: 0)
170
- raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
171
-
172
- tx = create_burn_tx(color_id: color_id, sender: sender, amount: amount)
173
- sender.internal_wallet.broadcast(tx)
174
- end
175
-
176
- # Return balance of token in the specified wallet.
177
- # @param wallet [Glueby::Wallet]
178
- # @return [Integer] amount of utxo value associated with this token.
179
- def amount(wallet:)
180
- # collect utxo associated with this address
181
- utxos = wallet.internal_wallet.list_unspent
182
- _, results = collect_colored_outputs(utxos, color_id)
183
- results.sum { |result| result[:amount] }
184
- end
185
-
186
- # Return token type
187
- # @return [Tapyrus::Color::TokenTypes]
188
- def token_type
189
- color_id.type
190
- end
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
-
198
- # Return serialized payload
199
- # @return [String] payload
200
- def to_payload
201
- payload = +''
202
- payload << @color_id.to_payload
203
- payload << @script_pubkey.to_payload if script_pubkey
204
- payload
205
- end
206
-
207
- # Restore token from payload
208
- # @param payload [String]
209
- # @return [Glueby::Contract::Token]
210
- def self.parse_from_payload(payload)
211
- color_id, script_pubkey = payload.unpack('a33a*')
212
- color_id = Tapyrus::Color::ColorIdentifier.parse_from_payload(color_id)
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)
219
- end
220
-
221
- # Generate Token Instance
222
- # @param color_id [String]
223
- def initialize(color_id:)
224
- @color_id = color_id
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
242
- end
243
- end
244
- end
1
+ # frozen_string_literal: true
2
+ require 'active_record'
3
+
4
+ module Glueby
5
+ module Contract
6
+ # This class represents custom token issued by application user.
7
+ # Application users can
8
+ # - issue their own tokens.
9
+ # - send to other users.
10
+ # - make the tokens disable.
11
+ #
12
+ # Examples:
13
+ #
14
+ # alice = Glueby::Wallet.create
15
+ # bob = Glueby::Wallet.create
16
+ #
17
+ # Use `Glueby::Internal::Wallet#receive_address` to generate the address of bob
18
+ # bob.internal_wallet.receive_address
19
+ # => '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a'
20
+ #
21
+ # Issue
22
+ # token = Token.issue!(issuer: alice, amount: 100)
23
+ # token.amount(wallet: alice)
24
+ # => 100
25
+ #
26
+ # Send
27
+ # token.transfer!(sender: alice, receiver_address: '1CY6TSSARn8rAFD9chCghX5B7j4PKR8S1a', amount: 1)
28
+ # token.amount(wallet: alice)
29
+ # => 99
30
+ # token.amount(wallet: bob)
31
+ # => 1
32
+ #
33
+ # Burn
34
+ # token.burn!(sender: alice, amount: 10)
35
+ # token.amount(wallet: alice)
36
+ # => 89
37
+ # token.burn!(sender: alice)
38
+ # token.amount(wallet: alice)
39
+ # => 0
40
+ #
41
+ # Reissue
42
+ # token.reissue!(issuer: alice, amount: 100)
43
+ # token.amount(wallet: alice)
44
+ # => 100
45
+ #
46
+ class Token
47
+ include Glueby::Contract::TxBuilder
48
+ extend Glueby::Contract::TxBuilder
49
+
50
+ class << self
51
+ # Issue new token with specified amount and token type.
52
+ # REISSUABLE token can be reissued with #reissue! method, and
53
+ # NON_REISSUABLE and NFT token can not.
54
+ # Amount is set to 1 when the token type is NFT
55
+ #
56
+ # @param issuer [Glueby::Wallet]
57
+ # @param token_type [TokenTypes]
58
+ # @param amount [Integer]
59
+ # @return [Array<token, Array<tx>>] Tuple of tx array and token object
60
+ # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
61
+ # @raise [InvalidAmount] if amount is not positive integer.
62
+ # @raise [UnspportedTokenType] if token is not supported.
63
+ def issue!(issuer:, token_type: Tapyrus::Color::TokenTypes::REISSUABLE, amount: 1)
64
+ raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
65
+
66
+ txs, color_id = case token_type
67
+ when Tapyrus::Color::TokenTypes::REISSUABLE
68
+ issue_reissuable_token(issuer: issuer, amount: amount)
69
+ when Tapyrus::Color::TokenTypes::NON_REISSUABLE
70
+ issue_non_reissuable_token(issuer: issuer, amount: amount)
71
+ when Tapyrus::Color::TokenTypes::NFT
72
+ issue_nft_token(issuer: issuer)
73
+ else
74
+ raise Glueby::Contract::Errors::UnsupportedTokenType
75
+ end
76
+
77
+ [new(color_id: color_id), txs]
78
+ end
79
+
80
+ private
81
+
82
+ def issue_reissuable_token(issuer:, amount:)
83
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
84
+ funding_tx = create_funding_tx(wallet: issuer, utxo_provider: utxo_provider)
85
+ script_pubkey = funding_tx.outputs.first.script_pubkey
86
+ color_id = Tapyrus::Color::ColorIdentifier.reissuable(script_pubkey)
87
+
88
+ ActiveRecord::Base.transaction(joinable: false, requires_new: true) do
89
+ # Store the script_pubkey for reissue the token.
90
+ Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex)
91
+
92
+ funding_tx = issuer.internal_wallet.broadcast(funding_tx)
93
+ tx = create_issue_tx_for_reissuable_token(funding_tx: funding_tx, issuer: issuer, amount: amount)
94
+ tx = issuer.internal_wallet.broadcast(tx)
95
+ [[funding_tx, tx], color_id]
96
+ end
97
+ end
98
+
99
+ def issue_non_reissuable_token(issuer:, amount:)
100
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
101
+ funding_tx = create_funding_tx(wallet: issuer, utxo_provider: utxo_provider) if utxo_provider
102
+ funding_tx = issuer.internal_wallet.broadcast(funding_tx) if funding_tx
103
+
104
+ tx = create_issue_tx_for_non_reissuable_token(funding_tx: funding_tx, issuer: issuer, amount: amount)
105
+ tx = issuer.internal_wallet.broadcast(tx)
106
+
107
+ out_point = tx.inputs.first.out_point
108
+ color_id = Tapyrus::Color::ColorIdentifier.non_reissuable(out_point)
109
+ if funding_tx
110
+ [[funding_tx, tx], color_id]
111
+ else
112
+ [[tx], color_id]
113
+ end
114
+ end
115
+
116
+ def issue_nft_token(issuer:)
117
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
118
+ funding_tx = create_funding_tx(wallet: issuer, utxo_provider: utxo_provider) if utxo_provider
119
+ funding_tx = issuer.internal_wallet.broadcast(funding_tx) if funding_tx
120
+
121
+ tx = create_issue_tx_for_nft_token(funding_tx: funding_tx, issuer: issuer)
122
+ tx = issuer.internal_wallet.broadcast(tx)
123
+
124
+ out_point = tx.inputs.first.out_point
125
+ color_id = Tapyrus::Color::ColorIdentifier.nft(out_point)
126
+ if funding_tx
127
+ [[funding_tx, tx], color_id]
128
+ else
129
+ [[tx], color_id]
130
+ end
131
+ end
132
+ end
133
+
134
+ attr_reader :color_id
135
+
136
+ # Re-issue the token with specified amount.
137
+ # A wallet can issue the token only when it is REISSUABLE token.
138
+ # @param issuer [Glueby::Wallet]
139
+ # @param amount [Integer]
140
+ # @return [Array<String, tx>] Tuple of color_id and tx object
141
+ # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
142
+ # @raise [InvalidAmount] if amount is not positive integer.
143
+ # @raise [InvalidTokenType] if token is not reissuable.
144
+ # @raise [UnknownScriptPubkey] when token is reissuable but it doesn't know script pubkey to issue token.
145
+ def reissue!(issuer:, amount:)
146
+ raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
147
+ raise Glueby::Contract::Errors::InvalidTokenType unless token_type == Tapyrus::Color::TokenTypes::REISSUABLE
148
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
149
+
150
+ if validate_reissuer(wallet: issuer)
151
+ funding_tx = create_funding_tx(wallet: issuer, script: @script_pubkey, utxo_provider: utxo_provider)
152
+ funding_tx = issuer.internal_wallet.broadcast(funding_tx)
153
+ tx = create_reissue_tx(funding_tx: funding_tx, issuer: issuer, amount: amount, color_id: color_id)
154
+ tx = issuer.internal_wallet.broadcast(tx)
155
+
156
+ [color_id, tx]
157
+ else
158
+ raise Glueby::Contract::Errors::UnknownScriptPubkey
159
+ end
160
+ end
161
+
162
+ # Send the token to other wallet
163
+ #
164
+ # @param sender [Glueby::Wallet] wallet to send this token
165
+ # @param receiver_address [String] address to receive this token
166
+ # @param amount [Integer]
167
+ # @return [Array<String, tx>] Tuple of color_id and tx object
168
+ # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
169
+ # @raise [InsufficientTokens] if wallet does not have enough token to send.
170
+ # @raise [InvalidAmount] if amount is not positive integer.
171
+ def transfer!(sender:, receiver_address:, amount: 1)
172
+ raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
173
+
174
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
175
+ funding_tx = create_funding_tx(wallet: sender, utxo_provider: utxo_provider) if utxo_provider
176
+ funding_tx = sender.internal_wallet.broadcast(funding_tx) if funding_tx
177
+
178
+ tx = create_transfer_tx(funding_tx: funding_tx, color_id: color_id, sender: sender, receiver_address: receiver_address, amount: amount)
179
+ sender.internal_wallet.broadcast(tx)
180
+ [color_id, tx]
181
+ end
182
+
183
+ # Burn token
184
+ # If amount is not specified or 0, burn all token associated with the wallet.
185
+ #
186
+ # @param sender [Glueby::Wallet] wallet to send this token
187
+ # @param amount [Integer]
188
+ # @raise [InsufficientFunds] if wallet does not have enough TPC to send transaction.
189
+ # @raise [InsufficientTokens] if wallet does not have enough token to send transaction.
190
+ # @raise [InvalidAmount] if amount is not positive integer.
191
+ def burn!(sender:, amount: 0)
192
+ raise Glueby::Contract::Errors::InvalidAmount unless amount.positive?
193
+
194
+ utxo_provider = Glueby::UtxoProvider.new if Glueby.configuration.use_utxo_provider?
195
+ funding_tx = create_funding_tx(wallet: sender, utxo_provider: utxo_provider) if utxo_provider
196
+ funding_tx = sender.internal_wallet.broadcast(funding_tx) if funding_tx
197
+
198
+ tx = create_burn_tx(funding_tx: funding_tx, color_id: color_id, sender: sender, amount: amount)
199
+ sender.internal_wallet.broadcast(tx)
200
+ end
201
+
202
+ # Return balance of token in the specified wallet.
203
+ # @param wallet [Glueby::Wallet]
204
+ # @return [Integer] amount of utxo value associated with this token.
205
+ def amount(wallet:)
206
+ # collect utxo associated with this address
207
+ utxos = wallet.internal_wallet.list_unspent
208
+ _, results = collect_colored_outputs(utxos, color_id)
209
+ results.sum { |result| result[:amount] }
210
+ end
211
+
212
+ # Return token type
213
+ # @return [Tapyrus::Color::TokenTypes]
214
+ def token_type
215
+ color_id.type
216
+ end
217
+
218
+ # Return the script_pubkey of the token from ActiveRecord
219
+ # @return [String] script_pubkey
220
+ def script_pubkey
221
+ @script_pubkey ||= Glueby::Contract::AR::ReissuableToken.script_pubkey(@color_id.to_hex)
222
+ end
223
+
224
+ # Return serialized payload
225
+ # @return [String] payload
226
+ def to_payload
227
+ payload = +''
228
+ payload << @color_id.to_payload
229
+ payload << @script_pubkey.to_payload if script_pubkey
230
+ payload
231
+ end
232
+
233
+ # Restore token from payload
234
+ # @param payload [String]
235
+ # @return [Glueby::Contract::Token]
236
+ def self.parse_from_payload(payload)
237
+ color_id, script_pubkey = payload.unpack('a33a*')
238
+ color_id = Tapyrus::Color::ColorIdentifier.parse_from_payload(color_id)
239
+ if color_id.type == Tapyrus::Color::TokenTypes::REISSUABLE
240
+ raise ArgumentError, 'script_pubkey should not be empty' if script_pubkey.empty?
241
+ script_pubkey = Tapyrus::Script.parse_from_payload(script_pubkey)
242
+ Glueby::Contract::AR::ReissuableToken.create!(color_id: color_id.to_hex, script_pubkey: script_pubkey.to_hex)
243
+ end
244
+ new(color_id: color_id)
245
+ end
246
+
247
+ # Generate Token Instance
248
+ # @param color_id [String]
249
+ def initialize(color_id:)
250
+ @color_id = color_id
251
+ end
252
+
253
+ private
254
+
255
+ # Verify that wallet is the issuer of the reissuable token
256
+ # reutrn [Boolean]
257
+ def validate_reissuer(wallet:)
258
+ addresses = wallet.internal_wallet.get_addresses
259
+ addresses.each do |address|
260
+ decoded_address = Tapyrus.decode_base58_address(address)
261
+ pubkey_hash_from_address = decoded_address[0]
262
+ pubkey_hash_from_script = Tapyrus::Script.parse_from_payload(script_pubkey.chunks[2])
263
+ if pubkey_hash_from_address == pubkey_hash_from_script.to_s
264
+ return true
265
+ end
266
+ end
267
+ false
268
+ end
269
+ end
270
+ end
245
271
  end