vendi 0.1.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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 19f37dec74417ace8e5980584edc2e5c9b02fb03e5480abf35f2d929126df521
4
+ data.tar.gz: bc8ac9b3addadc2c10230468ed18a2081a86c42681ec2beccdd798ba6b1b311f
5
+ SHA512:
6
+ metadata.gz: f7b93150c1274e0f5bf81687ba4e42ba5bd291f747397c900ad2d73aa986ac1a926cad2c5bf579aea7b78012b0b02a99ad838df4cbfdc1f152675e84566aceb2
7
+ data.tar.gz: 79644c8d01cd86d11803ec04c9cc2f692537009a20bdeb2753ef679a9301317df078b913a532383d8612fc65eba85cd3ba9a597627d92d35deac88c13fd66cd4
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 Piotr Stachyra
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,38 @@
1
+ # Vendi
2
+
3
+ Vendi is simple CNFT vending machine based on [`cardano-wallet`](https://github.com/input-output-hk/cardano-wallet).
4
+ You need to have `cardano-wallet` started and synced.
5
+
6
+ It... seems to work, check out the [Demo](https://github.com/piotr-iohk/vendi/tree/master/demo). :)
7
+
8
+ ## Installation
9
+
10
+ **Rubygem:**
11
+
12
+ $ gem install vendi
13
+
14
+ ## Usage
15
+
16
+ Fill vending machine with exemplary NFT collection:
17
+
18
+ $ vendi fill --collection TestBudz --price 10000000 --nft-count 100
19
+
20
+ Now check out `$HOME/.vendi-nft-machine/TestBudz`, refine configs as you prefer.
21
+ When ready start vending machine:
22
+
23
+ $ vendi serve --collection TestBudz
24
+
25
+
26
+
27
+ ## Contributing
28
+
29
+ Bug reports and pull requests are welcome on GitHub at https://github.com/piotr-iohk/vendi. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/piotr-iohk/vendi/blob/master/CODE_OF_CONDUCT.md).
30
+
31
+
32
+ ## License
33
+
34
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
35
+
36
+ ## Code of Conduct
37
+
38
+ Everyone interacting in the Vendi project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/piotr-iohk/vendi/blob/master/CODE_OF_CONDUCT.md).
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'vendi'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/rebuild ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+ gem build *.gemspec -o vendi.gem
3
+ gem install vendi.gem
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/bin/vendi ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'vendi'
5
+ require 'docopt'
6
+
7
+ doc = <<~DOCOPT
8
+ Vendi - CNFT Vending Machine.
9
+
10
+ Usage:
11
+ #{File.basename(__FILE__)} fill --collection <name> --price <lovelace> --nft-count <int> [--wallet-port <port>]
12
+ #{File.basename(__FILE__)} serve --collection <name> [--wallet-port <port>] [--logfile <file>]
13
+ #{File.basename(__FILE__)} -v | --version
14
+ #{File.basename(__FILE__)} -h | --help
15
+
16
+ Options:
17
+ fill Setup vending machine by filling it with exemplary set of NFT CIP-25 metadata
18
+ and creating a source wallet for minting.
19
+ serve Start vending machine.
20
+ --collection <name> Name of the collection.
21
+ --price <lovelace> Single NFT price in lovelace.
22
+ --nft-count <int> How many NFTs would you like to generate.
23
+ --wallet-port <port> Cardano-wallet port [default: 8090].
24
+ --logfile <file> Logfile (will be rotated daily).
25
+
26
+ -v --version Check #{File.basename(__FILE__)} version.
27
+ -h --help This help.
28
+
29
+ Example:
30
+ Fill vending machine with exemplary NFT collection:
31
+
32
+ $ vendi fill --collection TestBudz --price 10000000 --nft-count 100
33
+
34
+ Now check out $HOME/.vendi-nft-machine/TestBudz, refine configs as you prefer.
35
+ When ready start vending machine:
36
+
37
+ $ vendi serve --collection TestBudz
38
+
39
+ DOCOPT
40
+
41
+ begin
42
+ o = Docopt.docopt(doc)
43
+
44
+ warn Vendi::VERSION if o['--version']
45
+
46
+ if o['fill']
47
+ collection_name = o['--collection']
48
+ price = o['--price']
49
+ nft_count = o['--nft-count']
50
+ wallet_port = o['--wallet-port']
51
+ vendi = Vendi.init({ port: wallet_port.to_i })
52
+ begin
53
+ if File.directory?(File.join(vendi.config_dir, collection_name))
54
+ $stdout.print "Collection '#{collection_name}' already exists do you want to overwrite? (y/n): "
55
+ yn = $stdin.gets.chomp
56
+ raise Interrupt unless %w[y Y].include?(yn)
57
+ end
58
+ vendi.fill(collection_name, price, nft_count)
59
+ rescue StandardError => e
60
+ vendi.logger.error e.message
61
+ rescue Interrupt
62
+ warn ''
63
+ vendi.logger.info 'Vending machine filling stopped.'
64
+ end
65
+ end
66
+
67
+ if o['serve']
68
+ collection_name = o['--collection']
69
+ wallet_port = o['--wallet-port']
70
+ logfile = o['--logfile']
71
+ vendi = if logfile
72
+ Vendi.init({ port: wallet_port.to_i }, :info, logfile)
73
+ else
74
+ Vendi.init({ port: wallet_port.to_i })
75
+ end
76
+ begin
77
+ vendi.serve(collection_name)
78
+ rescue Errno::ECONNREFUSED => e
79
+ # retry if cannot connect to cardano-wallet
80
+ vendi.logger.error e.message
81
+ sleep 5
82
+ retry
83
+ rescue StandardError => e
84
+ vendi.logger.error e.message
85
+ rescue Interrupt
86
+ vendi.logger.info 'Vending machine stopped.'
87
+ end
88
+ end
89
+ rescue Docopt::Exit => e
90
+ puts e.message
91
+ end
@@ -0,0 +1,245 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vendi
4
+ # Vending Machine: fill it with NFTs and serve to the hungry and in need!
5
+ class Machine
6
+ include Vendi::Utils
7
+ include Vendi::Monitor
8
+ include Vendi::Minter
9
+
10
+ attr_reader :logger, :cw
11
+ attr_accessor :config_dir
12
+
13
+ def initialize(wallet_opts = {}, log_level = :info, log_file = nil)
14
+ @cw = CardanoWallet.new(wallet_opts)
15
+ progname = 'vendi'
16
+ datetime_format = '%Y-%m-%d %H:%M:%S'
17
+ @logger = if log_file
18
+ Logger.new(log_file,
19
+ 'daily',
20
+ # shift_size = 10,
21
+ progname: progname,
22
+ level: log_level,
23
+ datetime_format: datetime_format)
24
+ else
25
+ Logger.new($stdout,
26
+ progname: progname,
27
+ level: log_level,
28
+ datetime_format: datetime_format)
29
+ end
30
+ @config_dir = File.join(Dir.home, '.vendi-nft-machine')
31
+ end
32
+
33
+ def collection_dir(collection_name)
34
+ File.join(@config_dir, collection_name)
35
+ end
36
+
37
+ def config_path(collection_name)
38
+ File.join(collection_dir(collection_name), 'config.json')
39
+ end
40
+
41
+ def metadata_path(collection_name)
42
+ File.join(collection_dir(collection_name), 'metadata.json')
43
+ end
44
+
45
+ def metadata_vending_path(collection_name)
46
+ File.join(collection_dir(collection_name), 'metadata-vending.json')
47
+ end
48
+
49
+ def metadata_sent_path(collection_name)
50
+ File.join(collection_dir(collection_name), 'metadata-sent.json')
51
+ end
52
+
53
+ def config(collection_name)
54
+ from_json(config_path(collection_name))
55
+ end
56
+
57
+ def metadata_vending(collection_name)
58
+ from_json(metadata_vending_path(collection_name))
59
+ end
60
+
61
+ def metadata_sent(collection_name)
62
+ from_json(metadata_sent_path(collection_name))
63
+ end
64
+
65
+ def metadata(collection_name)
66
+ from_json(metadata_path(collection_name))
67
+ end
68
+
69
+ def set_metadata(collection_name, metadata)
70
+ to_json(metadata_path(collection_name), metadata)
71
+ end
72
+
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)
79
+ end
80
+
81
+ def set_config(collection_name, configuration)
82
+ to_json(config_path(collection_name), configuration)
83
+ end
84
+
85
+ # Fill vending machine with exemplary set of CIP-25 metadata for minting,
86
+ # set up basic config and create wallet for minting
87
+ def fill(collection_name, price, nft_count, skip_wallet: false)
88
+ FileUtils.mkdir_p(collection_dir(collection_name))
89
+ if skip_wallet
90
+ @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: '' }
97
+ else
98
+ @logger.info('Generating wallet for your collection.')
99
+ wallet_details = create_wallet("Vendi wallet - #{collection_name}")
100
+ end
101
+
102
+ @logger.info("Generating your NFT collection config into #{config_path(collection_name)}.")
103
+ @logger.info("NFT price: #{as_ada(price.to_i)}.")
104
+ config = { price: price.to_i }
105
+ mnemonics = wallet_details[:wallet_mnemonics]
106
+ wallet_details.delete(:wallet_mnemonics)
107
+ config.merge!(wallet_details)
108
+ set_config(collection_name, config)
109
+
110
+ @logger.info("Generating exemplary CIP-25 metadata set into #{metadata_path(collection_name)}.")
111
+ metadatas = generate_metadata(collection_name, nft_count.to_i)
112
+ set_metadata(collection_name, metadatas)
113
+
114
+ @logger.info('IMPORTANT NOTES! 👇')
115
+ @logger.info('----------------')
116
+ @logger.info("Check contents of #{collection_dir(collection_name)} and edit files as needed.")
117
+ @logger.info('Before starting vending machine make sure your wallet is synced and has enough funds.')
118
+ @logger.info("To fund your wallet send ADA to: #{wallet_details[:wallet_address]}")
119
+ @logger.info("❗ Write down your wallet mnemonics: #{mnemonics}.")
120
+ end
121
+
122
+ # Turn on vending machine and make it serve NFTs for anyone who dares to
123
+ # pay the 'price' to the 'address', that is specified in the config_file
124
+ def serve(collection_name)
125
+ set_metadata_sent(collection_name, {}) unless File.exist?(metadata_sent_path(collection_name))
126
+
127
+ c = config(collection_name)
128
+ wid = c[:wallet_id]
129
+ pass = c[:wallet_pass]
130
+ address = c[:wallet_address]
131
+ policy_id = c[:wallet_policy_id]
132
+ price = c[:price]
133
+ wallet = @cw.shelley.wallets.get(wid)
134
+
135
+ raise "Wallet #{wid} does not exist!" if wallet.code == 404
136
+ raise "Wallet #{wid} is not synced (#{wallet['state']})!" if wallet['state']['status'] != 'ready'
137
+ raise "Wallet #{wid} has no funds!" if (wallet['balance']['available']['quantity']).zero?
138
+
139
+ @logger.info 'Vending machine started.'
140
+ @logger.info "Wallet id: #{wid}"
141
+ @logger.info "Address: #{address}"
142
+ @logger.info "NFT price: #{as_ada(price)}"
143
+ @logger.info "Original NFT stock: #{metadata(collection_name).size}"
144
+ @logger.info '----------------'
145
+ unless File.exist?(metadata_vending_path(collection_name))
146
+ @logger.info "Making copy of #{metadata_path(collection_name)} to #{metadata_vending_path(collection_name)}."
147
+ FileUtils.cp(metadata_path(collection_name), metadata_vending_path(collection_name))
148
+ end
149
+
150
+ txs = get_incoming_txs(wid)
151
+ until metadata_vending(collection_name).empty?
152
+ nfts = metadata_vending(collection_name)
153
+ nfts_sent = metadata_sent(collection_name)
154
+ 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)}]"
156
+
157
+ txs_new = get_incoming_txs(wid)
158
+ if txs.size < txs_new.size
159
+ txs_to_check = get_transactions_to_process(tx_delta, txs)
160
+ @logger.info "New txs arrived: #{txs_to_check.size}"
161
+ @logger.info (txs_to_check.map { |t| t['id'] }).to_s
162
+
163
+ txs_to_check.each do |t|
164
+ @logger.info "Checking #{t['id']}"
165
+ if incoming_tx_ok?(t, address, price)
166
+ @logger.info 'OK! VENDING!'
167
+ @logger.info '----------------'
168
+ 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)
180
+ if outgoing_tx_ok?(tx_res)
181
+ mint_tx_id = tx_res.last['id']
182
+ wait_for_tx_in_ledger(wid, mint_tx_id)
183
+ # update metadata files
184
+ update_metadata_files(nfts, key, metadata_vending_path(collection_name), metadata_sent_path(collection_name))
185
+ else
186
+ @logger.error 'Minting tx failed!'
187
+ @logger.error "Construct tx: #{JSON.pretty_generate(tx_res[0])}"
188
+ @logger.error "Sign tx: #{JSON.pretty_generate(tx_res[1])}"
189
+ @logger.error "Submit tx: #{JSON.pretty_generate(tx_res[2])}"
190
+ end
191
+ @logger.info '----------------'
192
+
193
+ else
194
+ @logger.warn "NO GOOD! NOT VENDING! Tx: #{t['id']}"
195
+ end
196
+ end
197
+
198
+ txs = txs_new
199
+ end
200
+
201
+ sleep 5
202
+ end
203
+ @logger.info 'Turning off! Vending machine empty!'
204
+ end
205
+
206
+ private
207
+
208
+ def create_wallet(wallet_name = nil, wallet_pass = nil, wallet_mnemonics = nil)
209
+ wallet_name ||= 'Vendi wallet'
210
+ wallet_mnemonics ||= @cw.utils.mnemonic_sentence
211
+ wallet_pass ||= 'Secure Passphrase'
212
+ wallet = @cw.shelley.wallets.create({ name: wallet_name,
213
+ mnemonic_sentence: wallet_mnemonics,
214
+ passphrase: wallet_pass })
215
+
216
+ @logger.debug('!!!!! Write down wallet mnemonics !!!!')
217
+ @logger.debug(wallet_mnemonics.to_s)
218
+ wid = wallet['id']
219
+ wallet_address = @cw.shelley.addresses.list(wid).first['id']
220
+ wallet_policy_id = @cw.shelley.keys.create_policy_id(wid, Vendi::POLICY_SCRIPT_TEMPLATE)['policy_id']
221
+ { wallet_id: wallet['id'],
222
+ wallet_name: wallet_name,
223
+ wallet_pass: wallet_pass,
224
+ wallet_address: wallet_address,
225
+ wallet_policy_id: wallet_policy_id,
226
+ wallet_mnemonics: wallet_mnemonics }
227
+ end
228
+
229
+ def generate_metadata(nft_name_prefix, nft_count)
230
+ metadata = {}
231
+ 1.upto nft_count do |i|
232
+ nft_metadata = {
233
+ "#{nft_name_prefix}_#{i}" => {
234
+ 'name' => "#{nft_name_prefix.upcase} No #{i}",
235
+ 'image' => 'ipfs://QmRhTTbUrPYEw3mJGGhQqQST9k86v1DPBiTTWJGKDJsVFw',
236
+ 'Copyright' => "Vendi #{Time.now.year}",
237
+ 'Collection' => "#{nft_name_prefix} #{Time.now.year}"
238
+ }
239
+ }
240
+ metadata.merge!(nft_metadata)
241
+ end
242
+ metadata
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vendi
4
+ # helper methods for minting NFT
5
+ module Minter
6
+ def prepare_metadata(nfts, key, policy_id)
7
+ {
8
+ '721' => {
9
+ policy_id => {
10
+ key => nfts[key]
11
+ }
12
+ }
13
+ }
14
+ end
15
+
16
+ def update_metadata_files(nfts, key, metadata_vending_file, metadata_sent_file)
17
+ # metadata sent
18
+ m = { key => nfts[key] }
19
+ if File.exist? metadata_sent_file
20
+ sent = from_json(metadata_sent_file)
21
+ sent.merge!(m)
22
+ to_json(metadata_sent_file, sent)
23
+ else
24
+ to_json(metadata_sent_file, m)
25
+ end
26
+ # metadata available
27
+ nfts.delete(key)
28
+ to_json(metadata_vending_file, nfts)
29
+ end
30
+
31
+ # 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]
38
+ end
39
+
40
+ # Construct -> Sign -> Submit
41
+ def construct_sign_submit(wid, pass, metadata, mint_payload)
42
+ tx_constructed = @cw.shelley.transactions.construct(wid,
43
+ nil,
44
+ nil,
45
+ metadata,
46
+ nil,
47
+ mint_payload,
48
+ nil)
49
+ # puts tx_constructed.request.options[:body]
50
+ tx_signed = @cw.shelley.transactions.sign(wid, pass, tx_constructed['transaction'])
51
+ # puts tx_signed
52
+ tx_submitted = @cw.shelley.transactions.submit(wid, tx_signed['transaction'])
53
+ # puts tx_submitted
54
+
55
+ [tx_constructed, tx_signed, tx_submitted]
56
+ end
57
+
58
+ def outgoing_tx_ok?(tx_res)
59
+ tx_constructed, tx_signed, tx_submitted = tx_res
60
+ tx_constructed.code == 202 && tx_signed.code == 202 && tx_submitted.code == 202
61
+ end
62
+
63
+ def wait_for_tx_in_ledger(wid, tx_id)
64
+ eventually "Tx #{tx_id} is in ledger" do
65
+ @logger.info "Waiting for #{tx_id} to get in_ledger"
66
+ tx = @cw.shelley.transactions.get(wid, tx_id)
67
+ tx.code == 200 && tx['status'] == 'in_ledger'
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vendi
4
+ # helper methods for monitoring incoming transactions
5
+ module Monitor
6
+ def get_incoming_txs(wid)
7
+ @cw.shelley.transactions.list(wid).select { |t| t['direction'] == 'incoming' }
8
+ end
9
+
10
+ def get_transactions_to_process(txs_new, txs)
11
+ txs_new[0..(txs_new.size - txs.size - 1)]
12
+ end
13
+
14
+ # incoming tx is correct when the address is on any of the outputs (means that someone was sending to it)
15
+ # and tx amount is >= price set in the config
16
+ def incoming_tx_ok?(tx, address, price)
17
+ (tx['outputs'].any? { |o| (o['address'] == address) }) && (tx['amount']['quantity'] >= price)
18
+ end
19
+
20
+ # trying to naively get address to send back NFT, take first address from the output that isn't our address
21
+ # assuming that it is a change address
22
+ def get_dest_addr(tx, address)
23
+ output_dest_addr = tx['outputs'].map { |o| o['address'] }.reject { |a| a == address }.first
24
+
25
+ # if no address in outputs try to get one from inputs
26
+ output_dest_addr || tx['inputs'].map { |o| o['address'] }.reject { |a| a == address }.first
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vendi
4
+ # general utility methods
5
+ module Utils
6
+ def from_json(file)
7
+ JSON.parse(File.read(file), { symbolize_names: true })
8
+ end
9
+
10
+ def to_json(file, hash)
11
+ File.write(file, JSON.pretty_generate(hash))
12
+ end
13
+
14
+ def as_ada(quantity)
15
+ res = quantity.to_f / 1_000_000
16
+ str_res = format('%.7f', res)
17
+ last = str_res[-1]
18
+ final = ''
19
+ until last == '.'
20
+ str_res.chop!
21
+ if last == '0'
22
+ final = str_res
23
+ break unless final[-1] == '0'
24
+ end
25
+ last = str_res[-1]
26
+ end
27
+ final.chop! if final[-1] == '.'
28
+ "#{final} ₳"
29
+ end
30
+
31
+ ##
32
+ # wait until action passed as &block returns true or TIMEOUT is reached
33
+ def eventually(label, &block)
34
+ current_time = Time.now
35
+ timeout_treshold = current_time + Vendi::TIMEOUT
36
+ while (block.call == false) && (current_time <= timeout_treshold)
37
+ sleep 5
38
+ current_time = Time.now
39
+ end
40
+ if current_time > timeout_treshold
41
+ @logger.error "Action '#{label}' did not resolve within timeout: #{Vendi::TIMEOUT}s"
42
+ false
43
+ else
44
+ true
45
+ end
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
+ end
54
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vendi
4
+ VERSION = '0.1.0'
5
+ end
data/lib/vendi.rb ADDED
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+ require 'fileutils'
5
+ require 'cardano_wallet'
6
+ require 'vendi/version'
7
+ require 'vendi/utils'
8
+ require 'vendi/monitor'
9
+ require 'vendi/minter'
10
+ require 'vendi/machine'
11
+
12
+ # Vendi CNFT Vending machine
13
+ module Vendi
14
+ # Timeout for tx to get into ledger
15
+ TIMEOUT = 300
16
+
17
+ POLICY_SCRIPT_TEMPLATE = 'cosigner#0'
18
+
19
+ def self.init(wallet_opts = {}, log_level = :info, logfile = nil)
20
+ Vendi::Machine.new(wallet_opts, log_level, logfile)
21
+ end
22
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vendi
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Piotr Stachyra
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2022-10-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cardano_wallet
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 0.3.28
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 0.3.28
27
+ - !ruby/object:Gem::Dependency
28
+ name: docopt
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 0.6.1
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 0.6.1
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 12.3.3
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 12.3.3
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - '='
60
+ - !ruby/object:Gem::Version
61
+ version: 3.11.0
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - '='
67
+ - !ruby/object:Gem::Version
68
+ version: 3.11.0
69
+ description: CNFT Vending Machine - cardano-wallet based
70
+ email:
71
+ - piotr.stachyra@gmail.com
72
+ executables:
73
+ - vendi
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - LICENSE.txt
78
+ - README.md
79
+ - bin/console
80
+ - bin/rebuild
81
+ - bin/setup
82
+ - bin/vendi
83
+ - lib/vendi.rb
84
+ - lib/vendi/machine.rb
85
+ - lib/vendi/minter.rb
86
+ - lib/vendi/monitor.rb
87
+ - lib/vendi/utils.rb
88
+ - lib/vendi/version.rb
89
+ homepage: https://github.com/piotr-iohk/vendi
90
+ licenses:
91
+ - MIT
92
+ metadata:
93
+ allowed_push_host: https://rubygems.org/
94
+ homepage_uri: https://github.com/piotr-iohk/vendi
95
+ source_code_uri: https://github.com/piotr-iohk/vendi
96
+ changelog_uri: https://github.com/piotr-iohk/vendi
97
+ rubygems_mfa_required: 'true'
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ - bin
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: 2.7.0
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.3.7
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: CNFT Vending Machine - cardano-wallet based
118
+ test_files: []