Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 67 additions & 1 deletion index.html
Original file line number Diff line number Diff line change
Expand Up @@ -495,6 +495,32 @@

.error-step h2 {
color: #f44336;
margin-bottom: 8px;
}

.error-code-badge {
display: inline-block;
background: rgba(244, 67, 54, 0.15);
color: #f44336;
font-family: monospace;
font-size: 0.85rem;
font-weight: 600;
padding: 4px 12px;
border-radius: 4px;
margin-bottom: 12px;
letter-spacing: 0.5px;
}

.error-label {
font-size: 1rem;
font-weight: 500;
color: #e0e0e0;
margin-bottom: 4px;
}

.error-failed-step {
font-size: 0.85rem;
color: #9e9e9e;
margin-bottom: 16px;
}

Expand All @@ -504,8 +530,48 @@
padding: 16px;
border-radius: 8px;
margin-bottom: 24px;
font-size: 0.9rem;
font-size: 0.85rem;
word-break: break-word;
text-align: left;
}

.error-actions {
display: flex;
gap: 12px;
justify-content: center;
flex-wrap: wrap;
}

.error-technical {
margin-bottom: 20px;
text-align: left;
}

.error-technical summary {
cursor: pointer;
font-size: 0.8rem;
color: #9e9e9e;
margin-bottom: 8px;
user-select: none;
}

.error-technical summary:hover {
color: #e0e0e0;
}

.error-technical-content {
background: rgba(0, 0, 0, 0.3);
border: 1px solid rgba(255, 255, 255, 0.1);
padding: 12px;
border-radius: 6px;
font-size: 0.75rem;
line-height: 1.5;
overflow-x: auto;
white-space: pre-wrap;
word-break: break-all;
max-height: 300px;
overflow-y: auto;
color: #bdbdbd;
}

