Skip to content

Instantly share code, notes, and snippets.

@nothingalike
Last active October 14, 2021 13:00
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nothingalike/983c9e783640c8405af12e9bbe64b870 to your computer and use it in GitHub Desktop.
Save nothingalike/983c9e783640c8405af12e9bbe64b870 to your computer and use it in GitHub Desktop.
Simple Cardano Lite Wallet

Warning

This is purely for demonstration purposes only.

Requirements

  • node v10+
  • cardano-cli
  • cardano-address

Setup

  1. create new folder
  2. add generateWallet.js and sendTransaction.js
  3. npm init
  4. npm install

generateWallet.js

cli: node .\generateWallet.js walletname network

network: Supported values are mainnet and testnet

If you want to import a mnemonic, create a subfolder with the name of the wallet. Then create a file called mnemonic.prv with the words as the contents.

sendTransaction.js

cli: node .\sendTransaction.js walletname amount_to_send_in_ada receivers_addr network magic

network: Supported values are mainnet and testnet-magic

magic: Only required if you use testnet-magic for the network. Value can be found in the shelley testnet genesis json file.

References

var lib = require('cardano-crypto.js')
var bip39 = require('bip39')
var fs = require('fs')
var path = require('path')
var util = require('util')
var exec = util.promisify(require('child_process').exec);
function getBufferHexFromFile(file) {
return lib.bech32.decode(fs.readFileSync(file).toString()).data.toString('hex');
}
(async () => {
var walletName = process.argv[2];
var networkTag = process.argv[3];
var WALLET_PATH = path.resolve(walletName);
var ROOT_PRIV_KEY_FILE=path.resolve(WALLET_PATH, 'root.prv');
var MNEMONICS_PRIV_FILE=path.resolve(WALLET_PATH, 'mnemonics.prv');
var STAKE_XPRIV_KEY_FILE=path.resolve(WALLET_PATH, 'stake.xprv');
var STAKE_XPUB_KEY_FILE=path.resolve(WALLET_PATH, 'stake.xpub');
var STAKE_ADDRESS_FILE=path.resolve(WALLET_PATH, 'stake.addr');
var STAKE_SKEY_FILE=path.resolve(WALLET_PATH, 'stake.skey');
var STAKE_EVKEY_FILE=path.resolve(WALLET_PATH, 'stake.evkey');
var STAKE_VKEY_FILE=path.resolve(WALLET_PATH, 'stake.vkey');
var PAYMENT_XPRV_KEY_FILE=path.resolve(WALLET_PATH, 'payment.xprv');
var PAYMENT_XPUB_KEY_FILE=path.resolve(WALLET_PATH, 'payment.xpub');
var PAYMENT_ADDRESS_FILE=path.resolve(WALLET_PATH, 'payment.addr');
var PAYMENT_SKEY_FILE=path.resolve(WALLET_PATH, 'payment.skey');
var BASE_ADDRESS_FILE=path.resolve(WALLET_PATH, 'base.payment.addr');
var CHANGE_XPRV_KEY_FILE=path.resolve(WALLET_PATH, 'change.xprv');
var CHANGE_XPUB_KEY_FILE=path.resolve(WALLET_PATH, 'change.xpub');
var CHANGE_ADDRESS_FILE=path.resolve(WALLET_PATH, 'change.addr');
var CHANGE_SKEY_FILE=path.resolve(WALLET_PATH, 'change.skey');
var BASE_CHANGE_FILE=path.resolve(WALLET_PATH, 'base.change.addr');
//create wallet if path does not exist with mnemonic
if(!fs.existsSync(WALLET_PATH))
{
fs.mkdirSync(WALLET_PATH);
fs.writeFileSync(MNEMONICS_PRIV_FILE, bip39.generateMnemonic(256)) // 256 = 24 words
}
//import wallet
mnemonic = fs.readFileSync(MNEMONICS_PRIV_FILE).toString();
//root key
await exec(`type ${MNEMONICS_PRIV_FILE} | cardano-address key from-recovery-phrase Shelley > ${ROOT_PRIV_KEY_FILE}`);
//stake keys
await exec(`type ${ROOT_PRIV_KEY_FILE} | cardano-address key child 1852H/1815H/0H/2/0 > ${STAKE_XPRIV_KEY_FILE}`);
await exec(`type ${STAKE_XPRIV_KEY_FILE} | cardano-address key public --with-chain-code > ${STAKE_XPUB_KEY_FILE}`);
await exec(`type ${STAKE_XPUB_KEY_FILE} | cardano-address address stake --network-tag ${networkTag} > ${STAKE_ADDRESS_FILE}`);
await exec(`type ${STAKE_XPRIV_KEY_FILE} | cardano-address key inspect > ${STAKE_SKEY_FILE}`);
//payment keys
await exec(`type ${ROOT_PRIV_KEY_FILE} | cardano-address key child 1852H/1815H/0H/0/0 > ${PAYMENT_XPRV_KEY_FILE}`);
await exec(`type ${PAYMENT_XPRV_KEY_FILE} | cardano-address key public --with-chain-code > ${PAYMENT_XPUB_KEY_FILE}`);
await exec(`type ${PAYMENT_XPUB_KEY_FILE} | cardano-address address payment --network-tag ${networkTag} > ${PAYMENT_ADDRESS_FILE}`);
await exec(`type ${PAYMENT_XPRV_KEY_FILE} | cardano-address key inspect > ${PAYMENT_SKEY_FILE}`);
await exec(`type ${PAYMENT_ADDRESS_FILE} | cardano-address address delegation ${fs.readFileSync(STAKE_XPUB_KEY_FILE)} > ${BASE_ADDRESS_FILE}`);
//change keys
await exec(`type ${ROOT_PRIV_KEY_FILE} | cardano-address key child 1852H/1815H/0H/1/0 > ${CHANGE_XPRV_KEY_FILE}`);
await exec(`type ${CHANGE_XPRV_KEY_FILE} | cardano-address key public --with-chain-code > ${CHANGE_XPUB_KEY_FILE}`);
await exec(`type ${CHANGE_XPUB_KEY_FILE} | cardano-address address payment --network-tag ${networkTag} > ${CHANGE_ADDRESS_FILE}`);
await exec(`type ${CHANGE_XPRV_KEY_FILE} | cardano-address key inspect > ${CHANGE_SKEY_FILE}`);
await exec(`type ${CHANGE_ADDRESS_FILE} | cardano-address address delegation ${fs.readFileSync(STAKE_XPUB_KEY_FILE)} > ${BASE_CHANGE_FILE}`);
//signing keys
console.log()
var SESKEY = getBufferHexFromFile(STAKE_XPRIV_KEY_FILE).slice(0, 128) + getBufferHexFromFile(STAKE_XPUB_KEY_FILE)
var PESKEY = getBufferHexFromFile(PAYMENT_XPRV_KEY_FILE).slice(0, 128) + getBufferHexFromFile(PAYMENT_XPUB_KEY_FILE)
var CESKEY = getBufferHexFromFile(CHANGE_XPRV_KEY_FILE).slice(0, 128) + getBufferHexFromFile(CHANGE_XPUB_KEY_FILE)
fs.writeFileSync(STAKE_SKEY_FILE, `{
"type": "StakeExtendedSigningKeyShelley_ed25519_bip32",
"description": "",
"cborHex": "5880${SESKEY}"
}`);
fs.writeFileSync(PAYMENT_SKEY_FILE, `{
"type": "PaymentExtendedSigningKeyShelley_ed25519_bip32",
"description": "Payment Signing Key",
"cborHex": "5880${PESKEY}"
}`);
fs.writeFileSync(CHANGE_SKEY_FILE, `{
"type": "PaymentExtendedSigningKeyShelley_ed25519_bip32",
"description": "Change Signing Key",
"cborHex": "5880${CESKEY}"
}`);
})();
var axios = require('axios').default;
var fs = require('fs');
var path = require('path');
var util = require('util');
var exec = util.promisify(require('child_process').exec);
var dandelionUrl = "https://graphql-api.testnet.dandelion.link/";
function getTotalUtxoBalance(utxos) {
let total = 0;
utxos.forEach(o => {
total += parseInt(o.value)
})
return total;
}
function getBinaryFromHexString(hexString) {
return new Uint8Array(hexString.match(/.{1,2}/g).map(b => parseInt(b, 16)));
}
function getGraphqlAddresses(addresses) {
let result = '[';
addresses.forEach(a => {
result += `"${a}"`;
})
result += ']';
return result;
}
(async () => {
//grabbing parameters
var walletName = process.argv[2];
var amountToSend = process.argv[3]*1000000;
var toAddress = process.argv[4];
var networkTag = process.argv[5];
var magic = null;
if(process.argv.length == 7){
magic = process.argv[6];
}
//protocol params
var protocolParamsFile = path.resolve('protocolParams.json')
//transaction files / new files
var draftTx = path.resolve(walletName, 'draft.tx');
var rawTx = path.resolve(walletName, 'raw.tx');
var signedTx = path.resolve(walletName, 'signed.tx');
var txBinary = path.resolve(walletName, 'binary.tx');
//addresses/keys / already exist from generateWallet.js
var paymentSigningFile = path.resolve(walletName, 'payment.skey');
var changeSigningFile = path.resolve(walletName, 'change.skey');
var changeAddress = fs.readFileSync(path.resolve(walletName, 'base.change.addr'));
var addressPath=path.resolve(walletName, 'base.payment.addr');
var paymentAddress = fs.readFileSync(addressPath);
var addresses = [changeAddress.toString(), paymentAddress.toString()];
//gathering data for constructing a transaction
var tipQuery = "{ cardano { tip { slotNo } } }"
var tipResult = await axios.post(dandelionUrl, { query: tipQuery });
const slotNo = tipResult.data.data.cardano.tip.slotNo;
var protocolParamsQuery = "{ genesis { shelley { protocolParams { a0 decentralisationParam eMax extraEntropy keyDeposit maxBlockBodySize maxBlockHeaderSize maxTxSize minFeeA minFeeB minPoolCost minUTxOValue nOpt poolDeposit protocolVersion rho tau } } } }"
var protocolParamsResult = await axios.post(dandelionUrl, { query: protocolParamsQuery });
fs.writeFileSync(protocolParamsFile, Buffer.from(JSON.stringify(protocolParamsResult.data.data.genesis.shelley.protocolParams)));
var utxos = `{ utxos( order_by: { value: desc } where: { address: { _in: ${getGraphqlAddresses(addresses)} }} ) { address index txHash value } }`
var utxosResult = await axios.post(dandelionUrl, { query: utxos });
const addressUtxos = utxosResult.data.data.utxos;
const total = getTotalUtxoBalance(addressUtxos);
if(amountToSend > total) throw "Cannot send that much."
//draft transaction
let txDraft = 'cardano-cli transaction build-raw --allegra-era --fee 0 --ttl 0';
let totalUsed = 0;
let utxoInCount = 0;
for(let u of addressUtxos)
{
totalUsed += parseInt(u.value);
txDraft += ` --tx-in ${u.txHash}#${u.index}`
utxoInCount++;
if(totalUsed >= amountToSend)
break;
}
txDraft += ` --tx-out ${toAddress}+${amountToSend}`;
txDraft += ` --tx-out ${changeAddress}+${totalUsed - amountToSend}`;
txDraft += ` --out-file ${draftTx}`;
await exec(txDraft);
//calculate fee
let txFee = 'cardano-cli transaction calculate-min-fee';
txFee += ` --tx-body-file ${draftTx}`;
txFee += ` --tx-in-count ${utxoInCount}`;
txFee += ' --tx-out-count 2';
txFee += ' --witness-count 1';
txFee += ' --byron-witness-count 0';
txFee += ` --protocol-params-file ${protocolParamsFile}`;
const feeResult = await exec(txFee);
//originally tried to just calculate the fee locally
// but had issues when trying to use multiple --tx-in
//minFeeA * txSize + minFeeB
//note the output of the 'calculate-min-fee' is: 'XXXX Lovelace'
// this is why i split and take index 0
const fee = feeResult.stdout.split(' ')[0];
//raw transaction
let txRaw = 'cardano-cli transaction build-raw --allegra-era';
totalUsed = 0;
for(let u of addressUtxos)
{
totalUsed += parseInt(u.value);
txRaw += ` --tx-in ${u.txHash}#${u.index}`
if(totalUsed >= (amountToSend + fee))
break;
}
txRaw += ` --ttl ${slotNo + 1000}`;
txRaw += ` --fee ${fee}`;
txRaw += ` --tx-out ${toAddress}+${amountToSend}`;
txRaw += ` --tx-out ${changeAddress}+${totalUsed - amountToSend - fee}`;
txRaw += ` --out-file ${rawTx}`;
await exec(txRaw);
//sign the transaction
let txSign = 'cardano-cli transaction sign';
txSign += ` --${networkTag}`;
if(magic != null) txSign += ` ${magic}`;
txSign += ` --signing-key-file ${paymentSigningFile}`;
txSign += ` --signing-key-file ${changeSigningFile}`;
txSign += ` --tx-body-file ${rawTx}`;
txSign += ` --out-file ${signedTx}`;
await exec(txSign);
//send transaction
var dataHex = JSON.parse(fs.readFileSync(signedTx)).cborHex
var dataBinary = getBinaryFromHexString(dataHex)
fs.writeFileSync(txBinary, dataBinary);
//Option 1) curl
//var sendResult = await exec(`curl -X POST --header "Content-Type: application/cbor" --data-binary @${txBinary} https://submit-api.testnet.dandelion.link/api/submit/tx`)
//Option 2) axios
try{
var sendResult = await axios(
{
method: 'post',
url: 'https://submit-api.testnet.dandelion.link/api/submit/tx',
data: dataBinary,
headers: {
"Content-Type": "application/cbor"
}
});
console.log(sendResult.data);
}catch(err) {
console.log(err.response.data)
}
})();
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment