@nockchain/rose 0.1.4-nightly.5

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.
Files changed (205) hide show
  1. package/.github/workflows/artifacts.yml +33 -0
  2. package/.github/workflows/ci.yml +68 -0
  3. package/.github/workflows/publish-sdk.yml +35 -0
  4. package/.nvmrc +1 -0
  5. package/.prettierignore +5 -0
  6. package/.prettierrc +8 -0
  7. package/LICENSE +22 -0
  8. package/README.md +117 -0
  9. package/extension/background/index.ts +1500 -0
  10. package/extension/content/index.ts +59 -0
  11. package/extension/icons/rose.svg +27 -0
  12. package/extension/icons/rose128.png +0 -0
  13. package/extension/icons/rose16.png +0 -0
  14. package/extension/icons/rose256.png +0 -0
  15. package/extension/icons/rose32.png +0 -0
  16. package/extension/icons/rose48.png +0 -0
  17. package/extension/icons/rose512.png +0 -0
  18. package/extension/inpage/index.ts +86 -0
  19. package/extension/manifest.json +48 -0
  20. package/extension/popup/Popup.tsx +94 -0
  21. package/extension/popup/Router.tsx +121 -0
  22. package/extension/popup/assets/arrow-down-icon.svg +3 -0
  23. package/extension/popup/assets/arrow-left-icon.svg +3 -0
  24. package/extension/popup/assets/arrow-right-icon.svg +3 -0
  25. package/extension/popup/assets/arrow-up-icon.svg +3 -0
  26. package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
  27. package/extension/popup/assets/checkmark-icon.svg +3 -0
  28. package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
  29. package/extension/popup/assets/checkmark-success-icon.svg +3 -0
  30. package/extension/popup/assets/clock-icon.svg +3 -0
  31. package/extension/popup/assets/close-x-icon.svg +3 -0
  32. package/extension/popup/assets/copy-icon.svg +6 -0
  33. package/extension/popup/assets/explorer-icon.svg +3 -0
  34. package/extension/popup/assets/eye-off-icon.svg +3 -0
  35. package/extension/popup/assets/eye-open-icon.svg +4 -0
  36. package/extension/popup/assets/feedback-icon.svg +3 -0
  37. package/extension/popup/assets/green-status-dot.svg +3 -0
  38. package/extension/popup/assets/info-icon.svg +3 -0
  39. package/extension/popup/assets/iris-logo-40.svg +27 -0
  40. package/extension/popup/assets/iris-logo-96.svg +27 -0
  41. package/extension/popup/assets/iris-logo-blue.svg +27 -0
  42. package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
  43. package/extension/popup/assets/iris-logo-orange.svg +27 -0
  44. package/extension/popup/assets/iris-logo.svg +27 -0
  45. package/extension/popup/assets/key-icon.svg +3 -0
  46. package/extension/popup/assets/lock-icon-yellow.svg +3 -0
  47. package/extension/popup/assets/lock-icon.svg +3 -0
  48. package/extension/popup/assets/pencil-edit-icon.svg +3 -0
  49. package/extension/popup/assets/permissions-icon.svg +3 -0
  50. package/extension/popup/assets/receipt-icon.svg +5 -0
  51. package/extension/popup/assets/refresh-icon.svg +3 -0
  52. package/extension/popup/assets/settings-gear-icon.svg +8 -0
  53. package/extension/popup/assets/settings-icon.svg +3 -0
  54. package/extension/popup/assets/theme-icon.svg +3 -0
  55. package/extension/popup/assets/trash-bin-icon.svg +3 -0
  56. package/extension/popup/assets/trend-down-arrow.svg +5 -0
  57. package/extension/popup/assets/trend-up-arrow.svg +5 -0
  58. package/extension/popup/assets/user-account-icon.svg +3 -0
  59. package/extension/popup/assets/vector-bottom-left.svg +9 -0
  60. package/extension/popup/assets/vector-left.svg +9 -0
  61. package/extension/popup/assets/vector-right.svg +9 -0
  62. package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
  63. package/extension/popup/assets/vector-top-right.svg +9 -0
  64. package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
  65. package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
  66. package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
  67. package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
  68. package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
  69. package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
  70. package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
  71. package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
  72. package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
  73. package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
  74. package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
  75. package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
  76. package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
  77. package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
  78. package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
  79. package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
  80. package/extension/popup/components/AccountIcon.tsx +78 -0
  81. package/extension/popup/components/AccountSelector.tsx +246 -0
  82. package/extension/popup/components/Alert.tsx +48 -0
  83. package/extension/popup/components/ConfirmModal.tsx +81 -0
  84. package/extension/popup/components/PasswordInput.tsx +49 -0
  85. package/extension/popup/components/ScreenContainer.tsx +17 -0
  86. package/extension/popup/components/SiteIcon.tsx +60 -0
  87. package/extension/popup/components/ThemeToggle.tsx +44 -0
  88. package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
  89. package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
  90. package/extension/popup/components/icons/CheckIcon.tsx +20 -0
  91. package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
  92. package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
  93. package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
  94. package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
  95. package/extension/popup/components/icons/CloseIcon.tsx +26 -0
  96. package/extension/popup/components/icons/CopyIcon.tsx +20 -0
  97. package/extension/popup/components/icons/EditIcon.tsx +20 -0
  98. package/extension/popup/components/icons/EyeIcon.tsx +13 -0
  99. package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
  100. package/extension/popup/components/icons/InfoIcon.tsx +20 -0
  101. package/extension/popup/components/icons/LockIcon.tsx +20 -0
  102. package/extension/popup/components/icons/PlusIcon.tsx +15 -0
  103. package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
  104. package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
  105. package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
  106. package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
  107. package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
  108. package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
  109. package/extension/popup/components/icons/UploadIcon.tsx +20 -0
  110. package/extension/popup/components/icons/WalletIcon.tsx +20 -0
  111. package/extension/popup/contexts/ThemeContext.tsx +105 -0
  112. package/extension/popup/hooks/useApprovalDetection.ts +128 -0
  113. package/extension/popup/hooks/useAutoFocus.ts +36 -0
  114. package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
  115. package/extension/popup/hooks/useClickOutside.ts +33 -0
  116. package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
  117. package/extension/popup/hooks/useFavicon.ts +64 -0
  118. package/extension/popup/hooks/useNumericInput.ts +93 -0
  119. package/extension/popup/index.html +13 -0
  120. package/extension/popup/index.tsx +24 -0
  121. package/extension/popup/screens/AboutScreen.tsx +118 -0
  122. package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
  123. package/extension/popup/screens/HomeScreen.tsx +902 -0
  124. package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
  125. package/extension/popup/screens/LockTimeScreen.tsx +155 -0
  126. package/extension/popup/screens/ReceiveScreen.tsx +149 -0
  127. package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
  128. package/extension/popup/screens/SendReviewScreen.tsx +308 -0
  129. package/extension/popup/screens/SendScreen.tsx +825 -0
  130. package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
  131. package/extension/popup/screens/SettingsScreen.tsx +116 -0
  132. package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
  133. package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
  134. package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
  135. package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
  136. package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
  137. package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
  138. package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
  139. package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
  140. package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
  141. package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
  142. package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
  143. package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
  144. package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
  145. package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
  146. package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
  147. package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
  148. package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
  149. package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
  150. package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
  151. package/extension/popup/screens/system/LockedScreen.tsx +288 -0
  152. package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
  153. package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
  154. package/extension/popup/store.ts +482 -0
  155. package/extension/popup/styles.css +246 -0
  156. package/extension/popup/utils/format.ts +58 -0
  157. package/extension/popup/utils/formatWalletError.ts +36 -0
  158. package/extension/popup/utils/memo.ts +299 -0
  159. package/extension/popup/utils/messaging.ts +16 -0
  160. package/extension/shared/address-encoding.ts +69 -0
  161. package/extension/shared/balance-query.ts +123 -0
  162. package/extension/shared/constants.ts +386 -0
  163. package/extension/shared/currency.ts +128 -0
  164. package/extension/shared/first-name-derivation.ts +128 -0
  165. package/extension/shared/keyfile.ts +58 -0
  166. package/extension/shared/onboarding.ts +78 -0
  167. package/extension/shared/price-api.ts +79 -0
  168. package/extension/shared/rpc-client-browser.ts +315 -0
  169. package/extension/shared/transaction-builder.ts +443 -0
  170. package/extension/shared/types.ts +450 -0
  171. package/extension/shared/utxo-diff.ts +212 -0
  172. package/extension/shared/utxo-store.ts +548 -0
  173. package/extension/shared/utxo-sync.ts +343 -0
  174. package/extension/shared/validators.ts +26 -0
  175. package/extension/shared/vault.ts +1580 -0
  176. package/extension/shared/wallet-crypto.ts +77 -0
  177. package/extension/shared/wasm-utils.ts +76 -0
  178. package/extension/shared/webcrypto.ts +67 -0
  179. package/extension/types/wasm.d.ts +13 -0
  180. package/package.json +39 -0
  181. package/postcss.config.js +6 -0
  182. package/rose-extension-dist.zip +0 -0
  183. package/sdk/README.md +88 -0
  184. package/sdk/examples/app.ts +166 -0
  185. package/sdk/examples/index.html +51 -0
  186. package/sdk/examples/tsconfig.json +15 -0
  187. package/sdk/examples/tx-builder.html +532 -0
  188. package/sdk/examples/tx-builder.ts +1766 -0
  189. package/sdk/package-lock.json +424 -0
  190. package/sdk/package.json +68 -0
  191. package/sdk/src/constants.ts +28 -0
  192. package/sdk/src/errors.ts +74 -0
  193. package/sdk/src/hooks/index.ts +1 -0
  194. package/sdk/src/hooks/use-rose.ts +94 -0
  195. package/sdk/src/index.ts +12 -0
  196. package/sdk/src/provider.ts +396 -0
  197. package/sdk/src/transaction.ts +163 -0
  198. package/sdk/src/types/rose-wasm.d.ts +14 -0
  199. package/sdk/src/types.ts +97 -0
  200. package/sdk/src/wasm.ts +13 -0
  201. package/sdk/tsconfig.json +20 -0
  202. package/sdk/vite.config.examples.ts +32 -0
  203. package/tailwind.config.ts +38 -0
  204. package/tsconfig.json +20 -0
  205. package/vite.config.ts +60 -0