/* Key configuration styles */
Expand Down
17 changes: 10 additions & 7 deletions src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ import {
setInstantLockReceived,
setIdentityRegistered,
setError,
toError,
ErrorCodes,
setDepositTimedOut,
setNetwork,
updateIdentityKey,
Expand Down Expand Up @@ -420,7 +422,8 @@ function setupEventListeners(container: HTMLElement) {
} else {
updateState(setError(
state,
new Error('No valid key for DPNS registration. You need an AUTHENTICATION key with CRITICAL or HIGH security level.')
new Error('No valid key for DPNS registration. You need an AUTHENTICATION key with CRITICAL or HIGH security level.'),
ErrorCodes.CONFIG
));
}
});
Expand Down Expand Up @@ -1104,7 +1107,7 @@ async function startTopUp() {

} catch (error) {
console.error('Top-up error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down Expand Up @@ -1206,7 +1209,7 @@ async function startSendToAddress() {

} catch (error) {
console.error('Send to platform address error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down Expand Up @@ -1326,7 +1329,7 @@ async function startBridge() {

} catch (error) {
console.error('Bridge error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down Expand Up @@ -1451,7 +1454,7 @@ async function recheckDeposit() {

} catch (error) {
console.error('Bridge error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down Expand Up @@ -1482,7 +1485,7 @@ async function startDpnsCheck() {

} catch (error) {
console.error('DPNS check error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down Expand Up @@ -1532,7 +1535,7 @@ async function startDpnsRegistration() {

} catch (error) {
console.error('DPNS registration error:', error);
updateState(setError(state, error instanceof Error ? error : new Error(String(error))));
updateState(setError(state, toError(error)));
}
}

Expand Down
4 changes: 4 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,6 +215,10 @@ export interface BridgeState {
assetLockProof?: AssetLockProofData;
identityId?: string;
error?: Error;
/** Error code for user-facing display (e.g., "ERR-1006") */
errorCode?: string;
/** The step that was active when the error occurred */
errorStep?: BridgeStep;
/** True when deposit detection timed out and needs manual recheck */
depositTimedOut?: boolean;
/** Current detected deposit amount (may be below minimum) */
Expand Down
123 changes: 120 additions & 3 deletions src/ui/components.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { BridgeState, KeyType, KeyPurpose, SecurityLevel } from '../types.js';
import { getStepProgress } from './state.js';
import { getStepProgress, getStepDescription, ErrorCodes, ErrorCodeLabels } from './state.js';
import { shouldShowContestedWarning, countUsernameStatuses } from '../platform/dpns.js';
import { generateQRCodeDataUrl } from './qrcode.js';
import { privateKeyToWif } from '../utils/wif.js';
Expand Down Expand Up @@ -810,17 +810,134 @@ function renderCompleteStep(state: BridgeState): HTMLElement {
return div;
}

/** Build a full diagnostic object from the error state for dev troubleshooting */
function buildErrorDiagnostics(state: BridgeState): Record<string, unknown> {
const errorCode = state.errorCode ?? ErrorCodes.UNKNOWN;
const diag: Record<string, unknown> = {
errorCode,
errorLabel: ErrorCodeLabels[errorCode] ?? 'Unknown error',
errorStep: state.errorStep ?? null,
message: state.error?.message ?? 'An unknown error occurred',
stack: state.error?.stack ?? null,
network: state.network,
mode: state.mode,
timestamp: new Date().toISOString(),
};

// Transaction context — critical for tracking on-chain state
// NOTE: signedTxHex is intentionally excluded — if the error occurred before
// broadcast, a signed tx in a bug report lets anyone steal the user's funds.
// txid + UTXO data is sufficient for on-chain lookup.
if (state.depositAddress) diag.depositAddress = state.depositAddress;
if (state.txid) diag.txid = state.txid;
if (state.depositAmount !== undefined) diag.depositAmount = String(state.depositAmount);
if (state.detectedUtxo) {
diag.utxo = {
txid: state.detectedUtxo.txid,
vout: state.detectedUtxo.vout,
satoshis: state.detectedUtxo.satoshis,
};
}

// Identity context
if (state.identityId) diag.identityId = state.identityId;
if (state.targetIdentityId) diag.targetIdentityId = state.targetIdentityId;
if (state.recipientPlatformAddress) diag.recipientPlatformAddress = state.recipientPlatformAddress;

// DPNS context
if (state.dpnsUsernames?.length) {
diag.dpnsUsernames = state.dpnsUsernames.map(u => ({
label: u.label,
status: u.status,
isAvailable: u.isAvailable ?? null,
isContested: u.isContested ?? null,
}));
}
if (state.dpnsPublicKeyId !== undefined) diag.dpnsPublicKeyId = state.dpnsPublicKeyId;
if (state.dpnsRegistrationProgress !== undefined) diag.dpnsRegistrationProgress = state.dpnsRegistrationProgress;
if (state.dpnsResults?.length) {
diag.dpnsResults = state.dpnsResults.map(r => ({
label: r.label,
success: r.success,
error: r.error ?? null,
isContested: r.isContested,
}));
}

// Identity management context
if (state.manageSigningKeyInfo) diag.manageSigningKeyInfo = state.manageSigningKeyInfo;
// Count only — ManageNewKeyConfig objects contain private key material
if (state.manageKeysToAdd?.length) diag.manageKeysToAddCount = state.manageKeysToAdd.length;
if (state.manageKeyIdsToDisable?.length) diag.manageKeyIdsToDisable = state.manageKeyIdsToDisable;

// Retry state
if (state.retryStatus) diag.retryStatus = state.retryStatus;

// Identity keys count (not the keys themselves)
if (state.identityKeys.length) diag.identityKeysCount = state.identityKeys.length;

return diag;
}

function renderErrorStep(state: BridgeState): HTMLElement {
const div = document.createElement('div');
div.className = 'error-step';

const diag = buildErrorDiagnostics(state);
const errorCode = String(diag.errorCode);
const errorLabel = String(diag.errorLabel);
const errorMessage = String(diag.message);
const failedStep = state.errorStep ? getStepDescription(state.errorStep) : undefined;

const failedStepHtml = failedStep
? `<p class="error-failed-step">Failed during: ${escapeHtml(failedStep)}</p>`
: '';

// Build a concise technical summary from the diagnostic object
const techLines: string[] = [];
if (diag.errorStep) techLines.push(`Step: ${diag.errorStep}`);
if (diag.depositAddress) techLines.push(`Deposit: ${diag.depositAddress}`);
if (diag.txid) techLines.push(`TxID: ${diag.txid}`);
if (diag.identityId) techLines.push(`Identity: ${diag.identityId}`);
if (diag.targetIdentityId) techLines.push(`Target Identity: ${diag.targetIdentityId}`);
if (diag.recipientPlatformAddress) techLines.push(`Recipient: ${diag.recipientPlatformAddress}`);
if (diag.stack) techLines.push(`\nStack Trace:\n${diag.stack}`);

const techDetailsHtml = techLines.length > 0 ? `
<details class="error-technical">
<summary>Technical Details</summary>
<pre class="error-technical-content">${escapeHtml(techLines.join('\n'))}</pre>
</details>
` : '';

div.innerHTML = `
<div class="error-icon">❌</div>
<h2>Error</h2>
<p class="error-message">${state.error?.message || 'An unknown error occurred'}</p>
<button id="retry-btn" class="secondary-btn">Try Again</button>
<div class="error-code-badge">${escapeHtml(errorCode)}</div>
<p class="error-label">${escapeHtml(errorLabel)}</p>
${failedStepHtml}
<p class="error-message">${escapeHtml(errorMessage)}</p>
${techDetailsHtml}
<div class="error-actions">
<button id="retry-btn" class="secondary-btn">Try Again</button>
<button id="copy-error-btn" class="secondary-btn">Copy Error Details</button>
</div>
`;

const copyBtn = div.querySelector('#copy-error-btn');
if (copyBtn) {
copyBtn.addEventListener('click', () => {
const copyText = JSON.stringify(diag, null, 2);
navigator.clipboard.writeText(copyText).then(() => {
copyBtn.textContent = 'Copied!';
setTimeout(() => { copyBtn.textContent = 'Copy Error Details'; }, 2000);
}).catch(() => {
copyBtn.textContent = 'Copy failed';
setTimeout(() => { copyBtn.textContent = 'Copy Error Details'; }, 2000);
});
});
}

return div;
}

Expand Down
3 changes: 3 additions & 0 deletions src/ui/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,9 @@ export {
setInstantLockReceived,
setIdentityRegistered,
setError,
toError,
ErrorCodes,
ErrorCodeLabels,
setDepositTimedOut,
setNetwork,
getStepDescription,
Expand Down
Loading
Loading