Skip to content

Commit

Permalink
Merge branch 'paulccari/mui-replacing-rp-components-4' into paulccari…
Browse files Browse the repository at this point in the history
…/mui-replacing-rp-components-5
  • Loading branch information
paulclindo committed Oct 12, 2021
2 parents 3f0d34a + f3cab8b commit 02b194a
Show file tree
Hide file tree
Showing 191 changed files with 8,480 additions and 951 deletions.
38 changes: 38 additions & 0 deletions docs/readme/cardano-token-deposit-explained.md
@@ -0,0 +1,38 @@
# Cardano Token Deposit Explained

## Intro

Storing any assets on the Cardano blockchain "costs" some money, but you are not actually paying them to anyone, instead you are just depositing them as a collateral to ensure that you have some motivation to not leave lots of worthless assets just lying around forever and forget about them.

For example, at the moment it takes at least ~1 ADA to store any ADA, which might sound weird, but in reality it just means that you cannot send any amount less than 1 ADA (for now, but it can be changed at any moment as a simple protocol parameter). This ensures that any non-empty wallet will have at least 1 ADA in it, which might be enough motivation to not just leave it there and rather get it out and send it into circulation.

Similar to that, storing any native tokens or NFTs requires having a minimum deposit of ADA and this number scales depending on how the assets are being stored in the wallet.

## "Storage boxes" analogy

Whenever someone else sends you a token or an NFT, you can imagine it as if you are receiving a separate new box with this token and some ADA in it. This is due to the Cardano blockchain having the UTxO technical model, more similar to Bitcoin and different from Ethereum. The received box is sitting in the wallet separately from all other boxes received previously, and the box is required to have enough ADA deposited to store the ADA itself and the token.

If you have received many NFTs or separate token transactions, your wallet has many separate small such boxes where each box has 1 NFT, for example, and enough ADA as a deposit for that NFT, for example 1.5 ADA. Any of these small boxes are not spendable by themselves, e.g. you cannot just send someone a 1.5 ADA box with an NFT, because the amount after paying the transaction fees might not be enough as the required deposit for a box like that.

This way, if you have received 20 NFTs as 20 separate boxes with 1.5 ADA in each box - Yoroi will display that you have ~30 ADA of **locked asset deposit**. So, for example, if you have 100 ADA in total and you see that you have 30 ADA of locked asset deposit - it might be **hard** to spend anything more than 70 ADA. Hard, but still possible, due to repackaging.

## "Repackaging"

The goods can be repackaged from existing boxes to new tighter boxes, though, and the main reason for it would be that storing multiple tokens together in the same box required **SMALLER** minimum deposit, than storing them separately. Storing a single NFT in a box might require 1.5 ADA for each one, but storing 5 NFTs together in one box might require ~3 ADA in total, instead of 7.5 ADA in 5 separate boxes.

This repackaging only can happen in a blockchain transaction, though, so it costs a fee to do it. This is why Yoroi is not doing any repackaging just on its own in the background until you send some transactions. But whenever you **do** send a transaction - a repackaging might happen, which will put your assets tighter together and therefore reduce the locked deposit value, which should not surprise you.

An example of such a reducing case would be if you have 100 ADA and 20 NFTs that you received all separately 1 by 1 - Yoroi will display that you have ~30 ADA of locked asset deposit. But then if you try to send 85 ADA - it might succeed, because then Yoroi will consider doing the repackaging and will estimate that putting all 20 NFTs together will only require ~10 ADA, so spending 85 ADA is fine. After this transaction you will see the deposit value going down from ~30 ADA to ~10 ADA and the total wallet balance is ~15 ADA now. But then next time trying to spend anything more than 4-5 ADA will not be allowed because all the NFTs are already packed together and cannot be repackaged anymore.

It is fairly complicated to make the final estimation of what would be the deposit when the NFTs are already all packed, until you actually try making that transaction, which is why Yoroi would have to display ~30 ADA deposit when the NFTs are all separate in individual boxes initially. And how exactly the repackaging should be done might also depend on user preferences and wallet settings.

