vendi 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4b6e8fbf628bc618da176a73c2f7e77d9ff300bb022dc8dfbe672e5fe3e7e1a1
4
- data.tar.gz: 4c1d42a3aecf7eace980b4d91c2b5ff8378cc22d5fac64ad7b671cdcabf8490f
3
+ metadata.gz: a41b9c15bcb8f4c01eaf41358958fd00b4afa10da4a48b86fdd1463a0c7f0605
4
+ data.tar.gz: c3ab3e68705decb0ea6a9031eddcb77689fdbff85dae62d0436c0ed512e6c95d
5
5
  SHA512:
6
- metadata.gz: c98f5cec080dfe60b0258963a81bd2bdd17e6545b5f93b8e64819ab6827fe77e2e3d607997199543e48eaedc130b004cdde0941e9818703220e985deb78b7999
7
- data.tar.gz: cb5354b00b067303470076e72b9368e2664b5fa2c75cb4ac7ba902b4433c1b6d7d65e7fd5aebd235f393fd11d4d3d6cfd1f95a2b7fed0927deafe617ea69e7d1
6
+ metadata.gz: 7c37116a06be27482dc5e330bc8bd652b1eeeeb599c338c6ceeb9e11f2f259c15c18dede31fe652c48e6a0345cf55203550a83e584888965f5dc776748aba229
7
+ data.tar.gz: 8ab473e74a1090701beecb4815a6fa7ac2d2040a585c6ad7529eee89c072ac1bd6da1498b486f694b994ba65e813c56f4a8c565952e98bea96b40d8dd87e5b4d
data/README.md CHANGED
@@ -1,9 +1,14 @@
1
1
  # Vendi
2
2
 
