Merge pull request #11 from ranchimall/dev

Blockbook based UI and backend
This commit is contained in:
sairaj mote 2023-07-06 18:32:33 +05:30 committed by GitHub
commit 9983059012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 428 additions and 18191 deletions

File diff suppressed because one or more lines are too long

View File

@ -776,7 +776,7 @@ h3 {
}
.transaction {
gap: 1rem;
gap: 1.2rem;
padding: 1rem;
background-color: rgba(var(--text-color), 0.03);
border-radius: 0.3rem;
@ -792,7 +792,29 @@ h3 {
}
.transaction__receiver {
margin-left: 0.5rem;
color: rgba(var(--text-color), 0.8);
color: rgba(var(--text-color), 0.9);
font-weight: 500;
}
.transaction__amount {
font-weight: 700;
}
.transaction.mined .transaction__icon .icon, .transaction.received .transaction__icon .icon, .transaction.self .transaction__icon .icon {
fill: var(--green);
}
.transaction.mined .transaction__amount, .transaction.received .transaction__amount, .transaction.self .transaction__amount {
color: var(--green);
}
.transaction.mined .transaction__amount::before, .transaction.received .transaction__amount::before, .transaction.self .transaction__amount::before {
content: "+ ";
}
.transaction.sent .transaction__icon .icon {
fill: var(--danger-color);
}
.transaction.sent .transaction__amount {
color: var(--danger-color);
}
.transaction.sent .transaction__amount::before {
content: "- ";
}
.transaction p {
font-size: 0.9rem;

2
css/main.min.css vendored

File diff suppressed because one or more lines are too long

View File

@ -558,17 +558,17 @@ h3 {
#main_header {
padding: 0.6rem 1rem;
}
.app-brand{
.app-brand {
display: flex;
gap: 0.3rem;
align-items: center;
.icon{
.icon {
height: 1.7rem;
width: 1.7rem;
}
}
.app-name{
&__company{
.app-name {
&__company {
font-size: 0.8rem;
font-weight: 500;
color: rgba(var(--text-color), 0.8);
@ -741,7 +741,7 @@ h3 {
border-radius: 0.3rem;
}
.transaction {
gap: 1rem;
gap: 1.2rem;
padding: 1rem;
background-color: rgba(var(--text-color), 0.03);
border-radius: 0.3rem;
@ -758,7 +758,39 @@ h3 {
}
&__receiver {
margin-left: 0.5rem;
color: rgba(var(--text-color), 0.8);
color: rgba(var(--text-color), 0.9);
font-weight: 500;
}
&__amount {
font-weight: 700;
}
&.mined,
&.received,
&.self {
.transaction__icon {
.icon {
fill: var(--green);
}
}
.transaction__amount {
color: var(--green);
&::before {
content: "+ ";
}
}
}
&.sent {
.transaction__icon {
.icon {
fill: var(--danger-color);
}
}
.transaction__amount {
color: var(--danger-color);
&::before {
content: "- ";
}
}
}
p {
font-size: 0.9rem;

View File

@ -187,13 +187,14 @@
</div>
</div>
<section class="grid gap-1">
<div class="flex align-center space-between sticky top-0"
style="background-color: rgba(var(--foreground-color), 1);transition: background-color .3s;">
<div class="flex align-center space-between sticky top-0 flex-wrap gap-1"
style="background-color: rgba(var(--foreground-color), 1);transition: background-color .3s; padding-bottom: 0.5rem;">
<h4>Transactions</h4>
<sm-chips id="filter_selector" class="hidden">
<sm-chip value="sent" selected>Sent</sm-chip>
<sm-chip value="all" selected>All</sm-chip>
<sm-chip value="sent">Sent</sm-chip>
<sm-chip value="received">Received</sm-chip>
<sm-chip value="all">All</sm-chip>
<sm-chip value="mined">Mined</sm-chip>
</sm-chips>
</div>
<ul id="queried_address_transactions" class="observe-empty-state"></ul>
@ -830,7 +831,14 @@
<div class="transaction__icon"></div>
<div class="transaction__receiver breakable"></div>
</div>
<p class="transaction__flo-data breakable"></p>
<div class="grid gap-0-3">
<div class="label">Amount</div>
<div class="transaction__amount"></div>
</div>
<div class="grid gap-0-3">
<div class="label">FLO Data</div>
<p class="transaction__flo-data breakable"></p>
</div>
<div class="flex align-center space-between">
<a href="" class="transaction__link flex align-center" target="_blank" rel="noopener noreferrer">
<svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
@ -1208,15 +1216,15 @@
page = parseInt(page)
if (floGlobals.query.string !== query) {
checkBalance(query)
fetchTransactions(query).then(() => {
filterFetchedTransactions()
render.paginatedTransactions(page)
})
} else if (page % Math.ceil(1000 / txsPerPage) === 0 && floGlobals.query.transactions.length <= page * txsPerPage) {
}/* else if (floGlobals.query.totalPages <= page * txsPerPage) {
loadMoreTransactions()
} else {
render.paginatedTransactions(page)
}
}*/
fetchTransactions(query, page).then(() => {
filterFetchedTransactions()
render.paginatedTransactions(page)
})
} catch (err) {
notify(err, 'error')
}
@ -1877,45 +1885,55 @@
})
},
transactionCard(details) {
const { sender, receiver, floData, time, txid } = details
const { sender, receiver, floData, time, txid, netValue, mine } = details
const { query: queriedFloId } = pagesData.params
const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild;
if (sender === receiver) {
if (mine) {
clone.classList.add('mined')
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> <path fill-rule="evenodd" clip-rule="evenodd" d="M22.0861 14.6534C22.3924 14.3472 22.7898 14.1486 23.2186 14.0876L44.6946 11.0319C45.2676 10.9504 45.8455 11.1432 46.2547 11.5524C47.2381 12.5358 46.8168 14.2128 45.4853 14.6146L28.6869 19.6843C28.3711 19.7796 28.0838 19.9515 27.8505 20.1848L27.0092 21.0261L28.3236 22.3404C29.0306 23.0475 29.0976 24.1522 28.5245 24.9346L52.2562 48.6662C53.0372 49.4473 53.0372 50.7136 52.2562 51.4946L51.4947 52.2561C50.7137 53.0372 49.4473 53.0372 48.6663 52.2561L24.9346 28.5245C24.1522 29.0976 23.0475 29.0306 22.3404 28.3236L21.0261 27.0092L20.1848 27.8505C19.9515 28.0838 19.7796 28.3711 19.6843 28.6869L14.6146 45.4853C14.2128 46.8168 12.5358 47.2381 11.5524 46.2547C11.1432 45.8455 10.9504 45.2675 11.0319 44.6946L14.0876 23.2186C14.1486 22.7898 14.3472 22.3924 14.6534 22.0861L15.3949 21.3447C14.8777 20.6386 14.8818 19.67 15.4072 18.9681C14.8209 18.1848 14.8837 17.0693 15.5958 16.3572L16.3573 15.5958C17.0694 14.8837 18.1848 14.8208 18.9681 15.4072C19.6701 14.8818 20.6386 14.8777 21.3447 15.3949L22.0861 14.6534Z"/> </svg>`;
clone.querySelector('.transaction__receiver').textContent = 'Coinbase'
} else if (sender === receiver) {
clone.classList.add('self')
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><title>sender and receiver is same</title><path d="M.01 0h24v24h-24V0z" fill="none"/><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z"/></svg>`;
clone.querySelector('.transaction__receiver').textContent = receiver
} else if (queriedFloId === sender) {
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M9 5v2h6.59L4 18.59 5.41 20 17 8.41V15h2V5z"/></svg>`;
clone.classList.add('sent')
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M4 12l1.41 1.41L11 7.83V20h2V7.83l5.58 5.59L20 12l-8-8-8 8z"/></svg>`;
clone.querySelector('.transaction__receiver').textContent = receiver
} else {
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0z" fill="none"/><path d="M20 5.41L18.59 4 7 15.59V9H5v10h10v-2H8.41z"/></svg>`;
clone.classList.add('received')
clone.querySelector('.transaction__icon').innerHTML = `<svg class="icon" xmlns="http://www.w3.org/2000/svg" height="24px" viewBox="0 0 24 24" width="24px" fill="#000000"><path d="M0 0h24v24H0V0z" fill="none"/><path d="M20 12l-1.41-1.41L13 16.17V4h-2v12.17l-5.58-5.59L4 12l8 8 8-8z"/></svg>`;
clone.querySelector('.transaction__receiver').textContent = sender
}
if (netValue) {
clone.querySelector('.transaction__amount').textContent = `${netValue} FLO`
} else {
clone.querySelector('.transaction__amount').parentNode.remove()
}
if (floData) {
clone.querySelector('.transaction__flo-data').textContent = floData
} else {
clone.querySelector('.transaction__flo-data').parentNode.remove()
}
clone.querySelector('.transaction__receiver').textContent = queriedFloId === sender ? receiver : sender
clone.querySelector('.transaction__flo-data').textContent = floData
clone.querySelector('.transaction__link').href = `${floBlockchainAPI.current_server}tx/${txid}`
clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000)
return clone
},
paginatedTransactions(page = parseInt(pagesData.params.page) || 1) {
const { transactions, string: address, filteredTransactions } = floGlobals.query
let startingIndex = ((page - 1) * txsPerPage)
if ((filteredTransactions?.length || transactions.length) < startingIndex) {
startingIndex = 0;
window.history.replaceState({}, '', `#/search?type=address&query=${address}&page=1`)
pagesData.params.page = page = 1;
}
const endingIndex = startingIndex + txsPerPage
const { transactions, string: address, filteredTransactions, totalPages } = floGlobals.query
const renderedTransactions = (filteredTransactions || transactions)
.slice(startingIndex, endingIndex)
.map(transaction => render.transactionCard(transaction))
renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`)
getRef('transactions_hero_section').scrollIntoView({
behavior: 'smooth',
block: 'start'
})
if (floGlobals.query.transactions.length) {
if (transactions.length) {
getRef('filter_selector').classList.remove('hidden')
} else {
getRef('filter_selector').classList.add('hidden')
}
const paginationSegments = (filteredTransactions || transactions) ? Math.ceil((filteredTransactions || transactions).length / txsPerPage) : 0;
const paginationSegments = totalPages;
let pagination = []
let startingPage = page - 2;
let showTill = page + 2;
@ -1956,11 +1974,11 @@
} else {
getRef('pagination_wrapper').classList.add('hidden')
}
if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) {
/* if (filteredTransactions && paginationSegments === page && filteredTransactions.length % txsPerPage !== 0 && transactions.length % txsPerPage === 0) {
document.getElementById('load_more_transactions').classList.remove('hidden')
} else {
document.getElementById('load_more_transactions').classList.add('hidden')
}
} */
},
availableAssetOptions() {
return (floGlobals.tokens || []).map(token => html` <sm-option value=${token}>${token}</sm-option> `)
@ -2496,12 +2514,12 @@
getRef('address_details_wrapper').classList.remove('hidden')
floWebWallet.getLabels().then(allLabels => {
if (allLabels[queriedFloId]) {
renderElem(getRef('queried_flo_address'), html`<h4>${allLabels[queriedFloId]}</h4> <sm-copy clip-text value=${queriedFloId}></sm-copy>`)
getRef('queried_flo_address').innerHTML = `<h4>${allLabels[queriedFloId]}</h4> <sm-copy clip-text value=${queriedFloId}></sm-copy>`;
} else {
renderElem(getRef('queried_flo_address'), html`
getRef('queried_flo_address').innerHTML = `
<p class="label">FLO Address </p>
<h4><sm-copy clip-text value=${queriedFloId}></sm-copy></h4>
`)
`;
}
})
const queriedFloId = address || getRef('search_query_input').value.trim()
@ -2524,7 +2542,7 @@
getRef('token_list_wrapper').classList.remove('hidden')
}
// retrieve FLO balance
getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(3))} FLO`;
getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(8))} FLO`;
} catch (e) {
console.error(e)
}
@ -2551,8 +2569,8 @@
string: '',
filteredTransactions: null
}
const txsPerPage = 25;
async function fetchTransactions(address, loadOlder = false) {
const txsPerPage = 100;
async function fetchTransactions(address, page = 1) {
try {
document.getElementById('load_more_transactions').classList.add('hidden')
renderElem(getRef('pagination_wrapper'), html``)
@ -2562,23 +2580,12 @@
<span>Loading transactions...</span>
</div>
`)
if (loadOlder) {
const { items, initItem } = await floWebWallet.listTransactions.syncOld(address, floGlobals.query.initItem)
floGlobals.query = {
transactions: [...items, ...floGlobals.query.transactions],
string: address,
initItem,
filteredTransactions: null
}
} else {
const { items, lastItem, initItem } = await floWebWallet.listTransactions(address)
floGlobals.query = {
transactions: items,
string: address,
lastItem,
initItem,
filteredTransactions: null
}
const { items, totalPages } = await floWebWallet.listTransactions(address, { pageSize: txsPerPage, page })
floGlobals.query = {
transactions: items,
string: address,
filteredTransactions: null,
totalPages
}
} catch (err) {
renderElem(getRef('queried_address_transactions'), html` <span>Failed to load transactions</span> `)
@ -2588,7 +2595,19 @@
function filterFetchedTransactions() {
const filter = getRef('filter_selector').value;
if (filter !== 'all') {
floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => filter === 'sent' ? t.sender === floGlobals.query.string : t.receiver === floGlobals.query.string)
floGlobals.query.filteredTransactions = floGlobals.query.transactions.filter(t => {
switch (filter) {
case 'sent':
return t.sender === floGlobals.query.string
break
case 'received':
return t.receiver === floGlobals.query.string
break
case 'mined':
return t.mined
break
}
})
} else {
floGlobals.query.filteredTransactions = null
}

6766
old.html

File diff suppressed because it is too large Load Diff

File diff suppressed because one or more lines are too long

View File

@ -48,46 +48,76 @@
})
}
function listTransactions_raw(address, options = {}) {
function formatTx(address, tx) {
let result = {
time: tx.time,
block: tx.blockheight,
blockhash: tx.blockhash,
txid: tx.txid,
floData: tx.floData,
confirmations: tx.confirmations
}
//format receivers
let receivers = {};
for (let vout of tx.vout) {
if (vout.scriptPubKey.isAddress) {
let id = vout.scriptPubKey.addresses[0];
if (id in receivers)
receivers[id] += vout.value;
else receivers[id] = vout.value;
}
}
result.receivers = receivers;
//format senders (or mined)
if (!tx.vin[0].isAddress) { //mined (ie, coinbase)
let coinbase = tx.vin[0].coinbase;
result.mine = coinbase;
result.mined = { [coinbase]: tx.valueOut }
} else {
result.sender = tx.vin[0].addresses[0];
result.receiver = tx.vout[0].scriptPubKey.addresses[0];
result.fees = tx.fees;
let senders = {};
for (let vin of tx.vin) {
if (vin.isAddress) {
let id = vin.addresses[0];
if (id in senders)
senders[id] += vin.value;
else senders[id] = vin.value;
}
}
result.senders = senders;
//remove change amounts
for (let id in senders) {
if (id in receivers) {
if (senders[id] > receivers[id]) {
senders[id] -= receivers[id];
delete receivers[id];
} else if (senders[id] < receivers[id]) { //&& id != address
receivers[id] -= senders[id];
delete senders[id];
}
}
}
}
return result;
}
floWebWallet.listTransactions = function (address, page_options = {}) {
return new Promise((resolve, reject) => {
options.latest = true;
let options = {};
if (Number.isInteger(page_options.page))
options.page = page_options.page;
if (Number.isInteger(page_options.pageSize))
options.pageSize = page_options.pageSize;
floBlockchainAPI.readTxs(address, options).then(response => {
const result = {}
result.items = response.items.map(({ time, txid, floData, isCoinBase, vin, vout }) => ({
time, txid, floData, isCoinBase,
sender: isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr,
receiver: isCoinBase ? address : vout[0].scriptPubKey.addresses[0]
}));
result.lastItem = response.lastItem;
result.initItem = response.initItem;
resolve(result);
}).catch(error => reject(error))
})
}
floWebWallet.listTransactions = function (address) {
return new Promise((resolve, reject) => {
listTransactions_raw(address)
.then(result => resolve(result))
.catch(error => reject(error))
})
}
floWebWallet.listTransactions.syncNew = function (address, lastItem) {
return new Promise((resolve, reject) => {
listTransactions_raw(address, { after: lastItem }).then(result => {
delete result.initItem;
resolve(result);
}).catch(error => reject(error))
})
}
floWebWallet.listTransactions.syncOld = function (address, initItem) {
return new Promise((resolve, reject) => {
listTransactions_raw(address, { before: initItem }).then(result => {
delete result.lastItem;
result.items = response.txs.map(tx => formatTx(address, tx));
result.page = response.page;
result.totalPages = response.totalPages;
resolve(result);
}).catch(error => reject(error))
})

View File

@ -1,13 +1,13 @@
(function (EXPORTS) { //floBlockchainAPI v2.5.6a
/* FLO Blockchain Operator to send/receive data from blockchain using API calls*/
(function (EXPORTS) { //floBlockchainAPI v3.0.1b
/* FLO Blockchain Operator to send/receive data from blockchain using API calls via FLO Blockbook*/
'use strict';
const floBlockchainAPI = EXPORTS;
const DEFAULT = {
blockchain: floGlobals.blockchain,
apiURL: {
FLO: ['https://flosight.ranchimall.net/'],
FLO_TEST: ['https://flosight-testnet.ranchimall.net/']
FLO: ['https://blockbook.ranchimall.net/'],
FLO_TEST: []
},
sendAmt: 0.0003,
fee: 0.0002,
@ -61,9 +61,9 @@
var serverList = Array.from(allServerList);
var curPos = floCrypto.randInt(0, serverList.length - 1);
function fetch_retry(apicall, rm_flosight) {
function fetch_retry(apicall, rm_node) {
return new Promise((resolve, reject) => {
let i = serverList.indexOf(rm_flosight)
let i = serverList.indexOf(rm_node)
if (i != -1) serverList.splice(i, 1);
curPos = floCrypto.randInt(0, serverList.length - 1);
fetch_api(apicall, false)
@ -82,19 +82,19 @@
.then(result => resolve(result))
.catch(error => reject(error));
} else
reject("No floSight server working");
reject("No FLO blockbook server working");
} else {
let flosight = serverList[curPos];
fetch(flosight + apicall).then(response => {
let serverURL = serverList[curPos];
fetch(serverURL + apicall).then(response => {
if (response.ok)
response.json().then(data => resolve(data));
else {
fetch_retry(apicall, flosight)
fetch_retry(apicall, serverURL)
.then(result => resolve(result))
.catch(error => reject(error));
}
}).catch(error => {
fetch_retry(apicall, flosight)
fetch_retry(apicall, serverURL)
.then(result => resolve(result))
.catch(error => reject(error));
})
@ -124,43 +124,27 @@
}
//Get balance for the given Address
const getBalance = floBlockchainAPI.getBalance = function (addr, after = null) {
const getBalance = floBlockchainAPI.getBalance = function (addr) {
return new Promise((resolve, reject) => {
let api = `api/addr/${addr}/balance`, query_params = {};
if (after) {
if (typeof after === 'string' && /^[0-9a-z]{64}$/i.test(after))
query_params.after = after;
else return reject("Invalid 'after' parameter");
}
promisedAPI(api, query_params).then(result => {
if (typeof result === 'object' && result.lastItem) {
getBalance(addr, result.lastItem)
.then(r => resolve(util.toFixed(r + result.data)))
.catch(error => reject(error))
} else resolve(result);
}).catch(error => reject(error))
let api = `api/address/${addr}`;
promisedAPI(api, { details: "basic" })
.then(result => resolve(result["balance"]))
.catch(error => reject(error))
});
}
const getUTXOs = address => new Promise((resolve, reject) => {
promisedAPI(`api/addr/${address}/utxo`)
.then(utxo => resolve(utxo))
.catch(error => reject(error))
})
function getScriptPubKey(address) {
var tx = bitjs.transaction();
tx.addoutput(address, 0);
let outputBuffer = tx.outputs.pop().script;
return Crypto.util.bytesToHex(outputBuffer)
}
const getUnconfirmedSpent = address => new Promise((resolve, reject) => {
readTxs(address, { mempool: "only" }).then(result => {
let unconfirmedSpent = {};
for (let tx of result.items)
if (tx.confirmations == 0)
for (let vin of tx.vin)
if (vin.addr === address) {
if (Array.isArray(unconfirmedSpent[vin.txid]))
unconfirmedSpent[vin.txid].push(vin.vout);
else
unconfirmedSpent[vin.txid] = [vin.vout];
}
resolve(unconfirmedSpent);
const getUTXOs = address => new Promise((resolve, reject) => {
promisedAPI(`api/utxo/${address}`, { confirmed: true }).then(utxos => {
let scriptPubKey = getScriptPubKey(address);
utxos.forEach(u => u.scriptPubKey = scriptPubKey);
resolve(utxos);
}).catch(error => reject(error))
})
@ -180,32 +164,28 @@
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
getUTXOs(senderAddr).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
continue; //A transaction has already used the utxo, but is unconfirmed.
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
trx.addoutput(receiverAddr, sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
getUTXOs(senderAddr).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
trx.addoutput(receiverAddr, sendAmt);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
}).catch(error => reject(error))
})
@ -293,34 +273,30 @@
if (balance < totalAmt + fee)
return reject("Insufficient FLO balance!");
//get unconfirmed tx list
getUnconfirmedSpent(floID).then(unconfirmedSpent => {
getUTXOs(floID).then(utxos => {
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
continue; //A transaction has already used the utxo, but is unconfirmed.
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < totalAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
for (let i = 0; i < count; i++)
trx.addoutput(floID, splitAmt);
var change = utxoAmt - totalAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(floID, change);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}
}).catch(error => reject(error))
getUTXOs(floID).then(utxos => {
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (let i = utxos.length - 1; (i >= 0) && (utxoAmt < totalAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
trx.addinput(utxos[i].txid, utxos[i].vout, utxos[i].scriptPubKey);
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < totalAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
for (let i = 0; i < count; i++)
trx.addoutput(floID, splitAmt);
var change = utxoAmt - totalAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(floID, change);
trx.addflodata(floData.replace(/\n/g, ' '));
var signedTxHash = trx.sign(privKey, 1);
broadcastTx(signedTxHash)
.then(txid => resolve(txid))
.catch(error => reject(error))
}
}).catch(error => reject(error))
}).catch(error => reject(error))
})
@ -551,33 +527,29 @@
var fee = DEFAULT.fee;
if (balance < sendAmt + fee)
return reject("Insufficient FLO balance!");
getUnconfirmedSpent(senderAddr).then(unconfirmedSpent => {
getUTXOs(senderAddr).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
if (utxos[i].txid in unconfirmedSpent && unconfirmedSpent[utxos[i].txid].includes(utxos[i].vout))
continue; //A transaction has already used the utxo, but is unconfirmed.
trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
for (let i in receivers)
trx.addoutput(receivers[i], amounts[i]);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
getUTXOs(senderAddr).then(utxos => {
//form/construct the transaction data
var trx = bitjs.transaction();
var utxoAmt = 0.0;
for (var i = utxos.length - 1;
(i >= 0) && (utxoAmt < sendAmt + fee); i--) {
//use only utxos with confirmations (strict_utxo mode)
if (utxos[i].confirmations || !strict_utxo) {
trx.addinput(utxos[i].txid, utxos[i].vout, redeemScript); //for multisig, script=redeemScript
utxoAmt += utxos[i].amount;
};
}
if (utxoAmt < sendAmt + fee)
reject("Insufficient FLO: Some UTXOs are unconfirmed");
else {
for (let i in receivers)
trx.addoutput(receivers[i], amounts[i]);
var change = utxoAmt - sendAmt - fee;
if (change > DEFAULT.minChangeAmt)
trx.addoutput(senderAddr, change);
trx.addflodata(floData.replace(/\n/g, ' '));
resolve(trx);
}
}).catch(error => reject(error))
}).catch(error => reject(error))
});
@ -773,20 +745,11 @@
const broadcastTx = floBlockchainAPI.broadcastTx = function (signedTxHash) {
return new Promise((resolve, reject) => {
if (signedTxHash.length < 1)
return reject("Empty Signature");
var url = serverList[curPos] + 'api/tx/send';
fetch(url, {
method: "POST",
headers: {
'Content-Type': 'application/json'
},
body: `{"rawtx":"${signedTxHash}"}`
}).then(response => {
if (response.ok)
response.json().then(data => resolve(data.txid.result));
else
response.text().then(data => resolve(data));
}).catch(error => reject(error));
return reject("Empty Transaction Data");
promisedAPI('/api/sendtx/' + signedTxHash)
.then(response => resolve(response["result"]))
.catch(error => reject(error))
})
}
@ -825,61 +788,96 @@
})
}
//Read Txs of Address between from and to
//Read Txs of Address
const readTxs = floBlockchainAPI.readTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => {
let api = `api/addrs/${addr}/txs`;
//API options
let query_params = {};
if (!isUndefined(options.after) || !isUndefined(options.before)) {
if (!isUndefined(options.after))
query_params.after = options.after;
if (!isUndefined(options.before))
query_params.before = options.before;
} else {
if (!isUndefined(options.from))
query_params.from = options.from;
if (!isUndefined(options.to))
query_params.to = options.to;
}
if (!isUndefined(options.latest))
query_params.latest = options.latest;
if (!isUndefined(options.mempool))
query_params.mempool = options.mempool;
promisedAPI(api, query_params)
.then(response => resolve(response))
.catch(error => reject(error))
let query_params = { details: 'txs' };
//page options
if (!isUndefined(options.page) && Number.isInteger(options.page))
query_params.page = options.page;
if (!isUndefined(options.pageSize) && Number.isInteger(options.pageSize))
query_params.pageSize = options.pageSize;
//only confirmed tx
if (options.confirmed) //Default is false in server, so only add confirmed filter if confirmed has a true value
query_params.confirmed = true;
promisedAPI(`api/address/${addr}`, query_params).then(response => {
if (!Array.isArray(response.txs)) //set empty array if address doesnt have any tx
response.txs = [];
resolve(response)
}).catch(error => reject(error))
});
}
//backward support (floBlockchainAPI < v2.5.6)
function readAllTxs_oldSupport(addr, options, ignoreOld = 0, cacheTotal = 0) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
cacheTotal += response.txs.length;
let n_remaining = response.txApperances - cacheTotal
if (n_remaining < ignoreOld) { // must remove tx that would have been fetch during prev call
let n_remove = ignoreOld - n_remaining;
resolve(response.txs.slice(0, -n_remove));
} else if (response.page == response.totalPages) //last page reached
resolve(response.txs);
else {
options.page = response.page + 1;
readAllTxs_oldSupport(addr, options, ignoreOld, cacheTotal)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
function readAllTxs_new(addr, options, lastItem) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
let i = response.txs.findIndex(t => t.txid === lastItem);
if (i != -1) //found lastItem
resolve(response.txs.slice(0, i))
else if (response.page == response.totalPages) //last page reached
resolve(response.txs);
else {
options.page = response.page + 1;
readAllTxs_new(addr, options, lastItem)
.then(result => resolve(response.txs.concat(result)))
.catch(error => reject(error))
}
}).catch(error => reject(error))
})
}
//Read All Txs of Address (newest first)
const readAllTxs = floBlockchainAPI.readAllTxs = function (addr, options = {}) {
return new Promise((resolve, reject) => {
readTxs(addr, options).then(response => {
if (response.incomplete) {
let next_options = Object.assign({}, options);
if (options.latest)
next_options.before = response.initItem; //update before for chain query (latest 1st)
else
next_options.after = response.lastItem; //update after for chain query (oldest 1st)
readAllTxs(addr, next_options).then(r => {
r.items = r.items.concat(response.items); //latest tx are 1st in array
resolve(r);
}).catch(error => reject(error))
} else
if (Number.isInteger(options.ignoreOld)) //backward support: data from floBlockchainAPI < v2.5.6
readAllTxs_oldSupport(addr, options, options.ignoreOld).then(txs => {
let last_tx = txs.find(t => t.confirmations > 0);
let new_lastItem = last_tx ? last_tx.txid : options.ignoreOld;
resolve({
lastItem: response.lastItem || options.after,
items: response.items
});
})
});
lastItem: new_lastItem,
items: txs
})
}).catch(error => reject(error))
else //New format for floBlockchainAPI >= v2.5.6
readAllTxs_new(addr, options, options.after).then(txs => {
let last_tx = txs.find(t => t.confirmations > 0);
let new_lastItem = last_tx ? last_tx.txid : options.after;
resolve({
lastItem: new_lastItem,
items: txs
})
}).catch(error => reject(error))
})
}
/*Read flo Data from txs of given Address
options can be used to filter data
after : query after the given txid
before : query before the given txid
mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx)
confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx)
ignoreOld : ignore old txs (deprecated: support for backward compatibility only, cannot be used with 'after')
sentOnly : filters only sent data
receivedOnly: filters only received data
@ -894,18 +892,14 @@
//fetch options
let query_options = {};
query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx
if (!isUndefined(options.after) || !isUndefined(options.before)) {
if (!isUndefined(options.ignoreOld)) //Backward support
return reject("Invalid options: cannot use after/before and ignoreOld in same query");
//use passed after and/or before options (options remain undefined if not passed)
query_options.after = options.after;
query_options.before = options.before;
}
readAllTxs(addr, query_options).then(response => {
query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: ignore unconfirmed tx
if (Number.isInteger(options.ignoreOld)) //backward support, cannot be used with options.after or options.before
response.items.splice(-options.ignoreOld); //negative to count from end of the array
if (!isUndefined(options.after))
query_options.after = options.after;
else if (!isUndefined(options.ignoreOld))
query_options.ignoreOld = options.ignoreOld;
readAllTxs(addr, query_options).then(response => {
if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers];
@ -916,9 +910,9 @@
if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query
return false;
if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr))
if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr))
return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr)))
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0])))
return false;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@ -944,7 +938,7 @@
txid: tx.txid,
time: tx.time,
blockheight: tx.blockheight,
senders: new Set(tx.vin.map(v => v.addr)),
senders: new Set(tx.vin.map(v => v.addresses[0])),
receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])),
data: tx.floData
} : tx.floData);
@ -964,8 +958,7 @@
caseFn: (function) flodata => return bool value
options can be used to filter data
after : query after the given txid
before : query before the given txid
mempool : query mempool tx or not (options same as readAllTx, DEFAULT=false: ignore unconfirmed tx)
confirmed : query only confirmed tx or not (options same as readAllTx, DEFAULT=true: only_confirmed_tx)
sentOnly : filters only sent data
receivedOnly: filters only received data
tx : (boolean) resolve tx data or not (resolves an Array of Object with tx details)
@ -975,23 +968,37 @@
const getLatestData = floBlockchainAPI.getLatestData = function (addr, caseFn, options = {}) {
return new Promise((resolve, reject) => {
//fetch options
let query_options = { latest: true };
query_options.mempool = isUndefined(options.mempool) ? false : options.mempool; //DEFAULT: ignore unconfirmed tx
if (!isUndefined(options.after)) query_options.after = options.after;
if (!isUndefined(options.before)) query_options.before = options.before;
let query_options = {};
query_options.confirmed = isUndefined(options.confirmed) ? true : options.confirmed; //DEFAULT: confirmed tx only
if (!isUndefined(options.page))
query_options.page = options.page;
//if (!isUndefined(options.after)) query_options.after = options.after;
let new_lastItem;
readTxs(addr, query_options).then(response => {
//lastItem confirmed tx checked
if (!new_lastItem) {
let last_tx = response.items.find(t => t.confirmations > 0);
if (last_tx)
new_lastItem = last_tx.txid;
}
if (typeof options.senders === "string") options.senders = [options.senders];
if (typeof options.receivers === "string") options.receivers = [options.receivers];
//check if `after` txid is in the response
let i_after = response.txs.findIndex(t => t.txid === options.after);
if (i_after != -1) //found lastItem, hence remove it and all txs before that
response.items.splice(i_after);
var item = response.items.find(tx => {
if (!tx.confirmations) //unconfirmed transactions: this should not happen as we send mempool=false in API query
return false;
if (options.sentOnly && !tx.vin.some(vin => vin.addr === addr))
if (options.sentOnly && !tx.vin.some(vin => vin.addresses[0] === addr))
return false;
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addr)))
else if (Array.isArray(options.senders) && !tx.vin.some(vin => options.senders.includes(vin.addresses[0])))
return false;
if (options.receivedOnly && !tx.vout.some(vout => vout.scriptPubKey.addresses[0] === addr))
@ -1004,32 +1011,31 @@
//if item found, then resolve the result
if (!isUndefined(item)) {
const result = { lastItem: response.lastItem };
const result = { lastItem: new_lastItem || item.txid };
if (options.tx) {
result.item = {
txid: tx.txid,
time: tx.time,
blockheight: tx.blockheight,
senders: new Set(tx.vin.map(v => v.addr)),
receivers: new Set(tx.vout.map(v => v.scriptPubKey.addresses[0])),
data: tx.floData
txid: item.txid,
time: item.time,
blockheight: item.blockheight,
senders: new Set(item.vin.map(v => v.addresses[0])),
receivers: new Set(item.vout.map(v => v.scriptPubKey.addresses[0])),
data: item.floData
}
} else
result.data = tx.floData;
result.data = item.floData;
return resolve(result);
}
if (response.page == response.totalPages || i_after != -1) //reached last page to check
resolve({ lastItem: new_lastItem || options.after }); //no data match the caseFn, resolve just the lastItem
//else if address needs chain query
else if (response.incomplete) {
let next_options = Object.assign({}, options);
options.before = response.initItem; //this fn uses latest option, so using before to chain query
getLatestData(addr, caseFn, next_options).then(r => {
r.lastItem = response.lastItem; //update last key as it should be the newest tx
resolve(r);
}).catch(error => reject(error))
else {
options.page = response.page + 1;
getLatestData(addr, caseFn, options)
.then(result => resolve(result))
.catch(error => reject(error))
}
//no data match the caseFn, resolve just the lastItem
else
resolve({ lastItem: response.lastItem });
}).catch(error => reject(error))
})

11088
testnet.html

File diff suppressed because it is too large Load Diff