vendi 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/vendi.svg)](https://badge.fury.io/rb/vendi)
|
4
|
+
|
5
|
+
## Overview
|
6
|
+
|
3
7
|
Vendi is simple CNFT vending machine based on [`cardano-wallet`](https://github.com/input-output-hk/cardano-wallet).
|
4
8
|
You need to have `cardano-wallet` started and synced.
|
5
9
|
|
6
|
-
|
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
|