glueby 0.4.3 → 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,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