@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,443 @@
1
+ /**
2
+ * Transaction Builder
3
+ * High-level API for constructing Nockchain transactions
4
+ */
5
+
6
+ import * as wasm from '@nockchain/sdk/wasm';
7
+ import { publicKeyToPKHDigest } from './address-encoding.js';
8
+ import { base58 } from '@scure/base';
9
+ import { DEFAULT_FEE_PER_WORD } from './constants.js';
10
+ import { initIrisSdkOnce } from './wasm-utils.js';
11
+
12
+ /**
13
+ * Discover the correct spend condition for a note by matching lock-root to name.first
14
+ *
15
+ * The note's name.first commits to the lock-root (Merkle root of spend condition).
16
+ * We try different candidate spend conditions and find which one matches.
17
+ *
18
+ * @param senderPKH - Base58 PKH digest of the sender's public key
19
+ * @param note - Note with nameFirst (lock-root) and originPage
20
+ * @returns The matching SpendCondition
21
+ */
22
+ export async function discoverSpendConditionForNote(
23
+ senderPKH: string,
24
+ note: { nameFirst: string; originPage: number }
25
+ ): Promise<wasm.SpendCondition> {
26
+ await initIrisSdkOnce();
27
+
28
+ const candidates: Array<{ name: string; condition: wasm.SpendCondition }> = [];
29
+
30
+ // 1) PKH only (standard simple note)
31
+ try {
32
+ const pkhLeaf = wasm.LockPrimitive.newPkh(wasm.Pkh.single(senderPKH));
33
+ const condition = new wasm.SpendCondition([pkhLeaf]);
34
+ candidates.push({ name: 'PKH-only', condition });
35
+ } catch (e) {
36
+ console.warn('[TxBuilder] Failed to create PKH-only condition:', e);
37
+ }
38
+
39
+ // 2) PKH ∧ TIM (coinbase helper)
40
+ try {
41
+ const pkhLeaf = wasm.LockPrimitive.newPkh(wasm.Pkh.single(senderPKH));
42
+ const timLeaf = wasm.LockPrimitive.newTim(wasm.LockTim.coinbase());
43
+ const condition = new wasm.SpendCondition([pkhLeaf, timLeaf]);
44
+ candidates.push({ name: 'PKH+TIM(coinbase)', condition });
45
+ } catch (e) {
46
+ console.warn('[TxBuilder] Failed to create PKH+TIM(coinbase) condition:', e);
47
+ }
48
+
49
+ // 3) PKH ∧ TIM (relative 100 blocks - common coinbase maturity)
50
+ try {
51
+ const pkhLeaf = wasm.LockPrimitive.newPkh(wasm.Pkh.single(senderPKH));
52
+ const timLeaf = wasm.LockPrimitive.newTim(
53
+ new wasm.LockTim(new wasm.TimelockRange(100n, null), new wasm.TimelockRange(null, null))
54
+ );
55
+ const condition = new wasm.SpendCondition([pkhLeaf, timLeaf]);
56
+ candidates.push({ name: 'PKH+TIM(rel:100)', condition });
57
+ } catch (e) {
58
+ console.warn('[TxBuilder] Failed to create PKH+TIM(rel:100) condition:', e);
59
+ }
60
+
61
+ // 4) PKH ∧ TIM (absolute = originPage + 100)
62
+ try {
63
+ const absMin = BigInt(note.originPage) + 100n;
64
+ const pkhLeaf = wasm.LockPrimitive.newPkh(wasm.Pkh.single(senderPKH));
65
+ const timLeaf = wasm.LockPrimitive.newTim(
66
+ new wasm.LockTim(new wasm.TimelockRange(null, null), new wasm.TimelockRange(absMin, null))
67
+ );
68
+ const condition = new wasm.SpendCondition([pkhLeaf, timLeaf]);
69
+ candidates.push({ name: 'PKH+TIM(abs:origin+100)', condition });
70
+ } catch (e) {
71
+ console.warn('[TxBuilder] Failed to create PKH+TIM(abs:origin+100) condition:', e);
72
+ }
73
+
74
+ // Find the candidate whose first-name matches note.nameFirst
75
+ for (const candidate of candidates) {
76
+ const derivedFirstName = candidate.condition.firstName().value;
77
+ if (derivedFirstName === note.nameFirst) {
78
+ return candidate.condition;
79
+ }
80
+ }
81
+
82
+ throw new Error(
83
+ `No matching spend condition for note.name.first (${note.nameFirst.slice(0, 20)}...). ` +
84
+ `Cannot spend this UTXO. It may require a different lock configuration.`
85
+ );
86
+ }
87
+
88
+ /**
89
+ * Note data in V1 WASM format (local interface for transaction builder)
90
+ */
91
+ export interface Note {
92
+ originPage: number;
93
+ nameFirst: string; // base58 digest string
94
+ nameLast: string; // base58 digest string
95
+ noteDataHash: string; // base58 digest string
96
+ assets: number;
97
+ protoNote?: any;
98
+ }
99
+
100
+ /**
101
+ * Transaction parameters for new builder API
102
+ */
103
+ export interface TransactionParams {
104
+ /** Notes (UTXOs) to spend */
105
+ notes: Note[];
106
+ /** Spend condition(s) - single condition applied to all notes, or array with one per note */
107
+ spendCondition: wasm.SpendCondition | wasm.SpendCondition[];
108
+ /** Recipient's PKH as digest string */
109
+ recipientPKH: string;
110
+ /** Amount to send in nicks */
111
+ amount: number;
112
+ /** Transaction fee override in nicks */
113
+ fee?: number;
114
+ /** Your PKH for receiving change (as digest string) */
115
+ refundPKH: string;
116
+ /** Private key for signing (32 bytes) */
117
+ privateKey: Uint8Array;
118
+ /** Whether to include lock data or not */
119
+ includeLockData: boolean;
120
+ }
121
+
122
+ /**
123
+ * Constructed transaction ready for broadcast
124
+ */
125
+ export interface ConstructedTransaction {
126
+ /** Transaction ID as digest string */
127
+ txId: string;
128
+ /** Transaction version */
129
+ version: number;
130
+ /** Raw transaction object (for additional operations) */
131
+ nockchainTx: wasm.NockchainTx;
132
+ /** Fee used in the transaction (in nicks) */
133
+ feeUsed: number;
134
+ }
135
+
136
+ /**
137
+ * Build a complete Nockchain transaction using the new builder API
138
+ *
139
+ * @param params - Transaction parameters
140
+ * @returns Constructed transaction ready for broadcast
141
+ */
142
+ export async function buildTransaction(params: TransactionParams): Promise<ConstructedTransaction> {
143
+ // Initialize both WASM modules
144
+ await initIrisSdkOnce();
145
+
146
+ const {
147
+ notes,
148
+ spendCondition,
149
+ recipientPKH,
150
+ amount,
151
+ fee,
152
+ refundPKH,
153
+ privateKey,
154
+ includeLockData,
155
+ } = params;
156
+
157
+ // Validate inputs
158
+ if (notes.length === 0) {
159
+ throw new Error('At least one note (UTXO) is required');
160
+ }
161
+ if (privateKey.length !== 32) {
162
+ throw new Error('Private key must be 32 bytes');
163
+ }
164
+
165
+ // Calculate total available from notes
166
+ const totalAvailable = notes.reduce((sum, note) => sum + note.assets, 0);
167
+
168
+ if (totalAvailable < amount + (fee || 0)) {
169
+ throw new Error(
170
+ `Insufficient funds: have ${totalAvailable} nicks, need ${amount + (fee || 0)} (${amount} amount + ${fee} fee)`
171
+ );
172
+ }
173
+
174
+ // Convert notes using Note.fromProtobuf() to preserve correct NoteData
175
+ const wasmNotes = notes.map(note => {
176
+ if (!note.protoNote) {
177
+ throw new Error(
178
+ 'Note missing protoNote - cannot build transaction. RPC must provide full note data.'
179
+ );
180
+ }
181
+ return wasm.Note.fromProtobuf(note.protoNote);
182
+ });
183
+
184
+ // Create transaction builder with PKH digests (builder computes lock-roots)
185
+ // include_lock_data: false keeps note-data empty (0.5 NOCK fee component)
186
+ // Each note needs its own spend condition (array of conditions, one per note)
187
+ const spendConditions = Array.isArray(spendCondition)
188
+ ? spendCondition // Use provided array (one per note)
189
+ : notes.map(() => spendCondition); // Single condition applied to all notes
190
+
191
+ if (spendConditions.length !== notes.length) {
192
+ throw new Error(
193
+ `Spend condition count mismatch: ${spendConditions.length} conditions for ${notes.length} notes`
194
+ );
195
+ }
196
+
197
+ // New WASM API: constructor takes fee_per_word
198
+ const builder = new wasm.TxBuilder(BigInt(DEFAULT_FEE_PER_WORD));
199
+
200
+ // simpleSpend now takes fee_override (user-specified fee) instead of fee_per_word
201
+ builder.simpleSpend(
202
+ wasmNotes,
203
+ spendConditions,
204
+ new wasm.Digest(recipientPKH),
205
+ BigInt(amount), // gift
206
+ fee !== undefined ? BigInt(fee) : null, // fee_override (user-specified fee)
207
+ new wasm.Digest(refundPKH),
208
+ includeLockData
209
+ );
210
+
211
+ // Sign and validate the transaction
212
+ builder.sign(privateKey);
213
+ builder.validate();
214
+
215
+ // Get the fee before building (for return value)
216
+ const feeUsed = Number(builder.curFee());
217
+
218
+ // Build the final transaction
219
+ const nockchainTx = builder.build();
220
+
221
+ return {
222
+ txId: nockchainTx.id.value,
223
+ version: 1, // V1 only
224
+ nockchainTx,
225
+ feeUsed,
226
+ };
227
+ }
228
+
229
+ /**
230
+ * Create a simple payment transaction (single recipient)
231
+ *
232
+ * This is a convenience wrapper around buildTransaction for the common case
233
+ * of sending a payment to one recipient with change back to yourself.
234
+ *
235
+ * @param note - UTXO to spend
236
+ * @param recipientPKH - Recipient's PKH digest string
237
+ * @param amount - Amount to send in nicks
238
+ * @param senderPublicKey - Your public key (97 bytes, for creating spend condition)
239
+ * @param fee - Transaction fee in nicks
240
+ * @param privateKey - Your private key (32 bytes)
241
+ * @returns Constructed transaction
242
+ */
243
+ export async function buildPayment(
244
+ note: Note,
245
+ recipientPKH: string,
246
+ amount: number,
247
+ senderPublicKey: Uint8Array,
248
+ privateKey: Uint8Array,
249
+ fee?: number
250
+ ): Promise<ConstructedTransaction> {
251
+ // Initialize WASM
252
+ await initIrisSdkOnce();
253
+
254
+ const totalNeeded = amount + (fee || 0);
255
+
256
+ if (note.assets < totalNeeded) {
257
+ throw new Error(`Insufficient funds in note: have ${note.assets} nicks, need ${totalNeeded}`);
258
+ }
259
+
260
+ // Create sender's PKH digest string for change
261
+ const senderPKH = publicKeyToPKHDigest(senderPublicKey);
262
+
263
+ // Discover the correct spend condition by matching lock-root to note.nameFirst
264
+ // the spend condition MUST match what was locked on the note
265
+ const spendCondition = await discoverSpendConditionForNote(senderPKH, {
266
+ nameFirst: note.nameFirst,
267
+ originPage: note.originPage,
268
+ });
269
+
270
+ // Sanity check: verify the derived first-name matches
271
+ const derivedFirstName = spendCondition.firstName().value;
272
+ if (derivedFirstName !== note.nameFirst) {
273
+ throw new Error(
274
+ `First-name mismatch! Computed: ${derivedFirstName.slice(0, 20)}..., ` +
275
+ `Expected: ${note.nameFirst.slice(0, 20)}...`
276
+ );
277
+ }
278
+
279
+ return buildTransaction({
280
+ notes: [note],
281
+ spendCondition,
282
+ recipientPKH,
283
+ amount,
284
+ fee,
285
+ refundPKH: senderPKH,
286
+ privateKey,
287
+ // include_lock_data: false for lower fees (0.5 NOCK per word saved)
288
+ includeLockData: false,
289
+ });
290
+ }
291
+
292
+ /**
293
+ * Create a payment transaction using multiple notes (UTXOs)
294
+ *
295
+ * This allows spending from multiple UTXOs when a single UTXO doesn't have
296
+ * sufficient balance. The transaction will use all provided notes as inputs.
297
+ *
298
+ * @param notes - Array of UTXOs to spend
299
+ * @param recipientPKH - Recipient's PKH digest string
300
+ * @param amount - Amount to send in nicks
301
+ * @param senderPublicKey - Your public key (97 bytes, for creating spend condition)
302
+ * @param privateKey - Your private key (32 bytes)
303
+ * @param fee - Transaction fee in nicks (optional, WASM will auto-calculate if not provided)
304
+ * @param refundPKH - Override for change address (optional, defaults to sender's PKH).
305
+ * Set to recipientPKH for "send max" to sweep all funds to recipient.
306
+ * @returns Constructed transaction
307
+ */
308
+ export async function buildMultiNotePayment(
309
+ notes: Note[],
310
+ recipientPKH: string,
311
+ amount: number,
312
+ senderPublicKey: Uint8Array,
313
+ privateKey: Uint8Array,
314
+ fee?: number,
315
+ refundPKH?: string
316
+ ): Promise<ConstructedTransaction> {
317
+ // Initialize WASM
318
+ await initIrisSdkOnce();
319
+
320
+ if (notes.length === 0) {
321
+ throw new Error('At least one note is required');
322
+ }
323
+
324
+ // Calculate total available from all notes
325
+ const totalAvailable = notes.reduce((sum, note) => sum + note.assets, 0);
326
+ const totalNeeded = amount + (fee || 0);
327
+
328
+ if (totalAvailable < totalNeeded) {
329
+ throw new Error(
330
+ `Insufficient funds: have ${totalAvailable} nicks across ${notes.length} notes, need ${totalNeeded}`
331
+ );
332
+ }
333
+
334
+ // Create sender's PKH digest string for change
335
+ const senderPKH = publicKeyToPKHDigest(senderPublicKey);
336
+
337
+ // Discover the correct spend condition for each note
338
+ // Each note may have different spend conditions (e.g., some are coinbase with timelocks)
339
+ const spendConditions: wasm.SpendCondition[] = [];
340
+
341
+ for (let i = 0; i < notes.length; i++) {
342
+ const note = notes[i];
343
+ const spendCondition = await discoverSpendConditionForNote(senderPKH, {
344
+ nameFirst: note.nameFirst,
345
+ originPage: note.originPage,
346
+ });
347
+
348
+ // Sanity check: verify the derived first-name matches
349
+ const derivedFirstName = spendCondition.firstName().value;
350
+ if (derivedFirstName !== note.nameFirst) {
351
+ throw new Error(
352
+ `First-name mismatch for note ${i}! Computed: ${derivedFirstName.slice(0, 20)}..., ` +
353
+ `Expected: ${note.nameFirst.slice(0, 20)}...`
354
+ );
355
+ }
356
+
357
+ spendConditions.push(spendCondition);
358
+ }
359
+
360
+ // Use provided refundPKH or default to sender's PKH
361
+ // For "send max", refundPKH = recipientPKH to sweep all funds to recipient
362
+ const changeAddress = refundPKH ?? senderPKH;
363
+
364
+ // Build transaction with all notes and their individual spend conditions
365
+ return buildTransaction({
366
+ notes,
367
+ spendCondition: spendConditions, // Array of spend conditions (one per note)
368
+ recipientPKH,
369
+ amount,
370
+ fee,
371
+ refundPKH: changeAddress,
372
+ privateKey,
373
+ // include_lock_data: false for lower fees (0.5 NOCK per word saved)
374
+ includeLockData: false,
375
+ });
376
+ }
377
+
378
+ /**
379
+ * Create a spend condition for a single public key
380
+ * Helper function for the common case
381
+ *
382
+ * @param publicKey - The 97-byte public key
383
+ * @returns SpendCondition for this public key
384
+ */
385
+ export async function createSinglePKHSpendCondition(
386
+ publicKey: Uint8Array
387
+ ): Promise<wasm.SpendCondition> {
388
+ await initIrisSdkOnce();
389
+
390
+ const pkhDigest = publicKeyToPKHDigest(publicKey);
391
+ const pkh = wasm.Pkh.single(pkhDigest);
392
+ return wasm.SpendCondition.newPkh(pkh);
393
+ }
394
+
395
+ /**
396
+ * Calculate the note data hash for a given spend condition
397
+ * This is needed when converting legacy notes to new format
398
+ *
399
+ * @param spendCondition - The spend condition
400
+ * @returns The note data hash as 40-byte digest
401
+ */
402
+ export async function calculateNoteDataHash(
403
+ spendCondition: wasm.SpendCondition
404
+ ): Promise<Uint8Array> {
405
+ await initIrisSdkOnce();
406
+
407
+ const hashDigest = spendCondition.hash();
408
+ // The digest value is already a base58 string, decode it to bytes
409
+ return base58.decode(hashDigest.value);
410
+ }
411
+
412
+ /**
413
+ * Estimate transaction size in bytes (for fee estimation)
414
+ * This is a rough estimate - actual size depends on serialization format
415
+ *
416
+ * @param inputCount - Number of inputs
417
+ * @param outputCount - Number of outputs
418
+ * @returns Estimated size in bytes
419
+ */
420
+ export function estimateTransactionSize(inputCount: number, outputCount: number): number {
421
+ // Rough estimates based on typical sizes:
422
+ // - Each input: ~200 bytes (note data + signature)
423
+ // - Each output: ~150 bytes (seed data)
424
+ // - Transaction overhead: ~100 bytes
425
+ return 100 + inputCount * 200 + outputCount * 150;
426
+ }
427
+
428
+ /**
429
+ * Calculate recommended fee based on transaction size
430
+ *
431
+ * @param inputCount - Number of inputs
432
+ * @param outputCount - Number of outputs
433
+ * @param feePerByte - Fee per byte in nicks (default: 1 nick/byte)
434
+ * @returns Recommended fee in nicks
435
+ */
436
+ export function calculateRecommendedFee(
437
+ inputCount: number,
438
+ outputCount: number,
439
+ feePerByte: number = 1
440
+ ): number {
441
+ const size = estimateTransactionSize(inputCount, outputCount);
442
+ return size * feePerByte;
443
+ }