vendi 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +38 -0
- data/bin/console +15 -0
- data/bin/rebuild +3 -0
- data/bin/setup +8 -0
- data/bin/vendi +91 -0
- data/lib/vendi/machine.rb +245 -0
- data/lib/vendi/minter.rb +71 -0
- data/lib/vendi/monitor.rb +29 -0
- data/lib/vendi/utils.rb +54 -0
- data/lib/vendi/version.rb +5 -0
- data/lib/vendi.rb +22 -0
- metadata +118 -0
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
data/bin/setup
ADDED
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
|
data/lib/vendi/minter.rb
ADDED
@@ -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
|
data/lib/vendi/utils.rb
ADDED
@@ -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
|
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: []
|