diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 17184e7..f3707c0 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -41,6 +41,9 @@ model Transaction { lastRetryAt DateTime? retryHistory RetryAttempt[] amlAlerts AMLAlert[] + + @@index([senderId, createdAt]) + @@index([recipientId, createdAt]) } model RetryAttempt { @@ -142,6 +145,8 @@ model KYCRecord { riskLevel String? submittedAt DateTime @default(now()) updatedAt DateTime @updatedAt + + @@index([status]) } model AMLAlert { @@ -156,6 +161,9 @@ model AMLAlert { riskScore Int riskLevel String createdAt DateTime @default(now()) + + @@index([userId]) + @@index([severity, createdAt]) } model FeeBumpStat { diff --git a/frontend/src/components/SecurityKeyWarning.jsx b/frontend/src/components/SecurityKeyWarning.jsx index ea44c63..74c1f53 100644 --- a/frontend/src/components/SecurityKeyWarning.jsx +++ b/frontend/src/components/SecurityKeyWarning.jsx @@ -13,77 +13,44 @@ export function SecurityKeyWarning({ onAcknowledge }) { initial={{ opacity: 0, scale: 0.95 }} animate={{ opacity: 1, scale: 1 }} exit={{ opacity: 0, scale: 0.95 }} - style={{ - background: 'linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%)', - border: '2px solid #ef4444', - borderRadius: 8, - padding: 16, - marginBottom: 16, - }} > - {/* Header */} -
- Your secret key is displayed. Keep it secure and private. -
+Your secret key is displayed. Keep it secure and private.
- {publicKey}
-
+ {publicKey}
+
+
+
+
{revealed ? secretKey : masked}
setRevealed(!revealed)}
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
- style={{
- background: revealed ? '#fecaca' : 'white',
- color: revealed ? '#991b1b' : '#666',
- border: '1px solid #ef4444',
- borderRadius: 4,
- padding: '4px 8px',
- fontSize: 12,
- cursor: 'pointer',
- width: 'auto',
- minHeight: 'unset',
- fontWeight: 600,
- }}
+ className="secret-key-display__button secret-key-display__button--reveal"
>
{revealed ? '👁 Hide' : '👁 Show'}
@@ -229,18 +129,7 @@ export function SecretKeyDisplay({ secretKey, publicKey }) {
whileHover={{ scale: 1.05 }}
whileTap={{ scale: 0.95 }}
disabled={!revealed}
- style={{
- background: revealed ? '#ef4444' : '#e5e7eb',
- color: revealed ? 'white' : '#999',
- border: 'none',
- borderRadius: 4,
- padding: '4px 8px',
- fontSize: 12,
- cursor: revealed ? 'pointer' : 'not-allowed',
- width: 'auto',
- minHeight: 'unset',
- fontWeight: 600,
- }}
+ className="secret-key-display__button secret-key-display__button--copy"
>
{copied === 'secret' ? '✓ Copied' : 'Copy'}
@@ -248,18 +137,10 @@ export function SecretKeyDisplay({ secretKey, publicKey }) {
💡 Tip: Save both keys somewhere secure before leaving this page.
They will not be displayed again.
diff --git a/frontend/src/components/StatusMessage.jsx b/frontend/src/components/StatusMessage.jsx
index c3cdcb8..79ad421 100644
--- a/frontend/src/components/StatusMessage.jsx
+++ b/frontend/src/components/StatusMessage.jsx
@@ -16,7 +16,6 @@ function Message({ msg, onRemove, onRetry }) {
initial="hidden" animate="visible" exit="exit"
layout
role="alert"
- aria-live={msg.type === 'error' ? 'assertive' : 'polite'}
aria-atomic="true"
>
@@ -34,7 +33,7 @@ export function StatusMessage({ messages, onRemove, showHistory = false, history
const [historyOpen, setHistoryOpen] = useState(false);
return (
-
+
{messages.map((msg) => (
diff --git a/frontend/src/index.css b/frontend/src/index.css
index 2beb6be..7be2721 100644
--- a/frontend/src/index.css
+++ b/frontend/src/index.css
@@ -1918,3 +1918,213 @@ kbd {
font-size: 0.8rem;
}
}
+
+
+/* ── Security Key Warning & Display ──────────────────────────────────────── */
+
+.security-warning {
+ background: linear-gradient(135deg, #fef2f2 0%, #fee2e2 100%);
+ border: 2px solid #ef4444;
+ border-radius: 8px;
+ padding: 16px;
+ margin-bottom: 16px;
+}
+
+.security-warning__header {
+ display: flex;
+ align-items: flex-start;
+ gap: 12px;
+ margin-bottom: 12px;
+}
+
+.security-warning__icon {
+ font-size: 24px;
+ flex-shrink: 0;
+}
+
+.security-warning__title {
+ margin: 0 0 4px 0;
+ color: #991b1b;
+ font-size: 16px;
+ font-weight: 600;
+}
+
+.security-warning__subtitle {
+ margin: 0;
+ font-size: 12px;
+ color: #7f1d1d;
+}
+
+.security-warning__list {
+ background: white;
+ border-radius: 6px;
+ padding: 12px;
+ margin-bottom: 12px;
+ list-style: none;
+ padding-left: 0;
+ margin-top: 0;
+ font-size: 13px;
+ color: #7f1d1d;
+}
+
+.security-warning__list-item {
+ display: flex;
+ gap: 8px;
+ margin-bottom: 8px;
+}
+
+.security-warning__list-item:last-child {
+ margin-bottom: 0;
+}
+
+.security-warning__list-icon {
+ flex-shrink: 0;
+}
+
+.security-warning__actions {
+ display: flex;
+ gap: 8px;
+ flex-wrap: wrap;
+}
+
+.security-warning__button {
+ background: #ef4444;
+ color: white;
+ border: none;
+ border-radius: 4px;
+ padding: 8px 12px;
+ font-size: 13px;
+ font-weight: 600;
+ cursor: pointer;
+ flex: 1;
+ min-width: 120px;
+ transition: background 0.2s;
+}
+
+.security-warning__button:hover {
+ background: #dc2626;
+}
+
+.secret-key-display {
+ margin-top: 16px;
+}
+
+.secret-key-display__field {
+ margin-bottom: 12px;
+}
+
+.secret-key-display__label {
+ display: block;
+ font-size: 12px;
+ font-weight: 600;
+ margin-bottom: 6px;
+ color: #555;
+}
+
+.secret-key-display__input-group {
+ display: flex;
+ gap: 8px;
+ align-items: center;
+ border-radius: 4px;
+ padding: 10px;
+}
+
+.secret-key-display__input-group--public {
+ background: #f0f9ff;
+ border: 1px solid #bfdbfe;
+}
+
+.secret-key-display__input-group--secret {
+ background: #fef2f2;
+ border: 2px solid #ef4444;
+}
+
+.secret-key-display__code {
+ flex: 1;
+ font-size: 12px;
+ overflow: auto;
+ font-family: monospace;
+ word-break: break-all;
+}
+
+.secret-key-display__code--secret {
+ color: #991b1b;
+}
+
+.secret-key-display__code--masked {
+ color: #999;
+}
+
+.secret-key-display__button {
+ background: white;
+ border: 1px solid #bfdbfe;
+ border-radius: 4px;
+ padding: 4px 8px;
+ font-size: 12px;
+ cursor: pointer;
+ width: auto;
+ min-height: unset;
+ transition: all 0.2s;
+}
+
+.secret-key-display__button:hover {
+ background: #f0f9ff;
+}
+
+.secret-key-display__button--reveal {
+ background: #fecaca;
+ color: #991b1b;
+ border: 1px solid #ef4444;
+ font-weight: 600;
+}
+
+.secret-key-display__button--reveal:hover {
+ background: #fca5a5;
+}
+
+.secret-key-display__button--copy {
+ background: #ef4444;
+ color: white;
+ border: none;
+ font-weight: 600;
+}
+
+.secret-key-display__button--copy:hover {
+ background: #dc2626;
+}
+
+.secret-key-display__button--copy:disabled {
+ background: #e5e7eb;
+ color: #999;
+ cursor: not-allowed;
+}
+
+.secret-key-display__tip {
+ background: #fef3c7;
+ border: 1px solid #fbbf24;
+ border-radius: 4px;
+ padding: 10px;
+ margin-top: 10px;
+ font-size: 12px;
+ color: #78350f;
+}
+
+@media (max-width: 480px) {
+ .security-warning__actions {
+ flex-direction: column;
+ }
+
+ .security-warning__button {
+ width: 100%;
+ min-width: unset;
+ }
+
+ .secret-key-display__input-group {
+ flex-wrap: wrap;
+ }
+
+ .secret-key-display__button {
+ font-size: 11px;
+ padding: 3px 6px;
+ }
+}
diff --git a/frontend/src/utils/errorMessages.js b/frontend/src/utils/errorMessages.js
index 9d736ce..38f408c 100644
--- a/frontend/src/utils/errorMessages.js
+++ b/frontend/src/utils/errorMessages.js
@@ -7,7 +7,58 @@ const ERROR_MAP = [
{ match: /tx_failed/i, message: 'Transaction was rejected by the Stellar network.' },
];
+const STELLAR_RESULT_CODES = {
+ // Transaction result codes
+ tx_success: 'Transaction completed successfully.',
+ tx_failed: 'Transaction failed.',
+ tx_too_early: 'Transaction timestamp is too early.',
+ tx_too_late: 'Transaction timestamp is too late.',
+ tx_missing_operation: 'Transaction has no operations.',
+ tx_bad_seq: 'Transaction sequence number is invalid.',
+ tx_bad_auth: 'Transaction authentication failed.',
+ tx_insufficient_balance: 'Insufficient balance for this transaction.',
+ tx_no_source_account: 'Source account does not exist.',
+ tx_insufficient_fee: 'Transaction fee is too low.',
+ tx_fee_bump_inner_failed: 'Inner transaction of fee bump failed.',
+ tx_bad_auth_extra: 'Extra signers provided but not required.',
+ tx_internal_error: 'Internal Stellar network error.',
+ tx_not_supported: 'Transaction type is not supported.',
+ tx_bad_sponsorship: 'Sponsorship setup is invalid.',
+ tx_bad_min_seq_age: 'Minimum sequence age requirement not met.',
+ tx_malformed: 'Transaction is malformed.',
+
+ // Operation result codes
+ op_success: 'Operation completed successfully.',
+ op_inner: 'Operation failed with inner error.',
+ op_bad_auth: 'Operation authentication failed.',
+ op_no_destination: 'Destination account does not exist.',
+ op_no_trust: 'Destination has no trust line for this asset.',
+ op_not_authorized: 'Operation not authorized.',
+ op_underfunded: 'Source account has insufficient funds.',
+ op_line_full: 'Destination trust line is full.',
+ op_self_not_allowed: 'Cannot send to self.',
+ op_not_supported: 'Operation type is not supported.',
+};
+
export function getFriendlyError(error) {
+ // Check for Stellar SDK result codes first
+ const resultCodes = error?.response?.data?.extras?.result_codes;
+ if (resultCodes) {
+ if (resultCodes.transaction) {
+ const txCode = resultCodes.transaction;
+ if (STELLAR_RESULT_CODES[txCode]) {
+ return STELLAR_RESULT_CODES[txCode];
+ }
+ }
+ if (resultCodes.operations && resultCodes.operations.length > 0) {
+ const opCode = resultCodes.operations[0];
+ if (STELLAR_RESULT_CODES[opCode]) {
+ return STELLAR_RESULT_CODES[opCode];
+ }
+ }
+ }
+
+ // Fall back to string matching
const raw = error?.response?.data?.error || error?.message || String(error);
console.error('[Stellar Error]', raw);
const match = ERROR_MAP.find(e => e.match.test(raw));