glueby 0.4.3 → 0.4.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -1,245 +1,245 @@
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
+ 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
245
245
  end