But we are working on improving this process now as much as possible in next versions, so this deposit number should now become more and more useful. At the moment the main point is that when you see the **locked asset deposit** value in your wallet - know that it might be **hard or impossible** to reduce it and therefore you might get a **"Not enough funds"** error when trying to send a transaction not leaving enough ADAs in your wallet.

One additional feature that is being developed at the moment and is targeted to improve this process is allowing to combine multiple assets together to be sent in a single transaction, in case you want to send someone multiple NFTs at once or different tokens together. Once available, this not only will improve the user experience but will allow to send a lower ADA deposit along with those assets, because they will be sent in a single box together.

----
----

### References

For more technical details and the exact math plz check the IOHK document for "Min-Ada-Value Requirement" explanation: https://github.com/input-output-hk/cardano-ledger-specs/blob/master/doc/explanations/min-utxo.rst
3 changes: 2 additions & 1 deletion install-all.sh
@@ -1,4 +1,5 @@
npm i
npm i --prefix packages/yoroi-extension
npm i --prefix packages/yoroi-ergo-connector
npm i --prefix packages/yoroi-ergo-connector/example
npm i --prefix packages/yoroi-ergo-connector/example-ergo
npm i --prefix packages/yoroi-ergo-connector/example-cardano
39 changes: 37 additions & 2 deletions packages/yoroi-ergo-connector/example-cardano/index.html
Expand Up @@ -3,11 +3,46 @@
<head>
<meta charset="utf-8">
<link rel="shortcut icon" href="favicon.ico">
<title>Cardano Test dApp</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous"> <title>Cardano Test dApp</title>
</head>
<body>
<button id="cardano-access">Request access to yoroi (Cardano)</button>
<div class="container">
<div class="my-3">
<h1 class="display-4 text-center">Cardano dApp Example</h1>
</div>
<div class="row">
<div class="col-12 text-center my-3">
<button id="request-access" class="btn btn-primary">Request access to yoroi</button>
</div>
<div class="col-6 mb-4">
<button id="get-balance" class="btn btn-light w-100" >Get Account Balance</button>
</div>
<div class="col-6 mb-4">
<button id="get-unused-addresses" class="btn btn-light w-100" >Get Unused Addresses</button>
</div>
<div class="col-6 mb-4">
<button id="get-used-addresses" class="btn btn-light w-100" >Get Used Addresses</button>
</div>
<div class="col-6 mb-4">
<button id="get-change-address" class="btn btn-light w-100" >Get Change Address</button>
</div>
<div class="col-6 mb-4">
<button id="get-utxos" class="btn btn-light w-100" >Get Utxos</button>
</div>
<div class="col-6 mb-4">
<button id="submit-tx" class="btn btn-light w-100">Submit Tx</button>
</div>
<div class="col-6 mb-4">
<button id="sign-tx" class="btn btn-light w-100">Sign Tx</button>
</div>
</div>
<div class="w-100 d-flex justify-content-center my-2">
<div class="d-none" id="spinner" role="status"></div>
</div>
<div class="d-none" id="alert" role="alert"></div>
</div>
<noscript>This page contains webassembly and javascript content, please enable javascript in your browser.</noscript>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
<script src="./bootstrap.js"></script>
</body>
</html>
253 changes: 241 additions & 12 deletions packages/yoroi-ergo-connector/example-cardano/index.js
@@ -1,12 +1,30 @@
import * as wasm from "ergo-lib-wasm-browser";
const cardanoAccessBtn = document.querySelector('#cardano-access')
import * as CardanoWasm from "@emurgo/cardano-serialization-lib-browser"
import { getTtl} from './utils'
const cardanoAccessBtn = document.querySelector('#request-access')
const getUnUsedAddresses = document.querySelector('#get-unused-addresses')
const getUsedAddresses = document.querySelector('#get-used-addresses')
const getChangeAddress = document.querySelector('#get-change-address')
const getAccountBalance = document.querySelector('#get-balance')
const getUtxos = document.querySelector('#get-utxos')
const submitTx = document.querySelector('#submit-tx')
const signTx = document.querySelector('#sign-tx')
const alertEl = document.querySelector('#alert')
const spinner = document.querySelector('#spinner')

