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 { .transaction {
gap: 1rem; gap: 1.2rem;
padding: 1rem; padding: 1rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 0.3rem; border-radius: 0.3rem;
@ -792,7 +792,29 @@ h3 {
} }
.transaction__receiver { .transaction__receiver {
margin-left: 0.5rem; 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 { .transaction p {
font-size: 0.9rem; 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 { #main_header {
padding: 0.6rem 1rem; padding: 0.6rem 1rem;
} }
.app-brand{ .app-brand {
display: flex; display: flex;
gap: 0.3rem; gap: 0.3rem;
align-items: center; align-items: center;
.icon{ .icon {
height: 1.7rem; height: 1.7rem;
width: 1.7rem; width: 1.7rem;
} }
} }
.app-name{ .app-name {
&__company{ &__company {
font-size: 0.8rem; font-size: 0.8rem;
font-weight: 500; font-weight: 500;
color: rgba(var(--text-color), 0.8); color: rgba(var(--text-color), 0.8);
@ -741,7 +741,7 @@ h3 {
border-radius: 0.3rem; border-radius: 0.3rem;
} }
.transaction { .transaction {
gap: 1rem; gap: 1.2rem;
padding: 1rem; padding: 1rem;
background-color: rgba(var(--text-color), 0.03); background-color: rgba(var(--text-color), 0.03);
border-radius: 0.3rem; border-radius: 0.3rem;
@ -758,7 +758,39 @@ h3 {
} }
&__receiver { &__receiver {
margin-left: 0.5rem; 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 { p {
font-size: 0.9rem; font-size: 0.9rem;

View File

@ -187,13 +187,14 @@
</div> </div>
</div> </div>
<section class="grid gap-1"> <section class="grid gap-1">
<div class="flex align-center space-between sticky top-0" <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;"> style="background-color: rgba(var(--foreground-color), 1);transition: background-color .3s; padding-bottom: 0.5rem;">
<h4>Transactions</h4> <h4>Transactions</h4>
<sm-chips id="filter_selector" class="hidden"> <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="received">Received</sm-chip>
<sm-chip value="all">All</sm-chip> <sm-chip value="mined">Mined</sm-chip>
</sm-chips> </sm-chips>
</div> </div>
<ul id="queried_address_transactions" class="observe-empty-state"></ul> <ul id="queried_address_transactions" class="observe-empty-state"></ul>
@ -830,7 +831,14 @@
<div class="transaction__icon"></div> <div class="transaction__icon"></div>
<div class="transaction__receiver breakable"></div> <div class="transaction__receiver breakable"></div>
</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"> <div class="flex align-center space-between">
<a href="" class="transaction__link flex align-center" target="_blank" rel="noopener noreferrer"> <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" <svg class="icon margin-right-0-5" xmlns="http://www.w3.org/2000/svg" height="24px"
@ -1208,15 +1216,15 @@
page = parseInt(page) page = parseInt(page)
if (floGlobals.query.string !== query) { if (floGlobals.query.string !== query) {
checkBalance(query) checkBalance(query)
fetchTransactions(query).then(() => { }/* else if (floGlobals.query.totalPages <= page * txsPerPage) {
filterFetchedTransactions()
render.paginatedTransactions(page)
})
} else if (page % Math.ceil(1000 / txsPerPage) === 0 && floGlobals.query.transactions.length <= page * txsPerPage) {
loadMoreTransactions() loadMoreTransactions()
} else { } else {
render.paginatedTransactions(page) render.paginatedTransactions(page)
} }*/
fetchTransactions(query, page).then(() => {
filterFetchedTransactions()
render.paginatedTransactions(page)
})
} catch (err) { } catch (err) {
notify(err, 'error') notify(err, 'error')
} }
@ -1877,45 +1885,55 @@
}) })
}, },
transactionCard(details) { transactionCard(details) {
const { sender, receiver, floData, time, txid } = details const { sender, receiver, floData, time, txid, netValue, mine } = details
const { query: queriedFloId } = pagesData.params const { query: queriedFloId } = pagesData.params
const clone = getRef('transaction_template').content.cloneNode(true).firstElementChild; 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__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) { } 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 { } 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__link').href = `${floBlockchainAPI.current_server}tx/${txid}`
clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000) clone.querySelector('.transaction__time').textContent = getFormattedTime(time * 1000)
return clone return clone
}, },
paginatedTransactions(page = parseInt(pagesData.params.page) || 1) { paginatedTransactions(page = parseInt(pagesData.params.page) || 1) {
const { transactions, string: address, filteredTransactions } = floGlobals.query const { transactions, string: address, filteredTransactions, totalPages } = 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 renderedTransactions = (filteredTransactions || transactions) const renderedTransactions = (filteredTransactions || transactions)
.slice(startingIndex, endingIndex)
.map(transaction => render.transactionCard(transaction)) .map(transaction => render.transactionCard(transaction))
renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`) renderElem(getRef('queried_address_transactions'), html`${renderedTransactions}`)
getRef('transactions_hero_section').scrollIntoView({ getRef('transactions_hero_section').scrollIntoView({
behavior: 'smooth', behavior: 'smooth',
block: 'start' block: 'start'
}) })
if (floGlobals.query.transactions.length) { if (transactions.length) {
getRef('filter_selector').classList.remove('hidden') getRef('filter_selector').classList.remove('hidden')
} else { } else {
getRef('filter_selector').classList.add('hidden') getRef('filter_selector').classList.add('hidden')
} }
const paginationSegments = (filteredTransactions || transactions) ? Math.ceil((filteredTransactions || transactions).length / txsPerPage) : 0; const paginationSegments = totalPages;
let pagination = [] let pagination = []
let startingPage = page - 2; let startingPage = page - 2;
let showTill = page + 2; let showTill = page + 2;
@ -1956,11 +1974,11 @@
} else { } else {
getRef('pagination_wrapper').classList.add('hidden') 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') document.getElementById('load_more_transactions').classList.remove('hidden')
} else { } else {
document.getElementById('load_more_transactions').classList.add('hidden') document.getElementById('load_more_transactions').classList.add('hidden')
} } */
}, },
availableAssetOptions() { availableAssetOptions() {
return (floGlobals.tokens || []).map(token => html` <sm-option value=${token}>${token}</sm-option> `) return (floGlobals.tokens || []).map(token => html` <sm-option value=${token}>${token}</sm-option> `)
@ -2496,12 +2514,12 @@
getRef('address_details_wrapper').classList.remove('hidden') getRef('address_details_wrapper').classList.remove('hidden')
floWebWallet.getLabels().then(allLabels => { floWebWallet.getLabels().then(allLabels => {
if (allLabels[queriedFloId]) { 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 { } else {
renderElem(getRef('queried_flo_address'), html` getRef('queried_flo_address').innerHTML = `
<p class="label">FLO Address </p> <p class="label">FLO Address </p>
<h4><sm-copy clip-text value=${queriedFloId}></sm-copy></h4> <h4><sm-copy clip-text value=${queriedFloId}></sm-copy></h4>
`) `;
} }
}) })
const queriedFloId = address || getRef('search_query_input').value.trim() const queriedFloId = address || getRef('search_query_input').value.trim()
@ -2524,7 +2542,7 @@
getRef('token_list_wrapper').classList.remove('hidden') getRef('token_list_wrapper').classList.remove('hidden')
} }
// retrieve FLO balance // retrieve FLO balance
getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(3))} FLO`; getRef('flo_balance').textContent = `${parseFloat(floBalance.toFixed(8))} FLO`;
} catch (e) { } catch (e) {
console.error(e) console.error(e)
} }
@ -2551,8 +2569,8 @@
string: '', string: '',
filteredTransactions: null filteredTransactions: null
} }
const txsPerPage = 25; const txsPerPage = 100;
async function fetchTransactions(address, loadOlder = false) { async function fetchTransactions(address, page = 1) {
try { try {
document.getElementById('load_more_transactions').classList.add('hidden') document.getElementById('load_more_transactions').classList.add('hidden')
renderElem(getRef('pagination_wrapper'), html``) renderElem(getRef('pagination_wrapper'), html``)
@ -2562,23 +2580,12 @@
<span>Loading transactions...</span> <span>Loading transactions...</span>
</div> </div>
`) `)
if (loadOlder) { const { items, totalPages } = await floWebWallet.listTransactions(address, { pageSize: txsPerPage, page })
const { items, initItem } = await floWebWallet.listTransactions.syncOld(address, floGlobals.query.initItem) floGlobals.query = {
floGlobals.query = { transactions: items,
transactions: [...items, ...floGlobals.query.transactions], string: address,
string: address, filteredTransactions: null,
initItem, totalPages
filteredTransactions: null
}
} else {
const { items, lastItem, initItem } = await floWebWallet.listTransactions(address)
floGlobals.query = {
transactions: items,
string: address,
lastItem,
initItem,
filteredTransactions: null
}
} }
} catch (err) { } catch (err) {
renderElem(getRef('queried_address_transactions'), html` <span>Failed to load transactions</span> `) renderElem(getRef('queried_address_transactions'), html` <span>Failed to load transactions</span> `)
@ -2588,7 +2595,19 @@
function filterFetchedTransactions() { function filterFetchedTransactions() {
const filter = getRef('filter_selector').value; const filter = getRef('filter_selector').value;
if (filter !== 'all') { 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 { } else {
floGlobals.query.filteredTransactions = null 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) => { 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 => { floBlockchainAPI.readTxs(address, options).then(response => {
const result = {} const result = {}
result.items = response.items.map(({ time, txid, floData, isCoinBase, vin, vout }) => ({ result.items = response.txs.map(tx => formatTx(address, tx));
time, txid, floData, isCoinBase, result.page = response.page;
sender: isCoinBase ? `(mined)${vin[0].coinbase}` : vin[0].addr, result.totalPages = response.totalPages;
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;
resolve(result); resolve(result);
}).catch(error => reject(error)) }).catch(error => reject(error))
}) })

View File

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

11088
testnet.html

File diff suppressed because it is too large Load Diff