3
+ [![Gem Version](https://badge.fury.io/rb/vendi.svg)](https://badge.fury.io/rb/vendi)
4
+
5
+ ## Overview
6
+
3
7
  Vendi is simple CNFT vending machine based on [`cardano-wallet`](https://github.com/input-output-hk/cardano-wallet).
4
8
  You need to have `cardano-wallet` started and synced.
5
9
 
6
- It... seems to work, check out the [Demo](https://github.com/piotr-iohk/vendi/tree/master/demo). :)
10
+ Check out the
11
+ 👉 **[Demo](http://185.201.114.10:4321)** 👈 and [how it was prepared](https://github.com/piotr-iohk/vendi/tree/master/demo#how-it-was-prepared).
7
12
 
8
13
  ## Installation
9
14
 
data/bin/vendi CHANGED
@@ -8,8 +8,8 @@ doc = <<~DOCOPT
8
8
  Vendi - CNFT Vending Machine.
9
9
 
10
10
  Usage:
11
- #{File.basename(__FILE__)} fill --collection <name> --price <lovelace> --nft-count <int> [--wallet-port <port>] [--skip-wallet-creation]
12
- #{File.basename(__FILE__)} serve --collection <name> [--wallet-port <port>] [--logfile <file>]
11
+ #{File.basename(__FILE__)} fill --collection <name> --price <lovelace> --nft-count <int> [--wallet-port <port>] [--skip-wallet]
12
+ #{File.basename(__FILE__)} serve --collection <name> [--vend-max <int>] [--wallet-port <port>] [--logfile <file>]
13
13
  #{File.basename(__FILE__)} -v | --version
14
14
  #{File.basename(__FILE__)} -h | --help
15
15
 
@@ -19,9 +19,11 @@ doc = <<~DOCOPT
19
19
  serve Start vending machine.
20
20
  --collection <name> Name of the collection.
21
21
  --price <lovelace> Single NFT price in lovelace.
22
+ --vend-max <int> Max number of NFTs vended in single transaction [default: 1].
22
23
  --nft-count <int> How many NFTs would you like to generate.
23
24
  --wallet-port <port> Cardano-wallet port [default: 8090].
24
25
  --logfile <file> Logfile (will be rotated daily).
26
+ --skip-wallet Skip creation of wallet when filling vending machine.
25
27
 
26
28
  -v --version Check #{File.basename(__FILE__)} version.
27
29
  -h --help This help.
@@ -47,7 +49,7 @@ begin
47
49
  price = o['--price']
48
50
  nft_count = o['--nft-count']
49
51
  wallet_port = o['--wallet-port']
50
- skip_wallet = o['--skip-wallet-creation']
52
+ skip_wallet = o['--skip-wallet']
51
53
  vendi = Vendi.init({ port: wallet_port.to_i })
52
54
  begin
53
55
  if File.directory?(File.join(vendi.config_dir, collection_name))
@@ -72,13 +74,14 @@ begin
72
74
  collection_name = o['--collection']
73
75
  wallet_port = o['--wallet-port']
74
76
  logfile = o['--logfile']
77
+ vend_max = o['--vend-max']
75
78
  vendi = if logfile
76
79
  Vendi.init({ port: wallet_port.to_i }, :info, logfile)
77
80
  else
78
81
  Vendi.init({ port: wallet_port.to_i })
79
82
  end
80
83
  begin
81
- vendi.serve(collection_name)
84
+ vendi.serve(collection_name, vend_max)
82
85
  rescue Errno::ECONNREFUSED => e
83
86
  # retry if cannot connect to cardano-wallet
84
87
  vendi.logger.error e.message
data/lib/vendi/machine.rb CHANGED
@@ -50,18 +50,34 @@ module Vendi
50
50
  File.join(collection_dir(collection_name), 'metadata-sent.json')
51
51
  end
52
52
 
53
+ def failed_mints_path(collection_name)
54
+ File.join(collection_dir(collection_name), 'failed-mints.json')
55
+ end
56
+
53
57
  def config(collection_name)
54
58
  from_json(config_path(collection_name))
55
59
  end
56
60
 
61
+ def set_config(collection_name, configuration)
62
+ to_json(config_path(collection_name), configuration)
63
+ end
64
+
57
65
  def metadata_vending(collection_name)
58
66
  from_json(metadata_vending_path(collection_name))
59
67
  end
60
68
 
69
+ def set_metadata_vending(collection_name, metadata)
70
+ to_json(metadata_vending_path(collection_name), metadata)
71
+ end
72
+
61
73
  def metadata_sent(collection_name)
62
74
  from_json(metadata_sent_path(collection_name))
63
75
  end
64
76
 
77
+ def set_metadata_sent(collection_name, metadata)
78
+ to_json(metadata_sent_path(collection_name), metadata)
79
+ end
80
+
65
81
  def metadata(collection_name)
66
82
  from_json(metadata_path(collection_name))
67
83
  end
@@ -70,16 +86,12 @@ module Vendi
70
86
  to_json(metadata_path(collection_name), metadata)
71
87
  end
72
88
 
73
- def set_metadata_sent(collection_name, metadata)
74
- to_json(metadata_sent_path(collection_name), metadata)
75
- end
76
-
77
- def set_metadata_vending(collection_name, metadata)
78
- to_json(metadata_vending_path(collection_name), metadata)
89
+ def failed_mints(collection_name)
90
+ from_json(failed_mints_path(collection_name))
79
91
  end
80
92
 
81
- def set_config(collection_name, configuration)
82
- to_json(config_path(collection_name), configuration)
93
+ def set_failed_mints(collection_name, failed_mints)
94
+ to_json(failed_mints_path(collection_name), failed_mints)
83
95
  end
84
96
 
85
97
  # Fill vending machine with exemplary set of CIP-25 metadata for minting,
@@ -88,12 +100,18 @@ module Vendi
88
100
  FileUtils.mkdir_p(collection_dir(collection_name))
89
101
  if skip_wallet
90
102
  @logger.info('Skipping wallet generation for your collection.')
91
- wallet_details = { wallet_id: '',
92
- wallet_name: '',
93
- wallet_pass: '',
94
- wallet_address: '',
95
- wallet_policy_id: '',
96
- wallet_mnemonics: '' }
103
+ wallet_details = if File.exist?(config_path(collection_name))
104
+ c = config(collection_name)
105
+ c.delete(:price)
106
+ c
107
+ else
108
+ { wallet_id: '',
109
+ wallet_name: '',
110
+ wallet_pass: '',
111
+ wallet_address: '',
112
+ wallet_policy_id: '',
113
+ wallet_mnemonics: '' }
114
+ end
97
115
  else
98
116
  @logger.info('Generating wallet for your collection.')
99
117
  wallet_details = create_wallet("Vendi wallet - #{collection_name}")
@@ -110,6 +128,9 @@ module Vendi
110
128
  @logger.info("Generating exemplary CIP-25 metadata set into #{metadata_path(collection_name)}.")
111
129
  metadatas = generate_metadata(collection_name, nft_count.to_i)
112
130
  set_metadata(collection_name, metadatas)
131
+ set_metadata_vending(collection_name, metadatas)
132
+ set_metadata_sent(collection_name, {})
133
+ set_failed_mints(collection_name, {})
113
134
 
114
135
  @logger.info('IMPORTANT NOTES! 👇')
115
136
  @logger.info('----------------')
@@ -121,12 +142,11 @@ module Vendi
121
142
 
122
143
  # Turn on vending machine and make it serve NFTs for anyone who dares to
123
144
  # pay the 'price' to the 'address', that is specified in the config_file
124
- def serve(collection_name)
145
+ def serve(collection_name, vend_max = 1)
125
146
  set_metadata_sent(collection_name, {}) unless File.exist?(metadata_sent_path(collection_name))
126
147
 
127
148
  c = config(collection_name)
128
149
  wid = c[:wallet_id]
129
- pass = c[:wallet_pass]
130
150
  address = c[:wallet_address]
131
151
  policy_id = c[:wallet_policy_id]
132
152
  price = c[:price]
@@ -138,8 +158,10 @@ module Vendi
138
158
 
139
159
  @logger.info 'Vending machine started.'
140
160
  @logger.info "Wallet id: #{wid}"
161
+ @logger.info "Policy id: #{policy_id}"
141
162
  @logger.info "Address: #{address}"
142
163
  @logger.info "NFT price: #{as_ada(price)}"
164
+ @logger.info "Vend max NFTs: #{vend_max}"
143
165
  @logger.info "Original NFT stock: #{metadata(collection_name).size}"
144
166
  @logger.info '----------------'
145
167
  unless File.exist?(metadata_vending_path(collection_name))
@@ -152,7 +174,12 @@ module Vendi
152
174
  nfts = metadata_vending(collection_name)
153
175
  nfts_sent = metadata_sent(collection_name)
154
176
  wallet_balance = @cw.shelley.wallets.get(wid)['balance']['available']['quantity']
155
- @logger.info "Vending machine [In stock: #{nfts.size}, Sent: #{nfts_sent.size}, NFT price: #{as_ada(price)}, Balance: #{as_ada(wallet_balance)}]"
177
+ @logger.info "[In stock: #{nfts.size}, Sent: #{nfts_sent.size}, NFT price: #{as_ada(price)}, Vend max: #{vend_max}, Balance: #{as_ada(wallet_balance)}]"
178
+ n = @cw.misc.network.information
179
+ unless n['sync_progress']['status'] == 'ready'
180
+ @logger.error "Network is not synced (#{n['sync_progress']['status']} #{n['sync_progress']['progress']['quantity']}%), waiting..."
181
+ sleep 5
182
+ end
156
183
 
157
184
  txs_new = get_incoming_txs(wid)
158
185
  if txs.size < txs_new.size
@@ -166,32 +193,34 @@ module Vendi
166
193
  @logger.info 'OK! VENDING!'
167
194
  @logger.info '----------------'
168
195
  dest_addr = get_dest_addr(t, address)
169
-
170
- # prepare metadata and mint payload
171
- key = nfts.keys.sample
172
- metadata = prepare_metadata(nfts, key, policy_id)
173
- mint = mint_payload(asset_name(key.to_s), dest_addr)
174
- @logger.debug JSON.pretty_generate(metadata)
175
- @logger.debug JSON.pretty_generate(mint)
176
-
177
- # mint
178
- @logger.info "Minting NFT: #{key} to #{dest_addr}"
179
- tx_res = construct_sign_submit(wid, pass, metadata, mint)
196
+ minted = mint_nft(collection_name, t['amount']['quantity'], vend_max, dest_addr)
197
+ tx_res = minted[:tx_res]
198
+ keys = minted[:keys]
180
199
  if outgoing_tx_ok?(tx_res)
181
200
  mint_tx_id = tx_res.last['id']
182
201
  wait_for_tx_in_ledger(wid, mint_tx_id)
183
202
  # update metadata files
184
- update_metadata_files(nfts, key, metadata_vending_path(collection_name), metadata_sent_path(collection_name))
203
+ update_metadata_files(keys, collection_name)
185
204
  else
186
205
  @logger.error 'Minting tx failed!'
187
206
  @logger.error "Construct tx: #{JSON.pretty_generate(tx_res[0])}"
188
207
  @logger.error "Sign tx: #{JSON.pretty_generate(tx_res[1])}"
189
208
  @logger.error "Submit tx: #{JSON.pretty_generate(tx_res[2])}"
209
+ @logger.warn "Updating #{failed_mints_path(collection_name)} file."
210
+ update_failed_mints(collection_name, t['id'], tx_res, keys)
190
211
  end
191
212
  @logger.info '----------------'
192
213
 
193
214
  else
194
- @logger.warn "NO GOOD! NOT VENDING! Tx: #{t['id']}"
215
+ amt = t['amount']['quantity']
216
+ @logger.warn "NOT VENDING! Amt: #{as_ada(amt)}, Tx: #{t['id']}"
217
+ @logger.warn "Updating #{failed_mints_path(collection_name)} file."
218
+ reason = if amt.to_i < price.to_i
219
+ "wrong_amount = #{as_ada(amt)}"
220
+ else
221
+ "wrong_address = #{address} was not in incoming tx outputs"
222
+ end
223
+ update_failed_mints(collection_name, t['id'], reason, keys)
195
224
  end
196
225
  end
197
226
 
data/lib/vendi/minter.rb CHANGED
@@ -3,19 +3,46 @@
3
3
  module Vendi
4
4
  # helper methods for minting NFT
5
5
  module Minter
6
- def prepare_metadata(nfts, key, policy_id)
6
+ ##
7
+ # encode string asset_name to hex representation
8
+ def asset_name(asset_name)
9
+ asset_name.unpack1('H*')
10
+ end
11
+
12
+ ##
13
+ # get list of keys of metadata to be minted
14
+ def keys_to_mint(tx_amt, price, vend_max, collection_name)
15
+ nfts = metadata_vending(collection_name)
16
+ to_mint = (tx_amt / price) > vend_max.to_i ? vend_max.to_i : (tx_amt / price)
17
+ nfts.keys.sample(to_mint)
18
+ end
19
+
20
+ ##
21
+ # get metadata for keys to be minted
22
+ def metadatas(keys, collection_name)
23
+ nfts = metadata_vending(collection_name)
24
+ keys.to_h do |key|
25
+ [key, nfts[key]]
26
+ end
27
+ end
28
+
29
+ ##
30
+ # prepare metadata for minting
31
+ def prepare_metadata(keys, collection_name, policy_id)
7
32
  {
8
33
  '721' => {
9
- policy_id => {
10
- key => nfts[key]
11
- }
34
+ policy_id => metadatas(keys, collection_name)
12
35
  }
13
36
  }
14
37
  end
15
38
 
16
- def update_metadata_files(nfts, key, metadata_vending_file, metadata_sent_file)
39
+ ##
40
+ # update metadata files after minting
41
+ def update_metadata_files(keys, collection_name)
42
+ metadata_vending_file = metadata_vending_path(collection_name)
43
+ metadata_sent_file = metadata_sent_path(collection_name)
17
44
  # metadata sent
18
- m = { key => nfts[key] }
45
+ m = metadatas(keys, collection_name)
19
46
  if File.exist? metadata_sent_file
20
47
  sent = from_json(metadata_sent_file)
21
48
  sent.merge!(m)
@@ -24,20 +51,37 @@ module Vendi
24
51
  to_json(metadata_sent_file, m)
25
52
  end
26
53
  # metadata available
27
- nfts.delete(key)
54
+ nfts = metadata_vending(collection_name)
55
+ keys.each do |key|
56
+ nfts.delete(key)
57
+ end
28
58
  to_json(metadata_vending_file, nfts)
29
59
  end
30
60
 
61
+ ##
62
+ # update failed mints
63
+ def update_failed_mints(collection_name, tx_id, reason, keys)
64
+ failed_mints_file = failed_mints_path(collection_name)
65
+ if File.exist? failed_mints_file
66
+ failed_mints = from_json(failed_mints_file)
67
+ failed_mints[tx_id] = [reason, keys]
68
+ to_json(failed_mints_file, failed_mints)
69
+ else
70
+ to_json(failed_mints_file, tx_id => [reason, keys])
71
+ end
72
+ end
73
+
31
74
  # Build mint payload for construct tx
32
- def mint_payload(asset_name, address, quantity = 1)
33
- mint = { 'operation' => { 'mint' => { 'quantity' => quantity,
34
- 'receiving_address' => address } },
35
- 'policy_script_template' => Vendi::POLICY_SCRIPT_TEMPLATE }
36
- mint['asset_name'] = asset_name unless asset_name.nil?
37
- [mint]
75
+ def mint_payload(keys, address, quantity = 1)
76
+ keys.map do |key|
77
+ { 'operation' => { 'mint' => { 'quantity' => quantity,
78
+ 'receiving_address' => address } },
79
+ 'policy_script_template' => Vendi::POLICY_SCRIPT_TEMPLATE,
80
+ 'asset_name' => asset_name(key.to_s) }
81
+ end
38
82
  end
39
83
 
40
- # Construct -> Sign -> Submit
84
+ # Construct -> Sign -> Submit transaction
41
85
  def construct_sign_submit(wid, pass, metadata, mint_payload)
42
86
  tx_constructed = @cw.shelley.transactions.construct(wid,
43
87
  nil,
@@ -55,11 +99,30 @@ module Vendi
55
99
  [tx_constructed, tx_signed, tx_submitted]
56
100
  end
57
101
 
102
+ # Mint NFT
103
+ def mint_nft(collection_name, tx_amt, vend_max, dest_address)
104
+ c = config(collection_name)
105
+ wid = c[:wallet_id]
106
+ pass = c[:wallet_pass]
107
+ policy_id = c[:wallet_policy_id]
108
+ price = c[:price]
109
+ keys = keys_to_mint(tx_amt, price, vend_max, collection_name)
110
+ @logger.info "Minting #{keys.size} NFT(s): #{keys} to #{dest_address}"
111
+ metadata = prepare_metadata(keys, collection_name, policy_id)
112
+ mint_payload = mint_payload(keys, dest_address, 1)
113
+ tx_res = construct_sign_submit(wid, pass, metadata, mint_payload)
114
+ { keys: keys, tx_res: tx_res }
115
+ end
116
+
117
+ ##
118
+ # check if NFT mint transaction is successful
58
119
  def outgoing_tx_ok?(tx_res)
59
120
  tx_constructed, tx_signed, tx_submitted = tx_res
60
121
  tx_constructed.code == 202 && tx_signed.code == 202 && tx_submitted.code == 202
61
122
  end
62
123
 
124
+ ##
125
+ # wait for NFT to be minted
63
126
  def wait_for_tx_in_ledger(wid, tx_id)
64
127
  eventually "Tx #{tx_id} is in ledger" do
65
128
  @logger.info "Waiting for #{tx_id} to get in_ledger"
data/lib/vendi/monitor.rb CHANGED
@@ -14,7 +14,9 @@ module Vendi
14
14
  # incoming tx is correct when the address is on any of the outputs (means that someone was sending to it)
15
15
  # and tx amount is >= price set in the config
16
16
  def incoming_tx_ok?(tx, address, price)
17
- (tx['outputs'].any? { |o| (o['address'] == address) }) && (tx['amount']['quantity'] >= price)
17
+ (tx['outputs'].any? { |o| (o['address'] == address) }) &&
18
+ (tx['amount']['quantity'] >= price) &&
19
+ (tx['direction'] == 'incoming')
18
20
  end
19
21
 
20
22
  # trying to naively get address to send back NFT, take first address from the output that isn't our address
data/lib/vendi/utils.rb CHANGED
@@ -44,11 +44,5 @@ module Vendi
44
44
  true
45
45
  end
46
46
  end
47
-
48
- ##
49
- # encode string asset_name to hex representation
50
- def asset_name(asset_name)
51
- asset_name.unpack1('H*')
52
- end
53
47
  end
54
48
  end
data/lib/vendi/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vendi
4
- VERSION = '0.1.1'
4
+ VERSION = '0.2.0'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vendi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Piotr Stachyra
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-28 00:00:00.000000000 Z
11
+ date: 2022-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cardano_wallet
@@ -111,7 +111,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
111
111
  - !ruby/object:Gem::Version
112
112
  version: '0'
113
113
  requirements: []
114
- rubygems_version: 3.3.7
114
+ rubygems_version: 3.0.3.1
115
115
  signing_key:
116
116
  specification_version: 4
117
117
  summary: CNFT Vending Machine - cardano-wallet based