From c57a4086fa8b608bc5b9d21b04ccd777363ff869 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:44:57 -0500 Subject: [PATCH 01/75] WASM version bump + updated examples --- examples/app.ts | 94 +++++----- examples/tx-builder.ts | 406 +++++++++++++++++------------------------ package-lock.json | 8 +- package.json | 2 +- src/index.ts | 2 + 5 files changed, 230 insertions(+), 282 deletions(-) diff --git a/examples/app.ts b/examples/app.ts index 2603022..bbe88a6 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -67,18 +67,9 @@ signRawTxBtn.onclick = async () => { log('Creating gRPC client for: ' + grpcEndpoint); const grpcClient = new wasm.GrpcClient(grpcEndpoint); - // 3. Create spend condition using wallet PKH (single, no timelock) - log('Creating spend condition for PKH: ' + walletPkh); - const pkh = wasm.Pkh.single(walletPkh); - const spendCondition = wasm.SpendCondition.newPkh(pkh); - - // 4. Get firstName from spend condition - const firstName = spendCondition.firstName(); - log('First name: ' + firstName.value.substring(0, 20) + '...'); - - // 5. Query notes matching this firstName + // 3. Query notes by address (0.2 API) log('Querying notes from gRPC...'); - const balance = await grpcClient.getBalanceByFirstName(firstName.value); + const balance = await grpcClient.getBalanceByAddress(walletPkh); if (!balance || !balance.notes || balance.notes.length === 0) { log('No notes found - wallet might be empty'); @@ -87,76 +78,95 @@ signRawTxBtn.onclick = async () => { log('Found ' + balance.notes.length + ' notes'); - // Convert notes from protobuf - const notes = balance.notes.map((n: any) => wasm.Note.fromProtobuf(n.note)); + // Convert notes from protobuf (0.2: free function) + const notes = balance.notes + .map((entry: any) => entry.note) + .filter(Boolean) + .map((noteProto: any) => wasm.note_from_protobuf(noteProto)); + + if (!notes.length) { + log('No parseable notes found'); + return; + } + const note = notes[0]; const noteAssets = note.assets; log('Using note with ' + noteAssets + ' nicks'); - // 6. Build transaction (send 10 NOCK = 655360 nicks) - const TEN_NOCK_IN_NICKS = BigInt(10 * 65536); - const feePerWord = BigInt(32768); // 0.5 NOCK per word + // 4. Build transaction (send 10 NOCK = 655360 nicks) + const TEN_NOCK_IN_NICKS = String(10 * 65536); + const feePerWord = '32768'; // 0.5 NOCK per word log('Building transaction to send 10 NOCK...'); - const builder = new wasm.TxBuilder(feePerWord); - - // Create recipient digest - const recipientDigest = new wasm.Digest(recipient); + const builder = new wasm.TxBuilder({ + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: feePerWord, + witness_word_div: 1, + }); - // Create refund digest (same as wallet PKH) - const refundDigest = new wasm.Digest(walletPkh); + // 0.2 spend condition type: LockPrimitive[] + const spendCondition = [{ Pkh: { m: 1, hashes: [walletPkh] } }]; - // Use simpleSpend (no lockData for lower fees) + // Use simpleSpend (no lockData for lower fees), digest values are strings in 0.2 builder.simpleSpend( - [notes[0]], + [note], [spendCondition], - recipientDigest, + recipient, TEN_NOCK_IN_NICKS, null, // fee_override (let it auto-calculate) - refundDigest, + walletPkh, false // include_lock_data ); - // 7. Build the transaction and get notes/spend conditions + // 5. Build the transaction and get notes/spend conditions log('Building raw transaction...'); const nockchainTx = builder.build(); const txId = nockchainTx.id; - log('Transaction ID: ' + txId.value); - - const rawTxProtobuf = nockchainTx.toRawTx().toProtobuf(); + log('Transaction ID: ' + txId); // Get notes and spend conditions from builder const txNotes = builder.allNotes(); log('Notes count: ' + txNotes.notes.length); - log('Spend conditions count: ' + txNotes.spendConditions.length); + log('Spend conditions count: ' + txNotes.spend_conditions.length); - // 8. Sign using provider.signRawTx (pass wasm objects directly) + // 0.2 raw tx is plain data + const rawTx = { + version: 1 as const, + id: nockchainTx.id, + spends: nockchainTx.spends, + }; + + // 6. Sign using provider.signRawTx log('Signing transaction...'); const signedTxProtobuf = await provider.signRawTx({ - rawTx: rawTxProtobuf, // Pass wasm RawTx directly - notes: txNotes.notes, // Pass wasm Note objects directly - spendConditions: txNotes.spendConditions, // Pass wasm SpendCondition objects directly + rawTx, + notes: txNotes.notes, + spendConditions: txNotes.spend_conditions, }); log('Transaction signed successfully!'); - // Convert to jam string for file download - const signedTx = wasm.RawTx.fromProtobuf(signedTxProtobuf); - const jamBytes = signedTx.toJam(); - - // 9. Download to file using transaction ID + // 7. Convert signed tx to Jam and download + const signedTxProto = + typeof signedTxProtobuf === 'object' && !(signedTxProtobuf instanceof Uint8Array) + ? signedTxProtobuf + : (signedTxProtobuf as unknown as wasm.PbCom2RawTransaction); + const signedRawTx = wasm.rawTxFromProtobuf(signedTxProto); + const jamBytes = wasm.jam(signedRawTx as unknown as wasm.Noun); const blob = new Blob([new Uint8Array(jamBytes)], { type: 'application/jam' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; - a.download = `${txId.value}.tx`; + a.download = `${txId}.tx`; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); - log('Downloaded transaction to file: ' + txId.value + '.tx'); + log('Downloaded signed transaction (Jam): ' + txId + '.tx'); } catch (e: any) { log('Error: ' + e.message); console.error(e); diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 18c3d16..ae42057 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -1,17 +1,18 @@ import { NockchainProvider, wasm } from '../src/index'; // ===== Types ===== +// Note: Nicks are strings in current WASM version, so they need to be cast into integers for arithmetic interface Lock { id: string; name: string; - spendConditionProtobuf: Uint8Array; // Store protobuf instead of WASM object + spendCondition: wasm.SpendCondition; // plain array, e.g. [{ Pkh: { m: 1, hashes: [pkh] } }] expanded: boolean; } interface NoteData { note: wasm.Note; - assets: bigint; + assets: wasm.Nicks; firstName: string; lastName: string; } @@ -24,13 +25,13 @@ interface SelectedInput { interface Seed { lockId: string; - amount: bigint; // in nicks + amount: wasm.Nicks; } interface Spend { inputId: string; input: SelectedInput; - fee: bigint; // in nicks + fee: wasm.Nicks; seeds: Seed[]; } @@ -104,12 +105,12 @@ function truncateAddress(addr: string | undefined): string { return `${addr.slice(0, 4)}...${addr.slice(-4)}`; } -function nicksToNock(nicks: bigint): number { - return Number(nicks) / 65536; +function nicksToNock(nicks: wasm.Nicks): number { + return Number(BigInt(nicks)) / 65536; } -function nockToNicks(nock: number): bigint { - return BigInt(Math.floor(nock * 65536)); +function nockToNicks(nock: number): wasm.Nicks { + return String(BigInt(Math.floor(nock * 65536))); } function formatNock(nock: number): string { @@ -253,33 +254,32 @@ function createDefaultLocksWithPkh() { const coinbaseDefaultExists = state.locks.some(l => l.id === 'coinbase-default'); if (!pkhDefaultExists) { - // 1. PKH lock - const pkhLock = wasm.Pkh.single(state.walletPkh); - const pkhSpendCondition = wasm.SpendCondition.newPkh(pkhLock); + // Simple 1-of-1 PKH lock + const pkhPrim: wasm.LockPrimitive = { Pkh: { m: 1, hashes: [state.walletPkh] } }; + const spendCondition: wasm.SpendCondition = [pkhPrim]; state.locks.push({ id: 'pkh-default', name: 'Wallet PKH', - spendConditionProtobuf: pkhSpendCondition.toProtobuf(), + spendCondition, expanded: false, }); - pkhSpendCondition.free(); } if (!coinbaseDefaultExists) { - // 2. Coinbase lock (PKH + timelock) - const coinbaseLock = wasm.Pkh.single(state.walletPkh); - const timelockCoinbase = wasm.LockTim.coinbase(); - const coinbaseSpendCondition = new wasm.SpendCondition([ - wasm.LockPrimitive.newPkh(coinbaseLock), - wasm.LockPrimitive.newTim(timelockCoinbase), - ]); + // Coinbase lock (PKH + timelock) + const pkhPrim: wasm.LockPrimitive = { Pkh: { m: 1, hashes: [state.walletPkh] } }; + const coinbaseTim: wasm.LockTim = { + rel: { min: 100, max: undefined }, + abs: { min: undefined, max: undefined }, + }; + const timPrim: wasm.LockPrimitive = { Tim: coinbaseTim }; + const spendCondition: wasm.SpendCondition = [pkhPrim, timPrim]; state.locks.push({ id: 'coinbase-default', name: 'Wallet Coinbase', - spendConditionProtobuf: coinbaseSpendCondition.toProtobuf(), + spendCondition, expanded: false, }); - coinbaseSpendCondition.free(); } } @@ -327,15 +327,11 @@ async function refreshNotesForLock(lockId: string) { if (!lock || !state.grpcClient) return; try { - // Deserialize spend condition from protobuf to get firstName - const spendCondition = wasm.SpendCondition.fromProtobuf(lock.spendConditionProtobuf); - const firstName = spendCondition.firstName(); - console.log( - `Fetching notes for lock ${lockId}, firstName: ${firstName.value.substring(0, 20)}...` - ); + // 0.2: first name = hash of spend condition (Digest string) + const firstName = wasm.spendConditionHash(lock.spendCondition); + console.log(`Fetching notes for lock ${lockId}, firstName: ${firstName.substring(0, 20)}...`); - const balance = await state.grpcClient.getBalanceByFirstName(firstName.value); - spendCondition.free(); // Clean up + const balance = await state.grpcClient.getBalanceByFirstName(firstName); if (!balance || !balance.notes || balance.notes.length === 0) { state.notes.set(lockId, []); @@ -343,27 +339,21 @@ async function refreshNotesForLock(lockId: string) { return; } - const notes: NoteData[] = ( - balance.notes as Array<{ note: any; firstName?: string; lastName?: string }> - ).map(n => { - const note = wasm.Note.fromProtobuf(n.note); - - // Try to extract name from protobuf if top-level fields are empty - let firstName = n.firstName || ''; - let lastName = n.lastName || ''; - - if (!firstName && n.note?.note_version?.V1?.name) { - firstName = n.note.note_version.V1.name.first || ''; - lastName = n.note.note_version.V1.name.last || ''; - } - - return { - note, - assets: note.assets, - firstName, - lastName, - }; - }); + const notes: NoteData[] = balance.notes + .map((entry: wasm.PbCom2BalanceEntry) => entry.note) + .filter((note): note is wasm.PbCom2Note => note != null) + .map((noteProto: wasm.PbCom2Note) => { + const note = wasm.note_from_protobuf(noteProto); + const assets = note.assets != null ? String(note.assets) : '0'; + const firstNameStr = note.name?.first ?? ''; + const lastNameStr = note.name?.last ?? ''; + return { + note, + assets, + firstName: firstNameStr, + lastName: lastNameStr, + }; + }); state.notes.set(lockId, notes); renderNotesForLock(lockId); @@ -434,7 +424,7 @@ function addInputToSpend(lockId: string, noteIndex: number) { const spend: Spend = { inputId: input.id, input, - fee: BigInt(0), + fee: '0', seeds: [], }; @@ -507,18 +497,20 @@ function renderSeeds(spend: Spend): string { } function isSpendBalanced(spend: Spend): boolean { - const total = spend.fee + spend.seeds.reduce((sum, seed) => sum + seed.amount, BigInt(0)); - return total === spend.input.note.assets; + const total = + BigInt(spend.fee) + spend.seeds.reduce((sum, seed) => sum + BigInt(seed.amount), BigInt(0)); + return total === BigInt(spend.input.note.assets); } function getSpendBalanceText(spend: Spend): string { - const total = spend.fee + spend.seeds.reduce((sum, seed) => sum + seed.amount, BigInt(0)); - const diff = spend.input.note.assets - total; + const total = + BigInt(spend.fee) + spend.seeds.reduce((sum, seed) => sum + BigInt(seed.amount), BigInt(0)); + const diff = BigInt(spend.input.note.assets) - total; if (diff === BigInt(0)) { - return `✓ Balanced (${formatNock(nicksToNock(total))} NOCK)`; + return `✓ Balanced (${formatNock(nicksToNock(String(total)))} NOCK)`; } - const sign = diff > 0 ? '+' : ''; - return `${sign}${formatNock(nicksToNock(diff))} NOCK`; + const sign = diff > 0n ? '+' : ''; + return `${sign}${formatNock(nicksToNock(String(diff)))} NOCK`; } function removeSpend(inputId: string) { @@ -544,17 +536,16 @@ function balanceSeed(inputId: string, seedIndex: number) { const seed = spend.seeds[seedIndex]; if (!seed) return; - // Calculate remaining assets: input assets - fee - other seeds - const inputAssets = spend.input.note.assets; - const fee = spend.fee; + const inputAssets = BigInt(spend.input.note.assets); + const fee = BigInt(spend.fee); let otherSeedsTotal = BigInt(0); for (let i = 0; i < spend.seeds.length; i++) { if (i !== seedIndex) { - otherSeedsTotal += spend.seeds[i].amount; + otherSeedsTotal += BigInt(spend.seeds[i].amount); } } - + // Calculate remaining assets const remaining = inputAssets - fee - otherSeedsTotal; if (remaining < 0n) { @@ -562,7 +553,7 @@ function balanceSeed(inputId: string, seedIndex: number) { return; } - seed.amount = remaining; + seed.amount = String(remaining); renderSpends(); updateBuilder(); } @@ -574,9 +565,9 @@ function updateSpendFee(inputId: string, feeStr: string, shouldRender: boolean = // Convert NOCK to nicks const nock = parseFloat(feeStr); if (!isNaN(nock) && nock >= 0) { - spend.fee = BigInt(Math.floor(nock * 65536)); + spend.fee = nockToNicks(nock); } else { - spend.fee = BigInt(0); + spend.fee = '0'; } if (shouldRender) renderSpends(); updateBuilder(); @@ -591,7 +582,7 @@ function addSeed(inputId: string) { if (spend && state.locks.length > 0) { spend.seeds.push({ lockId: state.locks[0].id, - amount: BigInt(0), + amount: '0', }); renderSpends(); updateBuilder(); @@ -619,9 +610,9 @@ function updateSeedAmount( // Convert NOCK to nicks const nock = parseFloat(amountStr); if (!isNaN(nock) && nock >= 0) { - spend.seeds[seedIndex].amount = BigInt(Math.floor(nock * 65536)); + spend.seeds[seedIndex].amount = nockToNicks(nock); } else { - spend.seeds[seedIndex].amount = BigInt(0); + spend.seeds[seedIndex].amount = '0'; } if (shouldRender) renderSpends(); updateBuilder(); @@ -660,60 +651,49 @@ function updateBuilder() { try { console.log('Building transaction...'); - // Create TxBuilder with default fee-per-word - const feePerWord = BigInt(32768); // 0.5 NOCK per word - const builder = new wasm.TxBuilder(feePerWord); + const txSettings: wasm.TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: '32768', + witness_word_div: 1, + }; + const builder = new wasm.TxBuilder(txSettings); - // Add each spend to the builder using SpendBuilder for (const spend of state.spends) { const lock = state.locks.find(l => l.id === spend.input.lockId); if (!lock) continue; - // Determine refund lock protobuf (use first seed lock if available, otherwise same as input) - const refundLockProtobuf = + const refundLock: wasm.SpendCondition | null = spend.seeds.length > 0 - ? state.locks.find(l => l.id === spend.seeds[0].lockId)?.spendConditionProtobuf - : lock.spendConditionProtobuf; + ? (state.locks.find(l => l.id === spend.seeds[0].lockId)?.spendCondition ?? null) + : lock.spendCondition; - // Deserialize WASM objects from protobuf (fresh instances every time) - const noteClone = wasm.Note.fromProtobuf(spend.input.note.note.toProtobuf()); - const spendConditionClone = wasm.SpendCondition.fromProtobuf(lock.spendConditionProtobuf); - const refundLockClone = refundLockProtobuf - ? wasm.SpendCondition.fromProtobuf(refundLockProtobuf) - : null; - - // Create SpendBuilder with note, spend condition, and refund lock - const spendBuilder = new wasm.SpendBuilder(noteClone, spendConditionClone, refundLockClone); + const spendBuilder = new wasm.SpendBuilder( + spend.input.note.note, + lock.spendCondition, + refundLock + ); - // Add seeds (outputs) for (const seed of spend.seeds) { const seedLock = state.locks.find(l => l.id === seed.lockId); if (seedLock) { - // Deserialize spend condition from protobuf - const seedSpendCondition = wasm.SpendCondition.fromProtobuf( - seedLock.spendConditionProtobuf - ); - // Create a Seed for this output using the constructor - const seedObj = new wasm.Seed( - null, // output_source - wasm.LockRoot.fromSpendCondition(seedSpendCondition), // lock_root - seed.amount, // gift - wasm.NoteData.empty(), // note_data - spend.input.note.note.hash() // parent_hash - ); - spendBuilder.seed(seedObj); + const seedV1: wasm.SeedV1 = { + output_source: undefined, + lock_root: { Lock: seedLock.spendCondition }, + note_data: [], + gift: seed.amount, + parent_hash: wasm.note_hash(spend.input.note.note), + }; + spendBuilder.seed(seedV1); } } - // Set fee if specified - if (spend.fee > 0) { + if (BigInt(spend.fee) > 0n) { spendBuilder.fee(spend.fee); } - // Compute refund to balance out the spend spendBuilder.computeRefund(false); - - // Attach this spend to the main builder builder.spend(spendBuilder); } @@ -942,8 +922,10 @@ function renderTransaction() { } } - const fee = state.builder.curFee(); - const calcFee = state.builder.calcFee(); + const feeStr = state.builder.curFee(); + const calcFeeStr = state.builder.calcFee(); + const fee = BigInt(feeStr); + const calcFee = BigInt(calcFeeStr); const feeSufficient = fee >= calcFee; // If validate() failed, it might be due to fee or missing unlocks. @@ -961,32 +943,30 @@ function renderTransaction() { txInfo.innerHTML = `
- Fee: ${formatNock(nicksToNock(fee))} NOCK + Fee: ${formatNock(nicksToNock(String(fee)))} NOCK
- Calculated Fee: ${formatNock(nicksToNock(calcFee))} NOCK + Calculated Fee: ${formatNock(nicksToNock(String(calcFee)))} NOCK
- TX ID: ${renderCopyableId(state.nockchainTx.id.value, 'TX ID')} + TX ID: ${renderCopyableId(state.nockchainTx.id, 'TX ID')}
`; - // Outputs - const outputs = state.nockchainTx.outputs(); + // Outputs: 0.2 use nockchainTxToRaw + rawTxOutputs (maybe doesn't work) + const rawTx = wasm.nockchainTxToRaw(state.nockchainTx); + const outputs = wasm.rawTxOutputs(rawTx); if (outputs && outputs.length > 0) { outputsList.innerHTML = outputs .map((output: wasm.Note, index: number) => { - const amount = output.assets || BigInt(0); - - // Extract name directly from WASM object - const firstName = output.name?.first || ''; - const lastName = output.name?.last || ''; - + const amount = output.assets != null ? BigInt(output.assets) : BigInt(0); + const firstName = output.name?.first ?? ''; + const lastName = output.name?.last ?? ''; return `
- Output ${index + 1}: ${formatNock(nicksToNock(amount))} NOCK + Output ${index + 1}: ${formatNock(nicksToNock(String(amount)))} NOCK
${firstName ? renderNoteName(firstName, lastName) : 'Unknown'} @@ -1419,109 +1399,64 @@ function confirmAddLock() { } try { - // Build lock primitives - const primitives: wasm.LockPrimitive[] = []; + const primitives: wasm.SpendCondition = []; for (const prim of modalPrimitives) { switch (prim.type) { - case 'pkh': + case 'pkh': { const pkhConfig = prim.pkh || { m: 1, addrs: [] }; const validAddrs = pkhConfig.addrs.filter(a => a.trim() !== ''); - if (validAddrs.length === 0) { alert('PKH requires at least one address'); return; } - - // For now, if m=1 and 1 addr, use single. - // If m>1 or multiple addrs, we need multisig support in WASM or manual construction - // Assuming WASM has Pkh.new(m, addrs) or similar. - // Checking iris_wasm.d.ts... - // It seems Pkh.single(addr) is what we used. - // Let's assume for now we only support single if m=1 and len=1. - // Actually, let's try to find if there is a multisig constructor. - // If not, we might be limited. But user asked for it. - // Let's assume Pkh constructor takes (m, addrs) or similar if we look at the types. - // Based on previous knowledge, Pkh might be complex. - // Let's try to use Pkh.new(threshold, keys) if it exists, otherwise fallback/error. - - // I will assume for this step that I can create a Pkh object. - // If I look at how `wasm.Pkh.single` works, it probably creates a Pkh with threshold 1. - - // Let's try: - // const pkh = new wasm.Pkh(pkhConfig.m, pkhConfig.addrs); - // If that fails, I'll see in build. - // Actually, looking at previous code: `wasm.Pkh.single(state.walletPkh)` - - if (validAddrs.length === 1 && pkhConfig.m === 1) { - const pkh = wasm.Pkh.single(validAddrs[0]); - primitives.push(wasm.LockPrimitive.newPkh(pkh)); - } else { - try { - const pkh = new wasm.Pkh(BigInt(pkhConfig.m), validAddrs); - primitives.push(wasm.LockPrimitive.newPkh(pkh)); - } catch (e) { - console.error('Failed to create multisig PKH', e); - alert('Failed to create multisig PKH: ' + (e as Error).message); - return; - } - } + primitives.push({ Pkh: { m: pkhConfig.m, hashes: validAddrs } }); break; - - case 'tim': + } + case 'tim': { const timConfig = prim.tim || { type: 'csv', value: 1 }; - let tim; + const value = timConfig.value ?? 0; if (timConfig.type === 'csv') { - // Relative timelock (CSV) - // min=value, max=null for relative part - const rel = new wasm.TimelockRange(BigInt(timConfig.value), null); - const abs = new wasm.TimelockRange(null, null); - tim = new wasm.LockTim(rel, abs); + primitives.push({ + Tim: { + rel: { min: value, max: undefined }, + abs: { min: undefined, max: undefined }, + }, + }); } else { - // Absolute timelock (CLTV) - // min=value, max=null for absolute part - const rel = new wasm.TimelockRange(null, null); - const abs = new wasm.TimelockRange(BigInt(timConfig.value), null); - tim = new wasm.LockTim(rel, abs); + primitives.push({ + Tim: { + rel: { min: undefined, max: undefined }, + abs: { min: value, max: undefined }, + }, + }); } - primitives.push(wasm.LockPrimitive.newTim(tim)); break; - - case 'hax': + } + case 'hax': { const haxConfig = prim.hax || { hashes: [] }; const validHashes = haxConfig.hashes.filter(h => h.trim() !== ''); - if (validHashes.length === 0) { - // Allow empty for "any preimage"? User prompt said "leave blank to require any preimage" - // But HAX usually requires at least one hash unless it's a specific "any" type? - // If hashes is empty, maybe we don't add it? Or add empty Hax? - // `new wasm.Hax([])` might work. alert('HAX requires at least one hash.'); return; } - - const digests = validHashes.map(h => new wasm.Digest(h)); - const hax = new wasm.Hax(digests); - primitives.push(wasm.LockPrimitive.newHax(hax)); + primitives.push({ Hax: validHashes }); break; - + } case 'brn': - primitives.push(wasm.LockPrimitive.newBrn()); + primitives.push('Brn'); break; } } - const spendCondition = new wasm.SpendCondition(primitives); - const newLock: Lock = { id: generateId(), name, - spendConditionProtobuf: spendCondition.toProtobuf(), + spendCondition: primitives, expanded: false, }; state.locks.push(newLock); - spendCondition.free(); // Clean up WASM object renderLocks(); closeAddLockModal(); } catch (e) { @@ -1537,7 +1472,7 @@ function exportLocks() { const locksData = state.locks.map(lock => ({ id: lock.id, name: lock.name, - spendCondition: lock.spendConditionProtobuf, // Already protobuf + spendCondition: lock.spendCondition, // 0.2: plain array })); const json = JSON.stringify(locksData, null, 2); @@ -1578,32 +1513,23 @@ function importLocks() { let importedCount = 0; for (const lockData of locksData) { - // Validate it's indeed a spend condition by deserializing - const spendCondition = wasm.SpendCondition.fromProtobuf(lockData.spendCondition); - const hash = spendCondition.hash().value; - - // Check for duplicates by hash - const exists = state.locks.some(l => { - const existingSc = wasm.SpendCondition.fromProtobuf(l.spendConditionProtobuf); - const existingHash = existingSc.hash().value; - existingSc.free(); - return existingHash === hash; - }); - + const sc = lockData.spendCondition; + if (!Array.isArray(sc) || sc.length === 0) { + console.log('Skipping invalid lock (expected spendCondition array):', lockData.name); + continue; + } + const hash = wasm.spendConditionHash(sc as wasm.SpendCondition); + const exists = state.locks.some(l => wasm.spendConditionHash(l.spendCondition) === hash); if (exists) { console.log(`Skipping duplicate lock: ${lockData.name} (${hash})`); - spendCondition.free(); continue; } - - const lock: Lock = { + state.locks.push({ id: lockData.id || generateId(), name: lockData.name || 'Imported Lock', - spendConditionProtobuf: lockData.spendCondition, // Store as protobuf + spendCondition: sc as wasm.SpendCondition, expanded: false, - }; - state.locks.push(lock); - spendCondition.free(); + }); importedCount++; } @@ -1640,8 +1566,9 @@ downloadTxBtn.onclick = () => { if (!state.nockchainTx) return; try { - const jamBytes = state.nockchainTx.toJam(); - const txId = state.nockchainTx.id.value; + const rawTx = wasm.nockchainTxToRaw(state.nockchainTx); + const jamBytes = wasm.jam(rawTx as unknown as wasm.Noun); + const txId = state.nockchainTx.id; const blob = new Blob([new Uint8Array(jamBytes)], { type: 'application/jam' }); const url = URL.createObjectURL(blob); @@ -1664,31 +1591,40 @@ signTxBtn.onclick = async () => { if (!state.nockchainTx || !state.builder || !state.provider) return; try { - // Get all notes and spend conditions from builder const txNotes = state.builder.allNotes(); + const rawTx = wasm.nockchainTxToRaw(state.nockchainTx) as wasm.RawTxV1; + const rawTxProto = wasm.rawTxToProtobuf(rawTx); + const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.note_to_protobuf(n)); - // Sign using provider const signedTxProtobuf = await state.provider.signRawTx({ - rawTx: state.nockchainTx.toRawTx(), - notes: txNotes.notes, - spendConditions: txNotes.spendConditions, + rawTx: rawTxProto, + notes: notesProto, + spendConditions: txNotes.spend_conditions, }); - // Store signed TX - // NockchainTx doesn't have fromProtobuf, so we go via RawTx - const signedRawTx = wasm.RawTx.fromProtobuf(signedTxProtobuf); - state.signedTx = signedRawTx.toNockchainTx(); + const signedTxProtoObj = + typeof signedTxProtobuf === 'object' && !(signedTxProtobuf instanceof Uint8Array) + ? signedTxProtobuf + : (signedTxProtobuf as unknown as wasm.PbCom2RawTransaction); + const signedRawTx = wasm.rawTxFromProtobuf(signedTxProtoObj) as wasm.RawTxV1; + state.signedTx = wasm.rawTxToNockchainTx(signedRawTx); - // Validate the signed transaction - console.log('Validating signed transaction...'); let isValid = true; let validationError = ''; try { + const txSettings: wasm.TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: '32768', + witness_word_div: 1, + }; const signedBuilder = wasm.TxBuilder.fromTx( - state.signedTx.toRawTx(), + signedRawTx, txNotes.notes, - txNotes.spendConditions + txNotes.spend_conditions, + txSettings ); signedBuilder.validate(); signedBuilder.free(); @@ -1698,15 +1634,13 @@ signTxBtn.onclick = async () => { isValid = false; validationError = e instanceof Error ? e.message : String(e); } - - console.log(state.signedTx.toRawTx().toProtobuf()); if (state.signedTx) { - state.signedTxId = state.signedTx.id.value; - // Show signed TX section + state.signedTxId = state.signedTx.id; signedTxSection.classList.remove('hidden'); - - // Render TX ID - signedTxIdEl.innerHTML = renderCopyableId(state.signedTxId, 'Signed TX ID'); + signedTxIdEl.innerHTML = renderCopyableId( + state.signedTxId ?? state.signedTx.id, + 'Signed TX ID' + ); // Render Validation Status const signedTxValidation = document.getElementById('signedTxValidation'); @@ -1738,7 +1672,8 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => { if (!state.signedTx || !state.signedTxId) return; try { - const jamBytes = state.signedTx.toJam(); + const rawTx = wasm.nockchainTxToRaw(state.signedTx); + const jamBytes = wasm.jam(rawTx as unknown as wasm.Noun); const blob = new Blob([new Uint8Array(jamBytes)], { type: 'application/jam' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -1749,7 +1684,8 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => { document.body.removeChild(a); URL.revokeObjectURL(url); - const rawTxBytes = state.signedTx.toRawTx().toJam(); + const rawTxForJam = wasm.nockchainTxToRaw(state.signedTx); + const rawTxBytes = wasm.jam(rawTxForJam as unknown as wasm.Noun); const blobRaw = new Blob([new Uint8Array(rawTxBytes)], { type: 'application/jam' }); const urlRaw = URL.createObjectURL(blobRaw); const aRaw = document.createElement('a'); @@ -1771,8 +1707,8 @@ document.getElementById('submitSignedTxBtn')!.onclick = async () => { try { if (!state.grpcClient) throw new Error('gRPC client not initialized'); - // Convert to protobuf for sending - const txProtobuf = state.signedTx.toRawTx().toProtobuf(); + const rawTx = wasm.nockchainTxToRaw(state.signedTx) as wasm.RawTxV1; + const txProtobuf = wasm.rawTxToProtobuf(rawTx); await state.grpcClient.sendTransaction(txProtobuf); console.log('Transaction submitted successfully!'); diff --git a/package-lock.json b/package-lock.json index cd670e6..0fc228a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.1", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "^0.1.1", + "@nockbox/iris-wasm": "0.2.0-alpha.2", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.1.1.tgz", - "integrity": "sha512-uLtRNl6serdk8Ysu5BN38NCs2vWWKsvvfTmT54CFtk4ebOsjIOcg1ViBeeUVpikToTgTlijtMYYEpjs2KIBLHg==", + "version": "0.2.0-alpha.2", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.2.tgz", + "integrity": "sha512-5fG6h/RwDuAFfQ0JThgQvLIu9Jf9P9fmg0LimWpOkCaHvNSVIWielvXVyHHrZRExI7vGgVAAwCWCnbSSQcd2cQ==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { diff --git a/package.json b/package.json index 40910ed..6ce8f85 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,6 @@ }, "dependencies": { "@scure/base": "^2.0.0", - "@nockbox/iris-wasm": "^0.1.1" + "@nockbox/iris-wasm": "0.2.0-alpha.2" } } diff --git a/src/index.ts b/src/index.ts index dd6f7fe..99de07c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,6 @@ export * from './provider.js'; export * from './transaction.js'; export * from './errors.js'; export * from './constants.js'; +export * from './bridge.js'; +export * from './migration.js'; export * as wasm from './wasm.js'; From d19184ed5c7edda15c068c972700d0730a2e0492 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:47:54 -0500 Subject: [PATCH 02/75] hotfix for modules not relevant to this PR --- src/index.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/index.ts b/src/index.ts index 99de07c..dd6f7fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,4 @@ export * from './provider.js'; export * from './transaction.js'; export * from './errors.js'; export * from './constants.js'; -export * from './bridge.js'; -export * from './migration.js'; export * as wasm from './wasm.js'; From 03c6a1d7d65009ff1907d7a4ed82062aa6a1b14c Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:23:11 -0500 Subject: [PATCH 03/75] version bump again + new export style --- package-lock.json | 8 ++++---- package.json | 8 ++++++-- src/wasm.ts | 8 ++++++++ 3 files changed, 18 insertions(+), 6 deletions(-) diff --git a/package-lock.json b/package-lock.json index 0fc228a..f6135c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.1", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "0.2.0-alpha.2", + "@nockbox/iris-wasm": "^0.2.0-alpha.3", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.2", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.2.tgz", - "integrity": "sha512-5fG6h/RwDuAFfQ0JThgQvLIu9Jf9P9fmg0LimWpOkCaHvNSVIWielvXVyHHrZRExI7vGgVAAwCWCnbSSQcd2cQ==", + "version": "0.2.0-alpha.3", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.3.tgz", + "integrity": "sha512-ADXzyn0ewm08/rF8hlBogU0LrIhYiDFVQp0EmUmzYh/Ah80lUTpG/j6nlvwEXRKR2O5jfMOChugtKZIyTujpqw==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { diff --git a/package.json b/package.json index 6ce8f85..440dfc7 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,10 @@ ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js" + }, + "./wasm": { + "types": "./dist/wasm.d.ts", + "import": "./dist/wasm.js" } }, "files": [ @@ -46,7 +50,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@scure/base": "^2.0.0", - "@nockbox/iris-wasm": "0.2.0-alpha.2" + "@nockbox/iris-wasm": "^0.2.0-alpha.3", + "@scure/base": "^2.0.0" } } diff --git a/src/wasm.ts b/src/wasm.ts index 12313e9..971cad3 100644 --- a/src/wasm.ts +++ b/src/wasm.ts @@ -4,3 +4,11 @@ export * from '@nockbox/iris-wasm/iris_wasm.js'; export { default } from '@nockbox/iris-wasm/iris_wasm.js'; + +import init from '@nockbox/iris-wasm/iris_wasm.js'; + +/** + * Canonical initializer re-export for SDK consumers. + * Prefer using this instead of importing iris-wasm directly. + */ +export const initWasm = init; From 2c1f8a8402f4b35a47f7c6a9d2937a34532d24a3 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 1 Mar 2026 03:26:03 -0500 Subject: [PATCH 04/75] got rid of undefined --- examples/tx-builder.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index ae42057..52b5af7 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -269,8 +269,8 @@ function createDefaultLocksWithPkh() { // Coinbase lock (PKH + timelock) const pkhPrim: wasm.LockPrimitive = { Pkh: { m: 1, hashes: [state.walletPkh] } }; const coinbaseTim: wasm.LockTim = { - rel: { min: 100, max: undefined }, - abs: { min: undefined, max: undefined }, + rel: { min: 100, max: null }, + abs: { min: null, max: null }, }; const timPrim: wasm.LockPrimitive = { Tim: coinbaseTim }; const spendCondition: wasm.SpendCondition = [pkhPrim, timPrim]; @@ -679,7 +679,7 @@ function updateBuilder() { const seedLock = state.locks.find(l => l.id === seed.lockId); if (seedLock) { const seedV1: wasm.SeedV1 = { - output_source: undefined, + output_source: null, lock_root: { Lock: seedLock.spendCondition }, note_data: [], gift: seed.amount, @@ -1419,15 +1419,15 @@ function confirmAddLock() { if (timConfig.type === 'csv') { primitives.push({ Tim: { - rel: { min: value, max: undefined }, - abs: { min: undefined, max: undefined }, + rel: { min: value, max: null }, + abs: { min: null, max: null }, }, }); } else { primitives.push({ Tim: { - rel: { min: undefined, max: undefined }, - abs: { min: value, max: undefined }, + rel: { min: null, max: null }, + abs: { min: value, max: null }, }, }); } From cd56208067c7578f7e7e7a631cae1eee01a4d350 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 1 Mar 2026 12:51:58 -0500 Subject: [PATCH 05/75] compatibility for Nicks in all potential formats --- src/compat.ts | 62 ++++++++++++++++++++++++++++++++++++++++++++++ src/index.ts | 2 ++ src/provider.ts | 5 +++- src/transaction.ts | 18 +++++++++++--- src/types.ts | 10 +++++--- 5 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 src/compat.ts diff --git a/src/compat.ts b/src/compat.ts new file mode 100644 index 0000000..fd9c4a3 --- /dev/null +++ b/src/compat.ts @@ -0,0 +1,62 @@ +/** + * Backward-compatibility helpers for SDK request payloads. + * Accepts legacy numeric forms and normalizes to canonical Nicks strings. + */ + +import type { Transaction, NicksLike } from './types.js'; + +/** + * Canonical transaction payload expected by migrated wallets. + */ +export interface CanonicalTransaction { + to: string; + amount: string; + fee?: string; +} + +function assertIntegerString(value: string, field: 'amount' | 'fee'): string { + const trimmed = value.trim(); + if (!/^-?\d+$/.test(trimmed)) { + throw new Error(`Invalid ${field}: expected an integer-like value`); + } + return trimmed; +} + +/** + * Parse legacy number/bigint/string nicks into canonical string format. + */ +export function parseNicksLike(value: NicksLike, field: 'amount' | 'fee'): string { + if (typeof value === 'bigint') { + return value.toString(); + } + + if (typeof value === 'number') { + if (!Number.isFinite(value) || !Number.isInteger(value)) { + throw new Error(`Invalid ${field}: expected a finite integer number`); + } + return String(value); + } + + if (typeof value === 'string') { + return assertIntegerString(value, field); + } + + throw new Error(`Invalid ${field}: unsupported value type`); +} + +/** + * Normalize SDK transaction input into canonical payload: + * - amount/fee become integer strings (Nicks) + * - optional fee remains optional + */ +export function normalizeTransaction(transaction: Transaction): CanonicalTransaction { + const amount = parseNicksLike(transaction.amount, 'amount'); + const fee = transaction.fee === undefined ? undefined : parseNicksLike(transaction.fee, 'fee'); + + return { + to: transaction.to, + amount, + ...(fee !== undefined ? { fee } : {}), + }; +} + diff --git a/src/index.ts b/src/index.ts index dd6f7fe..f9e0d03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,4 +8,6 @@ export * from './provider.js'; export * from './transaction.js'; export * from './errors.js'; export * from './constants.js'; +export * from './compat.js'; export * as wasm from './wasm.js'; +export { initWasm } from './wasm.js'; diff --git a/src/provider.ts b/src/provider.ts index cb3ee83..93ed32f 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -6,6 +6,7 @@ import type { Transaction, NockchainEvent, EventListener, InjectedNockchain } fr import { TransactionBuilder } from './transaction.js'; import { WalletNotInstalledError, UserRejectedError, RpcError, NoAccountError } from './errors.js'; import { PROVIDER_METHODS } from './constants.js'; +import { normalizeTransaction } from './compat.js'; /** * NockchainProvider class - Main interface for dApps to interact with Iris wallet @@ -115,9 +116,11 @@ export class NockchainProvider { throw new NoAccountError(); } + const normalizedTx = normalizeTransaction(transaction); + return this.request({ method: PROVIDER_METHODS.SEND_TRANSACTION, - params: [transaction], + params: [normalizedTx], }); } diff --git a/src/transaction.ts b/src/transaction.ts index 58b8ab8..bbfed3d 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -5,6 +5,7 @@ import { base58 } from '@scure/base'; import type { Transaction } from './types.js'; import { InvalidAddressError, InvalidTransactionError } from './errors.js'; +import { parseNicksLike } from './compat.js'; /** * Conversion rate: 1 NOCK = 65,536 nicks (2^16) @@ -151,11 +152,20 @@ export class TransactionBuilder { * @throws {InvalidTransactionError} If the transaction amount or fee is invalid */ static fromTransaction(tx: Transaction): TransactionBuilder { - // Use setters to ensure validation is applied - let builder = new TransactionBuilder().to(tx.to).amount(tx.amount); + const amountNumber = Number(parseNicksLike(tx.amount, 'amount')); + if (!Number.isSafeInteger(amountNumber) || amountNumber < MIN_AMOUNT) { + throw new InvalidTransactionError('Amount is outside safe integer range'); + } - if (typeof tx.fee === 'number') { - builder = builder.fee(tx.fee); + // Use setters to ensure validation is applied + let builder = new TransactionBuilder().to(tx.to).amount(amountNumber); + + if (tx.fee !== undefined) { + const feeNumber = Number(parseNicksLike(tx.fee, 'fee')); + if (!Number.isSafeInteger(feeNumber) || feeNumber < 0) { + throw new InvalidTransactionError('Fee is outside safe integer range'); + } + builder = builder.fee(feeNumber); } return builder; diff --git a/src/types.ts b/src/types.ts index 72544f2..3cea7e4 100644 --- a/src/types.ts +++ b/src/types.ts @@ -5,13 +5,15 @@ /** * Transaction object representing a Nockchain transaction */ +export type NicksLike = number | string | bigint; + export interface Transaction { /** Recipient address (base58-encoded public key hash / PKH) */ to: string; - /** Amount to send in nicks (1 NOCK = 65,536 nicks) */ - amount: number; - /** Transaction fee in nicks (optional, defaults to 32,768 = 0.5 NOCK) */ - fee?: number; + /** Amount to send in nicks (legacy number + canonical string/bigint accepted) */ + amount: NicksLike; + /** Transaction fee in nicks (legacy number + canonical string/bigint accepted) */ + fee?: NicksLike; } /** From ecc60e4c48d4d475a22e1fdc3eb37eb417d835ed Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:04:08 -0500 Subject: [PATCH 06/75] add support for native and legacy signtx --- src/compat.ts | 109 +++++++++++++++++++++++++++++++++++++++++------- src/provider.ts | 55 ++++++++++-------------- 2 files changed, 115 insertions(+), 49 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index fd9c4a3..0e9ec86 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -1,19 +1,70 @@ /** * Backward-compatibility helpers for SDK request payloads. - * Accepts legacy numeric forms and normalizes to canonical Nicks strings. */ import type { Transaction, NicksLike } from './types.js'; +import type { + PbCom2RawTransaction, + PbCom2Note, + PbCom2SpendCondition, + RawTx, + Note, + SpendCondition, +} from '@nockbox/iris-wasm/iris_wasm.js'; +import * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; -/** - * Canonical transaction payload expected by migrated wallets. - */ -export interface CanonicalTransaction { +/** Simple send payload: to, amount, fee as nicks strings (for sendTransaction). */ +export interface CanonicalSendPayload { to: string; amount: string; fee?: string; } +/** + * Normalize simple send input: amount/fee to nicks strings. + */ +export function normalizeSendTransaction(transaction: Transaction): CanonicalSendPayload { + const amount = parseNicksLike(transaction.amount, 'amount'); + const fee = transaction.fee === undefined ? undefined : parseNicksLike(transaction.fee, 'fee'); + + return { + to: transaction.to, + amount, + ...(fee !== undefined ? { fee } : {}), + }; +} + +/** Protobuf signRawTx payload (gRPC wire format). */ +export interface LegacySignRawTxRequest { + rawTx: PbCom2RawTransaction; + notes: PbCom2Note[]; + spendConditions: PbCom2SpendCondition[]; +} + +/** Native signRawTx payload. */ +export interface SignRawTxRequest { + rawTx: RawTx; + notes: Note[]; + spendConditions: SpendCondition[]; +} + +/** SignRawTx params: native or protobuf. */ +export type SignRawTxParams = SignRawTxRequest | LegacySignRawTxRequest; + +export function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRequest { + if (!obj || typeof obj !== 'object') return false; + const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; + return ( + guard.isPbCom2RawTransaction(p.rawTx) && + Array.isArray(p.notes) && + p.notes.length > 0 && + p.notes.every((n: unknown) => guard.isPbCom2Note(n)) && + Array.isArray(p.spendConditions) && + p.spendConditions.length > 0 && + p.spendConditions.every((sc: unknown) => guard.isPbCom2SpendCondition(sc)) + ); +} + function assertIntegerString(value: string, field: 'amount' | 'fee'): string { const trimmed = value.trim(); if (!/^-?\d+$/.test(trimmed)) { @@ -44,19 +95,45 @@ export function parseNicksLike(value: NicksLike, field: 'amount' | 'fee'): strin throw new Error(`Invalid ${field}: unsupported value type`); } +function toProtobufIfNeeded(v: unknown): unknown { + if (v && typeof v === 'object' && typeof (v as { toProtobuf?: () => unknown }).toProtobuf === 'function') { + return (v as { toProtobuf: () => unknown }).toProtobuf(); + } + return v; +} + /** - * Normalize SDK transaction input into canonical payload: - * - amount/fee become integer strings (Nicks) - * - optional fee remains optional + * Validate signRawTx params. Accepts all-native or all-protobuf (no mixing). */ -export function normalizeTransaction(transaction: Transaction): CanonicalTransaction { - const amount = parseNicksLike(transaction.amount, 'amount'); - const fee = transaction.fee === undefined ? undefined : parseNicksLike(transaction.fee, 'fee'); +export function normalizeSignRawTxParams(params: { + rawTx: unknown; + notes: unknown[]; + spendConditions: unknown[]; +}): SignRawTxParams { + const rawTx = toProtobufIfNeeded(params.rawTx); + const notes = params.notes.map(toProtobufIfNeeded); + const spendConditions = params.spendConditions.map(toProtobufIfNeeded); - return { - to: transaction.to, - amount, - ...(fee !== undefined ? { fee } : {}), - }; + if (!Array.isArray(notes) || notes.length === 0 || !Array.isArray(spendConditions) || spendConditions.length === 0) { + throw new Error('Invalid signRawTx params: notes and spendConditions must be non-empty arrays'); + } + + const rawTxNative = guard.isRawTx(rawTx); + const rawTxProtobuf = guard.isPbCom2RawTransaction(rawTx); + const allNotesNative = notes.every(n => guard.isNote(n)); + const allNotesProtobuf = notes.every(n => guard.isPbCom2Note(n)); + const allScNative = spendConditions.every(sc => guard.isSpendCondition(sc)); + const allScProtobuf = spendConditions.every(sc => guard.isPbCom2SpendCondition(sc)); + + const validNative = rawTxNative && allNotesNative && allScNative; + const validProtobuf = rawTxProtobuf && allNotesProtobuf && allScProtobuf; + + if (!validNative && !validProtobuf) { + throw new Error( + 'Invalid signRawTx params: expected all-native (RawTx, Note[], SpendCondition[]) or all-protobuf (PbCom2*), no mixing' + ); + } + + return { rawTx, notes, spendConditions } as SignRawTxParams; } diff --git a/src/provider.ts b/src/provider.ts index 93ed32f..5b8fe29 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -6,7 +6,11 @@ import type { Transaction, NockchainEvent, EventListener, InjectedNockchain } fr import { TransactionBuilder } from './transaction.js'; import { WalletNotInstalledError, UserRejectedError, RpcError, NoAccountError } from './errors.js'; import { PROVIDER_METHODS } from './constants.js'; -import { normalizeTransaction } from './compat.js'; +import { + normalizeSignRawTxParams, + normalizeSendTransaction, + type SignRawTxParams, +} from './compat.js'; /** * NockchainProvider class - Main interface for dApps to interact with Iris wallet @@ -116,7 +120,7 @@ export class NockchainProvider { throw new NoAccountError(); } - const normalizedTx = normalizeTransaction(transaction); + const normalizedTx = normalizeSendTransaction(transaction); return this.request({ method: PROVIDER_METHODS.SEND_TRANSACTION, @@ -145,7 +149,7 @@ export class NockchainProvider { /** * Sign a raw transaction - * Accepts either wasm objects (with toProtobuf() method) or protobuf JS objects + * Accepts native (RawTx, Note[], SpendCondition[]) or protobuf (PbCom2*) format. * @param params - The transaction parameters (rawTx, notes, spendConditions) * @returns Promise resolving to the signed raw transaction as protobuf Uint8Array * @throws {NoAccountError} If no account is connected @@ -154,53 +158,38 @@ export class NockchainProvider { * * @example * ```typescript - * // Option 1: Pass wasm objects directly (auto-converts to protobuf) - * const rawTx = builder.build(); + * // Option 1: Pass native WASM types (preferred) + * const rawTx = wasm.nockchainTxToRaw(builder.build()); * const txNotes = builder.allNotes(); * * const signedTx = await provider.signRawTx({ - * rawTx: rawTx, // wasm RawTx object - * notes: txNotes.notes, // array of wasm Note objects - * spendConditions: txNotes.spendConditions // array of wasm SpendCondition objects + * rawTx, + * notes: txNotes.notes, + * spendConditions: txNotes.spend_conditions, * }); * - * // Option 2: Pass protobuf JS objects directly + * // Option 2: Pass protobuf JS objects * const signedTx = await provider.signRawTx({ - * rawTx: rawTxProtobufObject, // protobuf JS object - * notes: noteProtobufObjects, // array of protobuf JS objects - * spendConditions: spendCondProtobufObjects // array of protobuf JS objects + * rawTx: rawTxProtobufObject, + * notes: noteProtobufObjects, + * spendConditions: spendCondProtobufObjects, * }); * ``` */ - async signRawTx(params: { - rawTx: any; - notes: any[]; - spendConditions: any[]; + async signRawTx(params: SignRawTxParams | { + rawTx: unknown; + notes: unknown[]; + spendConditions: unknown[]; }): Promise { if (!this.isConnected) { throw new NoAccountError(); } - // Helper to convert to protobuf if it's a wasm object - const toProtobuf = (obj: any): any => { - // If object has toProtobuf method, it's a wasm object - convert it - if (obj && typeof obj.toProtobuf === 'function') { - return obj.toProtobuf(); - } - // Otherwise assume it's already a protobuf JS object - return obj; - }; - - // Convert wasm objects to protobuf (if needed) - const protobufParams = { - rawTx: toProtobuf(params.rawTx), - notes: params.notes.map(toProtobuf), - spendConditions: params.spendConditions.map(toProtobuf), - }; + const validatedParams = normalizeSignRawTxParams(params); return this.request({ method: PROVIDER_METHODS.SIGN_RAW_TX, - params: [protobufParams], + params: [validatedParams], }); } From 22d176603aec8fa990fb219af725ee3d21b9f4b4 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 15:10:24 -0500 Subject: [PATCH 07/75] concrete type for legacy transaction signing --- src/compat.ts | 14 +++++++++++--- src/provider.ts | 14 +++++++++----- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index 0e9ec86..d0ed392 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -96,7 +96,11 @@ export function parseNicksLike(value: NicksLike, field: 'amount' | 'fee'): strin } function toProtobufIfNeeded(v: unknown): unknown { - if (v && typeof v === 'object' && typeof (v as { toProtobuf?: () => unknown }).toProtobuf === 'function') { + if ( + v && + typeof v === 'object' && + typeof (v as { toProtobuf?: () => unknown }).toProtobuf === 'function' + ) { return (v as { toProtobuf: () => unknown }).toProtobuf(); } return v; @@ -114,7 +118,12 @@ export function normalizeSignRawTxParams(params: { const notes = params.notes.map(toProtobufIfNeeded); const spendConditions = params.spendConditions.map(toProtobufIfNeeded); - if (!Array.isArray(notes) || notes.length === 0 || !Array.isArray(spendConditions) || spendConditions.length === 0) { + if ( + !Array.isArray(notes) || + notes.length === 0 || + !Array.isArray(spendConditions) || + spendConditions.length === 0 + ) { throw new Error('Invalid signRawTx params: notes and spendConditions must be non-empty arrays'); } @@ -136,4 +145,3 @@ export function normalizeSignRawTxParams(params: { return { rawTx, notes, spendConditions } as SignRawTxParams; } - diff --git a/src/provider.ts b/src/provider.ts index 5b8fe29..1c6c783 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -176,11 +176,15 @@ export class NockchainProvider { * }); * ``` */ - async signRawTx(params: SignRawTxParams | { - rawTx: unknown; - notes: unknown[]; - spendConditions: unknown[]; - }): Promise { + async signRawTx( + params: + | SignRawTxParams + | { + rawTx: unknown; + notes: unknown[]; + spendConditions: unknown[]; + } + ): Promise { if (!this.isConnected) { throw new NoAccountError(); } From 199f26ad3772e8a0f8f266d3541e24e5a77b992b Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:14:25 -0500 Subject: [PATCH 08/75] fix examples --- examples/app.ts | 34 +++++++++++++++++----------------- examples/tx-builder.ts | 5 ++++- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/examples/app.ts b/examples/app.ts index bbe88a6..1a196d9 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -67,9 +67,11 @@ signRawTxBtn.onclick = async () => { log('Creating gRPC client for: ' + grpcEndpoint); const grpcClient = new wasm.GrpcClient(grpcEndpoint); - // 3. Query notes by address (0.2 API) - log('Querying notes from gRPC...'); - const balance = await grpcClient.getBalanceByAddress(walletPkh); + // 3. Derive first-name from PKH and query notes (notes are indexed by first-name, not address) + const spendCondition = [{ Pkh: { m: 1, hashes: [walletPkh] } }] as wasm.SpendCondition; + const firstName = wasm.spendConditionFirstName(spendCondition); + log('Querying notes by first-name...'); + const balance = await grpcClient.getBalanceByFirstName(firstName); if (!balance || !balance.notes || balance.notes.length === 0) { log('No notes found - wallet might be empty'); @@ -106,9 +108,6 @@ signRawTxBtn.onclick = async () => { witness_word_div: 1, }); - // 0.2 spend condition type: LockPrimitive[] - const spendCondition = [{ Pkh: { m: 1, hashes: [walletPkh] } }]; - // Use simpleSpend (no lockData for lower fees), digest values are strings in 0.2 builder.simpleSpend( [note], @@ -132,24 +131,25 @@ signRawTxBtn.onclick = async () => { log('Notes count: ' + txNotes.notes.length); log('Spend conditions count: ' + txNotes.spend_conditions.length); - // 0.2 raw tx is plain data - const rawTx = { - version: 1 as const, - id: nockchainTx.id, - spends: nockchainTx.spends, - }; + // 6. Convert to protobuf (API boundary requires protobuf) + const rawTx = wasm.nockchainTxToRaw(nockchainTx) as wasm.RawTxV1; + const rawTxProto = wasm.rawTxToProtobuf(rawTx); + const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.note_to_protobuf(n)); + const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => + wasm.spendConditionToProtobuf(sc) + ); - // 6. Sign using provider.signRawTx + // 7. Sign using provider.signRawTx log('Signing transaction...'); const signedTxProtobuf = await provider.signRawTx({ - rawTx, - notes: txNotes.notes, - spendConditions: txNotes.spend_conditions, + rawTx: rawTxProto, + notes: notesProto, + spendConditions: spendCondProto, }); log('Transaction signed successfully!'); - // 7. Convert signed tx to Jam and download + // 8. Convert signed tx to Jam and download const signedTxProto = typeof signedTxProtobuf === 'object' && !(signedTxProtobuf instanceof Uint8Array) ? signedTxProtobuf diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 52b5af7..80d0d4b 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -1595,11 +1595,14 @@ signTxBtn.onclick = async () => { const rawTx = wasm.nockchainTxToRaw(state.nockchainTx) as wasm.RawTxV1; const rawTxProto = wasm.rawTxToProtobuf(rawTx); const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.note_to_protobuf(n)); + const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => + wasm.spendConditionToProtobuf(sc) + ); const signedTxProtobuf = await state.provider.signRawTx({ rawTx: rawTxProto, notes: notesProto, - spendConditions: txNotes.spend_conditions, + spendConditions: spendCondProto, }); const signedTxProtoObj = From 34c9e70bb4054c2d77b2b0e5ce99852ce2aaea30 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 23:26:05 -0500 Subject: [PATCH 09/75] fixes according to comments, added guard export --- package.json | 4 ++ src/compat.ts | 86 +++++++++++++++++------------------------- src/iris_wasm.guard.ts | 5 +++ src/provider.ts | 33 +++++----------- 4 files changed, 53 insertions(+), 75 deletions(-) create mode 100644 src/iris_wasm.guard.ts diff --git a/package.json b/package.json index 440dfc7..5bb965d 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,10 @@ "./wasm": { "types": "./dist/wasm.d.ts", "import": "./dist/wasm.js" + }, + "./iris_wasm.guard": { + "types": "./dist/iris_wasm.guard.d.ts", + "import": "./dist/iris_wasm.guard.js" } }, "files": [ diff --git a/src/compat.ts b/src/compat.ts index d0ed392..2e088d5 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -11,7 +11,7 @@ import type { Note, SpendCondition, } from '@nockbox/iris-wasm/iris_wasm.js'; -import * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; +import * as guard from './iris_wasm.guard.js'; /** Simple send payload: to, amount, fee as nicks strings (for sendTransaction). */ export interface CanonicalSendPayload { @@ -34,23 +34,30 @@ export function normalizeSendTransaction(transaction: Transaction): CanonicalSen }; } -/** Protobuf signRawTx payload (gRPC wire format). */ +/** Protobuf signRawTx payload (gRPC wire format). Only format supported at API boundary. */ export interface LegacySignRawTxRequest { + /** Raw transaction protobuf */ rawTx: PbCom2RawTransaction; + /** Input notes (protobuf) */ notes: PbCom2Note[]; + /** Spend conditions (protobuf) */ spendConditions: PbCom2SpendCondition[]; } -/** Native signRawTx payload. */ -export interface SignRawTxRequest { +/** Params for signRawTx. Protobuf only for now (native RawTx not supported at RPC boundary. */ +export type SignRawTxParams = LegacySignRawTxRequest; + +/** Native signRawTx payload (RawTx, Note[], SpendCondition[]). Used internally after protobuf conversion. */ +export interface NativeSignRawTxRequest { + /** Raw transaction (native) */ rawTx: RawTx; + /** Input notes (native) */ notes: Note[]; + /** Spend conditions (native) */ spendConditions: SpendCondition[]; } -/** SignRawTx params: native or protobuf. */ -export type SignRawTxParams = SignRawTxRequest | LegacySignRawTxRequest; - +/** Type guard: validates protobuf signRawTx payload. Use at API boundary (SDK + extension). */ export function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRequest { if (!obj || typeof obj !== 'object') return false; const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; @@ -65,6 +72,21 @@ export function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRe ); } +/** Type guard: validates native signRawTx payload. Use internally after protobuf→native conversion. */ +export function isNativeSignRawTxPayload(obj: unknown): obj is NativeSignRawTxRequest { + if (!obj || typeof obj !== 'object') return false; + const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; + return ( + guard.isRawTx(p.rawTx) && + Array.isArray(p.notes) && + p.notes.length > 0 && + p.notes.every((n: unknown) => guard.isNote(n)) && + Array.isArray(p.spendConditions) && + p.spendConditions.length > 0 && + p.spendConditions.every((sc: unknown) => guard.isSpendCondition(sc)) + ); +} + function assertIntegerString(value: string, field: 'amount' | 'fee'): string { const trimmed = value.trim(); if (!/^-?\d+$/.test(trimmed)) { @@ -95,53 +117,15 @@ export function parseNicksLike(value: NicksLike, field: 'amount' | 'fee'): strin throw new Error(`Invalid ${field}: unsupported value type`); } -function toProtobufIfNeeded(v: unknown): unknown { - if ( - v && - typeof v === 'object' && - typeof (v as { toProtobuf?: () => unknown }).toProtobuf === 'function' - ) { - return (v as { toProtobuf: () => unknown }).toProtobuf(); - } - return v; -} - /** - * Validate signRawTx params. Accepts all-native or all-protobuf (no mixing). + * Validate signRawTx params. Input must be protobuf (LegacySignRawTxRequest). + * RPC sends only LegacySignRawTxRequest. Native (SignRawTxRequest) not supported. */ -export function normalizeSignRawTxParams(params: { - rawTx: unknown; - notes: unknown[]; - spendConditions: unknown[]; -}): SignRawTxParams { - const rawTx = toProtobufIfNeeded(params.rawTx); - const notes = params.notes.map(toProtobufIfNeeded); - const spendConditions = params.spendConditions.map(toProtobufIfNeeded); - - if ( - !Array.isArray(notes) || - notes.length === 0 || - !Array.isArray(spendConditions) || - spendConditions.length === 0 - ) { - throw new Error('Invalid signRawTx params: notes and spendConditions must be non-empty arrays'); - } - - const rawTxNative = guard.isRawTx(rawTx); - const rawTxProtobuf = guard.isPbCom2RawTransaction(rawTx); - const allNotesNative = notes.every(n => guard.isNote(n)); - const allNotesProtobuf = notes.every(n => guard.isPbCom2Note(n)); - const allScNative = spendConditions.every(sc => guard.isSpendCondition(sc)); - const allScProtobuf = spendConditions.every(sc => guard.isPbCom2SpendCondition(sc)); - - const validNative = rawTxNative && allNotesNative && allScNative; - const validProtobuf = rawTxProtobuf && allNotesProtobuf && allScProtobuf; - - if (!validNative && !validProtobuf) { +export function normalizeSignRawTxParams(params: SignRawTxParams): SignRawTxParams { + if (!isLegacySignRawTxRequest(params)) { throw new Error( - 'Invalid signRawTx params: expected all-native (RawTx, Note[], SpendCondition[]) or all-protobuf (PbCom2*), no mixing' + 'Invalid signRawTx params: expected protobuf (PbCom2RawTransaction, PbCom2Note[], PbCom2SpendCondition[])' ); } - - return { rawTx, notes, spendConditions } as SignRawTxParams; + return params; } diff --git a/src/iris_wasm.guard.ts b/src/iris_wasm.guard.ts new file mode 100644 index 0000000..cc8c397 --- /dev/null +++ b/src/iris_wasm.guard.ts @@ -0,0 +1,5 @@ +/** + * Re-export iris-wasm type guards for extension/consumers. + * Use this instead of importing from @nockbox/iris-wasm directly. + */ +export * from '@nockbox/iris-wasm/iris_wasm.guard'; diff --git a/src/provider.ts b/src/provider.ts index 1c6c783..f841319 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -149,8 +149,8 @@ export class NockchainProvider { /** * Sign a raw transaction - * Accepts native (RawTx, Note[], SpendCondition[]) or protobuf (PbCom2*) format. - * @param params - The transaction parameters (rawTx, notes, spendConditions) + * Input must be protobuf (PbCom2RawTransaction, PbCom2Note[], PbCom2SpendCondition[]). + * @param params - The transaction parameters in protobuf format * @returns Promise resolving to the signed raw transaction as protobuf Uint8Array * @throws {NoAccountError} If no account is connected * @throws {UserRejectedError} If the user rejects the signing request @@ -158,33 +158,18 @@ export class NockchainProvider { * * @example * ```typescript - * // Option 1: Pass native WASM types (preferred) - * const rawTx = wasm.nockchainTxToRaw(builder.build()); - * const txNotes = builder.allNotes(); + * const rawTxProto = wasm.rawTxToProtobuf(rawTx); + * const notesProto = notes.map(n => wasm.note_to_protobuf(n)); + * const spendCondProto = spendConditions.map(sc => wasm.spendConditionToProtobuf(sc)); * * const signedTx = await provider.signRawTx({ - * rawTx, - * notes: txNotes.notes, - * spendConditions: txNotes.spend_conditions, - * }); - * - * // Option 2: Pass protobuf JS objects - * const signedTx = await provider.signRawTx({ - * rawTx: rawTxProtobufObject, - * notes: noteProtobufObjects, - * spendConditions: spendCondProtobufObjects, + * rawTx: rawTxProto, + * notes: notesProto, + * spendConditions: spendCondProto, * }); * ``` */ - async signRawTx( - params: - | SignRawTxParams - | { - rawTx: unknown; - notes: unknown[]; - spendConditions: unknown[]; - } - ): Promise { + async signRawTx(params: SignRawTxParams): Promise { if (!this.isConnected) { throw new NoAccountError(); } From 1a806bf7dac207fdb9e112d61fad2b91edaa8387 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 5 Mar 2026 13:50:58 +0000 Subject: [PATCH 10/75] Update package.lock --- package-lock.json | 9 +++++---- package.json | 2 +- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package-lock.json b/package-lock.json index f6135c2..9201a6f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.1.1", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.3", + "@nockbox/iris-wasm": "^0.2.0-alpha.4", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.3", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.3.tgz", - "integrity": "sha512-ADXzyn0ewm08/rF8hlBogU0LrIhYiDFVQp0EmUmzYh/Ah80lUTpG/j6nlvwEXRKR2O5jfMOChugtKZIyTujpqw==", + "version": "0.2.0-alpha.4", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.4.tgz", + "integrity": "sha512-V1fdky5lp1F01/2Yvca4dsP0hWi0JwbqV8uyzCqk1dpMRbz3w+ByR6tfKQoUzsgsubYWAhs5Am+UxtmH7G0QDw==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -746,6 +746,7 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } diff --git a/package.json b/package.json index 5bb965d..c23bf21 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.3", + "@nockbox/iris-wasm": "^0.2.0-alpha.4", "@scure/base": "^2.0.0" } } From f6b193540b383ab656eb9e7e77201b0932a25a85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 5 Mar 2026 14:01:26 +0000 Subject: [PATCH 11/75] Fix examples --- examples/app.ts | 6 +++--- examples/tx-builder.ts | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/examples/app.ts b/examples/app.ts index 1a196d9..2f179f8 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -84,7 +84,7 @@ signRawTxBtn.onclick = async () => { const notes = balance.notes .map((entry: any) => entry.note) .filter(Boolean) - .map((noteProto: any) => wasm.note_from_protobuf(noteProto)); + .map((noteProto: any) => wasm.noteFromProtobuf(noteProto)); if (!notes.length) { log('No parseable notes found'); @@ -132,9 +132,9 @@ signRawTxBtn.onclick = async () => { log('Spend conditions count: ' + txNotes.spend_conditions.length); // 6. Convert to protobuf (API boundary requires protobuf) - const rawTx = wasm.nockchainTxToRaw(nockchainTx) as wasm.RawTxV1; + const rawTx = wasm.nockchainTxToRawTx(nockchainTx); const rawTxProto = wasm.rawTxToProtobuf(rawTx); - const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.note_to_protobuf(n)); + const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.noteToProtobuf(n)); const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => wasm.spendConditionToProtobuf(sc) ); diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 80d0d4b..40c5044 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -343,7 +343,7 @@ async function refreshNotesForLock(lockId: string) { .map((entry: wasm.PbCom2BalanceEntry) => entry.note) .filter((note): note is wasm.PbCom2Note => note != null) .map((noteProto: wasm.PbCom2Note) => { - const note = wasm.note_from_protobuf(noteProto); + const note = wasm.noteFromProtobuf(noteProto); const assets = note.assets != null ? String(note.assets) : '0'; const firstNameStr = note.name?.first ?? ''; const lastNameStr = note.name?.last ?? ''; @@ -683,7 +683,7 @@ function updateBuilder() { lock_root: { Lock: seedLock.spendCondition }, note_data: [], gift: seed.amount, - parent_hash: wasm.note_hash(spend.input.note.note), + parent_hash: wasm.noteHash(spend.input.note.note), }; spendBuilder.seed(seedV1); } @@ -954,8 +954,8 @@ function renderTransaction() {
`; - // Outputs: 0.2 use nockchainTxToRaw + rawTxOutputs (maybe doesn't work) - const rawTx = wasm.nockchainTxToRaw(state.nockchainTx); + // Outputs: 0.2 use nockchainTxToRawTx + rawTxOutputs (maybe doesn't work) + const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx); const outputs = wasm.rawTxOutputs(rawTx); if (outputs && outputs.length > 0) { outputsList.innerHTML = outputs @@ -1566,7 +1566,7 @@ downloadTxBtn.onclick = () => { if (!state.nockchainTx) return; try { - const rawTx = wasm.nockchainTxToRaw(state.nockchainTx); + const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx); const jamBytes = wasm.jam(rawTx as unknown as wasm.Noun); const txId = state.nockchainTx.id; @@ -1592,9 +1592,9 @@ signTxBtn.onclick = async () => { try { const txNotes = state.builder.allNotes(); - const rawTx = wasm.nockchainTxToRaw(state.nockchainTx) as wasm.RawTxV1; + const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx) as wasm.RawTxV1; const rawTxProto = wasm.rawTxToProtobuf(rawTx); - const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.note_to_protobuf(n)); + const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.noteToProtobuf(n)); const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => wasm.spendConditionToProtobuf(sc) ); @@ -1610,7 +1610,7 @@ signTxBtn.onclick = async () => { ? signedTxProtobuf : (signedTxProtobuf as unknown as wasm.PbCom2RawTransaction); const signedRawTx = wasm.rawTxFromProtobuf(signedTxProtoObj) as wasm.RawTxV1; - state.signedTx = wasm.rawTxToNockchainTx(signedRawTx); + state.signedTx = wasm.rawTxV1ToNockchainTx(signedRawTx); let isValid = true; let validationError = ''; @@ -1675,7 +1675,7 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => { if (!state.signedTx || !state.signedTxId) return; try { - const rawTx = wasm.nockchainTxToRaw(state.signedTx); + const rawTx = wasm.nockchainTxToRawTx(state.signedTx); const jamBytes = wasm.jam(rawTx as unknown as wasm.Noun); const blob = new Blob([new Uint8Array(jamBytes)], { type: 'application/jam' }); const url = URL.createObjectURL(blob); @@ -1687,7 +1687,7 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => { document.body.removeChild(a); URL.revokeObjectURL(url); - const rawTxForJam = wasm.nockchainTxToRaw(state.signedTx); + const rawTxForJam = wasm.nockchainTxToRawTx(state.signedTx); const rawTxBytes = wasm.jam(rawTxForJam as unknown as wasm.Noun); const blobRaw = new Blob([new Uint8Array(rawTxBytes)], { type: 'application/jam' }); const urlRaw = URL.createObjectURL(blobRaw); @@ -1710,7 +1710,7 @@ document.getElementById('submitSignedTxBtn')!.onclick = async () => { try { if (!state.grpcClient) throw new Error('gRPC client not initialized'); - const rawTx = wasm.nockchainTxToRaw(state.signedTx) as wasm.RawTxV1; + const rawTx = wasm.nockchainTxToRawTx(state.signedTx) as wasm.RawTxV1; const txProtobuf = wasm.rawTxToProtobuf(rawTx); await state.grpcClient.sendTransaction(txProtobuf); From e0320cd218e058f14b4184a4bc63f2ad8cfe024e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 5 Mar 2026 14:16:42 +0000 Subject: [PATCH 12/75] Bump version --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c23bf21..c293173 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nockbox/iris-sdk", - "version": "0.1.1", + "version": "0.2.0-alpha.4", "description": "TypeScript SDK for interacting with Iris wallet extension", "type": "module", "main": "./dist/index.js", From e9adb62aa5ddc915c4004d5b66e6bc13c370579b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Thu, 5 Mar 2026 18:26:51 +0000 Subject: [PATCH 13/75] Update iris-wasm --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c293173..e524b44 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.4", + "@nockbox/iris-wasm": "^0.2.0-alpha.5", "@scure/base": "^2.0.0" } } From faaf42be65fd833f798e16712e269dbe4d5bc84a Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:18:53 -0400 Subject: [PATCH 14/75] bump iris-wasm to alpha.6 --- package-lock.json | 13 ++++++------- package.json | 2 +- 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9201a6f..543009b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@nockbox/iris-sdk", - "version": "0.1.1", + "version": "0.2.0-alpha.4", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nockbox/iris-sdk", - "version": "0.1.1", + "version": "0.2.0-alpha.4", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.4", + "@nockbox/iris-wasm": "^0.2.0-alpha.6", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.4", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.4.tgz", - "integrity": "sha512-V1fdky5lp1F01/2Yvca4dsP0hWi0JwbqV8uyzCqk1dpMRbz3w+ByR6tfKQoUzsgsubYWAhs5Am+UxtmH7G0QDw==", + "version": "0.2.0-alpha.6", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.6.tgz", + "integrity": "sha512-hyVH7PEvPCq0ti25tQ5AkvrINpnyXKnY6n3wKR9tKsbUiR/TJ4o/uc6gAmptf5vBY7d/BL/3HHcwptYFx+H79A==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -746,7 +746,6 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } diff --git a/package.json b/package.json index e524b44..694e709 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.5", + "@nockbox/iris-wasm": "^0.2.0-alpha.6", "@scure/base": "^2.0.0" } } From 56794119fd42bd97946adadd7cafd0ef9345d6b6 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Feb 2026 09:50:30 -0500 Subject: [PATCH 15/75] added APIs for v0 to v1 migration and swap (bridge) to be consumed by the extension --- src/bridge-types.ts | 82 +++++++++ src/bridge.ts | 388 +++++++++++++++++++++++++++++++++++++++++ src/migration-types.ts | 60 +++++++ src/migration.ts | 117 +++++++++++++ 4 files changed, 647 insertions(+) create mode 100644 src/bridge-types.ts create mode 100644 src/bridge.ts create mode 100644 src/migration-types.ts create mode 100644 src/migration.ts diff --git a/src/bridge-types.ts b/src/bridge-types.ts new file mode 100644 index 0000000..c0b0c9d --- /dev/null +++ b/src/bridge-types.ts @@ -0,0 +1,82 @@ +/** + * Bridge configuration and transaction types for the SDK. + * Consumers (nockswap, extension) provide BridgeConfig; the SDK handles tx construction and validation. + */ + +import type { NockchainTx, Nicks, Note, SpendCondition } from '@nockbox/iris-wasm/iris_wasm.js'; + +export type { Nicks }; + +/** + * Configuration for a specific bridge (e.g. Zorp Nock→Base). + * Pass this to all bridge functions so the SDK can build/validate transactions without hardcoded constants. + */ +export interface BridgeConfig { + /** Multisig threshold (e.g. 3 for 3-of-5) */ + threshold: number; + /** Multisig PKH addresses (Nockchain addresses) */ + addresses: string[]; + /** Note data key used for bridge payload (e.g. "bridge") */ + noteDataKey: string; + /** Chain identifier in note data, hex string (e.g. "65736162" for %base) */ + chainTag: string; + /** Version tag in note data (e.g. "0") */ + versionTag: string; + /** Fee per word in nicks (WASM Nicks = string, e.g. "32768") */ + feePerWord: Nicks; + /** Minimum amount in nicks for a valid bridge output (for validation) */ + minAmountNicks: Nicks; + /** Optional: expected lock root hash for bridge output (if set, validation checks it) */ + expectedLockRoot?: string; +} + +/** + * Parameters for building a bridge transaction. + * Input notes and spend conditions are supplied by the consumer (e.g. from gRPC). + */ +export interface BridgeTransactionParams { + /** User's input notes (UTXOs to spend) */ + inputNotes: Note[]; + /** Spend conditions for each input note */ + spendConditions: SpendCondition[]; + /** Amount to bridge in nicks (WASM Nicks = string) */ + amountInNicks: Nicks; + /** Destination EVM address on the target chain */ + destinationAddress: string; + /** User's PKH for refunds/change */ + refundPkh: string; + /** Optional fee override in nicks */ + feeOverride?: Nicks; +} + +/** + * Result of building a bridge transaction (unsigned). + */ +export interface BridgeTransactionResult { + /** The built transaction (ready for signing) */ + transaction: NockchainTx; + /** Transaction ID */ + txId: string; + /** Calculated fee in nicks (WASM Nicks = string) */ + fee: Nicks; +} + +/** + * Result of validating a bridge transaction (pre- or post-signing). + */ +export interface BridgeValidationResult { + valid: boolean; + error?: string; + /** Amount being sent to bridge in nicks */ + bridgeAmountNicks?: Nicks; + /** Destination EVM address extracted from note data */ + destinationAddress?: string; + /** Belt encoding extracted from note data */ + belts?: [bigint, bigint, bigint]; + /** Note data key */ + noteDataKey?: string; + /** Bridge version */ + version?: string; + /** Chain identifier */ + chain?: string; +} diff --git a/src/bridge.ts b/src/bridge.ts new file mode 100644 index 0000000..59618ab --- /dev/null +++ b/src/bridge.ts @@ -0,0 +1,388 @@ +/** + * Bridge utilities for Nockchain ↔ EVM bridging. + * Encoding uses the Goldilocks prime field (3 belts) for EVM addresses. + * Consumers provide BridgeConfig; the SDK handles transaction construction and validation. + */ + +import type { + BridgeConfig, + BridgeTransactionParams, + BridgeTransactionResult, + BridgeValidationResult, +} from './bridge-types.js'; +import type { + LockRoot, + Note, + NoteData, + Noun, + PbCom2RawTransaction, + SeedV1, + SpendCondition, +} from '@nockbox/iris-wasm/iris_wasm.js'; +import * as wasm from './wasm.js'; + +// Goldilocks prime: 2^64 - 2^32 + 1 +export const GOLDILOCKS_PRIME = 2n ** 64n - 2n ** 32n + 1n; + +/** Simple EVM address check (0x + 40 hex chars). No checksum validation. */ +export function isEvmAddress(address: string): boolean { + const s = (address || '').trim(); + const normalized = s.startsWith('0x') ? s : `0x${s}`; + return /^0x[0-9a-fA-F]{40}$/.test(normalized); +} + +/** + * Convert an EVM address to 3 belts (Goldilocks field elements). + */ +export function evmAddressToBelts(address: string): [bigint, bigint, bigint] { + if (!isEvmAddress(address)) { + throw new Error(`Invalid EVM address: ${address}`); + } + const normalized = address.startsWith('0x') ? address : `0x${address}`; + const addr = BigInt(normalized); + + const belt1 = addr % GOLDILOCKS_PRIME; + const q1 = addr / GOLDILOCKS_PRIME; + const belt2 = q1 % GOLDILOCKS_PRIME; + const belt3 = q1 / GOLDILOCKS_PRIME; + + return [belt1, belt2, belt3]; +} + +/** + * Convert 3 belts back to an EVM address. + */ +export function beltsToEvmAddress(belt1: bigint, belt2: bigint, belt3: bigint): string { + const p = GOLDILOCKS_PRIME; + const address = belt1 + belt2 * p + belt3 * p * p; + return '0x' + address.toString(16).padStart(40, '0'); +} + +/** Encode a string as a Hoon cord (little-endian hex). */ +export function stringToAtom(str: string): string { + const bytes = new TextEncoder().encode(str); + let hex = ''; + for (let i = bytes.length - 1; i >= 0; i--) { + hex += bytes[i].toString(16).padStart(2, '0'); + } + return hex || '0'; +} + +/** Encode a bigint as hex (no 0x prefix). */ +export function bigintToAtom(n: bigint): string { + if (n === 0n) return '0'; + return n.toString(16); +} + +/** + * Build the bridge noun structure for an EVM address. + * Structure: [versionTag [chainTag [belt1 [belt2 belt3]]]] + */ +export function buildBridgeNoun( + evmAddress: string, + config: Pick +): unknown { + const [belt1, belt2, belt3] = evmAddressToBelts(evmAddress); + return [ + config.versionTag, + [config.chainTag, [bigintToAtom(belt1), [bigintToAtom(belt2), bigintToAtom(belt3)]]], + ]; +} + +/** + * Verify belt encoding round-trips correctly. + */ +export function verifyBeltEncoding(address: string): boolean { + if (!isEvmAddress(address)) return false; + const normalized = address.toLowerCase().startsWith('0x') + ? address.toLowerCase() + : `0x${address.toLowerCase()}`; + const [belt1, belt2, belt3] = evmAddressToBelts(normalized); + const recovered = beltsToEvmAddress(belt1, belt2, belt3); + return normalized === recovered; +} + +/** + * Check if a bridge config is valid and usable. + */ +export function isBridgeConfigured(config: BridgeConfig): boolean { + return ( + config.addresses.length > 0 && + config.threshold > 0 && + config.threshold <= config.addresses.length + ); +} + +/** + * Create jammed bridge note data for an EVM address (requires WASM). + * Caller must have initialized WASM (e.g. await wasm.default()) before using. + */ +export async function createBridgeNoteData( + evmAddress: string, + config: BridgeConfig +): Promise { + const nounJs = buildBridgeNoun(evmAddress, config); + return wasm.jam(nounJs as Noun); +} + +/** + * Build a bridge transaction (requires WASM). + * Consumer supplies notes and spend conditions; SDK builds the tx from config. + */ +export async function buildBridgeTransaction( + params: BridgeTransactionParams, + config: BridgeConfig +): Promise { + if (!isBridgeConfigured(config)) { + throw new Error('Bridge not configured'); + } + if (!isEvmAddress(params.destinationAddress)) { + throw new Error(`Invalid destination address: ${params.destinationAddress}`); + } + + const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); + const noteData: NoteData = [[config.noteDataKey, bridgeNounJs as Noun]]; + + const bridgeSpendCondition: SpendCondition = [ + { Pkh: { m: config.threshold, hashes: config.addresses } }, + ]; + const bridgeLockRoot: LockRoot = { Lock: bridgeSpendCondition }; + const refundLock: SpendCondition = [{ Pkh: { m: 1, hashes: [params.refundPkh] } }]; + + const costPerWord = params.feeOverride ?? config.feePerWord; + const builder = new wasm.TxBuilder({ + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: costPerWord, + witness_word_div: 1, + }); + + for (let i = 0; i < params.inputNotes.length; i++) { + const note = params.inputNotes[i]; + const spendCondition = params.spendConditions[i]; + + const spendBuilder = new wasm.SpendBuilder(note, spendCondition, refundLock); + + const parentHash = wasm.note_hash(note); + const seed: SeedV1 = { + output_source: undefined, + lock_root: bridgeLockRoot, + note_data: noteData, + gift: params.amountInNicks, + parent_hash: parentHash, + }; + + spendBuilder.seed(seed); + spendBuilder.computeRefund(false); + builder.spend(spendBuilder); + } + + builder.recalcAndSetFee(false); + const feeResult = builder.calcFee(); + const transaction = builder.build(); + + const txId = transaction.id; + const fee = feeResult; + + return { + transaction, + txId, + fee, + }; +} + +/** + * Validate a bridge transaction (pre- or post-signing). + * Uses config for note key, min amount, and optional lock root. + */ +export async function validateBridgeTransaction( + rawTxProto: unknown, + config: BridgeConfig +): Promise { + try { + const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); + const outputs = wasm.rawTxOutputs(rawTx); + + if (outputs.length === 0) { + return { valid: false, error: 'Transaction has no outputs' }; + } + + const outputData: Array<{ + assets: bigint; + noteData: NoteData; + }> = outputs.map((output: Note) => { + const noteData = 'note_data' in output ? ((output.note_data as NoteData) ?? []) : []; + return { + assets: BigInt(output.assets ?? 0), + noteData, + }; + }); + + let bridgeOutput: (typeof outputData)[0] | null = null; + for (const output of outputData) { + const hasKey = output.noteData?.some( + (entry: [string, Noun]) => entry[0] === config.noteDataKey + ); + if (hasKey) { + bridgeOutput = output; + break; + } + } + + if (!bridgeOutput) { + return { + valid: false, + error: `No output with '${config.noteDataKey}' note data found in transaction`, + }; + } + + if (BigInt(bridgeOutput.assets) < BigInt(config.minAmountNicks)) { + return { + valid: false, + error: `Bridge amount ${bridgeOutput.assets} nicks is below minimum ${config.minAmountNicks} nicks`, + }; + } + + if (!bridgeOutput.noteData?.length) { + return { valid: false, error: 'Bridge output missing note data' }; + } + + const bridgeEntryPair = bridgeOutput.noteData.find( + (entry: [string, Noun]) => entry[0] === config.noteDataKey + ); + if (!bridgeEntryPair) { + return { + valid: false, + error: `Bridge output missing '${config.noteDataKey}' note data entry`, + }; + } + const bridgeEntryValue = bridgeEntryPair[1]; + + let destinationAddress: string | undefined; + let belts: [bigint, bigint, bigint] | undefined; + let validatedVersion: string | undefined; + let validatedChain: string | undefined; + const validatedNoteDataKey = bridgeEntryPair[0]; + + try { + const value = bridgeEntryValue as unknown; + const decoded: unknown = Array.isArray(value) + ? value + : wasm.cue( + value instanceof Uint8Array ? value : new Uint8Array(value as unknown as number[]) + ); + + if (!Array.isArray(decoded) || decoded.length !== 2) { + return { + valid: false, + error: 'Invalid bridge note data structure: expected [version, [chain, belts]]', + }; + } + + const version = decoded[0]; + if (version !== config.versionTag && version !== Number(config.versionTag)) { + return { + valid: false, + error: `Invalid bridge note data version: expected ${config.versionTag}, got ${version}`, + }; + } + validatedVersion = String(version); + + const chainAndBelts = decoded[1]; + if (!Array.isArray(chainAndBelts) || chainAndBelts.length !== 2) { + return { + valid: false, + error: 'Invalid bridge note data: missing chain and belts', + }; + } + + const chain = chainAndBelts[0]; + if (String(chain) !== config.chainTag) { + return { + valid: false, + error: `Invalid bridge chain: expected ${config.chainTag}, got ${chain}`, + }; + } + validatedChain = String(chain); + + const beltData = chainAndBelts[1]; + if (!Array.isArray(beltData) || beltData.length !== 2) { + return { + valid: false, + error: 'Invalid bridge note data: invalid belt structure', + }; + } + + const belt1Hex = beltData[0]; + const belt2And3 = beltData[1]; + if (!Array.isArray(belt2And3) || belt2And3.length !== 2) { + return { + valid: false, + error: 'Invalid bridge note data: invalid belt2/belt3 structure', + }; + } + + const belt2Hex = belt2And3[0]; + const belt3Hex = belt2And3[1]; + + const belt1 = BigInt('0x' + belt1Hex); + const belt2 = BigInt('0x' + belt2Hex); + const belt3 = BigInt('0x' + belt3Hex); + belts = [belt1, belt2, belt3]; + destinationAddress = beltsToEvmAddress(belt1, belt2, belt3); + + if (!isEvmAddress(destinationAddress)) { + return { + valid: false, + error: `Reconstructed address is invalid: ${destinationAddress}`, + }; + } + } catch (err) { + return { + valid: false, + error: `Failed to decode bridge note data: ${ + err instanceof Error ? err.message : String(err) + }`, + }; + } + + return { + valid: true, + bridgeAmountNicks: String(bridgeOutput.assets), + destinationAddress, + belts, + noteDataKey: validatedNoteDataKey, + version: validatedVersion, + chain: validatedChain, + }; + } catch (err) { + return { + valid: false, + error: `Transaction validation failed: ${err instanceof Error ? err.message : String(err)}`, + }; + } +} + +/** + * Validate and throw if invalid (convenience wrapper). + */ +export async function assertValidBridgeTransaction( + rawTxProto: unknown, + context: 'pre-signing' | 'post-signing', + config: BridgeConfig +): Promise { + const result = await validateBridgeTransaction(rawTxProto, config); + if (!result.valid) { + throw new Error(`${context} validation failed: ${result.error}`); + } + return result; +} + +// Re-export types +export type { + BridgeConfig, + BridgeTransactionParams, + BridgeTransactionResult, + BridgeValidationResult, +} from './bridge-types.js'; diff --git a/src/migration-types.ts b/src/migration-types.ts new file mode 100644 index 0000000..8685a3b --- /dev/null +++ b/src/migration-types.ts @@ -0,0 +1,60 @@ +/** + * Types for querying v0 balance and building v0 -> v1 migration transactions. + * Aligned with @nockbox/iris-wasm (Nicks, NockchainTx, NoteV0, RawTx, SpendCondition). + */ +import type { + NockchainTx, + Nicks, + NoteV0, + PbCom2Balance, + RawTx, + SpendCondition, +} from '@nockbox/iris-wasm/iris_wasm.js'; + +export type { Nicks }; + +/** Input required to query legacy (v0) notes for a seed-derived PKH. */ +export interface QueryV0BalanceParams { + /** gRPC endpoint to query notes from. */ + grpcEndpoint: string; + /** PKH derived from the user's seedphrase. */ + sourcePkh: string; +} + +/** Result of querying v0 balance via first-name lookup. */ +export interface QueryV0BalanceResult { + /** Optional first-name digest (not required when querying by address). */ + firstName?: string; + /** Raw balance payload from gRPC. */ + balance: PbCom2Balance; + /** Parsed v0 notes found in the balance response. */ + v0Notes: NoteV0[]; +} + +/** Input for building a migration transaction from v0 notes to a v1 PKH address. */ +export interface BuildV0MigrationTransactionParams { + /** Legacy notes to migrate (typically from queryV0BalanceForPkh). */ + v0Notes: NoteV0[]; + /** Destination v1 PKH (usually the extension wallet PKH). */ + targetV1Pkh: string; + /** Fee-per-word used by TxBuilder (WASM Nicks = string, e.g. "32768"). */ + feePerWord?: Nicks; + /** Whether to include lock metadata in refund/migration seeds. */ + includeLockData?: boolean; +} + +/** Result of building a v0 -> v1 migration transaction. */ +export interface BuildV0MigrationTransactionResult { + /** Built transaction object returned by iris-wasm TxBuilder. */ + transaction: NockchainTx; + /** Transaction id (normalized as a string). */ + txId: string; + /** Calculated fee in nicks (WASM Nicks = string). */ + fee: Nicks; + /** Payload compatible with provider.signRawTx(...) */ + signRawTxPayload: { + rawTx: RawTx; + notes: NoteV0[]; + spendConditions: SpendCondition[]; + }; +} diff --git a/src/migration.ts b/src/migration.ts new file mode 100644 index 0000000..2ffefb6 --- /dev/null +++ b/src/migration.ts @@ -0,0 +1,117 @@ +import type { + BuildV0MigrationTransactionParams, + BuildV0MigrationTransactionResult, + QueryV0BalanceParams, + QueryV0BalanceResult, +} from './migration-types.js'; +import type { + Note, + NoteV0, + PbCom2BalanceEntry, + RawTxV1, + SpendCondition, + TxEngineSettings, + TxNotes, +} from '@nockbox/iris-wasm/iris_wasm.js'; +import * as wasm from './wasm.js'; + +function buildSinglePkhSpendCondition(pkh: string): SpendCondition { + return [{ Pkh: { m: 1, hashes: [pkh] } }]; +} + +function isLegacyEntry(entry: PbCom2BalanceEntry): boolean { + return !!entry.note?.note_version && 'Legacy' in entry.note.note_version; +} + +function isNoteV0(note: Note): note is NoteV0 { + return 'inner' in note && 'sig' in note && 'source' in note; +} + +/** + * Query address balance and return only v0 (Legacy) notes. + * Caller must have initialized WASM (e.g. await wasm.default()) before using. + */ +export async function queryV0BalanceForPkh( + params: QueryV0BalanceParams +): Promise { + const grpcClient = new wasm.GrpcClient(params.grpcEndpoint); + const balance = await grpcClient.getBalanceByAddress(params.sourcePkh); + + const v0Notes: NoteV0[] = []; + const entries = balance.notes ?? []; + for (const entry of entries) { + if (!isLegacyEntry(entry) || !entry.note) { + continue; + } + const parsed = wasm.note_from_protobuf(entry.note); + if (isNoteV0(parsed)) { + v0Notes.push(parsed); + } + } + + return { + balance, + v0Notes, + }; +} + +/** + * Build a transaction that migrates v0 notes into a v1 PKH lock. + * Caller must have initialized WASM (e.g. await wasm.default()) before using. + */ +export async function buildV0MigrationTransaction( + params: BuildV0MigrationTransactionParams +): Promise { + if (!params.v0Notes.length) { + throw new Error('No v0 notes provided for migration'); + } + + const includeLockData = !!params.includeLockData; + const costPerWord = params.feePerWord ?? '32768'; + + const targetSpendCondition = buildSinglePkhSpendCondition(params.targetV1Pkh); + const settings: TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: costPerWord, + witness_word_div: 1, + }; + const builder = new wasm.TxBuilder(settings); + + for (const note of params.v0Notes) { + const spendBuilder = new wasm.SpendBuilder(note, null, targetSpendCondition); + // Use refund path to migrate full note value into target lock. + spendBuilder.computeRefund(includeLockData); + builder.spend(spendBuilder); + } + + builder.recalcAndSetFee(includeLockData); + const feeResult = builder.calcFee(); + const transaction = builder.build(); + const txNotes = builder.allNotes() as TxNotes; + const txId = transaction.id; + const rawTx: RawTxV1 = { + version: 1, + id: transaction.id, + spends: transaction.spends, + }; + + return { + transaction, + txId, + fee: feeResult, + signRawTxPayload: { + rawTx, + notes: txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)), + spendConditions: txNotes.spend_conditions, + }, + }; +} + +export type { + BuildV0MigrationTransactionParams, + BuildV0MigrationTransactionResult, + QueryV0BalanceParams, + QueryV0BalanceResult, +} from './migration-types.js'; From 61bea2ed4e25bcc53f83b09694988b86730623ad Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Sun, 1 Mar 2026 16:20:06 -0500 Subject: [PATCH 16/75] temporary WASM module --- package-lock.json | 11 +++++++++++ package.json | 4 ++-- src/bridge.ts | 2 +- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 543009b..c2244eb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,6 +19,12 @@ "vite": "^5.0.0" } }, + "../iris-rs/crates/iris-wasm/pkg": { + "name": "@nockbox/iris-wasm", + "version": "0.2.0-alpha.3", + "extraneous": true, + "license": "MIT" + }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -1007,6 +1013,11 @@ "optional": true } } + }, + "vendor/iris-wasm": { + "name": "@nockbox/iris-wasm", + "version": "0.2.0-alpha.3", + "license": "MIT" } } } diff --git a/package.json b/package.json index 694e709..812c6a7 100644 --- a/package.json +++ b/package.json @@ -21,10 +21,10 @@ }, "files": [ "dist", - "README.md" + "README.md", + "vendor/iris-wasm" ], "scripts": { - "prepare": "npm run build", "build": "tsc", "build-example": "npm run build && tsc --project examples/tsconfig.json && vite build --config vite.config.examples.ts", "dev": "tsc --watch", diff --git a/src/bridge.ts b/src/bridge.ts index 59618ab..827de22 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -166,7 +166,7 @@ export async function buildBridgeTransaction( const parentHash = wasm.note_hash(note); const seed: SeedV1 = { - output_source: undefined, + output_source: null, lock_root: bridgeLockRoot, note_data: noteData, gift: params.amountInNicks, From e3fd27ef1268864c3d01624f6e6fe3ed8a408c30 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 2 Mar 2026 12:56:27 -0500 Subject: [PATCH 17/75] update migration --- src/index.ts | 1 + src/migration-types.ts | 59 +++++++++++++++++++++++-- src/migration.ts | 98 +++++++++++++++++++++++++++++++++++++++++- 3 files changed, 153 insertions(+), 5 deletions(-) diff --git a/src/index.ts b/src/index.ts index f9e0d03..966d44d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,6 +6,7 @@ export * from './types.js'; export * from './provider.js'; export * from './transaction.js'; +export * from './migration.js'; export * from './errors.js'; export * from './constants.js'; export * from './compat.js'; diff --git a/src/migration-types.ts b/src/migration-types.ts index 8685a3b..68c0adc 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -13,12 +13,43 @@ import type { export type { Nicks }; -/** Input required to query legacy (v0) notes for a seed-derived PKH. */ +/** Input required to derive the legacy (v0) address from seedphrase. */ +export interface DeriveV0AddressParams { + /** BIP-39 mnemonic for the legacy wallet. */ + mnemonic: string; + /** Optional BIP-39 passphrase. */ + passphrase?: string; + /** + * Optional child derivation index. + * If omitted, the master key public key is used. + */ + childIndex?: number; +} + +/** Derived legacy address metadata used for v0 discovery. */ +export interface DerivedV0Address { + /** Base58-encoded bare public key (legacy address form used by v0 notes). */ + sourceAddress: string; + /** PKH digest for the same public key (base58 digest string). */ + sourcePkh: string; + /** Public key bytes encoded as hex (debug/inspection helper). */ + publicKeyHex: string; +} + +/** Input required to query legacy (v0) notes by address. */ export interface QueryV0BalanceParams { /** gRPC endpoint to query notes from. */ grpcEndpoint: string; - /** PKH derived from the user's seedphrase. */ - sourcePkh: string; + /** + * Legacy base58 address (bare pubkey). + * Preferred field. + */ + sourceAddress?: string; + /** + * Back-compat alias. Historically named as PKH, but routed to getBalanceByAddress. + * If sourceAddress is provided, this field is ignored. + */ + sourcePkh?: string; } /** Result of querying v0 balance via first-name lookup. */ @@ -29,6 +60,28 @@ export interface QueryV0BalanceResult { balance: PbCom2Balance; /** Parsed v0 notes found in the balance response. */ v0Notes: NoteV0[]; + /** Sum of v0 note assets in nicks (string for bigint-safe transport). */ + totalNicks: Nicks; +} + +/** Input required to derive v0 address and immediately query legacy notes. */ +export interface QueryV0BalanceFromMnemonicParams extends DeriveV0AddressParams { + /** gRPC endpoint to query notes from. */ + grpcEndpoint: string; +} + +/** Result of mnemonic-based v0 note discovery. */ +export interface QueryV0BalanceFromMnemonicResult extends QueryV0BalanceResult, DerivedV0Address {} + +/** Inputs to build a migration transaction directly from mnemonic + destination. */ +export interface BuildV0MigrationFromMnemonicParams + extends QueryV0BalanceFromMnemonicParams, + Omit {} + +/** Output of mnemonic-based migration transaction building. */ +export interface BuildV0MigrationFromMnemonicResult extends BuildV0MigrationTransactionResult { + /** Discovery data used to assemble the transaction. */ + discovery: QueryV0BalanceFromMnemonicResult; } /** Input for building a migration transaction from v0 notes to a v1 PKH address. */ diff --git a/src/migration.ts b/src/migration.ts index 2ffefb6..dc61522 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,7 +1,13 @@ import type { + BuildV0MigrationFromMnemonicParams, + BuildV0MigrationFromMnemonicResult, BuildV0MigrationTransactionParams, BuildV0MigrationTransactionResult, + DeriveV0AddressParams, + DerivedV0Address, QueryV0BalanceParams, + QueryV0BalanceFromMnemonicParams, + QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, } from './migration-types.js'; import type { @@ -13,6 +19,7 @@ import type { TxEngineSettings, TxNotes, } from '@nockbox/iris-wasm/iris_wasm.js'; +import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; function buildSinglePkhSpendCondition(pkh: string): SpendCondition { @@ -27,15 +34,47 @@ function isNoteV0(note: Note): note is NoteV0 { return 'inner' in note && 'sig' in note && 'source' in note; } +function toHex(bytes: Uint8Array): string { + return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); +} + +function sumNicks(notes: NoteV0[]): string { + const total = notes.reduce((acc, note) => acc + BigInt(note.assets), 0n); + return total.toString(); +} + +/** + * Derive legacy v0 address metadata from mnemonic. + * + * v0 discovery queries use the base58-encoded bare public key ("sourceAddress"). + * We also expose the hashed PKH digest for callers that need lock condition metadata. + */ +export function deriveV0AddressFromMnemonic(params: DeriveV0AddressParams): DerivedV0Address { + const master = wasm.deriveMasterKeyFromMnemonic(params.mnemonic, params.passphrase ?? ''); + const key = params.childIndex === undefined ? master : master.deriveChild(params.childIndex); + const publicKey = Uint8Array.from(key.publicKey); + + return { + sourceAddress: base58.encode(publicKey), + sourcePkh: wasm.hashPublicKey(publicKey), + publicKeyHex: toHex(publicKey), + }; +} + /** * Query address balance and return only v0 (Legacy) notes. * Caller must have initialized WASM (e.g. await wasm.default()) before using. */ -export async function queryV0BalanceForPkh( +export async function queryV0BalanceForAddress( params: QueryV0BalanceParams ): Promise { + const sourceAddress = params.sourceAddress ?? params.sourcePkh; + if (!sourceAddress) { + throw new Error('sourceAddress is required'); + } + const grpcClient = new wasm.GrpcClient(params.grpcEndpoint); - const balance = await grpcClient.getBalanceByAddress(params.sourcePkh); + const balance = await grpcClient.getBalanceByAddress(sourceAddress); const v0Notes: NoteV0[] = []; const entries = balance.notes ?? []; @@ -52,6 +91,35 @@ export async function queryV0BalanceForPkh( return { balance, v0Notes, + totalNicks: sumNicks(v0Notes), + }; +} + +/** + * Back-compat alias kept for older callers. + * Despite the name, this resolves to address-based lookup via getBalanceByAddress. + */ +export async function queryV0BalanceForPkh( + params: QueryV0BalanceParams +): Promise { + return queryV0BalanceForAddress(params); +} + +/** + * Derive v0 discovery address from mnemonic and query legacy notes in one step. + */ +export async function queryV0BalanceFromMnemonic( + params: QueryV0BalanceFromMnemonicParams +): Promise { + const derived = deriveV0AddressFromMnemonic(params); + const queried = await queryV0BalanceForAddress({ + grpcEndpoint: params.grpcEndpoint, + sourceAddress: derived.sourceAddress, + }); + + return { + ...derived, + ...queried, }; } @@ -109,9 +177,35 @@ export async function buildV0MigrationTransaction( }; } +/** + * Derive v0 address, query legacy notes, and build migration transaction. + */ +export async function buildV0MigrationFromMnemonic( + params: BuildV0MigrationFromMnemonicParams +): Promise { + const discovery = await queryV0BalanceFromMnemonic(params); + const built = await buildV0MigrationTransaction({ + v0Notes: discovery.v0Notes, + targetV1Pkh: params.targetV1Pkh, + feePerWord: params.feePerWord, + includeLockData: params.includeLockData, + }); + + return { + ...built, + discovery, + }; +} + export type { + BuildV0MigrationFromMnemonicParams, + BuildV0MigrationFromMnemonicResult, BuildV0MigrationTransactionParams, BuildV0MigrationTransactionResult, + DeriveV0AddressParams, + DerivedV0Address, QueryV0BalanceParams, + QueryV0BalanceFromMnemonicParams, + QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, } from './migration-types.js'; From 940bd481f09642c78047093033bc23309b1c1089 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:35:12 -0500 Subject: [PATCH 18/75] refactored and simplified the migration logic and the SDK API --- src/migration-types.ts | 91 +++++------------------------ src/migration.ts | 128 ++++++++++++++++++++--------------------- 2 files changed, 75 insertions(+), 144 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index 68c0adc..1aa3694 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -13,101 +13,38 @@ import type { export type { Nicks }; -/** Input required to derive the legacy (v0) address from seedphrase. */ -export interface DeriveV0AddressParams { - /** BIP-39 mnemonic for the legacy wallet. */ - mnemonic: string; - /** Optional BIP-39 passphrase. */ - passphrase?: string; - /** - * Optional child derivation index. - * If omitted, the master key public key is used. - */ - childIndex?: number; -} - -/** Derived legacy address metadata used for v0 discovery. */ +/** Legacy address metadata derived from mnemonic. */ export interface DerivedV0Address { - /** Base58-encoded bare public key (legacy address form used by v0 notes). */ + /** Base58 bare public key (legacy address form). */ sourceAddress: string; - /** PKH digest for the same public key (base58 digest string). */ - sourcePkh: string; - /** Public key bytes encoded as hex (debug/inspection helper). */ - publicKeyHex: string; -} - -/** Input required to query legacy (v0) notes by address. */ -export interface QueryV0BalanceParams { - /** gRPC endpoint to query notes from. */ - grpcEndpoint: string; - /** - * Legacy base58 address (bare pubkey). - * Preferred field. - */ - sourceAddress?: string; - /** - * Back-compat alias. Historically named as PKH, but routed to getBalanceByAddress. - * If sourceAddress is provided, this field is ignored. - */ - sourcePkh?: string; } -/** Result of querying v0 balance via first-name lookup. */ +/** Result of querying v0 balance. */ export interface QueryV0BalanceResult { - /** Optional first-name digest (not required when querying by address). */ - firstName?: string; - /** Raw balance payload from gRPC. */ balance: PbCom2Balance; - /** Parsed v0 notes found in the balance response. */ v0Notes: NoteV0[]; - /** Sum of v0 note assets in nicks (string for bigint-safe transport). */ totalNicks: Nicks; } -/** Input required to derive v0 address and immediately query legacy notes. */ -export interface QueryV0BalanceFromMnemonicParams extends DeriveV0AddressParams { - /** gRPC endpoint to query notes from. */ - grpcEndpoint: string; -} - -/** Result of mnemonic-based v0 note discovery. */ -export interface QueryV0BalanceFromMnemonicResult extends QueryV0BalanceResult, DerivedV0Address {} - -/** Inputs to build a migration transaction directly from mnemonic + destination. */ -export interface BuildV0MigrationFromMnemonicParams - extends QueryV0BalanceFromMnemonicParams, - Omit {} - -/** Output of mnemonic-based migration transaction building. */ -export interface BuildV0MigrationFromMnemonicResult extends BuildV0MigrationTransactionResult { - /** Discovery data used to assemble the transaction. */ - discovery: QueryV0BalanceFromMnemonicResult; -} - -/** Input for building a migration transaction from v0 notes to a v1 PKH address. */ -export interface BuildV0MigrationTransactionParams { - /** Legacy notes to migrate (typically from queryV0BalanceForPkh). */ - v0Notes: NoteV0[]; - /** Destination v1 PKH (usually the extension wallet PKH). */ - targetV1Pkh: string; - /** Fee-per-word used by TxBuilder (WASM Nicks = string, e.g. "32768"). */ - feePerWord?: Nicks; - /** Whether to include lock metadata in refund/migration seeds. */ - includeLockData?: boolean; -} +/** Result of mnemonic-based v0 discovery (derived address + balance). */ +export interface QueryV0BalanceFromMnemonicResult + extends QueryV0BalanceResult, + DerivedV0Address {} -/** Result of building a v0 -> v1 migration transaction. */ +/** Result of building a migration transaction. */ export interface BuildV0MigrationTransactionResult { - /** Built transaction object returned by iris-wasm TxBuilder. */ transaction: NockchainTx; - /** Transaction id (normalized as a string). */ txId: string; - /** Calculated fee in nicks (WASM Nicks = string). */ fee: Nicks; - /** Payload compatible with provider.signRawTx(...) */ signRawTxPayload: { rawTx: RawTx; notes: NoteV0[]; spendConditions: SpendCondition[]; }; } + +/** Result of mnemonic-based migration build. */ +export interface BuildV0MigrationFromMnemonicResult + extends BuildV0MigrationTransactionResult { + discovery: QueryV0BalanceFromMnemonicResult; +} diff --git a/src/migration.ts b/src/migration.ts index dc61522..d99607f 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,17 +1,13 @@ import type { - BuildV0MigrationFromMnemonicParams, BuildV0MigrationFromMnemonicResult, - BuildV0MigrationTransactionParams, BuildV0MigrationTransactionResult, - DeriveV0AddressParams, DerivedV0Address, - QueryV0BalanceParams, - QueryV0BalanceFromMnemonicParams, QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, } from './migration-types.js'; import type { Note, + Nicks, NoteV0, PbCom2BalanceEntry, RawTxV1, @@ -34,10 +30,6 @@ function isNoteV0(note: Note): note is NoteV0 { return 'inner' in note && 'sig' in note && 'source' in note; } -function toHex(bytes: Uint8Array): string { - return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join(''); -} - function sumNicks(notes: NoteV0[]): string { const total = notes.reduce((acc, note) => acc + BigInt(note.assets), 0n); return total.toString(); @@ -47,17 +39,18 @@ function sumNicks(notes: NoteV0[]): string { * Derive legacy v0 address metadata from mnemonic. * * v0 discovery queries use the base58-encoded bare public key ("sourceAddress"). - * We also expose the hashed PKH digest for callers that need lock condition metadata. */ -export function deriveV0AddressFromMnemonic(params: DeriveV0AddressParams): DerivedV0Address { - const master = wasm.deriveMasterKeyFromMnemonic(params.mnemonic, params.passphrase ?? ''); - const key = params.childIndex === undefined ? master : master.deriveChild(params.childIndex); +export function deriveV0AddressFromMnemonic( + mnemonic: string, + passphrase?: string, + childIndex?: number +): DerivedV0Address { + const master = wasm.deriveMasterKeyFromMnemonic(mnemonic, passphrase ?? ''); + const key = childIndex === undefined ? master : master.deriveChild(childIndex); const publicKey = Uint8Array.from(key.publicKey); return { sourceAddress: base58.encode(publicKey), - sourcePkh: wasm.hashPublicKey(publicKey), - publicKeyHex: toHex(publicKey), }; } @@ -66,15 +59,15 @@ export function deriveV0AddressFromMnemonic(params: DeriveV0AddressParams): Deri * Caller must have initialized WASM (e.g. await wasm.default()) before using. */ export async function queryV0BalanceForAddress( - params: QueryV0BalanceParams + grpcEndpoint: string, + address: string ): Promise { - const sourceAddress = params.sourceAddress ?? params.sourcePkh; - if (!sourceAddress) { - throw new Error('sourceAddress is required'); + if (!address) { + throw new Error('address is required'); } - const grpcClient = new wasm.GrpcClient(params.grpcEndpoint); - const balance = await grpcClient.getBalanceByAddress(sourceAddress); + const grpcClient = new wasm.GrpcClient(grpcEndpoint); + const balance = await grpcClient.getBalanceByAddress(address); const v0Notes: NoteV0[] = []; const entries = balance.notes ?? []; @@ -95,27 +88,17 @@ export async function queryV0BalanceForAddress( }; } -/** - * Back-compat alias kept for older callers. - * Despite the name, this resolves to address-based lookup via getBalanceByAddress. - */ -export async function queryV0BalanceForPkh( - params: QueryV0BalanceParams -): Promise { - return queryV0BalanceForAddress(params); -} - /** * Derive v0 discovery address from mnemonic and query legacy notes in one step. */ export async function queryV0BalanceFromMnemonic( - params: QueryV0BalanceFromMnemonicParams + mnemonic: string, + grpcEndpoint: string, + passphrase?: string, + childIndex?: number ): Promise { - const derived = deriveV0AddressFromMnemonic(params); - const queried = await queryV0BalanceForAddress({ - grpcEndpoint: params.grpcEndpoint, - sourceAddress: derived.sourceAddress, - }); + const derived = deriveV0AddressFromMnemonic(mnemonic, passphrase, childIndex); + const queried = await queryV0BalanceForAddress(grpcEndpoint, derived.sourceAddress); return { ...derived, @@ -123,38 +106,46 @@ export async function queryV0BalanceFromMnemonic( }; } +const DEFAULT_TX_ENGINE_SETTINGS: TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: '32768', + witness_word_div: 1, +}; + /** * Build a transaction that migrates v0 notes into a v1 PKH lock. * Caller must have initialized WASM (e.g. await wasm.default()) before using. */ export async function buildV0MigrationTransaction( - params: BuildV0MigrationTransactionParams + v0Notes: NoteV0[], + targetV1Pkh: string, + feePerWord?: Nicks, + includeLockData?: boolean, + settings?: Partial ): Promise { - if (!params.v0Notes.length) { + if (!v0Notes.length) { throw new Error('No v0 notes provided for migration'); } - const includeLockData = !!params.includeLockData; - const costPerWord = params.feePerWord ?? '32768'; - - const targetSpendCondition = buildSinglePkhSpendCondition(params.targetV1Pkh); - const settings: TxEngineSettings = { - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256', - cost_per_word: costPerWord, - witness_word_div: 1, + const includeLockDataVal = !!includeLockData; + const txSettings: TxEngineSettings = { + ...DEFAULT_TX_ENGINE_SETTINGS, + ...settings, + cost_per_word: feePerWord ?? settings?.cost_per_word ?? DEFAULT_TX_ENGINE_SETTINGS.cost_per_word, }; - const builder = new wasm.TxBuilder(settings); + const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); + const builder = new wasm.TxBuilder(txSettings); - for (const note of params.v0Notes) { + for (const note of v0Notes) { const spendBuilder = new wasm.SpendBuilder(note, null, targetSpendCondition); // Use refund path to migrate full note value into target lock. - spendBuilder.computeRefund(includeLockData); + spendBuilder.computeRefund(includeLockDataVal); builder.spend(spendBuilder); } - builder.recalcAndSetFee(includeLockData); + builder.recalcAndSetFee(includeLockDataVal); const feeResult = builder.calcFee(); const transaction = builder.build(); const txNotes = builder.allNotes() as TxNotes; @@ -178,18 +169,26 @@ export async function buildV0MigrationTransaction( } /** - * Derive v0 address, query legacy notes, and build migration transaction. + * Derive v0 address, query legacy notes, and build migration transaction in one step. */ export async function buildV0MigrationFromMnemonic( - params: BuildV0MigrationFromMnemonicParams + mnemonic: string, + grpcEndpoint: string, + targetV1Pkh: string, + passphrase?: string, + childIndex?: number, + feePerWord?: Nicks, + includeLockData?: boolean, + settings?: Partial ): Promise { - const discovery = await queryV0BalanceFromMnemonic(params); - const built = await buildV0MigrationTransaction({ - v0Notes: discovery.v0Notes, - targetV1Pkh: params.targetV1Pkh, - feePerWord: params.feePerWord, - includeLockData: params.includeLockData, - }); + const discovery = await queryV0BalanceFromMnemonic(mnemonic, grpcEndpoint, passphrase, childIndex); + const built = await buildV0MigrationTransaction( + discovery.v0Notes, + targetV1Pkh, + feePerWord, + includeLockData, + settings + ); return { ...built, @@ -198,14 +197,9 @@ export async function buildV0MigrationFromMnemonic( } export type { - BuildV0MigrationFromMnemonicParams, BuildV0MigrationFromMnemonicResult, - BuildV0MigrationTransactionParams, BuildV0MigrationTransactionResult, - DeriveV0AddressParams, DerivedV0Address, - QueryV0BalanceParams, - QueryV0BalanceFromMnemonicParams, QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, } from './migration-types.js'; From 8d0c8ca2c0a224fdb33b66259227fe5bc33e94b8 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:29:42 -0500 Subject: [PATCH 19/75] temporary commit for testing: use the smallest note for migration --- src/migration-types.ts | 9 +++ src/migration.ts | 162 ++++++++++++++++++++++++++++++++++++++--- 2 files changed, 161 insertions(+), 10 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index 1aa3694..e2cdc9f 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -43,6 +43,15 @@ export interface BuildV0MigrationTransactionResult { }; } +/** Result of single-note migration (matches extension logic). */ +export interface BuildV0MigrationSingleNoteResult extends BuildV0MigrationTransactionResult { + migratedNicks: Nicks; + migratedNock: number; + selectedNoteNicks: Nicks; + selectedNoteNock: number; + feeNock: number; +} + /** Result of mnemonic-based migration build. */ export interface BuildV0MigrationFromMnemonicResult extends BuildV0MigrationTransactionResult { diff --git a/src/migration.ts b/src/migration.ts index d99607f..4fd41bc 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,6 +1,7 @@ import type { BuildV0MigrationFromMnemonicResult, BuildV0MigrationTransactionResult, + BuildV0MigrationSingleNoteResult, DerivedV0Address, QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, @@ -35,6 +36,8 @@ function sumNicks(notes: NoteV0[]): string { return total.toString(); } +const NOCK_TO_NICKS = 65_536; + /** * Derive legacy v0 address metadata from mnemonic. * @@ -48,9 +51,10 @@ export function deriveV0AddressFromMnemonic( const master = wasm.deriveMasterKeyFromMnemonic(mnemonic, passphrase ?? ''); const key = childIndex === undefined ? master : master.deriveChild(childIndex); const publicKey = Uint8Array.from(key.publicKey); + const sourceAddress = base58.encode(publicKey); return { - sourceAddress: base58.encode(publicKey), + sourceAddress, }; } @@ -106,29 +110,48 @@ export async function queryV0BalanceFromMnemonic( }; } +/** Patch 1 (Bythos) - fee auto-calculated via recalcAndSetFee */ const DEFAULT_TX_ENGINE_SETTINGS: TxEngineSettings = { tx_engine_version: 1, - tx_engine_patch: 0, + tx_engine_patch: 1, min_fee: '256', - cost_per_word: '32768', - witness_word_div: 1, + cost_per_word: '16384', // 1 << 14 + witness_word_div: 4, }; /** * Build a transaction that migrates v0 notes into a v1 PKH lock. * Caller must have initialized WASM (e.g. await wasm.default()) before using. + * + * @param options.singleNoteOnly - [TEMPORARY] When true, uses single-note logic for testing. + * @param options.debug - [TEMPORARY] When true, logs the built result to console. */ export async function buildV0MigrationTransaction( v0Notes: NoteV0[], targetV1Pkh: string, feePerWord?: Nicks, includeLockData?: boolean, - settings?: Partial -): Promise { + settings?: Partial, + options?: { singleNoteOnly?: boolean; debug?: boolean } +): Promise { if (!v0Notes.length) { throw new Error('No v0 notes provided for migration'); } + const singleNoteOnly = options?.singleNoteOnly ?? false; + const debug = options?.debug ?? false; + + if (singleNoteOnly) { + const result = await buildV0MigrationTransactionSingleNote( + v0Notes, + targetV1Pkh, + feePerWord, + settings, + debug + ); + return result; + } + const includeLockDataVal = !!includeLockData; const txSettings: TxEngineSettings = { ...DEFAULT_TX_ENGINE_SETTINGS, @@ -156,7 +179,7 @@ export async function buildV0MigrationTransaction( spends: transaction.spends, }; - return { + const result: BuildV0MigrationTransactionResult = { transaction, txId, fee: feeResult, @@ -166,10 +189,88 @@ export async function buildV0MigrationTransaction( spendConditions: txNotes.spend_conditions, }, }; + + if (debug) { + console.log('[SDK Migration] buildV0MigrationTransaction (full)', result); + } + + return result; +} + +/** + * Single-note migration (same logic as regular path, but one note). + * Picks any of the smallest notes (there may be multiple with the same size). + */ +async function buildV0MigrationTransactionSingleNote( + v0Notes: NoteV0[], + targetV1Pkh: string, + feePerWord?: Nicks, + settings?: Partial, + debug?: boolean +): Promise { + const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); + const txSettings: TxEngineSettings = { + ...DEFAULT_TX_ENGINE_SETTINGS, + ...settings, + cost_per_word: feePerWord ?? settings?.cost_per_word ?? DEFAULT_TX_ENGINE_SETTINGS.cost_per_word, + }; + const builder = new wasm.TxBuilder(txSettings); + + const candidates: Array<{ note: NoteV0; assets: bigint }> = v0Notes.map(note => ({ + note, + assets: BigInt(note.assets), + })); + + if (!candidates.length) { + throw new Error('No v0 notes to migrate.'); + } + + const minAssets = candidates.reduce((min, c) => (c.assets < min ? c.assets : min), candidates[0].assets); + const selected = candidates.find(c => c.assets === minAssets)!; + + const spendBuilder = new wasm.SpendBuilder(selected.note, null, targetSpendCondition); + spendBuilder.computeRefund(false); + builder.spend(spendBuilder); + + builder.recalcAndSetFee(false); + const feeNicks = builder.calcFee(); + const transaction = builder.build(); + const txNotes = builder.allNotes() as TxNotes; + const rawTx: RawTxV1 = { + version: 1, + id: transaction.id, + spends: transaction.spends, + }; + + const feeNicksBigInt = BigInt(feeNicks); + const result: BuildV0MigrationSingleNoteResult = { + transaction, + txId: transaction.id, + fee: feeNicks, + migratedNicks: selected.assets.toString(), + migratedNock: Number(selected.assets) / NOCK_TO_NICKS, + selectedNoteNicks: selected.assets.toString(), + selectedNoteNock: Number(selected.assets) / NOCK_TO_NICKS, + feeNock: Number(feeNicksBigInt) / NOCK_TO_NICKS, + signRawTxPayload: { + rawTx, + notes: txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)), + spendConditions: txNotes.spend_conditions, + }, + }; + + if (debug) { + console.log('[SDK Migration] buildV0MigrationTransactionSingleNote', result); + } + + return result; } /** * Derive v0 address, query legacy notes, and build migration transaction in one step. + * + * @param options.singleNoteOnly - [TEMPORARY] When true, migrates 200 NOCK from one note. + * @param options.debug - [TEMPORARY] When true, logs the built result to console. */ export async function buildV0MigrationFromMnemonic( mnemonic: string, @@ -179,26 +280,67 @@ export async function buildV0MigrationFromMnemonic( childIndex?: number, feePerWord?: Nicks, includeLockData?: boolean, - settings?: Partial + settings?: Partial, + options?: { singleNoteOnly?: boolean; debug?: boolean } ): Promise { const discovery = await queryV0BalanceFromMnemonic(mnemonic, grpcEndpoint, passphrase, childIndex); + const buildOptions = options?.singleNoteOnly + ? { singleNoteOnly: true as const, debug: options?.debug } + : { debug: options?.debug }; const built = await buildV0MigrationTransaction( discovery.v0Notes, targetV1Pkh, feePerWord, includeLockData, - settings + settings, + buildOptions ); - return { + const result = { ...built, discovery, }; + + if (options?.debug) { + console.log('[SDK Migration] buildV0MigrationFromMnemonic', result); + } + + return result; +} + +/** + * Build migration from protobuf notes (matches extension API). + * Caller must have initialized WASM before using. + */ +export async function buildV0MigrationTransactionFromNotes( + v0NotesProtobuf: unknown[], + targetV1Pkh: string, + feePerWord: Nicks = '32768', + options?: { debug?: boolean } +): Promise { + const v0Notes: NoteV0[] = []; + for (const notePb of v0NotesProtobuf) { + const parsed = wasm.note_from_protobuf(notePb as Parameters[0]); + if (isNoteV0(parsed)) { + v0Notes.push(parsed); + } + } + + const result = await buildV0MigrationTransactionSingleNote( + v0Notes, + targetV1Pkh, + feePerWord, + undefined, + options?.debug ?? false + ); + + return result; } export type { BuildV0MigrationFromMnemonicResult, BuildV0MigrationTransactionResult, + BuildV0MigrationSingleNoteResult, DerivedV0Address, QueryV0BalanceFromMnemonicResult, QueryV0BalanceResult, From 079f53538bcbd6fb8b5539eb84def8bbae56cd10 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:16:46 -0500 Subject: [PATCH 20/75] add spend condition to the rebuilt transaction to satisfy the API --- src/migration.ts | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index 4fd41bc..321d85d 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -179,14 +179,19 @@ export async function buildV0MigrationTransaction( spends: transaction.spends, }; + const inputNotes = txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)); + const spendConditions = + txNotes.spend_conditions.length === inputNotes.length + ? txNotes.spend_conditions + : inputNotes.map(() => targetSpendCondition); const result: BuildV0MigrationTransactionResult = { transaction, txId, fee: feeResult, signRawTxPayload: { rawTx, - notes: txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)), - spendConditions: txNotes.spend_conditions, + notes: inputNotes, + spendConditions, }, }; @@ -243,6 +248,11 @@ async function buildV0MigrationTransactionSingleNote( }; const feeNicksBigInt = BigInt(feeNicks); + const inputNotes = txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)); + const spendConditions = + txNotes.spend_conditions.length === inputNotes.length + ? txNotes.spend_conditions + : inputNotes.map(() => targetSpendCondition); const result: BuildV0MigrationSingleNoteResult = { transaction, txId: transaction.id, @@ -254,8 +264,8 @@ async function buildV0MigrationTransactionSingleNote( feeNock: Number(feeNicksBigInt) / NOCK_TO_NICKS, signRawTxPayload: { rawTx, - notes: txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)), - spendConditions: txNotes.spend_conditions, + notes: inputNotes, + spendConditions, }, }; From dc5101cbf41e407ca335718fc00d17d586a3ac44 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 4 Mar 2026 10:43:33 -0500 Subject: [PATCH 21/75] fix RPC --- src/migration.ts | 47 +++++++++++++++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 12 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index 321d85d..452ba71 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -38,6 +38,12 @@ function sumNicks(notes: NoteV0[]): string { const NOCK_TO_NICKS = 65_536; +function normalizeGrpcEndpoint(endpoint: string): string { + const trimmed = endpoint?.trim() || ''; + if (!trimmed) return trimmed; + return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; +} + /** * Derive legacy v0 address metadata from mnemonic. * @@ -49,13 +55,18 @@ export function deriveV0AddressFromMnemonic( childIndex?: number ): DerivedV0Address { const master = wasm.deriveMasterKeyFromMnemonic(mnemonic, passphrase ?? ''); - const key = childIndex === undefined ? master : master.deriveChild(childIndex); - const publicKey = Uint8Array.from(key.publicKey); - const sourceAddress = base58.encode(publicKey); - - return { - sourceAddress, - }; + try { + const key = childIndex === undefined ? master : master.deriveChild(childIndex); + try { + const publicKey = Uint8Array.from(key.publicKey); + const sourceAddress = base58.encode(publicKey); + return { sourceAddress }; + } finally { + if (key !== master) key.free(); + } + } finally { + master.free(); + } } /** @@ -70,7 +81,8 @@ export async function queryV0BalanceForAddress( throw new Error('address is required'); } - const grpcClient = new wasm.GrpcClient(grpcEndpoint); + const normalizedEndpoint = normalizeGrpcEndpoint(grpcEndpoint); + const grpcClient = new wasm.GrpcClient(normalizedEndpoint); const balance = await grpcClient.getBalanceByAddress(address); const v0Notes: NoteV0[] = []; @@ -94,6 +106,8 @@ export async function queryV0BalanceForAddress( /** * Derive v0 discovery address from mnemonic and query legacy notes in one step. + * Tries master key first; if no Legacy notes found, retries with child index 0 + * (some v0 wallets used child derivation). */ export async function queryV0BalanceFromMnemonic( mnemonic: string, @@ -104,10 +118,19 @@ export async function queryV0BalanceFromMnemonic( const derived = deriveV0AddressFromMnemonic(mnemonic, passphrase, childIndex); const queried = await queryV0BalanceForAddress(grpcEndpoint, derived.sourceAddress); - return { - ...derived, - ...queried, - }; + if (queried.v0Notes.length > 0) { + return { ...derived, ...queried }; + } + + if (childIndex === undefined) { + const derivedChild0 = deriveV0AddressFromMnemonic(mnemonic, passphrase, 0); + const queriedChild0 = await queryV0BalanceForAddress(grpcEndpoint, derivedChild0.sourceAddress); + if (queriedChild0.v0Notes.length > 0) { + return { ...derivedChild0, ...queriedChild0 }; + } + } + + return { ...derived, ...queried }; } /** Patch 1 (Bythos) - fee auto-calculated via recalcAndSetFee */ From c0101ac63a967f73d63d22a5e37afecdf2afe57c Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 9 Mar 2026 11:31:35 -0400 Subject: [PATCH 22/75] Remove vendor/iris-wasm from package, add vendor/ to gitignore Made-with: Cursor --- .gitignore | 1 + package.json | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index ea00288..2ed29cb 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ lib/nbx-wasm # Build output dist/ examples-dist/ +vendor/ # TypeScript *.tsbuildinfo diff --git a/package.json b/package.json index 812c6a7..46b3e6a 100644 --- a/package.json +++ b/package.json @@ -21,8 +21,7 @@ }, "files": [ "dist", - "README.md", - "vendor/iris-wasm" + "README.md" ], "scripts": { "build": "tsc", From 2b989a89d9b7ba8a06b6ba0077e786fbcae0e948 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 10 Mar 2026 13:26:28 -0400 Subject: [PATCH 23/75] update iris-wasm API calls --- src/bridge.ts | 14 +++++++------- src/migration.ts | 30 ++++++++++++------------------ 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index 827de22..fce7943 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -143,11 +143,11 @@ export async function buildBridgeTransaction( const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); const noteData: NoteData = [[config.noteDataKey, bridgeNounJs as Noun]]; - const bridgeSpendCondition: SpendCondition = [ - { Pkh: { m: config.threshold, hashes: config.addresses } }, - ]; - const bridgeLockRoot: LockRoot = { Lock: bridgeSpendCondition }; - const refundLock: SpendCondition = [{ Pkh: { m: 1, hashes: [params.refundPkh] } }]; + const bridgePkh = wasm.pkhNew(BigInt(config.threshold), config.addresses); + const bridgeSpendCondition: SpendCondition = wasm.spendConditionNewPkh(bridgePkh); + const bridgeLockRoot: LockRoot = bridgeSpendCondition; + const refundPkhObj = wasm.pkhSingle(params.refundPkh); + const refundLock: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); const costPerWord = params.feeOverride ?? config.feePerWord; const builder = new wasm.TxBuilder({ @@ -162,9 +162,9 @@ export async function buildBridgeTransaction( const note = params.inputNotes[i]; const spendCondition = params.spendConditions[i]; - const spendBuilder = new wasm.SpendBuilder(note, spendCondition, refundLock); + const spendBuilder = new wasm.SpendBuilder(note, spendCondition, null, refundLock); - const parentHash = wasm.note_hash(note); + const parentHash = wasm.noteHash(note); const seed: SeedV1 = { output_source: null, lock_root: bridgeLockRoot, diff --git a/src/migration.ts b/src/migration.ts index 452ba71..76c48eb 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -14,13 +14,13 @@ import type { RawTxV1, SpendCondition, TxEngineSettings, - TxNotes, } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; function buildSinglePkhSpendCondition(pkh: string): SpendCondition { - return [{ Pkh: { m: 1, hashes: [pkh] } }]; + const pkhObj = wasm.pkhSingle(pkh); + return wasm.spendConditionNewPkh(pkhObj); } function isLegacyEntry(entry: PbCom2BalanceEntry): boolean { @@ -91,7 +91,7 @@ export async function queryV0BalanceForAddress( if (!isLegacyEntry(entry) || !entry.note) { continue; } - const parsed = wasm.note_from_protobuf(entry.note); + const parsed = wasm.noteFromProtobuf(entry.note); if (isNoteV0(parsed)) { v0Notes.push(parsed); } @@ -185,7 +185,7 @@ export async function buildV0MigrationTransaction( const builder = new wasm.TxBuilder(txSettings); for (const note of v0Notes) { - const spendBuilder = new wasm.SpendBuilder(note, null, targetSpendCondition); + const spendBuilder = new wasm.SpendBuilder(note, targetSpendCondition, null, null); // Use refund path to migrate full note value into target lock. spendBuilder.computeRefund(includeLockDataVal); builder.spend(spendBuilder); @@ -194,7 +194,7 @@ export async function buildV0MigrationTransaction( builder.recalcAndSetFee(includeLockDataVal); const feeResult = builder.calcFee(); const transaction = builder.build(); - const txNotes = builder.allNotes() as TxNotes; + const allNotes = builder.allNotes(); const txId = transaction.id; const rawTx: RawTxV1 = { version: 1, @@ -202,11 +202,8 @@ export async function buildV0MigrationTransaction( spends: transaction.spends, }; - const inputNotes = txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)); - const spendConditions = - txNotes.spend_conditions.length === inputNotes.length - ? txNotes.spend_conditions - : inputNotes.map(() => targetSpendCondition); + const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); + const spendConditions = inputNotes.map(() => targetSpendCondition); const result: BuildV0MigrationTransactionResult = { transaction, txId, @@ -256,14 +253,14 @@ async function buildV0MigrationTransactionSingleNote( const minAssets = candidates.reduce((min, c) => (c.assets < min ? c.assets : min), candidates[0].assets); const selected = candidates.find(c => c.assets === minAssets)!; - const spendBuilder = new wasm.SpendBuilder(selected.note, null, targetSpendCondition); + const spendBuilder = new wasm.SpendBuilder(selected.note, targetSpendCondition, null, null); spendBuilder.computeRefund(false); builder.spend(spendBuilder); builder.recalcAndSetFee(false); const feeNicks = builder.calcFee(); const transaction = builder.build(); - const txNotes = builder.allNotes() as TxNotes; + const allNotes = builder.allNotes(); const rawTx: RawTxV1 = { version: 1, id: transaction.id, @@ -271,11 +268,8 @@ async function buildV0MigrationTransactionSingleNote( }; const feeNicksBigInt = BigInt(feeNicks); - const inputNotes = txNotes.notes.filter((note): note is NoteV0 => isNoteV0(note)); - const spendConditions = - txNotes.spend_conditions.length === inputNotes.length - ? txNotes.spend_conditions - : inputNotes.map(() => targetSpendCondition); + const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); + const spendConditions = inputNotes.map(() => targetSpendCondition); const result: BuildV0MigrationSingleNoteResult = { transaction, txId: transaction.id, @@ -353,7 +347,7 @@ export async function buildV0MigrationTransactionFromNotes( ): Promise { const v0Notes: NoteV0[] = []; for (const notePb of v0NotesProtobuf) { - const parsed = wasm.note_from_protobuf(notePb as Parameters[0]); + const parsed = wasm.noteFromProtobuf(notePb as Parameters[0]); if (isNoteV0(parsed)) { v0Notes.push(parsed); } From 2bb34b47d7bb462cc88376e0852a88bd3f180d95 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 11 Mar 2026 14:26:18 -0400 Subject: [PATCH 24/75] clean up migration logic --- src/migration-types.ts | 63 +++---- src/migration.ts | 406 ++++++++++++----------------------------- 2 files changed, 143 insertions(+), 326 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index e2cdc9f..456503b 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -1,9 +1,9 @@ /** * Types for querying v0 balance and building v0 -> v1 migration transactions. - * Aligned with @nockbox/iris-wasm (Nicks, NockchainTx, NoteV0, RawTx, SpendCondition). + * Aligned with @nockbox/iris-wasm (Nicks, NoteV0, RawTx, SpendCondition). */ import type { - NockchainTx, + LockRoot, Nicks, NoteV0, PbCom2Balance, @@ -13,47 +13,38 @@ import type { export type { Nicks }; -/** Legacy address metadata derived from mnemonic. */ -export interface DerivedV0Address { - /** Base58 bare public key (legacy address form). */ - sourceAddress: string; -} +/** Base58 bare public key derived from mnemonic. */ +export type DerivedV0Address = string; -/** Result of querying v0 balance. */ -export interface QueryV0BalanceResult { +/** Result of querying v0 balance. Use this to construct a migration transaction. */ +export interface V0BalanceResult { + sourceAddress: string; balance: PbCom2Balance; v0Notes: NoteV0[]; totalNicks: Nicks; + totalNock: number; + smallestNoteNock?: number; + rawNotesFromRpc?: number; } -/** Result of mnemonic-based v0 discovery (derived address + balance). */ -export interface QueryV0BalanceFromMnemonicResult - extends QueryV0BalanceResult, - DerivedV0Address {} - -/** Result of building a migration transaction. */ -export interface BuildV0MigrationTransactionResult { - transaction: NockchainTx; - txId: string; - fee: Nicks; - signRawTxPayload: { +/** buildV0MigrationTx result: balance fields always present; tx fields when target provided and build succeeded. */ +export interface BuildV0MigrationTxResult { + sourceAddress: string; + balance: PbCom2Balance; + v0Notes: NoteV0[]; + totalNicks: Nicks; + totalNock: number; + smallestNoteNock?: number; + rawNotesFromRpc?: number; + txId?: string; + fee?: Nicks; + feeNock?: number; + signRawTxPayload?: { rawTx: RawTx; notes: NoteV0[]; - spendConditions: SpendCondition[]; + spendConditions: (SpendCondition | null)[]; + refundLock: LockRoot; }; -} - -/** Result of single-note migration (matches extension logic). */ -export interface BuildV0MigrationSingleNoteResult extends BuildV0MigrationTransactionResult { - migratedNicks: Nicks; - migratedNock: number; - selectedNoteNicks: Nicks; - selectedNoteNock: number; - feeNock: number; -} - -/** Result of mnemonic-based migration build. */ -export interface BuildV0MigrationFromMnemonicResult - extends BuildV0MigrationTransactionResult { - discovery: QueryV0BalanceFromMnemonicResult; + migratedNicks?: Nicks; + migratedNock?: number; } diff --git a/src/migration.ts b/src/migration.ts index 76c48eb..2bd9cac 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,10 +1,7 @@ import type { - BuildV0MigrationFromMnemonicResult, - BuildV0MigrationTransactionResult, - BuildV0MigrationSingleNoteResult, + BuildV0MigrationTxResult, DerivedV0Address, - QueryV0BalanceFromMnemonicResult, - QueryV0BalanceResult, + V0BalanceResult, } from './migration-types.js'; import type { Note, @@ -38,52 +35,30 @@ function sumNicks(notes: NoteV0[]): string { const NOCK_TO_NICKS = 65_536; -function normalizeGrpcEndpoint(endpoint: string): string { - const trimmed = endpoint?.trim() || ''; - if (!trimmed) return trimmed; - return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`; -} - /** - * Derive legacy v0 address metadata from mnemonic. - * - * v0 discovery queries use the base58-encoded bare public key ("sourceAddress"). + * Derive legacy v0 address (base58 bare public key) from mnemonic. */ -export function deriveV0AddressFromMnemonic( - mnemonic: string, - passphrase?: string, - childIndex?: number -): DerivedV0Address { - const master = wasm.deriveMasterKeyFromMnemonic(mnemonic, passphrase ?? ''); +export function deriveV0AddressFromMnemonic(mnemonic: string): DerivedV0Address { + const master = wasm.deriveMasterKeyFromMnemonic(mnemonic); try { - const key = childIndex === undefined ? master : master.deriveChild(childIndex); - try { - const publicKey = Uint8Array.from(key.publicKey); - const sourceAddress = base58.encode(publicKey); - return { sourceAddress }; - } finally { - if (key !== master) key.free(); - } + const publicKey = Uint8Array.from(master.publicKey); + return base58.encode(publicKey); } finally { master.free(); } } /** - * Query address balance and return only v0 (Legacy) notes. + * Query v0 (Legacy) balance for a mnemonic. Discovery only; does not build a transaction. * Caller must have initialized WASM (e.g. await wasm.default()) before using. */ -export async function queryV0BalanceForAddress( - grpcEndpoint: string, - address: string -): Promise { - if (!address) { - throw new Error('address is required'); - } - - const normalizedEndpoint = normalizeGrpcEndpoint(grpcEndpoint); - const grpcClient = new wasm.GrpcClient(normalizedEndpoint); - const balance = await grpcClient.getBalanceByAddress(address); +export async function queryV0Balance( + mnemonic: string, + grpcEndpoint: string +): Promise { + const sourceAddress = deriveV0AddressFromMnemonic(mnemonic); + const grpcClient = new wasm.GrpcClient(grpcEndpoint); + const balance = await grpcClient.getBalanceByAddress(sourceAddress); const v0Notes: NoteV0[] = []; const entries = balance.notes ?? []; @@ -97,278 +72,129 @@ export async function queryV0BalanceForAddress( } } + const totalNicks = sumNicks(v0Notes); + const totalNock = Number(BigInt(totalNicks)) / NOCK_TO_NICKS; + const smallestNoteNock = + v0Notes.length > 0 + ? Number(v0Notes.reduce((min, n) => (BigInt(n.assets) < min ? BigInt(n.assets) : min), BigInt(v0Notes[0].assets))) / + NOCK_TO_NICKS + : undefined; + return { + sourceAddress, balance, v0Notes, - totalNicks: sumNicks(v0Notes), + totalNicks, + totalNock, + smallestNoteNock, + rawNotesFromRpc: entries.length, }; } -/** - * Derive v0 discovery address from mnemonic and query legacy notes in one step. - * Tries master key first; if no Legacy notes found, retries with child index 0 - * (some v0 wallets used child derivation). - */ -export async function queryV0BalanceFromMnemonic( - mnemonic: string, - grpcEndpoint: string, - passphrase?: string, - childIndex?: number -): Promise { - const derived = deriveV0AddressFromMnemonic(mnemonic, passphrase, childIndex); - const queried = await queryV0BalanceForAddress(grpcEndpoint, derived.sourceAddress); - - if (queried.v0Notes.length > 0) { - return { ...derived, ...queried }; - } - - if (childIndex === undefined) { - const derivedChild0 = deriveV0AddressFromMnemonic(mnemonic, passphrase, 0); - const queriedChild0 = await queryV0BalanceForAddress(grpcEndpoint, derivedChild0.sourceAddress); - if (queriedChild0.v0Notes.length > 0) { - return { ...derivedChild0, ...queriedChild0 }; - } - } - - return { ...derived, ...queried }; +function defaultTxEngineSettings(): TxEngineSettings { + return wasm.txEngineSettingsV1BythosDefault(); } -/** Patch 1 (Bythos) - fee auto-calculated via recalcAndSetFee */ -const DEFAULT_TX_ENGINE_SETTINGS: TxEngineSettings = { - tx_engine_version: 1, - tx_engine_patch: 1, - min_fee: '256', - cost_per_word: '16384', // 1 << 14 - witness_word_div: 4, -}; - /** - * Build a transaction that migrates v0 notes into a v1 PKH lock. + * Fetch v0 balance and optionally build migration tx to a v1 PKH lock. * Caller must have initialized WASM (e.g. await wasm.default()) before using. * - * @param options.singleNoteOnly - [TEMPORARY] When true, uses single-note logic for testing. - * @param options.debug - [TEMPORARY] When true, logs the built result to console. + * @param targetV1Pkh - When provided, builds the migration tx. Omit for balance only. + * @param options.debug - When true, logs the built result to console. */ -export async function buildV0MigrationTransaction( - v0Notes: NoteV0[], - targetV1Pkh: string, - feePerWord?: Nicks, - includeLockData?: boolean, - settings?: Partial, - options?: { singleNoteOnly?: boolean; debug?: boolean } -): Promise { - if (!v0Notes.length) { - throw new Error('No v0 notes provided for migration'); - } - - const singleNoteOnly = options?.singleNoteOnly ?? false; - const debug = options?.debug ?? false; - - if (singleNoteOnly) { - const result = await buildV0MigrationTransactionSingleNote( - v0Notes, - targetV1Pkh, - feePerWord, - settings, - debug - ); - return result; - } - - const includeLockDataVal = !!includeLockData; - const txSettings: TxEngineSettings = { - ...DEFAULT_TX_ENGINE_SETTINGS, - ...settings, - cost_per_word: feePerWord ?? settings?.cost_per_word ?? DEFAULT_TX_ENGINE_SETTINGS.cost_per_word, - }; - const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); - const builder = new wasm.TxBuilder(txSettings); - - for (const note of v0Notes) { - const spendBuilder = new wasm.SpendBuilder(note, targetSpendCondition, null, null); - // Use refund path to migrate full note value into target lock. - spendBuilder.computeRefund(includeLockDataVal); - builder.spend(spendBuilder); - } - - builder.recalcAndSetFee(includeLockDataVal); - const feeResult = builder.calcFee(); - const transaction = builder.build(); - const allNotes = builder.allNotes(); - const txId = transaction.id; - const rawTx: RawTxV1 = { - version: 1, - id: transaction.id, - spends: transaction.spends, - }; - - const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); - const spendConditions = inputNotes.map(() => targetSpendCondition); - const result: BuildV0MigrationTransactionResult = { - transaction, - txId, - fee: feeResult, - signRawTxPayload: { - rawTx, - notes: inputNotes, - spendConditions, - }, - }; - - if (debug) { - console.log('[SDK Migration] buildV0MigrationTransaction (full)', result); - } - - return result; -} - -/** - * Single-note migration (same logic as regular path, but one note). - * Picks any of the smallest notes (there may be multiple with the same size). - */ -async function buildV0MigrationTransactionSingleNote( - v0Notes: NoteV0[], - targetV1Pkh: string, - feePerWord?: Nicks, - settings?: Partial, - debug?: boolean -): Promise { - const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); - const txSettings: TxEngineSettings = { - ...DEFAULT_TX_ENGINE_SETTINGS, - ...settings, - cost_per_word: feePerWord ?? settings?.cost_per_word ?? DEFAULT_TX_ENGINE_SETTINGS.cost_per_word, - }; - const builder = new wasm.TxBuilder(txSettings); - - const candidates: Array<{ note: NoteV0; assets: bigint }> = v0Notes.map(note => ({ - note, - assets: BigInt(note.assets), - })); - - if (!candidates.length) { - throw new Error('No v0 notes to migrate.'); - } - - const minAssets = candidates.reduce((min, c) => (c.assets < min ? c.assets : min), candidates[0].assets); - const selected = candidates.find(c => c.assets === minAssets)!; - - const spendBuilder = new wasm.SpendBuilder(selected.note, targetSpendCondition, null, null); - spendBuilder.computeRefund(false); - builder.spend(spendBuilder); - - builder.recalcAndSetFee(false); - const feeNicks = builder.calcFee(); - const transaction = builder.build(); - const allNotes = builder.allNotes(); - const rawTx: RawTxV1 = { - version: 1, - id: transaction.id, - spends: transaction.spends, - }; - - const feeNicksBigInt = BigInt(feeNicks); - const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); - const spendConditions = inputNotes.map(() => targetSpendCondition); - const result: BuildV0MigrationSingleNoteResult = { - transaction, - txId: transaction.id, - fee: feeNicks, - migratedNicks: selected.assets.toString(), - migratedNock: Number(selected.assets) / NOCK_TO_NICKS, - selectedNoteNicks: selected.assets.toString(), - selectedNoteNock: Number(selected.assets) / NOCK_TO_NICKS, - feeNock: Number(feeNicksBigInt) / NOCK_TO_NICKS, - signRawTxPayload: { - rawTx, - notes: inputNotes, - spendConditions, - }, - }; - - if (debug) { - console.log('[SDK Migration] buildV0MigrationTransactionSingleNote', result); - } - - return result; -} - -/** - * Derive v0 address, query legacy notes, and build migration transaction in one step. - * - * @param options.singleNoteOnly - [TEMPORARY] When true, migrates 200 NOCK from one note. - * @param options.debug - [TEMPORARY] When true, logs the built result to console. - */ -export async function buildV0MigrationFromMnemonic( +export async function buildV0MigrationTx( mnemonic: string, grpcEndpoint: string, - targetV1Pkh: string, - passphrase?: string, - childIndex?: number, - feePerWord?: Nicks, - includeLockData?: boolean, - settings?: Partial, - options?: { singleNoteOnly?: boolean; debug?: boolean } -): Promise { - const discovery = await queryV0BalanceFromMnemonic(mnemonic, grpcEndpoint, passphrase, childIndex); - const buildOptions = options?.singleNoteOnly - ? { singleNoteOnly: true as const, debug: options?.debug } - : { debug: options?.debug }; - const built = await buildV0MigrationTransaction( - discovery.v0Notes, - targetV1Pkh, - feePerWord, - includeLockData, - settings, - buildOptions - ); - - const result = { - ...built, - discovery, - }; - - if (options?.debug) { - console.log('[SDK Migration] buildV0MigrationFromMnemonic', result); + targetV1Pkh?: string, + options?: { debug?: boolean } +): Promise { + const balanceResult = await queryV0Balance(mnemonic, grpcEndpoint); + if (!targetV1Pkh) { + return balanceResult; } - return result; -} + const debug = options?.debug ?? false; + const useSingleNote = debug; -/** - * Build migration from protobuf notes (matches extension API). - * Caller must have initialized WASM before using. - */ -export async function buildV0MigrationTransactionFromNotes( - v0NotesProtobuf: unknown[], - targetV1Pkh: string, - feePerWord: Nicks = '32768', - options?: { debug?: boolean } -): Promise { - const v0Notes: NoteV0[] = []; - for (const notePb of v0NotesProtobuf) { - const parsed = wasm.noteFromProtobuf(notePb as Parameters[0]); - if (isNoteV0(parsed)) { - v0Notes.push(parsed); + try { + const v0Notes = balanceResult.v0Notes; + if (!v0Notes.length) { + throw new Error('No v0 notes to migrate'); } - } - const result = await buildV0MigrationTransactionSingleNote( - v0Notes, - targetV1Pkh, - feePerWord, - undefined, - options?.debug ?? false - ); + const notesToUse: NoteV0[] = useSingleNote + ? (() => { + const sorted = [...v0Notes] + .map(n => ({ note: n, assets: BigInt(n.assets) })) + .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0)); + return [sorted[0].note]; + })() + : v0Notes; + + const txSettings = defaultTxEngineSettings(); + const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); + const refundLock = wasm.locky(targetSpendCondition); + const builder = new wasm.TxBuilder(txSettings); + + for (const note of notesToUse) { + const spendBuilder = new wasm.SpendBuilder(note, null, null, refundLock); + spendBuilder.computeRefund(false); + builder.spend(spendBuilder); + } - return result; + builder.recalcAndSetFee(false); + const feeNicks = builder.curFee(); + const transaction = builder.build(); + const allNotes = builder.allNotes(); + const rawTx: RawTxV1 = { + version: 1, + id: transaction.id, + spends: transaction.spends, + }; + + const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); + const feeNock = Number(BigInt(feeNicks)) / NOCK_TO_NICKS; + + const migrated = useSingleNote + ? (() => { + const note = notesToUse[0]; + const nock = Number(BigInt(note.assets)) / NOCK_TO_NICKS; + return { + migratedNicks: BigInt(note.assets).toString(), + migratedNock: nock, + }; + })() + : { + migratedNicks: (BigInt(balanceResult.totalNicks) - BigInt(feeNicks)).toString(), + migratedNock: balanceResult.totalNock - feeNock, + }; + + const result: BuildV0MigrationTxResult = { + ...balanceResult, + txId: transaction.id, + fee: feeNicks, + feeNock, + signRawTxPayload: { + rawTx, + notes: inputNotes, + spendConditions: inputNotes.map(() => null), + refundLock, + }, + ...migrated, + }; + + if (debug) { + console.log('[SDK Migration] buildV0MigrationTx', result); + } + return result; + } catch (e) { + console.warn('[SDK Migration] Build failed, returning balance only:', e); + return balanceResult; + } } export type { - BuildV0MigrationFromMnemonicResult, - BuildV0MigrationTransactionResult, - BuildV0MigrationSingleNoteResult, + BuildV0MigrationTxResult, DerivedV0Address, - QueryV0BalanceFromMnemonicResult, - QueryV0BalanceResult, + V0BalanceResult, } from './migration-types.js'; From 82e165e15f54c63ebaebb5d065d8a04f239d28a8 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 12 Mar 2026 14:46:42 -0400 Subject: [PATCH 25/75] update and fix bridging logic --- src/bridge-types.ts | 11 +++++++++++ src/bridge.ts | 46 +++++++++++++++++++++++++++------------------ src/index.ts | 1 + 3 files changed, 40 insertions(+), 18 deletions(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index c0b0c9d..1552038 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -30,6 +30,15 @@ export interface BridgeConfig { expectedLockRoot?: string; } +/** Tx engine settings for TxBuilder (matches wasm.TxEngineSettings). */ +export interface TxEngineSettings { + tx_engine_version: 0 | 1 | 2; + tx_engine_patch: number; + min_fee: string; + cost_per_word: string; + witness_word_div: number; +} + /** * Parameters for building a bridge transaction. * Input notes and spend conditions are supplied by the consumer (e.g. from gRPC). @@ -47,6 +56,8 @@ export interface BridgeTransactionParams { refundPkh: string; /** Optional fee override in nicks */ feeOverride?: Nicks; + /** Optional: tx engine settings (Bythos at block ≥54000). When provided, overrides config.feePerWord. */ + txEngineSettings?: TxEngineSettings; } /** diff --git a/src/bridge.ts b/src/bridge.ts index fce7943..0e3f984 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -149,37 +149,46 @@ export async function buildBridgeTransaction( const refundPkhObj = wasm.pkhSingle(params.refundPkh); const refundLock: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); - const costPerWord = params.feeOverride ?? config.feePerWord; - const builder = new wasm.TxBuilder({ - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256', - cost_per_word: costPerWord, - witness_word_div: 1, - }); + const txSettings = + params.txEngineSettings ?? { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256', + cost_per_word: params.feeOverride ?? config.feePerWord, + witness_word_div: 1, + }; + const builder = new wasm.TxBuilder(txSettings); + + let remainingGift = BigInt(params.amountInNicks); for (let i = 0; i < params.inputNotes.length; i++) { const note = params.inputNotes[i]; const spendCondition = params.spendConditions[i]; + const noteAssets = BigInt(note.assets ?? 0); + + const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; + remainingGift -= giftPortion; const spendBuilder = new wasm.SpendBuilder(note, spendCondition, null, refundLock); - const parentHash = wasm.noteHash(note); - const seed: SeedV1 = { - output_source: null, - lock_root: bridgeLockRoot, - note_data: noteData, - gift: params.amountInNicks, - parent_hash: parentHash, - }; + if (giftPortion > 0n) { + const parentHash = wasm.noteHash(note); + const seed: SeedV1 = { + output_source: null, + lock_root: bridgeLockRoot, + note_data: noteData, + gift: String(giftPortion), + parent_hash: parentHash, + }; + spendBuilder.seed(seed); + } - spendBuilder.seed(seed); spendBuilder.computeRefund(false); builder.spend(spendBuilder); } builder.recalcAndSetFee(false); - const feeResult = builder.calcFee(); + const feeResult = builder.curFee(); const transaction = builder.build(); const txId = transaction.id; @@ -385,4 +394,5 @@ export type { BridgeTransactionParams, BridgeTransactionResult, BridgeValidationResult, + TxEngineSettings, } from './bridge-types.js'; diff --git a/src/index.ts b/src/index.ts index 966d44d..b625923 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,5 +10,6 @@ export * from './migration.js'; export * from './errors.js'; export * from './constants.js'; export * from './compat.js'; +export * from './bridge.js'; export * as wasm from './wasm.js'; export { initWasm } from './wasm.js'; From 1cbf49ea7d216761b7aa6b86493697f4c119b1c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Aurimas=20Bla=C5=BEulionis?= <0x60@pm.me> Date: Fri, 20 Mar 2026 17:42:42 +0000 Subject: [PATCH 26/75] Define RPC API V1 and switch SDK to it --- package-lock.json | 9 +- package.json | 2 +- src/compat.ts | 335 ++++++++++++++++++++++++++++++----------- src/constants.ts | 43 +++++- src/index.ts | 1 - src/iris_wasm.guard.ts | 5 - src/provider.ts | 86 ++++------- src/transaction.ts | 173 --------------------- src/types.ts | 89 +++++++++-- src/wasm.ts | 7 +- 10 files changed, 402 insertions(+), 348 deletions(-) delete mode 100644 src/iris_wasm.guard.ts delete mode 100644 src/transaction.ts diff --git a/package-lock.json b/package-lock.json index 543009b..28a0083 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0-alpha.4", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.6", + "@nockbox/iris-wasm": "^0.2.0-alpha.10", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.6", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.6.tgz", - "integrity": "sha512-hyVH7PEvPCq0ti25tQ5AkvrINpnyXKnY6n3wKR9tKsbUiR/TJ4o/uc6gAmptf5vBY7d/BL/3HHcwptYFx+H79A==", + "version": "0.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.10.tgz", + "integrity": "sha512-/CuHKz1Q2In/CdAQfUn1LgyOvFbc5mVkH94ko8gOxV/HU81C1Ctg2XMRP5lX2FoRR3OonEbBC4InUtzOPkWJfw==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { @@ -746,6 +746,7 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", + "peer": true, "dependencies": { "undici-types": "~6.21.0" } diff --git a/package.json b/package.json index 694e709..051cda3 100644 --- a/package.json +++ b/package.json @@ -54,7 +54,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.6", + "@nockbox/iris-wasm": "^0.2.0-alpha.10", "@scure/base": "^2.0.0" } } diff --git a/src/compat.ts b/src/compat.ts index 2e088d5..70883ef 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -2,40 +2,28 @@ * Backward-compatibility helpers for SDK request payloads. */ -import type { Transaction, NicksLike } from './types.js'; -import type { +import type { SendTransactionRequest, NicksLike, RpcRequest, RpcResponse, ConnectRequest, ConnectResponse, SignMessageRequest, SignTxRequest, SignMessageResponse, SignTxResponse } from './types.js'; +import { PbCom2RawTransaction, PbCom2Note, PbCom2SpendCondition, - RawTx, - Note, + guard, + rawTxFromProtobuf, + rawTxV1ToNockchainTx, + nockchainTxToRawTx, + rawTxInputSpendConditions, + publicKeyFromHex, + publicKeyToHex, + RawTxV1, + noteToProtobuf, + spendConditionToProtobuf, SpendCondition, -} from '@nockbox/iris-wasm/iris_wasm.js'; -import * as guard from './iris_wasm.guard.js'; - -/** Simple send payload: to, amount, fee as nicks strings (for sendTransaction). */ -export interface CanonicalSendPayload { - to: string; - amount: string; - fee?: string; -} - -/** - * Normalize simple send input: amount/fee to nicks strings. - */ -export function normalizeSendTransaction(transaction: Transaction): CanonicalSendPayload { - const amount = parseNicksLike(transaction.amount, 'amount'); - const fee = transaction.fee === undefined ? undefined : parseNicksLike(transaction.fee, 'fee'); - - return { - to: transaction.to, - amount, - ...(fee !== undefined ? { fee } : {}), - }; -} + Digest, +} from './wasm.js'; +import { PROVIDER_METHODS, RPC_API_VERSION, DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS } from './constants.js'; /** Protobuf signRawTx payload (gRPC wire format). Only format supported at API boundary. */ -export interface LegacySignRawTxRequest { +interface LegacySignRawTxRequest { /** Raw transaction protobuf */ rawTx: PbCom2RawTransaction; /** Input notes (protobuf) */ @@ -44,21 +32,8 @@ export interface LegacySignRawTxRequest { spendConditions: PbCom2SpendCondition[]; } -/** Params for signRawTx. Protobuf only for now (native RawTx not supported at RPC boundary. */ -export type SignRawTxParams = LegacySignRawTxRequest; - -/** Native signRawTx payload (RawTx, Note[], SpendCondition[]). Used internally after protobuf conversion. */ -export interface NativeSignRawTxRequest { - /** Raw transaction (native) */ - rawTx: RawTx; - /** Input notes (native) */ - notes: Note[]; - /** Spend conditions (native) */ - spendConditions: SpendCondition[]; -} - /** Type guard: validates protobuf signRawTx payload. Use at API boundary (SDK + extension). */ -export function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRequest { +function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRequest { if (!obj || typeof obj !== 'object') return false; const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; return ( @@ -72,60 +47,248 @@ export function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRe ); } -/** Type guard: validates native signRawTx payload. Use internally after protobuf→native conversion. */ -export function isNativeSignRawTxPayload(obj: unknown): obj is NativeSignRawTxRequest { - if (!obj || typeof obj !== 'object') return false; - const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; - return ( - guard.isRawTx(p.rawTx) && - Array.isArray(p.notes) && - p.notes.length > 0 && - p.notes.every((n: unknown) => guard.isNote(n)) && - Array.isArray(p.spendConditions) && - p.spendConditions.length > 0 && - p.spendConditions.every((sc: unknown) => guard.isSpendCondition(sc)) - ); +interface LegacyConnectResponse { + grpcEndpoint: string; + pkh: Digest; } -function assertIntegerString(value: string, field: 'amount' | 'fee'): string { - const trimmed = value.trim(); - if (!/^-?\d+$/.test(trimmed)) { - throw new Error(`Invalid ${field}: expected an integer-like value`); - } - return trimmed; +interface LegacySignMessageResponse { + signature: string; + publicKeyHex: string } -/** - * Parse legacy number/bigint/string nicks into canonical string format. - */ -export function parseNicksLike(value: NicksLike, field: 'amount' | 'fee'): string { - if (typeof value === 'bigint') { - return value.toString(); - } +/** Map an RPC request from one API version to another. */ +function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcRequest { + if (fromApi === toApi) return request; + + const fromV1 = fromApi === RPC_API_VERSION; + const toV1 = toApi === RPC_API_VERSION; - if (typeof value === 'number') { - if (!Number.isFinite(value) || !Number.isInteger(value)) { - throw new Error(`Invalid ${field}: expected a finite integer number`); + switch (request.method) { + case PROVIDER_METHODS.CONNECT: { + if (fromV1 && !toV1) { + // API 1 → legacy: { api: string } → [] + return { ...request, params: [] }; + } + if (!fromV1 && toV1) { + // legacy → API 1: [] → { api: string } + const connectParams: ConnectRequest = { api: RPC_API_VERSION }; + return { ...request, params: connectParams as unknown }; + } + return request; + } + case PROVIDER_METHODS.SEND_TRANSACTION: { + if (fromV1 && !toV1) { + return { ...request, params: [request.params] }; + } + if (!fromV1 && toV1) { + const params = request.params as unknown as unknown[] | undefined; + return { ...request, params: params?.[0] }; + } + return request; + } + case PROVIDER_METHODS.SIGN_MESSAGE: { + if (fromV1 && !toV1) { + // API 1 → legacy: { message: string } → [message] + const params = request.params as unknown as SignMessageRequest | undefined; + return { ...request, params: params?.message ? [params.message] : request.params }; + } + if (!fromV1 && toV1) { + // legacy → API 1: [message] → { message: string } + const legacyParams = request.params as unknown as unknown[] | undefined; + const message = legacyParams?.[0] as string | undefined; + if (message) { + const signParams: SignMessageRequest = { message }; + return { ...request, params: signParams as unknown }; + } + } + return request; + } + case PROVIDER_METHODS.GET_WALLET_INFO: { + return request; } - return String(value); + case "nock_signRawTx": { + if (fromV1) { + throw new Error('signRawTx not implemented for API 1'); + } + if (toV1) { + const req = (request as any).params?.[0]; + if (!isLegacySignRawTxRequest(req)) { + throw new Error('Invalid legacyRawTx'); + } + const rawTx = rawTxFromProtobuf(req.rawTx); + if (!guard.isRawTxV1(rawTx)) { + throw new Error('Only V1 Raw TXs are supported at the moment'); + } + const tx = rawTxV1ToNockchainTx(rawTx); + const signParams = { tx }; + return { ...request, method: PROVIDER_METHODS.SIGN_TX, params: signParams as unknown }; + } + return request; + } + case PROVIDER_METHODS.SIGN_TX: { + if (!fromV1) { + throw new Error('signTx not implemented for API 0'); + } + if (!toV1) { + const req = request.params as unknown as SignTxRequest; + const rawTx = nockchainTxToRawTx(req.tx); + if (!req.notes) { + throw new Error('notes not found in SignTxRequest. This is required for API 0 wallets.'); + } + const notesNative = req.notes; + const notes = notesNative.map((note) => noteToProtobuf(note)); + const spendConditionsNative = rawTxInputSpendConditions(rawTx); + const spendConditions = spendConditionsNative.map((spendCondition: SpendCondition) => spendConditionToProtobuf(spendCondition)); + const legacyReq = { rawTx, notes, spendConditions }; + return { ...request, method: "nock_signRawTx", params: [legacyReq] }; + } + return request; + } + default: + return request; } +} - if (typeof value === 'string') { - return assertIntegerString(value, field); - } +/** Map an RPC response from one API version to another. */ +function mapResponse(method: string, response: RpcResponse, fromApi?: string, toApi?: string): RpcResponse { + if (fromApi === toApi) return response; + if (response.error) return response; + + const fromV1 = fromApi === RPC_API_VERSION; + const toV1 = toApi === RPC_API_VERSION; - throw new Error(`Invalid ${field}: unsupported value type`); + switch (method) { + case PROVIDER_METHODS.CONNECT: { + if (!fromV1 && toV1) { + // legacy → API 1: { grpcEndpoint, pkh } → { account, rpcConfig } + const legacy = response.result as LegacyConnectResponse; + const result: ConnectResponse = { + account: { type: 'v1', address: legacy.pkh }, + rpcConfig: { + rpcUrl: legacy.grpcEndpoint, + networkName: 'mainnet', + blockExplorerUrl: '', + txEngineActivationHeights: DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS, + coinbaseTimelockBlocks: 100, + }, + }; + return { ...response, result }; + } + if (fromV1 && !toV1) { + // API 1 → legacy: { account, rpcConfig } → { grpcEndpoint, pkh } + const v1 = response.result as ConnectResponse; + if (v1.account.type !== 'v1') { + throw new Error('Invalid account type'); + } + const result: LegacyConnectResponse = { + grpcEndpoint: v1.rpcConfig.rpcUrl, + pkh: v1.account.address, + }; + return { ...response, result }; + } + return response; + } + case PROVIDER_METHODS.SIGN_MESSAGE: { + if (!fromV1 && toV1) { + // legacy → API 1: { signature, publicKeyHex } → { signature, publicKey } + const legacy = response.result as LegacySignMessageResponse; + const signature = JSON.parse(legacy.signature) as { c: number[]; s: number[] }; + + const fromLegacyHex = (bytes: number[]): string => { + return bytes.reverse().map((b) => b.toString(16).padStart(2, '0')).join(''); + }; + const publicKey = publicKeyFromHex(legacy.publicKeyHex); + if (!publicKey) { + throw new Error('Invalid public key'); + } + const result: SignMessageResponse = { + signature: { + c: fromLegacyHex(signature.c), + s: fromLegacyHex(signature.s), + }, + publicKey, + }; + return { ...response, result }; + } + if (fromV1 && !toV1) { + // API 1 → legacy: { signature, publicKey } → { signature, publicKeyHex } + const v1 = response.result as SignMessageResponse; + + const toLegacyHex = (v: string | Uint8Array): number[] => { + if (typeof v === 'string') { + let bytes = []; + for (let i = 0; i < v.length; i += 2) { + bytes.push(parseInt(v.substr(i, 2), 16)); + } + return bytes.reverse(); + } + return [...v]; + }; + const signatureJson = JSON.stringify({ + c: toLegacyHex(v1.signature.c), + s: toLegacyHex(v1.signature.s), + }); + + const result: LegacySignMessageResponse = { + signature: signatureJson, + publicKeyHex: publicKeyToHex(v1.publicKey), + }; + return { ...response, result }; + } + return response; + } + case "nock_signRawTx": { + if (!fromV1) { + throw new Error('signRawTx not implemented for API 0'); + } + if (toV1) { + const req = response.result as any; + const rawTx = rawTxFromProtobuf(req.rawTx) as RawTxV1; + const nockchainTx = rawTxV1ToNockchainTx(rawTx); + const result: SignTxResponse = { tx: nockchainTx }; + return { ...response, result }; + } + return response; + } + case PROVIDER_METHODS.SIGN_TX: { + if (fromV1) { + throw new Error('signRawTx not implemented for API 1'); + } + if (!toV1) { + const req = (response as any).result?.[0]; + if (!isLegacySignRawTxRequest(req)) { + throw new Error('Invalid legacyRawTx'); + } + const rawTx = rawTxFromProtobuf(req.rawTx); + if (!guard.isRawTxV1(rawTx)) { + throw new Error('Only V1 Raw TXs are supported at the moment'); + } + const tx = rawTxV1ToNockchainTx(rawTx); + const result = { tx }; + return { ...response, result }; + } + return response; + } + default: + return response; + } } /** - * Validate signRawTx params. Input must be protobuf (LegacySignRawTxRequest). - * RPC sends only LegacySignRawTxRequest. Native (SignRawTxRequest) not supported. + * Bridge RPC requests between two API versions, converting request params + * and response payloads as needed. + * + * Maps the request from sourceApi → targetApi, calls target, then maps + * the response back from targetApi → sourceApi (inverted). */ -export function normalizeSignRawTxParams(params: SignRawTxParams): SignRawTxParams { - if (!isLegacySignRawTxRequest(params)) { - throw new Error( - 'Invalid signRawTx params: expected protobuf (PbCom2RawTransaction, PbCom2Note[], PbCom2SpendCondition[])' - ); - } - return params; -} +export async function requestBridge( + request: RpcRequest, + target: (request: RpcRequest) => Promise>, + sourceApi?: string, + targetApi?: string, +): Promise> { + const mappedReq = mapRequest(request, sourceApi, targetApi); + const res = await target(mappedReq); + return mapResponse(mappedReq.method, res, targetApi, sourceApi) as RpcResponse; +} \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index cd2cfa0..abaab9f 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,5 @@ +import { Nicks, TxEngineSettings } from '@nockbox/iris-wasm/iris_wasm'; + /** * Provider method constants for Nockchain wallet * These methods can be called by dApps via window.nockchain @@ -15,8 +17,45 @@ export const PROVIDER_METHODS = { /** Get wallet information (PKH + gRPC endpoint) */ GET_WALLET_INFO: 'nock_getWalletInfo', - /** Sign a raw transaction */ - SIGN_RAW_TX: 'nock_signRawTx', + /** Sign a nockchain transaction */ + SIGN_TX: 'nock_signTx', } as const; export type ProviderMethod = (typeof PROVIDER_METHODS)[keyof typeof PROVIDER_METHODS]; + +export const RPC_API_VERSION = '1.0.0'; + +export const V0_TX_ENGINE_SETTINGS: TxEngineSettings = { + tx_engine_version: 0, + tx_engine_patch: 0, + min_fee: '0' as Nicks, + cost_per_word: '0' as Nicks, + witness_word_div: 0, +}; + +/** Default V1 tx engine settings (mainnet: v1 from 39000). */ +export const V1_TX_ENGINE_SETTINGS: TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: '256' as Nicks, + cost_per_word: String(1 << 15) as Nicks, + witness_word_div: 1, +}; + +/** Bythos tx engine (v1 patch 1): witness_word_div 4, min_fee 256 */ +export const BYTHOS_TX_ENGINE_SETTINGS: TxEngineSettings = { + tx_engine_version: 1, + tx_engine_patch: 1, + min_fee: '256' as Nicks, + cost_per_word: String(1 << 14) as Nicks, + witness_word_div: 4, +}; + +export const DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS: Record = { + 1: V0_TX_ENGINE_SETTINGS, + 39000: V1_TX_ENGINE_SETTINGS, + 54000: BYTHOS_TX_ENGINE_SETTINGS, +}; + +/** Default coinbase timelock (mainnet maturity) */ +const DEFAULT_COINBASE_TIMELOCK_BLOCKS = 100; \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index f9e0d03..e4047fe 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ export * from './types.js'; export * from './provider.js'; -export * from './transaction.js'; export * from './errors.js'; export * from './constants.js'; export * from './compat.js'; diff --git a/src/iris_wasm.guard.ts b/src/iris_wasm.guard.ts deleted file mode 100644 index cc8c397..0000000 --- a/src/iris_wasm.guard.ts +++ /dev/null @@ -1,5 +0,0 @@ -/** - * Re-export iris-wasm type guards for extension/consumers. - * Use this instead of importing from @nockbox/iris-wasm directly. - */ -export * from '@nockbox/iris-wasm/iris_wasm.guard'; diff --git a/src/provider.ts b/src/provider.ts index f841319..43319bf 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -2,15 +2,10 @@ * NockchainProvider - Main SDK class for interacting with Iris wallet */ -import type { Transaction, NockchainEvent, EventListener, InjectedNockchain } from './types.js'; -import { TransactionBuilder } from './transaction.js'; +import type { Account, ConnectResponse, NockchainEvent, EventListener, InjectedNockchain, SignTxResponse, RpcRequest, SignTxRequest, SignMessageRequest, SignMessageResponse, ConnectRequest, SendTransactionRequest } from './types.js'; import { WalletNotInstalledError, UserRejectedError, RpcError, NoAccountError } from './errors.js'; -import { PROVIDER_METHODS } from './constants.js'; -import { - normalizeSignRawTxParams, - normalizeSendTransaction, - type SignRawTxParams, -} from './compat.js'; +import { PROVIDER_METHODS, RPC_API_VERSION } from './constants.js'; +import { NockchainTx, Note } from '@nockbox/iris-wasm'; /** * NockchainProvider class - Main interface for dApps to interact with Iris wallet @@ -34,7 +29,7 @@ import { export class NockchainProvider { private injected: InjectedNockchain; private eventListeners: Map>; - private _accounts: string[] = []; + private _accounts: Account[] = []; private _chainId: string | null = null; private _messageHandler?: (event: MessageEvent) => void; @@ -73,21 +68,22 @@ export class NockchainProvider { * @throws {UserRejectedError} If the user rejects the request * @throws {RpcError} If the RPC call fails */ - async connect(): Promise<{ pkh: string; grpcEndpoint: string }> { - const info = await this.request<{ pkh: string; grpcEndpoint: string }>({ + async connect(): Promise { + const info = await this.request({ method: PROVIDER_METHODS.CONNECT, + params: { api: RPC_API_VERSION }, }); // Store the PKH as the connected account - this._accounts = [info.pkh]; + this._accounts = [info.account]; return info; } /** * Get the currently connected accounts (if any) - * @returns Array of connected account addresses (PKH) + * @returns Array of connected accounts */ - get accounts(): string[] { + get accounts(): Account[] { return [...this._accounts]; } @@ -115,16 +111,14 @@ export class NockchainProvider { * @throws {UserRejectedError} If the user rejects the transaction * @throws {RpcError} If the RPC call fails */ - async sendTransaction(transaction: Transaction): Promise { + async sendTransaction(transaction: SendTransactionRequest): Promise { if (!this.isConnected) { throw new NoAccountError(); } - const normalizedTx = normalizeSendTransaction(transaction); - - return this.request({ + return this.request({ method: PROVIDER_METHODS.SEND_TRANSACTION, - params: [normalizedTx], + params: transaction, }); } @@ -136,69 +130,43 @@ export class NockchainProvider { * @throws {UserRejectedError} If the user rejects the signing request * @throws {RpcError} If the RPC call fails */ - async signMessage(message: string): Promise<{ signature: string; publicKeyHex: string }> { + async signMessage(message: string): Promise { if (!this.isConnected) { throw new NoAccountError(); } - return this.request<{ signature: string; publicKeyHex: string }>({ + return this.request({ method: PROVIDER_METHODS.SIGN_MESSAGE, - params: [message], + params: { message }, }); } /** * Sign a raw transaction - * Input must be protobuf (PbCom2RawTransaction, PbCom2Note[], PbCom2SpendCondition[]). - * @param params - The transaction parameters in protobuf format - * @returns Promise resolving to the signed raw transaction as protobuf Uint8Array + * Input must be NockchainTx. + * @param tx - The transaction to sign + * @returns Promise resolving to the signed transaction * @throws {NoAccountError} If no account is connected * @throws {UserRejectedError} If the user rejects the signing request * @throws {RpcError} If the RPC call fails * * @example * ```typescript - * const rawTxProto = wasm.rawTxToProtobuf(rawTx); - * const notesProto = notes.map(n => wasm.note_to_protobuf(n)); - * const spendCondProto = spendConditions.map(sc => wasm.spendConditionToProtobuf(sc)); - * - * const signedTx = await provider.signRawTx({ - * rawTx: rawTxProto, - * notes: notesProto, - * spendConditions: spendCondProto, - * }); + * const nockchainTx = wasm.rawTxV1ToNockchainTx(rawTx); + * const signedTx = await provider.signTx(nockchainTx); * ``` */ - async signRawTx(params: SignRawTxParams): Promise { + async signTx(tx: NockchainTx, notes?: Note[]): Promise { if (!this.isConnected) { throw new NoAccountError(); } - const validatedParams = normalizeSignRawTxParams(params); - - return this.request({ - method: PROVIDER_METHODS.SIGN_RAW_TX, - params: [validatedParams], + return this.request({ + method: PROVIDER_METHODS.SIGN_TX, + params: { tx, notes }, }); } - /** - * Create a new transaction builder - * @returns A new TransactionBuilder instance - * - * @example - * ```typescript - * const tx = provider.transaction() - * .to('recipient_address') - * .amount(1_000_000) - * .fee(50_000) - * .build(); - * ``` - */ - transaction(): TransactionBuilder { - return new TransactionBuilder(); - } - /** * Add an event listener for wallet events * @param event - The event to listen for @@ -249,9 +217,9 @@ export class NockchainProvider { * @throws {UserRejectedError} If the user rejects the request * @throws {RpcError} If the RPC call fails */ - public async request(args: { method: string; params?: unknown[] }): Promise { + public async request(args: RpcRequest): Promise { try { - const result = await this.injected.request(args); + const result = await this.injected.request(args); return result; } catch (error) { // Handle RPC errors and map known error codes diff --git a/src/transaction.ts b/src/transaction.ts deleted file mode 100644 index bbfed3d..0000000 --- a/src/transaction.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * Transaction builder with fluent API for constructing Nockchain transactions - */ - -import { base58 } from '@scure/base'; -import type { Transaction } from './types.js'; -import { InvalidAddressError, InvalidTransactionError } from './errors.js'; -import { parseNicksLike } from './compat.js'; - -/** - * Conversion rate: 1 NOCK = 65,536 nicks (2^16) - */ -export const NOCK_TO_NICKS = 65_536; - -/** - * Default transaction fee in nicks (32,768 nicks = 0.5 NOCK) - */ -export const DEFAULT_FEE = 32_768; - -/** - * Minimum amount in nicks (must be positive) - */ -export const MIN_AMOUNT = 1; - -/** - * TransactionBuilder class implementing the builder pattern for type-safe transaction construction - * - * @example - * ```typescript - * const tx = new TransactionBuilder() - * .to('nock1recipient_address') - * .amount(1_000_000) - * .fee(50_000) - * .build(); - * ``` - */ -export class TransactionBuilder { - private _to?: string; - private _amount?: number; - private _fee?: number; - - /** - * Set the recipient address for the transaction - * @param address - Base58-encoded Nockchain V1 PKH address (40 bytes, ~54-55 chars) - * @returns A new TransactionBuilder instance with the address set - * @throws {InvalidAddressError} If the address format is invalid - */ - to(address: string): TransactionBuilder { - if (!this.isValidAddress(address)) { - throw new InvalidAddressError(address); - } - - const builder = new TransactionBuilder(); - builder._to = address; - builder._amount = this._amount; - builder._fee = this._fee; - return builder; - } - - /** - * Set the amount to send in nicks (1 NOCK = 65,536 nicks) - * @param nicks - Amount in nicks (must be a positive integer) - * @returns A new TransactionBuilder instance with the amount set - * @throws {InvalidTransactionError} If the amount is invalid - */ - amount(nicks: number): TransactionBuilder { - if (!Number.isInteger(nicks)) { - throw new InvalidTransactionError('Amount must be an integer'); - } - if (nicks < MIN_AMOUNT) { - throw new InvalidTransactionError(`Amount must be at least ${MIN_AMOUNT} nick`); - } - // Note: MIN_AMOUNT = 1, so the above check already covers negative amounts - - const builder = new TransactionBuilder(); - builder._to = this._to; - builder._amount = nicks; - builder._fee = this._fee; - return builder; - } - - /** - * Set the transaction fee in nicks (optional, defaults to 32,768 nicks) - * @param nicks - Fee amount in nicks (must be a positive integer) - * @returns A new TransactionBuilder instance with the fee set - * @throws {InvalidTransactionError} If the fee is invalid - */ - fee(nicks: number): TransactionBuilder { - if (!Number.isInteger(nicks)) { - throw new InvalidTransactionError('Fee must be an integer'); - } - if (nicks < 0) { - throw new InvalidTransactionError('Fee must be non-negative'); - } - - const builder = new TransactionBuilder(); - builder._to = this._to; - builder._amount = this._amount; - builder._fee = nicks; - return builder; - } - - /** - * Build and validate the transaction - * @returns The constructed Transaction object - * @throws {InvalidTransactionError} If required fields are missing - */ - build(): Transaction { - if (!this._to) { - throw new InvalidTransactionError('Missing required field: to (recipient address)'); - } - if (this._amount === undefined || this._amount === null) { - throw new InvalidTransactionError('Missing required field: amount'); - } - - return { - to: this._to, - amount: this._amount, - fee: this._fee ?? DEFAULT_FEE, - }; - } - - /** - * Validate a Nockchain V1 PKH address format - * V1 PKH addresses are TIP5 hash (40 bytes) of public key, base58-encoded - * - * Validates by decoding the base58 string and checking for exactly 40 bytes - * rather than relying on character count which can vary - * - * @param address - The address to validate - * @returns true if valid, false otherwise - */ - private isValidAddress(address: string): boolean { - try { - const trimmed = (address || '').trim(); - if (trimmed.length === 0) return false; - - const bytes = base58.decode(trimmed); - return bytes.length === 40; - } catch { - // Invalid base58 encoding - return false; - } - } - - /** - * Create a transaction builder from an existing transaction object - * Useful for modifying existing transactions - * @param tx - The transaction to create a builder from - * @returns A new TransactionBuilder instance with values from the transaction - * @throws {InvalidAddressError} If the transaction address is invalid - * @throws {InvalidTransactionError} If the transaction amount or fee is invalid - */ - static fromTransaction(tx: Transaction): TransactionBuilder { - const amountNumber = Number(parseNicksLike(tx.amount, 'amount')); - if (!Number.isSafeInteger(amountNumber) || amountNumber < MIN_AMOUNT) { - throw new InvalidTransactionError('Amount is outside safe integer range'); - } - - // Use setters to ensure validation is applied - let builder = new TransactionBuilder().to(tx.to).amount(amountNumber); - - if (tx.fee !== undefined) { - const feeNumber = Number(parseNicksLike(tx.fee, 'fee')); - if (!Number.isSafeInteger(feeNumber) || feeNumber < 0) { - throw new InvalidTransactionError('Fee is outside safe integer range'); - } - builder = builder.fee(feeNumber); - } - - return builder; - } -} diff --git a/src/types.ts b/src/types.ts index 3cea7e4..1a9f908 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2,28 +2,21 @@ * TypeScript type definitions for Iris SDK */ +import { Nicks, TxEngineSettings, PublicKey, Signature, Digest, NockchainTx, Note } from './wasm'; + /** * Transaction object representing a Nockchain transaction */ -export type NicksLike = number | string | bigint; - -export interface Transaction { - /** Recipient address (base58-encoded public key hash / PKH) */ - to: string; - /** Amount to send in nicks (legacy number + canonical string/bigint accepted) */ - amount: NicksLike; - /** Transaction fee in nicks (legacy number + canonical string/bigint accepted) */ - fee?: NicksLike; -} +export type NicksLike = number | Nicks | bigint; /** * RPC request object for communicating with the extension */ -export interface RpcRequest { +export interface RpcRequest { /** The RPC method to call */ method: string; /** Optional parameters for the method */ - params?: unknown[]; + params?: T; /** Optional timeout for the request */ timeout?: number; } @@ -42,6 +35,69 @@ export interface RpcResponse { }; } +export interface ConnectRequest { + /** SDK will pass its RPC_API_VERSION */ + api: string; +} + +export interface RpcConfig { + rpcUrl: string; + networkName: string; + blockExplorerUrl: string; + txEngineActivationHeights: Record; + coinbaseTimelockBlocks: number; +}; + +export interface ConnectResponse { + account: Account; + rpcConfig: RpcConfig; +} + +export interface V0Account { + type: "v0"; + address: PublicKey; +} + +export interface V1Account { + type: "v1"; + address: Digest; +} + +export type Account = V0Account | V1Account; +export type Address = PublicKey | Digest; + +export interface SignMessageRequest { + message: string; +} + +export interface SignMessageResponse { + signature: Signature; + /** Base58 encoded public key */ + publicKey: PublicKey; +} + +export interface SendTransactionRequest { + /** Recipient address (base58-encoded public key hash / PKH) */ + to: Address; + /** Amount to send in nicks (legacy number + canonical string/bigint accepted) */ + amount: Nicks; + /** Transaction fee in nicks (legacy number + canonical string/bigint accepted) */ + fee?: Nicks; +} + +export interface SignTxRequest { + tx: NockchainTx; + /** + * Optional notes for the transaction. Required for API 0 wallet compatibility. + * This will be removed in future SDK releases. + */ + notes?: Note[]; +} + +export interface SignTxResponse { + tx: NockchainTx; +} + /** * Event types that the provider can emit */ @@ -61,7 +117,7 @@ export interface InjectedNockchain { * @param request - The RPC request object * @returns Promise resolving to the result */ - request(request: RpcRequest): Promise; + request(request: RpcRequest): Promise; /** * Provider name (e.g., 'iris') @@ -72,6 +128,11 @@ export interface InjectedNockchain { * Provider version */ version?: string; + + /** + * Supported RPC API version + */ + api?: string; } /** @@ -101,4 +162,4 @@ export interface AccountInfo { address: string; /** Account balance in nicks (optional, may not be available) */ balance?: number; -} +} \ No newline at end of file diff --git a/src/wasm.ts b/src/wasm.ts index 971cad3..966c8ff 100644 --- a/src/wasm.ts +++ b/src/wasm.ts @@ -2,10 +2,11 @@ * WASM module exports */ -export * from '@nockbox/iris-wasm/iris_wasm.js'; -export { default } from '@nockbox/iris-wasm/iris_wasm.js'; +export * from '@nockbox/iris-wasm/iris_wasm'; +export { default } from '@nockbox/iris-wasm/iris_wasm'; +export * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; -import init from '@nockbox/iris-wasm/iris_wasm.js'; +import init from '@nockbox/iris-wasm/iris_wasm'; /** * Canonical initializer re-export for SDK consumers. From 31a973d6f2d38174685537ab8a67d44f57d6dc8f Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Mar 2026 09:59:50 -0400 Subject: [PATCH 27/75] use new API, fix issues with parsing Nicks --- src/migration.ts | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index 2bd9cac..665be3c 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -10,12 +10,13 @@ import type { PbCom2BalanceEntry, RawTxV1, SpendCondition, + Digest, TxEngineSettings, } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; -function buildSinglePkhSpendCondition(pkh: string): SpendCondition { +function buildSinglePkhSpendCondition(pkh: Digest): SpendCondition { const pkhObj = wasm.pkhSingle(pkh); return wasm.spendConditionNewPkh(pkhObj); } @@ -28,9 +29,9 @@ function isNoteV0(note: Note): note is NoteV0 { return 'inner' in note && 'sig' in note && 'source' in note; } -function sumNicks(notes: NoteV0[]): string { +function sumNicks(notes: NoteV0[]): Nicks { const total = notes.reduce((acc, note) => acc + BigInt(note.assets), 0n); - return total.toString(); + return total.toString() as Nicks; } const NOCK_TO_NICKS = 65_536; @@ -105,7 +106,7 @@ function defaultTxEngineSettings(): TxEngineSettings { export async function buildV0MigrationTx( mnemonic: string, grpcEndpoint: string, - targetV1Pkh?: string, + targetV1Pkh?: Digest, options?: { debug?: boolean } ): Promise { const balanceResult = await queryV0Balance(mnemonic, grpcEndpoint); @@ -133,7 +134,7 @@ export async function buildV0MigrationTx( const txSettings = defaultTxEngineSettings(); const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); - const refundLock = wasm.locky(targetSpendCondition); + const refundLock = wasm.lockHash(targetSpendCondition); const builder = new wasm.TxBuilder(txSettings); for (const note of notesToUse) { @@ -145,14 +146,9 @@ export async function buildV0MigrationTx( builder.recalcAndSetFee(false); const feeNicks = builder.curFee(); const transaction = builder.build(); - const allNotes = builder.allNotes(); - const rawTx: RawTxV1 = { - version: 1, - id: transaction.id, - spends: transaction.spends, - }; + const rawTx: RawTxV1 = wasm.nockchainTxToRawTx(transaction); - const inputNotes = allNotes.filter((note): note is NoteV0 => isNoteV0(note)); + const inputNotes = notesToUse; const feeNock = Number(BigInt(feeNicks)) / NOCK_TO_NICKS; const migrated = useSingleNote @@ -160,12 +156,12 @@ export async function buildV0MigrationTx( const note = notesToUse[0]; const nock = Number(BigInt(note.assets)) / NOCK_TO_NICKS; return { - migratedNicks: BigInt(note.assets).toString(), + migratedNicks: note.assets, migratedNock: nock, }; })() : { - migratedNicks: (BigInt(balanceResult.totalNicks) - BigInt(feeNicks)).toString(), + migratedNicks: (BigInt(balanceResult.totalNicks) - BigInt(feeNicks)).toString() as Nicks, migratedNock: balanceResult.totalNock - feeNock, }; From d7140b0bc7cd0fd10a6c83ee7644ed97f8f33211 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Mar 2026 10:49:17 -0400 Subject: [PATCH 28/75] update bridge for parsing new pkh digest format --- src/bridge-types.ts | 19 ++++++++----------- src/bridge.ts | 27 +++++++++++++++++++++------ src/index.ts | 1 - 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index 1552038..caa2dd8 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -3,9 +3,15 @@ * Consumers (nockswap, extension) provide BridgeConfig; the SDK handles tx construction and validation. */ -import type { NockchainTx, Nicks, Note, SpendCondition } from '@nockbox/iris-wasm/iris_wasm.js'; +import type { + NockchainTx, + Nicks, + Note, + SpendCondition, + TxEngineSettings, +} from '@nockbox/iris-wasm/iris_wasm.js'; -export type { Nicks }; +export type { Nicks, TxEngineSettings }; /** * Configuration for a specific bridge (e.g. Zorp Nock→Base). @@ -30,15 +36,6 @@ export interface BridgeConfig { expectedLockRoot?: string; } -/** Tx engine settings for TxBuilder (matches wasm.TxEngineSettings). */ -export interface TxEngineSettings { - tx_engine_version: 0 | 1 | 2; - tx_engine_patch: number; - min_fee: string; - cost_per_word: string; - witness_word_div: number; -} - /** * Parameters for building a bridge transaction. * Input notes and spend conditions are supplied by the consumer (e.g. from gRPC). diff --git a/src/bridge.ts b/src/bridge.ts index 0e3f984..c5877bc 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -12,13 +12,16 @@ import type { } from './bridge-types.js'; import type { LockRoot, + Digest, Note, NoteData, + Nicks, Noun, PbCom2RawTransaction, SeedV1, SpendCondition, } from '@nockbox/iris-wasm/iris_wasm.js'; +import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; // Goldilocks prime: 2^64 - 2^32 + 1 @@ -113,6 +116,15 @@ export function isBridgeConfigured(config: BridgeConfig): boolean { ); } +function parseDigestString(value: string, field: string): Digest { + const trimmed = value.trim(); + const bytes = base58.decode(trimmed); + if (bytes.length !== 40) { + throw new Error(`Invalid ${field}: expected a 40-byte base58 digest`); + } + return trimmed as Digest; +} + /** * Create jammed bridge note data for an EVM address (requires WASM). * Caller must have initialized WASM (e.g. await wasm.default()) before using. @@ -143,17 +155,20 @@ export async function buildBridgeTransaction( const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); const noteData: NoteData = [[config.noteDataKey, bridgeNounJs as Noun]]; - const bridgePkh = wasm.pkhNew(BigInt(config.threshold), config.addresses); + const bridgePkh = wasm.pkhNew( + BigInt(config.threshold), + config.addresses.map(address => parseDigestString(address, 'bridge address')) + ); const bridgeSpendCondition: SpendCondition = wasm.spendConditionNewPkh(bridgePkh); const bridgeLockRoot: LockRoot = bridgeSpendCondition; - const refundPkhObj = wasm.pkhSingle(params.refundPkh); + const refundPkhObj = wasm.pkhSingle(parseDigestString(params.refundPkh, 'refund pkh')); const refundLock: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); const txSettings = params.txEngineSettings ?? { tx_engine_version: 1, tx_engine_patch: 0, - min_fee: '256', + min_fee: '256' as Nicks, cost_per_word: params.feeOverride ?? config.feePerWord, witness_word_div: 1, }; @@ -177,7 +192,7 @@ export async function buildBridgeTransaction( output_source: null, lock_root: bridgeLockRoot, note_data: noteData, - gift: String(giftPortion), + gift: giftPortion.toString() as Nicks, parent_hash: parentHash, }; spendBuilder.seed(seed); @@ -211,7 +226,7 @@ export async function validateBridgeTransaction( ): Promise { try { const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); - const outputs = wasm.rawTxOutputs(rawTx); + const outputs = wasm.rawTxOutputs(rawTx, 0, wasm.txEngineSettingsV1BythosDefault()); if (outputs.length === 0) { return { valid: false, error: 'Transaction has no outputs' }; @@ -358,7 +373,7 @@ export async function validateBridgeTransaction( return { valid: true, - bridgeAmountNicks: String(bridgeOutput.assets), + bridgeAmountNicks: bridgeOutput.assets.toString() as Nicks, destinationAddress, belts, noteDataKey: validatedNoteDataKey, diff --git a/src/index.ts b/src/index.ts index b625923..08a1f5e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,7 +5,6 @@ export * from './types.js'; export * from './provider.js'; -export * from './transaction.js'; export * from './migration.js'; export * from './errors.js'; export * from './constants.js'; From 2653e4338767a5d305bed4fa6acaed4b08ad4c88 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 26 Mar 2026 11:04:08 -0400 Subject: [PATCH 29/75] build sdk upon install --- package.json | 1 + 1 file changed, 1 insertion(+) diff --git a/package.json b/package.json index 8e5af73..b39486e 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ ], "scripts": { "build": "tsc", + "prepare": "npm run build", "build-example": "npm run build && tsc --project examples/tsconfig.json && vite build --config vite.config.examples.ts", "dev": "tsc --watch", "dev-example": "vite --config vite.config.examples.ts", From abd99a20a1fd58000af6a7f86f807599e1d6976d Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 8 Apr 2026 09:54:41 -0400 Subject: [PATCH 30/75] move constant to constants --- src/constants.ts | 3 +++ src/migration.ts | 3 +-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index abaab9f..ac82d10 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,5 +1,8 @@ import { Nicks, TxEngineSettings } from '@nockbox/iris-wasm/iris_wasm'; +/** Conversion rate: 1 NOCK = 65,536 nicks (2^16). */ +export const NOCK_TO_NICKS = 65_536; + /** * Provider method constants for Nockchain wallet * These methods can be called by dApps via window.nockchain diff --git a/src/migration.ts b/src/migration.ts index 665be3c..d28da5f 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -15,6 +15,7 @@ import type { } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; +import { NOCK_TO_NICKS } from './constants.js'; function buildSinglePkhSpendCondition(pkh: Digest): SpendCondition { const pkhObj = wasm.pkhSingle(pkh); @@ -34,8 +35,6 @@ function sumNicks(notes: NoteV0[]): Nicks { return total.toString() as Nicks; } -const NOCK_TO_NICKS = 65_536; - /** * Derive legacy v0 address (base58 bare public key) from mnemonic. */ From bd4fcad666d9d5cdaa974f84f3df3acb65d3cf63 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:09:50 -0400 Subject: [PATCH 31/75] add type guard and change derivedV0address to public key --- src/migration-types.ts | 5 +++-- src/migration.ts | 43 ++++++++++++++++++++---------------------- 2 files changed, 23 insertions(+), 25 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index 456503b..c079e76 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -6,6 +6,7 @@ import type { LockRoot, Nicks, NoteV0, + PublicKey, PbCom2Balance, RawTx, SpendCondition, @@ -13,8 +14,8 @@ import type { export type { Nicks }; -/** Base58 bare public key derived from mnemonic. */ -export type DerivedV0Address = string; +/** WASM public key bytes derived from mnemonic (legacy v0 key). */ +export type DerivedV0Address = PublicKey; /** Result of querying v0 balance. Use this to construct a migration transaction. */ export interface V0BalanceResult { diff --git a/src/migration.ts b/src/migration.ts index d28da5f..5aee74c 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -4,10 +4,9 @@ import type { V0BalanceResult, } from './migration-types.js'; import type { - Note, Nicks, NoteV0, - PbCom2BalanceEntry, + PbCom2Note, RawTxV1, SpendCondition, Digest, @@ -15,6 +14,7 @@ import type { } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; +import * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; import { NOCK_TO_NICKS } from './constants.js'; function buildSinglePkhSpendCondition(pkh: Digest): SpendCondition { @@ -22,27 +22,24 @@ function buildSinglePkhSpendCondition(pkh: Digest): SpendCondition { return wasm.spendConditionNewPkh(pkhObj); } -function isLegacyEntry(entry: PbCom2BalanceEntry): boolean { - return !!entry.note?.note_version && 'Legacy' in entry.note.note_version; -} - -function isNoteV0(note: Note): note is NoteV0 { - return 'inner' in note && 'sig' in note && 'source' in note; -} - function sumNicks(notes: NoteV0[]): Nicks { const total = notes.reduce((acc, note) => acc + BigInt(note.assets), 0n); return total.toString() as Nicks; } +function parseV0Note(note?: PbCom2Note | null): NoteV0 | null { + if (!note) return null; + const parsed = wasm.noteFromProtobuf(note); + return guard.isNoteV0(parsed) ? parsed : null; +} + /** - * Derive legacy v0 address (base58 bare public key) from mnemonic. + * Derive legacy v0 WASM public key from mnemonic. */ export function deriveV0AddressFromMnemonic(mnemonic: string): DerivedV0Address { const master = wasm.deriveMasterKeyFromMnemonic(mnemonic); try { - const publicKey = Uint8Array.from(master.publicKey); - return base58.encode(publicKey); + return wasm.publicKeyFromBeBytes(Uint8Array.from(master.publicKey)); } finally { master.free(); } @@ -56,28 +53,28 @@ export async function queryV0Balance( mnemonic: string, grpcEndpoint: string ): Promise { - const sourceAddress = deriveV0AddressFromMnemonic(mnemonic); + const sourcePublicKey = deriveV0AddressFromMnemonic(mnemonic); + const sourceAddress = base58.encode(wasm.publicKeyToBeBytesVec(sourcePublicKey)); const grpcClient = new wasm.GrpcClient(grpcEndpoint); const balance = await grpcClient.getBalanceByAddress(sourceAddress); const v0Notes: NoteV0[] = []; const entries = balance.notes ?? []; for (const entry of entries) { - if (!isLegacyEntry(entry) || !entry.note) { - continue; - } - const parsed = wasm.noteFromProtobuf(entry.note); - if (isNoteV0(parsed)) { - v0Notes.push(parsed); - } + const parsedV0 = parseV0Note(entry.note); + if (parsedV0) v0Notes.push(parsedV0); } const totalNicks = sumNicks(v0Notes); const totalNock = Number(BigInt(totalNicks)) / NOCK_TO_NICKS; const smallestNoteNock = v0Notes.length > 0 - ? Number(v0Notes.reduce((min, n) => (BigInt(n.assets) < min ? BigInt(n.assets) : min), BigInt(v0Notes[0].assets))) / - NOCK_TO_NICKS + ? Number( + v0Notes.reduce( + (min, n) => (BigInt(n.assets) < min ? BigInt(n.assets) : min), + BigInt(v0Notes[0].assets) + ) + ) / NOCK_TO_NICKS : undefined; return { From 5c998dd5aa412be13427b2737ed294dfe1259ad9 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:18:25 -0400 Subject: [PATCH 32/75] change signatures to accept public keys instead of deriving internally --- src/migration-types.ts | 4 ---- src/migration.ts | 24 +++++------------------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index c079e76..6647bdc 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -6,7 +6,6 @@ import type { LockRoot, Nicks, NoteV0, - PublicKey, PbCom2Balance, RawTx, SpendCondition, @@ -14,9 +13,6 @@ import type { export type { Nicks }; -/** WASM public key bytes derived from mnemonic (legacy v0 key). */ -export type DerivedV0Address = PublicKey; - /** Result of querying v0 balance. Use this to construct a migration transaction. */ export interface V0BalanceResult { sourceAddress: string; diff --git a/src/migration.ts b/src/migration.ts index 5aee74c..1afbcf6 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,12 +1,12 @@ import type { BuildV0MigrationTxResult, - DerivedV0Address, V0BalanceResult, } from './migration-types.js'; import type { Nicks, NoteV0, PbCom2Note, + PublicKey, RawTxV1, SpendCondition, Digest, @@ -34,26 +34,13 @@ function parseV0Note(note?: PbCom2Note | null): NoteV0 | null { } /** - * Derive legacy v0 WASM public key from mnemonic. - */ -export function deriveV0AddressFromMnemonic(mnemonic: string): DerivedV0Address { - const master = wasm.deriveMasterKeyFromMnemonic(mnemonic); - try { - return wasm.publicKeyFromBeBytes(Uint8Array.from(master.publicKey)); - } finally { - master.free(); - } -} - -/** - * Query v0 (Legacy) balance for a mnemonic. Discovery only; does not build a transaction. + * Query v0 (Legacy) balance for a v0 public key. Discovery only; does not build a transaction. * Caller must have initialized WASM (e.g. await wasm.default()) before using. */ export async function queryV0Balance( - mnemonic: string, + sourcePublicKey: PublicKey, grpcEndpoint: string ): Promise { - const sourcePublicKey = deriveV0AddressFromMnemonic(mnemonic); const sourceAddress = base58.encode(wasm.publicKeyToBeBytesVec(sourcePublicKey)); const grpcClient = new wasm.GrpcClient(grpcEndpoint); const balance = await grpcClient.getBalanceByAddress(sourceAddress); @@ -100,12 +87,12 @@ function defaultTxEngineSettings(): TxEngineSettings { * @param options.debug - When true, logs the built result to console. */ export async function buildV0MigrationTx( - mnemonic: string, + sourcePublicKey: PublicKey, grpcEndpoint: string, targetV1Pkh?: Digest, options?: { debug?: boolean } ): Promise { - const balanceResult = await queryV0Balance(mnemonic, grpcEndpoint); + const balanceResult = await queryV0Balance(sourcePublicKey, grpcEndpoint); if (!targetV1Pkh) { return balanceResult; } @@ -187,6 +174,5 @@ export async function buildV0MigrationTx( export type { BuildV0MigrationTxResult, - DerivedV0Address, V0BalanceResult, } from './migration-types.js'; From a3ba39f7fafeb10761d50de30a9480f1048f4d40 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 8 Apr 2026 10:24:08 -0400 Subject: [PATCH 33/75] add tx engine as a required parameter, change debug to be specific to the # of notes being used and lift it up into options --- src/migration-types.ts | 7 +++++ src/migration.ts | 62 ++++++++++++++++-------------------------- 2 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index 6647bdc..dbd77b7 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -9,6 +9,7 @@ import type { PbCom2Balance, RawTx, SpendCondition, + TxEngineSettings, } from '@nockbox/iris-wasm/iris_wasm.js'; export type { Nicks }; @@ -45,3 +46,9 @@ export interface BuildV0MigrationTxResult { migratedNicks?: Nicks; migratedNock?: number; } + +export interface BuildV0MigrationTxOptions { + txEngineSettings: TxEngineSettings; + /** Optional cap on how many smallest legacy notes to include. */ + maxNotes?: number; +} diff --git a/src/migration.ts b/src/migration.ts index 1afbcf6..48d72ef 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -1,5 +1,6 @@ import type { BuildV0MigrationTxResult, + BuildV0MigrationTxOptions, V0BalanceResult, } from './migration-types.js'; import type { @@ -10,7 +11,6 @@ import type { RawTxV1, SpendCondition, Digest, - TxEngineSettings, } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; @@ -75,30 +75,28 @@ export async function queryV0Balance( }; } -function defaultTxEngineSettings(): TxEngineSettings { - return wasm.txEngineSettingsV1BythosDefault(); -} - /** * Fetch v0 balance and optionally build migration tx to a v1 PKH lock. * Caller must have initialized WASM (e.g. await wasm.default()) before using. * * @param targetV1Pkh - When provided, builds the migration tx. Omit for balance only. - * @param options.debug - When true, logs the built result to console. + * @param options.txEngineSettings - Required tx engine settings for deterministic tx building. + * @param options.maxNotes - Optional cap on number of smallest legacy notes to include. */ export async function buildV0MigrationTx( sourcePublicKey: PublicKey, grpcEndpoint: string, targetV1Pkh?: Digest, - options?: { debug?: boolean } + options?: BuildV0MigrationTxOptions ): Promise { const balanceResult = await queryV0Balance(sourcePublicKey, grpcEndpoint); if (!targetV1Pkh) { return balanceResult; } - - const debug = options?.debug ?? false; - const useSingleNote = debug; + if (!options?.txEngineSettings) { + throw new Error('txEngineSettings is required when building migration tx'); + } + const maxNotes = options.maxNotes; try { const v0Notes = balanceResult.v0Notes; @@ -106,19 +104,18 @@ export async function buildV0MigrationTx( throw new Error('No v0 notes to migrate'); } - const notesToUse: NoteV0[] = useSingleNote - ? (() => { - const sorted = [...v0Notes] - .map(n => ({ note: n, assets: BigInt(n.assets) })) - .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0)); - return [sorted[0].note]; - })() - : v0Notes; + const sortedNotes = [...v0Notes] + .map(note => ({ note, assets: BigInt(note.assets) })) + .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0)); + + const notesToUse: NoteV0[] = + typeof maxNotes === 'number' && Number.isFinite(maxNotes) && maxNotes > 0 + ? sortedNotes.slice(0, Math.floor(maxNotes)).map(entry => entry.note) + : sortedNotes.map(entry => entry.note); - const txSettings = defaultTxEngineSettings(); const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); const refundLock = wasm.lockHash(targetSpendCondition); - const builder = new wasm.TxBuilder(txSettings); + const builder = new wasm.TxBuilder(options.txEngineSettings); for (const note of notesToUse) { const spendBuilder = new wasm.SpendBuilder(note, null, null, refundLock); @@ -133,20 +130,9 @@ export async function buildV0MigrationTx( const inputNotes = notesToUse; const feeNock = Number(BigInt(feeNicks)) / NOCK_TO_NICKS; - - const migrated = useSingleNote - ? (() => { - const note = notesToUse[0]; - const nock = Number(BigInt(note.assets)) / NOCK_TO_NICKS; - return { - migratedNicks: note.assets, - migratedNock: nock, - }; - })() - : { - migratedNicks: (BigInt(balanceResult.totalNicks) - BigInt(feeNicks)).toString() as Nicks, - migratedNock: balanceResult.totalNock - feeNock, - }; + const selectedTotal = inputNotes.reduce((sum, note) => sum + BigInt(note.assets), 0n); + const migratedNicks = (selectedTotal - BigInt(feeNicks)).toString() as Nicks; + const migratedNock = Number(selectedTotal) / NOCK_TO_NICKS - feeNock; const result: BuildV0MigrationTxResult = { ...balanceResult, @@ -159,12 +145,9 @@ export async function buildV0MigrationTx( spendConditions: inputNotes.map(() => null), refundLock, }, - ...migrated, + migratedNicks, + migratedNock, }; - - if (debug) { - console.log('[SDK Migration] buildV0MigrationTx', result); - } return result; } catch (e) { console.warn('[SDK Migration] Build failed, returning balance only:', e); @@ -173,6 +156,7 @@ export async function buildV0MigrationTx( } export type { + BuildV0MigrationTxOptions, BuildV0MigrationTxResult, V0BalanceResult, } from './migration-types.js'; From 7e13cca125195df660ec8284a4d3c4053169eaf9 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:28:06 -0400 Subject: [PATCH 34/75] add options for bridge to get tx engine settings from the caller --- src/bridge-types.ts | 11 +++++++---- src/bridge.ts | 31 +++++++++++++++++-------------- 2 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index caa2dd8..af1038b 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -51,10 +51,13 @@ export interface BridgeTransactionParams { destinationAddress: string; /** User's PKH for refunds/change */ refundPkh: string; - /** Optional fee override in nicks */ - feeOverride?: Nicks; - /** Optional: tx engine settings (Bythos at block ≥54000). When provided, overrides config.feePerWord. */ - txEngineSettings?: TxEngineSettings; +} + +/** + * Options for bridge transaction build / validation (mirrors migration: caller supplies tx engine). + */ +export interface BuildBridgeTransactionOptions { + txEngineSettings: TxEngineSettings; } /** diff --git a/src/bridge.ts b/src/bridge.ts index c5877bc..a2c97f3 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -9,6 +9,7 @@ import type { BridgeTransactionParams, BridgeTransactionResult, BridgeValidationResult, + BuildBridgeTransactionOptions, } from './bridge-types.js'; import type { LockRoot, @@ -143,11 +144,15 @@ export async function createBridgeNoteData( */ export async function buildBridgeTransaction( params: BridgeTransactionParams, - config: BridgeConfig + config: BridgeConfig, + options: BuildBridgeTransactionOptions ): Promise { if (!isBridgeConfigured(config)) { throw new Error('Bridge not configured'); } + if (!options?.txEngineSettings) { + throw new Error('txEngineSettings is required in options (see BuildBridgeTransactionOptions)'); + } if (!isEvmAddress(params.destinationAddress)) { throw new Error(`Invalid destination address: ${params.destinationAddress}`); } @@ -164,15 +169,7 @@ export async function buildBridgeTransaction( const refundPkhObj = wasm.pkhSingle(parseDigestString(params.refundPkh, 'refund pkh')); const refundLock: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); - const txSettings = - params.txEngineSettings ?? { - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256' as Nicks, - cost_per_word: params.feeOverride ?? config.feePerWord, - witness_word_div: 1, - }; - const builder = new wasm.TxBuilder(txSettings); + const builder = new wasm.TxBuilder(options.txEngineSettings); let remainingGift = BigInt(params.amountInNicks); @@ -222,11 +219,15 @@ export async function buildBridgeTransaction( */ export async function validateBridgeTransaction( rawTxProto: unknown, - config: BridgeConfig + config: BridgeConfig, + options: BuildBridgeTransactionOptions ): Promise { + if (!options?.txEngineSettings) { + throw new Error('txEngineSettings is required in options (see BuildBridgeTransactionOptions)'); + } try { const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); - const outputs = wasm.rawTxOutputs(rawTx, 0, wasm.txEngineSettingsV1BythosDefault()); + const outputs = wasm.rawTxOutputs(rawTx, 0, options.txEngineSettings); if (outputs.length === 0) { return { valid: false, error: 'Transaction has no outputs' }; @@ -394,9 +395,10 @@ export async function validateBridgeTransaction( export async function assertValidBridgeTransaction( rawTxProto: unknown, context: 'pre-signing' | 'post-signing', - config: BridgeConfig + config: BridgeConfig, + options: BuildBridgeTransactionOptions ): Promise { - const result = await validateBridgeTransaction(rawTxProto, config); + const result = await validateBridgeTransaction(rawTxProto, config, options); if (!result.valid) { throw new Error(`${context} validation failed: ${result.error}`); } @@ -409,5 +411,6 @@ export type { BridgeTransactionParams, BridgeTransactionResult, BridgeValidationResult, + BuildBridgeTransactionOptions, TxEngineSettings, } from './bridge-types.js'; From 0ee3503133f723f1c655ee177689929dbf3f67a4 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:30:25 -0400 Subject: [PATCH 35/75] fix comment --- src/compat.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index 70883ef..f0220a8 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -22,17 +22,17 @@ import { } from './wasm.js'; import { PROVIDER_METHODS, RPC_API_VERSION, DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS } from './constants.js'; -/** Protobuf signRawTx payload (gRPC wire format). Only format supported at API boundary. */ +/** + * Legacy `nock_signRawTx` RPC params (API 0): protobuf raw tx plus matching notes and spend conditions. + * Compat maps this to v1 `SIGN_TX` with a native `NockchainTx`. v1 callers use `SignTxRequest` instead. + */ interface LegacySignRawTxRequest { - /** Raw transaction protobuf */ rawTx: PbCom2RawTransaction; - /** Input notes (protobuf) */ notes: PbCom2Note[]; - /** Spend conditions (protobuf) */ spendConditions: PbCom2SpendCondition[]; } -/** Type guard: validates protobuf signRawTx payload. Use at API boundary (SDK + extension). */ +/** Type guard for the legacy `nock_signRawTx` payload shape. */ function isLegacySignRawTxRequest(obj: unknown): obj is LegacySignRawTxRequest { if (!obj || typeof obj !== 'object') return false; const p = obj as { rawTx?: unknown; notes?: unknown; spendConditions?: unknown }; From 1c3741a7f1b8601d36826927259d725d946a0838 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:32:21 -0400 Subject: [PATCH 36/75] define default coinbase inside of SDK --- src/constants.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index ac82d10..6283a48 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -54,11 +54,14 @@ export const BYTHOS_TX_ENGINE_SETTINGS: TxEngineSettings = { witness_word_div: 4, }; +/** + * Default mainnet activation map + */ export const DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS: Record = { 1: V0_TX_ENGINE_SETTINGS, 39000: V1_TX_ENGINE_SETTINGS, 54000: BYTHOS_TX_ENGINE_SETTINGS, }; -/** Default coinbase timelock (mainnet maturity) */ -const DEFAULT_COINBASE_TIMELOCK_BLOCKS = 100; \ No newline at end of file +/** Default coinbase maturity in blocks (mainnet-style; e.g. 100). */ +export const DEFAULT_COINBASE_TIMELOCK_BLOCKS = 100; \ No newline at end of file From 420f43bd89a51ff54461da2c753e0c9a67096ace Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 14 Apr 2026 23:39:51 -0400 Subject: [PATCH 37/75] add sign-tx-request guard in the SDK --- src/index.ts | 1 + src/validate-sign-tx-request.ts | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) create mode 100644 src/validate-sign-tx-request.ts diff --git a/src/index.ts b/src/index.ts index 08a1f5e..9bdc74a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -8,6 +8,7 @@ export * from './provider.js'; export * from './migration.js'; export * from './errors.js'; export * from './constants.js'; +export * from './validate-sign-tx-request.js'; export * from './compat.js'; export * from './bridge.js'; export * as wasm from './wasm.js'; diff --git a/src/validate-sign-tx-request.ts b/src/validate-sign-tx-request.ts new file mode 100644 index 0000000..7ffa3b3 --- /dev/null +++ b/src/validate-sign-tx-request.ts @@ -0,0 +1,16 @@ +/** + * Runtime validation for `SignTxRequest` at RPC / postMessage boundaries. + */ + +import * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; +import type { SignTxRequest } from './types.js'; + +export function isSignTxRequest(obj: unknown): obj is SignTxRequest { + if (!obj || typeof obj !== 'object') return false; + const p = obj as { tx?: unknown; notes?: unknown }; + return ( + guard.isNockchainTx(p.tx) && + (typeof p.notes === 'undefined' || + (Array.isArray(p.notes) && p.notes.every((note: unknown) => guard.isNote(note)))) + ); +} From b0f132cd963ae5fdb8a3bc01edf30eec8e3225f9 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:09:45 -0400 Subject: [PATCH 38/75] more consolidation --- src/migration-types.ts | 17 ++++++++++------ src/migration.ts | 45 +++++++++++++++++++++++++++++++++++------- 2 files changed, 49 insertions(+), 13 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index dbd77b7..33781e8 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -25,6 +25,16 @@ export interface V0BalanceResult { rawNotesFromRpc?: number; } +/** + * Data needed to reconstruct a v0→v1 migration `TxBuilder` for signing (distinct from dApp `signRawTx` APIs). + */ +export interface V0MigrationTxSignPayload { + rawTx: RawTx; + notes: NoteV0[]; + spendConditions: (SpendCondition | null)[]; + refundLock: LockRoot; +} + /** buildV0MigrationTx result: balance fields always present; tx fields when target provided and build succeeded. */ export interface BuildV0MigrationTxResult { sourceAddress: string; @@ -37,12 +47,7 @@ export interface BuildV0MigrationTxResult { txId?: string; fee?: Nicks; feeNock?: number; - signRawTxPayload?: { - rawTx: RawTx; - notes: NoteV0[]; - spendConditions: (SpendCondition | null)[]; - refundLock: LockRoot; - }; + v0MigrationTxSignPayload?: V0MigrationTxSignPayload; migratedNicks?: Nicks; migratedNock?: number; } diff --git a/src/migration.ts b/src/migration.ts index 48d72ef..5f8bbbe 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -2,6 +2,7 @@ import type { BuildV0MigrationTxResult, BuildV0MigrationTxOptions, V0BalanceResult, + V0MigrationTxSignPayload, } from './migration-types.js'; import type { Nicks, @@ -11,6 +12,9 @@ import type { RawTxV1, SpendCondition, Digest, + Lock, + LockRoot, + TxEngineSettings, } from '@nockbox/iris-wasm/iris_wasm.js'; import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; @@ -33,6 +37,37 @@ function parseV0Note(note?: PbCom2Note | null): NoteV0 | null { return guard.isNoteV0(parsed) ? parsed : null; } +function appendV0MigrationSpends( + builder: wasm.TxBuilder, + notes: NoteV0[], + spendConditions: (SpendCondition | null)[] | undefined, + refundLock: LockRoot +): void { + for (let i = 0; i < notes.length; i++) { + const spendBuilder = new wasm.SpendBuilder( + notes[i], + (spendConditions?.[i] ?? null) as Lock | null, + null, + refundLock + ); + spendBuilder.computeRefund(false); + builder.spend(spendBuilder); + } +} + +/** + * Reconstruct a `TxBuilder` from {@link BuildV0MigrationTxResult.v0MigrationTxSignPayload}, + * using the same `txEngineSettings` that were used to build the unsigned tx (e.g. for fee iteration / signing). + */ +export function buildV0MigrationTxBuilderFromPayload( + payload: V0MigrationTxSignPayload, + txEngineSettings: TxEngineSettings +): wasm.TxBuilder { + const builder = new wasm.TxBuilder(txEngineSettings); + appendV0MigrationSpends(builder, payload.notes, payload.spendConditions, payload.refundLock); + return builder; +} + /** * Query v0 (Legacy) balance for a v0 public key. Discovery only; does not build a transaction. * Caller must have initialized WASM (e.g. await wasm.default()) before using. @@ -116,12 +151,7 @@ export async function buildV0MigrationTx( const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); const refundLock = wasm.lockHash(targetSpendCondition); const builder = new wasm.TxBuilder(options.txEngineSettings); - - for (const note of notesToUse) { - const spendBuilder = new wasm.SpendBuilder(note, null, null, refundLock); - spendBuilder.computeRefund(false); - builder.spend(spendBuilder); - } + appendV0MigrationSpends(builder, notesToUse, notesToUse.map(() => null), refundLock); builder.recalcAndSetFee(false); const feeNicks = builder.curFee(); @@ -139,7 +169,7 @@ export async function buildV0MigrationTx( txId: transaction.id, fee: feeNicks, feeNock, - signRawTxPayload: { + v0MigrationTxSignPayload: { rawTx, notes: inputNotes, spendConditions: inputNotes.map(() => null), @@ -159,4 +189,5 @@ export type { BuildV0MigrationTxOptions, BuildV0MigrationTxResult, V0BalanceResult, + V0MigrationTxSignPayload, } from './migration-types.js'; From aa275508ac61b01260af22fa5bc4f0ad80471bd5 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:36:31 -0400 Subject: [PATCH 39/75] add default activation height to sdk --- src/compat.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index f0220a8..4ff8393 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -7,7 +7,6 @@ import { PbCom2RawTransaction, PbCom2Note, PbCom2SpendCondition, - guard, rawTxFromProtobuf, rawTxV1ToNockchainTx, nockchainTxToRawTx, @@ -20,7 +19,13 @@ import { SpendCondition, Digest, } from './wasm.js'; -import { PROVIDER_METHODS, RPC_API_VERSION, DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS } from './constants.js'; +import * as guard from '@nockbox/iris-wasm/iris_wasm.guard'; +import { + PROVIDER_METHODS, + RPC_API_VERSION, + DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS, + DEFAULT_COINBASE_TIMELOCK_BLOCKS, +} from './constants.js'; /** * Legacy `nock_signRawTx` RPC params (API 0): protobuf raw tx plus matching notes and spend conditions. @@ -170,7 +175,7 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s networkName: 'mainnet', blockExplorerUrl: '', txEngineActivationHeights: DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS, - coinbaseTimelockBlocks: 100, + coinbaseTimelockBlocks: DEFAULT_COINBASE_TIMELOCK_BLOCKS, }, }; return { ...response, result }; From 0e3aa870dae806a63a36e497deae71edd1f594c2 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 01:47:53 -0400 Subject: [PATCH 40/75] enforce a clearer API: v0 transaction building not used anymore for balance results --- src/migration-types.ts | 2 +- src/migration.ts | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/migration-types.ts b/src/migration-types.ts index 33781e8..dfd12fa 100644 --- a/src/migration-types.ts +++ b/src/migration-types.ts @@ -35,7 +35,7 @@ export interface V0MigrationTxSignPayload { refundLock: LockRoot; } -/** buildV0MigrationTx result: balance fields always present; tx fields when target provided and build succeeded. */ +/** buildV0MigrationTx result: balance fields always present; tx fields when build succeeded. */ export interface BuildV0MigrationTxResult { sourceAddress: string; balance: PbCom2Balance; diff --git a/src/migration.ts b/src/migration.ts index 5f8bbbe..707e57e 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -111,26 +111,19 @@ export async function queryV0Balance( } /** - * Fetch v0 balance and optionally build migration tx to a v1 PKH lock. + * Build a v0→v1 migration transaction to `targetV1Pkh` (after querying balance on-chain). + * For balance only, use {@link queryV0Balance}. * Caller must have initialized WASM (e.g. await wasm.default()) before using. * - * @param targetV1Pkh - When provided, builds the migration tx. Omit for balance only. - * @param options.txEngineSettings - Required tx engine settings for deterministic tx building. * @param options.maxNotes - Optional cap on number of smallest legacy notes to include. */ export async function buildV0MigrationTx( sourcePublicKey: PublicKey, grpcEndpoint: string, - targetV1Pkh?: Digest, - options?: BuildV0MigrationTxOptions + targetV1Pkh: Digest, + options: BuildV0MigrationTxOptions ): Promise { const balanceResult = await queryV0Balance(sourcePublicKey, grpcEndpoint); - if (!targetV1Pkh) { - return balanceResult; - } - if (!options?.txEngineSettings) { - throw new Error('txEngineSettings is required when building migration tx'); - } const maxNotes = options.maxNotes; try { From 9feb0576d6862778b8b04df7cdbfb76ea2b0aba2 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 03:08:59 -0400 Subject: [PATCH 41/75] moved config INTO SDK --- src/bridge-types.ts | 2 -- src/constants.ts | 19 ++++++++++++++++++- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index af1038b..9e541e6 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -28,8 +28,6 @@ export interface BridgeConfig { chainTag: string; /** Version tag in note data (e.g. "0") */ versionTag: string; - /** Fee per word in nicks (WASM Nicks = string, e.g. "32768") */ - feePerWord: Nicks; /** Minimum amount in nicks for a valid bridge output (for validation) */ minAmountNicks: Nicks; /** Optional: expected lock root hash for bridge output (if set, validation checks it) */ diff --git a/src/constants.ts b/src/constants.ts index 6283a48..e9860f0 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -64,4 +64,21 @@ export const DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS: Record Date: Wed, 15 Apr 2026 04:19:32 -0400 Subject: [PATCH 42/75] depract api in connection request since RPC request now holds it --- src/provider.ts | 6 ++++-- src/types.ts | 6 ++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/provider.ts b/src/provider.ts index 43319bf..922fed2 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -71,7 +71,6 @@ export class NockchainProvider { async connect(): Promise { const info = await this.request({ method: PROVIDER_METHODS.CONNECT, - params: { api: RPC_API_VERSION }, }); // Store the PKH as the connected account @@ -219,7 +218,10 @@ export class NockchainProvider { */ public async request(args: RpcRequest): Promise { try { - const result = await this.injected.request(args); + const result = await this.injected.request({ + ...args, + api: args.api ?? RPC_API_VERSION, + }); return result; } catch (error) { // Handle RPC errors and map known error codes diff --git a/src/types.ts b/src/types.ts index 1a9f908..e1c18f3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -15,6 +15,8 @@ export type NicksLike = number | Nicks | bigint; export interface RpcRequest { /** The RPC method to call */ method: string; + /** API version of this request payload (defaults to legacy API 0 when omitted) */ + api?: string; /** Optional parameters for the method */ params?: T; /** Optional timeout for the request */ @@ -36,8 +38,8 @@ export interface RpcResponse { } export interface ConnectRequest { - /** SDK will pass its RPC_API_VERSION */ - api: string; + /** @deprecated API version now lives on RpcRequest.api */ + api?: string; } export interface RpcConfig { From ef3d324bb6018342692579cc3c36058f817e5042 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:02:19 -0400 Subject: [PATCH 43/75] expose internal helpers to fit the extension better, add legacy path for nock_signRawTx --- src/compat.ts | 75 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 14 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index 4ff8393..127a883 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -10,6 +10,7 @@ import { rawTxFromProtobuf, rawTxV1ToNockchainTx, nockchainTxToRawTx, + rawTxToProtobuf, rawTxInputSpendConditions, publicKeyFromHex, publicKeyToHex, @@ -155,6 +156,17 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR } } +/** + * Pure request mapper for callers that need request translation without invoking a target. + */ +export function mapRpcRequest( + request: RpcRequest, + sourceApi?: string, + targetApi?: string +): RpcRequest { + return mapRequest(request, sourceApi, targetApi); +} + /** Map an RPC response from one API version to another. */ function mapResponse(method: string, response: RpcResponse, fromApi?: string, toApi?: string): RpcResponse { if (fromApi === toApi) return response; @@ -244,12 +256,24 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s return response; } case "nock_signRawTx": { - if (!fromV1) { - throw new Error('signRawTx not implemented for API 0'); + if (fromV1 && !toV1) { + const v1 = response.result as SignTxResponse; + if (!v1?.tx) { + throw new Error('Invalid signTx response'); + } + const rawTx = nockchainTxToRawTx(v1.tx); + if (!guard.isRawTxV1(rawTx)) { + throw new Error('Only V1 Raw TXs are supported at the moment'); + } + const result = { rawTx: rawTxToProtobuf(rawTx) }; + return { ...response, result }; } - if (toV1) { - const req = response.result as any; - const rawTx = rawTxFromProtobuf(req.rawTx) as RawTxV1; + if (!fromV1 && toV1) { + const legacy = response.result as { rawTx?: PbCom2RawTransaction }; + if (!legacy?.rawTx || !guard.isPbCom2RawTransaction(legacy.rawTx)) { + throw new Error('Invalid legacy signRawTx response'); + } + const rawTx = rawTxFromProtobuf(legacy.rawTx) as RawTxV1; const nockchainTx = rawTxV1ToNockchainTx(rawTx); const result: SignTxResponse = { tx: nockchainTx }; return { ...response, result }; @@ -257,15 +281,24 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s return response; } case PROVIDER_METHODS.SIGN_TX: { - if (fromV1) { - throw new Error('signRawTx not implemented for API 1'); + if (fromV1 && !toV1) { + const v1 = response.result as SignTxResponse; + if (!v1?.tx) { + throw new Error('Invalid signTx response'); + } + const rawTx = nockchainTxToRawTx(v1.tx); + if (!guard.isRawTxV1(rawTx)) { + throw new Error('Only V1 Raw TXs are supported at the moment'); + } + const result = { rawTx: rawTxToProtobuf(rawTx) }; + return { ...response, result }; } - if (!toV1) { - const req = (response as any).result?.[0]; - if (!isLegacySignRawTxRequest(req)) { - throw new Error('Invalid legacyRawTx'); + if (!fromV1 && toV1) { + const legacy = response.result as { rawTx?: PbCom2RawTransaction }; + if (!legacy?.rawTx || !guard.isPbCom2RawTransaction(legacy.rawTx)) { + throw new Error('Invalid legacy signRawTx response'); } - const rawTx = rawTxFromProtobuf(req.rawTx); + const rawTx = rawTxFromProtobuf(legacy.rawTx); if (!guard.isRawTxV1(rawTx)) { throw new Error('Only V1 Raw TXs are supported at the moment'); } @@ -280,6 +313,20 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s } } +/** + * Pure response mapper for callers that already executed a mapped request. + * + * `method` should be the method of the mapped request that produced `response`. + */ +export function mapRpcResponse( + method: string, + response: RpcResponse, + sourceApi?: string, + targetApi?: string +): RpcResponse { + return mapResponse(method, response, sourceApi, targetApi); +} + /** * Bridge RPC requests between two API versions, converting request params * and response payloads as needed. @@ -293,7 +340,7 @@ export async function requestBridge( sourceApi?: string, targetApi?: string, ): Promise> { - const mappedReq = mapRequest(request, sourceApi, targetApi); + const mappedReq = mapRpcRequest(request, sourceApi, targetApi); const res = await target(mappedReq); - return mapResponse(mappedReq.method, res, targetApi, sourceApi) as RpcResponse; + return mapRpcResponse(mappedReq.method, res, targetApi, sourceApi) as RpcResponse; } \ No newline at end of file From 5cce6f60a50834adec33faf2c1dc95bdcad00e1e Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 05:16:27 -0400 Subject: [PATCH 44/75] formatting, small API change, update package.json to not export guard there anymore --- package-lock.json | 2 +- package.json | 4 ---- src/compat.ts | 48 ++++++++++++++++++++++++++++++++++------------- src/constants.ts | 2 +- src/migration.ts | 7 ++++++- src/provider.ts | 15 ++++++++++++++- src/types.ts | 8 ++++---- 7 files changed, 61 insertions(+), 25 deletions(-) diff --git a/package-lock.json b/package-lock.json index b1407d2..a51202c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -752,7 +752,6 @@ "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "undici-types": "~6.21.0" } @@ -1018,6 +1017,7 @@ "vendor/iris-wasm": { "name": "@nockbox/iris-wasm", "version": "0.2.0-alpha.3", + "extraneous": true, "license": "MIT" } } diff --git a/package.json b/package.json index b39486e..8e4a29d 100644 --- a/package.json +++ b/package.json @@ -13,10 +13,6 @@ "./wasm": { "types": "./dist/wasm.d.ts", "import": "./dist/wasm.js" - }, - "./iris_wasm.guard": { - "types": "./dist/iris_wasm.guard.d.ts", - "import": "./dist/iris_wasm.guard.js" } }, "files": [ diff --git a/src/compat.ts b/src/compat.ts index 127a883..12fe255 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -2,7 +2,18 @@ * Backward-compatibility helpers for SDK request payloads. */ -import type { SendTransactionRequest, NicksLike, RpcRequest, RpcResponse, ConnectRequest, ConnectResponse, SignMessageRequest, SignTxRequest, SignMessageResponse, SignTxResponse } from './types.js'; +import type { + SendTransactionRequest, + NicksLike, + RpcRequest, + RpcResponse, + ConnectRequest, + ConnectResponse, + SignMessageRequest, + SignTxRequest, + SignMessageResponse, + SignTxResponse, +} from './types.js'; import { PbCom2RawTransaction, PbCom2Note, @@ -60,7 +71,7 @@ interface LegacyConnectResponse { interface LegacySignMessageResponse { signature: string; - publicKeyHex: string + publicKeyHex: string; } /** Map an RPC request from one API version to another. */ @@ -85,10 +96,11 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR } case PROVIDER_METHODS.SEND_TRANSACTION: { if (fromV1 && !toV1) { - return { ...request, params: [request.params] }; + const params = request.params as SendTransactionRequest | undefined; + return { ...request, params: params ? [params] : request.params }; } if (!fromV1 && toV1) { - const params = request.params as unknown as unknown[] | undefined; + const params = request.params as SendTransactionRequest[] | undefined; return { ...request, params: params?.[0] }; } return request; @@ -113,7 +125,7 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR case PROVIDER_METHODS.GET_WALLET_INFO: { return request; } - case "nock_signRawTx": { + case 'nock_signRawTx': { if (fromV1) { throw new Error('signRawTx not implemented for API 1'); } @@ -143,11 +155,13 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR throw new Error('notes not found in SignTxRequest. This is required for API 0 wallets.'); } const notesNative = req.notes; - const notes = notesNative.map((note) => noteToProtobuf(note)); + const notes = notesNative.map(note => noteToProtobuf(note)); const spendConditionsNative = rawTxInputSpendConditions(rawTx); - const spendConditions = spendConditionsNative.map((spendCondition: SpendCondition) => spendConditionToProtobuf(spendCondition)); + const spendConditions = spendConditionsNative.map((spendCondition: SpendCondition) => + spendConditionToProtobuf(spendCondition) + ); const legacyReq = { rawTx, notes, spendConditions }; - return { ...request, method: "nock_signRawTx", params: [legacyReq] }; + return { ...request, method: 'nock_signRawTx', params: [legacyReq] }; } return request; } @@ -168,7 +182,12 @@ export function mapRpcRequest( } /** Map an RPC response from one API version to another. */ -function mapResponse(method: string, response: RpcResponse, fromApi?: string, toApi?: string): RpcResponse { +function mapResponse( + method: string, + response: RpcResponse, + fromApi?: string, + toApi?: string +): RpcResponse { if (fromApi === toApi) return response; if (response.error) return response; @@ -213,7 +232,10 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s const signature = JSON.parse(legacy.signature) as { c: number[]; s: number[] }; const fromLegacyHex = (bytes: number[]): string => { - return bytes.reverse().map((b) => b.toString(16).padStart(2, '0')).join(''); + return bytes + .reverse() + .map(b => b.toString(16).padStart(2, '0')) + .join(''); }; const publicKey = publicKeyFromHex(legacy.publicKeyHex); if (!publicKey) { @@ -255,7 +277,7 @@ function mapResponse(method: string, response: RpcResponse, fromApi?: s } return response; } - case "nock_signRawTx": { + case 'nock_signRawTx': { if (fromV1 && !toV1) { const v1 = response.result as SignTxResponse; if (!v1?.tx) { @@ -338,9 +360,9 @@ export async function requestBridge( request: RpcRequest, target: (request: RpcRequest) => Promise>, sourceApi?: string, - targetApi?: string, + targetApi?: string ): Promise> { const mappedReq = mapRpcRequest(request, sourceApi, targetApi); const res = await target(mappedReq); return mapRpcResponse(mappedReq.method, res, targetApi, sourceApi) as RpcResponse; -} \ No newline at end of file +} diff --git a/src/constants.ts b/src/constants.ts index e9860f0..0c47cab 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -81,4 +81,4 @@ export const ZORP_BRIDGE_ADDRESSES: string[] = [ 'CDLzgKWAKFXYABkuQaMwbttDSTDMh3Wy2Eoq2XiArsyxn7vScNHupBb', // Pero '7E47xYNVEyt7jGmLsiChUHnyw88AfBvzJfXfEQkPmMo2ZWsdcPudwmV', // Nockbox '3xSyK6RQUaYzE8YDUamkpKRHALxaYo8E7eppawwE4sP35c3PASc6koq', // SWPS -]; \ No newline at end of file +]; diff --git a/src/migration.ts b/src/migration.ts index 707e57e..aff26be 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -144,7 +144,12 @@ export async function buildV0MigrationTx( const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); const refundLock = wasm.lockHash(targetSpendCondition); const builder = new wasm.TxBuilder(options.txEngineSettings); - appendV0MigrationSpends(builder, notesToUse, notesToUse.map(() => null), refundLock); + appendV0MigrationSpends( + builder, + notesToUse, + notesToUse.map(() => null), + refundLock + ); builder.recalcAndSetFee(false); const feeNicks = builder.curFee(); diff --git a/src/provider.ts b/src/provider.ts index 922fed2..ec480ac 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -2,7 +2,20 @@ * NockchainProvider - Main SDK class for interacting with Iris wallet */ -import type { Account, ConnectResponse, NockchainEvent, EventListener, InjectedNockchain, SignTxResponse, RpcRequest, SignTxRequest, SignMessageRequest, SignMessageResponse, ConnectRequest, SendTransactionRequest } from './types.js'; +import type { + Account, + ConnectResponse, + NockchainEvent, + EventListener, + InjectedNockchain, + SignTxResponse, + RpcRequest, + SignTxRequest, + SignMessageRequest, + SignMessageResponse, + ConnectRequest, + SendTransactionRequest, +} from './types.js'; import { WalletNotInstalledError, UserRejectedError, RpcError, NoAccountError } from './errors.js'; import { PROVIDER_METHODS, RPC_API_VERSION } from './constants.js'; import { NockchainTx, Note } from '@nockbox/iris-wasm'; diff --git a/src/types.ts b/src/types.ts index e1c18f3..16814c2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -48,7 +48,7 @@ export interface RpcConfig { blockExplorerUrl: string; txEngineActivationHeights: Record; coinbaseTimelockBlocks: number; -}; +} export interface ConnectResponse { account: Account; @@ -56,12 +56,12 @@ export interface ConnectResponse { } export interface V0Account { - type: "v0"; + type: 'v0'; address: PublicKey; } export interface V1Account { - type: "v1"; + type: 'v1'; address: Digest; } @@ -164,4 +164,4 @@ export interface AccountInfo { address: string; /** Account balance in nicks (optional, may not be available) */ balance?: number; -} \ No newline at end of file +} From c2a06a1d6fe1f785072a5f007e562c2b59798c11 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:02:20 -0400 Subject: [PATCH 45/75] fix examples, add small helper --- examples/app.ts | 71 +++++++----------- examples/tx-builder.ts | 162 +++++++++++++++++++++-------------------- src/constants.ts | 19 +++++ 3 files changed, 130 insertions(+), 122 deletions(-) diff --git a/examples/app.ts b/examples/app.ts index 2f179f8..1420990 100644 --- a/examples/app.ts +++ b/examples/app.ts @@ -1,4 +1,4 @@ -import { NockchainProvider, wasm } from '../src/index'; +import { NockchainProvider, getLatestTxEngineSettings, wasm } from '../src/index'; const statusDiv = document.getElementById('status')!; const outputPre = document.getElementById('output')!; @@ -9,6 +9,15 @@ const recipientInput = document.getElementById('recipientInput') as HTMLInputEle let provider: NockchainProvider; let grpcEndpoint: string | null = null; let walletPkh: string | null = null; +let txEngineSettings = getLatestTxEngineSettings(); + +function asDigest(value: string): wasm.Digest { + return value as wasm.Digest; +} + +function asNicks(value: string): wasm.Nicks { + return value as wasm.Nicks; +} function log(msg: string) { outputPre.textContent += msg + '\n'; @@ -34,10 +43,11 @@ connectBtn.onclick = async () => { return; } try { - // Connect to wallet (returns pkh and grpcEndpoint) + // Connect to wallet const info = await provider.connect(); - grpcEndpoint = info.grpcEndpoint; - walletPkh = info.pkh; + grpcEndpoint = info.rpcConfig.rpcUrl; + walletPkh = info.account.address; + txEngineSettings = getLatestTxEngineSettings(info.rpcConfig.txEngineActivationHeights); statusDiv.textContent = 'Connected: ' + walletPkh; signRawTxBtn.disabled = false; @@ -68,7 +78,9 @@ signRawTxBtn.onclick = async () => { const grpcClient = new wasm.GrpcClient(grpcEndpoint); // 3. Derive first-name from PKH and query notes (notes are indexed by first-name, not address) - const spendCondition = [{ Pkh: { m: 1, hashes: [walletPkh] } }] as wasm.SpendCondition; + const spendCondition: wasm.SpendCondition = [ + { tag: 'pkh', m: 1, hashes: [asDigest(walletPkh)] }, + ]; const firstName = wasm.spendConditionFirstName(spendCondition); log('Querying notes by first-name...'); const balance = await grpcClient.getBalanceByFirstName(firstName); @@ -84,7 +96,7 @@ signRawTxBtn.onclick = async () => { const notes = balance.notes .map((entry: any) => entry.note) .filter(Boolean) - .map((noteProto: any) => wasm.noteFromProtobuf(noteProto)); + .map((noteProto: wasm.PbCom2Note) => wasm.noteFromProtobuf(noteProto)); if (!notes.length) { log('No parseable notes found'); @@ -96,26 +108,19 @@ signRawTxBtn.onclick = async () => { log('Using note with ' + noteAssets + ' nicks'); // 4. Build transaction (send 10 NOCK = 655360 nicks) - const TEN_NOCK_IN_NICKS = String(10 * 65536); - const feePerWord = '32768'; // 0.5 NOCK per word + const TEN_NOCK_IN_NICKS = asNicks(String(10 * 65536)); log('Building transaction to send 10 NOCK...'); - const builder = new wasm.TxBuilder({ - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256', - cost_per_word: feePerWord, - witness_word_div: 1, - }); + const builder = new wasm.TxBuilder(txEngineSettings); // Use simpleSpend (no lockData for lower fees), digest values are strings in 0.2 builder.simpleSpend( [note], - [spendCondition], - recipient, + [spendCondition as unknown as wasm.TxLock], + asDigest(recipient), TEN_NOCK_IN_NICKS, null, // fee_override (let it auto-calculate) - walletPkh, + asDigest(walletPkh), false // include_lock_data ); @@ -125,36 +130,14 @@ signRawTxBtn.onclick = async () => { const txId = nockchainTx.id; log('Transaction ID: ' + txId); - // Get notes and spend conditions from builder - const txNotes = builder.allNotes(); - - log('Notes count: ' + txNotes.notes.length); - log('Spend conditions count: ' + txNotes.spend_conditions.length); - - // 6. Convert to protobuf (API boundary requires protobuf) - const rawTx = wasm.nockchainTxToRawTx(nockchainTx); - const rawTxProto = wasm.rawTxToProtobuf(rawTx); - const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.noteToProtobuf(n)); - const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => - wasm.spendConditionToProtobuf(sc) - ); - - // 7. Sign using provider.signRawTx + // 6. Sign using provider.signTx log('Signing transaction...'); - const signedTxProtobuf = await provider.signRawTx({ - rawTx: rawTxProto, - notes: notesProto, - spendConditions: spendCondProto, - }); + const signed = await provider.signTx(nockchainTx); log('Transaction signed successfully!'); - // 8. Convert signed tx to Jam and download - const signedTxProto = - typeof signedTxProtobuf === 'object' && !(signedTxProtobuf instanceof Uint8Array) - ? signedTxProtobuf - : (signedTxProtobuf as unknown as wasm.PbCom2RawTransaction); - const signedRawTx = wasm.rawTxFromProtobuf(signedTxProto); + // 7. Convert signed tx to Jam and download + const signedRawTx = wasm.nockchainTxToRawTx(signed.tx as any); const jamBytes = wasm.jam(signedRawTx as unknown as wasm.Noun); const blob = new Blob([new Uint8Array(jamBytes)], { type: 'application/jam' }); const url = URL.createObjectURL(blob); diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 40c5044..40e58d4 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -110,13 +110,43 @@ function nicksToNock(nicks: wasm.Nicks): number { } function nockToNicks(nock: number): wasm.Nicks { - return String(BigInt(Math.floor(nock * 65536))); + return asNicks(String(BigInt(Math.floor(nock * 65536)))); } function formatNock(nock: number): string { return nock.toFixed(4); } +function asDigest(value: string): wasm.Digest { + return value as wasm.Digest; +} + +function asNicks(value: string): wasm.Nicks { + return value as wasm.Nicks; +} + +function asBlockHeight(value: number): wasm.BlockHeight { + return value as wasm.BlockHeight; +} + +function asLock(spendCondition: wasm.SpendCondition): wasm.Lock { + return spendCondition as unknown as wasm.Lock; +} + +function asTxLock(spendCondition: wasm.SpendCondition): wasm.TxLock { + return spendCondition as unknown as wasm.TxLock; +} + +function getTxEngineSettings(): wasm.TxEngineSettings { + return { + tx_engine_version: 1, + tx_engine_patch: 0, + min_fee: asNicks('256'), + cost_per_word: asNicks('32768'), + witness_word_div: 1, + }; +} + declare global { interface Window { copyToClipboard: (text: string, element: HTMLElement) => void; @@ -227,8 +257,8 @@ connectBtn.onclick = async () => { try { const info = await state.provider.connect(); - state.grpcEndpoint = info.grpcEndpoint; - state.walletPkh = info.pkh; + state.grpcEndpoint = info.rpcConfig.rpcUrl; + state.walletPkh = info.account.address; state.connected = true; connectBtn.textContent = truncateAddress(state.walletPkh); @@ -255,7 +285,11 @@ function createDefaultLocksWithPkh() { if (!pkhDefaultExists) { // Simple 1-of-1 PKH lock - const pkhPrim: wasm.LockPrimitive = { Pkh: { m: 1, hashes: [state.walletPkh] } }; + const pkhPrim: wasm.LockPrimitive = { + tag: 'pkh', + m: 1, + hashes: [asDigest(state.walletPkh)], + }; const spendCondition: wasm.SpendCondition = [pkhPrim]; state.locks.push({ id: 'pkh-default', @@ -267,12 +301,16 @@ function createDefaultLocksWithPkh() { if (!coinbaseDefaultExists) { // Coinbase lock (PKH + timelock) - const pkhPrim: wasm.LockPrimitive = { Pkh: { m: 1, hashes: [state.walletPkh] } }; + const pkhPrim: wasm.LockPrimitive = { + tag: 'pkh', + m: 1, + hashes: [asDigest(state.walletPkh)], + }; const coinbaseTim: wasm.LockTim = { - rel: { min: 100, max: null }, + rel: { min: asBlockHeight(100), max: null }, abs: { min: null, max: null }, }; - const timPrim: wasm.LockPrimitive = { Tim: coinbaseTim }; + const timPrim: wasm.LockPrimitive = { tag: 'tim', ...coinbaseTim }; const spendCondition: wasm.SpendCondition = [pkhPrim, timPrim]; state.locks.push({ id: 'coinbase-default', @@ -344,7 +382,7 @@ async function refreshNotesForLock(lockId: string) { .filter((note): note is wasm.PbCom2Note => note != null) .map((noteProto: wasm.PbCom2Note) => { const note = wasm.noteFromProtobuf(noteProto); - const assets = note.assets != null ? String(note.assets) : '0'; + const assets = note.assets ?? asNicks('0'); const firstNameStr = note.name?.first ?? ''; const lastNameStr = note.name?.last ?? ''; return { @@ -424,7 +462,7 @@ function addInputToSpend(lockId: string, noteIndex: number) { const spend: Spend = { inputId: input.id, input, - fee: '0', + fee: asNicks('0'), seeds: [], }; @@ -507,10 +545,10 @@ function getSpendBalanceText(spend: Spend): string { BigInt(spend.fee) + spend.seeds.reduce((sum, seed) => sum + BigInt(seed.amount), BigInt(0)); const diff = BigInt(spend.input.note.assets) - total; if (diff === BigInt(0)) { - return `✓ Balanced (${formatNock(nicksToNock(String(total)))} NOCK)`; + return `✓ Balanced (${formatNock(nicksToNock(asNicks(String(total))))} NOCK)`; } const sign = diff > 0n ? '+' : ''; - return `${sign}${formatNock(nicksToNock(String(diff)))} NOCK`; + return `${sign}${formatNock(nicksToNock(asNicks(String(diff))))} NOCK`; } function removeSpend(inputId: string) { @@ -553,7 +591,7 @@ function balanceSeed(inputId: string, seedIndex: number) { return; } - seed.amount = String(remaining); + seed.amount = asNicks(String(remaining)); renderSpends(); updateBuilder(); } @@ -567,7 +605,7 @@ function updateSpendFee(inputId: string, feeStr: string, shouldRender: boolean = if (!isNaN(nock) && nock >= 0) { spend.fee = nockToNicks(nock); } else { - spend.fee = '0'; + spend.fee = asNicks('0'); } if (shouldRender) renderSpends(); updateBuilder(); @@ -582,7 +620,7 @@ function addSeed(inputId: string) { if (spend && state.locks.length > 0) { spend.seeds.push({ lockId: state.locks[0].id, - amount: '0', + amount: asNicks('0'), }); renderSpends(); updateBuilder(); @@ -612,7 +650,7 @@ function updateSeedAmount( if (!isNaN(nock) && nock >= 0) { spend.seeds[seedIndex].amount = nockToNicks(nock); } else { - spend.seeds[seedIndex].amount = '0'; + spend.seeds[seedIndex].amount = asNicks('0'); } if (shouldRender) renderSpends(); updateBuilder(); @@ -651,28 +689,23 @@ function updateBuilder() { try { console.log('Building transaction...'); - const txSettings: wasm.TxEngineSettings = { - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256', - cost_per_word: '32768', - witness_word_div: 1, - }; - const builder = new wasm.TxBuilder(txSettings); + const builder = new wasm.TxBuilder(getTxEngineSettings()); for (const spend of state.spends) { const lock = state.locks.find(l => l.id === spend.input.lockId); if (!lock) continue; - const refundLock: wasm.SpendCondition | null = + const refundLock: wasm.LockRoot | null = spend.seeds.length > 0 - ? (state.locks.find(l => l.id === spend.seeds[0].lockId)?.spendCondition ?? null) + ? ((state.locks.find(l => l.id === spend.seeds[0].lockId)?.spendCondition ?? + null) as wasm.SpendCondition | null) : lock.spendCondition; const spendBuilder = new wasm.SpendBuilder( spend.input.note.note, - lock.spendCondition, - refundLock + asLock(lock.spendCondition), + null, + refundLock ? asLock(refundLock) : null ); for (const seed of spend.seeds) { @@ -680,7 +713,7 @@ function updateBuilder() { if (seedLock) { const seedV1: wasm.SeedV1 = { output_source: null, - lock_root: { Lock: seedLock.spendCondition }, + lock_root: asLock(seedLock.spendCondition), note_data: [], gift: seed.amount, parent_hash: wasm.noteHash(spend.input.note.note), @@ -943,10 +976,10 @@ function renderTransaction() { txInfo.innerHTML = `
- Fee: ${formatNock(nicksToNock(String(fee)))} NOCK + Fee: ${formatNock(nicksToNock(feeStr))} NOCK
- Calculated Fee: ${formatNock(nicksToNock(String(calcFee)))} NOCK + Calculated Fee: ${formatNock(nicksToNock(calcFeeStr))} NOCK
TX ID: ${renderCopyableId(state.nockchainTx.id, 'TX ID')} @@ -956,17 +989,17 @@ function renderTransaction() { // Outputs: 0.2 use nockchainTxToRawTx + rawTxOutputs (maybe doesn't work) const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx); - const outputs = wasm.rawTxOutputs(rawTx); + const outputs = wasm.rawTxOutputs(rawTx, 0, getTxEngineSettings()); if (outputs && outputs.length > 0) { outputsList.innerHTML = outputs .map((output: wasm.Note, index: number) => { - const amount = output.assets != null ? BigInt(output.assets) : BigInt(0); + const amount = output.assets ?? asNicks('0'); const firstName = output.name?.first ?? ''; const lastName = output.name?.last ?? ''; return `
- Output ${index + 1}: ${formatNock(nicksToNock(String(amount)))} NOCK + Output ${index + 1}: ${formatNock(nicksToNock(amount))} NOCK
${firstName ? renderNoteName(firstName, lastName) : 'Unknown'} @@ -1410,25 +1443,27 @@ function confirmAddLock() { alert('PKH requires at least one address'); return; } - primitives.push({ Pkh: { m: pkhConfig.m, hashes: validAddrs } }); + primitives.push({ + tag: 'pkh', + m: pkhConfig.m, + hashes: validAddrs.map(addr => asDigest(addr)), + }); break; } case 'tim': { const timConfig = prim.tim || { type: 'csv', value: 1 }; - const value = timConfig.value ?? 0; + const value = asBlockHeight(timConfig.value ?? 0); if (timConfig.type === 'csv') { primitives.push({ - Tim: { - rel: { min: value, max: null }, - abs: { min: null, max: null }, - }, + tag: 'tim', + rel: { min: value, max: null }, + abs: { min: null, max: null }, }); } else { primitives.push({ - Tim: { - rel: { min: null, max: null }, - abs: { min: value, max: null }, - }, + tag: 'tim', + rel: { min: null, max: null }, + abs: { min: value, max: null }, }); } break; @@ -1440,11 +1475,11 @@ function confirmAddLock() { alert('HAX requires at least one hash.'); return; } - primitives.push({ Hax: validHashes }); + primitives.push({ tag: 'hax', preimages: validHashes.map(hash => asDigest(hash)) }); break; } case 'brn': - primitives.push('Brn'); + primitives.push({ tag: 'brn' }); break; } } @@ -1591,44 +1626,15 @@ signTxBtn.onclick = async () => { if (!state.nockchainTx || !state.builder || !state.provider) return; try { - const txNotes = state.builder.allNotes(); - const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx) as wasm.RawTxV1; - const rawTxProto = wasm.rawTxToProtobuf(rawTx); - const notesProto = txNotes.notes.map((n: wasm.Note) => wasm.noteToProtobuf(n)); - const spendCondProto = txNotes.spend_conditions.map((sc: wasm.SpendCondition) => - wasm.spendConditionToProtobuf(sc) - ); - - const signedTxProtobuf = await state.provider.signRawTx({ - rawTx: rawTxProto, - notes: notesProto, - spendConditions: spendCondProto, - }); - - const signedTxProtoObj = - typeof signedTxProtobuf === 'object' && !(signedTxProtobuf instanceof Uint8Array) - ? signedTxProtobuf - : (signedTxProtobuf as unknown as wasm.PbCom2RawTransaction); - const signedRawTx = wasm.rawTxFromProtobuf(signedTxProtoObj) as wasm.RawTxV1; - state.signedTx = wasm.rawTxV1ToNockchainTx(signedRawTx); + const signed = await state.provider.signTx(state.nockchainTx); + const signedRawTx = wasm.nockchainTxToRawTx(signed.tx) as wasm.RawTxV1; + state.signedTx = signed.tx; let isValid = true; let validationError = ''; try { - const txSettings: wasm.TxEngineSettings = { - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: '256', - cost_per_word: '32768', - witness_word_div: 1, - }; - const signedBuilder = wasm.TxBuilder.fromTx( - signedRawTx, - txNotes.notes, - txNotes.spend_conditions, - txSettings - ); + const signedBuilder = wasm.TxBuilder.fromNockchainTx(signed.tx, getTxEngineSettings()); signedBuilder.validate(); signedBuilder.free(); console.log('Signed transaction validated successfully'); diff --git a/src/constants.ts b/src/constants.ts index 0c47cab..855c41b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -63,6 +63,25 @@ export const DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS: Record = DEFAULT_TX_ENGINE_ACTIVATION_HEIGHTS +): TxEngineSettings { + const heights = Object.keys(activationHeights) + .map(Number) + .filter(Number.isFinite) + .sort((a, b) => a - b); + + if (heights.length === 0) { + return BYTHOS_TX_ENGINE_SETTINGS; + } + + return activationHeights[heights[heights.length - 1]]; +} + /** Default coinbase maturity in blocks (mainnet-style; e.g. 100). */ export const DEFAULT_COINBASE_TIMELOCK_BLOCKS = 100; From c9874cd45241c04bfcf224254eadb9618fb4ccfe Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 06:35:34 -0400 Subject: [PATCH 46/75] connect fix --- src/compat.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/compat.ts b/src/compat.ts index 12fe255..74e671b 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -213,8 +213,21 @@ function mapResponse( } if (fromV1 && !toV1) { // API 1 → legacy: { account, rpcConfig } → { grpcEndpoint, pkh } + const raw = response.result; + if (raw && typeof raw === 'object') { + const r = raw as Record; + // Extension may already return legacy `{ pkh, grpcEndpoint }` after internal + // handling; do not assume `account` exists (avoids reading undefined.type). + if ( + typeof r.pkh === 'string' && + typeof r.grpcEndpoint === 'string' && + !('account' in r) + ) { + return response; + } + } const v1 = response.result as ConnectResponse; - if (v1.account.type !== 'v1') { + if (!v1?.account || v1.account.type !== 'v1') { throw new Error('Invalid account type'); } const result: LegacyConnectResponse = { From 6762b9d80d8ce86b7ad7e0bbd51202347ad494ea Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:03:39 -0400 Subject: [PATCH 47/75] add guard to sign_message provider method --- src/compat.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/compat.ts b/src/compat.ts index 74e671b..c858f94 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -265,6 +265,19 @@ function mapResponse( } if (fromV1 && !toV1) { // API 1 → legacy: { signature, publicKey } → { signature, publicKeyHex } + const raw = response.result; + if (raw && typeof raw === 'object') { + const r = raw as Record; + // Extension returns legacy `{ signature: JSON string, publicKeyHex }`; do not + // assume WASM-shaped `signature.c` / `publicKey` (same pattern as CONNECT). + if ( + typeof r.signature === 'string' && + typeof r.publicKeyHex === 'string' && + !('publicKey' in r) + ) { + return response; + } + } const v1 = response.result as SignMessageResponse; const toLegacyHex = (v: string | Uint8Array): number[] => { From 734896fc038e325a9b9bace55bc44f81fd298aa7 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:37:56 -0400 Subject: [PATCH 48/75] bridge debugging + hotfix for spend condition index --- src/bridge-types.ts | 2 ++ src/bridge.ts | 67 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index 9e541e6..cd12866 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -56,6 +56,8 @@ export interface BridgeTransactionParams { */ export interface BuildBridgeTransactionOptions { txEngineSettings: TxEngineSettings; + /** Enable verbose SDK bridge debug logs */ + debug?: boolean; } /** diff --git a/src/bridge.ts b/src/bridge.ts index a2c97f3..b379e45 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -12,6 +12,7 @@ import type { BuildBridgeTransactionOptions, } from './bridge-types.js'; import type { + Lock, LockRoot, Digest, Note, @@ -117,6 +118,15 @@ export function isBridgeConfigured(config: BridgeConfig): boolean { ); } +function logBridgeDebug( + options: BuildBridgeTransactionOptions | undefined, + label: string, + payload: Record +): void { + if (options?.debug !== true) return; + console.log(`[SDK Bridge] ${label}:`, payload); +} + function parseDigestString(value: string, field: string): Digest { const trimmed = value.trim(); const bytes = base58.decode(trimmed); @@ -156,6 +166,18 @@ export async function buildBridgeTransaction( if (!isEvmAddress(params.destinationAddress)) { throw new Error(`Invalid destination address: ${params.destinationAddress}`); } + if (params.inputNotes.length !== params.spendConditions.length) { + throw new Error( + `Input note/spend condition length mismatch: ${params.inputNotes.length} notes vs ${params.spendConditions.length} conditions` + ); + } + + logBridgeDebug(options, 'Build start', { + destinationAddress: params.destinationAddress, + amountInNicks: params.amountInNicks, + inputCount: params.inputNotes.length, + refundPkh: params.refundPkh, + }); const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); const noteData: NoteData = [[config.noteDataKey, bridgeNounJs as Noun]]; @@ -176,12 +198,35 @@ export async function buildBridgeTransaction( for (let i = 0; i < params.inputNotes.length; i++) { const note = params.inputNotes[i]; const spendCondition = params.spendConditions[i]; + if (!spendCondition) { + logBridgeDebug(options, 'Missing spend condition', { + inputIndex: i, + noteAssets: note.assets, + }); + throw new Error('Spend condition is missing for this input note'); + } const noteAssets = BigInt(note.assets ?? 0); const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; remainingGift -= giftPortion; - const spendBuilder = new wasm.SpendBuilder(note, spendCondition, null, refundLock); + // V1 inputs require a lock + spend-condition index. Single-lock spends use index 0. + let spendBuilder: InstanceType; + try { + spendBuilder = new wasm.SpendBuilder( + note, + spendCondition as unknown as Lock, + 0, + refundLock as unknown as LockRoot + ); + } catch (error) { + logBridgeDebug(options, 'SpendBuilder creation failed', { + inputIndex: i, + noteAssets: note.assets, + error: error instanceof Error ? error.message : String(error), + }); + throw error; + } if (giftPortion > 0n) { const parentHash = wasm.noteHash(note); @@ -197,6 +242,13 @@ export async function buildBridgeTransaction( spendBuilder.computeRefund(false); builder.spend(spendBuilder); + + logBridgeDebug(options, 'Input processed', { + inputIndex: i, + noteAssetsNicks: noteAssets.toString(), + giftPortionNicks: giftPortion.toString(), + remainingGiftNicks: remainingGift.toString(), + }); } builder.recalcAndSetFee(false); @@ -206,6 +258,12 @@ export async function buildBridgeTransaction( const txId = transaction.id; const fee = feeResult; + logBridgeDebug(options, 'Build complete', { + txId, + feeNicks: fee, + remainingGiftNicks: remainingGift.toString(), + }); + return { transaction, txId, @@ -228,6 +286,10 @@ export async function validateBridgeTransaction( try { const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); const outputs = wasm.rawTxOutputs(rawTx, 0, options.txEngineSettings); + logBridgeDebug(options, 'Validate start', { + outputCount: outputs.length, + noteDataKey: config.noteDataKey, + }); if (outputs.length === 0) { return { valid: false, error: 'Transaction has no outputs' }; @@ -382,6 +444,9 @@ export async function validateBridgeTransaction( chain: validatedChain, }; } catch (err) { + logBridgeDebug(options, 'Validate failed', { + error: err instanceof Error ? err.message : String(err), + }); return { valid: false, error: `Transaction validation failed: ${err instanceof Error ? err.message : String(err)}`, From 056c90788f0e69582a1fee98ae8ded2676127727 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 10:54:20 -0400 Subject: [PATCH 49/75] hotfix for bridge and logs --- src/bridge.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index b379e45..c596a43 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -187,9 +187,10 @@ export async function buildBridgeTransaction( config.addresses.map(address => parseDigestString(address, 'bridge address')) ); const bridgeSpendCondition: SpendCondition = wasm.spendConditionNewPkh(bridgePkh); - const bridgeLockRoot: LockRoot = bridgeSpendCondition; + const bridgeLockRoot: LockRoot = wasm.lockFromList([bridgeSpendCondition]); const refundPkhObj = wasm.pkhSingle(parseDigestString(params.refundPkh, 'refund pkh')); - const refundLock: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); + const refundSpendCondition: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); + const refundLockRoot: LockRoot = wasm.lockFromList([refundSpendCondition]); const builder = new wasm.TxBuilder(options.txEngineSettings); @@ -210,14 +211,16 @@ export async function buildBridgeTransaction( const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; remainingGift -= giftPortion; - // V1 inputs require a lock + spend-condition index. Single-lock spends use index 0. + // V1 inputs require a lock + spend-condition index. + // Convert the discovered spend condition into a single-leaf lock for this input. let spendBuilder: InstanceType; try { + const inputLock = wasm.lockFromList([spendCondition]); spendBuilder = new wasm.SpendBuilder( note, - spendCondition as unknown as Lock, + inputLock, 0, - refundLock as unknown as LockRoot + refundLockRoot ); } catch (error) { logBridgeDebug(options, 'SpendBuilder creation failed', { From 1ec925c7bb658fc284bd66fee605d76cecb000f8 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 15 Apr 2026 11:19:55 -0400 Subject: [PATCH 50/75] replicate nockswap --- src/bridge.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index c596a43..95473fa 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -187,10 +187,10 @@ export async function buildBridgeTransaction( config.addresses.map(address => parseDigestString(address, 'bridge address')) ); const bridgeSpendCondition: SpendCondition = wasm.spendConditionNewPkh(bridgePkh); - const bridgeLockRoot: LockRoot = wasm.lockFromList([bridgeSpendCondition]); + const bridgeLockRoot: LockRoot = bridgeSpendCondition as unknown as LockRoot; const refundPkhObj = wasm.pkhSingle(parseDigestString(params.refundPkh, 'refund pkh')); const refundSpendCondition: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); - const refundLockRoot: LockRoot = wasm.lockFromList([refundSpendCondition]); + const refundLockRoot: LockRoot = refundSpendCondition as unknown as LockRoot; const builder = new wasm.TxBuilder(options.txEngineSettings); @@ -211,14 +211,11 @@ export async function buildBridgeTransaction( const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; remainingGift -= giftPortion; - // V1 inputs require a lock + spend-condition index. - // Convert the discovered spend condition into a single-leaf lock for this input. let spendBuilder: InstanceType; try { - const inputLock = wasm.lockFromList([spendCondition]); spendBuilder = new wasm.SpendBuilder( note, - inputLock, + spendCondition as unknown as Lock, 0, refundLockRoot ); From 087d141aaced8c08ab4a3a066c5064d523aeac94 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 20 Apr 2026 06:21:06 -0400 Subject: [PATCH 51/75] fix atom decoding for bridge logic --- src/bridge.ts | 167 ++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 67 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index 95473fa..0b6507d 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -19,6 +19,8 @@ import type { NoteData, Nicks, Noun, + PbCom2Note, + PbCom2NoteDataEntry, PbCom2RawTransaction, SeedV1, SpendCondition, @@ -81,17 +83,20 @@ export function bigintToAtom(n: bigint): string { /** * Build the bridge noun structure for an EVM address. - * Structure: [versionTag [chainTag [belt1 [belt2 belt3]]]] + * + * Shape: `[version [chain [belt1 [belt2 belt3]]]]` — a chain of right-nested + * pairs with five hex-encoded atom leaves, matching the Hoon cell stored in + * the on-chain bridge note. */ export function buildBridgeNoun( evmAddress: string, config: Pick -): unknown { +): Noun { const [belt1, belt2, belt3] = evmAddressToBelts(evmAddress); return [ config.versionTag, [config.chainTag, [bigintToAtom(belt1), [bigintToAtom(belt2), bigintToAtom(belt3)]]], - ]; + ] as unknown as Noun; } /** @@ -127,6 +132,50 @@ function logBridgeDebug( console.log(`[SDK Bridge] ${label}:`, payload); } +// TODO(iris-wasm): `isAtom` and `readPair` are adapters for `Noun`'s runtime +// shape, not bridge logic. They should live alongside `cue`/`jam` in +// `iris-wasm` (ideally replaced by a proper `nounToJs` that returns nested +// pairs). Keep them here until that package exposes an equivalent. + +/** + * Type guard: is this noun an atom (a leaf, represented as a hex string)? + * + * After this returns `true` TypeScript narrows the noun to `string`, so + * callers can use the value directly without a separate rebinding. + */ +function isAtom(noun: Noun | undefined): noun is string { + return typeof noun === 'string'; +} + +/** + * Read one `[head, tail]` pair out of a noun. + * + * A noun is either an atom (string) or a pair of nouns. On-chain the bridge + * note is stored as a chain of right-nested pairs: `[v [c [b1 [b2 b3]]]]`. + * + * The wasm we use here returns that nested structure to JS in a *flattened* + * form: instead of `["v", ["c", ["b1", ["b2", "b3"]]]]` we receive + * `["v", "c", "b1", "b2", "b3"]`. Same data, same pairing intent, just + * collapsed by the serializer on the way across the JS boundary. + * + * To read it back as logical pairs we treat the flat array as + * `[head, ...tail]`: the first element is this pair's head, and everything + * after it is the tail (itself another noun). This function applies that + * convention once; call it repeatedly to walk the chain. + * + * Returns `null` if the noun is not a pair (e.g. an atom, or malformed). + */ +function readPair(noun: Noun | undefined): [Noun, Noun] | null { + // The wasm `Noun` type is declared as `string | [Noun]`, but at runtime a + // pair arrives as a flat array of length >= 2. Re-narrow through `unknown` + // so we can inspect the real shape. + const arr = noun as unknown; + if (!Array.isArray(arr) || arr.length < 2) return null; + const head = arr[0] as Noun; + const tail = (arr.length === 2 ? arr[1] : arr.slice(1)) as Noun; + return [head, tail]; +} + function parseDigestString(value: string, field: string): Digest { const trimmed = value.trim(); const bytes = base58.decode(trimmed); @@ -145,7 +194,7 @@ export async function createBridgeNoteData( config: BridgeConfig ): Promise { const nounJs = buildBridgeNoun(evmAddress, config); - return wasm.jam(nounJs as Noun); + return wasm.jam(nounJs); } /** @@ -180,7 +229,7 @@ export async function buildBridgeTransaction( }); const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); - const noteData: NoteData = [[config.noteDataKey, bridgeNounJs as Noun]]; + const noteData: NoteData = [[config.noteDataKey, bridgeNounJs]]; const bridgePkh = wasm.pkhNew( BigInt(config.threshold), @@ -295,23 +344,22 @@ export async function validateBridgeTransaction( return { valid: false, error: 'Transaction has no outputs' }; } - const outputData: Array<{ - assets: bigint; - noteData: NoteData; - }> = outputs.map((output: Note) => { - const noteData = 'note_data' in output ? ((output.note_data as NoteData) ?? []) : []; + // Read each output via its protobuf form so the bridge note data is the raw + // jammed bytes (blob), not a serde-shaped JS value. + const outputData = outputs.map((output: Note) => { + const proto = wasm.noteToProtobuf(output) as PbCom2Note; + const version = proto.note_version; + const v1 = version && 'V1' in version ? version.V1 : undefined; + const entries: PbCom2NoteDataEntry[] = v1?.note_data?.entries ?? []; return { - assets: BigInt(output.assets ?? 0), - noteData, + assets: BigInt(v1?.assets?.value ?? 0), + entries, }; }); let bridgeOutput: (typeof outputData)[0] | null = null; for (const output of outputData) { - const hasKey = output.noteData?.some( - (entry: [string, Noun]) => entry[0] === config.noteDataKey - ); - if (hasKey) { + if (output.entries.some(e => e.key === config.noteDataKey)) { bridgeOutput = output; break; } @@ -331,91 +379,76 @@ export async function validateBridgeTransaction( }; } - if (!bridgeOutput.noteData?.length) { - return { valid: false, error: 'Bridge output missing note data' }; - } - - const bridgeEntryPair = bridgeOutput.noteData.find( - (entry: [string, Noun]) => entry[0] === config.noteDataKey - ); - if (!bridgeEntryPair) { + const bridgeEntry = bridgeOutput.entries.find(e => e.key === config.noteDataKey); + if (!bridgeEntry) { return { valid: false, error: `Bridge output missing '${config.noteDataKey}' note data entry`, }; } - const bridgeEntryValue = bridgeEntryPair[1]; let destinationAddress: string | undefined; let belts: [bigint, bigint, bigint] | undefined; let validatedVersion: string | undefined; let validatedChain: string | undefined; - const validatedNoteDataKey = bridgeEntryPair[0]; + const validatedNoteDataKey = bridgeEntry.key; try { - const value = bridgeEntryValue as unknown; - const decoded: unknown = Array.isArray(value) - ? value - : wasm.cue( - value instanceof Uint8Array ? value : new Uint8Array(value as unknown as number[]) - ); - - if (!Array.isArray(decoded) || decoded.length !== 2) { + // Deserialize the jammed blob into a noun, then walk it as a chain of + // right-nested pairs: [version [chain [belt1 [belt2 belt3]]]]. + const noun = wasm.cue(new Uint8Array(bridgeEntry.blob)); + + const versionPair = readPair(noun); + if (!versionPair) { return { valid: false, error: 'Invalid bridge note data structure: expected [version, [chain, belts]]', }; } - - const version = decoded[0]; - if (version !== config.versionTag && version !== Number(config.versionTag)) { + const [version, chainAndBelts] = versionPair; + if (!isAtom(version)) { + return { valid: false, error: 'Invalid bridge note data: version is not an atom' }; + } + if (version !== config.versionTag && version !== String(Number(config.versionTag))) { return { valid: false, error: `Invalid bridge note data version: expected ${config.versionTag}, got ${version}`, }; } - validatedVersion = String(version); + validatedVersion = version; - const chainAndBelts = decoded[1]; - if (!Array.isArray(chainAndBelts) || chainAndBelts.length !== 2) { - return { - valid: false, - error: 'Invalid bridge note data: missing chain and belts', - }; + const chainPair = readPair(chainAndBelts); + if (!chainPair) { + return { valid: false, error: 'Invalid bridge note data: missing chain and belts' }; } - - const chain = chainAndBelts[0]; - if (String(chain) !== config.chainTag) { + const [chain, beltData] = chainPair; + if (!isAtom(chain) || chain !== config.chainTag) { return { valid: false, - error: `Invalid bridge chain: expected ${config.chainTag}, got ${chain}`, + error: `Invalid bridge chain: expected ${config.chainTag}, got ${String(chain)}`, }; } - validatedChain = String(chain); + validatedChain = chain; - const beltData = chainAndBelts[1]; - if (!Array.isArray(beltData) || beltData.length !== 2) { - return { - valid: false, - error: 'Invalid bridge note data: invalid belt structure', - }; + const belt1Pair = readPair(beltData); + if (!belt1Pair) { + return { valid: false, error: 'Invalid bridge note data: invalid belt structure' }; } + const [belt1Noun, belt2And3] = belt1Pair; - const belt1Hex = beltData[0]; - const belt2And3 = beltData[1]; - if (!Array.isArray(belt2And3) || belt2And3.length !== 2) { - return { - valid: false, - error: 'Invalid bridge note data: invalid belt2/belt3 structure', - }; + const belt2Pair = readPair(belt2And3); + if (!belt2Pair) { + return { valid: false, error: 'Invalid bridge note data: invalid belt2/belt3 structure' }; } + const [belt2Noun, belt3Noun] = belt2Pair; - const belt2Hex = belt2And3[0]; - const belt3Hex = belt2And3[1]; + if (!isAtom(belt1Noun) || !isAtom(belt2Noun) || !isAtom(belt3Noun)) { + return { valid: false, error: 'Invalid bridge note data: belt values are not atoms' }; + } - const belt1 = BigInt('0x' + belt1Hex); - const belt2 = BigInt('0x' + belt2Hex); - const belt3 = BigInt('0x' + belt3Hex); + const belt1 = BigInt('0x' + belt1Noun); + const belt2 = BigInt('0x' + belt2Noun); + const belt3 = BigInt('0x' + belt3Noun); belts = [belt1, belt2, belt3]; destinationAddress = beltsToEvmAddress(belt1, belt2, belt3); From 7d0bc1c2e6acc920d32d84dceec7682c443057b4 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 23 Apr 2026 05:41:27 -0400 Subject: [PATCH 52/75] pick smallest note that is larger than 100 nocks for debug --- src/migration.ts | 50 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index aff26be..d10758a 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -37,6 +37,14 @@ function parseV0Note(note?: PbCom2Note | null): NoteV0 | null { return guard.isNoteV0(parsed) ? parsed : null; } +function summarizeNote(note: NoteV0): { assetsNicks: string; assetsNock: number } { + const assets = BigInt(note.assets); + return { + assetsNicks: assets.toString(), + assetsNock: Number(assets) / NOCK_TO_NICKS, + }; +} + function appendV0MigrationSpends( builder: wasm.TxBuilder, notes: NoteV0[], @@ -125,6 +133,8 @@ export async function buildV0MigrationTx( ): Promise { const balanceResult = await queryV0Balance(sourcePublicKey, grpcEndpoint); const maxNotes = options.maxNotes; + const debugSingleNote = + typeof maxNotes === 'number' && Number.isFinite(maxNotes) && Math.floor(maxNotes) === 1; try { const v0Notes = balanceResult.v0Notes; @@ -136,11 +146,36 @@ export async function buildV0MigrationTx( .map(note => ({ note, assets: BigInt(note.assets) })) .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0)); - const notesToUse: NoteV0[] = - typeof maxNotes === 'number' && Number.isFinite(maxNotes) && maxNotes > 0 + // In single-note / debug mode, the smallest raw note often can't cover + // the migration fee, which causes build to fail. Heuristic: among notes + // >= 100 NOCK, take the smallest. Fall back to the absolute smallest only + // if no note clears the threshold (build may still fail, but we surface + // the real "not enough value to cover fee" error instead of silently + // picking a doomed note). + const MIN_SINGLE_NOTE_NICKS = BigInt(100) * BigInt(NOCK_TO_NICKS); + const singleNoteCandidates = sortedNotes.filter( + entry => entry.assets >= MIN_SINGLE_NOTE_NICKS + ); + const singleNotePick = (singleNoteCandidates[0] ?? sortedNotes[0])?.note; + + const notesToUse: NoteV0[] = debugSingleNote + ? singleNotePick + ? [singleNotePick] + : [] + : typeof maxNotes === 'number' && Number.isFinite(maxNotes) && maxNotes > 0 ? sortedNotes.slice(0, Math.floor(maxNotes)).map(entry => entry.note) : sortedNotes.map(entry => entry.note); + if (debugSingleNote && singleNotePick) { + console.log('[SDK Migration] Single-note mode selected note:', { + pickedNote: summarizeNote(singleNotePick), + viaThreshold: singleNoteCandidates.length > 0, + thresholdNock: Number(MIN_SINGLE_NOTE_NICKS) / NOCK_TO_NICKS, + totalLegacyNotes: v0Notes.length, + notesAboveThreshold: singleNoteCandidates.length, + }); + } + const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); const refundLock = wasm.lockHash(targetSpendCondition); const builder = new wasm.TxBuilder(options.txEngineSettings); @@ -178,7 +213,16 @@ export async function buildV0MigrationTx( }; return result; } catch (e) { - console.warn('[SDK Migration] Build failed, returning balance only:', e); + console.warn('[SDK Migration] Build failed, returning balance only:', { + error: e instanceof Error ? e.message : String(e), + sourceAddress: balanceResult.sourceAddress, + rawNotesFromRpc: balanceResult.rawNotesFromRpc, + legacyV0Notes: balanceResult.v0Notes.length, + totalNicks: balanceResult.totalNicks, + smallestNoteNock: balanceResult.smallestNoteNock, + maxNotes, + singleNoteMode: debugSingleNote, + }); return balanceResult; } } From 94a8fc080d939c537e748f3182cb3acb091f9377 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:19:43 -0400 Subject: [PATCH 53/75] fix response type for legacy API request shapes --- src/compat.ts | 48 ++++++++++++++++++------------------------------ 1 file changed, 18 insertions(+), 30 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index c858f94..8d81e33 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -303,33 +303,11 @@ function mapResponse( } return response; } - case 'nock_signRawTx': { - if (fromV1 && !toV1) { - const v1 = response.result as SignTxResponse; - if (!v1?.tx) { - throw new Error('Invalid signTx response'); - } - const rawTx = nockchainTxToRawTx(v1.tx); - if (!guard.isRawTxV1(rawTx)) { - throw new Error('Only V1 Raw TXs are supported at the moment'); - } - const result = { rawTx: rawTxToProtobuf(rawTx) }; - return { ...response, result }; - } - if (!fromV1 && toV1) { - const legacy = response.result as { rawTx?: PbCom2RawTransaction }; - if (!legacy?.rawTx || !guard.isPbCom2RawTransaction(legacy.rawTx)) { - throw new Error('Invalid legacy signRawTx response'); - } - const rawTx = rawTxFromProtobuf(legacy.rawTx) as RawTxV1; - const nockchainTx = rawTxV1ToNockchainTx(rawTx); - const result: SignTxResponse = { tx: nockchainTx }; - return { ...response, result }; - } - return response; - } case PROVIDER_METHODS.SIGN_TX: { if (fromV1 && !toV1) { + // Legacy callers reach this path via `nock_signRawTx → SIGN_TX` request + // remapping, and their historical contract was a bare + // `PbCom2RawTransaction` (fed straight into `rpcClient.sendTransaction`). const v1 = response.result as SignTxResponse; if (!v1?.tx) { throw new Error('Invalid signTx response'); @@ -338,15 +316,25 @@ function mapResponse( if (!guard.isRawTxV1(rawTx)) { throw new Error('Only V1 Raw TXs are supported at the moment'); } - const result = { rawTx: rawTxToProtobuf(rawTx) }; - return { ...response, result }; + return { ...response, result: rawTxToProtobuf(rawTx) }; } if (!fromV1 && toV1) { - const legacy = response.result as { rawTx?: PbCom2RawTransaction }; - if (!legacy?.rawTx || !guard.isPbCom2RawTransaction(legacy.rawTx)) { + // Accept both the historical bare shape and the previously-wrapped + // `{ rawTx }` shape. + const raw = response.result; + let legacyRawTx: PbCom2RawTransaction | undefined; + if (guard.isPbCom2RawTransaction(raw)) { + legacyRawTx = raw; + } else if (raw && typeof raw === 'object' && 'rawTx' in (raw as Record)) { + const wrapped = (raw as { rawTx?: unknown }).rawTx; + if (guard.isPbCom2RawTransaction(wrapped)) { + legacyRawTx = wrapped; + } + } + if (!legacyRawTx) { throw new Error('Invalid legacy signRawTx response'); } - const rawTx = rawTxFromProtobuf(legacy.rawTx); + const rawTx = rawTxFromProtobuf(legacyRawTx); if (!guard.isRawTxV1(rawTx)) { throw new Error('Only V1 Raw TXs are supported at the moment'); } From 5daf5b0f5cabeed481fd027ab2f83833de65176f Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 23 Apr 2026 07:26:23 -0400 Subject: [PATCH 54/75] display input notes properly on legacy sign raw tx request --- src/compat.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/compat.ts b/src/compat.ts index 8d81e33..f2a5a0e 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -27,6 +27,7 @@ import { publicKeyToHex, RawTxV1, noteToProtobuf, + noteFromProtobuf, spendConditionToProtobuf, SpendCondition, Digest, @@ -139,7 +140,8 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR throw new Error('Only V1 Raw TXs are supported at the moment'); } const tx = rawTxV1ToNockchainTx(rawTx); - const signParams = { tx }; + const notes = req.notes.map(n => noteFromProtobuf(n)); + const signParams: SignTxRequest = { tx, notes }; return { ...request, method: PROVIDER_METHODS.SIGN_TX, params: signParams as unknown }; } return request; From 8a5e47e2067648cdb06e76355ce6fa2855befb7e Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 23 Apr 2026 11:48:31 -0400 Subject: [PATCH 55/75] use smallest 2 notes with more than 100 nocks total --- src/migration.ts | 67 ++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 53 insertions(+), 14 deletions(-) diff --git a/src/migration.ts b/src/migration.ts index d10758a..7f3ad59 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -45,6 +45,34 @@ function summarizeNote(note: NoteV0): { assetsNicks: string; assetsNock: number }; } +/** + * Pick `count` notes in ascending value order. Prefer notes with value >= + * `minNicks` each (migration fee heuristic); if fewer than `count` qualify, + * fill remaining slots from the smallest notes overall (same order as + * `sortedNotes`). + */ +function selectNotesWithMinThreshold( + sortedNotes: Array<{ note: NoteV0; assets: bigint }>, + count: number, + minNicks: bigint +): NoteV0[] { + const out: NoteV0[] = []; + const used = new Set(); + + for (let i = 0; i < sortedNotes.length && out.length < count; i++) { + if (sortedNotes[i].assets >= minNicks) { + out.push(sortedNotes[i].note); + used.add(i); + } + } + for (let i = 0; i < sortedNotes.length && out.length < count; i++) { + if (used.has(i)) continue; + out.push(sortedNotes[i].note); + used.add(i); + } + return out; +} + function appendV0MigrationSpends( builder: wasm.TxBuilder, notes: NoteV0[], @@ -123,7 +151,9 @@ export async function queryV0Balance( * For balance only, use {@link queryV0Balance}. * Caller must have initialized WASM (e.g. await wasm.default()) before using. * - * @param options.maxNotes - Optional cap on number of smallest legacy notes to include. + * @param options.maxNotes - Optional cap on how many inputs to include (ascending + * by value). Each slot prefers notes with value >= 100 NOCK, then fills from + * the smallest notes overall if needed (same idea as single-note debug mode). */ export async function buildV0MigrationTx( sourcePublicKey: PublicKey, @@ -146,34 +176,43 @@ export async function buildV0MigrationTx( .map(note => ({ note, assets: BigInt(note.assets) })) .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0)); - // In single-note / debug mode, the smallest raw note often can't cover - // the migration fee, which causes build to fail. Heuristic: among notes - // >= 100 NOCK, take the smallest. Fall back to the absolute smallest only - // if no note clears the threshold (build may still fail, but we surface - // the real "not enough value to cover fee" error instead of silently - // picking a doomed note). - const MIN_SINGLE_NOTE_NICKS = BigInt(100) * BigInt(NOCK_TO_NICKS); - const singleNoteCandidates = sortedNotes.filter( - entry => entry.assets >= MIN_SINGLE_NOTE_NICKS - ); + // Notes below ~100 NOCK often cannot cover the migration fee alone. For + // capped builds (`maxNotes`), prefer notes >= 100 NOCK (smallest first), + // then fill any remaining slots from the absolute smallest notes. + const MIN_NOTE_NICKS = BigInt(100) * BigInt(NOCK_TO_NICKS); + const singleNoteCandidates = sortedNotes.filter(entry => entry.assets >= MIN_NOTE_NICKS); const singleNotePick = (singleNoteCandidates[0] ?? sortedNotes[0])?.note; + const cappedCount = + typeof maxNotes === 'number' && Number.isFinite(maxNotes) && maxNotes > 0 + ? Math.floor(maxNotes) + : 0; + const notesToUse: NoteV0[] = debugSingleNote ? singleNotePick ? [singleNotePick] : [] - : typeof maxNotes === 'number' && Number.isFinite(maxNotes) && maxNotes > 0 - ? sortedNotes.slice(0, Math.floor(maxNotes)).map(entry => entry.note) + : cappedCount > 0 + ? selectNotesWithMinThreshold(sortedNotes, cappedCount, MIN_NOTE_NICKS) : sortedNotes.map(entry => entry.note); if (debugSingleNote && singleNotePick) { console.log('[SDK Migration] Single-note mode selected note:', { pickedNote: summarizeNote(singleNotePick), viaThreshold: singleNoteCandidates.length > 0, - thresholdNock: Number(MIN_SINGLE_NOTE_NICKS) / NOCK_TO_NICKS, + thresholdNock: Number(MIN_NOTE_NICKS) / NOCK_TO_NICKS, totalLegacyNotes: v0Notes.length, notesAboveThreshold: singleNoteCandidates.length, }); + } else if (cappedCount > 1) { + console.log('[SDK Migration] Capped-note mode selected notes:', { + requested: cappedCount, + pickedCount: notesToUse.length, + picked: notesToUse.slice(0, 10).map(summarizeNote), + thresholdNock: Number(MIN_NOTE_NICKS) / NOCK_TO_NICKS, + notesAboveThreshold: singleNoteCandidates.length, + totalLegacyNotes: v0Notes.length, + }); } const targetSpendCondition = buildSinglePkhSpendCondition(targetV1Pkh); From a2caf78d9d948c4c15fc6bdc2cb209b9e21f081f Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 27 Apr 2026 18:00:58 -0400 Subject: [PATCH 56/75] fix: remove redundant guard: extension only returns modern format --- src/compat.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index f2a5a0e..c438397 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -267,19 +267,6 @@ function mapResponse( } if (fromV1 && !toV1) { // API 1 → legacy: { signature, publicKey } → { signature, publicKeyHex } - const raw = response.result; - if (raw && typeof raw === 'object') { - const r = raw as Record; - // Extension returns legacy `{ signature: JSON string, publicKeyHex }`; do not - // assume WASM-shaped `signature.c` / `publicKey` (same pattern as CONNECT). - if ( - typeof r.signature === 'string' && - typeof r.publicKeyHex === 'string' && - !('publicKey' in r) - ) { - return response; - } - } const v1 = response.result as SignMessageResponse; const toLegacyHex = (v: string | Uint8Array): number[] => { From 562b61e0dfa8567bfd38319d9ba53efd5ed200a6 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:19:02 -0400 Subject: [PATCH 57/75] more informative comments --- src/compat.ts | 6 +++++- src/provider.ts | 1 + src/types.ts | 4 ++-- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/compat.ts b/src/compat.ts index c438397..3c48e15 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -42,7 +42,8 @@ import { /** * Legacy `nock_signRawTx` RPC params (API 0): protobuf raw tx plus matching notes and spend conditions. - * Compat maps this to v1 `SIGN_TX` with a native `NockchainTx`. v1 callers use `SignTxRequest` instead. + * Compat maps this to v1 `SIGN_TX` with a native `NockchainTx`. + * Notes are preserved as approval-display metadata; signing uses the canonical transaction. */ interface LegacySignRawTxRequest { rawTx: PbCom2RawTransaction; @@ -141,6 +142,9 @@ function mapRequest(request: RpcRequest, fromApi?: string, toApi?: string): RpcR } const tx = rawTxV1ToNockchainTx(rawTx); const notes = req.notes.map(n => noteFromProtobuf(n)); + // `spendConditions` are validated above for legacy request compatibility. They are + // encoded in / derivable from the raw transaction, so the v1 request only carries + // notes as display metadata. const signParams: SignTxRequest = { tx, notes }; return { ...request, method: PROVIDER_METHODS.SIGN_TX, params: signParams as unknown }; } diff --git a/src/provider.ts b/src/provider.ts index ec480ac..cae3a11 100644 --- a/src/provider.ts +++ b/src/provider.ts @@ -157,6 +157,7 @@ export class NockchainProvider { * Sign a raw transaction * Input must be NockchainTx. * @param tx - The transaction to sign + * @param notes - Optional approval-display metadata for legacy compatibility * @returns Promise resolving to the signed transaction * @throws {NoAccountError} If no account is connected * @throws {UserRejectedError} If the user rejects the signing request diff --git a/src/types.ts b/src/types.ts index 16814c2..c18d543 100644 --- a/src/types.ts +++ b/src/types.ts @@ -90,8 +90,8 @@ export interface SendTransactionRequest { export interface SignTxRequest { tx: NockchainTx; /** - * Optional notes for the transaction. Required for API 0 wallet compatibility. - * This will be removed in future SDK releases. + * Optional input notes supplied for approval display and API 0 wallet compatibility. + * The canonical signer consumes `tx`; notes are sidecar metadata. */ notes?: Note[]; } From 62c67a88c9304de58a94fa0edbbc06b8dfb94302 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:21:19 -0400 Subject: [PATCH 58/75] fix example to use tx engine settings from provider --- examples/tx-builder.ts | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 40e58d4..4fb36dd 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -1,4 +1,4 @@ -import { NockchainProvider, wasm } from '../src/index'; +import { NockchainProvider, getLatestTxEngineSettings, wasm } from '../src/index'; // ===== Types ===== // Note: Nicks are strings in current WASM version, so they need to be cast into integers for arithmetic @@ -42,6 +42,7 @@ const state = { connected: false, walletPkh: null as string | null, grpcEndpoint: null as string | null, + txEngineSettings: getLatestTxEngineSettings(), provider: null as NockchainProvider | null, grpcClient: null as wasm.GrpcClient | null, @@ -137,16 +138,6 @@ function asTxLock(spendCondition: wasm.SpendCondition): wasm.TxLock { return spendCondition as unknown as wasm.TxLock; } -function getTxEngineSettings(): wasm.TxEngineSettings { - return { - tx_engine_version: 1, - tx_engine_patch: 0, - min_fee: asNicks('256'), - cost_per_word: asNicks('32768'), - witness_word_div: 1, - }; -} - declare global { interface Window { copyToClipboard: (text: string, element: HTMLElement) => void; @@ -259,6 +250,7 @@ connectBtn.onclick = async () => { const info = await state.provider.connect(); state.grpcEndpoint = info.rpcConfig.rpcUrl; state.walletPkh = info.account.address; + state.txEngineSettings = getLatestTxEngineSettings(info.rpcConfig.txEngineActivationHeights); state.connected = true; connectBtn.textContent = truncateAddress(state.walletPkh); @@ -689,7 +681,7 @@ function updateBuilder() { try { console.log('Building transaction...'); - const builder = new wasm.TxBuilder(getTxEngineSettings()); + const builder = new wasm.TxBuilder(state.txEngineSettings); for (const spend of state.spends) { const lock = state.locks.find(l => l.id === spend.input.lockId); @@ -989,7 +981,7 @@ function renderTransaction() { // Outputs: 0.2 use nockchainTxToRawTx + rawTxOutputs (maybe doesn't work) const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx); - const outputs = wasm.rawTxOutputs(rawTx, 0, getTxEngineSettings()); + const outputs = wasm.rawTxOutputs(rawTx, 0, state.txEngineSettings); if (outputs && outputs.length > 0) { outputsList.innerHTML = outputs .map((output: wasm.Note, index: number) => { @@ -1634,7 +1626,7 @@ signTxBtn.onclick = async () => { let validationError = ''; try { - const signedBuilder = wasm.TxBuilder.fromNockchainTx(signed.tx, getTxEngineSettings()); + const signedBuilder = wasm.TxBuilder.fromNockchainTx(signed.tx, state.txEngineSettings); signedBuilder.validate(); signedBuilder.free(); console.log('Signed transaction validated successfully'); From cae86c63b9e42ef105b20dee825011d3d6ad245d Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:32:16 -0400 Subject: [PATCH 59/75] clarify tx builder output preview comment --- examples/tx-builder.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/tx-builder.ts b/examples/tx-builder.ts index 4fb36dd..6266b6d 100644 --- a/examples/tx-builder.ts +++ b/examples/tx-builder.ts @@ -979,7 +979,7 @@ function renderTransaction() {
`; - // Outputs: 0.2 use nockchainTxToRawTx + rawTxOutputs (maybe doesn't work) + // Preview outputs from the raw transaction using the connected network's tx engine settings. const rawTx = wasm.nockchainTxToRawTx(state.nockchainTx); const outputs = wasm.rawTxOutputs(rawTx, 0, state.txEngineSettings); if (outputs && outputs.length > 0) { From 049ef0c70b45d5c9d55b86cf2e284664e0339df8 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:13:58 -0400 Subject: [PATCH 60/75] fix edge case of a silent fail when the note amount isn't enough yet it still passes --- src/bridge.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/bridge.ts b/src/bridge.ts index 0b6507d..fe239d6 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -300,6 +300,19 @@ export async function buildBridgeTransaction( }); } + if (remainingGift > 0n) { + const requestedGift = BigInt(params.amountInNicks); + const fundedGift = requestedGift - remainingGift; + logBridgeDebug(options, 'Insufficient bridge input assets', { + requestedGiftNicks: requestedGift.toString(), + fundedGiftNicks: fundedGift.toString(), + shortfallNicks: remainingGift.toString(), + }); + throw new Error( + `Insufficient input note assets for bridge amount: requested ${requestedGift.toString()} nicks, funded ${fundedGift.toString()} nicks, shortfall ${remainingGift.toString()} nicks` + ); + } + builder.recalcAndSetFee(false); const feeResult = builder.curFee(); const transaction = builder.build(); From 06901aeb10291a08594abcc306d04369cc44d5bb Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 28 Apr 2026 06:14:41 -0400 Subject: [PATCH 61/75] format --- src/bridge.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/bridge.ts b/src/bridge.ts index fe239d6..bf4eb51 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -358,7 +358,7 @@ export async function validateBridgeTransaction( } // Read each output via its protobuf form so the bridge note data is the raw - // jammed bytes (blob), not a serde-shaped JS value. + // jammed bytes (blob), not a serde-shaped JS value. const outputData = outputs.map((output: Note) => { const proto = wasm.noteToProtobuf(output) as PbCom2Note; const version = proto.note_version; From 4406861adcf3fed2e79ad621ae6c6b9ab4c27b82 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 29 Apr 2026 07:55:27 -0400 Subject: [PATCH 62/75] remove stringtoatoms method --- src/bridge.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index bf4eb51..8af7ba4 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -65,15 +65,6 @@ export function beltsToEvmAddress(belt1: bigint, belt2: bigint, belt3: bigint): return '0x' + address.toString(16).padStart(40, '0'); } -/** Encode a string as a Hoon cord (little-endian hex). */ -export function stringToAtom(str: string): string { - const bytes = new TextEncoder().encode(str); - let hex = ''; - for (let i = bytes.length - 1; i >= 0; i--) { - hex += bytes[i].toString(16).padStart(2, '0'); - } - return hex || '0'; -} /** Encode a bigint as hex (no 0x prefix). */ export function bigintToAtom(n: bigint): string { From f1b73c6f67d9d1b85d26d26ddb329c34330858db Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:30:41 -0400 Subject: [PATCH 63/75] remove bridge debug options --- src/bridge-types.ts | 2 - src/bridge.ts | 161 ++++++++++++++++---------------------------- 2 files changed, 58 insertions(+), 105 deletions(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index cd12866..9e541e6 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -56,8 +56,6 @@ export interface BridgeTransactionParams { */ export interface BuildBridgeTransactionOptions { txEngineSettings: TxEngineSettings; - /** Enable verbose SDK bridge debug logs */ - debug?: boolean; } /** diff --git a/src/bridge.ts b/src/bridge.ts index 8af7ba4..fa072a8 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -114,15 +114,6 @@ export function isBridgeConfigured(config: BridgeConfig): boolean { ); } -function logBridgeDebug( - options: BuildBridgeTransactionOptions | undefined, - label: string, - payload: Record -): void { - if (options?.debug !== true) return; - console.log(`[SDK Bridge] ${label}:`, payload); -} - // TODO(iris-wasm): `isAtom` and `readPair` are adapters for `Noun`'s runtime // shape, not bridge logic. They should live alongside `cue`/`jam` in // `iris-wasm` (ideally replaced by a proper `nounToJs` that returns nested @@ -212,13 +203,6 @@ export async function buildBridgeTransaction( ); } - logBridgeDebug(options, 'Build start', { - destinationAddress: params.destinationAddress, - amountInNicks: params.amountInNicks, - inputCount: params.inputNotes.length, - refundPkh: params.refundPkh, - }); - const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config); const noteData: NoteData = [[config.noteDataKey, bridgeNounJs]]; @@ -234,94 +218,72 @@ export async function buildBridgeTransaction( const builder = new wasm.TxBuilder(options.txEngineSettings); - let remainingGift = BigInt(params.amountInNicks); - - for (let i = 0; i < params.inputNotes.length; i++) { - const note = params.inputNotes[i]; - const spendCondition = params.spendConditions[i]; - if (!spendCondition) { - logBridgeDebug(options, 'Missing spend condition', { - inputIndex: i, - noteAssets: note.assets, - }); - throw new Error('Spend condition is missing for this input note'); - } - const noteAssets = BigInt(note.assets ?? 0); + try { + let remainingGift = BigInt(params.amountInNicks); - const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; - remainingGift -= giftPortion; + for (let i = 0; i < params.inputNotes.length; i++) { + const note = params.inputNotes[i]; + const spendCondition = params.spendConditions[i]; + if (!spendCondition) { + throw new Error('Spend condition is missing for this input note'); + } + const noteAssets = BigInt(note.assets ?? 0); + + const giftPortion = remainingGift < noteAssets ? remainingGift : noteAssets; + remainingGift -= giftPortion; + + let spendBuilder: InstanceType | undefined; + try { + spendBuilder = new wasm.SpendBuilder( + note, + spendCondition as unknown as Lock, + 0, + refundLockRoot + ); + + if (giftPortion > 0n) { + const parentHash = wasm.noteHash(note); + const seed: SeedV1 = { + output_source: null, + lock_root: bridgeLockRoot, + note_data: noteData, + gift: giftPortion.toString() as Nicks, + parent_hash: parentHash, + }; + spendBuilder.seed(seed); + } + + spendBuilder.computeRefund(false); + builder.spend(spendBuilder); + spendBuilder = undefined; + } finally { + spendBuilder?.free(); + } + } - let spendBuilder: InstanceType; - try { - spendBuilder = new wasm.SpendBuilder( - note, - spendCondition as unknown as Lock, - 0, - refundLockRoot + if (remainingGift > 0n) { + const requestedGift = BigInt(params.amountInNicks); + const fundedGift = requestedGift - remainingGift; + throw new Error( + `Insufficient input note assets for bridge amount: requested ${requestedGift.toString()} nicks, funded ${fundedGift.toString()} nicks, shortfall ${remainingGift.toString()} nicks` ); - } catch (error) { - logBridgeDebug(options, 'SpendBuilder creation failed', { - inputIndex: i, - noteAssets: note.assets, - error: error instanceof Error ? error.message : String(error), - }); - throw error; } - if (giftPortion > 0n) { - const parentHash = wasm.noteHash(note); - const seed: SeedV1 = { - output_source: null, - lock_root: bridgeLockRoot, - note_data: noteData, - gift: giftPortion.toString() as Nicks, - parent_hash: parentHash, - }; - spendBuilder.seed(seed); - } + builder.recalcAndSetFee(false); + const feeResult = builder.curFee(); + const transaction = builder.build(); - spendBuilder.computeRefund(false); - builder.spend(spendBuilder); + const txId = transaction.id; + const fee = feeResult; - logBridgeDebug(options, 'Input processed', { - inputIndex: i, - noteAssetsNicks: noteAssets.toString(), - giftPortionNicks: giftPortion.toString(), - remainingGiftNicks: remainingGift.toString(), - }); - } - - if (remainingGift > 0n) { - const requestedGift = BigInt(params.amountInNicks); - const fundedGift = requestedGift - remainingGift; - logBridgeDebug(options, 'Insufficient bridge input assets', { - requestedGiftNicks: requestedGift.toString(), - fundedGiftNicks: fundedGift.toString(), - shortfallNicks: remainingGift.toString(), - }); - throw new Error( - `Insufficient input note assets for bridge amount: requested ${requestedGift.toString()} nicks, funded ${fundedGift.toString()} nicks, shortfall ${remainingGift.toString()} nicks` - ); + return { + transaction, + txId, + fee, + }; + } finally { + builder.free(); } - - builder.recalcAndSetFee(false); - const feeResult = builder.curFee(); - const transaction = builder.build(); - - const txId = transaction.id; - const fee = feeResult; - - logBridgeDebug(options, 'Build complete', { - txId, - feeNicks: fee, - remainingGiftNicks: remainingGift.toString(), - }); - - return { - transaction, - txId, - fee, - }; } /** @@ -339,10 +301,6 @@ export async function validateBridgeTransaction( try { const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); const outputs = wasm.rawTxOutputs(rawTx, 0, options.txEngineSettings); - logBridgeDebug(options, 'Validate start', { - outputCount: outputs.length, - noteDataKey: config.noteDataKey, - }); if (outputs.length === 0) { return { valid: false, error: 'Transaction has no outputs' }; @@ -481,9 +439,6 @@ export async function validateBridgeTransaction( chain: validatedChain, }; } catch (err) { - logBridgeDebug(options, 'Validate failed', { - error: err instanceof Error ? err.message : String(err), - }); return { valid: false, error: `Transaction validation failed: ${err instanceof Error ? err.message : String(err)}`, From 5971b2a89755aaa04cddc8e333be3db30290d305 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 30 Apr 2026 11:41:19 -0400 Subject: [PATCH 64/75] remove debug --- src/bridge.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/bridge.ts b/src/bridge.ts index fa072a8..9b09aa6 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -65,7 +65,6 @@ export function beltsToEvmAddress(belt1: bigint, belt2: bigint, belt3: bigint): return '0x' + address.toString(16).padStart(40, '0'); } - /** Encode a bigint as hex (no 0x prefix). */ export function bigintToAtom(n: bigint): string { if (n === 0n) return '0'; From adfce93d0d738e3d07b8cd313e69443d41345820 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 13 May 2026 07:21:44 -0400 Subject: [PATCH 65/75] fix: set bridge fee display to 0.3%, not 3% --- src/constants.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constants.ts b/src/constants.ts index 855c41b..3e41718 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -88,8 +88,8 @@ export const DEFAULT_COINBASE_TIMELOCK_BLOCKS = 100; /** Zorp bridge (Nockchain → Base) — minimum amount in NOCK (UI / validation guardrail). */ export const MIN_BRIDGE_AMOUNT_NOCK = 100_000; -/** Bridge protocol fee rate used for extension review display (3%). */ -export const BRIDGE_PROTOCOL_FEE_RATE = 0.03; +/** Bridge protocol fee rate used for extension review display (0.3%). */ +export const BRIDGE_PROTOCOL_FEE_RATE = 0.003; /** Zorp bridge 3-of-5 multisig (Nockchain → Base). */ export const ZORP_BRIDGE_THRESHOLD = 3; From 2969ee5390c17db54c07e63356b3799abecafda7 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Fri, 22 May 2026 07:42:45 -0400 Subject: [PATCH 66/75] fix: add guard to the transaction --- src/migration.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/migration.ts b/src/migration.ts index 7f3ad59..3aa299c 100644 --- a/src/migration.ts +++ b/src/migration.ts @@ -229,6 +229,9 @@ export async function buildV0MigrationTx( const feeNicks = builder.curFee(); const transaction = builder.build(); const rawTx: RawTxV1 = wasm.nockchainTxToRawTx(transaction); + if (!guard.isRawTxV1(rawTx)) { + throw new Error('Built v0 migration transaction contains a spend without seeds'); + } const inputNotes = notesToUse; const feeNock = Number(BigInt(feeNicks)) / NOCK_TO_NICKS; From 15076b5e52bbbe9eb14b5f36f6e2caf334ceb5ba Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Fri, 22 May 2026 07:43:19 -0400 Subject: [PATCH 67/75] fix: added more defensive validation of the bridge transaction flow --- src/bridge-types.ts | 8 +++ src/bridge.ts | 126 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 133 insertions(+), 1 deletion(-) diff --git a/src/bridge-types.ts b/src/bridge-types.ts index 9e541e6..277c881 100644 --- a/src/bridge-types.ts +++ b/src/bridge-types.ts @@ -58,6 +58,12 @@ export interface BuildBridgeTransactionOptions { txEngineSettings: TxEngineSettings; } +/** Intent a built bridge transaction must match (same fields used to construct it). */ +export type BridgeValidationParams = Pick< + BridgeTransactionParams, + 'destinationAddress' | 'amountInNicks' | 'refundPkh' +>; + /** * Result of building a bridge transaction (unsigned). */ @@ -88,4 +94,6 @@ export interface BridgeValidationResult { version?: string; /** Chain identifier */ chain?: string; + /** Lock root digest for the bridge output seed */ + bridgeLockRoot?: string; } diff --git a/src/bridge.ts b/src/bridge.ts index 9b09aa6..5312db8 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -8,6 +8,7 @@ import type { BridgeConfig, BridgeTransactionParams, BridgeTransactionResult, + BridgeValidationParams, BridgeValidationResult, BuildBridgeTransactionOptions, } from './bridge-types.js'; @@ -22,6 +23,7 @@ import type { PbCom2Note, PbCom2NoteDataEntry, PbCom2RawTransaction, + RawTxV1, SeedV1, SpendCondition, } from '@nockbox/iris-wasm/iris_wasm.js'; @@ -166,6 +168,54 @@ function parseDigestString(value: string, field: string): Digest { return trimmed as Digest; } +function normalizeEvmAddress(address: string): string { + const trimmed = address.trim(); + const withPrefix = trimmed.startsWith('0x') ? trimmed : `0x${trimmed}`; + return withPrefix.toLowerCase(); +} + +function noteDataHasKey(noteData: NoteData | undefined, key: string): boolean { + if (!Array.isArray(noteData)) return false; + return noteData.some(entry => Array.isArray(entry) && entry[0] === key); +} + +function digestFromLockRoot(lockRoot: LockRoot): string { + if (typeof lockRoot === 'string') { + return lockRoot; + } + return wasm.lockRootHash(lockRoot); +} + +function collectBridgeSeeds(rawTx: RawTxV1, noteDataKey: string): SeedV1[] { + const bridgeSeeds: SeedV1[] = []; + for (const [, spend] of rawTx.spends) { + for (const seed of spend.seeds) { + if (noteDataHasKey(seed.note_data, noteDataKey)) { + bridgeSeeds.push(seed); + } + } + } + return bridgeSeeds; +} + +/** + * Derive the bridge multisig lock root from config (same path as buildBridgeTransaction). + */ +export function computeBridgeLockRoot(config: BridgeConfig): string { + const bridgePkh = wasm.pkhNew( + BigInt(config.threshold), + config.addresses.map(address => parseDigestString(address, 'bridge address')) + ); + const bridgeSpendCondition = wasm.spendConditionNewPkh(bridgePkh); + return wasm.lockHash(bridgeSpendCondition); +} + +function computeRefundLockRoot(refundPkh: string): string { + const refundPkhObj = wasm.pkhSingle(parseDigestString(refundPkh, 'refund pkh')); + const refundSpendCondition = wasm.spendConditionNewPkh(refundPkhObj); + return wasm.lockHash(refundSpendCondition); +} + /** * Create jammed bridge note data for an EVM address (requires WASM). * Caller must have initialized WASM (e.g. await wasm.default()) before using. @@ -291,14 +341,61 @@ export async function buildBridgeTransaction( */ export async function validateBridgeTransaction( rawTxProto: unknown, + params: BridgeValidationParams, config: BridgeConfig, options: BuildBridgeTransactionOptions ): Promise { if (!options?.txEngineSettings) { throw new Error('txEngineSettings is required in options (see BuildBridgeTransactionOptions)'); } + if (!isEvmAddress(params.destinationAddress)) { + return { valid: false, error: `Invalid destination address: ${params.destinationAddress}` }; + } try { const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); + if (!('version' in rawTx) || rawTx.version !== 1) { + return { valid: false, error: 'Bridge transaction must be version 1' }; + } + const rawTxV1 = rawTx as RawTxV1; + + const bridgeLockRoot = computeBridgeLockRoot(config); + if (config.expectedLockRoot && config.expectedLockRoot !== bridgeLockRoot) { + return { + valid: false, + error: 'Bridge configuration lock root does not match configured bridge signer set', + }; + } + const refundLockRoot = computeRefundLockRoot(params.refundPkh); + + const bridgeSeeds = collectBridgeSeeds(rawTxV1, config.noteDataKey); + if (bridgeSeeds.length === 0) { + return { + valid: false, + error: `No bridge seed with '${config.noteDataKey}' note data found in transaction spends`, + }; + } + + let bridgeSeedGiftTotal = 0n; + for (const [, spend] of rawTxV1.spends) { + for (const seed of spend.seeds) { + const seedLockRoot = digestFromLockRoot(seed.lock_root); + if (noteDataHasKey(seed.note_data, config.noteDataKey)) { + if (seedLockRoot !== bridgeLockRoot) { + return { + valid: false, + error: `Bridge seed lock root mismatch: expected ${bridgeLockRoot}, got ${seedLockRoot}`, + }; + } + bridgeSeedGiftTotal += BigInt(seed.gift ?? 0); + } else if (seedLockRoot !== refundLockRoot) { + return { + valid: false, + error: `Refund seed lock root mismatch: expected ${refundLockRoot}, got ${seedLockRoot}`, + }; + } + } + } + const outputs = wasm.rawTxOutputs(rawTx, 0, options.txEngineSettings); if (outputs.length === 0) { @@ -340,6 +437,21 @@ export async function validateBridgeTransaction( }; } + if (bridgeSeedGiftTotal !== BigInt(bridgeOutput.assets)) { + return { + valid: false, + error: `Bridge seed gifts (${bridgeSeedGiftTotal} nicks) do not match bridge output (${bridgeOutput.assets} nicks)`, + }; + } + + const expectedAmount = BigInt(params.amountInNicks); + if (BigInt(bridgeOutput.assets) !== expectedAmount) { + return { + valid: false, + error: `Bridge amount mismatch: expected ${expectedAmount} nicks, got ${bridgeOutput.assets} nicks`, + }; + } + const bridgeEntry = bridgeOutput.entries.find(e => e.key === config.noteDataKey); if (!bridgeEntry) { return { @@ -419,6 +531,15 @@ export async function validateBridgeTransaction( error: `Reconstructed address is invalid: ${destinationAddress}`, }; } + + const expectedDestination = normalizeEvmAddress(params.destinationAddress); + const actualDestination = normalizeEvmAddress(destinationAddress); + if (expectedDestination !== actualDestination) { + return { + valid: false, + error: `Destination address mismatch: expected ${expectedDestination}, got ${actualDestination}`, + }; + } } catch (err) { return { valid: false, @@ -436,6 +557,7 @@ export async function validateBridgeTransaction( noteDataKey: validatedNoteDataKey, version: validatedVersion, chain: validatedChain, + bridgeLockRoot, }; } catch (err) { return { @@ -451,10 +573,11 @@ export async function validateBridgeTransaction( export async function assertValidBridgeTransaction( rawTxProto: unknown, context: 'pre-signing' | 'post-signing', + params: BridgeValidationParams, config: BridgeConfig, options: BuildBridgeTransactionOptions ): Promise { - const result = await validateBridgeTransaction(rawTxProto, config, options); + const result = await validateBridgeTransaction(rawTxProto, params, config, options); if (!result.valid) { throw new Error(`${context} validation failed: ${result.error}`); } @@ -466,6 +589,7 @@ export type { BridgeConfig, BridgeTransactionParams, BridgeTransactionResult, + BridgeValidationParams, BridgeValidationResult, BuildBridgeTransactionOptions, TxEngineSettings, From 6876994aa5d37ff35d7793b333a8fee87bbff839 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Mon, 25 May 2026 13:59:17 -0400 Subject: [PATCH 68/75] chore: bump lock and package --- package-lock.json | 9 +++------ package.json | 6 +++--- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/package-lock.json b/package-lock.json index a51202c..1e2253f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0-alpha.4", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.10", + "@nockbox/iris-wasm": "file:../iris-rs/crates/iris-wasm/pkg", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -22,7 +22,6 @@ "../iris-rs/crates/iris-wasm/pkg": { "name": "@nockbox/iris-wasm", "version": "0.2.0-alpha.3", - "extraneous": true, "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { @@ -417,10 +416,8 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.10", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.10.tgz", - "integrity": "sha512-/CuHKz1Q2In/CdAQfUn1LgyOvFbc5mVkH94ko8gOxV/HU81C1Ctg2XMRP5lX2FoRR3OonEbBC4InUtzOPkWJfw==", - "license": "MIT" + "resolved": "../iris-rs/crates/iris-wasm/pkg", + "link": true }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.53.3", diff --git a/package.json b/package.json index 8e4a29d..535557a 100644 --- a/package.json +++ b/package.json @@ -41,16 +41,16 @@ "license": "MIT", "devDependencies": { "@types/node": "^22.0.0", + "prettier": "^3.4.2", "typescript": "^5.5.3", - "vite": "^5.0.0", - "prettier": "^3.4.2" + "vite": "^5.0.0" }, "repository": { "type": "git", "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "^0.2.0-alpha.10", + "@nockbox/iris-wasm": "file:../iris-rs/crates/iris-wasm/pkg", "@scure/base": "^2.0.0" } } From 60443b040ab7b3744a5f755cfd61cdd4d0bd6673 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 27 May 2026 07:29:42 -0400 Subject: [PATCH 69/75] chore: push package lock --- package-lock.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package-lock.json b/package-lock.json index 1e2253f..c3cac43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,7 +21,7 @@ }, "../iris-rs/crates/iris-wasm/pkg": { "name": "@nockbox/iris-wasm", - "version": "0.2.0-alpha.3", + "version": "0.2.0-alpha.10", "license": "MIT" }, "node_modules/@esbuild/aix-ppc64": { From 84b7a6beec435644383d11b95096160c6bb8a689 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 27 May 2026 07:38:19 -0400 Subject: [PATCH 70/75] chore: update package lock --- package-lock.json | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package-lock.json b/package-lock.json index c3cac43..9e3b020 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.2.0-alpha.4", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "file:../iris-rs/crates/iris-wasm/pkg", + "@nockbox/iris-wasm": "0.2.0-alpha.10", "@scure/base": "^2.0.0" }, "devDependencies": { diff --git a/package.json b/package.json index 535557a..f3a740e 100644 --- a/package.json +++ b/package.json @@ -50,7 +50,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "file:../iris-rs/crates/iris-wasm/pkg", + "@nockbox/iris-wasm": "0.2.0-alpha.10", "@scure/base": "^2.0.0" } } From 2d81ca022a9d0dffd753967365d22c9d86ab737b Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 27 May 2026 07:50:15 -0400 Subject: [PATCH 71/75] fix: resolve iris-wasm from npm registry in lockfile The lockfile still pointed @nockbox/iris-wasm at the monorepo file path, so git installs failed during prepare when that path did not exist. --- package-lock.json | 276 ++++++++++++++++++++++++++-------------------- 1 file changed, 156 insertions(+), 120 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9e3b020..f33bcd7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,11 +19,6 @@ "vite": "^5.0.0" } }, - "../iris-rs/crates/iris-wasm/pkg": { - "name": "@nockbox/iris-wasm", - "version": "0.2.0-alpha.10", - "license": "MIT" - }, "node_modules/@esbuild/aix-ppc64": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", @@ -416,13 +411,15 @@ } }, "node_modules/@nockbox/iris-wasm": { - "resolved": "../iris-rs/crates/iris-wasm/pkg", - "link": true + "version": "0.2.0-alpha.10", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.10.tgz", + "integrity": "sha512-/CuHKz1Q2In/CdAQfUn1LgyOvFbc5mVkH94ko8gOxV/HU81C1Ctg2XMRP5lX2FoRR3OonEbBC4InUtzOPkWJfw==", + "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.53.3.tgz", - "integrity": "sha512-mRSi+4cBjrRLoaal2PnqH82Wqyb+d3HsPUN/W+WslCXsZsyHa9ZeQQX/pQsZaVIWDkPcpV6jJ+3KLbTbgnwv8w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.4.tgz", + "integrity": "sha512-F5QXMSiFebS9hKZj02XhWLLnRpJ3B3AROP0tWbFBSj+6kCbg5m9j5JoHKd4mmSVy5mS/IMQloYgYxCuJC0fxEQ==", "cpu": [ "arm" ], @@ -434,9 +431,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.53.3.tgz", - "integrity": "sha512-CbDGaMpdE9sh7sCmTrTUyllhrg65t6SwhjlMJsLr+J8YjFuPmCEjbBSx4Z/e4SmDyH3aB5hGaJUP2ltV/vcs4w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.4.tgz", + "integrity": "sha512-GxxTKApUpzRhof7poWvCJHRF51C67u1R7D6DiluBE8wKU1u5GWE8t+v81JvJYtbawoBFX1hLv5Ei4eVjkWokaw==", "cpu": [ "arm64" ], @@ -448,9 +445,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.53.3.tgz", - "integrity": "sha512-Nr7SlQeqIBpOV6BHHGZgYBuSdanCXuw09hon14MGOLGmXAFYjx1wNvquVPmpZnl0tLjg25dEdr4IQ6GgyToCUA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.4.tgz", + "integrity": "sha512-tua0TaJxMOB1R0V0RS1jFZ/RpURFDJIOR2A6jWwQeawuFyS4gBW+rntLRaQd0EQ4bd6Vp44Z2rXW+YYDBsj6IA==", "cpu": [ "arm64" ], @@ -462,9 +459,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.53.3.tgz", - "integrity": "sha512-DZ8N4CSNfl965CmPktJ8oBnfYr3F8dTTNBQkRlffnUarJ2ohudQD17sZBa097J8xhQ26AwhHJ5mvUyQW8ddTsQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.4.tgz", + "integrity": "sha512-CSKq7MsP+5PFIcydhAiR1K0UhEI1A2jWXVKHPCBZ151yOutENwvnPocgVHkivu2kviURtCEB6zUQw0vs8RrhMg==", "cpu": [ "x64" ], @@ -476,9 +473,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.53.3.tgz", - "integrity": "sha512-yMTrCrK92aGyi7GuDNtGn2sNW+Gdb4vErx4t3Gv/Tr+1zRb8ax4z8GWVRfr3Jw8zJWvpGHNpss3vVlbF58DZ4w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.4.tgz", + "integrity": "sha512-+O8OkVdyvXMtJEciu2wS/pzm1IxntEEQx3z5TAVy4l32G0etZn+RsA48ARRrFm6Ri8fvqPQfgrvNxSjKAbnd3g==", "cpu": [ "arm64" ], @@ -490,9 +487,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.53.3.tgz", - "integrity": "sha512-lMfF8X7QhdQzseM6XaX0vbno2m3hlyZFhwcndRMw8fbAGUGL3WFMBdK0hbUBIUYcEcMhVLr1SIamDeuLBnXS+Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.4.tgz", + "integrity": "sha512-Iw3oMskH3AfNuhU0MSN7vNbdi4me/NiYo2azqPz/Le16zHSa+3RRmliCMWWQmh4lcndccU40xcJuTYJZxNo/lw==", "cpu": [ "x64" ], @@ -504,9 +501,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.53.3.tgz", - "integrity": "sha512-k9oD15soC/Ln6d2Wv/JOFPzZXIAIFLp6B+i14KhxAfnq76ajt0EhYc5YPeX6W1xJkAdItcVT+JhKl1QZh44/qw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.4.tgz", + "integrity": "sha512-EIPRXTVQpHyF8WOo219AD2yEltPehLTcTMz2fn6JsatLYSzQf00hj3rulF+yauOlF9/FtM2WpkT/hJh/KJFGhA==", "cpu": [ "arm" ], @@ -518,9 +515,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.53.3.tgz", - "integrity": "sha512-vTNlKq+N6CK/8UktsrFuc+/7NlEYVxgaEgRXVUVK258Z5ymho29skzW1sutgYjqNnquGwVUObAaxae8rZ6YMhg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.4.tgz", + "integrity": "sha512-J3Yh9PzzF1Ovah2At+lHiGQdsYgArxBbXv/zHfSyaiFQEqvNv7DcW98pCrmdjCZBrqBiKrKKe2V+aaSGWuBe/w==", "cpu": [ "arm" ], @@ -532,9 +529,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.53.3.tgz", - "integrity": "sha512-RGrFLWgMhSxRs/EWJMIFM1O5Mzuz3Xy3/mnxJp/5cVhZ2XoCAxJnmNsEyeMJtpK+wu0FJFWz+QF4mjCA7AUQ3w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.4.tgz", + "integrity": "sha512-BFDEZMYfUvLn37ONE1yMBojPxnMlTFsdyNoqncT0qFq1mAfllL+ATMMJd8TeuVMiX84s1KbcxcZbXInmcO2mRg==", "cpu": [ "arm64" ], @@ -546,9 +543,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.53.3.tgz", - "integrity": "sha512-kASyvfBEWYPEwe0Qv4nfu6pNkITLTb32p4yTgzFCocHnJLAHs+9LjUu9ONIhvfT/5lv4YS5muBHyuV84epBo/A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.4.tgz", + "integrity": "sha512-pc9EYOSlOgdQ2uPl1o9PF6/kLSgaUosia7gOuS8mB69IxJvlclko1MECXysjs5ryez1/5zjYqx3+xYU0TU6R1A==", "cpu": [ "arm64" ], @@ -560,9 +557,23 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.53.3.tgz", - "integrity": "sha512-JiuKcp2teLJwQ7vkJ95EwESWkNRFJD7TQgYmCnrPtlu50b4XvT5MOmurWNrCj3IFdyjBQ5p9vnrX4JM6I8OE7g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.4.tgz", + "integrity": "sha512-NxnomyxYerDh5n4iLrNa+sH+Z+U4BMEE46V2PgQ/hoB909i8gV1M5wPojWg9fk1jWpO3IQnOs20K4wyZuFLEFQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.4.tgz", + "integrity": "sha512-nbJnQ8a3z1mtmrwImCYhc6BGpThAyYVRQxw9uKSKG4wR6aAYno9sVjJ0zaZcW9BPJX1GbrDPf+SvdWjgTuDmnw==", "cpu": [ "loong64" ], @@ -574,9 +585,23 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.53.3.tgz", - "integrity": "sha512-EoGSa8nd6d3T7zLuqdojxC20oBfNT8nexBbB/rkxgKj5T5vhpAQKKnD+h3UkoMuTyXkP5jTjK/ccNRmQrPNDuw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.4.tgz", + "integrity": "sha512-2EU6acNrQLd8tYvo/LXW535wupT3m6fo7HKo6lr7ktQoItxTyOL1ZCR/GfGCuXl2vR+zmfI6eRXkSemafv+iVg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.4.tgz", + "integrity": "sha512-WeBtoMuaMxiiIrO2IYP3xs6GMWkJP2C0EoT8beTLkUPmzV1i/UcOSVw1d5r9KBODtHKilG5yFxsGRnBbK3wJ4A==", "cpu": [ "ppc64" ], @@ -588,9 +613,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.53.3.tgz", - "integrity": "sha512-4s+Wped2IHXHPnAEbIB0YWBv7SDohqxobiiPA1FIWZpX+w9o2i4LezzH/NkFUl8LRci/8udci6cLq+jJQlh+0g==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.4.tgz", + "integrity": "sha512-FJHFfqpKUI3A10WrWKiFbBZ7yVbGT4q4B5o1qKFFojqpaYoh9LrQgqWCmmcxQzVSXYtyB5bzkXrYzlHTs21MYA==", "cpu": [ "riscv64" ], @@ -602,9 +627,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.53.3.tgz", - "integrity": "sha512-68k2g7+0vs2u9CxDt5ktXTngsxOQkSEV/xBbwlqYcUrAVh6P9EgMZvFsnHy4SEiUl46Xf0IObWVbMvPrr2gw8A==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.4.tgz", + "integrity": "sha512-mcEl6CUT5IAUmQf1m9FYSmVqCJlpQ8r8eyftFUHG8i9OhY7BkBXSUdnLH5DOf0wCOjcP9v/QO93zpmF1SptCCw==", "cpu": [ "riscv64" ], @@ -616,9 +641,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.53.3.tgz", - "integrity": "sha512-VYsFMpULAz87ZW6BVYw3I6sWesGpsP9OPcyKe8ofdg9LHxSbRMd7zrVrr5xi/3kMZtpWL/wC+UIJWJYVX5uTKg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.4.tgz", + "integrity": "sha512-ynt3JxVd2w2buzoKDWIyiV1pJW93xlQic1THVLXilz429oijRpSHivZAgp65KBu+cMcgf1eVVjdnTLvPxgCuoQ==", "cpu": [ "s390x" ], @@ -630,9 +655,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz", - "integrity": "sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.4.tgz", + "integrity": "sha512-Boiz5+MsaROEWDf+GGEwF8VMHGhlUoQMtIPjOgA5fv4osupqTVnJteQNKJwUcnUog2G55jYXH7KZFFiJe0TEzQ==", "cpu": [ "x64" ], @@ -644,9 +669,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.53.3.tgz", - "integrity": "sha512-eoROhjcc6HbZCJr+tvVT8X4fW3/5g/WkGvvmwz/88sDtSJzO7r/blvoBDgISDiCjDRZmHpwud7h+6Q9JxFwq1Q==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.4.tgz", + "integrity": "sha512-+qfSY27qIrFfI/Hom04KYFw3GKZSGU4lXus51wsb5EuySfFlWRwjkKWoE9emgRw/ukoT4Udsj4W/+xxG8VbPKg==", "cpu": [ "x64" ], @@ -657,10 +682,24 @@ "linux" ] }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.4.tgz", + "integrity": "sha512-VpTfOPHgVXEBeeR8hZ2O0F3aSso+JDWqTWmTmzcQKted54IAdUVbxE+j/MVxUsKa8L20HJhv3vUezVPoquqWjA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.53.3.tgz", - "integrity": "sha512-OueLAWgrNSPGAdUdIjSWXw+u/02BRTcnfw9PN41D2vq/JSEPnJnVuBgw18VkN8wcd4fjUs+jFHVM4t9+kBSNLw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.4.tgz", + "integrity": "sha512-IPOsh5aRYuLv/nkU51X10Bf75Bsf6+gZdx1X+QP5QM6lIJFHHqbHLG0uJn/hWthzo13UAc2umiUorqZy3axoZg==", "cpu": [ "arm64" ], @@ -672,9 +711,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.53.3.tgz", - "integrity": "sha512-GOFuKpsxR/whszbF/bzydebLiXIHSgsEUp6M0JI8dWvi+fFa1TD6YQa4aSZHtpmh2/uAlj/Dy+nmby3TJ3pkTw==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.4.tgz", + "integrity": "sha512-4QzE9E81OohJ/HKzHhsqU+zcYYojVOXlFMs1DdyMT6qXl/niOH7AVElmmEdUNHHS/oRkc++d5k6Vy85zFs0DEw==", "cpu": [ "arm64" ], @@ -686,9 +725,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.53.3.tgz", - "integrity": "sha512-iah+THLcBJdpfZ1TstDFbKNznlzoxa8fmnFYK4V67HvmuNYkVdAywJSoteUszvBQ9/HqN2+9AZghbajMsFT+oA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.4.tgz", + "integrity": "sha512-zTPgT1YuHHcd+Tmx7h8aml0FWFVelV5N54oHow9SLj+GfoDy/huQ+UV396N/C7KpMDMiPspRktzM1/0r1usYEA==", "cpu": [ "ia32" ], @@ -700,9 +739,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.53.3.tgz", - "integrity": "sha512-J9QDiOIZlZLdcot5NXEepDkstocktoVjkaKUtqzgzpt2yWjGlbYiKyp05rWwk4nypbYUNoFAztEgixoLaSETkg==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.4.tgz", + "integrity": "sha512-DRS4G7mi9lJxqEDezIkKCaUIKCrLUUDCUaCsTPCi/rtqaC6D/jjwslMQyiDU50Ka0JKpeXeRBFBAXwArY52vBw==", "cpu": [ "x64" ], @@ -714,9 +753,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.53.3.tgz", - "integrity": "sha512-UhTd8u31dXadv0MopwGgNOBpUVROFKWVQgAg5N1ESyCz8AuBcMqm4AuTjrwgQKGDfoFuz02EuMRHQIw/frmYKQ==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.4.tgz", + "integrity": "sha512-QVTUovf40zgTqlFVrKA1uXMVvU2QWEFWfAH8Wdc48IxLvrJMQVMBRjuQyUpzZCDkakImib9eVazbWlC6ksWtJw==", "cpu": [ "x64" ], @@ -728,9 +767,9 @@ ] }, "node_modules/@scure/base": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.0.0.tgz", - "integrity": "sha512-3E1kpuZginKkek01ovG8krQ0Z44E3DHPjc5S2rjJw9lZn3KSQOs8S7wqikF/AH7iRanHypj85uGyxk0XAyC37w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@scure/base/-/base-2.2.0.tgz", + "integrity": "sha512-b8XEupJibegiXV+tDUseI8oLQc8ei3d/4Jkb2RpbHh3MfE054ov3uIz2dhFkB3FI8iwYkEh0gGCApkrYggkPNg==", "license": "MIT", "funding": { "url": "https://paulmillr.com/funding/" @@ -744,9 +783,9 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "22.19.11", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.11.tgz", - "integrity": "sha512-BH7YwL6rA93ReqeQS1c4bsPpcfOmJasG+Fkr6Y59q83f9M1WcBRHR2vM+P9eOisYRcN3ujQoiZY8uk5W+1WL8w==", + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", "dev": true, "license": "MIT", "dependencies": { @@ -808,9 +847,9 @@ } }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", "dev": true, "funding": [ { @@ -834,9 +873,9 @@ "license": "ISC" }, "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", "dev": true, "funding": [ { @@ -854,7 +893,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -863,9 +902,9 @@ } }, "node_modules/prettier": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", - "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.8.3.tgz", + "integrity": "sha512-7igPTM53cGHMW8xWuVTydi2KO233VFiTNyF5hLJqpilHfmn8C8gPf+PS7dUT64YcXFbiMGZxS9pCSxL/Dxm/Jw==", "dev": true, "license": "MIT", "bin": { @@ -879,9 +918,9 @@ } }, "node_modules/rollup": { - "version": "4.53.3", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.53.3.tgz", - "integrity": "sha512-w8GmOxZfBmKknvdXU1sdM9NHcoQejwF/4mNgj2JuEEdRaHwwF12K7e9eXn1nLZ07ad+du76mkVsyeb2rKGllsA==", + "version": "4.60.4", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.4.tgz", + "integrity": "sha512-WHeFSbZYsPu3+bLoNRUuAO+wavNlocOPf3wSHTP7hcFKVnJeWsYlCDbr3mTS14FCizf9ccIxXA8sGL8zKeQN3g==", "dev": true, "license": "MIT", "dependencies": { @@ -895,28 +934,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.53.3", - "@rollup/rollup-android-arm64": "4.53.3", - "@rollup/rollup-darwin-arm64": "4.53.3", - "@rollup/rollup-darwin-x64": "4.53.3", - "@rollup/rollup-freebsd-arm64": "4.53.3", - "@rollup/rollup-freebsd-x64": "4.53.3", - "@rollup/rollup-linux-arm-gnueabihf": "4.53.3", - "@rollup/rollup-linux-arm-musleabihf": "4.53.3", - "@rollup/rollup-linux-arm64-gnu": "4.53.3", - "@rollup/rollup-linux-arm64-musl": "4.53.3", - "@rollup/rollup-linux-loong64-gnu": "4.53.3", - "@rollup/rollup-linux-ppc64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-gnu": "4.53.3", - "@rollup/rollup-linux-riscv64-musl": "4.53.3", - "@rollup/rollup-linux-s390x-gnu": "4.53.3", - "@rollup/rollup-linux-x64-gnu": "4.53.3", - "@rollup/rollup-linux-x64-musl": "4.53.3", - "@rollup/rollup-openharmony-arm64": "4.53.3", - "@rollup/rollup-win32-arm64-msvc": "4.53.3", - "@rollup/rollup-win32-ia32-msvc": "4.53.3", - "@rollup/rollup-win32-x64-gnu": "4.53.3", - "@rollup/rollup-win32-x64-msvc": "4.53.3", + "@rollup/rollup-android-arm-eabi": "4.60.4", + "@rollup/rollup-android-arm64": "4.60.4", + "@rollup/rollup-darwin-arm64": "4.60.4", + "@rollup/rollup-darwin-x64": "4.60.4", + "@rollup/rollup-freebsd-arm64": "4.60.4", + "@rollup/rollup-freebsd-x64": "4.60.4", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.4", + "@rollup/rollup-linux-arm-musleabihf": "4.60.4", + "@rollup/rollup-linux-arm64-gnu": "4.60.4", + "@rollup/rollup-linux-arm64-musl": "4.60.4", + "@rollup/rollup-linux-loong64-gnu": "4.60.4", + "@rollup/rollup-linux-loong64-musl": "4.60.4", + "@rollup/rollup-linux-ppc64-gnu": "4.60.4", + "@rollup/rollup-linux-ppc64-musl": "4.60.4", + "@rollup/rollup-linux-riscv64-gnu": "4.60.4", + "@rollup/rollup-linux-riscv64-musl": "4.60.4", + "@rollup/rollup-linux-s390x-gnu": "4.60.4", + "@rollup/rollup-linux-x64-gnu": "4.60.4", + "@rollup/rollup-linux-x64-musl": "4.60.4", + "@rollup/rollup-openbsd-x64": "4.60.4", + "@rollup/rollup-openharmony-arm64": "4.60.4", + "@rollup/rollup-win32-arm64-msvc": "4.60.4", + "@rollup/rollup-win32-ia32-msvc": "4.60.4", + "@rollup/rollup-win32-x64-gnu": "4.60.4", + "@rollup/rollup-win32-x64-msvc": "4.60.4", "fsevents": "~2.3.2" } }, @@ -1010,12 +1052,6 @@ "optional": true } } - }, - "vendor/iris-wasm": { - "name": "@nockbox/iris-wasm", - "version": "0.2.0-alpha.3", - "extraneous": true, - "license": "MIT" } } } From e4dd7df28e2d065c6ec25a4a3aecd2c401a8efb7 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Tue, 2 Jun 2026 05:50:19 -0400 Subject: [PATCH 72/75] fix: switch bridge validation signature to be properly typed (rawTx instead of casting to unknown --- src/bridge.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index 5312db8..3657f78 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -22,7 +22,6 @@ import type { Noun, PbCom2Note, PbCom2NoteDataEntry, - PbCom2RawTransaction, RawTxV1, SeedV1, SpendCondition, @@ -340,7 +339,7 @@ export async function buildBridgeTransaction( * Uses config for note key, min amount, and optional lock root. */ export async function validateBridgeTransaction( - rawTxProto: unknown, + rawTx: RawTxV1, params: BridgeValidationParams, config: BridgeConfig, options: BuildBridgeTransactionOptions @@ -352,12 +351,6 @@ export async function validateBridgeTransaction( return { valid: false, error: `Invalid destination address: ${params.destinationAddress}` }; } try { - const rawTx = wasm.rawTxFromProtobuf(rawTxProto as PbCom2RawTransaction); - if (!('version' in rawTx) || rawTx.version !== 1) { - return { valid: false, error: 'Bridge transaction must be version 1' }; - } - const rawTxV1 = rawTx as RawTxV1; - const bridgeLockRoot = computeBridgeLockRoot(config); if (config.expectedLockRoot && config.expectedLockRoot !== bridgeLockRoot) { return { @@ -367,7 +360,7 @@ export async function validateBridgeTransaction( } const refundLockRoot = computeRefundLockRoot(params.refundPkh); - const bridgeSeeds = collectBridgeSeeds(rawTxV1, config.noteDataKey); + const bridgeSeeds = collectBridgeSeeds(rawTx, config.noteDataKey); if (bridgeSeeds.length === 0) { return { valid: false, @@ -376,7 +369,7 @@ export async function validateBridgeTransaction( } let bridgeSeedGiftTotal = 0n; - for (const [, spend] of rawTxV1.spends) { + for (const [, spend] of rawTx.spends) { for (const seed of spend.seeds) { const seedLockRoot = digestFromLockRoot(seed.lock_root); if (noteDataHasKey(seed.note_data, config.noteDataKey)) { @@ -571,13 +564,13 @@ export async function validateBridgeTransaction( * Validate and throw if invalid (convenience wrapper). */ export async function assertValidBridgeTransaction( - rawTxProto: unknown, + rawTx: RawTxV1, context: 'pre-signing' | 'post-signing', params: BridgeValidationParams, config: BridgeConfig, options: BuildBridgeTransactionOptions ): Promise { - const result = await validateBridgeTransaction(rawTxProto, params, config, options); + const result = await validateBridgeTransaction(rawTx, params, config, options); if (!result.valid) { throw new Error(`${context} validation failed: ${result.error}`); } From e164480a63529aedfb5b2330d0bb28b6f001a7c9 Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Wed, 3 Jun 2026 07:17:53 -0400 Subject: [PATCH 73/75] fix: use atomtobelts/beltstoatom conversion for bridge note data encoding --- src/bridge.ts | 84 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 57 insertions(+), 27 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index 3657f78..ed6dd8a 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -1,6 +1,6 @@ /** * Bridge utilities for Nockchain ↔ EVM bridging. - * Encoding uses the Goldilocks prime field (3 belts) for EVM addresses. + * Encoding uses the wasm atom/belt conversion helpers for EVM addresses. * Consumers provide BridgeConfig; the SDK handles transaction construction and validation. */ @@ -29,9 +29,6 @@ import type { import { base58 } from '@scure/base'; import * as wasm from './wasm.js'; -// Goldilocks prime: 2^64 - 2^32 + 1 -export const GOLDILOCKS_PRIME = 2n ** 64n - 2n ** 32n + 1n; - /** Simple EVM address check (0x + 40 hex chars). No checksum validation. */ export function isEvmAddress(address: string): boolean { const s = (address || '').trim(); @@ -39,31 +36,61 @@ export function isEvmAddress(address: string): boolean { return /^0x[0-9a-fA-F]{40}$/.test(normalized); } +function evmAddressToAtom(address: string): string { + return normalizeEvmAddress(address).slice(2).replace(/^0+/, '') || '0'; +} + +function atomToEvmAddress(atom: string): string { + return `0x${atom.padStart(40, '0')}`; +} + +function atomToBigint(atom: string): bigint { + return BigInt(`0x${atom}`); +} + +function nounArray(head: Noun, ...tail: Noun[]): Noun { + const noun: [Noun] = [head]; + noun.push(...tail); + return noun; +} + +function beltTupleToNoun(belt1: bigint, belt2: bigint, belt3: bigint): Noun { + return nounArray(bigintToAtom(belt1), bigintToAtom(belt2), bigintToAtom(belt3)); +} + +function beltNounToTuple(noun: Noun): [bigint, bigint, bigint] { + const atoms = Array.isArray(noun) ? Array.from(noun) : null; + if (!Array.isArray(atoms) || atoms.length > 3 || !atoms.every(isAtom)) { + throw new Error('Invalid EVM address belt encoding: expected at most 3 belt atoms'); + } + + return [ + atomToBigint(atoms[0] ?? '0'), + atomToBigint(atoms[1] ?? '0'), + atomToBigint(atoms[2] ?? '0'), + ]; +} + /** - * Convert an EVM address to 3 belts (Goldilocks field elements). + * Convert an EVM address to 3 belts through the canonical atom encoding. */ export function evmAddressToBelts(address: string): [bigint, bigint, bigint] { if (!isEvmAddress(address)) { throw new Error(`Invalid EVM address: ${address}`); } - const normalized = address.startsWith('0x') ? address : `0x${address}`; - const addr = BigInt(normalized); - - const belt1 = addr % GOLDILOCKS_PRIME; - const q1 = addr / GOLDILOCKS_PRIME; - const belt2 = q1 % GOLDILOCKS_PRIME; - const belt3 = q1 / GOLDILOCKS_PRIME; - return [belt1, belt2, belt3]; + return beltNounToTuple(wasm.atomToBelts(evmAddressToAtom(address))); } /** * Convert 3 belts back to an EVM address. */ export function beltsToEvmAddress(belt1: bigint, belt2: bigint, belt3: bigint): string { - const p = GOLDILOCKS_PRIME; - const address = belt1 + belt2 * p + belt3 * p * p; - return '0x' + address.toString(16).padStart(40, '0'); + const atom = wasm.beltsToAtom(beltTupleToNoun(belt1, belt2, belt3)); + if (!isAtom(atom)) { + throw new Error('Invalid EVM address belt encoding: decoded atom is not an atom'); + } + return atomToEvmAddress(atom); } /** Encode a bigint as hex (no 0x prefix). */ @@ -84,10 +111,13 @@ export function buildBridgeNoun( config: Pick ): Noun { const [belt1, belt2, belt3] = evmAddressToBelts(evmAddress); - return [ + return nounArray( config.versionTag, - [config.chainTag, [bigintToAtom(belt1), [bigintToAtom(belt2), bigintToAtom(belt3)]]], - ] as unknown as Noun; + nounArray( + config.chainTag, + nounArray(bigintToAtom(belt1), nounArray(bigintToAtom(belt2), bigintToAtom(belt3))) + ) + ); } /** @@ -149,12 +179,12 @@ function isAtom(noun: Noun | undefined): noun is string { */ function readPair(noun: Noun | undefined): [Noun, Noun] | null { // The wasm `Noun` type is declared as `string | [Noun]`, but at runtime a - // pair arrives as a flat array of length >= 2. Re-narrow through `unknown` + // pair arrives as a flat array of length >= 2. Copy it into a normal array // so we can inspect the real shape. - const arr = noun as unknown; - if (!Array.isArray(arr) || arr.length < 2) return null; - const head = arr[0] as Noun; - const tail = (arr.length === 2 ? arr[1] : arr.slice(1)) as Noun; + const arr = Array.isArray(noun) ? Array.from(noun) : null; + if (!arr || arr.length < 2) return null; + const head = arr[0]; + const tail = arr.length === 2 ? arr[1] : nounArray(arr[1], ...arr.slice(2)); return [head, tail]; } @@ -259,10 +289,10 @@ export async function buildBridgeTransaction( config.addresses.map(address => parseDigestString(address, 'bridge address')) ); const bridgeSpendCondition: SpendCondition = wasm.spendConditionNewPkh(bridgePkh); - const bridgeLockRoot: LockRoot = bridgeSpendCondition as unknown as LockRoot; + const bridgeLockRoot: LockRoot = bridgeSpendCondition; const refundPkhObj = wasm.pkhSingle(parseDigestString(params.refundPkh, 'refund pkh')); const refundSpendCondition: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj); - const refundLockRoot: LockRoot = refundSpendCondition as unknown as LockRoot; + const refundLockRoot: LockRoot = refundSpendCondition; const builder = new wasm.TxBuilder(options.txEngineSettings); @@ -284,7 +314,7 @@ export async function buildBridgeTransaction( try { spendBuilder = new wasm.SpendBuilder( note, - spendCondition as unknown as Lock, + spendCondition, 0, refundLockRoot ); From 0930ed36a77dc69d601ebb01a1d834d6f6d27abc Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:20:15 -0400 Subject: [PATCH 74/75] chore: add bump version to 0.2.0 and fix note data validation path --- package-lock.json | 12 +++++----- package.json | 4 ++-- src/bridge.ts | 57 ++++++++++++++++++----------------------------- 3 files changed, 30 insertions(+), 43 deletions(-) diff --git a/package-lock.json b/package-lock.json index f33bcd7..86482b8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@nockbox/iris-sdk", - "version": "0.2.0-alpha.4", + "version": "0.2.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@nockbox/iris-sdk", - "version": "0.2.0-alpha.4", + "version": "0.2.0", "license": "MIT", "dependencies": { - "@nockbox/iris-wasm": "0.2.0-alpha.10", + "@nockbox/iris-wasm": "0.2.0", "@scure/base": "^2.0.0" }, "devDependencies": { @@ -411,9 +411,9 @@ } }, "node_modules/@nockbox/iris-wasm": { - "version": "0.2.0-alpha.10", - "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0-alpha.10.tgz", - "integrity": "sha512-/CuHKz1Q2In/CdAQfUn1LgyOvFbc5mVkH94ko8gOxV/HU81C1Ctg2XMRP5lX2FoRR3OonEbBC4InUtzOPkWJfw==", + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@nockbox/iris-wasm/-/iris-wasm-0.2.0.tgz", + "integrity": "sha512-FUtYQ8nakzjBXuWKhgw8/QvEzzHfpJkLkkzdv0xu4t6Nn0hNDTkO8SC/Av5O8mTHXsFNTRNva1i4Dab6r29lLA==", "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { diff --git a/package.json b/package.json index f3a740e..65e20c1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@nockbox/iris-sdk", - "version": "0.2.0-alpha.4", + "version": "0.2.0", "description": "TypeScript SDK for interacting with Iris wallet extension", "type": "module", "main": "./dist/index.js", @@ -50,7 +50,7 @@ "url": "https://github.com/nockbox/iris-sdk.git" }, "dependencies": { - "@nockbox/iris-wasm": "0.2.0-alpha.10", + "@nockbox/iris-wasm": "0.2.0", "@scure/base": "^2.0.0" } } diff --git a/src/bridge.ts b/src/bridge.ts index ed6dd8a..bb49505 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -16,12 +16,10 @@ import type { Lock, LockRoot, Digest, - Note, NoteData, + NoteV1, Nicks, Noun, - PbCom2Note, - PbCom2NoteDataEntry, RawTxV1, SeedV1, SpendCondition, @@ -227,6 +225,18 @@ function collectBridgeSeeds(rawTx: RawTxV1, noteDataKey: string): SeedV1[] { return bridgeSeeds; } +/** Read V1 output note data*/ +function bridgeOutputData(note: NoteV1, noteDataKey: string): { assets: bigint; noteData: Noun } | null { + const entry = note.note_data.find( + (row): row is [string, Noun] => Array.isArray(row) && row[0] === noteDataKey + ); + if (!entry) return null; + return { + assets: BigInt(note.assets ?? 0), + noteData: entry[1], + }; +} + /** * Derive the bridge multisig lock root from config (same path as buildBridgeTransaction). */ @@ -419,31 +429,16 @@ export async function validateBridgeTransaction( } } - const outputs = wasm.rawTxOutputs(rawTx, 0, options.txEngineSettings); + const outputs = wasm.rawTxV1Outputs(rawTx, 0, options.txEngineSettings); if (outputs.length === 0) { return { valid: false, error: 'Transaction has no outputs' }; } - // Read each output via its protobuf form so the bridge note data is the raw - // jammed bytes (blob), not a serde-shaped JS value. - const outputData = outputs.map((output: Note) => { - const proto = wasm.noteToProtobuf(output) as PbCom2Note; - const version = proto.note_version; - const v1 = version && 'V1' in version ? version.V1 : undefined; - const entries: PbCom2NoteDataEntry[] = v1?.note_data?.entries ?? []; - return { - assets: BigInt(v1?.assets?.value ?? 0), - entries, - }; - }); - - let bridgeOutput: (typeof outputData)[0] | null = null; - for (const output of outputData) { - if (output.entries.some(e => e.key === config.noteDataKey)) { - bridgeOutput = output; - break; - } + let bridgeOutput: ReturnType = null; + for (const output of outputs) { + bridgeOutput = bridgeOutputData(output, config.noteDataKey); + if (bridgeOutput) break; } if (!bridgeOutput) { @@ -475,24 +470,16 @@ export async function validateBridgeTransaction( }; } - const bridgeEntry = bridgeOutput.entries.find(e => e.key === config.noteDataKey); - if (!bridgeEntry) { - return { - valid: false, - error: `Bridge output missing '${config.noteDataKey}' note data entry`, - }; - } - let destinationAddress: string | undefined; let belts: [bigint, bigint, bigint] | undefined; let validatedVersion: string | undefined; let validatedChain: string | undefined; - const validatedNoteDataKey = bridgeEntry.key; + const validatedNoteDataKey = config.noteDataKey; try { - // Deserialize the jammed blob into a noun, then walk it as a chain of - // right-nested pairs: [version [chain [belt1 [belt2 belt3]]]]. - const noun = wasm.cue(new Uint8Array(bridgeEntry.blob)); + // Walk the native note-data noun as a chain of right-nested pairs: + // [version [chain [belt1 [belt2 belt3]]]]. + const noun = bridgeOutput.noteData; const versionPair = readPair(noun); if (!versionPair) { From 1f17655d7a794c26afb524127a248205cc71beff Mon Sep 17 00:00:00 2001 From: Gohlub <62673775+Gohlub@users.noreply.github.com> Date: Thu, 4 Jun 2026 15:20:43 -0400 Subject: [PATCH 75/75] chore: format --- src/bridge.ts | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/bridge.ts b/src/bridge.ts index bb49505..4406b32 100644 --- a/src/bridge.ts +++ b/src/bridge.ts @@ -226,7 +226,10 @@ function collectBridgeSeeds(rawTx: RawTxV1, noteDataKey: string): SeedV1[] { } /** Read V1 output note data*/ -function bridgeOutputData(note: NoteV1, noteDataKey: string): { assets: bigint; noteData: Noun } | null { +function bridgeOutputData( + note: NoteV1, + noteDataKey: string +): { assets: bigint; noteData: Noun } | null { const entry = note.note_data.find( (row): row is [string, Noun] => Array.isArray(row) && row[0] === noteDataKey ); @@ -322,12 +325,7 @@ export async function buildBridgeTransaction( let spendBuilder: InstanceType | undefined; try { - spendBuilder = new wasm.SpendBuilder( - note, - spendCondition, - 0, - refundLockRoot - ); + spendBuilder = new wasm.SpendBuilder(note, spendCondition, 0, refundLockRoot); if (giftPortion > 0n) { const parentHash = wasm.noteHash(note);