@hsuite/native-connect-ui 2.1.0 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/atoms/hs-transfer-item/hs-transfer-item.component.js +8 -2
- package/dist/lib/organisms/hs-transaction-detail/hs-transaction-detail.component.js +7 -12
- package/dist/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.d.ts +103 -2
- package/dist/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.js +33 -14
- package/dist/lib/utils/format-ledger-amount.d.ts +12 -3
- package/dist/lib/utils/format-ledger-amount.js +15 -4
- package/package.json +2 -2
- package/src/lib/atoms/hs-transfer-item/hs-transfer-item.component.ts +8 -2
- package/src/lib/organisms/hs-transaction-detail/hs-transaction-detail.component.ts +7 -11
- package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.html +6 -1
- package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.spec.ts +26 -0
- package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.ts +102 -15
- package/src/lib/utils/format-ledger-amount.spec.ts +35 -2
- package/src/lib/utils/format-ledger-amount.ts +30 -9
|
@@ -22,8 +22,10 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
22
22
|
import { Component, Input, computed } from '@angular/core';
|
|
23
23
|
import { CommonModule } from '@angular/common';
|
|
24
24
|
import { TranslocoModule } from '@ngneat/transloco';
|
|
25
|
+
import { LedgerIds } from '@hsuite/native-connect-types';
|
|
25
26
|
import { copyToClipboard } from '../../utils/clipboard';
|
|
26
27
|
import { getUILogger } from '../../utils/ui-logger';
|
|
28
|
+
import { formatLedgerAmount } from '../../utils/format-ledger-amount';
|
|
27
29
|
const logger = getUILogger('HsTransferItem');
|
|
28
30
|
import { IonicModule } from '@ionic/angular';
|
|
29
31
|
let HsTransferItemComponent = class HsTransferItemComponent {
|
|
@@ -78,9 +80,13 @@ let HsTransferItemComponent = class HsTransferItemComponent {
|
|
|
78
80
|
const numAmount = parseFloat(this.amount);
|
|
79
81
|
if (isNaN(numAmount))
|
|
80
82
|
return this.amount;
|
|
81
|
-
// For HBAR,
|
|
83
|
+
// For HBAR, render via the canonical formatter (display decimals + trailing-
|
|
84
|
+
// zero trim); the symbol is shown separately by typeLabel, so omit the suffix.
|
|
82
85
|
if (this.type === 'hbar') {
|
|
83
|
-
|
|
86
|
+
// HBAR is intrinsically Hedera's native unit, so the ledger is known at
|
|
87
|
+
// compile time here — expressed via the well-known-id constant rather
|
|
88
|
+
// than a bare 'hedera' literal (typo-safe, coherent with other sites).
|
|
89
|
+
return formatLedgerAmount(numAmount, LedgerIds.Hedera, { symbol: false });
|
|
84
90
|
}
|
|
85
91
|
// For tokens, use provided decimals
|
|
86
92
|
if (this.type === 'token' && this.decimals !== undefined) {
|
|
@@ -17,6 +17,7 @@ import { Component, Input, Output, EventEmitter, signal, ChangeDetectionStrategy
|
|
|
17
17
|
import { CommonModule } from '@angular/common';
|
|
18
18
|
import { IonIcon } from '@ionic/angular/standalone';
|
|
19
19
|
import { TranslocoModule } from '@ngneat/transloco';
|
|
20
|
+
import { formatLedgerAmount } from '../../utils/format-ledger-amount';
|
|
20
21
|
import { addIcons } from 'ionicons';
|
|
21
22
|
import { chevronDown, chevronUp, copy, openOutline, checkmark, arrowUp, arrowDown, swapHorizontal, arrowForward, linkOutline, addCircleOutline, flameOutline, personAddOutline, personOutline, personRemoveOutline, shieldCheckmarkOutline, shieldOutline, calendarOutline, createOutline, calendarClearOutline, chatbubblesOutline, chatbubbleOutline, documentOutline, settingsOutline, lockClosedOutline, lockOpenOutline, closeCircleOutline, gitBranchOutline, downloadOutline, documentTextOutline, cashOutline, peopleOutline, ticketOutline, imageOutline, pricetagOutline, checkmarkCircleOutline, layersOutline, helpCircleOutline, snowOutline, sunnyOutline, pauseCircleOutline, playCircleOutline, trashOutline, } from 'ionicons/icons';
|
|
22
23
|
import { HsBadgeComponent } from '../../atoms/hs-badge/hs-badge.component';
|
|
@@ -344,18 +345,12 @@ let HsTransactionDetailComponent = class HsTransactionDetailComponent {
|
|
|
344
345
|
return `${sign}${formatted}`;
|
|
345
346
|
}
|
|
346
347
|
formatNativeAmount(amount) {
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
//
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
}
|
|
353
|
-
else if (Math.abs(value) < 1) {
|
|
354
|
-
return `${value.toFixed(6)} ${this.currency}`;
|
|
355
|
-
}
|
|
356
|
-
else {
|
|
357
|
-
return `${value.toFixed(4)} ${this.currency}`;
|
|
358
|
-
}
|
|
348
|
+
// Canonical formatter (decimals via the bundled-chain display map,
|
|
349
|
+
// trailing-zero trim, NaN/Infinity safety) replaces the previous hardcoded
|
|
350
|
+
// `ledgerId === 'hedera' ? 1e8 : 1e6` divisor + bespoke precision buckets.
|
|
351
|
+
// Display path — formatLedgerAmount stays lenient (returns '—' on an
|
|
352
|
+
// unknown ledger) and never throws.
|
|
353
|
+
return formatLedgerAmount(amount, this.ledgerId, { symbol: this.currency });
|
|
359
354
|
}
|
|
360
355
|
formatTokenAmount(amount, decimals = 0) {
|
|
361
356
|
if (decimals === 0)
|
package/dist/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.d.ts
CHANGED
|
@@ -7,8 +7,94 @@
|
|
|
7
7
|
* This file is part of HSuite Native Connect. For commercial licensing,
|
|
8
8
|
* visit https://hsuite.finance/licensing
|
|
9
9
|
*/
|
|
10
|
-
import { TrackByFunction, AfterViewInit, OnDestroy } from '@angular/core';
|
|
10
|
+
import { TrackByFunction, AfterViewInit, OnDestroy, EventEmitter } from '@angular/core';
|
|
11
11
|
import { type RiskLevel, type RiskFactorInput } from '../../molecules/hs-risk-card/hs-risk-card.component';
|
|
12
|
+
/**
|
|
13
|
+
* @interface TransactionDetailData
|
|
14
|
+
* Canonical, read-only data shape for the transaction detail surface. It is the
|
|
15
|
+
* single source of truth shared by {@link HsTransactionDetailModalComponent}
|
|
16
|
+
* (which `implements` it, so the component is guaranteed to expose every field)
|
|
17
|
+
* and by host services/wrappers that drive it. Keeping one type prevents the
|
|
18
|
+
* field-drift that silently drops data when the component and its host disagree.
|
|
19
|
+
*/
|
|
20
|
+
export interface TransactionDetailData {
|
|
21
|
+
/** Transaction ID/hash */
|
|
22
|
+
transactionId: string;
|
|
23
|
+
/** Display name for the transaction type */
|
|
24
|
+
transactionName: string;
|
|
25
|
+
/** Transaction type for icon selection */
|
|
26
|
+
transactionType: string;
|
|
27
|
+
/** Custom icon override */
|
|
28
|
+
transactionIcon?: string;
|
|
29
|
+
/** Ledger identifier */
|
|
30
|
+
ledgerId: 'hedera' | 'xrpl';
|
|
31
|
+
/** Transaction status */
|
|
32
|
+
status?: string;
|
|
33
|
+
/** Transaction timestamp */
|
|
34
|
+
timestamp?: number;
|
|
35
|
+
/** Main amount in smallest units (tinybars/drops) */
|
|
36
|
+
mainAmount?: number;
|
|
37
|
+
/** Currency symbol */
|
|
38
|
+
currency?: string;
|
|
39
|
+
/** Native currency transfers */
|
|
40
|
+
nativeTransfers?: Array<{
|
|
41
|
+
account: string;
|
|
42
|
+
amount: number;
|
|
43
|
+
label?: string;
|
|
44
|
+
}>;
|
|
45
|
+
/** Token transfers */
|
|
46
|
+
tokenTransfers?: Array<{
|
|
47
|
+
token_id: string;
|
|
48
|
+
account: string;
|
|
49
|
+
amount: number;
|
|
50
|
+
decimals?: number;
|
|
51
|
+
}>;
|
|
52
|
+
/** NFT transfers */
|
|
53
|
+
nftTransfers?: Array<{
|
|
54
|
+
token_id: string;
|
|
55
|
+
sender_account_id: string;
|
|
56
|
+
receiver_account_id: string;
|
|
57
|
+
serial_number: number;
|
|
58
|
+
}>;
|
|
59
|
+
/** Transaction fee in smallest units */
|
|
60
|
+
transactionFee?: number;
|
|
61
|
+
/** Transaction memo */
|
|
62
|
+
memo?: string;
|
|
63
|
+
/** XRPL source account */
|
|
64
|
+
sourceAccount?: string;
|
|
65
|
+
/** XRPL destination account */
|
|
66
|
+
destinationAccount?: string;
|
|
67
|
+
/** XRPL memos */
|
|
68
|
+
xrplMemos?: Array<{
|
|
69
|
+
data?: string;
|
|
70
|
+
type?: string;
|
|
71
|
+
format?: string;
|
|
72
|
+
}>;
|
|
73
|
+
/** User's own account addresses for highlighting */
|
|
74
|
+
userAccounts?: string[];
|
|
75
|
+
/** Token metadata for symbol/name resolution */
|
|
76
|
+
tokenMetadataMap?: Map<string, {
|
|
77
|
+
name?: string;
|
|
78
|
+
symbol?: string;
|
|
79
|
+
decimals?: number;
|
|
80
|
+
}>;
|
|
81
|
+
/** Contact resolver function */
|
|
82
|
+
contactResolver?: (address: string, ledgerId: string) => string | null;
|
|
83
|
+
/**
|
|
84
|
+
* Host handler for the "View in Explorer" action. The component is
|
|
85
|
+
* presentational and delegates the click here; the host builds the
|
|
86
|
+
* network-aware URL and opens it. When omitted, the button is hidden.
|
|
87
|
+
*/
|
|
88
|
+
onExplorer?: (txId: string) => void;
|
|
89
|
+
/** Scam / suspicious-transaction risk level for this transaction. */
|
|
90
|
+
riskLevel?: RiskLevel;
|
|
91
|
+
/** Pre-translated risk factors shown in the banner (empty = none). */
|
|
92
|
+
riskFactors?: RiskFactorInput[];
|
|
93
|
+
/** Pre-translated recommendation line shown under the factors. */
|
|
94
|
+
riskRecommendation?: string | null;
|
|
95
|
+
/** Banner title (already localized, e.g. the high/medium badge label). */
|
|
96
|
+
riskTitle?: string;
|
|
97
|
+
}
|
|
12
98
|
/**
|
|
13
99
|
* @component HsTransactionDetailModalComponent
|
|
14
100
|
* Modal component for displaying comprehensive transaction details.
|
|
@@ -22,7 +108,7 @@ import { type RiskLevel, type RiskFactorInput } from '../../molecules/hs-risk-ca
|
|
|
22
108
|
* - Explorer link integration
|
|
23
109
|
* - Contact name resolution
|
|
24
110
|
*/
|
|
25
|
-
export declare class HsTransactionDetailModalComponent implements AfterViewInit, OnDestroy {
|
|
111
|
+
export declare class HsTransactionDetailModalComponent implements AfterViewInit, OnDestroy, TransactionDetailData {
|
|
26
112
|
/**
|
|
27
113
|
* Hedera system accounts that must NEVER appear in the Native Transfers
|
|
28
114
|
* section of the UI. Union of node-fee accounts (0.0.3..0.0.34) and the
|
|
@@ -82,11 +168,26 @@ export declare class HsTransactionDetailModalComponent implements AfterViewInit,
|
|
|
82
168
|
decimals?: number;
|
|
83
169
|
}>;
|
|
84
170
|
contactResolver?: (address: string, ledgerId: string) => string | null;
|
|
171
|
+
/**
|
|
172
|
+
* Host-provided handler for the "View in Explorer" action. The modal is a
|
|
173
|
+
* presentational component and must NOT know explorer domains or networks, so
|
|
174
|
+
* it delegates: the host builds the network-aware URL (mainnet vs testnet) and
|
|
175
|
+
* opens it via its SOC2-safe external-URL service. Called with the tx id/hash.
|
|
176
|
+
* When omitted, the "View in Explorer" button is hidden.
|
|
177
|
+
*/
|
|
178
|
+
onExplorer?: (txId: string) => void;
|
|
85
179
|
maxDisplayedTransfers: number;
|
|
86
180
|
riskLevel?: RiskLevel;
|
|
87
181
|
riskFactors: RiskFactorInput[];
|
|
88
182
|
riskRecommendation?: string | null;
|
|
89
183
|
riskTitle: string;
|
|
184
|
+
/**
|
|
185
|
+
* Emitted when the user requests to close this view. When a host binds this
|
|
186
|
+
* output (e.g. a drawer wrapper), it takes over closing and the component
|
|
187
|
+
* does NOT call ModalController — keeping the component container-agnostic.
|
|
188
|
+
* Unbound (the legacy modal path), `dismiss()` falls back to ModalController.
|
|
189
|
+
*/
|
|
190
|
+
closeRequested: EventEmitter<void>;
|
|
90
191
|
/**
|
|
91
192
|
* Whether to render the scam-risk banner. Only medium/high verdicts surface a
|
|
92
193
|
* banner; `'low'`/absent stays silent so benign transactions are visually
|
package/dist/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.js
CHANGED
|
@@ -14,7 +14,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
14
14
|
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
15
15
|
};
|
|
16
16
|
var HsTransactionDetailModalComponent_1;
|
|
17
|
-
import { Component, Input, signal, inject, ChangeDetectionStrategy, ElementRef, HostListener, } from '@angular/core';
|
|
17
|
+
import { Component, Input, signal, inject, ChangeDetectionStrategy, ElementRef, HostListener, Output, EventEmitter, } from '@angular/core';
|
|
18
18
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
19
19
|
import { IonContent, IonFooter, IonToolbar, IonButton, IonIcon, ModalController, } from '@ionic/angular/standalone';
|
|
20
20
|
import { addIcons } from 'ionicons';
|
|
@@ -82,6 +82,14 @@ let HsTransactionDetailModalComponent = class HsTransactionDetailModalComponent
|
|
|
82
82
|
tokenMetadataMap;
|
|
83
83
|
// Contact resolver
|
|
84
84
|
contactResolver;
|
|
85
|
+
/**
|
|
86
|
+
* Host-provided handler for the "View in Explorer" action. The modal is a
|
|
87
|
+
* presentational component and must NOT know explorer domains or networks, so
|
|
88
|
+
* it delegates: the host builds the network-aware URL (mainnet vs testnet) and
|
|
89
|
+
* opens it via its SOC2-safe external-URL service. Called with the tx id/hash.
|
|
90
|
+
* When omitted, the "View in Explorer" button is hidden.
|
|
91
|
+
*/
|
|
92
|
+
onExplorer;
|
|
85
93
|
maxDisplayedTransfers = 10;
|
|
86
94
|
// Scam / suspicious-transaction risk assessment.
|
|
87
95
|
// Surfaced as a banner at the top of the modal so a user who taps into a
|
|
@@ -93,6 +101,13 @@ let HsTransactionDetailModalComponent = class HsTransactionDetailModalComponent
|
|
|
93
101
|
riskFactors = [];
|
|
94
102
|
riskRecommendation;
|
|
95
103
|
riskTitle = 'Risk assessment';
|
|
104
|
+
/**
|
|
105
|
+
* Emitted when the user requests to close this view. When a host binds this
|
|
106
|
+
* output (e.g. a drawer wrapper), it takes over closing and the component
|
|
107
|
+
* does NOT call ModalController — keeping the component container-agnostic.
|
|
108
|
+
* Unbound (the legacy modal path), `dismiss()` falls back to ModalController.
|
|
109
|
+
*/
|
|
110
|
+
closeRequested = new EventEmitter();
|
|
96
111
|
/**
|
|
97
112
|
* Whether to render the scam-risk banner. Only medium/high verdicts surface a
|
|
98
113
|
* banner; `'low'`/absent stays silent so benign transactions are visually
|
|
@@ -513,22 +528,20 @@ let HsTransactionDetailModalComponent = class HsTransactionDetailModalComponent
|
|
|
513
528
|
}
|
|
514
529
|
}
|
|
515
530
|
async dismiss() {
|
|
531
|
+
// Container-agnostic close: if a host (e.g. drawer) listens, let it close;
|
|
532
|
+
// otherwise fall back to the Ionic modal path (legacy callers).
|
|
533
|
+
if (this.closeRequested.observed) {
|
|
534
|
+
this.closeRequested.emit();
|
|
535
|
+
return;
|
|
536
|
+
}
|
|
516
537
|
await this.modalController.dismiss(null, 'cancel');
|
|
517
538
|
}
|
|
518
539
|
onExplorerClick() {
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
}
|
|
525
|
-
else if (this.ledgerId === 'xrpl') {
|
|
526
|
-
explorerUrl = `https://testnet.xrpl.org/transactions/${txId}`;
|
|
527
|
-
}
|
|
528
|
-
if (explorerUrl) {
|
|
529
|
-
// §29.8 SOC2 CC6.1: noopener,noreferrer blocks reverse-tabnabbing.
|
|
530
|
-
window.open(explorerUrl, '_blank', 'noopener,noreferrer');
|
|
531
|
-
}
|
|
540
|
+
if (!this.transactionId)
|
|
541
|
+
return;
|
|
542
|
+
// Delegate to the host: it knows the network and opens the link through its
|
|
543
|
+
// SOC2-safe external-URL service. The modal never hardcodes a network.
|
|
544
|
+
this.onExplorer?.(this.transactionId);
|
|
532
545
|
}
|
|
533
546
|
};
|
|
534
547
|
__decorate([
|
|
@@ -591,6 +604,9 @@ __decorate([
|
|
|
591
604
|
__decorate([
|
|
592
605
|
Input()
|
|
593
606
|
], HsTransactionDetailModalComponent.prototype, "contactResolver", void 0);
|
|
607
|
+
__decorate([
|
|
608
|
+
Input()
|
|
609
|
+
], HsTransactionDetailModalComponent.prototype, "onExplorer", void 0);
|
|
594
610
|
__decorate([
|
|
595
611
|
Input()
|
|
596
612
|
], HsTransactionDetailModalComponent.prototype, "maxDisplayedTransfers", void 0);
|
|
@@ -606,6 +622,9 @@ __decorate([
|
|
|
606
622
|
__decorate([
|
|
607
623
|
Input()
|
|
608
624
|
], HsTransactionDetailModalComponent.prototype, "riskTitle", void 0);
|
|
625
|
+
__decorate([
|
|
626
|
+
Output()
|
|
627
|
+
], HsTransactionDetailModalComponent.prototype, "closeRequested", void 0);
|
|
609
628
|
__decorate([
|
|
610
629
|
HostListener('document:keydown.escape', ['$event'])
|
|
611
630
|
], HsTransactionDetailModalComponent.prototype, "onEscapeKey", null);
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
* visit https://hsuite.finance/licensing
|
|
9
9
|
*/
|
|
10
10
|
/**
|
|
11
|
-
* @file Canonical formatter for
|
|
11
|
+
* @file Canonical formatter for native-unit amounts across the wallet UI
|
|
12
|
+
* (HBAR, XRP, and any SDK-registered ledger when its decimals are supplied).
|
|
12
13
|
*
|
|
13
14
|
* Callers MUST pass raw base units — tinybars for Hedera, drops for XRPL.
|
|
14
15
|
* Handles `bigint` exactly (no precision loss above `Number.MAX_SAFE_INTEGER`)
|
|
15
16
|
* and returns a safe placeholder for `NaN` / `Infinity` so a malformed input
|
|
16
|
-
* cannot leak literal "NaN" text into the UI.
|
|
17
|
+
* cannot leak literal "NaN" text into the UI. Returns the `—` sentinel for an
|
|
18
|
+
* unknown ledger when no `decimals` override is supplied via options.
|
|
17
19
|
*/
|
|
18
20
|
export type LedgerId = 'hedera' | 'xrpl';
|
|
19
21
|
export interface FormatLedgerAmountOptions {
|
|
@@ -25,5 +27,12 @@ export interface FormatLedgerAmountOptions {
|
|
|
25
27
|
* xrpl).
|
|
26
28
|
*/
|
|
27
29
|
symbol?: string | false;
|
|
30
|
+
/**
|
|
31
|
+
* Authoritative decimal places for this ledger's native unit (e.g. from the
|
|
32
|
+
* SDK registry's `getNativeDecimals`). When provided it OVERRIDES the built-in
|
|
33
|
+
* display-default map, so any ledger — including ones not bundled here — formats
|
|
34
|
+
* correctly. Omit to fall back to the display-only default for known chains.
|
|
35
|
+
*/
|
|
36
|
+
decimals?: number;
|
|
28
37
|
}
|
|
29
|
-
export declare function formatLedgerAmount(rawBaseUnits: number | bigint, ledgerId: LedgerId, options?: FormatLedgerAmountOptions): string;
|
|
38
|
+
export declare function formatLedgerAmount(rawBaseUnits: number | bigint, ledgerId: LedgerId | (string & {}), options?: FormatLedgerAmountOptions): string;
|
|
@@ -7,6 +7,10 @@
|
|
|
7
7
|
* This file is part of HSuite Native Connect. For commercial licensing,
|
|
8
8
|
* visit https://hsuite.finance/licensing
|
|
9
9
|
*/
|
|
10
|
+
// Display-only last-resort decimals. The authoritative value is the adapter
|
|
11
|
+
// manifest's nativeDecimals (SDK registry); money-adjacent callers SHOULD pass
|
|
12
|
+
// it explicitly via options.decimals. This map only covers the bundled chains
|
|
13
|
+
// so the formatter still renders something sane when no override is supplied.
|
|
10
14
|
const DECIMALS = { hedera: 8, xrpl: 6 };
|
|
11
15
|
const DEFAULT_SYMBOL = { hedera: 'ℏ', xrpl: 'XRP' };
|
|
12
16
|
/**
|
|
@@ -21,11 +25,18 @@ function bigintToFixedString(abs, decimals) {
|
|
|
21
25
|
return `${whole}.${frac.toString().padStart(decimals, '0')}`;
|
|
22
26
|
}
|
|
23
27
|
export function formatLedgerAmount(rawBaseUnits, ledgerId, options = {}) {
|
|
24
|
-
|
|
25
|
-
|
|
28
|
+
// Authoritative override wins; otherwise fall back to the display-only default.
|
|
29
|
+
const decimals = options.decimals ?? DECIMALS[ledgerId];
|
|
30
|
+
// Unknown ledger with no explicit decimals — or an out-of-range override — cannot
|
|
31
|
+
// be scaled safely. Return the display "unavailable" sentinel rather than NaN or a
|
|
32
|
+
// thrown error: this is a display path (≠ money path), so it must stay lenient. The
|
|
33
|
+
// upper bound of 100 keeps `decimals` inside `Number.prototype.toFixed`'s legal
|
|
34
|
+
// range and prevents a pathological `10n ** BigInt(decimals)` from hanging the
|
|
35
|
+
// render — no real ledger exceeds ~18 native decimals.
|
|
36
|
+
if (decimals === undefined || !Number.isInteger(decimals) || decimals < 0 || decimals > 100) {
|
|
37
|
+
return '—';
|
|
26
38
|
}
|
|
27
|
-
const
|
|
28
|
-
const symbol = options.symbol === false ? '' : (options.symbol ?? DEFAULT_SYMBOL[ledgerId]);
|
|
39
|
+
const symbol = options.symbol === false ? '' : (options.symbol ?? DEFAULT_SYMBOL[ledgerId] ?? '');
|
|
29
40
|
const suffix = symbol ? ` ${symbol}` : '';
|
|
30
41
|
// Build the signed magnitude string. Bigint stays in bigint space so XRPL
|
|
31
42
|
// total-supply-sized amounts (>2^53 drops) format exactly.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hsuite/native-connect-ui",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.1",
|
|
4
4
|
"license": "PolyForm-Noncommercial-1.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"@angular/core": "^20.3.16",
|
|
28
28
|
"@angular/forms": "^20.3.16",
|
|
29
29
|
"@angular/router": "^20.3.16",
|
|
30
|
-
"@hsuite/native-connect-types": "^2.1.
|
|
30
|
+
"@hsuite/native-connect-types": "^2.1.1",
|
|
31
31
|
"@ionic/angular": "^8.0.0",
|
|
32
32
|
"@ngneat/transloco": "^6.0.0",
|
|
33
33
|
"animejs": "^3.2.2",
|
|
@@ -17,8 +17,10 @@
|
|
|
17
17
|
import { Component, Input, computed } from '@angular/core';
|
|
18
18
|
import { CommonModule } from '@angular/common';
|
|
19
19
|
import { TranslocoModule } from '@ngneat/transloco';
|
|
20
|
+
import { LedgerIds } from '@hsuite/native-connect-types';
|
|
20
21
|
import { copyToClipboard } from '../../utils/clipboard';
|
|
21
22
|
import { getUILogger } from '../../utils/ui-logger';
|
|
23
|
+
import { formatLedgerAmount } from '../../utils/format-ledger-amount';
|
|
22
24
|
|
|
23
25
|
const logger = getUILogger('HsTransferItem');
|
|
24
26
|
import { IonicModule } from '@ionic/angular';
|
|
@@ -93,9 +95,13 @@ export class HsTransferItemComponent {
|
|
|
93
95
|
const numAmount = parseFloat(this.amount);
|
|
94
96
|
if (isNaN(numAmount)) return this.amount;
|
|
95
97
|
|
|
96
|
-
// For HBAR,
|
|
98
|
+
// For HBAR, render via the canonical formatter (display decimals + trailing-
|
|
99
|
+
// zero trim); the symbol is shown separately by typeLabel, so omit the suffix.
|
|
97
100
|
if (this.type === 'hbar') {
|
|
98
|
-
|
|
101
|
+
// HBAR is intrinsically Hedera's native unit, so the ledger is known at
|
|
102
|
+
// compile time here — expressed via the well-known-id constant rather
|
|
103
|
+
// than a bare 'hedera' literal (typo-safe, coherent with other sites).
|
|
104
|
+
return formatLedgerAmount(numAmount, LedgerIds.Hedera, { symbol: false });
|
|
99
105
|
}
|
|
100
106
|
|
|
101
107
|
// For tokens, use provided decimals
|
|
@@ -23,6 +23,7 @@ import {
|
|
|
23
23
|
import { CommonModule } from '@angular/common';
|
|
24
24
|
import { IonIcon } from '@ionic/angular/standalone';
|
|
25
25
|
import { TranslocoModule } from '@ngneat/transloco';
|
|
26
|
+
import { formatLedgerAmount } from '../../utils/format-ledger-amount';
|
|
26
27
|
import { addIcons } from 'ionicons';
|
|
27
28
|
import {
|
|
28
29
|
chevronDown,
|
|
@@ -433,17 +434,12 @@ export class HsTransactionDetailComponent implements OnInit, OnChanges {
|
|
|
433
434
|
}
|
|
434
435
|
|
|
435
436
|
formatNativeAmount(amount: number): string {
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
//
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
} else if (Math.abs(value) < 1) {
|
|
443
|
-
return `${value.toFixed(6)} ${this.currency}`;
|
|
444
|
-
} else {
|
|
445
|
-
return `${value.toFixed(4)} ${this.currency}`;
|
|
446
|
-
}
|
|
437
|
+
// Canonical formatter (decimals via the bundled-chain display map,
|
|
438
|
+
// trailing-zero trim, NaN/Infinity safety) replaces the previous hardcoded
|
|
439
|
+
// `ledgerId === 'hedera' ? 1e8 : 1e6` divisor + bespoke precision buckets.
|
|
440
|
+
// Display path — formatLedgerAmount stays lenient (returns '—' on an
|
|
441
|
+
// unknown ledger) and never throws.
|
|
442
|
+
return formatLedgerAmount(amount, this.ledgerId, { symbol: this.currency });
|
|
447
443
|
}
|
|
448
444
|
|
|
449
445
|
formatTokenAmount(amount: number, decimals: number = 0): string {
|
package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.html
CHANGED
|
@@ -267,7 +267,12 @@
|
|
|
267
267
|
<!-- Footer with Explorer Action -->
|
|
268
268
|
<ion-footer class="tx-modal-footer">
|
|
269
269
|
<ion-toolbar>
|
|
270
|
-
<ion-button
|
|
270
|
+
<ion-button
|
|
271
|
+
expand="block"
|
|
272
|
+
fill="outline"
|
|
273
|
+
(click)="onExplorerClick()"
|
|
274
|
+
*ngIf="transactionId && onExplorer"
|
|
275
|
+
>
|
|
271
276
|
<ion-icon slot="start" name="open-outline"></ion-icon>
|
|
272
277
|
View in Explorer
|
|
273
278
|
</ion-button>
|
package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.spec.ts
CHANGED
|
@@ -174,4 +174,30 @@ describe('HsTransactionDetailModalComponent', () => {
|
|
|
174
174
|
expect(component.showRiskBanner).toBe(false);
|
|
175
175
|
});
|
|
176
176
|
});
|
|
177
|
+
|
|
178
|
+
describe('onExplorerClick', () => {
|
|
179
|
+
it('delegates to the onExplorer callback with the transaction id (no hardcoded network)', () => {
|
|
180
|
+
const onExplorer = vi.fn();
|
|
181
|
+
const openSpy = vi.spyOn(window, 'open').mockReturnValue(null);
|
|
182
|
+
component.transactionId = '0.0.55@1700000000.000000000';
|
|
183
|
+
component.ledgerId = 'hedera';
|
|
184
|
+
component.onExplorer = onExplorer;
|
|
185
|
+
|
|
186
|
+
component.onExplorerClick();
|
|
187
|
+
|
|
188
|
+
// The modal must delegate to the host (which builds the network-aware
|
|
189
|
+
// URL), not open a hardcoded-testnet link itself.
|
|
190
|
+
expect(onExplorer).toHaveBeenCalledWith('0.0.55@1700000000.000000000');
|
|
191
|
+
expect(openSpy).not.toHaveBeenCalled();
|
|
192
|
+
openSpy.mockRestore();
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('does nothing when no onExplorer handler is provided', () => {
|
|
196
|
+
component.transactionId = '0.0.55@1700000000.000000000';
|
|
197
|
+
component.ledgerId = 'hedera';
|
|
198
|
+
component.onExplorer = undefined;
|
|
199
|
+
|
|
200
|
+
expect(() => component.onExplorerClick()).not.toThrow();
|
|
201
|
+
});
|
|
202
|
+
});
|
|
177
203
|
});
|
package/src/lib/organisms/hs-transaction-detail-modal/hs-transaction-detail-modal.component.ts
CHANGED
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
OnDestroy,
|
|
20
20
|
ElementRef,
|
|
21
21
|
HostListener,
|
|
22
|
+
Output,
|
|
23
|
+
EventEmitter,
|
|
22
24
|
} from '@angular/core';
|
|
23
25
|
import { CommonModule, DOCUMENT } from '@angular/common';
|
|
24
26
|
import {
|
|
@@ -87,6 +89,76 @@ import {
|
|
|
87
89
|
} from '../../molecules/hs-risk-card/hs-risk-card.component';
|
|
88
90
|
import { formatLedgerAmount, type LedgerId } from '../../utils/format-ledger-amount';
|
|
89
91
|
|
|
92
|
+
/**
|
|
93
|
+
* @interface TransactionDetailData
|
|
94
|
+
* Canonical, read-only data shape for the transaction detail surface. It is the
|
|
95
|
+
* single source of truth shared by {@link HsTransactionDetailModalComponent}
|
|
96
|
+
* (which `implements` it, so the component is guaranteed to expose every field)
|
|
97
|
+
* and by host services/wrappers that drive it. Keeping one type prevents the
|
|
98
|
+
* field-drift that silently drops data when the component and its host disagree.
|
|
99
|
+
*/
|
|
100
|
+
export interface TransactionDetailData {
|
|
101
|
+
/** Transaction ID/hash */
|
|
102
|
+
transactionId: string;
|
|
103
|
+
/** Display name for the transaction type */
|
|
104
|
+
transactionName: string;
|
|
105
|
+
/** Transaction type for icon selection */
|
|
106
|
+
transactionType: string;
|
|
107
|
+
/** Custom icon override */
|
|
108
|
+
transactionIcon?: string;
|
|
109
|
+
/** Ledger identifier */
|
|
110
|
+
ledgerId: 'hedera' | 'xrpl';
|
|
111
|
+
/** Transaction status */
|
|
112
|
+
status?: string;
|
|
113
|
+
/** Transaction timestamp */
|
|
114
|
+
timestamp?: number;
|
|
115
|
+
/** Main amount in smallest units (tinybars/drops) */
|
|
116
|
+
mainAmount?: number;
|
|
117
|
+
/** Currency symbol */
|
|
118
|
+
currency?: string;
|
|
119
|
+
/** Native currency transfers */
|
|
120
|
+
nativeTransfers?: Array<{ account: string; amount: number; label?: string }>;
|
|
121
|
+
/** Token transfers */
|
|
122
|
+
tokenTransfers?: Array<{ token_id: string; account: string; amount: number; decimals?: number }>;
|
|
123
|
+
/** NFT transfers */
|
|
124
|
+
nftTransfers?: Array<{
|
|
125
|
+
token_id: string;
|
|
126
|
+
sender_account_id: string;
|
|
127
|
+
receiver_account_id: string;
|
|
128
|
+
serial_number: number;
|
|
129
|
+
}>;
|
|
130
|
+
/** Transaction fee in smallest units */
|
|
131
|
+
transactionFee?: number;
|
|
132
|
+
/** Transaction memo */
|
|
133
|
+
memo?: string;
|
|
134
|
+
/** XRPL source account */
|
|
135
|
+
sourceAccount?: string;
|
|
136
|
+
/** XRPL destination account */
|
|
137
|
+
destinationAccount?: string;
|
|
138
|
+
/** XRPL memos */
|
|
139
|
+
xrplMemos?: Array<{ data?: string; type?: string; format?: string }>;
|
|
140
|
+
/** User's own account addresses for highlighting */
|
|
141
|
+
userAccounts?: string[];
|
|
142
|
+
/** Token metadata for symbol/name resolution */
|
|
143
|
+
tokenMetadataMap?: Map<string, { name?: string; symbol?: string; decimals?: number }>;
|
|
144
|
+
/** Contact resolver function */
|
|
145
|
+
contactResolver?: (address: string, ledgerId: string) => string | null;
|
|
146
|
+
/**
|
|
147
|
+
* Host handler for the "View in Explorer" action. The component is
|
|
148
|
+
* presentational and delegates the click here; the host builds the
|
|
149
|
+
* network-aware URL and opens it. When omitted, the button is hidden.
|
|
150
|
+
*/
|
|
151
|
+
onExplorer?: (txId: string) => void;
|
|
152
|
+
/** Scam / suspicious-transaction risk level for this transaction. */
|
|
153
|
+
riskLevel?: RiskLevel;
|
|
154
|
+
/** Pre-translated risk factors shown in the banner (empty = none). */
|
|
155
|
+
riskFactors?: RiskFactorInput[];
|
|
156
|
+
/** Pre-translated recommendation line shown under the factors. */
|
|
157
|
+
riskRecommendation?: string | null;
|
|
158
|
+
/** Banner title (already localized, e.g. the high/medium badge label). */
|
|
159
|
+
riskTitle?: string;
|
|
160
|
+
}
|
|
161
|
+
|
|
90
162
|
/**
|
|
91
163
|
* @component HsTransactionDetailModalComponent
|
|
92
164
|
* Modal component for displaying comprehensive transaction details.
|
|
@@ -119,7 +191,9 @@ import { formatLedgerAmount, type LedgerId } from '../../utils/format-ledger-amo
|
|
|
119
191
|
styleUrls: ['./hs-transaction-detail-modal.component.scss'],
|
|
120
192
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
121
193
|
})
|
|
122
|
-
export class HsTransactionDetailModalComponent
|
|
194
|
+
export class HsTransactionDetailModalComponent
|
|
195
|
+
implements AfterViewInit, OnDestroy, TransactionDetailData
|
|
196
|
+
{
|
|
123
197
|
/**
|
|
124
198
|
* Hedera system accounts that must NEVER appear in the Native Transfers
|
|
125
199
|
* section of the UI. Union of node-fee accounts (0.0.3..0.0.34) and the
|
|
@@ -180,6 +254,15 @@ export class HsTransactionDetailModalComponent implements AfterViewInit, OnDestr
|
|
|
180
254
|
// Contact resolver
|
|
181
255
|
@Input() contactResolver?: (address: string, ledgerId: string) => string | null;
|
|
182
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Host-provided handler for the "View in Explorer" action. The modal is a
|
|
259
|
+
* presentational component and must NOT know explorer domains or networks, so
|
|
260
|
+
* it delegates: the host builds the network-aware URL (mainnet vs testnet) and
|
|
261
|
+
* opens it via its SOC2-safe external-URL service. Called with the tx id/hash.
|
|
262
|
+
* When omitted, the "View in Explorer" button is hidden.
|
|
263
|
+
*/
|
|
264
|
+
@Input() onExplorer?: (txId: string) => void;
|
|
265
|
+
|
|
183
266
|
@Input() maxDisplayedTransfers = 10;
|
|
184
267
|
|
|
185
268
|
// Scam / suspicious-transaction risk assessment.
|
|
@@ -193,6 +276,14 @@ export class HsTransactionDetailModalComponent implements AfterViewInit, OnDestr
|
|
|
193
276
|
@Input() riskRecommendation?: string | null;
|
|
194
277
|
@Input() riskTitle = 'Risk assessment';
|
|
195
278
|
|
|
279
|
+
/**
|
|
280
|
+
* Emitted when the user requests to close this view. When a host binds this
|
|
281
|
+
* output (e.g. a drawer wrapper), it takes over closing and the component
|
|
282
|
+
* does NOT call ModalController — keeping the component container-agnostic.
|
|
283
|
+
* Unbound (the legacy modal path), `dismiss()` falls back to ModalController.
|
|
284
|
+
*/
|
|
285
|
+
@Output() closeRequested = new EventEmitter<void>();
|
|
286
|
+
|
|
196
287
|
/**
|
|
197
288
|
* Whether to render the scam-risk banner. Only medium/high verdicts surface a
|
|
198
289
|
* banner; `'low'`/absent stays silent so benign transactions are visually
|
|
@@ -652,23 +743,19 @@ export class HsTransactionDetailModalComponent implements AfterViewInit, OnDestr
|
|
|
652
743
|
}
|
|
653
744
|
|
|
654
745
|
async dismiss(): Promise<void> {
|
|
746
|
+
// Container-agnostic close: if a host (e.g. drawer) listens, let it close;
|
|
747
|
+
// otherwise fall back to the Ionic modal path (legacy callers).
|
|
748
|
+
if (this.closeRequested.observed) {
|
|
749
|
+
this.closeRequested.emit();
|
|
750
|
+
return;
|
|
751
|
+
}
|
|
655
752
|
await this.modalController.dismiss(null, 'cancel');
|
|
656
753
|
}
|
|
657
754
|
|
|
658
755
|
onExplorerClick(): void {
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
// Default to testnet, could be enhanced with networkId input
|
|
664
|
-
explorerUrl = `https://hashscan.io/testnet/transaction/${txId}`;
|
|
665
|
-
} else if (this.ledgerId === 'xrpl') {
|
|
666
|
-
explorerUrl = `https://testnet.xrpl.org/transactions/${txId}`;
|
|
667
|
-
}
|
|
668
|
-
|
|
669
|
-
if (explorerUrl) {
|
|
670
|
-
// §29.8 SOC2 CC6.1: noopener,noreferrer blocks reverse-tabnabbing.
|
|
671
|
-
window.open(explorerUrl, '_blank', 'noopener,noreferrer');
|
|
672
|
-
}
|
|
756
|
+
if (!this.transactionId) return;
|
|
757
|
+
// Delegate to the host: it knows the network and opens the link through its
|
|
758
|
+
// SOC2-safe external-URL service. The modal never hardcodes a network.
|
|
759
|
+
this.onExplorer?.(this.transactionId);
|
|
673
760
|
}
|
|
674
761
|
}
|
|
@@ -86,8 +86,41 @@ describe('formatLedgerAmount', () => {
|
|
|
86
86
|
expect(formatLedgerAmount(NaN, 'hedera', { symbol: false })).toBe('?');
|
|
87
87
|
});
|
|
88
88
|
|
|
89
|
-
it('unknown ledgerId
|
|
90
|
-
|
|
89
|
+
it('unknown ledgerId without decimals returns the — sentinel (no throw, no NaN)', () => {
|
|
90
|
+
const out = formatLedgerAmount(1, 'hbar' as LedgerId);
|
|
91
|
+
expect(out).toBe('—');
|
|
92
|
+
expect(out).not.toMatch(/NaN/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('explicit decimals override formats an unknown ledger correctly', () => {
|
|
96
|
+
expect(
|
|
97
|
+
formatLedgerAmount(1_500_000_000n, 'solana' as LedgerId, { decimals: 9, symbol: 'SOL' }),
|
|
98
|
+
).toBe('1.5 SOL');
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it('explicit decimals override beats the built-in default for a known ledger', () => {
|
|
102
|
+
expect(formatLedgerAmount(1000n, 'hedera', { decimals: 3, symbol: false })).toBe('1');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('invalid explicit decimals (negative) returns — sentinel without throwing', () => {
|
|
106
|
+
expect(formatLedgerAmount(100n, 'hedera', { decimals: -1 })).toBe('—');
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('invalid explicit decimals (non-integer) returns — sentinel without throwing', () => {
|
|
110
|
+
expect(formatLedgerAmount(100n, 'hedera', { decimals: 1.5 })).toBe('—');
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('decimals:0 formats whole units (valid — must NOT hit the sentinel)', () => {
|
|
114
|
+
expect(formatLedgerAmount(1500n, 'solana' as LedgerId, { decimals: 0, symbol: 'TOKEN' })).toBe(
|
|
115
|
+
'1500 TOKEN',
|
|
116
|
+
);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('out-of-range decimals (>100) returns — sentinel without throwing or hanging', () => {
|
|
120
|
+
// .toFixed(>100) throws RangeError and 10n ** BigInt(1e9) would hang the render;
|
|
121
|
+
// the upper-bound guard must turn both into the lenient sentinel instead.
|
|
122
|
+
expect(formatLedgerAmount(100, 'hedera', { decimals: 1e9 })).toBe('—');
|
|
123
|
+
expect(formatLedgerAmount(100n, 'hedera', { decimals: 1e9 })).toBe('—');
|
|
91
124
|
});
|
|
92
125
|
|
|
93
126
|
it('xrpl with custom symbol override and signed', () => {
|
|
@@ -9,12 +9,14 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
/**
|
|
12
|
-
* @file Canonical formatter for
|
|
12
|
+
* @file Canonical formatter for native-unit amounts across the wallet UI
|
|
13
|
+
* (HBAR, XRP, and any SDK-registered ledger when its decimals are supplied).
|
|
13
14
|
*
|
|
14
15
|
* Callers MUST pass raw base units — tinybars for Hedera, drops for XRPL.
|
|
15
16
|
* Handles `bigint` exactly (no precision loss above `Number.MAX_SAFE_INTEGER`)
|
|
16
17
|
* and returns a safe placeholder for `NaN` / `Infinity` so a malformed input
|
|
17
|
-
* cannot leak literal "NaN" text into the UI.
|
|
18
|
+
* cannot leak literal "NaN" text into the UI. Returns the `—` sentinel for an
|
|
19
|
+
* unknown ledger when no `decimals` override is supplied via options.
|
|
18
20
|
*/
|
|
19
21
|
|
|
20
22
|
export type LedgerId = 'hedera' | 'xrpl';
|
|
@@ -28,10 +30,21 @@ export interface FormatLedgerAmountOptions {
|
|
|
28
30
|
* xrpl).
|
|
29
31
|
*/
|
|
30
32
|
symbol?: string | false;
|
|
33
|
+
/**
|
|
34
|
+
* Authoritative decimal places for this ledger's native unit (e.g. from the
|
|
35
|
+
* SDK registry's `getNativeDecimals`). When provided it OVERRIDES the built-in
|
|
36
|
+
* display-default map, so any ledger — including ones not bundled here — formats
|
|
37
|
+
* correctly. Omit to fall back to the display-only default for known chains.
|
|
38
|
+
*/
|
|
39
|
+
decimals?: number;
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
34
|
-
|
|
42
|
+
// Display-only last-resort decimals. The authoritative value is the adapter
|
|
43
|
+
// manifest's nativeDecimals (SDK registry); money-adjacent callers SHOULD pass
|
|
44
|
+
// it explicitly via options.decimals. This map only covers the bundled chains
|
|
45
|
+
// so the formatter still renders something sane when no override is supplied.
|
|
46
|
+
const DECIMALS: Record<string, number> = { hedera: 8, xrpl: 6 };
|
|
47
|
+
const DEFAULT_SYMBOL: Record<string, string> = { hedera: 'ℏ', xrpl: 'XRP' };
|
|
35
48
|
|
|
36
49
|
/**
|
|
37
50
|
* Convert a non-negative bigint to a fixed-decimal string without losing
|
|
@@ -47,15 +60,23 @@ function bigintToFixedString(abs: bigint, decimals: number): string {
|
|
|
47
60
|
|
|
48
61
|
export function formatLedgerAmount(
|
|
49
62
|
rawBaseUnits: number | bigint,
|
|
50
|
-
ledgerId: LedgerId,
|
|
63
|
+
ledgerId: LedgerId | (string & {}),
|
|
51
64
|
options: FormatLedgerAmountOptions = {},
|
|
52
65
|
): string {
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
// Authoritative override wins; otherwise fall back to the display-only default.
|
|
67
|
+
const decimals = options.decimals ?? DECIMALS[ledgerId];
|
|
68
|
+
|
|
69
|
+
// Unknown ledger with no explicit decimals — or an out-of-range override — cannot
|
|
70
|
+
// be scaled safely. Return the display "unavailable" sentinel rather than NaN or a
|
|
71
|
+
// thrown error: this is a display path (≠ money path), so it must stay lenient. The
|
|
72
|
+
// upper bound of 100 keeps `decimals` inside `Number.prototype.toFixed`'s legal
|
|
73
|
+
// range and prevents a pathological `10n ** BigInt(decimals)` from hanging the
|
|
74
|
+
// render — no real ledger exceeds ~18 native decimals.
|
|
75
|
+
if (decimals === undefined || !Number.isInteger(decimals) || decimals < 0 || decimals > 100) {
|
|
76
|
+
return '—';
|
|
55
77
|
}
|
|
56
78
|
|
|
57
|
-
const
|
|
58
|
-
const symbol = options.symbol === false ? '' : (options.symbol ?? DEFAULT_SYMBOL[ledgerId]);
|
|
79
|
+
const symbol = options.symbol === false ? '' : (options.symbol ?? DEFAULT_SYMBOL[ledgerId] ?? '');
|
|
59
80
|
const suffix = symbol ? ` ${symbol}` : '';
|
|
60
81
|
|
|
61
82
|
// Build the signed magnitude string. Bigint stays in bigint space so XRPL
|