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 +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: []
|