let accessGranted = false
let utxos
let changeAddress
let transactionHex

function initDapp(){
toggleSpinner('show')
cardano_request_read_access().then(function(access_granted){
toggleSpinner('hide')
if(!access_granted){
alert("Wallet access denied")
alertError('Access Denied')
}else {
alert("you have access now")
alertSuccess( 'You have access now')
accessGranted = true
}
});
}
Expand All @@ -15,19 +33,230 @@ cardanoAccessBtn.addEventListener('click', () => {
initDapp()
})

getAccountBalance.addEventListener('click', () => {
if(!accessGranted) {
alertError('Should request access first')
} else {
toggleSpinner('show')
cardano.get_balance().then(function(balance) {
toggleSpinner('hide')
alertSuccess(`Account Balance: ${balance}`)
});
}
})

getUnUsedAddresses.addEventListener('click', () => {
if(!accessGranted) {
alertError('Should request access first')
} else {
toggleSpinner('show')
cardano.get_unused_addresses().then(function(addresses) {
toggleSpinner('hide')
if(addresses.length === 0){
alertWarrning('No unused addresses')
} else {
alertSuccess(`Address: `)
alertEl.innerHTML = '<pre>' + JSON.stringify(addresses, undefined, 2) + '</pre>'
}
});
}
})

getUsedAddresses.addEventListener('click', () => {
if(!accessGranted) {
alertError('Should request access first')
} else {
toggleSpinner('show')
cardano.get_used_addresses().then(function(addresses) {
toggleSpinner('hide')
if(addresses.length === 0){
alertWarrning('No used addresses')
} else {
alertSuccess(`Address: ${addresses.concat(',')}`)
alertEl.innerHTML = '<pre>' + JSON.stringify(addresses, undefined, 2) + '</pre>'
}
});
}
})

getChangeAddress.addEventListener('click', () => {
if(!accessGranted) {
alertError('Should request access first')
} else {
toggleSpinner('show')
cardano.get_change_address().then(function(address) {
toggleSpinner('hide')
if(address.length === 0){
alertWarrning('No change addresses')
} else {
changeAddress = address
alertSuccess(`Address: `)
alertEl.innerHTML = '<pre>' + JSON.stringify(address, undefined, 2) + '</pre>'
}
});
}
})

getUtxos.addEventListener('click', () => {
if(!accessGranted) {
alertError('Should request access first')
return
}
toggleSpinner('show')
cardano.get_utxos().then(utxosResponse => {
toggleSpinner('hide')
if(utxosResponse.length === 0){
alertWarrning('NO UTXOS')
} else {
utxos = utxosResponse
alertSuccess(`Check the console`)
alertEl.innerHTML = '<pre>' + JSON.stringify(utxosResponse, undefined, 2) + '</pre>'
}
})
})

submitTx.addEventListener('click', () => {
if (!accessGranted) {
alertError('Should request access first')
return
}
if (!transactionHex) {
alertError('Should sign tx first')
return
}

toggleSpinner('show')
cardano.submit_tx(transactionHex).then(txId => {
toggleSpinner('hide')
alertSuccess(`Transaction ${txId} submitted`);
}).catch(error => {
toggleSpinner('hide')
alertWarrning('Transaction submission failed')
})
})

const AMOUNT_TO_SEND = '1000000'
const SEND_TO_ADDRESS = 'addr_test1qz8xh9w6f2vdnp89xzqlxnusldhz6kdm4rp970gl8swwjjkr3y3kdut55a40jff00qmg74686vz44v6k363md06qkq0q4lztj0'

