glueby 0.4.0 → 0.4.4

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