@@ -0,0 +1,136 @@
1
+ import { useEffect } from 'react';
2
+ import { useStore } from '../../store';
3
+ import { AccountIcon } from '../../components/AccountIcon';
4
+ import { SiteIcon } from '../../components/SiteIcon';
5
+ import { truncateAddress } from '../../utils/format';
6
+ import { send } from '../../utils/messaging';
7
+ import { INTERNAL_METHODS } from '../../../shared/constants';
8
+ import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose';
9
+
10
+ export function ConnectApprovalScreen() {
11
+ const { navigate, pendingConnectRequest, setPendingConnectRequest, wallet } = useStore();
12
+
13
+ const request = pendingConnectRequest;
14
+ const requestId = request?.id ?? '';
15
+
16
+ // Hooks must be unconditional; if there's no request, no-op.
17
+ useAutoRejectOnClose(requestId, INTERNAL_METHODS.REJECT_CONNECTION);
18
+
19
+ // Avoid calling navigate() during render.
20
+ useEffect(() => {
21
+ if (!request) navigate('home');
22
+ }, [request, navigate]);
23
+
24
+ if (!request) return null;
25
+
26
+ const { id, origin } = request;
27
+ const domain = origin.includes('://') ? new URL(origin).hostname : origin;
28
+
29
+ async function handleReject() {
30
+ await send(INTERNAL_METHODS.REJECT_CONNECTION, [id]);
31
+ setPendingConnectRequest(null);
32
+ window.close();
33
+ }
34
+
35
+ async function handleConnect() {
36
+ await send(INTERNAL_METHODS.APPROVE_CONNECTION, [id]);
37
+ setPendingConnectRequest(null);
38
+ window.close();
39
+ }
40
+
41
+ const bg = 'var(--color-bg)';
42
+ const surface = 'var(--color-surface-800)';
43
+ const textPrimary = 'var(--color-text-primary)';
44
+ const textMuted = 'var(--color-text-muted)';
45
+ const divider = 'var(--color-divider)';
46
+ const green = 'var(--color-green)';
47
+
48
+ return (
49
+ <div className="h-screen flex items-center justify-center" style={{ backgroundColor: bg }}>
50
+ <div
51
+ className="w-full h-full flex flex-col"
52
+ style={{ backgroundColor: bg, maxWidth: '357px', maxHeight: '600px' }}
53
+ >
54
+ {/* Header */}
55
+ <div className="flex items-center justify-center px-4 py-4 shrink-0">
56
+ <h2 className="text-xl font-semibold" style={{ color: textPrimary }}>
57
+ Connect Request
58
+ </h2>
59
+ </div>
60
+
61
+ {/* Content */}
62
+ <div className="flex-1 min-h-0 overflow-y-auto">
63
+ <div className="px-4 pb-2">
64
+ {/* Site Icon & Info */}
65
+ <div className="text-center mb-4">
66
+ <div className="mb-3">
67
+ <SiteIcon origin={origin} domain={domain} size="lg" />
68
+ </div>
69
+ <h3 className="text-lg font-semibold mb-1" style={{ color: textPrimary }}>
70
+ {domain}
71
+ </h3>
72
+ <p className="text-xs break-all px-4" style={{ color: textMuted }}>
73
+ {origin}
74
+ </p>
75
+ </div>
76
+
77
+ {/* Permissions */}
78
+ <div className="rounded-lg p-3 mb-3" style={{ backgroundColor: surface }}>
79
+ <p className="text-sm mb-2 font-medium" style={{ color: textPrimary }}>
80
+ Requesting permission to:
81
+ </p>
82
+ <div className="space-y-1.5">
83
+ <div className="flex items-start gap-2 text-sm" style={{ color: textPrimary }}>
84
+ <span style={{ color: green }}>✓</span>
85
+ <span>View your wallet address</span>
86
+ </div>
87
+ <div className="flex items-start gap-2 text-sm" style={{ color: textPrimary }}>
88
+ <span style={{ color: green }}>✓</span>
89
+ <span>Request transaction approvals</span>
90
+ </div>
91
+ </div>
92
+ </div>
93
+
94
+ {/* Account */}
95
+ <div>
96
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
97
+ Connecting Account
98
+ </label>
99
+ <div
100
+ className="rounded-lg p-3 flex items-center gap-2.5"
101
+ style={{ backgroundColor: surface }}
102
+ >
103
+ <AccountIcon
104
+ styleId={wallet.currentAccount?.iconStyleId}
105
+ color={wallet.currentAccount?.iconColor}
106
+ className="w-8 h-8 shrink-0"
107
+ />
108
+ <div className="flex-1 min-w-0">
109
+ <p className="text-sm font-medium" style={{ color: textPrimary }}>
110
+ {wallet.currentAccount?.name || 'Unknown'}
111
+ </p>
112
+ <p className="text-xs font-mono mt-0.5" style={{ color: textMuted }}>
113
+ {truncateAddress(wallet.currentAccount?.address)}
114
+ </p>
115
+ </div>
116
+ </div>
117
+ </div>
118
+ </div>
119
+ </div>
120
+
121
+ {/* Footer Buttons */}
122
+ <div
123
+ className="px-4 py-2.5 shrink-0 flex gap-3"
124
+ style={{ borderTop: `1px solid ${divider}` }}
125
+ >
126
+ <button onClick={handleReject} className="btn-secondary flex-1">
127
+ Cancel
128
+ </button>
129
+ <button onClick={handleConnect} className="btn-primary flex-1">
130
+ Connect
131
+ </button>
132
+ </div>
133
+ </div>
134
+ </div>
135
+ );
136
+ }
@@ -0,0 +1,140 @@
1
+ import { useStore } from '../../store';
2
+ import { AccountIcon } from '../../components/AccountIcon';
3
+ import { SiteIcon } from '../../components/SiteIcon';
4
+ import { truncateAddress } from '../../utils/format';
5
+ import { send } from '../../utils/messaging';
6
+ import { INTERNAL_METHODS } from '../../../shared/constants';
7
+ import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose';
8
+
9
+ export function SignMessageScreen() {
10
+ const { navigate, pendingSignRequest, setPendingSignRequest, wallet } = useStore();
11
+
12
+ if (!pendingSignRequest) {
13
+ navigate('home');
14
+ return null;
15
+ }
16
+
17
+ const { id, origin, message } = pendingSignRequest;
18
+
19
+ useAutoRejectOnClose(id, INTERNAL_METHODS.REJECT_SIGN_MESSAGE);
20
+
21
+ async function handleDecline() {
22
+ await send(INTERNAL_METHODS.REJECT_SIGN_MESSAGE, [id]);
23
+ setPendingSignRequest(null);
24
+ window.close();
25
+ }
26
+
27
+ async function handleSign() {
28
+ await send(INTERNAL_METHODS.APPROVE_SIGN_MESSAGE, [id]);
29
+ setPendingSignRequest(null);
30
+ window.close();
31
+ }
32
+
33
+ const bg = 'var(--color-bg)';
34
+ const surface = 'var(--color-surface-800)';
35
+ const textPrimary = 'var(--color-text-primary)';
36
+ const textMuted = 'var(--color-text-muted)';
37
+ const divider = 'var(--color-divider)';
38
+
39
+ return (
40
+ <div className="h-screen flex items-center justify-center" style={{ backgroundColor: bg }}>
41
+ <div
42
+ className="w-full h-full flex flex-col"
43
+ style={{ backgroundColor: bg, maxWidth: '357px', maxHeight: '600px' }}
44
+ >
45
+ {/* Header */}
46
+ <div className="flex items-center justify-center px-4 py-4 shrink-0">
47
+ <h2 className="text-xl font-semibold" style={{ color: textPrimary }}>
48
+ Sign Message
49
+ </h2>
50
+ </div>
51
+
52
+ {/* Content */}
53
+ <div className="flex-1 min-h-0 overflow-y-auto">
54
+ <div className="px-4 pb-2">
55
+ {/* Site Info */}
56
+ <div className="mb-3">
57
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
58
+ Requesting Site
59
+ </label>
60
+ <div
61
+ className="rounded-lg p-3 flex items-center gap-3"
62
+ style={{ backgroundColor: surface }}
63
+ >
64
+ <SiteIcon
65
+ origin={origin}
66
+ domain={origin.includes('://') ? new URL(origin).hostname : origin}
67
+ size="md"
68
+ />
69
+ <div className="flex-1 min-w-0">
70
+ <p className="text-sm font-semibold mb-0.5" style={{ color: textPrimary }}>
71
+ {origin.includes('://') ? new URL(origin).hostname : origin}
72
+ </p>
73
+ <p className="text-xs break-all" style={{ color: textMuted }}>
74
+ {origin}
75
+ </p>
76
+ </div>
77
+ </div>
78
+ </div>
79
+
80
+ {/* Message Content */}
81
+ <div className="mb-3">
82
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
83
+ Message
84
+ </label>
85
+ <div
86
+ className="rounded-lg p-3 max-h-48 overflow-y-auto"
87
+ style={{ backgroundColor: surface }}
88
+ >
89
+ <pre
90
+ className="text-sm whitespace-pre-wrap break-words font-mono"
91
+ style={{ color: textPrimary }}
92
+ >
93
+ {message}
94
+ </pre>
95
+ </div>
96
+ </div>
97
+
98
+ {/* Account */}
99
+ <div>
100
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
101
+ Signing Account
102
+ </label>
103
+ <div
104
+ className="rounded-lg p-3 flex items-center gap-2.5"
105
+ style={{ backgroundColor: surface }}
106
+ >
107
+ <AccountIcon
108
+ styleId={wallet.currentAccount?.iconStyleId}
109
+ color={wallet.currentAccount?.iconColor}
110
+ className="w-8 h-8 shrink-0"
111
+ />
112
+ <div className="flex-1 min-w-0">
113
+ <p className="text-sm font-medium" style={{ color: textPrimary }}>
114
+ {wallet.currentAccount?.name || 'Unknown'}
115
+ </p>
116
+ <p className="text-xs font-mono mt-0.5" style={{ color: textMuted }}>
117
+ {truncateAddress(wallet.currentAccount?.address)}
118
+ </p>
119
+ </div>
120
+ </div>
121
+ </div>
122
+ </div>
123
+ </div>
124
+
125
+ {/* Footer Buttons */}
126
+ <div
127
+ className="px-4 py-2.5 shrink-0 flex gap-3"
128
+ style={{ borderTop: `1px solid ${divider}` }}
129
+ >
130
+ <button onClick={handleDecline} className="btn-secondary flex-1">
131
+ Decline
132
+ </button>
133
+ <button onClick={handleSign} className="btn-primary flex-1">
134
+ Sign
135
+ </button>
136
+ </div>
137
+ </div>
138
+ </div>
139
+ );
140
+ }
@@ -0,0 +1,320 @@
1
+ import { useEffect, useState } from 'react';
2
+ import { useStore } from '../../store';
3
+ import { INTERNAL_METHODS, APPROVAL_CONSTANTS } from '../../../shared/constants';
4
+ import { send } from '../../utils/messaging';
5
+ import { SignRawTxRequest } from '../../../shared/types';
6
+ import { useAutoRejectOnClose } from '../../hooks/useAutoRejectOnClose';
7
+ import { AccountIcon } from '../../components/AccountIcon';
8
+ import { SiteIcon } from '../../components/SiteIcon';
9
+ import { truncateAddress } from '../../utils/format';
10
+ import { nickToNock, formatNock } from '../../../shared/currency';
11
+ import { extractMemo } from '../../utils/memo';
12
+
13
+ interface NoteItemProps {
14
+ note: any;
15
+ type: 'to' | 'from';
16
+ textPrimary: string;
17
+ textMuted: string;
18
+ surface: string;
19
+ }
20
+
21
+ function NoteItem({ note, type, textPrimary, textMuted, surface }: NoteItemProps) {
22
+ const [copied, setCopied] = useState(false);
23
+
24
+ // Extract data from the complex JSON structure
25
+ // Structure: [{"note_version":{"V1":{...}}}] or similar
26
+ // We need to handle potential variations if the structure isn't exactly as expected, but assuming the provided JSON is representative.
27
+
28
+ // The note object passed here is likely one item from the array, e.g. {"note_version":{"V1":{...}}}
29
+
30
+ let versionData: any = null;
31
+
32
+ if (note.note_version?.V1) {
33
+ versionData = note.note_version.V1;
34
+ }
35
+
36
+ if (!versionData) {
37
+ return (
38
+ <div className="rounded-lg p-3 mb-2" style={{ backgroundColor: surface }}>
39
+ <p className="text-sm text-red-500">Unknown note format</p>
40
+ <pre className="text-xs break-all">{JSON.stringify(note)}</pre>
41
+ </div>
42
+ );
43
+ }
44
+
45
+ const assetsValue = versionData.assets?.value || '0';
46
+ const nicks = parseInt(assetsValue, 10);
47
+ const nocks = nickToNock(nicks);
48
+ const formattedNocks = formatNock(nocks);
49
+
50
+ const firstName = versionData.name?.first || '';
51
+ const lastName = versionData.name?.last || '';
52
+ const fullName = `[ ${firstName} ${lastName} ]`;
53
+
54
+ // Truncate name: first 4 chars of first name ... last 4 chars of last name
55
+ const truncatedName = `[ ${firstName.slice(0, 4)}...${lastName.slice(-4)} ]`;
56
+
57
+ const handleCopy = () => {
58
+ navigator.clipboard.writeText(fullName);
59
+ setCopied(true);
60
+ setTimeout(() => setCopied(false), 2000);
61
+ };
62
+
63
+ return (
64
+ <div className="rounded-lg p-3 mb-2" style={{ backgroundColor: surface }}>
65
+ <div className="flex flex-row items-center gap-1 text-sm font-medium">
66
+ <span style={{ color: textPrimary }}>{formattedNocks} NOCK</span>
67
+ <span style={{ color: textMuted }}>{type}</span>
68
+ <span
69
+ className="font-mono cursor-pointer hover:opacity-80 transition-opacity relative group"
70
+ style={{ color: textMuted }}
71
+ onClick={handleCopy}
72
+ title={fullName}
73
+ >
74
+ {truncatedName}
75
+ {copied && (
76
+ <span className="absolute right-0 top-0 z-50 whitespace-nowrap bg-green-500 text-white text-[10px] px-1 rounded transform -translate-y-full">
77
+ Copied!
78
+ </span>
79
+ )}
80
+ </span>
81
+ </div>
82
+ </div>
83
+ );
84
+ }
85
+
86
+ export function SignRawTxScreen() {
87
+ const { pendingSignRawTxRequest, setPendingSignRawTxRequest, navigate, wallet } = useStore();
88
+ const [memo, setMemo] = useState<string | null>(null);
89
+
90
+ const request = pendingSignRawTxRequest;
91
+ const requestId = request?.id ?? '';
92
+
93
+ // Hooks must be unconditional; if there's no request, no-op.
94
+ useAutoRejectOnClose(requestId, INTERNAL_METHODS.REJECT_SIGN_RAW_TX);
95
+
96
+ // Avoid calling navigate() during render (it triggers "setState while rendering").
97
+ useEffect(() => {
98
+ if (!request) navigate('home');
99
+ }, [request, navigate]);
100
+
101
+ // Memo decoding must be unconditional too, otherwise when the request is cleared
102
+ // (e.g. after approval/reject), this component would render fewer hooks.
103
+ useEffect(() => {
104
+ let cancelled = false;
105
+
106
+ if (!request) {
107
+ setMemo(null);
108
+ return;
109
+ }
110
+
111
+ (async () => {
112
+ try {
113
+ const { ensureWasmInitialized } = await import('../../../shared/wasm-utils');
114
+ await ensureWasmInitialized();
115
+ if (cancelled) return;
116
+ setMemo(extractMemo({ rawTx: request.rawTx, outputs: request.outputs }));
117
+ } catch (err) {
118
+ console.error('[SignRawTx] Failed to decode memo:', err);
119
+ if (!cancelled) setMemo(null);
120
+ }
121
+ })();
122
+
123
+ return () => {
124
+ cancelled = true;
125
+ };
126
+ }, [request]);
127
+
128
+ if (!request) return null;
129
+
130
+ const { id, origin, rawTx, notes, outputs } = request;
131
+
132
+ async function handleDecline() {
133
+ await send(INTERNAL_METHODS.REJECT_SIGN_RAW_TX, [id]);
134
+ setPendingSignRawTxRequest(null);
135
+ window.close();
136
+ }
137
+
138
+ async function handleSign() {
139
+ await send(INTERNAL_METHODS.APPROVE_SIGN_RAW_TX, [id]);
140
+ setPendingSignRawTxRequest(null);
141
+ window.close();
142
+ }
143
+
144
+ // Calculate network fee
145
+ let totalFeeNicks = 0;
146
+ try {
147
+ if (rawTx && rawTx.spends && Array.isArray(rawTx.spends)) {
148
+ totalFeeNicks = rawTx.spends.reduce((sum: number, spend: any) => {
149
+ const feeValue = spend?.spend?.spend_kind?.Witness?.fee?.value;
150
+ const fee = feeValue ? parseInt(feeValue, 10) : 0;
151
+ return sum + (isNaN(fee) ? 0 : fee);
152
+ }, 0);
153
+ }
154
+ } catch (err) {
155
+ console.error('Error calculating fee:', err);
156
+ // Default to 0 if error
157
+ }
158
+
159
+ const totalFeeNocks = nickToNock(totalFeeNicks);
160
+ const formattedFee = formatNock(totalFeeNocks);
161
+
162
+ const bg = 'var(--color-bg)';
163
+ const surface = 'var(--color-surface-800)';
164
+ const textPrimary = 'var(--color-text-primary)';
165
+ const textMuted = 'var(--color-text-muted)';
166
+ const divider = 'var(--color-divider)';
167
+
168
+ return (
169
+ <div className="h-screen flex items-center justify-center" style={{ backgroundColor: bg }}>
170
+ <div
171
+ className="w-full h-full flex flex-col"
172
+ style={{ backgroundColor: bg, maxWidth: '357px', maxHeight: '600px' }}
173
+ >
174
+ {/* Header */}
175
+ <div className="flex items-center justify-center px-4 py-4 shrink-0">
176
+ <h2 className="text-xl font-semibold" style={{ color: textPrimary }}>
177
+ Sign Raw Transaction
178
+ </h2>
179
+ </div>
180
+
181
+ {/* Content */}
182
+ <div className="flex-1 min-h-0 overflow-y-auto">
183
+ <div className="px-4 pb-2">
184
+ {/* Site Info */}
185
+ <div className="mb-3">
186
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
187
+ Requesting Site
188
+ </label>
189
+ <div
190
+ className="rounded-lg p-3 flex items-center gap-3"
191
+ style={{ backgroundColor: surface }}
192
+ >
193
+ <SiteIcon
194
+ origin={origin}
195
+ domain={origin.includes('://') ? new URL(origin).hostname : origin}
196
+ size="md"
197
+ />
198
+ <div className="flex-1 min-w-0">
199
+ <p className="text-sm font-semibold mb-0.5" style={{ color: textPrimary }}>
200
+ {origin.includes('://') ? new URL(origin).hostname : origin}
201
+ </p>
202
+ <p className="text-xs break-all" style={{ color: textMuted }}>
203
+ {origin}
204
+ </p>
205
+ </div>
206
+ </div>
207
+ </div>
208
+
209
+ {/* Raw Transaction Content */}
210
+ <div className="mb-3">
211
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
212
+ Inputs ({notes.length})
213
+ </label>
214
+ <div className="max-h-48 overflow-y-auto">
215
+ {notes.map((note: any, index: number) => (
216
+ <NoteItem
217
+ key={`input-${index}`}
218
+ note={note}
219
+ type="from"
220
+ textPrimary={textPrimary}
221
+ textMuted={textMuted}
222
+ surface={surface}
223
+ />
224
+ ))}
225
+ </div>
226
+ </div>
227
+
228
+ {/* Raw Transaction Outputs */}
229
+ {outputs && outputs.length > 0 && (
230
+ <div className="mb-3">
231
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
232
+ Outputs ({outputs.length})
233
+ </label>
234
+ <div className="max-h-48 overflow-y-auto">
235
+ {outputs.map((output: any, index: number) => (
236
+ <NoteItem
237
+ key={`output-${index}`}
238
+ note={output}
239
+ type="to"
240
+ textPrimary={textPrimary}
241
+ textMuted={textMuted}
242
+ surface={surface}
243
+ />
244
+ ))}
245
+ </div>
246
+ </div>
247
+ )}
248
+
249
+ {/* Memo (optional) */}
250
+ {memo && (
251
+ <div className="mb-3">
252
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
253
+ Memo
254
+ </label>
255
+ <div className="rounded-lg p-3" style={{ backgroundColor: surface }}>
256
+ <p
257
+ className="text-sm whitespace-pre-wrap break-words"
258
+ style={{ color: textPrimary }}
259
+ >
260
+ {memo}
261
+ </p>
262
+ </div>
263
+ </div>
264
+ )}
265
+
266
+ {/* Network Fee */}
267
+ <div className="mb-3">
268
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
269
+ Network Fee
270
+ </label>
271
+ <div className="rounded-lg p-3" style={{ backgroundColor: surface }}>
272
+ <p className="text-sm font-semibold" style={{ color: textPrimary }}>
273
+ {formattedFee} NOCK
274
+ </p>
275
+ </div>
276
+ </div>
277
+
278
+ {/* Account */}
279
+ <div>
280
+ <label className="text-xs block mb-1.5 font-medium" style={{ color: textMuted }}>
281
+ Signing Account
282
+ </label>
283
+ <div
284
+ className="rounded-lg p-3 flex items-center gap-2.5"
285
+ style={{ backgroundColor: surface }}
286
+ >
287
+ <AccountIcon
288
+ styleId={wallet.currentAccount?.iconStyleId}
289
+ color={wallet.currentAccount?.iconColor}
290
+ className="w-8 h-8 shrink-0"
291
+ />
292
+ <div className="flex-1 min-w-0">
293
+ <p className="text-sm font-medium" style={{ color: textPrimary }}>
294
+ {wallet.currentAccount?.name || 'Unknown'}
295
+ </p>
296
+ <p className="text-xs font-mono mt-0.5" style={{ color: textMuted }}>
297
+ {truncateAddress(wallet.currentAccount?.address)}
298
+ </p>
299
+ </div>
300
+ </div>
301
+ </div>
302
+ </div>
303
+ </div>
304
+
305
+ {/* Footer Buttons */}
306
+ <div
307
+ className="px-4 py-2.5 shrink-0 flex gap-3"
308
+ style={{ borderTop: `1px solid ${divider}` }}
309
+ >
310
+ <button onClick={handleDecline} className="btn-secondary flex-1">
311
+ Decline
312
+ </button>
313
+ <button onClick={handleSign} className="btn-primary flex-1">
314
+ Sign
315
+ </button>
316
+ </div>
317
+ </div>
318
+ </div>
319
+ );
320
+ }