signTx.addEventListener('click', () => {
toggleSpinner('show');

if (!accessGranted) {
alertError('Should request access first');
return;
}

if (!utxos) {
alertError('Should request utxos first');
return
}

if (!changeAddress) {
alertError('Should request change address first')
}

const txBuilder = CardanoWasm.WalletV4TxBuilder(
// all of these are taken from the mainnet genesis settings
// linear fee parameters (a*size + b)
CardanoWasm.LinearFee.new(CardanoWasm.BigNum.from_str('44'), CardanoWasm.BigNum.from_str('155381')),
// minimum utxo value
CardanoWasm.BigNum.from_str('1000000'),
// pool deposit
CardanoWasm.BigNum.from_str('500000000'),
// key deposit
CardanoWasm.BigNum.from_str('2000000')
)

// add a keyhash input - for ADA held in a Shelley-era normal address (Base, Enterprise, Pointer)
const utxo = utxos[0]

const addr = CardanoWasm.Address.from_bytes(
Buffer.from(utxo.receiver, 'hex')
)
const baseAddr = CardanoWasm.BaseAddress.from_address(addr);
const keyHash = baseAddr.payment_cred().to_keyhash();
txBuilder.add_key_input(
keyHash,
CardanoWasm.TransactionInput.new(
CardanoWasm.TransactionHash.from_bytes(
Buffer.from(utxo.tx_hash, "hex")
), // tx hash
utxo.tx_index, // index
),
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(utxo.amount))
)

const shelleyOutputAddress = CardanoWasm.Address.from_bech32(SEND_TO_ADDRESS)

const shelleyChangeAddress = CardanoWasm.Address.from_bytes(
Buffer.from(changeAddress, 'hex')
)

// add output to the tx
txBuilder.add_output(
CardanoWasm.TransactionOutput.new(
shelleyOutputAddress,
CardanoWasm.Value.new(CardanoWasm.BigNum.from_str(AMOUNT_TO_SEND))
),
)

const ttl = getTtl()
txBuilder.set_ttl(ttl)

// calculate the min fee required and send any change to an address
txBuilder.add_change_if_needed(shelleyChangeAddress)

const txBody = txBuilder.build()
const txHex = Buffer.from(txBody.to_bytes()).toString('hex')

cardano.sign_tx(txHex, true).then(witnessSetHex => {
toggleSpinner('hide')
alertSuccess('Signing tx succeeds: ')
const witnessSet = CardanoWasm.TransactionWitnessSet.from_bytes(
Buffer.from(witnessSetHex, 'hex')
)
const transaction = CardanoWasm.Transaction.new(
txBody,
witnessSet,
undefined,
)
transactionHex = Buffer.from(transaction.to_bytes()).toString('hex')
}).catch(error => {
console.error(error)
toggleSpinner('hide')
alertWarrning('Signing tx fails')
})
})

if (typeof cardano_request_read_access === "undefined") {
alert("Cardano not found");
} else {
console.log("Cardano found");
window.addEventListener("ergo_wallet_disconnected", function(event) {
const status = document.getElementById("status");
status.innerText = "";
const div = document.getElementById("balance");
div.innerText = "Wallet disconnected.";
const button = document.createElement("button");
button.textContent = "Reconnect";
button.onclick = initDapp;
div.appendChild(button);
console.log("Wallet Disconnect")
});
}

function alertError (text) {
alertEl.className = 'alert alert-danger'
alertEl.innerHTML = text
}

function alertSuccess(text) {
alertEl.className = 'alert alert-success'
alertEl.innerHTML = text
}

function alertWarrning(text) {
alertEl.className = 'alert alert-warning'
alertEl.innerHTML = text
}

function toggleSpinner(status){
if(status === 'show') {
spinner.className = 'spinner-border'
alertEl.className = 'd-none'
} else {
spinner.className = 'd-none'
}
}

0 comments on commit 02b194a

Please sign in to comment.