Liquid support for unblinding transactions. (#588)

* Add docker file to generate wallycore wasm js lib.

* Add unblinded liquid transactions.

* Add background to unblided transactions.

* Check liquid network to try to unblind tx.

Ww don't want to try to unblind transactions in other networks.

Co-authored-by: Felipe Knorr Kuhn <100320+knorrium@users.noreply.github.com>

* Delete libwally-core dockerfile.

* Delete wallycore.html.

* Fix validation unblind tx.
Fix lint.
Add errorUnblinded.
Add vin.prevout unblinded tx.

* Add e2e testing to liquids unblinded tx.

* Load libwally.js dynamically.

* Fix table size.

* Add Blockstream License to libwally and wallycore.

Co-authored-by: Felipe Knorr Kuhn <100320+knorrium@users.noreply.github.com>
This commit is contained in:
Miguel Medeiros
2021-07-06 13:56:32 -03:00
committed by GitHub
parent 3ae3df6722
commit 9dae7020c8
10 changed files with 1408 additions and 184 deletions

View File

@@ -0,0 +1,184 @@
/*
The MIT License (MIT)
Copyright 2021 Blockstream Corp
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
const WALLY_OK = 0,
ASSET_COMMITMENT_LEN = 33,
ASSET_GENERATOR_LEN = 33,
ASSET_TAG_LEN = 32,
BLINDING_FACTOR_LEN = 32;
const WASM_URL = `./resources/wallycore/wallycore.js`;
let load_promise, Module;
export function load() {
return (
load_promise ||
(load_promise = new Promise((resolve, reject) => {
const script = document.createElement("script");
script.src = WASM_URL;
script.addEventListener("error", reject);
script.addEventListener("load", () =>
InitWally().then((module) => {
Module = module;
resolve();
}, reject)
);
document.body.appendChild(script);
}))
);
}
// Simple wrapper to execute both asset_generator_from_bytes and asset_value_commitment,
// with hex conversions
export function generate_commitments(
value,
asset_hex,
value_blinder_hex,
asset_blinder_hex
) {
const asset = parseHex(asset_hex, ASSET_TAG_LEN),
value_blinder = parseHex(value_blinder_hex, BLINDING_FACTOR_LEN),
asset_blinder = parseHex(asset_blinder_hex, BLINDING_FACTOR_LEN);
const asset_commitment = asset_generator_from_bytes(asset, asset_blinder),
value_commitment = asset_value_commitment(
value,
value_blinder,
asset_commitment
);
return {
asset_commitment: encodeHex(asset_commitment),
value_commitment: encodeHex(value_commitment),
};
}
export function asset_generator_from_bytes(asset, asset_blinder) {
const asset_commitment_ptr = Module._malloc(ASSET_GENERATOR_LEN);
checkCode(
Module.ccall(
"wally_asset_generator_from_bytes",
"number",
["array", "number", "array", "number", "number", "number"],
[
asset,
asset.length,
asset_blinder,
asset_blinder.length,
asset_commitment_ptr,
ASSET_GENERATOR_LEN,
]
)
);
const asset_commitment = readBytes(asset_commitment_ptr, ASSET_GENERATOR_LEN);
Module._free(asset_commitment_ptr);
return asset_commitment;
}
export function asset_value_commitment(value, value_blinder, asset_commitment) {
// Emscripten transforms int64 function arguments into two int32 arguments, see:
// https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-pass-int64-t-and-uint64-t-values-from-js-into-wasm-functions
const [value_lo, value_hi] = split_int52_lo_hi(value);
const value_commitment_ptr = Module._malloc(ASSET_COMMITMENT_LEN);
checkCode(
Module.ccall(
"wally_asset_value_commitment",
"number",
[
"number",
"number",
"array",
"number",
"array",
"number",
"number",
"number",
],
[
value_lo,
value_hi,
value_blinder,
value_blinder.length,
asset_commitment,
asset_commitment.length,
value_commitment_ptr,
ASSET_COMMITMENT_LEN,
]
)
);
const value_commitment = readBytes(
value_commitment_ptr,
ASSET_COMMITMENT_LEN
);
Module._free(value_commitment_ptr);
return value_commitment;
}
function checkCode(code) {
if (code != WALLY_OK) throw new Error(`libwally failed with code ${code}`);
}
function readBytes(ptr, size) {
const bytes = new Uint8Array(size);
for (let i = 0; i < size; i++) bytes[i] = Module.getValue(ptr + i, "i8");
return bytes;
}
// Split a 52-bit JavaScript number into two 32-bits numbers for the low and high bits
// https://stackoverflow.com/a/19274574
function split_int52_lo_hi(i) {
let lo = i | 0;
if (lo < 0) lo += 4294967296;
let hi = i - lo;
hi /= 4294967296;
if (hi < 0 || hi >= 1048576) throw new Error("not an int52: " + i);
return [lo, hi];
}
function encodeHex(bytes) {
// return Buffer.from(bytes).toString("hex");
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
// Parse hex string encoded in *reverse*
function parseHex(str, expected_size) {
if (!/^([0-9a-f]{2})+$/.test(str))
throw new Error("Invalid blinders (invalid hex)");
if (str.length != expected_size * 2)
throw new Error("Invalid blinders (invalid length)");
return new Uint8Array(
str
.match(/.{2}/g)
.map((hex_byte) => parseInt(hex_byte, 16))
.reverse()
);
}

View File

@@ -9,33 +9,33 @@
</a>
</div>
<div>
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<div>
<div class="title">
<h1 i18n="shared.transaction">Transaction</h1>
</div>
<div class="tx-link">
<a [routerLink]="['/tx/' | relativeUrl, txId]">
<span class="d-inline d-lg-none">{{ txId | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ txId }}</span>
</a>
<app-clipboard [text]="txId"></app-clipboard>
</div>
<div class="tx-link">
<a [routerLink]="['/tx/' | relativeUrl, txId]">
<span class="d-inline d-lg-none">{{ txId | shortenString : 24 }}</span>
<span class="d-none d-lg-inline">{{ txId }}</span>
</a>
<app-clipboard [text]="txId"></app-clipboard>
</div>
<div class="container-buttons">
<ng-template [ngIf]="tx?.status?.confirmed">
<button *ngIf="latestBlock" type="button" class="btn btn-sm btn-success">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status.confirmed">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
<div class="container-buttons">
<ng-template [ngIf]="tx?.status?.confirmed">
<button *ngIf="latestBlock" type="button" class="btn btn-sm btn-success">
<ng-container *ngTemplateOutlet="latestBlock.height - tx.status.block_height + 1 == 1 ? confirmationSingular : confirmationPlural; context: {$implicit: latestBlock.height - tx.status.block_height + 1}"></ng-container>
<ng-template #confirmationSingular let-i i18n="shared.confirmation-count.singular|Transaction singular confirmation count">{{ i }} confirmation</ng-template>
<ng-template #confirmationPlural let-i i18n="shared.confirmation-count.plural|Transaction plural confirmation count">{{ i }} confirmations</ng-template>
</button>
</ng-template>
<ng-template [ngIf]="tx && !tx?.status.confirmed">
<button type="button" class="btn btn-sm btn-danger" i18n="transaction.unconfirmed|Transaction unconfirmed state">Unconfirmed</button>
</ng-template>
</div>
</div>
</div>
</div>
<div class="clearfix"></div>
<ng-template [ngIf]="!isLoadingTx && !error">
@@ -198,7 +198,7 @@
<div class="clearfix"></div>
<app-transactions-list #txList [transactions]="[tx]" [transactionPage]="true"></app-transactions-list>
<app-transactions-list #txList [transactions]="[tx]" [errorUnblinded]="errorUnblinded" [transactionPage]="true"></app-transactions-list>
<h2 class="text-left" i18n="transaction.details">Details</h2>
<div class="box">

View File

@@ -1,7 +1,13 @@
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ElectrsApiService } from '../../services/electrs-api.service';
import { ActivatedRoute, ParamMap } from '@angular/router';
import { switchMap, filter, catchError, retryWhen, delay } from 'rxjs/operators';
import {
switchMap,
filter,
catchError,
retryWhen,
delay,
} from 'rxjs/operators';
import { Transaction, Block } from '../../interfaces/electrs.interface';
import { of, merge, Subscription, Observable, Subject } from 'rxjs';
import { StateService } from '../../services/state.service';
@@ -14,7 +20,7 @@ import { CpfpInfo } from 'src/app/interfaces/node-api.interface';
@Component({
selector: 'app-transaction',
templateUrl: './transaction.component.html',
styleUrls: ['./transaction.component.scss']
styleUrls: ['./transaction.component.scss'],
})
export class TransactionComponent implements OnInit, OnDestroy {
network = '';
@@ -23,6 +29,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
txInBlockIndex: number;
isLoadingTx = true;
error: any = undefined;
errorUnblinded: any = undefined;
waitingForTransaction = false;
latestBlock: Block;
transactionTime = -1;
@@ -32,6 +39,7 @@ export class TransactionComponent implements OnInit, OnDestroy {
cpfpInfo: CpfpInfo | null;
showCpfpDetails = false;
fetchCpfp$ = new Subject<string>();
commitments: Map<any, any>;
constructor(
private route: ActivatedRoute,
@@ -40,28 +48,36 @@ export class TransactionComponent implements OnInit, OnDestroy {
private websocketService: WebsocketService,
private audioService: AudioService,
private apiService: ApiService,
private seoService: SeoService,
) { }
private seoService: SeoService
) {}
ngOnInit() {
this.websocketService.want(['blocks', 'mempool-blocks']);
this.stateService.networkChanged$.subscribe((network) => this.network = network);
this.stateService.networkChanged$.subscribe(
(network) => (this.network = network)
);
this.fetchCpfpSubscription = this.fetchCpfp$
.pipe(
switchMap((txId) => this.apiService.getCpfpinfo$(txId)
.pipe(
retryWhen((errors) => errors.pipe(delay(2000)))
)
),
switchMap((txId) =>
this.apiService
.getCpfpinfo$(txId)
.pipe(retryWhen((errors) => errors.pipe(delay(2000))))
)
)
.subscribe((cpfpInfo) => {
if (!this.tx) {
return;
}
const lowerFeeParents = cpfpInfo.ancestors.filter((parent) => (parent.fee / (parent.weight / 4)) < this.tx.feePerVsize);
let totalWeight = this.tx.weight + lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees = this.tx.fee + lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
const lowerFeeParents = cpfpInfo.ancestors.filter(
(parent) => parent.fee / (parent.weight / 4) < this.tx.feePerVsize
);
let totalWeight =
this.tx.weight +
lowerFeeParents.reduce((prev, val) => prev + val.weight, 0);
let totalFees =
this.tx.fee +
lowerFeeParents.reduce((prev, val) => prev + val.fee, 0);
if (cpfpInfo.bestDescendant) {
totalWeight += cpfpInfo.bestDescendant.weight;
@@ -69,98 +85,116 @@ export class TransactionComponent implements OnInit, OnDestroy {
}
this.tx.effectiveFeePerVsize = totalFees / (totalWeight / 4);
this.stateService.markBlock$.next({ txFeePerVSize: this.tx.effectiveFeePerVsize });
this.stateService.markBlock$.next({
txFeePerVSize: this.tx.effectiveFeePerVsize,
});
this.cpfpInfo = cpfpInfo;
});
this.subscription = this.route.paramMap.pipe(
switchMap((params: ParamMap) => {
this.txId = params.get('id') || '';
this.seoService.setTitle($localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`);
this.resetTransaction();
return merge(
of(true),
this.stateService.connectionState$.pipe(
filter((state) => state === 2 && this.tx && !this.tx.status.confirmed)
),
);
}),
switchMap(() => {
let transactionObservable$: Observable<Transaction>;
if (history.state.data) {
transactionObservable$ = of(history.state.data);
} else {
transactionObservable$ = this.electrsApiService.getTransaction$(this.txId).pipe(
catchError(this.handleLoadElectrsTransactionError.bind(this))
this.subscription = this.route.paramMap
.pipe(
switchMap(async (params: ParamMap) => {
this.txId = params.get('id') || '';
await this.checkUnblindedTx();
this.seoService.setTitle(
$localize`:@@bisq.transaction.browser-title:Transaction: ${this.txId}:INTERPOLATION:`
);
}
return merge(
transactionObservable$,
this.stateService.mempoolTransactions$
);
})
)
.subscribe((tx: Transaction) => {
if (!tx) {
return;
}
this.tx = tx;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.setMempoolBlocksSubscription();
this.resetTransaction();
return merge(
of(true),
this.stateService.connectionState$.pipe(
filter(
(state) => state === 2 && this.tx && !this.tx.status.confirmed
)
)
);
}),
switchMap(() => {
let transactionObservable$: Observable<Transaction>;
if (history.state.data) {
transactionObservable$ = of(history.state.data);
} else {
transactionObservable$ = this.electrsApiService
.getTransaction$(this.txId)
.pipe(
catchError(this.handleLoadElectrsTransactionError.bind(this))
);
}
return merge(
transactionObservable$,
this.stateService.mempoolTransactions$
);
})
)
.subscribe(
async (tx: Transaction) => {
if (!tx) {
return;
}
this.tx = tx;
if (tx.fee === undefined) {
this.tx.fee = 0;
}
this.tx.feePerVsize = tx.fee / (tx.weight / 4);
this.isLoadingTx = false;
this.error = undefined;
this.waitingForTransaction = false;
this.setMempoolBlocksSubscription();
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
if (!tx.status.confirmed) {
this.websocketService.startTrackTransaction(tx.txid);
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
}
}
if (tx.firstSeen) {
this.transactionTime = tx.firstSeen;
} else {
this.getTransactionTime();
}
}
if (this.tx.status.confirmed) {
this.stateService.markBlock$.next({ blockHeight: tx.status.block_height });
} else {
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({ txFeePerVSize: tx.effectiveFeePerVsize });
this.cpfpInfo = {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
} else {
this.fetchCpfp$.next(this.tx.txid);
if (this.tx.status.confirmed) {
this.stateService.markBlock$.next({
blockHeight: tx.status.block_height,
});
} else {
if (tx.cpfpChecked) {
this.stateService.markBlock$.next({
txFeePerVSize: tx.effectiveFeePerVsize,
});
this.cpfpInfo = {
ancestors: tx.ancestors,
bestDescendant: tx.bestDescendant,
};
} else {
this.fetchCpfp$.next(this.tx.txid);
}
}
await this.checkUnblindedTx();
},
(error) => {
this.error = error;
this.isLoadingTx = false;
}
);
this.stateService.blocks$.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx) {
this.tx.status = {
confirmed: true,
block_height: block.height,
block_hash: block.id,
block_time: block.timestamp,
};
this.stateService.markBlock$.next({ blockHeight: block.height });
this.audioService.playSound('magic');
}
},
(error) => {
this.error = error;
this.isLoadingTx = false;
});
this.stateService.blocks$
.subscribe(([block, txConfirmed]) => {
this.latestBlock = block;
if (txConfirmed && this.tx) {
this.tx.status = {
confirmed: true,
block_height: block.height,
block_hash: block.id,
block_time: block.timestamp,
};
this.stateService.markBlock$.next({ blockHeight: block.height });
this.audioService.playSound('magic');
}
});
this.stateService.txReplaced$
.subscribe((rbfTransaction) => this.rbfTransaction = rbfTransaction);
this.stateService.txReplaced$.subscribe(
(rbfTransaction) => (this.rbfTransaction = rbfTransaction)
);
}
handleLoadElectrsTransactionError(error: any): Observable<any> {
@@ -174,26 +208,30 @@ export class TransactionComponent implements OnInit, OnDestroy {
}
setMempoolBlocksSubscription() {
this.stateService.mempoolBlocks$
.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
this.stateService.mempoolBlocks$.subscribe((mempoolBlocks) => {
if (!this.tx) {
return;
}
const txFeePerVSize = this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
const txFeePerVSize =
this.tx.effectiveFeePerVsize || this.tx.fee / (this.tx.weight / 4);
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (txFeePerVSize <= block.feeRange[i + 1] && txFeePerVSize >= block.feeRange[i]) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
}
for (const block of mempoolBlocks) {
for (let i = 0; i < block.feeRange.length - 1; i++) {
if (
txFeePerVSize <= block.feeRange[i + 1] &&
txFeePerVSize >= block.feeRange[i]
) {
this.txInBlockIndex = mempoolBlocks.indexOf(block);
}
}
});
}
});
}
getTransactionTime() {
this.apiService.getTransactionTimes$([this.tx.txid])
this.apiService
.getTransactionTimes$([this.tx.txid])
.subscribe((transactionTimes) => {
this.transactionTime = transactionTimes[0];
});
@@ -226,4 +264,145 @@ export class TransactionComponent implements OnInit, OnDestroy {
this.fetchCpfpSubscription.unsubscribe();
this.leaveTransaction();
}
// Parse the blinders data from a string encoded as a comma separated list, in the following format:
// <value_in_satoshis>,<asset_tag_hex>,<amount_blinder_hex>,<asset_blinder_hex>
// This can be repeated with a comma separator to specify blinders for multiple outputs.
parseBlinders(str: string) {
const parts = str.split(',');
const blinders = [];
while (parts.length) {
blinders.push({
value: this.verifyNum(parts.shift()),
asset: this.verifyHex32(parts.shift()),
value_blinder: this.verifyHex32(parts.shift()),
asset_blinder: this.verifyHex32(parts.shift()),
});
}
return blinders;
}
verifyNum(num: string) {
if (!+num) {
throw new Error('Invalid blinding data (invalid number)');
}
return +num;
}
verifyHex32(str: string) {
if (!str || !/^[0-9a-f]{64}$/i.test(str)) {
throw new Error('Invalid blinding data (invalid hex)');
}
return str;
}
async makeCommitmentMap(blinders: any) {
const libwally = await import('./libwally.js');
await libwally.load();
const commitments = new Map();
blinders.forEach(b => {
const { asset_commitment, value_commitment } =
libwally.generate_commitments(b.value, b.asset, b.value_blinder, b.asset_blinder);
commitments.set(`${asset_commitment}:${value_commitment}`, {
asset: b.asset,
value: b.value,
});
});
return commitments;
}
// Look for the given output, returning an { value, asset } object
find(vout: any) {
return vout.assetcommitment && vout.valuecommitment &&
this.commitments.get(`${vout.assetcommitment}:${vout.valuecommitment}`);
}
// Lookup all transaction inputs/outputs and attach the unblinded data
tryUnblindTx(tx: any) {
if (tx) {
if (tx._unblinded) { return tx._unblinded; }
let matched = 0;
if (tx.vout !== undefined) {
tx.vout.forEach(vout => matched += +this.tryUnblindOut(vout));
tx.vin.filter(vin => vin.prevout).forEach(vin => matched += +this.tryUnblindOut(vin.prevout));
}
if (this.commitments !== undefined) {
tx._unblinded = { matched, total: this.commitments.size };
this.deduceBlinded(tx);
if (matched < this.commitments.size) {
this.errorUnblinded = `Error: Invalid blinding data.`;
}
tx._deduced = false; // invalidate cache so deduction is attempted again
return tx._unblinded;
}
}
}
// Look the given output and attach the unblinded data
tryUnblindOut(vout: any) {
const unblinded = this.find(vout);
if (unblinded) { Object.assign(vout, unblinded); }
return !!unblinded;
}
// Attempt to deduce the blinded input/output based on the available information
deduceBlinded(tx: any) {
if (tx._deduced) { return; }
tx._deduced = true;
// Find ins/outs with unknown amounts (blinded ant not revealed via the `#blinded` hash fragment)
const unknownIns = tx.vin.filter(vin => vin.prevout && vin.prevout.value == null);
const unknownOuts = tx.vout.filter(vout => vout.value == null);
// If the transaction has a single unknown input/output, we can deduce its asset/amount
// based on the other known inputs/outputs.
if (unknownIns.length + unknownOuts.length === 1) {
// Keep a per-asset tally of all known input amounts, minus all known output amounts
const totals = new Map();
tx.vin.filter(vin => vin.prevout && vin.prevout.value != null)
.forEach(({ prevout }) =>
totals.set(prevout.asset, (totals.get(prevout.asset) || 0) + prevout.value));
tx.vout.filter(vout => vout.value != null)
.forEach(vout =>
totals.set(vout.asset, (totals.get(vout.asset) || 0) - vout.value));
// There should only be a single asset where the inputs and outputs amounts mismatch,
// which is the asset of the blinded input/output
const remainder = Array.from(totals.entries()).filter(([ asset, value ]) => value !== 0);
if (remainder.length !== 1) { throw new Error('unexpected remainder while deducing blinded tx'); }
const [ blindedAsset, blindedValue ] = remainder[0];
// A positive remainder (when known in > known out) is the asset/amount of the unknown blinded output,
// a negative one is the input.
if (blindedValue > 0) {
if (!unknownOuts.length) { throw new Error('expected unknown output'); }
unknownOuts[0].asset = blindedAsset;
unknownOuts[0].value = blindedValue;
} else {
if (!unknownIns.length) { throw new Error('expected unknown input'); }
unknownIns[0].prevout.asset = blindedAsset;
unknownIns[0].prevout.value = blindedValue * -1;
}
}
}
async checkUnblindedTx() {
try {
if (this.network === 'liquid') {
const windowLocationHash = window.location.hash.substring('#blinded='.length);
if (windowLocationHash.length > 0) {
const blinders = this.parseBlinders(windowLocationHash);
if (blinders) {
this.commitments = await this.makeCommitmentMap(blinders);
this.tryUnblindTx(this.tx);
}
}
}
} catch (error) {
this.errorUnblinded = error;
}
}
}

View File

@@ -12,13 +12,16 @@
</div>
<div class="clearfix"></div>
</div>
<div class="header-bg box" infiniteScroll [alwaysCallback]="true" [infiniteScrollDistance]="2" [infiniteScrollUpDistance]="1.5" [infiniteScrollThrottle]="50" (scrolled)="onScroll()">
<div *ngIf="errorUnblinded" class="error-unblinded">{{ errorUnblinded }}</div>
<div class="row">
<div class="col">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<table class="table table-borderless smaller-text table-sm" style="margin: 0;" id="table-tx-vin">
<tbody>
<ng-template ngFor let-vin [ngForOf]="tx['@vinLimit'] ? tx.vin.slice(0, 10) : tx.vin" [ngForTrackBy]="trackByIndexFn">
<tr>
<tr [ngClass]="assetsMinimal && assetsMinimal[vin.prevout.asset] && vin.prevout.scriptpubkey_address ? 'assetBox' : ''">
<td class="arrow-td">
<ng-template [ngIf]="vin.prevout === null && !vin.is_pegin" [ngIfElse]="hasPrevout">
<i class="arrow grey"></i>
@@ -66,7 +69,7 @@
</td>
</tr>
<tr *ngIf="displayDetails">
<td colspan="3">
<td colspan="3" class="details-container" >
<table class="table table-striped table-borderless details-table mb-3">
<tbody>
<ng-template [ngIf]="vin.scriptsig">
@@ -114,10 +117,10 @@
</div>
<div class="w-100 d-block d-md-none"></div>
<div class="col mobile-bottomcol">
<table class="table table-borderless smaller-text table-xs" style="margin: 0;">
<table class="table table-borderless smaller-text table-sm" style="margin: 0;" id="table-tx-vout">
<tbody>
<ng-template ngFor let-vout let-vindex="index" [ngForOf]="tx['@voutLimit'] ? tx.vout.slice(0, 10) : tx.vout" [ngForTrackBy]="trackByIndexFn">
<tr>
<tr [ngClass]="assetsMinimal && assetsMinimal[vout.asset] && vout.scriptpubkey_address ? 'assetBox' : ''">
<td>
<a *ngIf="vout.scriptpubkey_address; else scriptpubkey_type" [routerLink]="['/address/' | relativeUrl, vout.scriptpubkey_address]" title="{{ vout.scriptpubkey_address }}">
<span class="d-block d-lg-none">{{ vout.scriptpubkey_address | shortenString : 16 }}</span>
@@ -145,7 +148,7 @@
<ng-template [ngIf]="vout.asset && vout.asset !== nativeAssetId" [ngIfElse]="defaultOutput">
<div *ngIf="assetsMinimal && assetsMinimal[vout.asset]">
<ng-container *ngTemplateOutlet="assetBox; context:{ $implicit: vout }"></ng-container>
</div>
</div>
</ng-template>
<ng-template #defaultOutput>
<app-amount [satoshis]="vout.value"></app-amount>
@@ -162,7 +165,7 @@
</td>
</tr>
<tr *ngIf="displayDetails">
<td colspan="3">
<td colspan="3" class=" details-container" >
<table class="table table-striped table-borderless details-table mb-3">
<tbody>
<tr *ngIf="vout.scriptpubkey_type">
@@ -235,5 +238,4 @@
{{ assetsMinimal[item.asset][0] }}
<br />
<a [routerLink]="['/asset/' | relativeUrl, item.asset]">{{ item.asset | shortenString : 13 }}</a>
<br /><br />
</ng-template>

View File

@@ -4,9 +4,10 @@
.arrow {
display: inline-block!important;
position: relative;
position: absolute;
width: 14px;
height: 22px;
margin-top: 10px;
margin-left: -5px;
box-sizing: content-box
}
@@ -66,24 +67,6 @@
}
}
.details-table {
margin-top: 5px;
}
.details-table td {
padding: 0.75rem;
&:first-child {
white-space: pre-wrap;
}
}
.details-table td:nth-child(2) {
word-break: break-all;
white-space: normal;
font-family: "Courier New", Courier, monospace;
font-size: 12px;
}
.smaller-text {
font-size: 12px;
@media (min-width: 576px) {
@@ -137,4 +120,36 @@
padding: 10px;
margin-bottom: 10px;
margin-top: 10px;
}
.assetBox {
background-color: #653b9c90;
}
.details-container {
padding: 0px;
tr td {
padding: 0.75rem;
font-size: 12px;
&:first-child {
color: #ffffff66;
white-space: pre-wrap;
@media (min-width: 476px) {
white-space: nowrap;
}
}
&:nth-child(2) {
word-break: break-all;
white-space: normal;
font-family: "Courier New", Courier, monospace;
}
}
}
.error-unblinded {
display: block;
width: 100%;
color: #d43131;
text-align: right;
margin-top: 0px;
margin-bottom: 10px;
}

View File

@@ -21,6 +21,7 @@ export class TransactionsListComponent implements OnInit, OnChanges {
@Input() transactions: Transaction[];
@Input() showConfirmations = false;
@Input() transactionPage = false;
@Input() errorUnblinded = false;
@Output() loadMore = new EventEmitter();

View File

@@ -17,6 +17,8 @@ export interface Transaction {
bestDescendant?: BestDescendant | null;
cpfpChecked?: boolean;
deleteAfter?: number;
_unblinded?: any;
_deduced?: boolean;
}
interface Ancestor {