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/examples/app.ts b/examples/app.ts
index 2603022..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;
@@ -67,18 +77,13 @@ 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
- log('Querying notes from gRPC...');
- const balance = await grpcClient.getBalanceByFirstName(firstName.value);
+ // 3. Derive first-name from PKH and query notes (notes are indexed by first-name, not address)
+ 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);
if (!balance || !balance.notes || balance.notes.length === 0) {
log('No notes found - wallet might be empty');
@@ -87,76 +92,64 @@ 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: wasm.PbCom2Note) => wasm.noteFromProtobuf(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 = asNicks(String(10 * 65536));
log('Building transaction to send 10 NOCK...');
- const builder = new wasm.TxBuilder(feePerWord);
-
- // Create recipient digest
- const recipientDigest = new wasm.Digest(recipient);
-
- // Create refund digest (same as wallet PKH)
- const refundDigest = new wasm.Digest(walletPkh);
+ const builder = new wasm.TxBuilder(txEngineSettings);
- // 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]],
- [spendCondition],
- recipientDigest,
+ [note],
+ [spendCondition as unknown as wasm.TxLock],
+ asDigest(recipient),
TEN_NOCK_IN_NICKS,
null, // fee_override (let it auto-calculate)
- refundDigest,
+ asDigest(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);
+ log('Transaction ID: ' + txId);
- const rawTxProtobuf = nockchainTx.toRawTx().toProtobuf();
-
- // Get notes and spend conditions from builder
- const txNotes = builder.allNotes();
-
- log('Notes count: ' + txNotes.notes.length);
- log('Spend conditions count: ' + txNotes.spendConditions.length);
-
- // 8. Sign using provider.signRawTx (pass wasm objects directly)
+ // 6. Sign using provider.signTx
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
- });
+ const signed = await provider.signTx(nockchainTx);
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 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);
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..6266b6d 100644
--- a/examples/tx-builder.ts
+++ b/examples/tx-builder.ts
@@ -1,17 +1,18 @@
-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
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[];
}
@@ -41,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,
@@ -104,18 +106,38 @@ 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 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;
+}
+
declare global {
interface Window {
copyToClipboard: (text: string, element: HTMLElement) => void;
@@ -226,8 +248,9 @@ 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.txEngineSettings = getLatestTxEngineSettings(info.rpcConfig.txEngineActivationHeights);
state.connected = true;
connectBtn.textContent = truncateAddress(state.walletPkh);
@@ -253,33 +276,40 @@ 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 = {
+ tag: 'pkh',
+ m: 1,
+ hashes: [asDigest(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 = {
+ tag: 'pkh',
+ m: 1,
+ hashes: [asDigest(state.walletPkh)],
+ };
+ const coinbaseTim: wasm.LockTim = {
+ rel: { min: asBlockHeight(100), max: null },
+ abs: { min: null, max: null },
+ };
+ const timPrim: wasm.LockPrimitive = { tag: '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 +357,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 +369,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.noteFromProtobuf(noteProto);
+ const assets = note.assets ?? asNicks('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 +454,7 @@ function addInputToSpend(lockId: string, noteIndex: number) {
const spend: Spend = {
inputId: input.id,
input,
- fee: BigInt(0),
+ fee: asNicks('0'),
seeds: [],
};
@@ -507,18 +527,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(asNicks(String(total))))} NOCK)`;
}
- const sign = diff > 0 ? '+' : '';
- return `${sign}${formatNock(nicksToNock(diff))} NOCK`;
+ const sign = diff > 0n ? '+' : '';
+ return `${sign}${formatNock(nicksToNock(asNicks(String(diff))))} NOCK`;
}
function removeSpend(inputId: string) {
@@ -544,17 +566,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 +583,7 @@ function balanceSeed(inputId: string, seedIndex: number) {
return;
}
- seed.amount = remaining;
+ seed.amount = asNicks(String(remaining));
renderSpends();
updateBuilder();
}
@@ -574,9 +595,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 = asNicks('0');
}
if (shouldRender) renderSpends();
updateBuilder();
@@ -591,7 +612,7 @@ function addSeed(inputId: string) {
if (spend && state.locks.length > 0) {
spend.seeds.push({
lockId: state.locks[0].id,
- amount: BigInt(0),
+ amount: asNicks('0'),
});
renderSpends();
updateBuilder();
@@ -619,9 +640,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 = asNicks('0');
}
if (shouldRender) renderSpends();
updateBuilder();
@@ -660,60 +681,44 @@ 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 builder = new wasm.TxBuilder(state.txEngineSettings);
- // 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.LockRoot | null =
spend.seeds.length > 0
- ? state.locks.find(l => l.id === spend.seeds[0].lockId)?.spendConditionProtobuf
- : lock.spendConditionProtobuf;
-
- // 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);
+ ? ((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,
+ asLock(lock.spendCondition),
+ null,
+ refundLock ? asLock(refundLock) : null
+ );
- // 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: null,
+ lock_root: asLock(seedLock.spendCondition),
+ note_data: [],
+ gift: seed.amount,
+ parent_hash: wasm.noteHash(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 +947,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,28 +968,26 @@ function renderTransaction() {
txInfo.innerHTML = `
@@ -1419,109 +1424,66 @@ 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({
+ tag: 'pkh',
+ m: pkhConfig.m,
+ hashes: validAddrs.map(addr => asDigest(addr)),
+ });
break;
-
- case 'tim':
+ }
+ case 'tim': {
const timConfig = prim.tim || { type: 'csv', value: 1 };
- let tim;
+ const value = asBlockHeight(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({
+ tag: 'tim',
+ rel: { min: value, max: null },
+ abs: { min: null, max: null },
+ });
} 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({
+ tag: 'tim',
+ rel: { min: null, max: null },
+ abs: { min: value, max: null },
+ });
}
- 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({ tag: 'hax', preimages: validHashes.map(hash => asDigest(hash)) });
break;
-
+ }
case 'brn':
- primitives.push(wasm.LockPrimitive.newBrn());
+ primitives.push({ tag: '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 +1499,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 +1540,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 +1593,9 @@ downloadTxBtn.onclick = () => {
if (!state.nockchainTx) return;
try {
- const jamBytes = state.nockchainTx.toJam();
- const txId = state.nockchainTx.id.value;
+ const rawTx = wasm.nockchainTxToRawTx(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,32 +1618,15 @@ 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();
-
- // Sign using provider
- const signedTxProtobuf = await state.provider.signRawTx({
- rawTx: state.nockchainTx.toRawTx(),
- notes: txNotes.notes,
- spendConditions: txNotes.spendConditions,
- });
-
- // Store signed TX
- // NockchainTx doesn't have fromProtobuf, so we go via RawTx
- const signedRawTx = wasm.RawTx.fromProtobuf(signedTxProtobuf);
- state.signedTx = signedRawTx.toNockchainTx();
+ const signed = await state.provider.signTx(state.nockchainTx);
+ const signedRawTx = wasm.nockchainTxToRawTx(signed.tx) as wasm.RawTxV1;
+ state.signedTx = signed.tx;
- // Validate the signed transaction
- console.log('Validating signed transaction...');
let isValid = true;
let validationError = '';
try {
- const signedBuilder = wasm.TxBuilder.fromTx(
- state.signedTx.toRawTx(),
- txNotes.notes,
- txNotes.spendConditions
- );
+ const signedBuilder = wasm.TxBuilder.fromNockchainTx(signed.tx, state.txEngineSettings);
signedBuilder.validate();
signedBuilder.free();
console.log('Signed transaction validated successfully');
@@ -1698,15 +1635,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 +1673,8 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => {
if (!state.signedTx || !state.signedTxId) return;
try {
- const jamBytes = state.signedTx.toJam();
+ 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);
const a = document.createElement('a');
@@ -1749,7 +1685,8 @@ document.getElementById('downloadSignedTxBtn')!.onclick = () => {
document.body.removeChild(a);
URL.revokeObjectURL(url);
- const rawTxBytes = state.signedTx.toRawTx().toJam();
+ 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);
const aRaw = document.createElement('a');
@@ -1771,8 +1708,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.nockchainTxToRawTx(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..86482b8 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",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@nockbox/iris-sdk",
- "version": "0.1.1",
+ "version": "0.2.0",
"license": "MIT",
"dependencies": {
- "@nockbox/iris-wasm": "^0.1.1",
+ "@nockbox/iris-wasm": "0.2.0",
"@scure/base": "^2.0.0"
},
"devDependencies": {
@@ -411,15 +411,15 @@
}
},
"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",
+ "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": {
- "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"
],
@@ -431,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"
],
@@ -445,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"
],
@@ -459,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"
],
@@ -473,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"
],
@@ -487,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"
],
@@ -501,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"
],
@@ -515,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"
],
@@ -529,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"
],
@@ -543,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"
],
@@ -557,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"
],
@@ -571,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"
],
@@ -585,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"
],
@@ -599,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"
],
@@ -613,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"
],
@@ -627,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"
],
@@ -641,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"
],
@@ -654,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"
],
@@ -669,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"
],
@@ -683,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"
],
@@ -697,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"
],
@@ -711,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"
],
@@ -725,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/"
@@ -741,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": {
@@ -805,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": [
{
@@ -831,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": [
{
@@ -851,7 +893,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.11",
+ "nanoid": "^3.3.12",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -860,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": {
@@ -876,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": {
@@ -892,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"
}
},
diff --git a/package.json b/package.json
index 40910ed..65e20c1 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@nockbox/iris-sdk",
- "version": "0.1.1",
+ "version": "0.2.0",
"description": "TypeScript SDK for interacting with Iris wallet extension",
"type": "module",
"main": "./dist/index.js",
@@ -9,6 +9,10 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
+ },
+ "./wasm": {
+ "types": "./dist/wasm.d.ts",
+ "import": "./dist/wasm.js"
}
},
"files": [
@@ -16,8 +20,8 @@
"README.md"
],
"scripts": {
- "prepare": "npm run build",
"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",
@@ -37,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": {
- "@scure/base": "^2.0.0",
- "@nockbox/iris-wasm": "^0.1.1"
+ "@nockbox/iris-wasm": "0.2.0",
+ "@scure/base": "^2.0.0"
}
}
diff --git a/src/bridge-types.ts b/src/bridge-types.ts
new file mode 100644
index 0000000..277c881
--- /dev/null
+++ b/src/bridge-types.ts
@@ -0,0 +1,99 @@
+/**
+ * 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,
+ TxEngineSettings,
+} from '@nockbox/iris-wasm/iris_wasm.js';
+
+export type { Nicks, TxEngineSettings };
+
+/**
+ * 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;
+ /** 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;
+}
+
+/**
+ * Options for bridge transaction build / validation (mirrors migration: caller supplies tx engine).
+ */
+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).
+ */
+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;
+ /** Lock root digest for the bridge output seed */
+ bridgeLockRoot?: string;
+}
diff --git a/src/bridge.ts b/src/bridge.ts
new file mode 100644
index 0000000..4406b32
--- /dev/null
+++ b/src/bridge.ts
@@ -0,0 +1,604 @@
+/**
+ * Bridge utilities for Nockchain ↔ EVM bridging.
+ * Encoding uses the wasm atom/belt conversion helpers for EVM addresses.
+ * Consumers provide BridgeConfig; the SDK handles transaction construction and validation.
+ */
+
+import type {
+ BridgeConfig,
+ BridgeTransactionParams,
+ BridgeTransactionResult,
+ BridgeValidationParams,
+ BridgeValidationResult,
+ BuildBridgeTransactionOptions,
+} from './bridge-types.js';
+import type {
+ Lock,
+ LockRoot,
+ Digest,
+ NoteData,
+ NoteV1,
+ Nicks,
+ Noun,
+ RawTxV1,
+ SeedV1,
+ SpendCondition,
+} from '@nockbox/iris-wasm/iris_wasm.js';
+import { base58 } from '@scure/base';
+import * as wasm from './wasm.js';
+
+/** 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);
+}
+
+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 through the canonical atom encoding.
+ */
+export function evmAddressToBelts(address: string): [bigint, bigint, bigint] {
+ if (!isEvmAddress(address)) {
+ throw new Error(`Invalid EVM address: ${address}`);
+ }
+
+ 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 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). */
+export function bigintToAtom(n: bigint): string {
+ if (n === 0n) return '0';
+ return n.toString(16);
+}
+
+/**
+ * Build the bridge noun structure for an EVM address.
+ *
+ * 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
+): Noun {
+ const [belt1, belt2, belt3] = evmAddressToBelts(evmAddress);
+ return nounArray(
+ config.versionTag,
+ nounArray(
+ config.chainTag,
+ nounArray(bigintToAtom(belt1), nounArray(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
+ );
+}
+
+// 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. Copy it into a normal array
+ // so we can inspect the real shape.
+ 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];
+}
+
+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;
+}
+
+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;
+}
+
+/** 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).
+ */
+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.
+ */
+export async function createBridgeNoteData(
+ evmAddress: string,
+ config: BridgeConfig
+): Promise {
+ const nounJs = buildBridgeNoun(evmAddress, config);
+ return wasm.jam(nounJs);
+}
+
+/**
+ * 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,
+ 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}`);
+ }
+ 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`
+ );
+ }
+
+ const bridgeNounJs = buildBridgeNoun(params.destinationAddress, config);
+ const noteData: NoteData = [[config.noteDataKey, bridgeNounJs]];
+
+ 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(parseDigestString(params.refundPkh, 'refund pkh'));
+ const refundSpendCondition: SpendCondition = wasm.spendConditionNewPkh(refundPkhObj);
+ const refundLockRoot: LockRoot = refundSpendCondition;
+
+ const builder = new wasm.TxBuilder(options.txEngineSettings);
+
+ try {
+ 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) {
+ 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, 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();
+ }
+ }
+
+ 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`
+ );
+ }
+
+ builder.recalcAndSetFee(false);
+ const feeResult = builder.curFee();
+ const transaction = builder.build();
+
+ const txId = transaction.id;
+ const fee = feeResult;
+
+ return {
+ transaction,
+ txId,
+ fee,
+ };
+ } finally {
+ builder.free();
+ }
+}
+
+/**
+ * Validate a bridge transaction (pre- or post-signing).
+ * Uses config for note key, min amount, and optional lock root.
+ */
+export async function validateBridgeTransaction(
+ rawTx: RawTxV1,
+ 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 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(rawTx, 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 rawTx.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.rawTxV1Outputs(rawTx, 0, options.txEngineSettings);
+
+ if (outputs.length === 0) {
+ return { valid: false, error: 'Transaction has no outputs' };
+ }
+
+ let bridgeOutput: ReturnType = null;
+ for (const output of outputs) {
+ bridgeOutput = bridgeOutputData(output, config.noteDataKey);
+ if (bridgeOutput) 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 (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`,
+ };
+ }
+
+ let destinationAddress: string | undefined;
+ let belts: [bigint, bigint, bigint] | undefined;
+ let validatedVersion: string | undefined;
+ let validatedChain: string | undefined;
+ const validatedNoteDataKey = config.noteDataKey;
+
+ try {
+ // 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) {
+ return {
+ valid: false,
+ error: 'Invalid bridge note data structure: expected [version, [chain, belts]]',
+ };
+ }
+ 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 = version;
+
+ const chainPair = readPair(chainAndBelts);
+ if (!chainPair) {
+ return { valid: false, error: 'Invalid bridge note data: missing chain and belts' };
+ }
+ const [chain, beltData] = chainPair;
+ if (!isAtom(chain) || chain !== config.chainTag) {
+ return {
+ valid: false,
+ error: `Invalid bridge chain: expected ${config.chainTag}, got ${String(chain)}`,
+ };
+ }
+ validatedChain = chain;
+
+ const belt1Pair = readPair(beltData);
+ if (!belt1Pair) {
+ return { valid: false, error: 'Invalid bridge note data: invalid belt structure' };
+ }
+ const [belt1Noun, belt2And3] = belt1Pair;
+
+ const belt2Pair = readPair(belt2And3);
+ if (!belt2Pair) {
+ return { valid: false, error: 'Invalid bridge note data: invalid belt2/belt3 structure' };
+ }
+ const [belt2Noun, belt3Noun] = belt2Pair;
+
+ if (!isAtom(belt1Noun) || !isAtom(belt2Noun) || !isAtom(belt3Noun)) {
+ return { valid: false, error: 'Invalid bridge note data: belt values are not atoms' };
+ }
+
+ const belt1 = BigInt('0x' + belt1Noun);
+ const belt2 = BigInt('0x' + belt2Noun);
+ const belt3 = BigInt('0x' + belt3Noun);
+ belts = [belt1, belt2, belt3];
+ destinationAddress = beltsToEvmAddress(belt1, belt2, belt3);
+
+ if (!isEvmAddress(destinationAddress)) {
+ return {
+ valid: false,
+ 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,
+ error: `Failed to decode bridge note data: ${
+ err instanceof Error ? err.message : String(err)
+ }`,
+ };
+ }
+
+ return {
+ valid: true,
+ bridgeAmountNicks: bridgeOutput.assets.toString() as Nicks,
+ destinationAddress,
+ belts,
+ noteDataKey: validatedNoteDataKey,
+ version: validatedVersion,
+ chain: validatedChain,
+ bridgeLockRoot,
+ };
+ } 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(
+ rawTx: RawTxV1,
+ context: 'pre-signing' | 'post-signing',
+ params: BridgeValidationParams,
+ config: BridgeConfig,
+ options: BuildBridgeTransactionOptions
+): Promise {
+ const result = await validateBridgeTransaction(rawTx, params, config, options);
+ if (!result.valid) {
+ throw new Error(`${context} validation failed: ${result.error}`);
+ }
+ return result;
+}
+
+// Re-export types
+export type {
+ BridgeConfig,
+ BridgeTransactionParams,
+ BridgeTransactionResult,
+ BridgeValidationParams,
+ BridgeValidationResult,
+ BuildBridgeTransactionOptions,
+ TxEngineSettings,
+} from './bridge-types.js';
diff --git a/src/compat.ts b/src/compat.ts
new file mode 100644
index 0000000..3c48e15
--- /dev/null
+++ b/src/compat.ts
@@ -0,0 +1,375 @@
+/**
+ * Backward-compatibility helpers for SDK request payloads.
+ */
+
+import type {
+ SendTransactionRequest,
+ NicksLike,
+ RpcRequest,
+ RpcResponse,
+ ConnectRequest,
+ ConnectResponse,
+ SignMessageRequest,
+ SignTxRequest,
+ SignMessageResponse,
+ SignTxResponse,
+} from './types.js';
+import {
+ PbCom2RawTransaction,
+ PbCom2Note,
+ PbCom2SpendCondition,
+ rawTxFromProtobuf,
+ rawTxV1ToNockchainTx,
+ nockchainTxToRawTx,
+ rawTxToProtobuf,
+ rawTxInputSpendConditions,
+ publicKeyFromHex,
+ publicKeyToHex,
+ RawTxV1,
+ noteToProtobuf,
+ noteFromProtobuf,
+ spendConditionToProtobuf,
+ SpendCondition,
+ Digest,
+} from './wasm.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.
+ * 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;
+ notes: PbCom2Note[];
+ spendConditions: PbCom2SpendCondition[];
+}
+
+/** 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 };
+ 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))
+ );
+}
+
+interface LegacyConnectResponse {
+ grpcEndpoint: string;
+ pkh: Digest;
+}
+
+interface LegacySignMessageResponse {
+ signature: string;
+ publicKeyHex: string;
+}
+
+/** 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;
+
+ 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) {
+ const params = request.params as SendTransactionRequest | undefined;
+ return { ...request, params: params ? [params] : request.params };
+ }
+ if (!fromV1 && toV1) {
+ const params = request.params as SendTransactionRequest[] | 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;
+ }
+ 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 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 };
+ }
+ 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;
+ }
+}
+
+/**
+ * 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;
+ if (response.error) return response;
+
+ const fromV1 = fromApi === RPC_API_VERSION;
+ const toV1 = toApi === RPC_API_VERSION;
+
+ 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: DEFAULT_COINBASE_TIMELOCK_BLOCKS,
+ },
+ };
+ return { ...response, result };
+ }
+ 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 || 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 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');
+ }
+ const rawTx = nockchainTxToRawTx(v1.tx);
+ if (!guard.isRawTxV1(rawTx)) {
+ throw new Error('Only V1 Raw TXs are supported at the moment');
+ }
+ return { ...response, result: rawTxToProtobuf(rawTx) };
+ }
+ if (!fromV1 && toV1) {
+ // 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(legacyRawTx);
+ 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;
+ }
+}
+
+/**
+ * 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.
+ *
+ * Maps the request from sourceApi → targetApi, calls target, then maps
+ * the response back from targetApi → sourceApi (inverted).
+ */
+export async function requestBridge(
+ request: RpcRequest,
+ target: (request: RpcRequest) => Promise>,
+ sourceApi?: string,
+ targetApi?: string
+): Promise> {
+ const mappedReq = mapRpcRequest(request, sourceApi, targetApi);
+ const res = await target(mappedReq);
+ return mapRpcResponse(mappedReq.method, res, targetApi, sourceApi) as RpcResponse;
+}
diff --git a/src/constants.ts b/src/constants.ts
index cd2cfa0..3e41718 100644
--- a/src/constants.ts
+++ b/src/constants.ts
@@ -1,3 +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
@@ -15,8 +20,84 @@ 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,
+};
+
+/**
+ * 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,
+};
+
+/**
+ * Pick the latest tx engine from an activation-height map.
+ * Falls back to the SDK's default activation map when none is provided.
+ */
+export function getLatestTxEngineSettings(
+ activationHeights: 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;
+
+/** 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 (0.3%). */
+export const BRIDGE_PROTOCOL_FEE_RATE = 0.003;
+
+/** Zorp bridge 3-of-5 multisig (Nockchain → Base). */
+export const ZORP_BRIDGE_THRESHOLD = 3;
+
+export const ZORP_BRIDGE_ADDRESSES: string[] = [
+ 'AD6Mw1QUnPUrnVpyj2gW2jT6Jd6WsuZQmPn79XpZoFEocuvV12iDkvh', // Zorp #1
+ '6KrZT5hHLY1fva9AUDeGtZu5Jznm4RDLYfjcGjuU49nWoNym5ZeX5X5', // Zorp #2
+ 'CDLzgKWAKFXYABkuQaMwbttDSTDMh3Wy2Eoq2XiArsyxn7vScNHupBb', // Pero
+ '7E47xYNVEyt7jGmLsiChUHnyw88AfBvzJfXfEQkPmMo2ZWsdcPudwmV', // Nockbox
+ '3xSyK6RQUaYzE8YDUamkpKRHALxaYo8E7eppawwE4sP35c3PASc6koq', // SWPS
+];
diff --git a/src/index.ts b/src/index.ts
index dd6f7fe..9bdc74a 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -5,7 +5,11 @@
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 './validate-sign-tx-request.js';
+export * from './compat.js';
+export * from './bridge.js';
export * as wasm from './wasm.js';
+export { initWasm } from './wasm.js';
diff --git a/src/migration-types.ts b/src/migration-types.ts
new file mode 100644
index 0000000..dfd12fa
--- /dev/null
+++ b/src/migration-types.ts
@@ -0,0 +1,59 @@
+/**
+ * Types for querying v0 balance and building v0 -> v1 migration transactions.
+ * Aligned with @nockbox/iris-wasm (Nicks, NoteV0, RawTx, SpendCondition).
+ */
+import type {
+ LockRoot,
+ Nicks,
+ NoteV0,
+ PbCom2Balance,
+ RawTx,
+ SpendCondition,
+ TxEngineSettings,
+} from '@nockbox/iris-wasm/iris_wasm.js';
+
+export type { Nicks };
+
+/** 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;
+}
+
+/**
+ * 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 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;
+ v0MigrationTxSignPayload?: V0MigrationTxSignPayload;
+ 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
new file mode 100644
index 0000000..3aa299c
--- /dev/null
+++ b/src/migration.ts
@@ -0,0 +1,277 @@
+import type {
+ BuildV0MigrationTxResult,
+ BuildV0MigrationTxOptions,
+ V0BalanceResult,
+ V0MigrationTxSignPayload,
+} from './migration-types.js';
+import type {
+ Nicks,
+ NoteV0,
+ PbCom2Note,
+ PublicKey,
+ RawTxV1,
+ SpendCondition,
+ Digest,
+ Lock,
+ LockRoot,
+ TxEngineSettings,
+} 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 {
+ const pkhObj = wasm.pkhSingle(pkh);
+ return wasm.spendConditionNewPkh(pkhObj);
+}
+
+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;
+}
+
+function summarizeNote(note: NoteV0): { assetsNicks: string; assetsNock: number } {
+ const assets = BigInt(note.assets);
+ return {
+ assetsNicks: assets.toString(),
+ assetsNock: Number(assets) / NOCK_TO_NICKS,
+ };
+}
+
+/**
+ * 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[],
+ 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.
+ */
+export async function queryV0Balance(
+ sourcePublicKey: PublicKey,
+ grpcEndpoint: string
+): Promise {
+ 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) {
+ 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
+ : undefined;
+
+ return {
+ sourceAddress,
+ balance,
+ v0Notes,
+ totalNicks,
+ totalNock,
+ smallestNoteNock,
+ rawNotesFromRpc: entries.length,
+ };
+}
+
+/**
+ * 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 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,
+ grpcEndpoint: string,
+ targetV1Pkh: Digest,
+ options: BuildV0MigrationTxOptions
+): 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;
+ if (!v0Notes.length) {
+ throw new Error('No v0 notes to migrate');
+ }
+
+ const sortedNotes = [...v0Notes]
+ .map(note => ({ note, assets: BigInt(note.assets) }))
+ .sort((a, b) => (a.assets < b.assets ? -1 : a.assets > b.assets ? 1 : 0));
+
+ // 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]
+ : []
+ : 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_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);
+ const refundLock = wasm.lockHash(targetSpendCondition);
+ const builder = new wasm.TxBuilder(options.txEngineSettings);
+ appendV0MigrationSpends(
+ builder,
+ notesToUse,
+ notesToUse.map(() => null),
+ refundLock
+ );
+
+ builder.recalcAndSetFee(false);
+ 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;
+ 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,
+ txId: transaction.id,
+ fee: feeNicks,
+ feeNock,
+ v0MigrationTxSignPayload: {
+ rawTx,
+ notes: inputNotes,
+ spendConditions: inputNotes.map(() => null),
+ refundLock,
+ },
+ migratedNicks,
+ migratedNock,
+ };
+ return result;
+ } catch (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;
+ }
+}
+
+export type {
+ BuildV0MigrationTxOptions,
+ BuildV0MigrationTxResult,
+ V0BalanceResult,
+ V0MigrationTxSignPayload,
+} from './migration-types.js';
diff --git a/src/provider.ts b/src/provider.ts
index cb3ee83..cae3a11 100644
--- a/src/provider.ts
+++ b/src/provider.ts
@@ -2,10 +2,23 @@
* 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 { 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
@@ -29,7 +42,7 @@ import { PROVIDER_METHODS } from './constants.js';
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;
@@ -68,21 +81,21 @@ 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,
});
// 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];
}
@@ -110,14 +123,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();
}
- return this.request({
+ return this.request({
method: PROVIDER_METHODS.SEND_TRANSACTION,
- params: [transaction],
+ params: transaction,
});
}
@@ -129,95 +142,44 @@ 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
- * Accepts either wasm objects (with toProtobuf() method) or protobuf JS objects
- * @param params - The transaction parameters (rawTx, notes, spendConditions)
- * @returns Promise resolving to the signed raw transaction as protobuf Uint8Array
+ * 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
* @throws {RpcError} If the RPC call fails
*
* @example
* ```typescript
- * // Option 1: Pass wasm objects directly (auto-converts to protobuf)
- * const rawTx = 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
- * });
- *
- * // Option 2: Pass protobuf JS objects directly
- * const signedTx = await provider.signRawTx({
- * rawTx: rawTxProtobufObject, // protobuf JS object
- * notes: noteProtobufObjects, // array of protobuf JS objects
- * spendConditions: spendCondProtobufObjects // array of protobuf JS objects
- * });
+ * const nockchainTx = wasm.rawTxV1ToNockchainTx(rawTx);
+ * const signedTx = await provider.signTx(nockchainTx);
* ```
*/
- async signRawTx(params: {
- rawTx: any;
- notes: any[];
- spendConditions: any[];
- }): Promise {
+ async signTx(tx: NockchainTx, notes?: Note[]): 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),
- };
-
- return this.request({
- method: PROVIDER_METHODS.SIGN_RAW_TX,
- params: [protobufParams],
+ 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
@@ -268,9 +230,12 @@ 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,
+ api: args.api ?? RPC_API_VERSION,
+ });
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 58b8ab8..0000000
--- a/src/transaction.ts
+++ /dev/null
@@ -1,163 +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';
-
-/**
- * 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 {
- // Use setters to ensure validation is applied
- let builder = new TransactionBuilder().to(tx.to).amount(tx.amount);
-
- if (typeof tx.fee === 'number') {
- builder = builder.fee(tx.fee);
- }
-
- return builder;
- }
-}
diff --git a/src/types.ts b/src/types.ts
index 72544f2..c18d543 100644
--- a/src/types.ts
+++ b/src/types.ts
@@ -2,26 +2,23 @@
* TypeScript type definitions for Iris SDK
*/
+import { Nicks, TxEngineSettings, PublicKey, Signature, Digest, NockchainTx, Note } from './wasm';
+
/**
* Transaction object representing a Nockchain transaction
*/
-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;
-}
+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;
+ /** API version of this request payload (defaults to legacy API 0 when omitted) */
+ api?: string;
/** Optional parameters for the method */
- params?: unknown[];
+ params?: T;
/** Optional timeout for the request */
timeout?: number;
}
@@ -40,6 +37,69 @@ export interface RpcResponse {
};
}
+export interface ConnectRequest {
+ /** @deprecated API version now lives on RpcRequest.api */
+ 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 input notes supplied for approval display and API 0 wallet compatibility.
+ * The canonical signer consumes `tx`; notes are sidecar metadata.
+ */
+ notes?: Note[];
+}
+
+export interface SignTxResponse {
+ tx: NockchainTx;
+}
+
/**
* Event types that the provider can emit
*/
@@ -59,7 +119,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')
@@ -70,6 +130,11 @@ export interface InjectedNockchain {
* Provider version
*/
version?: string;
+
+ /**
+ * Supported RPC API version
+ */
+ api?: string;
}
/**
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))))
+ );
+}
diff --git a/src/wasm.ts b/src/wasm.ts
index 12313e9..966c8ff 100644
--- a/src/wasm.ts
+++ b/src/wasm.ts
@@ -2,5 +2,14 @@
* 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';
+
+/**
+ * Canonical initializer re-export for SDK consumers.
+ * Prefer using this instead of importing iris-wasm directly.
+ */
+export const initWasm = init;