|
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) |
|
} |
|
})(); |