@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.
- package/.github/workflows/artifacts.yml +33 -0
- package/.github/workflows/ci.yml +68 -0
- package/.github/workflows/publish-sdk.yml +35 -0
- package/.nvmrc +1 -0
- package/.prettierignore +5 -0
- package/.prettierrc +8 -0
- package/LICENSE +22 -0
- package/README.md +117 -0
- package/extension/background/index.ts +1500 -0
- package/extension/content/index.ts +59 -0
- package/extension/icons/rose.svg +27 -0
- package/extension/icons/rose128.png +0 -0
- package/extension/icons/rose16.png +0 -0
- package/extension/icons/rose256.png +0 -0
- package/extension/icons/rose32.png +0 -0
- package/extension/icons/rose48.png +0 -0
- package/extension/icons/rose512.png +0 -0
- package/extension/inpage/index.ts +86 -0
- package/extension/manifest.json +48 -0
- package/extension/popup/Popup.tsx +94 -0
- package/extension/popup/Router.tsx +121 -0
- package/extension/popup/assets/arrow-down-icon.svg +3 -0
- package/extension/popup/assets/arrow-left-icon.svg +3 -0
- package/extension/popup/assets/arrow-right-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-icon.svg +3 -0
- package/extension/popup/assets/arrow-up-right-icon.svg +3 -0
- package/extension/popup/assets/checkmark-icon.svg +3 -0
- package/extension/popup/assets/checkmark-pencil-icon.svg +3 -0
- package/extension/popup/assets/checkmark-success-icon.svg +3 -0
- package/extension/popup/assets/clock-icon.svg +3 -0
- package/extension/popup/assets/close-x-icon.svg +3 -0
- package/extension/popup/assets/copy-icon.svg +6 -0
- package/extension/popup/assets/explorer-icon.svg +3 -0
- package/extension/popup/assets/eye-off-icon.svg +3 -0
- package/extension/popup/assets/eye-open-icon.svg +4 -0
- package/extension/popup/assets/feedback-icon.svg +3 -0
- package/extension/popup/assets/green-status-dot.svg +3 -0
- package/extension/popup/assets/info-icon.svg +3 -0
- package/extension/popup/assets/iris-logo-40.svg +27 -0
- package/extension/popup/assets/iris-logo-96.svg +27 -0
- package/extension/popup/assets/iris-logo-blue.svg +27 -0
- package/extension/popup/assets/iris-logo-no-eye.svg +27 -0
- package/extension/popup/assets/iris-logo-orange.svg +27 -0
- package/extension/popup/assets/iris-logo.svg +27 -0
- package/extension/popup/assets/key-icon.svg +3 -0
- package/extension/popup/assets/lock-icon-yellow.svg +3 -0
- package/extension/popup/assets/lock-icon.svg +3 -0
- package/extension/popup/assets/pencil-edit-icon.svg +3 -0
- package/extension/popup/assets/permissions-icon.svg +3 -0
- package/extension/popup/assets/receipt-icon.svg +5 -0
- package/extension/popup/assets/refresh-icon.svg +3 -0
- package/extension/popup/assets/settings-gear-icon.svg +8 -0
- package/extension/popup/assets/settings-icon.svg +3 -0
- package/extension/popup/assets/theme-icon.svg +3 -0
- package/extension/popup/assets/trash-bin-icon.svg +3 -0
- package/extension/popup/assets/trend-down-arrow.svg +5 -0
- package/extension/popup/assets/trend-up-arrow.svg +5 -0
- package/extension/popup/assets/user-account-icon.svg +3 -0
- package/extension/popup/assets/vector-bottom-left.svg +9 -0
- package/extension/popup/assets/vector-left.svg +9 -0
- package/extension/popup/assets/vector-right.svg +9 -0
- package/extension/popup/assets/vector-top-right-rotated.svg +8 -0
- package/extension/popup/assets/vector-top-right.svg +9 -0
- package/extension/popup/assets/wallet-dropdown-arrow.svg +5 -0
- package/extension/popup/assets/wallet-icon-style-1.svg +6 -0
- package/extension/popup/assets/wallet-icon-style-10.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-11.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-12.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-13.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-14.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-15.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-2.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-3.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-4.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-5.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-6.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-7.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-8.svg +8 -0
- package/extension/popup/assets/wallet-icon-style-9.svg +8 -0
- package/extension/popup/components/AccountIcon.tsx +78 -0
- package/extension/popup/components/AccountSelector.tsx +246 -0
- package/extension/popup/components/Alert.tsx +48 -0
- package/extension/popup/components/ConfirmModal.tsx +81 -0
- package/extension/popup/components/PasswordInput.tsx +49 -0
- package/extension/popup/components/ScreenContainer.tsx +17 -0
- package/extension/popup/components/SiteIcon.tsx +60 -0
- package/extension/popup/components/ThemeToggle.tsx +44 -0
- package/extension/popup/components/icons/ArrowDownLeftIcon.tsx +20 -0
- package/extension/popup/components/icons/ArrowUpRightIcon.tsx +20 -0
- package/extension/popup/components/icons/CheckIcon.tsx +20 -0
- package/extension/popup/components/icons/ChevronDownIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronLeftIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronRightIcon.tsx +15 -0
- package/extension/popup/components/icons/ChevronUpIcon.tsx +15 -0
- package/extension/popup/components/icons/CloseIcon.tsx +26 -0
- package/extension/popup/components/icons/CopyIcon.tsx +20 -0
- package/extension/popup/components/icons/EditIcon.tsx +20 -0
- package/extension/popup/components/icons/EyeIcon.tsx +13 -0
- package/extension/popup/components/icons/EyeOffIcon.tsx +13 -0
- package/extension/popup/components/icons/InfoIcon.tsx +20 -0
- package/extension/popup/components/icons/LockIcon.tsx +20 -0
- package/extension/popup/components/icons/PlusIcon.tsx +15 -0
- package/extension/popup/components/icons/ReceiveArrowIcon.tsx +14 -0
- package/extension/popup/components/icons/ReceiveCircleIcon.tsx +20 -0
- package/extension/popup/components/icons/SendPaperPlaneIcon.tsx +18 -0
- package/extension/popup/components/icons/SentArrowIcon.tsx +21 -0
- package/extension/popup/components/icons/SettingsIcon.tsx +26 -0
- package/extension/popup/components/icons/ShieldIcon.tsx +20 -0
- package/extension/popup/components/icons/UploadIcon.tsx +20 -0
- package/extension/popup/components/icons/WalletIcon.tsx +20 -0
- package/extension/popup/contexts/ThemeContext.tsx +105 -0
- package/extension/popup/hooks/useApprovalDetection.ts +128 -0
- package/extension/popup/hooks/useAutoFocus.ts +36 -0
- package/extension/popup/hooks/useAutoRejectOnClose.ts +25 -0
- package/extension/popup/hooks/useClickOutside.ts +33 -0
- package/extension/popup/hooks/useCopyToClipboard.ts +33 -0
- package/extension/popup/hooks/useFavicon.ts +64 -0
- package/extension/popup/hooks/useNumericInput.ts +93 -0
- package/extension/popup/index.html +13 -0
- package/extension/popup/index.tsx +24 -0
- package/extension/popup/screens/AboutScreen.tsx +118 -0
- package/extension/popup/screens/HomeScreen.tailwind.css +85 -0
- package/extension/popup/screens/HomeScreen.tsx +902 -0
- package/extension/popup/screens/KeySettingsPasswordScreen.tsx +164 -0
- package/extension/popup/screens/LockTimeScreen.tsx +155 -0
- package/extension/popup/screens/ReceiveScreen.tsx +149 -0
- package/extension/popup/screens/RecoveryPhraseScreen.tsx +183 -0
- package/extension/popup/screens/SendReviewScreen.tsx +308 -0
- package/extension/popup/screens/SendScreen.tsx +825 -0
- package/extension/popup/screens/SendSubmittedScreen.tsx +193 -0
- package/extension/popup/screens/SettingsScreen.tsx +116 -0
- package/extension/popup/screens/ThemeSettingsScreen.tsx +107 -0
- package/extension/popup/screens/TransactionDetailsScreen.tsx +346 -0
- package/extension/popup/screens/ViewSecretPhraseScreen.tsx +212 -0
- package/extension/popup/screens/WalletPermissionsScreen.tsx +123 -0
- package/extension/popup/screens/WalletSettingsScreen.tsx +381 -0
- package/extension/popup/screens/WalletStylingScreen.tsx +306 -0
- package/extension/popup/screens/approvals/ConnectApprovalScreen.tsx +136 -0
- package/extension/popup/screens/approvals/SignMessageScreen.tsx +140 -0
- package/extension/popup/screens/approvals/SignRawTxScreen.tsx +320 -0
- package/extension/popup/screens/approvals/TransactionApprovalScreen.tsx +167 -0
- package/extension/popup/screens/onboarding/BackupScreen.tsx +254 -0
- package/extension/popup/screens/onboarding/CreateScreen.tsx +273 -0
- package/extension/popup/screens/onboarding/ImportScreen.tsx +676 -0
- package/extension/popup/screens/onboarding/ImportScreenV0.tsx +678 -0
- package/extension/popup/screens/onboarding/ImportSuccessScreen.tsx +236 -0
- package/extension/popup/screens/onboarding/ResumeBackupScreen.tsx +166 -0
- package/extension/popup/screens/onboarding/StartScreen.tsx +142 -0
- package/extension/popup/screens/onboarding/SuccessScreen.tsx +193 -0
- package/extension/popup/screens/onboarding/VerifyScreen.tsx +220 -0
- package/extension/popup/screens/system/LockedScreen.tsx +288 -0
- package/extension/popup/screens/transactions/ReceiveScreen.tsx +84 -0
- package/extension/popup/screens/transactions/SentScreen.tsx +138 -0
- package/extension/popup/store.ts +482 -0
- package/extension/popup/styles.css +246 -0
- package/extension/popup/utils/format.ts +58 -0
- package/extension/popup/utils/formatWalletError.ts +36 -0
- package/extension/popup/utils/memo.ts +299 -0
- package/extension/popup/utils/messaging.ts +16 -0
- package/extension/shared/address-encoding.ts +69 -0
- package/extension/shared/balance-query.ts +123 -0
- package/extension/shared/constants.ts +386 -0
- package/extension/shared/currency.ts +128 -0
- package/extension/shared/first-name-derivation.ts +128 -0
- package/extension/shared/keyfile.ts +58 -0
- package/extension/shared/onboarding.ts +78 -0
- package/extension/shared/price-api.ts +79 -0
- package/extension/shared/rpc-client-browser.ts +315 -0
- package/extension/shared/transaction-builder.ts +443 -0
- package/extension/shared/types.ts +450 -0
- package/extension/shared/utxo-diff.ts +212 -0
- package/extension/shared/utxo-store.ts +548 -0
- package/extension/shared/utxo-sync.ts +343 -0
- package/extension/shared/validators.ts +26 -0
- package/extension/shared/vault.ts +1580 -0
- package/extension/shared/wallet-crypto.ts +77 -0
- package/extension/shared/wasm-utils.ts +76 -0
- package/extension/shared/webcrypto.ts +67 -0
- package/extension/types/wasm.d.ts +13 -0
- package/package.json +39 -0
- package/postcss.config.js +6 -0
- package/rose-extension-dist.zip +0 -0
- package/sdk/README.md +88 -0
- package/sdk/examples/app.ts +166 -0
- package/sdk/examples/index.html +51 -0
- package/sdk/examples/tsconfig.json +15 -0
- package/sdk/examples/tx-builder.html +532 -0
- package/sdk/examples/tx-builder.ts +1766 -0
- package/sdk/package-lock.json +424 -0
- package/sdk/package.json +68 -0
- package/sdk/src/constants.ts +28 -0
- package/sdk/src/errors.ts +74 -0
- package/sdk/src/hooks/index.ts +1 -0
- package/sdk/src/hooks/use-rose.ts +94 -0
- package/sdk/src/index.ts +12 -0
- package/sdk/src/provider.ts +396 -0
- package/sdk/src/transaction.ts +163 -0
- package/sdk/src/types/rose-wasm.d.ts +14 -0
- package/sdk/src/types.ts +97 -0
- package/sdk/src/wasm.ts +13 -0
- package/sdk/tsconfig.json +20 -0
- package/sdk/vite.config.examples.ts +32 -0
- package/tailwind.config.ts +38 -0
- package/tsconfig.json +20 -0
- 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
|
+
}
|