Skip to content

Commit

Permalink
Merge pull request #1 from piotr-iohk/vend-max
Browse files Browse the repository at this point in the history
Vend max
  • Loading branch information
piotr-iohk authored Nov 8, 2022
2 parents c7eee34 + 5f37177 commit 84f1e7d
Show file tree
Hide file tree
Showing 13 changed files with 239 additions and 85 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ jobs:
- name: '🔨 Build'
run: |
gem install yard
readme_link=https://github.com/piotr-iohk/cardano-up/blob/${{ steps.versions.outputs.version }}/README.md
readme_link=https://github.com/piotr-iohk/vendi/blob/${{ steps.versions.outputs.version }}/README.md
yard doc --title "Documentation for Vendi (${{ steps.versions.outputs.version }})"
sed -i "s|<a href=\"index.html\" title=\"README\">|<a href=\"$readme_link\" title=\"README\">|" ./doc/_index.html
cp ./doc/_index.html ./doc/index.html
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@
Vendi is simple CNFT vending machine based on [`cardano-wallet`](https://github.com/input-output-hk/cardano-wallet).
You need to have `cardano-wallet` started and synced.

It... seems to work, check out the
👉 **[Demo](http://185.201.114.10:4321)** 👈 and [how it was done](https://github.com/piotr-iohk/vendi/tree/master/demo#how-it-was-done).
Check out the
👉 **[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).

## Installation

Expand Down
11 changes: 7 additions & 4 deletions bin/vendi
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ doc = <<~DOCOPT
Vendi - CNFT Vending Machine.
Usage:
#{File.basename(__FILE__)} fill --collection <name> --price <lovelace> --nft-count <int> [--wallet-port <port>] [--skip-wallet-creation]
#{File.basename(__FILE__)} serve --collection <name> [--wallet-port <port>] [--logfile <file>]
#{File.basename(__FILE__)} fill --collection <name> --price <lovelace> --nft-count <int> [--wallet-port <port>] [--skip-wallet]
#{File.basename(__FILE__)} serve --collection <name> [--vend-max <int>] [--wallet-port <port>] [--logfile <file>]
#{File.basename(__FILE__)} -v | --version
#{File.basename(__FILE__)} -h | --help
Expand All @@ -19,9 +19,11 @@ doc = <<~DOCOPT
serve Start vending machine.
--collection <name> Name of the collection.
--price <lovelace> Single NFT price in lovelace.
--vend-max <int> Max number of NFTs vended in single transaction [default: 1].
--nft-count <int> How many NFTs would you like to generate.
--wallet-port <port> Cardano-wallet port [default: 8090].
--logfile <file> Logfile (will be rotated daily).
--skip-wallet Skip creation of wallet when filling vending machine.
-v --version Check #{File.basename(__FILE__)} version.
-h --help This help.
Expand All @@ -47,7 +49,7 @@ begin
price = o['--price']
nft_count = o['--nft-count']
wallet_port = o['--wallet-port']
skip_wallet = o['--skip-wallet-creation']
skip_wallet = o['--skip-wallet']
vendi = Vendi.init({ port: wallet_port.to_i })
begin
if File.directory?(File.join(vendi.config_dir, collection_name))
Expand All @@ -72,13 +74,14 @@ begin
collection_name = o['--collection']
wallet_port = o['--wallet-port']
logfile = o['--logfile']
vend_max = o['--vend-max']
vendi = if logfile
Vendi.init({ port: wallet_port.to_i }, :info, logfile)
else
Vendi.init({ port: wallet_port.to_i })
end
begin
vendi.serve(collection_name)
vendi.serve(collection_name, vend_max)
rescue Errno::ECONNREFUSED => e
# retry if cannot connect to cardano-wallet
vendi.logger.error e.message
Expand Down
20 changes: 10 additions & 10 deletions demo/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,32 @@

Vendi demo is available [HERE](http://185.201.114.10:4321).

## How it was done
## How it was prepared

We have used [cardano-up](https://github.com/piotr-iohk/cardano-up) to spin up `cardano-node` and `cardano-wallet` on `preview` and `preprod` networks. That was easy!
I have used [cardano-up](https://github.com/piotr-iohk/cardano-up) to spin up `cardano-node` and `cardano-wallet` on `preview` and `preprod` networks. That was easy!

$ cardano-up preview up --port 8090
$ cardano-up preprod up --port 8091

Preview wallet works on port `8090`, preprod one on `8091`. Ok.

We have filled Vendi with exemplary set of NFTs for both networks:
I have filled Vendi with exemplary set of NFTs for both networks:

$ vendi fill --collection TestBudzPreview --price 10000000 --nft-count 100 --wallet-port 8090
$ vendi fill --collection TestBudzPreprod --price 10000000 --nft-count 100 --wallet-port 8091

Vendi told us which address we need to fund for each vending machine and that's what we did. Thank you [Faucet](https://docs.cardano.org/cardano-testnet/tools/faucet)!
Vendi told me which address I need to fund for each vending machine and that's what I did. Thank you [Faucet](https://docs.cardano.org/cardano-testnet/tools/faucet)!

We waited few minutes until our wallets synced and started our vending machines:
I waited few minutes until wallets synced and started vending machines:

$ vendi serve --collection TestBudzPreview --wallet-port 8090 --logfile preview.log
$ vendi serve --collection TestBudzPreprod --wallet-port 8091 --logfile preprod.log
$ vendi serve --collection TestBudzPreview --vend-max 5 --wallet-port 8090 --logfile preview.log
$ vendi serve --collection TestBudzPreprod --vend-max 5 --wallet-port 8091 --logfile preprod.log

We wanted our logs to be available on our demo frontend. For that we used [frontail](https://github.com/mthenw/frontail). Very cool!
I wanted the logs to be available on the demo frontend. For that I used [frontail](https://github.com/mthenw/frontail). Very cool!

$ frontail -p 9001 preview.log
$ frontail -p 9002 preprod.log

Finally we have started our demo frontend from this folder! :tada:
Finally I have started demo frontend from this folder! :tada:

$ HOST=<our_ip> ruby demo.rb
$ HOST=<my_ip> ruby demo.rb
4 changes: 2 additions & 2 deletions demo/demo.rb
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@

get '/preview' do
frontail_url = "http://#{ENV.fetch('HOST', nil)}:9001/"
price = @vendi.as_ada(@preview_config[:price])
price = @preview_config[:price]
address = @preview_config[:wallet_address]
policy_id = @preview_config[:wallet_policy_id]
erb :demo, { locals: { frontail_url: frontail_url,
Expand All @@ -37,7 +37,7 @@

get '/preprod' do
frontail_url = "http://#{ENV.fetch('HOST', nil)}:9002/"
price = @vendi.as_ada(@preprod_config[:price])
price = @preprod_config[:price]
address = @preprod_config[:wallet_address]
policy_id = @preprod_config[:wallet_policy_id]
erb :demo, { locals: { frontail_url: frontail_url,
Expand Down
10 changes: 9 additions & 1 deletion demo/views/demo.erb
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,16 @@
<h3><%= network.capitalize %></h3>
</center>
<center>
Send <b><%= price %></b> to <b><%= address %></b> and get back NFT from Vendi!
Send <b><%= @vendi.as_ada(price) %></b> to <b><%= address %></b> and get NFT back from Vendi!
</center>
<center>
Now you can get even up to <b>5</b> NFTs at once!
</center>
<% 1.upto 5 do |i|%>
<center>
Send <b><%= @vendi.as_ada(price * i) %></b> to get <b><%= i %></b> NFT<%= 's' if i != 1 %> back.
</center>
<% end %>
<br/>
<center>
Policy id: <a target="_blank" href="https://<%= network %>.cexplorer.io/policy/<%= policy_id %>"><%= policy_id %></a>.
Expand Down
2 changes: 1 addition & 1 deletion demo/views/layout.erb
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
<nav id="sidebarMenu" class="col-md-3 col-lg-2 d-md-block bg-light sidebar collapse">
<div class="position-sticky pt-3 sidebar-sticky">
<h6 class="sidebar-heading d-flex justify-content-between align-items-center px-3 mt-4 mb-1 text-muted text-uppercase">
<span>Vendi Demo</span>
<span>Vendi Demo (v<%= Vendi::VERSION %>)</span>
</h6>
<ul class="nav flex-column">
<li class="nav-item">
Expand Down
89 changes: 59 additions & 30 deletions lib/vendi/machine.rb
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,34 @@ def metadata_sent_path(collection_name)
File.join(collection_dir(collection_name), 'metadata-sent.json')
end

def failed_mints_path(collection_name)
File.join(collection_dir(collection_name), 'failed-mints.json')
end

def config(collection_name)
from_json(config_path(collection_name))
end

def set_config(collection_name, configuration)
to_json(config_path(collection_name), configuration)
end

def metadata_vending(collection_name)
from_json(metadata_vending_path(collection_name))
end

def set_metadata_vending(collection_name, metadata)
to_json(metadata_vending_path(collection_name), metadata)
end

def metadata_sent(collection_name)
from_json(metadata_sent_path(collection_name))
end

def set_metadata_sent(collection_name, metadata)
to_json(metadata_sent_path(collection_name), metadata)
end

def metadata(collection_name)
from_json(metadata_path(collection_name))
end
Expand All @@ -70,16 +86,12 @@ def set_metadata(collection_name, metadata)
to_json(metadata_path(collection_name), metadata)
end

def set_metadata_sent(collection_name, metadata)
to_json(metadata_sent_path(collection_name), metadata)
end

def set_metadata_vending(collection_name, metadata)
to_json(metadata_vending_path(collection_name), metadata)
def failed_mints(collection_name)
from_json(failed_mints_path(collection_name))
end

def set_config(collection_name, configuration)
to_json(config_path(collection_name), configuration)
def set_failed_mints(collection_name, failed_mints)
to_json(failed_mints_path(collection_name), failed_mints)
end

# Fill vending machine with exemplary set of CIP-25 metadata for minting,
Expand All @@ -88,12 +100,18 @@ def fill(collection_name, price, nft_count, skip_wallet: false)
FileUtils.mkdir_p(collection_dir(collection_name))
if skip_wallet
@logger.info('Skipping wallet generation for your collection.')
wallet_details = { wallet_id: '',
wallet_name: '',
wallet_pass: '',
wallet_address: '',
wallet_policy_id: '',
wallet_mnemonics: '' }
wallet_details = if File.exist?(config_path(collection_name))
c = config(collection_name)
c.delete(:price)
c
else
{ wallet_id: '',
wallet_name: '',
wallet_pass: '',
wallet_address: '',
wallet_policy_id: '',
wallet_mnemonics: '' }
end
else
@logger.info('Generating wallet for your collection.')
wallet_details = create_wallet("Vendi wallet - #{collection_name}")
Expand All @@ -110,6 +128,9 @@ def fill(collection_name, price, nft_count, skip_wallet: false)
@logger.info("Generating exemplary CIP-25 metadata set into #{metadata_path(collection_name)}.")
metadatas = generate_metadata(collection_name, nft_count.to_i)
set_metadata(collection_name, metadatas)
set_metadata_vending(collection_name, metadatas)
set_metadata_sent(collection_name, {})
set_failed_mints(collection_name, {})

@logger.info('IMPORTANT NOTES! 👇')
@logger.info('----------------')
Expand All @@ -121,12 +142,11 @@ def fill(collection_name, price, nft_count, skip_wallet: false)

# Turn on vending machine and make it serve NFTs for anyone who dares to
# pay the 'price' to the 'address', that is specified in the config_file
def serve(collection_name)
def serve(collection_name, vend_max = 1)
set_metadata_sent(collection_name, {}) unless File.exist?(metadata_sent_path(collection_name))

c = config(collection_name)
wid = c[:wallet_id]
pass = c[:wallet_pass]
address = c[:wallet_address]
policy_id = c[:wallet_policy_id]
price = c[:price]
Expand All @@ -138,8 +158,10 @@ def serve(collection_name)

@logger.info 'Vending machine started.'
@logger.info "Wallet id: #{wid}"
@logger.info "Policy id: #{policy_id}"
@logger.info "Address: #{address}"
@logger.info "NFT price: #{as_ada(price)}"
@logger.info "Vend max NFTs: #{vend_max}"
@logger.info "Original NFT stock: #{metadata(collection_name).size}"
@logger.info '----------------'
unless File.exist?(metadata_vending_path(collection_name))
Expand All @@ -152,7 +174,12 @@ def serve(collection_name)
nfts = metadata_vending(collection_name)
nfts_sent = metadata_sent(collection_name)
wallet_balance = @cw.shelley.wallets.get(wid)['balance']['available']['quantity']
@logger.info "Vending machine [In stock: #{nfts.size}, Sent: #{nfts_sent.size}, NFT price: #{as_ada(price)}, Balance: #{as_ada(wallet_balance)}]"
@logger.info "[In stock: #{nfts.size}, Sent: #{nfts_sent.size}, NFT price: #{as_ada(price)}, Vend max: #{vend_max}, Balance: #{as_ada(wallet_balance)}]"
n = @cw.misc.network.information
unless n['sync_progress']['status'] == 'ready'
@logger.error "Network is not synced (#{n['sync_progress']['status']} #{n['sync_progress']['progress']['quantity']}%), waiting..."
sleep 5
end

txs_new = get_incoming_txs(wid)
if txs.size < txs_new.size
Expand All @@ -166,32 +193,34 @@ def serve(collection_name)
@logger.info 'OK! VENDING!'
@logger.info '----------------'
dest_addr = get_dest_addr(t, address)

# prepare metadata and mint payload
key = nfts.keys.sample
metadata = prepare_metadata(nfts, key, policy_id)
mint = mint_payload(asset_name(key.to_s), dest_addr)
@logger.debug JSON.pretty_generate(metadata)
@logger.debug JSON.pretty_generate(mint)

# mint
@logger.info "Minting NFT: #{key} to #{dest_addr}"
tx_res = construct_sign_submit(wid, pass, metadata, mint)
minted = mint_nft(collection_name, t['amount']['quantity'], vend_max, dest_addr)
tx_res = minted[:tx_res]
keys = minted[:keys]
if outgoing_tx_ok?(tx_res)
mint_tx_id = tx_res.last['id']
wait_for_tx_in_ledger(wid, mint_tx_id)
# update metadata files
update_metadata_files(nfts, key, metadata_vending_path(collection_name), metadata_sent_path(collection_name))
update_metadata_files(keys, collection_name)
else
@logger.error 'Minting tx failed!'
@logger.error "Construct tx: #{JSON.pretty_generate(tx_res[0])}"
@logger.error "Sign tx: #{JSON.pretty_generate(tx_res[1])}"
@logger.error "Submit tx: #{JSON.pretty_generate(tx_res[2])}"
@logger.warn "Updating #{failed_mints_path(collection_name)} file."
update_failed_mints(collection_name, t['id'], tx_res, keys)
end
@logger.info '----------------'

else
@logger.warn "NO GOOD! NOT VENDING! Tx: #{t['id']}"
amt = t['amount']['quantity']
@logger.warn "NOT VENDING! Amt: #{as_ada(amt)}, Tx: #{t['id']}"
@logger.warn "Updating #{failed_mints_path(collection_name)} file."
reason = if amt.to_i < price.to_i
"wrong_amount = #{as_ada(amt)}"
else
"wrong_address = #{address} was not in incoming tx outputs"
end
update_failed_mints(collection_name, t['id'], reason, keys)
end
end

Expand Down
Loading

0 comments on commit 84f1e7d

Please sign in to comment.