vendi 0.1.1 → 0.2.0

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