glueby 0.4.0 → 0.4.4

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.
@@ -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