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 +4 -4
- data/README.md +6 -1
- data/bin/vendi +7 -4
- data/lib/vendi/machine.rb +59 -30
- data/lib/vendi/minter.rb +77 -14
- data/lib/vendi/monitor.rb +3 -1
- data/lib/vendi/utils.rb +0 -6
- data/lib/vendi/version.rb +1 -1
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a41b9c15bcb8f4c01eaf41358958fd00b4afa10da4a48b86fdd1463a0c7f0605
|
4
|
+
data.tar.gz: c3ab3e68705decb0ea6a9031eddcb77689fdbff85dae62d0436c0ed512e6c95d
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7c37116a06be27482dc5e330bc8bd652b1eeeeb599c338c6ceeb9e11f2f259c15c18dede31fe652c48e6a0345cf55203550a83e584888965f5dc776748aba229
|
7
|
+
data.tar.gz: 8ab473e74a1090701beecb4815a6fa7ac2d2040a585c6ad7529eee89c072ac1bd6da1498b486f694b994ba65e813c56f4a8c565952e98bea96b40d8dd87e5b4d
|
data/README.md
CHANGED
@@ -1,9 +1,14 @@
|
|
1
1
|
# Vendi
|
2
2
|
|
3
|
+
[](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
|
-
|
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
|
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
|
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
|
74
|
-
|
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
|
82
|
-
to_json(
|
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 =
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
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 "
|
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
|
-
|
171
|
-
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
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(
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
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) }) &&
|
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
data/lib/vendi/version.rb
CHANGED
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.
|
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-
|
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.
|
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
|