Compare commits
33 Commits
testing-ap
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0158c875e5 | |||
|
|
dbc5cfda95 | ||
|
|
b61565398c | ||
|
|
d6d3da01e0 | ||
|
|
bb8a5a4dd1 | ||
|
|
712515a990 | ||
|
|
6aa9fa0255 | ||
|
|
fca68debf1 | ||
|
|
c5b61f3c05 | ||
|
|
2e3b0209b5 | ||
|
|
33a972afc6 | ||
|
|
a52b27a4d5 | ||
|
|
74bb2608dc | ||
|
|
c857c04b85 | ||
|
|
cd89411365 | ||
|
|
4403cee668 | ||
|
|
32cc6e166c | ||
|
|
779af53fcb | ||
|
|
f09870bf3a | ||
|
|
74def25d14 | ||
|
|
83e3905408 | ||
|
|
5203782935 | ||
|
|
9412a707d8 | ||
|
|
f28ecf5f3f | ||
|
|
09a4943298 | ||
|
|
22672d0172 | ||
|
|
0b02732f48 | ||
|
|
52de5ff1cd | ||
|
|
1f11a8f286 | ||
|
|
1fd6dbbf9c | ||
|
|
e0c8f9ee7a | ||
|
|
129daf7a5b | ||
|
|
80c74cd9d9 |
32
.github/workflows/push-dappbundle.yml
vendored
Normal file
32
.github/workflows/push-dappbundle.yml
vendored
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
name: Workflow push to Dappbundle
|
||||||
|
on: [push]
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build
|
||||||
|
runs-on: self-hosted
|
||||||
|
steps:
|
||||||
|
- name: Executing remote command
|
||||||
|
uses: appleboy/ssh-action@v1.0.0
|
||||||
|
with:
|
||||||
|
host: ${{ secrets.R_HOST }}
|
||||||
|
username: ${{ secrets.P_USERNAME }}
|
||||||
|
password: ${{ secrets.P_PASSWORD }}
|
||||||
|
port: ${{ secrets.SSH_PORT }}
|
||||||
|
script: |
|
||||||
|
if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle" ]; then
|
||||||
|
echo "Folder exists. Skipping Git clone."
|
||||||
|
else
|
||||||
|
echo "Folder does not exist. Cloning repository..."
|
||||||
|
cd ${{ secrets.DEPLOYMENT_LOCATION}}/ && git clone https://github.com/ranchimall/dappbundle.git
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -d "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" ]; then
|
||||||
|
echo "Repository exists. Remove folder "
|
||||||
|
rm -r "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Cloning repository..."
|
||||||
|
cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle && git clone https://github.com/ranchimall/${{ github.event.repository.name }}
|
||||||
|
|
||||||
|
cd "${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/${{ github.event.repository.name }}" && rm -rf .gitattributes .git .github .gitignore
|
||||||
|
cd ${{ secrets.DEPLOYMENT_LOCATION}}/dappbundle/ && git add . && git commit -m "Workflow updating files of ${{ github.event.repository.name }}" && git push "https://ranchimalldev:${{ secrets.RM_ACCESS_TOKEN }}@github.com/ranchimall/dappbundle.git"
|
||||||
18
.github/workflows/repopush.yml
vendored
18
.github/workflows/repopush.yml
vendored
@ -1,18 +0,0 @@
|
|||||||
# push contents of this repo to specified repo on github on commit
|
|
||||||
name: push this repo to bundle repo
|
|
||||||
on: [push]
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
- name: push to bundle repo
|
|
||||||
uses: ad-m/github-push-action@master
|
|
||||||
with:
|
|
||||||
github_token: ${{ secrets.SM_ACCESS_TOKEN }}
|
|
||||||
branch: master
|
|
||||||
force: true
|
|
||||||
directory: .
|
|
||||||
repository: ranchimall/dappbundle
|
|
||||||
@ -1,4 +1,5 @@
|
|||||||
# BTC Wallet
|
# BTC Wallet
|
||||||
|
[](https://github.com/ranchimall/btcwallet/actions/workflows/push-dappbundle.yml)
|
||||||
|
|
||||||
BTC Wallet
|
BTC Wallet
|
||||||
It is a web-based Bitcoin wallet and Bitcoin explorer that promotes self-custody by generating Bitcoin addresses and private keys locally on the user's device. If you are suspicious about internet monitoring, then open the URL, disconnect your computer from the internet, and then generate the Bitcoin address and private key.
|
It is a web-based Bitcoin wallet and Bitcoin explorer that promotes self-custody by generating Bitcoin addresses and private keys locally on the user's device. If you are suspicious about internet monitoring, then open the URL, disconnect your computer from the internet, and then generate the Bitcoin address and private key.
|
||||||
|
|||||||
96
css/main.css
96
css/main.css
@ -27,7 +27,6 @@ body {
|
|||||||
scrollbar-gutter: stable;
|
scrollbar-gutter: stable;
|
||||||
color: rgba(var(--text-color), 1);
|
color: rgba(var(--text-color), 1);
|
||||||
background-color: rgba(var(--background-color), 1);
|
background-color: rgba(var(--background-color), 1);
|
||||||
transition: background-color 0.3s;
|
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@ -394,6 +393,10 @@ ol li::before {
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.align-self-start {
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
.align-center {
|
.align-center {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
}
|
}
|
||||||
@ -658,7 +661,7 @@ ol li::before {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#main_header {
|
#main_header {
|
||||||
padding: 1.5rem max(1rem, 4vw);
|
padding: 1rem max(1rem, 4vw);
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-brand {
|
.app-brand {
|
||||||
@ -683,7 +686,6 @@ ol li::before {
|
|||||||
grid-template-rows: auto 1fr;
|
grid-template-rows: auto 1fr;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
transition: background-color 0.3s;
|
|
||||||
background-color: rgba(var(--foreground-color), 1);
|
background-color: rgba(var(--foreground-color), 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -823,6 +825,10 @@ ol li::before {
|
|||||||
margin-bottom: 0.3rem;
|
margin-bottom: 0.3rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#search_query_input {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
#address_balance_card {
|
#address_balance_card {
|
||||||
padding: 1.5rem;
|
padding: 1.5rem;
|
||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
@ -837,6 +843,13 @@ ol li::before {
|
|||||||
color: var(--accent-color);
|
color: var(--accent-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#filter_selector {
|
||||||
|
--padding: 0.3rem 0.5rem;
|
||||||
|
}
|
||||||
|
#filter_selector sm-chip {
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.card {
|
.card {
|
||||||
padding: 0.5rem 0;
|
padding: 0.5rem 0;
|
||||||
border: none;
|
border: none;
|
||||||
@ -871,21 +884,55 @@ ol li::before {
|
|||||||
border-bottom: thin solid rgba(var(--text-color), 0.3);
|
border-bottom: thin solid rgba(var(--text-color), 0.3);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tx_details__header:has(#tx_status) time {
|
||||||
|
text-align: left;
|
||||||
|
margin-right: auto;
|
||||||
|
}
|
||||||
|
#tx_details__header:has(:not(#tx_status)) time {
|
||||||
|
text-align: right;
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
#transactions_list {
|
#transactions_list {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 2rem;
|
gap: 2rem;
|
||||||
padding-bottom: 4rem;
|
padding-bottom: 4rem;
|
||||||
|
padding-top: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
transaction-card {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
transaction-card:not(:last-of-type) {
|
||||||
|
padding-bottom: 2rem;
|
||||||
|
}
|
||||||
|
transaction-card:not(:last-of-type)::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
right: 0;
|
||||||
|
height: 1px;
|
||||||
|
width: calc(100% - 3.5rem);
|
||||||
|
background-color: rgba(var(--text-color), 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
.transaction {
|
.transaction {
|
||||||
|
position: relative;
|
||||||
grid-template-columns: auto 1fr;
|
grid-template-columns: auto 1fr;
|
||||||
gap: 0.5rem 1rem;
|
gap: 0.5rem 1rem;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
content-visibility: auto;
|
|
||||||
contain-intrinsic-height: 8rem;
|
|
||||||
}
|
}
|
||||||
.transaction:not(:last-of-type) {
|
.transaction.in .transaction__amount {
|
||||||
padding-bottom: 2rem;
|
color: var(--green);
|
||||||
|
}
|
||||||
|
.transaction.in .transaction__amount::before {
|
||||||
|
content: "+";
|
||||||
|
}
|
||||||
|
.transaction.out .transaction__amount {
|
||||||
|
color: var(--danger-color);
|
||||||
|
}
|
||||||
|
.transaction.out .transaction__amount::before {
|
||||||
|
content: "-";
|
||||||
}
|
}
|
||||||
.transaction__amount {
|
.transaction__amount {
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
@ -893,21 +940,9 @@ ol li::before {
|
|||||||
.transaction.out .transaction__icon .icon {
|
.transaction.out .transaction__icon .icon {
|
||||||
fill: var(--danger-color);
|
fill: var(--danger-color);
|
||||||
}
|
}
|
||||||
.transaction.out .transaction__amount {
|
|
||||||
color: var(--danger-color);
|
|
||||||
}
|
|
||||||
.transaction.out .transaction__amount::before {
|
|
||||||
content: "- ";
|
|
||||||
}
|
|
||||||
.transaction.in .transaction__icon .icon {
|
.transaction.in .transaction__icon .icon {
|
||||||
fill: var(--green);
|
fill: var(--green);
|
||||||
}
|
}
|
||||||
.transaction.in .transaction__amount {
|
|
||||||
color: var(--green);
|
|
||||||
}
|
|
||||||
.transaction.in .transaction__amount::before {
|
|
||||||
content: "+ ";
|
|
||||||
}
|
|
||||||
.transaction.unconfirmed-tx .transaction__icon .icon {
|
.transaction.unconfirmed-tx .transaction__icon .icon {
|
||||||
fill: var(--yellow);
|
fill: var(--yellow);
|
||||||
}
|
}
|
||||||
@ -928,6 +963,7 @@ ol li::before {
|
|||||||
.transaction__time {
|
.transaction__time {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
color: rgba(var(--text-color), 0.8);
|
color: rgba(var(--text-color), 0.8);
|
||||||
|
text-wrap: balance;
|
||||||
}
|
}
|
||||||
.transaction__amount {
|
.transaction__amount {
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
@ -954,6 +990,15 @@ ol li::before {
|
|||||||
content: ",";
|
content: ",";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tx_details time {
|
||||||
|
padding: 0.3rem 0.8rem;
|
||||||
|
border-radius: 5rem;
|
||||||
|
background-color: rgba(var(--text-color), 0.06);
|
||||||
|
font-weight: 500;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
|
||||||
#tx_status {
|
#tx_status {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -962,11 +1007,23 @@ ol li::before {
|
|||||||
border-radius: 0.5rem;
|
border-radius: 0.5rem;
|
||||||
background-color: rgba(var(--text-color), 0.03);
|
background-color: rgba(var(--text-color), 0.03);
|
||||||
color: var(--danger-color);
|
color: var(--danger-color);
|
||||||
|
width: -webkit-fit-content;
|
||||||
|
width: -moz-fit-content;
|
||||||
|
width: fit-content;
|
||||||
}
|
}
|
||||||
#tx_status .icon {
|
#tx_status .icon {
|
||||||
fill: var(--danger-color);
|
fill: var(--danger-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#tx_amount {
|
||||||
|
font-size: max(2rem, 4vw);
|
||||||
|
font-weight: 700;
|
||||||
|
text-wrap: balance;
|
||||||
|
}
|
||||||
|
#tx_amount:has(+ *) {
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
#tx_technicals .tx-detail:first-of-type {
|
#tx_technicals .tx-detail:first-of-type {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
@ -1197,6 +1254,7 @@ ol li::before {
|
|||||||
}
|
}
|
||||||
#main_header {
|
#main_header {
|
||||||
grid-area: header;
|
grid-area: header;
|
||||||
|
padding: 1.5rem max(1rem, 4vw);
|
||||||
}
|
}
|
||||||
#main_navbar {
|
#main_navbar {
|
||||||
grid-area: nav;
|
grid-area: nav;
|
||||||
|
|||||||
2
css/main.min.css
vendored
2
css/main.min.css
vendored
File diff suppressed because one or more lines are too long
349
css/main.scss
349
css/main.scss
File diff suppressed because it is too large
Load Diff
1370
index.html
1370
index.html
File diff suppressed because one or more lines are too long
@ -1,19 +1,437 @@
|
|||||||
(function (EXPORTS) { //btcOperator v1.1.4
|
(function (EXPORTS) { //btcOperator v1.2.10
|
||||||
/* BTC Crypto and API Operator */
|
/* BTC Crypto and API Operator */
|
||||||
const btcOperator = EXPORTS;
|
const btcOperator = EXPORTS;
|
||||||
|
const SATOSHI_IN_BTC = 1e8;
|
||||||
|
|
||||||
|
const util = btcOperator.util = {};
|
||||||
|
|
||||||
|
util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
|
||||||
|
util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
|
||||||
|
|
||||||
|
const checkIfTor = btcOperator.checkIfTor = () => {
|
||||||
|
return fetch('https://check.torproject.org/api/ip')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(res => {
|
||||||
|
return res.IsTor
|
||||||
|
}).catch(e => {
|
||||||
|
console.error(e)
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
let isTor = false;
|
||||||
|
checkIfTor().then(result => isTor = result);
|
||||||
|
|
||||||
|
async function post(url, data, { asText = false } = {}) {
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
method: 'POST',
|
||||||
|
mode: 'no-cors',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
if (response.ok) {
|
||||||
|
return asText ? await response.text() : await response.json()
|
||||||
|
} else {
|
||||||
|
throw response
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// NOTE: some APIs may not support all functions properly hence they are omitted
|
||||||
|
const APIs = btcOperator.APIs = [
|
||||||
|
{
|
||||||
|
url: 'https://api.blockcypher.com/v1/btc/main/',
|
||||||
|
name: 'Blockcypher',
|
||||||
|
balance({ addr }) {
|
||||||
|
return fetch_api(`addrs/${addr}/balance`, { url: this.url })
|
||||||
|
.then(result => util.Sat_to_BTC(result.balance))
|
||||||
|
},
|
||||||
|
async block({ id }) {
|
||||||
|
try {
|
||||||
|
let block = await fetch_api(`blocks/${id}`, { url: this.url })
|
||||||
|
return formatBlock(block)
|
||||||
|
} catch (e) {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async broadcast({ rawTxHex, url }) {
|
||||||
|
try {
|
||||||
|
const result = await post(`${url || this.url}pushtx`, { tx: rawTxHex })
|
||||||
|
return result.hash
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://blockstream.info/api/',
|
||||||
|
name: 'Blockstream',
|
||||||
|
hasOnion: true,
|
||||||
|
onionUrl: `http://explorerzydxu5ecjrkwceayqybizmpjjznk5izmitf2modhcusuqlid.onion/api/`,
|
||||||
|
balance({ addr, url }) {
|
||||||
|
return fetch_api(`address/${addr}/utxo`, { url: url || this.url })
|
||||||
|
.then(result => {
|
||||||
|
const balance = result.reduce((t, u) => t + u.value, 0)
|
||||||
|
return util.Sat_to_BTC(balance)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
latestBlock() {
|
||||||
|
return fetch_api(`blocks/tip/height`, { url: this.url })
|
||||||
|
},
|
||||||
|
tx({ txid, url }) {
|
||||||
|
return fetch_api(`tx/${txid}`, { url: url || this.url })
|
||||||
|
.then(result => formatTx(result))
|
||||||
|
},
|
||||||
|
txHex({ txid, url }) {
|
||||||
|
return fetch_api(`tx/${txid}/hex`, { url: url || this.url, asText: true })
|
||||||
|
},
|
||||||
|
txs({ addr, url, ...args }) {
|
||||||
|
let queryParams = Object.entries(args).map(([key, value]) => `${key}=${value}`).join('&')
|
||||||
|
if (queryParams)
|
||||||
|
queryParams = '?' + queryParams
|
||||||
|
return fetch_api(`address/${addr}/txs${queryParams}`, { url: url || this.url })
|
||||||
|
},
|
||||||
|
async block({ id, url }) {
|
||||||
|
// if id is hex string then it is block hash
|
||||||
|
try {
|
||||||
|
let blockHash = id
|
||||||
|
if (!/^[0-9a-f]{64}$/i.test(id))
|
||||||
|
blockHash = await fetch_api(`block-height/${id}`, { url: url || this.url, asText: true })
|
||||||
|
const block = await fetch_api(`block/${blockHash}`, { url: url || this.url })
|
||||||
|
return formatBlock(block)
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async broadcast({ rawTxHex, url }) {
|
||||||
|
return post(`${url || this.url}tx`, { tx: rawTxHex }, { asText: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://mempool.space/api/',
|
||||||
|
name: 'Mempool',
|
||||||
|
balance({ addr }) {
|
||||||
|
return fetch_api(`address/${addr}`, { url: this.url })
|
||||||
|
.then(result => util.Sat_to_BTC(result.chain_stats.funded_txo_sum - result.chain_stats.spent_txo_sum))
|
||||||
|
},
|
||||||
|
latestBlock() {
|
||||||
|
return fetch_api(`blocks/tip/height`, { url: this.url })
|
||||||
|
},
|
||||||
|
tx({ txid }) {
|
||||||
|
return fetch_api(`tx/${txid}`, { url: this.url })
|
||||||
|
.then(result => formatTx(result))
|
||||||
|
|
||||||
|
},
|
||||||
|
txHex({ txid }) {
|
||||||
|
return fetch_api(`tx/${txid}/hex`, { url: this.url, asText: true })
|
||||||
|
},
|
||||||
|
txs({ addr, ...args }) {
|
||||||
|
let queryParams = Object.entries(args).map(([key, value]) => `${key}=${value}`).join('&')
|
||||||
|
if (queryParams)
|
||||||
|
queryParams = '?' + queryParams
|
||||||
|
return fetch_api(`address/${addr}/txs${queryParams}`, { url: this.url })
|
||||||
|
},
|
||||||
|
async block({ id }) {
|
||||||
|
// if id is hex string then it is block hash
|
||||||
|
try {
|
||||||
|
let blockHash = id
|
||||||
|
if (!/^[0-9a-f]{64}$/i.test(id))
|
||||||
|
blockHash = await fetch_api(`block-height/${id}`, { url: this.url, asText: true })
|
||||||
|
const block = await fetch_api(`block/${blockHash}`, { url: this.url })
|
||||||
|
return formatBlock(block)
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async broadcast({ rawTxHex, url }) {
|
||||||
|
return post(`${url || this.url}tx`, { tx: rawTxHex }, { asText: true })
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://blockchain.info/',
|
||||||
|
name: 'Blockchain',
|
||||||
|
balance({ addr }) {
|
||||||
|
return fetch_api(`q/addressbalance/${addr}`, { url: this.url })
|
||||||
|
.then(result => util.Sat_to_BTC(result))
|
||||||
|
},
|
||||||
|
unspent({ addr, allowUnconfirmedUtxos = false }) {
|
||||||
|
return fetch_api(`unspent?active=${addr}`, { url: this.url })
|
||||||
|
.then(result => formatUtxos(result.unspent_outputs, allowUnconfirmedUtxos))
|
||||||
|
},
|
||||||
|
tx({ txid }) {
|
||||||
|
return fetch_api(`rawtx/${txid}`, { url: this.url })
|
||||||
|
.then(result => formatTx(result))
|
||||||
|
},
|
||||||
|
txHex({ txid }) {
|
||||||
|
return fetch_api(`rawtx/${txid}?format=hex`, { url: this.url, asText: true })
|
||||||
|
},
|
||||||
|
txs({ addr, ...args }) {
|
||||||
|
let queryParams = Object.entries(args).map(([key, value]) => `${key}=${value}`).join('&')
|
||||||
|
if (queryParams)
|
||||||
|
queryParams = '?' + queryParams
|
||||||
|
return fetch_api(`rawaddr/${addr}${queryParams}`, { url: this.url })
|
||||||
|
.then(result => result.txs)
|
||||||
|
},
|
||||||
|
latestBlock() {
|
||||||
|
return fetch_api(`q/getblockcount`, { url: this.url })
|
||||||
|
},
|
||||||
|
async block({ id }) {
|
||||||
|
try {
|
||||||
|
let block
|
||||||
|
// if id is hex string then it is block hash
|
||||||
|
if (/^[0-9a-f]{64}$/i.test(id))
|
||||||
|
block = await fetch_api(`rawblock/${id}`, { url: this.url })
|
||||||
|
else {
|
||||||
|
const result = await fetch_api(`block-height/${id}?format=json`, { url: this.url })
|
||||||
|
block = result.blocks[0]
|
||||||
|
}
|
||||||
|
return formatBlock(block)
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async blockTxs({ id }) {
|
||||||
|
try {
|
||||||
|
let block
|
||||||
|
// if id is hex string then it is block hash
|
||||||
|
if (/^[0-9a-f]{64}$/i.test(id))
|
||||||
|
block = await fetch_api(`rawblock/${id}`, { url: this.url })
|
||||||
|
else {
|
||||||
|
const result = await fetch_api(`block-height/${id}?format=json`, { url: this.url })
|
||||||
|
block = result.blocks[0]
|
||||||
|
}
|
||||||
|
return block.tx
|
||||||
|
} catch (e) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
url: 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction',
|
||||||
|
name: 'Coinb.in',
|
||||||
|
broadcast({ rawTxHex }) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
fetch(this.url, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/x-www-form-urlencoded'
|
||||||
|
},
|
||||||
|
body: "rawtx=" + rawTxHex
|
||||||
|
}).then(response => {
|
||||||
|
console.log(response)
|
||||||
|
response.text().then(resultText => {
|
||||||
|
let r = resultText.match(/<result>.*<\/result>/);
|
||||||
|
if (!r)
|
||||||
|
reject(resultText);
|
||||||
|
else {
|
||||||
|
r = r.pop().replace('<result>', '').replace('</result>', '');
|
||||||
|
if (r == '1') {
|
||||||
|
let txid = resultText.match(/<txid>.*<\/txid>/).pop().replace('<txid>', '').replace('</txid>', '');
|
||||||
|
resolve(txid);
|
||||||
|
} else if (r == '0') {
|
||||||
|
let error
|
||||||
|
if (resultText.includes('<message>')) {
|
||||||
|
error = resultText.match(/<message>.*<\/message>/).pop().replace('<message>', '').replace('</message>', '');
|
||||||
|
} else {
|
||||||
|
error = resultText.match(/<response>.*<\/response>/).pop().replace('<response>', '').replace('</response>', '');
|
||||||
|
}
|
||||||
|
reject(decodeURIComponent(error.replace(/\+/g, " ")));
|
||||||
|
} else reject(resultText);
|
||||||
|
}
|
||||||
|
}).catch(error => reject(error))
|
||||||
|
}).catch(error => reject(error))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
btcOperator.util.format = {} // functions to homogenize API results
|
||||||
|
const formatBlock = btcOperator.util.format.block = async (block) => {
|
||||||
|
try {
|
||||||
|
const { height, hash, id, time, timestamp, mrkl_root, merkle_root, prev_block, next_block, size } = block;
|
||||||
|
const details = {
|
||||||
|
height,
|
||||||
|
hash: hash || id,
|
||||||
|
time: (time || timestamp) * 1000,
|
||||||
|
merkle_root: merkle_root || mrkl_root,
|
||||||
|
size,
|
||||||
|
}
|
||||||
|
if (prev_block)
|
||||||
|
details.prev_block = prev_block
|
||||||
|
if (next_block)
|
||||||
|
details.next_block = next_block[0]
|
||||||
|
return details
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const formatUtxos = btcOperator.util.format.utxos = async (utxos, allowUnconfirmedUtxos = false) => {
|
||||||
|
try {
|
||||||
|
if (!allowUnconfirmedUtxos && !utxos || !Array.isArray(utxos))
|
||||||
|
throw {
|
||||||
|
message: "No utxos found",
|
||||||
|
code: 1000 //error code for when issue is not from API but situational (like no utxos found)
|
||||||
|
}
|
||||||
|
return utxos.map(utxo => {
|
||||||
|
const { tx_hash, tx_hash_big_endian, txid, tx_output_n, vout, value, script, confirmations, status: { confirmed } = {} } = utxo;
|
||||||
|
return {
|
||||||
|
confirmations: confirmations || confirmed,
|
||||||
|
tx_hash_big_endian: tx_hash_big_endian || tx_hash || txid,
|
||||||
|
tx_output_n: tx_output_n || vout,
|
||||||
|
value,
|
||||||
|
script
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const formatTx = btcOperator.util.format.tx = async (tx) => {
|
||||||
|
try {
|
||||||
|
let { txid, hash, time, block_height, fee, fees, received,
|
||||||
|
confirmed, size, double_spend, block_hash, confirmations,
|
||||||
|
status: { block_height: statusBlockHeight, block_hash: statusBlockHash, block_time } = {}
|
||||||
|
} = tx;
|
||||||
|
if ((block_height || statusBlockHeight) && confirmations === undefined || confirmations === null) {
|
||||||
|
const latestBlock = await multiApi('latestBlock');
|
||||||
|
confirmations = latestBlock - (block_height || statusBlockHeight);
|
||||||
|
}
|
||||||
|
const inputs = tx.vin || tx.inputs;
|
||||||
|
const outputs = tx.vout || tx.outputs || tx.out;
|
||||||
|
return {
|
||||||
|
hash: hash || txid,
|
||||||
|
size: size,
|
||||||
|
fee: fee || fees,
|
||||||
|
double_spend,
|
||||||
|
time: (time * 1000) || new Date(confirmed || received).getTime() || block_time * 1000 || Date.now(),
|
||||||
|
block_height: block_height || statusBlockHeight,
|
||||||
|
block_hash: block_hash || statusBlockHash,
|
||||||
|
confirmations,
|
||||||
|
inputs: inputs.map(input => {
|
||||||
|
return {
|
||||||
|
index: input.n || input.output_index || input.vout,
|
||||||
|
prev_out: {
|
||||||
|
addr: input.prev_out?.addr || input.addresses?.[0] || input.prev_out?.address || input.addr || input.prevout.scriptpubkey_address,
|
||||||
|
value: input.prev_out?.value || input.output_value || input.prevout.value,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
out: outputs.map(output => {
|
||||||
|
return {
|
||||||
|
addr: output.scriptpubkey_address || output.addresses?.[0] || output.scriptpubkey_address || output.addr,
|
||||||
|
value: output.value || output.scriptpubkey_value,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
throw e
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const multiApi = btcOperator.multiApi = async (fnName, { index = 0, ...args } = {}) => {
|
||||||
|
try {
|
||||||
|
let triedOnion = false;
|
||||||
|
while (index < APIs.length) {
|
||||||
|
if (!APIs[index][fnName] || (APIs[index].coolDownTime && APIs[index].coolDownTime > new Date().getTime())) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return await APIs[index][fnName](args);
|
||||||
|
}
|
||||||
|
if (isTor && !triedOnion) {
|
||||||
|
triedOnion = true;
|
||||||
|
index = 0;
|
||||||
|
while (index < APIs.length) {
|
||||||
|
if (!APIs[index].hasOnion || (APIs[index].coolDownTime && APIs[index].coolDownTime > new Date().getTime())) {
|
||||||
|
index += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
return await multiApi(fnName, { index: index + 1, ...args, url: APIs[index].onionUrl });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw "No API available"
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error)
|
||||||
|
if (!APIs[index])
|
||||||
|
throw "No API available"
|
||||||
|
APIs[index].coolDownTime = new Date().getTime() + 1000 * 60 * 10; // 10 minutes
|
||||||
|
return multiApi(fnName, { index: index + 1, ...args });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function parseTx(tx, addressOfTx) {
|
||||||
|
const { txid, hash, time, block_height, inputs, outputs, out, vin, vout, fee, fees, received, confirmed, status: { block_height: statusBlockHeight, block_time } = {} } = tx;
|
||||||
|
let parsedTx = {
|
||||||
|
txid: hash || txid,
|
||||||
|
time: (time * 1000) || new Date(confirmed || received).getTime() || block_time * 1000 || Date.now(),
|
||||||
|
block: block_height || statusBlockHeight,
|
||||||
|
}
|
||||||
|
//sender list
|
||||||
|
parsedTx.tx_senders = {};
|
||||||
|
(inputs || vin).forEach(i => {
|
||||||
|
const address = i.prev_out?.addr || i.addresses?.[0] || i.prev_out?.address || i.addr || i.prevout.scriptpubkey_address;
|
||||||
|
const value = i.prev_out?.value || i.output_value || i.value || i.prevout.value;
|
||||||
|
if (address in parsedTx.tx_senders)
|
||||||
|
parsedTx.tx_senders[address] += value;
|
||||||
|
else parsedTx.tx_senders[address] = value;
|
||||||
|
});
|
||||||
|
parsedTx.tx_input_value = 0;
|
||||||
|
for (let senderAddr in parsedTx.tx_senders) {
|
||||||
|
let val = parsedTx.tx_senders[senderAddr];
|
||||||
|
parsedTx.tx_senders[senderAddr] = util.Sat_to_BTC(val);
|
||||||
|
parsedTx.tx_input_value += val;
|
||||||
|
}
|
||||||
|
parsedTx.tx_input_value = util.Sat_to_BTC(parsedTx.tx_input_value);
|
||||||
|
//receiver list
|
||||||
|
parsedTx.tx_receivers = {};
|
||||||
|
(outputs || out || vout).forEach(o => {
|
||||||
|
const address = o.scriptpubkey_address || o.addresses?.[0] || o.scriptpubkey_address || o.addr;
|
||||||
|
const value = o.value || o.scriptpubkey_value;
|
||||||
|
if (address in parsedTx.tx_receivers)
|
||||||
|
parsedTx.tx_receivers[address] += value;
|
||||||
|
else parsedTx.tx_receivers[address] = value;
|
||||||
|
});
|
||||||
|
parsedTx.tx_output_value = 0;
|
||||||
|
for (let receiverAddr in parsedTx.tx_receivers) {
|
||||||
|
let val = parsedTx.tx_receivers[receiverAddr];
|
||||||
|
parsedTx.tx_receivers[receiverAddr] = util.Sat_to_BTC(val);
|
||||||
|
parsedTx.tx_output_value += val;
|
||||||
|
}
|
||||||
|
parsedTx.tx_output_value = util.Sat_to_BTC(parsedTx.tx_output_value);
|
||||||
|
// tx fee
|
||||||
|
parsedTx.tx_fee = util.Sat_to_BTC(fee || fees || (parsedTx.tx_input_value - parsedTx.tx_output_value));
|
||||||
|
//detect tx type (in, out, self)
|
||||||
|
if (Object.keys(parsedTx.tx_receivers).length === 1 && Object.keys(parsedTx.tx_senders).length === 1 && Object.keys(parsedTx.tx_senders)[0] === Object.keys(parsedTx.tx_receivers)[0]) {
|
||||||
|
parsedTx.type = 'self';
|
||||||
|
parsedTx.amount = parsedTx.tx_receivers[addressOfTx];
|
||||||
|
parsedTx.address = addressOfTx;
|
||||||
|
} else if (addressOfTx in parsedTx.tx_senders && Object.keys(parsedTx.tx_receivers).some(addr => addr !== addressOfTx)) {
|
||||||
|
parsedTx.type = 'out';
|
||||||
|
parsedTx.receiver = Object.keys(parsedTx.tx_receivers).filter(addr => addr != addressOfTx);
|
||||||
|
parsedTx.amount = parsedTx.receiver.reduce((t, addr) => t + parsedTx.tx_receivers[addr], 0) + parsedTx.tx_fee;
|
||||||
|
} else {
|
||||||
|
parsedTx.type = 'in';
|
||||||
|
parsedTx.sender = Object.keys(parsedTx.tx_senders).filter(addr => addr != addressOfTx);
|
||||||
|
parsedTx.amount = parsedTx.tx_receivers[addressOfTx];
|
||||||
|
}
|
||||||
|
return parsedTx;
|
||||||
|
}
|
||||||
|
|
||||||
//This library uses API provided by chain.so (https://chain.so/)
|
|
||||||
const URL = "https://blockchain.info/";
|
|
||||||
|
|
||||||
const DUST_AMT = 546,
|
const DUST_AMT = 546,
|
||||||
MIN_FEE_UPDATE = 219;
|
MIN_FEE_UPDATE = 219;
|
||||||
|
|
||||||
const fetch_api = btcOperator.fetch = function (api, json_res = true) {
|
const fetch_api = btcOperator.fetch = function (api, { asText = false, url = 'https://blockchain.info/' } = {}) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
console.debug(URL + api);
|
console.debug(url + api);
|
||||||
fetch(URL + api).then(response => {
|
fetch(url + api).then(response => {
|
||||||
if (response.ok) {
|
if (response.ok) {
|
||||||
(json_res ? response.json() : response.text())
|
(asText ? response.text() : response.json())
|
||||||
.then(result => resolve(result))
|
.then(result => resolve(result))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
} else {
|
} else {
|
||||||
@ -25,13 +443,6 @@
|
|||||||
})
|
})
|
||||||
};
|
};
|
||||||
|
|
||||||
const SATOSHI_IN_BTC = 1e8;
|
|
||||||
|
|
||||||
const util = btcOperator.util = {};
|
|
||||||
|
|
||||||
util.Sat_to_BTC = value => parseFloat((value / SATOSHI_IN_BTC).toFixed(8));
|
|
||||||
util.BTC_to_Sat = value => parseInt(value * SATOSHI_IN_BTC);
|
|
||||||
|
|
||||||
function get_fee_rate() {
|
function get_fee_rate() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
fetch('https://api.blockchain.info/mempool/fees').then(response => {
|
fetch('https://api.blockchain.info/mempool/fees').then(response => {
|
||||||
@ -47,6 +458,13 @@
|
|||||||
|
|
||||||
const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => {
|
const broadcastTx = btcOperator.broadcastTx = rawTxHex => new Promise((resolve, reject) => {
|
||||||
console.log('txHex:', rawTxHex)
|
console.log('txHex:', rawTxHex)
|
||||||
|
// return multiApi('broadcast', { rawTxHex })
|
||||||
|
// .then(result => {
|
||||||
|
// resolve(result)
|
||||||
|
// })
|
||||||
|
// .catch(error => {
|
||||||
|
// reject(error)
|
||||||
|
// })
|
||||||
let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction';
|
let url = 'https://coinb.in/api/?uid=1&key=12345678901234567890123456789012&setmodule=bitcoin&request=sendrawtransaction';
|
||||||
fetch(url, {
|
fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -55,6 +473,7 @@
|
|||||||
},
|
},
|
||||||
body: "rawtx=" + rawTxHex
|
body: "rawtx=" + rawTxHex
|
||||||
}).then(response => {
|
}).then(response => {
|
||||||
|
console.log(response)
|
||||||
response.text().then(resultText => {
|
response.text().then(resultText => {
|
||||||
let r = resultText.match(/<result>.*<\/result>/);
|
let r = resultText.match(/<result>.*<\/result>/);
|
||||||
if (!r)
|
if (!r)
|
||||||
@ -273,10 +692,11 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
//BTC blockchain APIs
|
//BTC blockchain APIs
|
||||||
|
|
||||||
btcOperator.getBalance = addr => new Promise((resolve, reject) => {
|
btcOperator.getBalance = addr => new Promise((resolve, reject) => {
|
||||||
fetch_api(`q/addressbalance/${addr}`)
|
if (!validateAddress(addr))
|
||||||
.then(result => resolve(util.Sat_to_BTC(result)))
|
return reject("Invalid address");
|
||||||
|
multiApi('balance', { addr })
|
||||||
|
.then(result => resolve(result))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -400,7 +820,11 @@
|
|||||||
}
|
}
|
||||||
btcOperator.validateTxParameters = validateTxParameters;
|
btcOperator.validateTxParameters = validateTxParameters;
|
||||||
|
|
||||||
function createTransaction(senders, redeemScripts, receivers, amounts, fee, change_address, fee_from_receiver, allowUnconfirmedUtxos = false) {
|
const createTransaction = btcOperator.createTransaction = ({
|
||||||
|
senders, redeemScripts, receivers, amounts, fee, change_address,
|
||||||
|
fee_from_receiver, allowUnconfirmedUtxos = false, sendingTx = false,
|
||||||
|
hasInsufficientBalance = false
|
||||||
|
}) => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
|
let total_amount = parseFloat(amounts.reduce((t, a) => t + a, 0).toFixed(8));
|
||||||
const tx = coinjs.transaction();
|
const tx = coinjs.transaction();
|
||||||
@ -433,11 +857,16 @@
|
|||||||
result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0);
|
result.output_amount = total_amount - (fee_from_receiver ? result.fee : 0);
|
||||||
result.total_size = BASE_TX_SIZE + output_size + result.input_size;
|
result.total_size = BASE_TX_SIZE + output_size + result.input_size;
|
||||||
result.transaction = tx;
|
result.transaction = tx;
|
||||||
resolve(result);
|
if (sendingTx && result.hasOwnProperty('hasInsufficientBalance') && result.hasInsufficientBalance)
|
||||||
|
reject({
|
||||||
|
message: "Insufficient balance",
|
||||||
|
...result
|
||||||
|
});
|
||||||
|
else
|
||||||
|
resolve(result);
|
||||||
}).catch(error => reject(error))
|
}).catch(error => reject(error))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
btcOperator.createTransaction = createTransaction;
|
|
||||||
|
|
||||||
function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver, allowUnconfirmedUtxos = false) {
|
function addInputs(tx, senders, redeemScripts, total_amount, fee, output_size, fee_from_receiver, allowUnconfirmedUtxos = false) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
@ -479,14 +908,18 @@
|
|||||||
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
|
change_amount: required_amount * -1 //required_amount will be -ve of change_amount
|
||||||
});
|
});
|
||||||
else if (rec_args.n >= senders.length) {
|
else if (rec_args.n >= senders.length) {
|
||||||
return reject("Insufficient Balance");
|
return resolve({
|
||||||
|
hasInsufficientBalance: true,
|
||||||
|
input_size: rec_args.input_size,
|
||||||
|
input_amount: rec_args.input_amount,
|
||||||
|
change_amount: required_amount * -1
|
||||||
|
});
|
||||||
}
|
}
|
||||||
let addr = senders[rec_args.n],
|
let addr = senders[rec_args.n],
|
||||||
rs = redeemScripts[rec_args.n];
|
rs = redeemScripts[rec_args.n];
|
||||||
let addr_type = coinjs.addressDecode(addr).type;
|
let addr_type = coinjs.addressDecode(addr).type;
|
||||||
let size_per_input = _sizePerInput(addr, rs);
|
let size_per_input = _sizePerInput(addr, rs);
|
||||||
fetch_api(`unspent?active=${addr}`).then(result => {
|
multiApi('unspent', { addr, allowUnconfirmedUtxos: rec_args.allowUnconfirmedUtxos }).then(utxos => {
|
||||||
let utxos = result.unspent_outputs;
|
|
||||||
//console.debug("add-utxo", addr, rs, required_amount, utxos);
|
//console.debug("add-utxo", addr, rs, required_amount, utxos);
|
||||||
for (let i = 0; i < utxos.length && required_amount > 0; i++) {
|
for (let i = 0; i < utxos.length && required_amount > 0; i++) {
|
||||||
if (utxos.length === 1 && rec_args.allowUnconfirmedUtxos) {
|
if (utxos.length === 1 && rec_args.allowUnconfirmedUtxos) {
|
||||||
@ -534,43 +967,6 @@
|
|||||||
}
|
}
|
||||||
btcOperator.addOutputs = addOutputs;
|
btcOperator.addOutputs = addOutputs;
|
||||||
|
|
||||||
/*
|
|
||||||
function autoFeeCalc(tx) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
get_fee_rate().then(fee_rate => {
|
|
||||||
let tx_size = tx.size();
|
|
||||||
for (var i = 0; i < this.ins.length; i++)
|
|
||||||
switch (tx.extractScriptKey(i).type) {
|
|
||||||
case 'scriptpubkey':
|
|
||||||
tx_size += SIGN_SIZE;
|
|
||||||
break;
|
|
||||||
case 'segwit':
|
|
||||||
case 'multisig':
|
|
||||||
tx_size += SIGN_SIZE * 0.25;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
console.warn('Unknown script-type');
|
|
||||||
tx_size += SIGN_SIZE;
|
|
||||||
}
|
|
||||||
resolve(tx_size * fee_rate);
|
|
||||||
}).catch(error => reject(error))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
function editFee(tx, current_fee, target_fee, index = -1) {
|
|
||||||
//values are in satoshi
|
|
||||||
index = parseInt(index >= 0 ? index : tx.outs.length - index);
|
|
||||||
if (index < 0 || index >= tx.outs.length)
|
|
||||||
throw "Invalid index";
|
|
||||||
let edit_value = parseInt(current_fee - target_fee), //rip of any decimal places
|
|
||||||
current_value = tx.outs[index].value; //could be BigInterger
|
|
||||||
if (edit_value < 0 && edit_value > current_value)
|
|
||||||
throw "Insufficient value at vout";
|
|
||||||
tx.outs[index].value = current_value instanceof BigInteger ?
|
|
||||||
current_value.add(new BigInteger('' + edit_value)) : parseInt(current_value + edit_value);
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
function tx_fetch_for_editing(tx) {
|
function tx_fetch_for_editing(tx) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid
|
if (typeof tx == 'string' && /^[0-9a-f]{64}$/i.test(tx)) { //tx is txid
|
||||||
@ -793,6 +1189,7 @@
|
|||||||
|
|
||||||
|
|
||||||
btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) {
|
btcOperator.sendTx = function (senders, privkeys, receivers, amounts, fee = null, options = {}) {
|
||||||
|
options.sendingTx = true;
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => {
|
createSignedTx(senders, privkeys, receivers, amounts, fee, options).then(result => {
|
||||||
broadcastTx(result.transaction.serialize())
|
broadcastTx(result.transaction.serialize())
|
||||||
@ -816,7 +1213,7 @@
|
|||||||
receivers,
|
receivers,
|
||||||
amounts,
|
amounts,
|
||||||
fee,
|
fee,
|
||||||
change_address: options.change_address
|
...options
|
||||||
}));
|
}));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return reject(e)
|
return reject(e)
|
||||||
@ -831,7 +1228,11 @@
|
|||||||
if (redeemScripts.includes(null)) //TODO: segwit
|
if (redeemScripts.includes(null)) //TODO: segwit
|
||||||
return reject("Unable to get redeem-script");
|
return reject("Unable to get redeem-script");
|
||||||
//create transaction
|
//create transaction
|
||||||
createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver).then(result => {
|
createTransaction({
|
||||||
|
senders, redeemScripts, receivers, amounts, fee,
|
||||||
|
change_address: options.change_address || senders[0],
|
||||||
|
...options
|
||||||
|
}).then(result => {
|
||||||
let tx = result.transaction;
|
let tx = result.transaction;
|
||||||
console.debug("Unsigned:", tx.serialize());
|
console.debug("Unsigned:", tx.serialize());
|
||||||
new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
|
new Set(wif_keys).forEach(key => tx.sign(key, 1 /*sighashtype*/)); //Sign the tx using private key WIF
|
||||||
@ -864,7 +1265,11 @@
|
|||||||
if (redeemScripts.includes(null)) //TODO: segwit
|
if (redeemScripts.includes(null)) //TODO: segwit
|
||||||
return reject("Unable to get redeem-script");
|
return reject("Unable to get redeem-script");
|
||||||
//create transaction
|
//create transaction
|
||||||
createTransaction(senders, redeemScripts, receivers, amounts, fee, options.change_address || senders[0], options.fee_from_receiver, options.allowUnconfirmedUtxos).then(result => {
|
createTransaction({
|
||||||
|
senders, redeemScripts, receivers, amounts, fee,
|
||||||
|
change_address: options.change_address || senders[0],
|
||||||
|
...options
|
||||||
|
}).then(result => {
|
||||||
result.tx_hex = result.transaction.serialize();
|
result.tx_hex = result.transaction.serialize();
|
||||||
delete result.transaction;
|
delete result.transaction;
|
||||||
resolve(result);
|
resolve(result);
|
||||||
@ -900,7 +1305,12 @@
|
|||||||
return reject(e)
|
return reject(e)
|
||||||
}
|
}
|
||||||
//create transaction
|
//create transaction
|
||||||
createTransaction([sender], [redeemScript], receivers, amounts, fee, options.change_address || sender, options.fee_from_receiver).then(result => {
|
createTransaction({
|
||||||
|
senders: [sender], redeemScripts: [redeemScript],
|
||||||
|
receivers, amounts, fee,
|
||||||
|
change_address: options.change_address || sender,
|
||||||
|
...options
|
||||||
|
}).then(result => {
|
||||||
result.tx_hex = result.transaction.serialize();
|
result.tx_hex = result.transaction.serialize();
|
||||||
delete result.transaction;
|
delete result.transaction;
|
||||||
resolve(result);
|
resolve(result);
|
||||||
@ -975,7 +1385,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
|
const getTxOutput = (txid, i) => new Promise((resolve, reject) => {
|
||||||
fetch_api(`rawtx/${txid}`)
|
multiApi('tx', { txid })
|
||||||
.then(result => resolve(result.out[i]))
|
.then(result => resolve(result.out[i]))
|
||||||
.catch(error => reject(error))
|
.catch(error => reject(error))
|
||||||
});
|
});
|
||||||
@ -1031,94 +1441,41 @@
|
|||||||
return Crypto.util.bytesToHex(txid);
|
return Crypto.util.bytesToHex(txid);
|
||||||
}
|
}
|
||||||
|
|
||||||
const getLatestBlock = btcOperator.getLatestBlock = () => new Promise((resolve, reject) => {
|
const getTx = btcOperator.getTx = txid => new Promise(async (resolve, reject) => {
|
||||||
fetch_api(`q/getblockcount`)
|
try {
|
||||||
.then(result => resolve(result))
|
const result = await multiApi('tx', { txid });
|
||||||
.catch(error => reject(error))
|
resolve({
|
||||||
})
|
confirmations: result.confirmations,
|
||||||
|
|
||||||
const getTx = btcOperator.getTx = txid => new Promise((resolve, reject) => {
|
|
||||||
fetch_api(`rawtx/${txid}`).then(result => {
|
|
||||||
console.debug("Tx:", result);
|
|
||||||
getLatestBlock().then(latest_block => resolve({
|
|
||||||
block: result.block_height,
|
block: result.block_height,
|
||||||
txid: result.hash,
|
txid: result.hash,
|
||||||
time: result.time * 1000,
|
time: result.time,
|
||||||
confirmations: result.block_height === null ? 0 : latest_block - result.block_height, //calculate confirmations using latest block number as api doesnt relay it
|
|
||||||
size: result.size,
|
size: result.size,
|
||||||
fee: util.Sat_to_BTC(result.fee),
|
fee: util.Sat_to_BTC(result.fee),
|
||||||
inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })),
|
inputs: result.inputs.map(i => Object({ address: i.prev_out.addr, value: util.Sat_to_BTC(i.prev_out.value) })),
|
||||||
total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)),
|
total_input_value: util.Sat_to_BTC(result.inputs.reduce((a, i) => a + i.prev_out.value, 0)),
|
||||||
outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })),
|
outputs: result.out.map(o => Object({ address: o.addr, value: util.Sat_to_BTC(o.value) })),
|
||||||
total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)),
|
total_output_value: util.Sat_to_BTC(result.out.reduce((a, o) => a += o.value, 0)),
|
||||||
}))
|
})
|
||||||
}).catch(error => reject(error))
|
} catch (error) {
|
||||||
});
|
reject(error)
|
||||||
|
}
|
||||||
getTx.hex = btcOperator.getTx.hex = txid => new Promise((resolve, reject) => {
|
|
||||||
fetch_api(`rawtx/${txid}?format=hex`, false)
|
|
||||||
.then(result => resolve(result))
|
|
||||||
.catch(error => reject(error))
|
|
||||||
})
|
})
|
||||||
|
|
||||||
|
getTx.hex = btcOperator.getTx.hex = txid => multiApi('txHex', { txid });
|
||||||
|
|
||||||
btcOperator.getAddressData = address => new Promise((resolve, reject) => {
|
btcOperator.getAddressData = address => new Promise((resolve, reject) => {
|
||||||
fetch_api(`rawaddr/${address}`).then(data => {
|
if (!validateAddress(address))
|
||||||
let details = {};
|
return reject("Invalid address");
|
||||||
details.balance = util.Sat_to_BTC(data.final_balance);
|
Promise.all([
|
||||||
details.address = data.address;
|
multiApi('balance', { addr: address }),
|
||||||
details.txs = data.txs.map(tx => {
|
multiApi('txs', { addr: address })
|
||||||
let d = {
|
]).then(([balance, txs]) => {
|
||||||
txid: tx.hash,
|
const parsedTxs = txs.map(tx => parseTx(tx, address));
|
||||||
time: tx.time * 1000, //s to ms
|
resolve({
|
||||||
block: tx.block_height,
|
address,
|
||||||
}
|
balance,
|
||||||
//sender list
|
txs: parsedTxs
|
||||||
d.tx_senders = {};
|
});
|
||||||
tx.inputs.forEach(i => {
|
|
||||||
if (i.prev_out.addr in d.tx_senders)
|
|
||||||
d.tx_senders[i.prev_out.addr] += i.prev_out.value;
|
|
||||||
else d.tx_senders[i.prev_out.addr] = i.prev_out.value;
|
|
||||||
});
|
|
||||||
d.tx_input_value = 0;
|
|
||||||
for (let s in d.tx_senders) {
|
|
||||||
let val = d.tx_senders[s];
|
|
||||||
d.tx_senders[s] = util.Sat_to_BTC(val);
|
|
||||||
d.tx_input_value += val;
|
|
||||||
}
|
|
||||||
d.tx_input_value = util.Sat_to_BTC(d.tx_input_value);
|
|
||||||
//receiver list
|
|
||||||
d.tx_receivers = {};
|
|
||||||
tx.out.forEach(o => {
|
|
||||||
if (o.addr in d.tx_receivers)
|
|
||||||
d.tx_receivers[o.addr] += o.value;
|
|
||||||
else d.tx_receivers[o.addr] = o.value;
|
|
||||||
});
|
|
||||||
d.tx_output_value = 0;
|
|
||||||
for (let r in d.tx_receivers) {
|
|
||||||
let val = d.tx_receivers[r];
|
|
||||||
d.tx_receivers[r] = util.Sat_to_BTC(val);
|
|
||||||
d.tx_output_value += val;
|
|
||||||
}
|
|
||||||
d.tx_output_value = util.Sat_to_BTC(d.tx_output_value);
|
|
||||||
d.tx_fee = util.Sat_to_BTC(tx.fee);
|
|
||||||
//tx type
|
|
||||||
if (tx.result > 0) { //net > 0, balance inc => type=in
|
|
||||||
d.type = "in";
|
|
||||||
d.amount = util.Sat_to_BTC(tx.result);
|
|
||||||
d.sender = Object.keys(d.tx_senders).filter(s => s !== address);
|
|
||||||
} else if (Object.keys(d.tx_receivers).some(r => r !== address)) { //net < 0, balance dec & receiver present => type=out
|
|
||||||
d.type = "out";
|
|
||||||
d.amount = util.Sat_to_BTC(tx.result * -1);
|
|
||||||
d.receiver = Object.keys(d.tx_receivers).filter(r => r !== address);
|
|
||||||
d.fee = d.tx_fee;
|
|
||||||
} else { //net < 0 (fee) & no other id in receiver list => type=self
|
|
||||||
d.type = "self";
|
|
||||||
d.amount = d.tx_receivers[address];
|
|
||||||
d.address = address
|
|
||||||
}
|
|
||||||
return d;
|
|
||||||
})
|
|
||||||
resolve(details);
|
|
||||||
}).catch(error => reject(error))
|
}).catch(error => reject(error))
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
2
scripts/btcOperator.min.js
vendored
2
scripts/btcOperator.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
2
scripts/components.min.js
vendored
2
scripts/components.min.js
vendored
File diff suppressed because one or more lines are too long
@ -2,45 +2,45 @@
|
|||||||
/* FLO Ethereum Operators */
|
/* FLO Ethereum Operators */
|
||||||
/* Make sure you added Taproot, Keccak, FLO and BTC Libraries before */
|
/* Make sure you added Taproot, Keccak, FLO and BTC Libraries before */
|
||||||
'use strict';
|
'use strict';
|
||||||
const floEthereum = EXPORTS;
|
const floEthereum = EXPORTS;
|
||||||
|
|
||||||
const ethPrivateKeyFromWif = floEthereum.ethPrivateKeyFromWif = function(privateKey,){
|
const ethPrivateKeyFromWif = floEthereum.ethPrivateKeyFromWif = function (privateKey,) {
|
||||||
return coinjs.wif2privkey(privateKey).privkey;
|
return coinjs.wif2privkey(privateKey).privkey;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ethAddressFromPrivateKey = floEthereum.ethAddressFromPrivateKey = function(privateKey, onlyEvenY = false){
|
|
||||||
var t1,t1_x,t1_y,t1_y_BigInt,t2,t3,t4;
|
|
||||||
var groupOrder = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");
|
|
||||||
|
|
||||||
t1 = bitjs.newPubkey(privateKey);
|
const ethAddressFromPrivateKey = floEthereum.ethAddressFromPrivateKey = function (privateKey, onlyEvenY = false) {
|
||||||
t1_x = t1.slice(2, 66); t1_y = t1.slice(-64);
|
var t1, t1_x, t1_y, t1_y_BigInt, t2, t3, t4;
|
||||||
if (onlyEvenY) {
|
var groupOrder = BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");
|
||||||
t1_y_BigInt = BigInt("0x"+t1_y);
|
|
||||||
if (t1_y_BigInt % 2n !== 0n) { t1_y_BigInt = (groupOrder-t1_y_BigInt)%groupOrder; t1_y=t1_y_BigInt.toString(16)}
|
|
||||||
};
|
|
||||||
|
|
||||||
t2 = t1_x.toString(16) + t1_y.toString(16);
|
|
||||||
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
|
||||||
t4 = keccak.extractLast20Bytes(t3);
|
|
||||||
return "0x" + t4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ethAddressFromCompressedPublicKey = floEthereum.ethAddressFromCompressedPublicKey = function(compressedPublicKey){
|
t1 = bitjs.newPubkey(privateKey);
|
||||||
var t1,t2,t3,t4;
|
t1_x = t1.slice(2, 66); t1_y = t1.slice(-64);
|
||||||
t1 = coinjs.compressedToUncompressed(compressedPublicKey);
|
if (onlyEvenY) {
|
||||||
t2 = t1.slice(2);
|
t1_y_BigInt = BigInt("0x" + t1_y);
|
||||||
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
if (t1_y_BigInt % 2n !== 0n) { t1_y_BigInt = (groupOrder - t1_y_BigInt) % groupOrder; t1_y = t1_y_BigInt.toString(16) }
|
||||||
t4 = keccak.extractLast20Bytes(t3);
|
};
|
||||||
return "0x" + t4;
|
|
||||||
}
|
|
||||||
|
|
||||||
const ethAddressFromUncompressedPublicKey = floEthereum.ethAddressFromUncompressedPublicKey = function(unCompressedPublicKey){
|
t2 = t1_x.toString(16) + t1_y.toString(16);
|
||||||
var t1,t2,t3,t4;
|
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
||||||
t1 = unCompressedPublicKey;
|
t4 = keccak.extractLast20Bytes(t3);
|
||||||
t2 = t1.slice(2);
|
return "0x" + t4;
|
||||||
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
}
|
||||||
t4 = keccak.extractLast20Bytes(t3);
|
|
||||||
return "0x" + t4;
|
const ethAddressFromCompressedPublicKey = floEthereum.ethAddressFromCompressedPublicKey = function (compressedPublicKey) {
|
||||||
}
|
var t1, t2, t3, t4;
|
||||||
|
t1 = coinjs.compressedToUncompressed(compressedPublicKey);
|
||||||
|
t2 = t1.slice(2);
|
||||||
|
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
||||||
|
t4 = keccak.extractLast20Bytes(t3);
|
||||||
|
return "0x" + t4;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ethAddressFromUncompressedPublicKey = floEthereum.ethAddressFromUncompressedPublicKey = function (unCompressedPublicKey) {
|
||||||
|
var t1, t2, t3, t4;
|
||||||
|
t1 = unCompressedPublicKey;
|
||||||
|
t2 = t1.slice(2);
|
||||||
|
t3 = keccak.keccak_256(Crypto.util.hexToBytes(t2));
|
||||||
|
t4 = keccak.extractLast20Bytes(t3);
|
||||||
|
return "0x" + t4;
|
||||||
|
}
|
||||||
|
|
||||||
})('object' === typeof module ? module.exports : window.floEthereum = {});
|
})('object' === typeof module ? module.exports : window.floEthereum = {});
|
||||||
|
|||||||
1
scripts/floEthereum.min.js
vendored
Normal file
1
scripts/floEthereum.min.js
vendored
Normal file
@ -0,0 +1 @@
|
|||||||
|
!function(EXPORTS){"use strict";const floEthereum="object"===typeof module?module.exports:window.floEthereum={};floEthereum.ethPrivateKeyFromWif=function(privateKey){return coinjs.wif2privkey(privateKey).privkey},floEthereum.ethAddressFromPrivateKey=function(privateKey,onlyEvenY=!1){var t1,t1_x,t1_y,t1_y_BigInt,t2,t3,groupOrder=BigInt("0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFC2F");return t1_x=(t1=bitjs.newPubkey(privateKey)).slice(2,66),t1_y=t1.slice(-64),onlyEvenY&&(t1_y_BigInt=BigInt("0x"+t1_y))%2n!==0n&&(t1_y=(t1_y_BigInt=(groupOrder-t1_y_BigInt)%groupOrder).toString(16)),t2=t1_x.toString(16)+t1_y.toString(16),t3=keccak.keccak_256(Crypto.util.hexToBytes(t2)),"0x"+keccak.extractLast20Bytes(t3)},floEthereum.ethAddressFromCompressedPublicKey=function(compressedPublicKey){var t2,t3;return t2=coinjs.compressedToUncompressed(compressedPublicKey).slice(2),t3=keccak.keccak_256(Crypto.util.hexToBytes(t2)),"0x"+keccak.extractLast20Bytes(t3)},floEthereum.ethAddressFromUncompressedPublicKey=function(unCompressedPublicKey){var t2,t3;return t2=unCompressedPublicKey.slice(2),t3=keccak.keccak_256(Crypto.util.hexToBytes(t2)),"0x"+keccak.extractLast20Bytes(t3)}}();
|
||||||
Loading…
Reference in New Issue
Block a user