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 = `
- 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);