+
+
-
- Transfer your balance from V0 to V1
+
+
+
+ v0 Funds Migration
+
+
+ Transfer your balance from V0 to V1
+
-
-
- The network has upgraded. If your wallet was created before
- October 25, 2025
- (block 39,000), your funds need to be migrated to remain accessible on the current network.
-
-
- This process transfers your full balance from your v0 wallet to v1. It only takes a moment.
+
+
+ The network has upgraded. If your wallet was created before
+ October 25, 2025
+
+ {' '}
+ (block 39,000), your funds need to be migrated to remain accessible on the current network.
+
+
+
+ This process transfers your full balance from your v0 wallet to v1. It only takes a moment.
+
+
+ {/* Footer CTA - matches other migration screens */}
+
Start Migration
@@ -61,4 +117,3 @@ export function V0MigrationIntroScreen() {
);
}
-
diff --git a/extension/popup/screens/V0MigrationReviewScreen.tsx b/extension/popup/screens/V0MigrationReviewScreen.tsx
index 85852a2..0905fcd 100644
--- a/extension/popup/screens/V0MigrationReviewScreen.tsx
+++ b/extension/popup/screens/V0MigrationReviewScreen.tsx
@@ -29,7 +29,6 @@ export function V0MigrationReviewScreen() {
const { txId, confirmed, skipped } = await signAndBroadcastV0Migration(
v0MigrationDraft.v0Mnemonic,
v0MigrationDraft.signRawTxPayload,
- undefined,
{ debug: true, skipBroadcast }
);
setV0MigrationDraft({
@@ -115,7 +114,7 @@ export function V0MigrationReviewScreen() {
Network fee
- {v0MigrationDraft.feeNock} NOCK
+ {v0MigrationDraft.feeNock != null ? `${v0MigrationDraft.feeNock} NOCK` : ''}
{sendError &&
{sendError} }
diff --git a/extension/popup/screens/V0MigrationSetupScreen.tsx b/extension/popup/screens/V0MigrationSetupScreen.tsx
index a724690..f8fc274 100644
--- a/extension/popup/screens/V0MigrationSetupScreen.tsx
+++ b/extension/popup/screens/V0MigrationSetupScreen.tsx
@@ -5,7 +5,7 @@ import { Alert } from '../components/Alert';
import lockIcon from '../assets/lock-icon.svg';
import { importKeyfile, type Keyfile } from '../../shared/keyfile';
import { UI_CONSTANTS } from '../../shared/constants';
-import { queryV0BalanceFromMnemonic } from '../../shared/v0-migration';
+import { queryV0Balance } from '../../shared/v0-migration';
const WORD_COUNT = 24;
@@ -116,34 +116,29 @@ export function V0MigrationSetupScreen() {
setDiscoverError('');
setIsDiscovering(true);
try {
- const discovery = await queryV0BalanceFromMnemonic(words.join(' ').trim());
+ const mnemonic = words.join(' ').trim();
+ const result = await queryV0Balance(mnemonic);
- if (!discovery.v0Notes.length) {
- const rawCount = discovery.rawNotesFromRpc ?? 0;
+ if (!result.v0Notes.length) {
+ const rawCount = result.rawNotesFromRpc ?? 0;
const msg =
rawCount > 0
? `No v0 (Legacy) notes found. RPC returned ${rawCount} note(s) but none match Legacy format. Check DevTools console for details.`
- : `No v0 notes found for this recovery phrase. Queried address: ${discovery.sourceAddress?.slice(0, 12)}... (see console for full address)`;
+ : `No v0 notes found for this recovery phrase. Queried address: ${result.sourceAddress?.slice(0, 12)}... (see console for full address)`;
throw new Error(msg);
}
- const mnemonic = words.join(' ').trim();
setV0MigrationDraft({
- sourceAddress: discovery.sourceAddress,
+ sourceAddress: result.sourceAddress,
v0Mnemonic: mnemonic,
- v0Notes: discovery.v0Notes,
- v0BalanceNock: discovery.totalNock,
+ v0Notes: result.v0Notes,
+ v0BalanceNock: result.totalNock,
migratedAmountNock: undefined,
- feeNock: 59,
+ feeNock: undefined,
keyfileName: undefined,
signRawTxPayload: undefined,
txId: undefined,
});
- console.log('[V0 Migration] derived query address', {
- sourceAddress: discovery.sourceAddress,
- totalNock: discovery.totalNock,
- legacyNotesCount: discovery.v0Notes.length,
- });
setWords(Array(WORD_COUNT).fill(''));
navigate('v0-migration-funds');
} catch (err) {
@@ -214,7 +209,7 @@ export function V0MigrationSetupScreen() {
Or import from keyfile
- {/* 24-word input grid - same as ImportScreen */}
+ {/* 24-word input grid */}
{Array.from({ length: 12 }).map((_, rowIndex) => (
diff --git a/extension/popup/store.ts b/extension/popup/store.ts
index 0d8f8e1..1d1b996 100644
--- a/extension/popup/store.ts
+++ b/extension/popup/store.ts
@@ -111,7 +111,7 @@ interface AppStore {
v0MigrationDraft: {
v0BalanceNock: number;
migratedAmountNock?: number;
- feeNock: number;
+ feeNock?: number;
destinationWalletIndex: number | null;
keyfileName?: string;
sourceAddress?: string;
@@ -130,7 +130,7 @@ interface AppStore {
value: Partial<{
v0BalanceNock: number;
migratedAmountNock?: number;
- feeNock: number;
+ feeNock?: number;
destinationWalletIndex: number | null;
keyfileName?: string;
sourceAddress?: string;
@@ -227,7 +227,7 @@ export const useStore = create
((set, get) => ({
v0MigrationDraft: {
v0BalanceNock: 2500,
migratedAmountNock: undefined,
- feeNock: 59,
+ feeNock: undefined,
destinationWalletIndex: null,
keyfileName: undefined,
sourceAddress: undefined,
@@ -313,7 +313,7 @@ export const useStore = create((set, get) => ({
v0MigrationDraft: {
v0BalanceNock: 2500,
migratedAmountNock: undefined,
- feeNock: 59,
+ feeNock: undefined,
destinationWalletIndex: null,
keyfileName: undefined,
sourceAddress: undefined,
diff --git a/extension/shared/v0-migration.ts b/extension/shared/v0-migration.ts
index 57d86cf..53690c3 100644
--- a/extension/shared/v0-migration.ts
+++ b/extension/shared/v0-migration.ts
@@ -2,112 +2,61 @@
* v0-to-v1 migration - delegates discovery and build to SDK.
*/
-import { NOCK_TO_NICKS, RPC_ENDPOINT } from './constants';
import { ensureWasmInitialized } from './wasm-utils';
+import { getEffectiveRpcEndpoint } from './rpc-config';
import {
- buildV0MigrationTransaction as sdkBuildFromV0Notes,
- deriveV0AddressFromMnemonic as sdkDeriveV0Address,
- queryV0BalanceFromMnemonic as sdkQueryV0Balance,
+ buildV0MigrationTx as sdkBuildV0MigrationTx,
+ queryV0Balance as sdkQueryV0Balance,
+ type BuildV0MigrationTxResult,
+ type V0BalanceResult,
} from '@nockbox/iris-sdk';
import wasm from './sdk-wasm.js';
-import { txEngineSettings } from './tx-engine-settings.js';
import { createBrowserClient } from './rpc-client-browser';
-export interface V0DiscoveryResult {
- sourceAddress: string;
- v0Notes: any[];
- totalNicks: string;
- totalNock: number;
- /** Raw notes count from RPC (for debugging when v0Notes is empty) */
- rawNotesFromRpc?: number;
-}
+export type { V0BalanceResult };
-export interface BuiltV0MigrationResult {
- txId: string;
- feeNicks: string;
- feeNock: number;
- migratedNicks: string;
- migratedNock: number;
- selectedNoteNicks: string;
- selectedNoteNock: number;
- signRawTxPayload: {
- rawTx: any;
- notes: any[];
- spendConditions: any[];
- };
-}
+const CONFIRM_POLL_INTERVAL_MS = 3000;
+const CONFIRM_TIMEOUT_MS = 90_000;
+/** [TEMPORARY] Set true to log unsigned tx before signing. Remove when migration is validated. */
+const DEBUG_V0_MIGRATION = true;
-export async function deriveV0AddressFromMnemonic(
- mnemonic: string,
- passphrase = ''
-): Promise<{ sourceAddress: string }> {
+/**
+ * Discovery only: query v0 (Legacy) balance for a mnemonic. Use this to display balance
+ * before building a migration tx. Does not build a transaction.
+ */
+export async function queryV0Balance(mnemonic: string): Promise {
await ensureWasmInitialized();
- const derived = sdkDeriveV0Address(mnemonic, passphrase);
- return { sourceAddress: derived.sourceAddress };
+ const grpcEndpoint = await getEffectiveRpcEndpoint();
+ return sdkQueryV0Balance(mnemonic, grpcEndpoint);
}
-export async function queryV0BalanceFromMnemonic(
+/**
+ * Build v0 migration transaction (queries balance internally, then builds tx when target provided).
+ * Use for fee estimation and for the actual migration payload on the Funds screen.
+ */
+export async function buildV0MigrationTx(
mnemonic: string,
- grpcEndpoint = RPC_ENDPOINT
-): Promise {
+ targetV1Pkh?: string,
+ debug = false
+): Promise {
await ensureWasmInitialized();
- const discovery = await sdkQueryV0Balance(mnemonic, grpcEndpoint);
- const rawNotesCount = discovery.balance?.notes?.length ?? 0;
- const legacyCount = discovery.v0Notes.length;
- console.log('[V0 Migration] Discovery result:', {
- sourceAddress: discovery.sourceAddress,
- rawNotesFromRpc: rawNotesCount,
- legacyV0Notes: legacyCount,
- totalNicks: discovery.totalNicks,
- });
- if (legacyCount === 0 && rawNotesCount > 0) {
- const first = discovery.balance?.notes?.[0];
- const nv = first?.note?.note_version;
- const nvKeys = nv && typeof nv === 'object' ? Object.keys(nv) : [];
- console.warn('[V0 Migration] RPC returned', rawNotesCount, 'notes but none are Legacy (v0). Check note_version structure.');
- console.warn('[V0 Migration] First entry note_version keys:', nvKeys, 'sample:', nv ? JSON.stringify(nv).slice(0, 300) : 'n/a');
+ const grpcEndpoint = await getEffectiveRpcEndpoint();
+ const result = await sdkBuildV0MigrationTx(mnemonic, grpcEndpoint, targetV1Pkh, { debug });
+
+ if (debug) {
+ console.log('[V0 Migration] Result:', {
+ sourceAddress: result.sourceAddress,
+ rawNotesFromRpc: result.rawNotesFromRpc,
+ legacyV0Notes: result.v0Notes.length,
+ totalNicks: result.totalNicks,
+ smallestNoteNock: result.smallestNoteNock,
+ txId: result.txId,
+ });
}
- return {
- sourceAddress: discovery.sourceAddress,
- v0Notes: discovery.v0Notes,
- totalNicks: discovery.totalNicks,
- totalNock: Number(BigInt(discovery.totalNicks)) / NOCK_TO_NICKS,
- rawNotesFromRpc: rawNotesCount,
- };
-}
-export async function buildV0MigrationTransactionFromNotes(
- v0Notes: any[],
- targetV1Pkh: string,
- feePerWord = '32768'
-): Promise {
- await ensureWasmInitialized();
- const built = await sdkBuildFromV0Notes(v0Notes, targetV1Pkh, feePerWord, undefined, undefined, {
- singleNoteOnly: true,
- debug: true, // [TEMPORARY] Remove when migration is validated
- }) as { txId: string; fee: string; feeNock: number; migratedNicks: string; migratedNock: number; selectedNoteNicks: string; selectedNoteNock: number; signRawTxPayload: { rawTx: any; notes: any[]; spendConditions: any[] } };
- return {
- txId: built.txId,
- feeNicks: built.fee,
- feeNock: built.feeNock,
- migratedNicks: built.migratedNicks,
- migratedNock: built.migratedNock,
- selectedNoteNicks: built.selectedNoteNicks,
- selectedNoteNock: built.selectedNoteNock,
- signRawTxPayload: {
- rawTx: built.signRawTxPayload.rawTx,
- notes: built.signRawTxPayload.notes,
- spendConditions: built.signRawTxPayload.spendConditions,
- },
- };
+ return result;
}
-const CONFIRM_POLL_INTERVAL_MS = 3000;
-const CONFIRM_TIMEOUT_MS = 90_000;
-
-/** [TEMPORARY] Set true to log unsigned tx before signing. Remove when migration is validated. */
-const DEBUG_V0_MIGRATION = true;
-
/**
* Sign a v0 migration raw transaction with the given mnemonic (master key) and broadcast.
* Polls until the transaction is confirmed on-chain or timeout.
@@ -117,11 +66,11 @@ const DEBUG_V0_MIGRATION = true;
*/
export async function signAndBroadcastV0Migration(
mnemonic: string,
- signRawTxPayload: { rawTx: any; notes: any[]; spendConditions: any[] },
- grpcEndpoint = RPC_ENDPOINT,
+ signRawTxPayload: { rawTx: any; notes: any[]; spendConditions?: (any | null)[]; refundLock?: any },
options?: { debug?: boolean; skipBroadcast?: boolean }
): Promise<{ txId: string; confirmed: boolean; skipped?: boolean }> {
await ensureWasmInitialized();
+ const grpcEndpoint = await getEffectiveRpcEndpoint();
const masterKey = wasm.deriveMasterKeyFromMnemonic(mnemonic, '');
if (!masterKey.privateKey || masterKey.privateKey.byteLength !== 32) {
@@ -133,36 +82,45 @@ export async function signAndBroadcastV0Migration(
const skipBroadcast = options?.skipBroadcast ?? false;
try {
- const { rawTx, notes, spendConditions } = signRawTxPayload;
+ const { rawTx, notes } = signRawTxPayload;
if (debug) {
- console.log('[V0 Migration] Unsigned transaction (before signing):', {
+ const debugPayload = {
rawTx: { id: rawTx?.id, version: rawTx?.version, spendsCount: rawTx?.spends?.length ?? 0 },
notesCount: notes.length,
- spendConditionsCount: spendConditions.length,
+ spendConditionsCount: signRawTxPayload.spendConditions?.length ?? 0,
fullRawTx: rawTx,
- });
+ };
+ console.log('[V0 Migration] Unsigned transaction (before signing):', debugPayload);
+ return { txId: rawTx?.id ?? '', confirmed: false, skipped: true, ...debugPayload };
}
+ // alpha.6: fromTx(tx, notes, refund_lock, settings) - use refundLock from payload (v0 notes pass null for spendConditions)
+ const sc = signRawTxPayload.spendConditions;
+ const refundLock =
+ signRawTxPayload.refundLock ??
+ (sc && sc.length > 0 && sc[0] ? wasm.locky(sc[0]) : null);
let builder: ReturnType;
try {
builder = wasm.TxBuilder.fromTx(
rawTx,
notes,
- spendConditions,
- txEngineSettings()
+ refundLock,
+ wasm.txEngineSettingsV1BythosDefault()
);
} catch (e) {
console.error('[V0 Migration] TxBuilder.fromTx failed:', e);
throw e;
}
- const signingKeyBytes = new Uint8Array(masterKey.privateKey.slice(0, 32));
+ const privateKey = wasm.PrivateKey.fromBytes(masterKey.privateKey);
try {
- builder.sign(signingKeyBytes);
+ await builder.sign(privateKey);
} catch (e) {
console.error('[V0 Migration] builder.sign failed:', e);
throw e;
+ } finally {
+ privateKey.free();
}
try {
@@ -173,7 +131,7 @@ export async function signAndBroadcastV0Migration(
}
const signedTx = builder.build();
- const signedRawTx = wasm.nockchainTxToRaw(signedTx) as wasm.RawTxV1;
+ const signedRawTx = wasm.nockchainTxToRawTx(signedTx) as wasm.RawTxV1;
const protobuf = wasm.rawTxToProtobuf(signedRawTx);
if (debug) {
From a303830f35ccdff312879c1ac0fdb3e170a0773c Mon Sep 17 00:00:00 2001
From: Gohlub <62673775+Gohlub@users.noreply.github.com>
Date: Mon, 30 Mar 2026 10:55:30 -0400
Subject: [PATCH 07/12] add backport
---
.../popup/screens/V0MigrationFundsScreen.tsx | 19 +++-
extension/shared/v0-migration.ts | 97 +++++++++++++++----
2 files changed, 93 insertions(+), 23 deletions(-)
diff --git a/extension/popup/screens/V0MigrationFundsScreen.tsx b/extension/popup/screens/V0MigrationFundsScreen.tsx
index 5d6243a..860eed6 100644
--- a/extension/popup/screens/V0MigrationFundsScreen.tsx
+++ b/extension/popup/screens/V0MigrationFundsScreen.tsx
@@ -15,6 +15,9 @@ import { buildV0MigrationTx } from '../../shared/v0-migration';
export function V0MigrationFundsScreen() {
const { navigate, wallet, v0MigrationDraft, setV0MigrationDraft } = useStore();
const visibleAccounts = wallet.accounts.filter(account => !account.hidden);
+ const debugSpendAmount = v0MigrationDraft.migratedAmountNock;
+ const isDebugSingleNoteSpend =
+ debugSpendAmount != null && debugSpendAmount !== v0MigrationDraft.v0BalanceNock;
const [showWalletPicker, setShowWalletPicker] = useState(false);
const [buildError, setBuildError] = useState('');
const [errorType, setErrorType] = useState<'fee_too_low' | 'general' | null>(null);
@@ -61,7 +64,7 @@ export function V0MigrationFundsScreen() {
);
if (ac.signal.aborted) return;
const feeNock = result.feeNock;
- setV0MigrationDraft({ feeNock });
+ setV0MigrationDraft({ feeNock, migratedAmountNock: result.migratedNock });
if (feeNock != null) {
setFee(feeNock.toString());
setEditedFee(feeNock.toString());
@@ -76,7 +79,7 @@ export function V0MigrationFundsScreen() {
if (ac.signal.aborted) return;
setBuildError(err instanceof Error ? err.message : 'Failed to estimate fee');
setErrorType('general');
- setV0MigrationDraft({ feeNock: undefined });
+ setV0MigrationDraft({ feeNock: undefined, migratedAmountNock: undefined });
setFee('');
setEditedFee('');
setMinimumFee(null);
@@ -219,6 +222,18 @@ export function V0MigrationFundsScreen() {
+ {isDebugSingleNoteSpend && (
+
+ Debug note spend
+
+ {debugSpendAmount.toLocaleString('en-US')} NOCK
+
+
+ )}
+
{
+ let lastFee: string | undefined;
+ for (let i = 0; i < 3; i++) {
+ builder.recalcAndSetFee(false);
+ const nextFee = builder.curFee() as string;
+ await builder.sign(privateKey);
+ if (nextFee === lastFee) {
+ return nextFee;
+ }
+ lastFee = nextFee;
+ }
+ return builder.curFee() as string;
+}
+
/**
* Discovery only: query v0 (Legacy) balance for a mnemonic. Use this to display balance
* before building a migration tx. Does not build a transaction.
@@ -41,7 +78,35 @@ export async function buildV0MigrationTx(
): Promise
{
await ensureWasmInitialized();
const grpcEndpoint = await getEffectiveRpcEndpoint();
- const result = await sdkBuildV0MigrationTx(mnemonic, grpcEndpoint, targetV1Pkh, { debug });
+ let result = await sdkBuildV0MigrationTx(
+ mnemonic,
+ grpcEndpoint,
+ targetV1Pkh as Digest | undefined,
+ { debug }
+ );
+
+ if (result.signRawTxPayload) {
+ const masterKey = wasm.deriveMasterKeyFromMnemonic(mnemonic, '');
+ try {
+ if (!masterKey.privateKey || masterKey.privateKey.byteLength !== 32) {
+ throw new Error('Cannot derive signing key from mnemonic');
+ }
+ const privateKey = wasm.PrivateKey.fromBytes(masterKey.privateKey);
+ try {
+ const builder = buildMigrationBuilder(result.signRawTxPayload);
+ const feeNicks = await stabilizeMigrationFee(builder, privateKey);
+ result = {
+ ...result,
+ fee: feeNicks as BuildV0MigrationTxResult['fee'],
+ feeNock: Number(BigInt(feeNicks)) / NOCK_TO_NICKS,
+ };
+ } finally {
+ privateKey.free();
+ }
+ } finally {
+ masterKey.free();
+ }
+ }
if (debug) {
console.log('[V0 Migration] Result:', {
@@ -51,6 +116,8 @@ export async function buildV0MigrationTx(
totalNicks: result.totalNicks,
smallestNoteNock: result.smallestNoteNock,
txId: result.txId,
+ feeNock: result.feeNock,
+ sdkDebugUsesSingleSmallestNote: debug,
});
}
@@ -79,43 +146,31 @@ export async function signAndBroadcastV0Migration(
}
const debug = options?.debug ?? DEBUG_V0_MIGRATION;
- const skipBroadcast = options?.skipBroadcast ?? false;
+ const skipBroadcast = options?.skipBroadcast ?? debug;
try {
- const { rawTx, notes } = signRawTxPayload;
+ const { rawTx, notes, spendConditions, refundLock } = signRawTxPayload;
if (debug) {
- const debugPayload = {
+ console.log('[V0 Migration] Unsigned transaction (before signing):', {
rawTx: { id: rawTx?.id, version: rawTx?.version, spendsCount: rawTx?.spends?.length ?? 0 },
notesCount: notes.length,
spendConditionsCount: signRawTxPayload.spendConditions?.length ?? 0,
fullRawTx: rawTx,
- };
- console.log('[V0 Migration] Unsigned transaction (before signing):', debugPayload);
- return { txId: rawTx?.id ?? '', confirmed: false, skipped: true, ...debugPayload };
+ });
}
- // alpha.6: fromTx(tx, notes, refund_lock, settings) - use refundLock from payload (v0 notes pass null for spendConditions)
- const sc = signRawTxPayload.spendConditions;
- const refundLock =
- signRawTxPayload.refundLock ??
- (sc && sc.length > 0 && sc[0] ? wasm.locky(sc[0]) : null);
- let builder: ReturnType;
+ let builder: wasm.TxBuilder;
try {
- builder = wasm.TxBuilder.fromTx(
- rawTx,
- notes,
- refundLock,
- wasm.txEngineSettingsV1BythosDefault()
- );
+ builder = buildMigrationBuilder({ rawTx, notes, spendConditions, refundLock });
} catch (e) {
- console.error('[V0 Migration] TxBuilder.fromTx failed:', e);
+ console.error('[V0 Migration] Failed to reconstruct signer builder from notes:', e);
throw e;
}
const privateKey = wasm.PrivateKey.fromBytes(masterKey.privateKey);
try {
- await builder.sign(privateKey);
+ await stabilizeMigrationFee(builder, privateKey);
} catch (e) {
console.error('[V0 Migration] builder.sign failed:', e);
throw e;
From 1c40c283eea0c37bdc344b5c98e6c6fe1c201e4e Mon Sep 17 00:00:00 2001
From: Gohlub <62673775+Gohlub@users.noreply.github.com>
Date: Wed, 15 Apr 2026 01:08:38 -0400
Subject: [PATCH 08/12] take active tx engine settings, streamline logic
---
.../popup/screens/V0MigrationFundsScreen.tsx | 6 +-
.../popup/screens/V0MigrationReviewScreen.tsx | 6 +-
.../popup/screens/V0MigrationSetupScreen.tsx | 2 +-
extension/popup/store.ts | 19 ++--
extension/shared/v0-migration.ts | 94 ++++++++++---------
5 files changed, 65 insertions(+), 62 deletions(-)
diff --git a/extension/popup/screens/V0MigrationFundsScreen.tsx b/extension/popup/screens/V0MigrationFundsScreen.tsx
index 860eed6..e821170 100644
--- a/extension/popup/screens/V0MigrationFundsScreen.tsx
+++ b/extension/popup/screens/V0MigrationFundsScreen.tsx
@@ -156,14 +156,14 @@ export function V0MigrationFundsScreen() {
destinationWallet.address,
true
);
- if (!result.txId || !result.signRawTxPayload) {
+ if (!result.txId || !result.v0MigrationTxSignPayload) {
throw new Error('Failed to build migration transaction');
}
setV0MigrationDraft({
migratedAmountNock: result.migratedNock,
feeNock: result.feeNock,
- signRawTxPayload: result.signRawTxPayload,
+ v0MigrationTxSignPayload: result.v0MigrationTxSignPayload,
txId: result.txId,
});
navigate('v0-migration-review');
@@ -480,7 +480,7 @@ export function V0MigrationFundsScreen() {
onClick={() => {
setV0MigrationDraft({
destinationWalletIndex: account.index,
- signRawTxPayload: undefined,
+ v0MigrationTxSignPayload: undefined,
txId: undefined,
migratedAmountNock: undefined,
feeNock: undefined,
diff --git a/extension/popup/screens/V0MigrationReviewScreen.tsx b/extension/popup/screens/V0MigrationReviewScreen.tsx
index 0905fcd..19c1d13 100644
--- a/extension/popup/screens/V0MigrationReviewScreen.tsx
+++ b/extension/popup/screens/V0MigrationReviewScreen.tsx
@@ -16,19 +16,19 @@ export function V0MigrationReviewScreen() {
const amount = v0MigrationDraft.migratedAmountNock ?? v0MigrationDraft.v0BalanceNock;
const usdAmount = amount * priceUsd;
const canSend =
- Boolean(v0MigrationDraft.signRawTxPayload) &&
+ Boolean(v0MigrationDraft.v0MigrationTxSignPayload) &&
Boolean(v0MigrationDraft.v0Mnemonic) &&
!isSending;
async function handleSend(skipBroadcast = false) {
- if (!canSend || !v0MigrationDraft.v0Mnemonic || !v0MigrationDraft.signRawTxPayload) return;
+ if (!canSend || !v0MigrationDraft.v0Mnemonic || !v0MigrationDraft.v0MigrationTxSignPayload) return;
setSendError('');
setIsSending(true);
try {
const { txId, confirmed, skipped } = await signAndBroadcastV0Migration(
v0MigrationDraft.v0Mnemonic,
- v0MigrationDraft.signRawTxPayload,
+ v0MigrationDraft.v0MigrationTxSignPayload,
{ debug: true, skipBroadcast }
);
setV0MigrationDraft({
diff --git a/extension/popup/screens/V0MigrationSetupScreen.tsx b/extension/popup/screens/V0MigrationSetupScreen.tsx
index f8fc274..9746d6c 100644
--- a/extension/popup/screens/V0MigrationSetupScreen.tsx
+++ b/extension/popup/screens/V0MigrationSetupScreen.tsx
@@ -136,7 +136,7 @@ export function V0MigrationSetupScreen() {
migratedAmountNock: undefined,
feeNock: undefined,
keyfileName: undefined,
- signRawTxPayload: undefined,
+ v0MigrationTxSignPayload: undefined,
txId: undefined,
});
setWords(Array(WORD_COUNT).fill(''));
diff --git a/extension/popup/store.ts b/extension/popup/store.ts
index 1d1b996..efc78c0 100644
--- a/extension/popup/store.ts
+++ b/extension/popup/store.ts
@@ -16,6 +16,7 @@ import {
WalletTransaction,
} from '../shared/types';
import { send } from './utils/messaging';
+import type { V0MigrationTxSignPayload } from '@nockbox/iris-sdk';
/**
* All available screens in the wallet
@@ -117,11 +118,7 @@ interface AppStore {
sourceAddress?: string;
v0Mnemonic?: string; // Kept in memory only until sign+broadcast
v0Notes?: any[];
- signRawTxPayload?: {
- rawTx: any;
- notes: any[];
- spendConditions: any[];
- };
+ v0MigrationTxSignPayload?: V0MigrationTxSignPayload;
txId?: string;
v0TxConfirmed?: boolean;
v0TxSkipped?: boolean;
@@ -136,11 +133,7 @@ interface AppStore {
sourceAddress?: string;
v0Mnemonic?: string;
v0Notes?: any[];
- signRawTxPayload?: {
- rawTx: any;
- notes: any[];
- spendConditions: any[];
- };
+ v0MigrationTxSignPayload?: V0MigrationTxSignPayload;
txId?: string;
v0TxConfirmed?: boolean;
v0TxSkipped?: boolean;
@@ -233,12 +226,12 @@ export const useStore = create((set, get) => ({
sourceAddress: undefined,
sourcePkh: undefined,
v0Notes: undefined,
- signRawTxPayload: undefined,
+ v0MigrationTxSignPayload: undefined,
txId: undefined,
v0TxConfirmed: undefined,
v0TxSkipped: undefined,
},
- pendingConnectRequest: null,
+ pendingConnectRequest: null,
pendingSignRequest: null,
pendingSignRawTxRequest: null,
pendingTransactionRequest: null,
@@ -319,7 +312,7 @@ export const useStore = create((set, get) => ({
sourceAddress: undefined,
v0Mnemonic: undefined,
v0Notes: undefined,
- signRawTxPayload: undefined,
+ v0MigrationTxSignPayload: undefined,
txId: undefined,
v0TxConfirmed: undefined,
v0TxSkipped: undefined,
diff --git a/extension/shared/v0-migration.ts b/extension/shared/v0-migration.ts
index e42a47e..d15b5f7 100644
--- a/extension/shared/v0-migration.ts
+++ b/extension/shared/v0-migration.ts
@@ -3,12 +3,14 @@
*/
import { ensureWasmInitialized } from './wasm-utils';
-import { getEffectiveRpcEndpoint } from './rpc-config';
+import { getEffectiveRpcEndpoint, getTxEngineSettingsForHeight } from './rpc-config';
import {
buildV0MigrationTx as sdkBuildV0MigrationTx,
+ buildV0MigrationTxBuilderFromPayload,
queryV0Balance as sdkQueryV0Balance,
type BuildV0MigrationTxResult,
type V0BalanceResult,
+ type V0MigrationTxSignPayload,
} from '@nockbox/iris-sdk';
import type { Digest } from '@nockbox/iris-sdk/wasm';
import wasm from './sdk-wasm.js';
@@ -22,39 +24,32 @@ const NOCK_TO_NICKS = 65536;
/** [TEMPORARY] Set true to log unsigned tx before signing. Remove when migration is validated. */
const DEBUG_V0_MIGRATION = true;
-function buildMigrationBuilder(signRawTxPayload: {
- rawTx: any;
- notes: any[];
- spendConditions?: (any | null)[];
- refundLock?: any;
-}): wasm.TxBuilder {
- const { notes, spendConditions, refundLock } = signRawTxPayload;
- const builder = new wasm.TxBuilder(wasm.txEngineSettingsV1BythosDefault());
- for (let i = 0; i < notes.length; i++) {
- const spendBuilder = new wasm.SpendBuilder(
- notes[i] as wasm.Note,
- (spendConditions?.[i] ?? null) as wasm.Lock | null,
- null,
- (refundLock ?? null) as wasm.Digest | null
- );
- spendBuilder.computeRefund(false);
- builder.spend(spendBuilder);
- }
- return builder;
+async function migrationTxEngineSettings(grpcEndpoint: string): Promise {
+ const client = createBrowserClient(grpcEndpoint);
+ const blockHeight = await client.getCurrentBlockHeight();
+ return (await getTxEngineSettingsForHeight(blockHeight)) as wasm.TxEngineSettings;
}
-async function stabilizeMigrationFee(builder: wasm.TxBuilder, privateKey: wasm.PrivateKey): Promise {
- let lastFee: string | undefined;
- for (let i = 0; i < 3; i++) {
- builder.recalcAndSetFee(false);
- const nextFee = builder.curFee() as string;
- await builder.sign(privateKey);
- if (nextFee === lastFee) {
- return nextFee;
+function v0SourcePublicKeyFromMnemonic(mnemonic: string): wasm.PublicKey {
+ const masterKey = wasm.deriveMasterKeyFromMnemonic(mnemonic, '');
+ try {
+ const pk = wasm.publicKeyFromBeBytes(masterKey.publicKey);
+ if (!pk) {
+ throw new Error('Could not derive v0 public key from mnemonic');
}
- lastFee = nextFee;
+ return pk;
+ } finally {
+ masterKey.free();
}
- return builder.curFee() as string;
+}
+
+/**
+ * Same fee read pattern as `buildTransaction`: sign, validate, then `calcFee()` (post-signature).
+ */
+async function feeNicksAfterSign(builder: wasm.TxBuilder, privateKey: wasm.PrivateKey): Promise {
+ await builder.sign(privateKey);
+ builder.validate();
+ return String(builder.calcFee());
}
/**
@@ -64,7 +59,8 @@ async function stabilizeMigrationFee(builder: wasm.TxBuilder, privateKey: wasm.P
export async function queryV0Balance(mnemonic: string): Promise {
await ensureWasmInitialized();
const grpcEndpoint = await getEffectiveRpcEndpoint();
- return sdkQueryV0Balance(mnemonic, grpcEndpoint);
+ const sourcePublicKey = v0SourcePublicKeyFromMnemonic(mnemonic);
+ return sdkQueryV0Balance(sourcePublicKey, grpcEndpoint);
}
/**
@@ -78,14 +74,20 @@ export async function buildV0MigrationTx(
): Promise {
await ensureWasmInitialized();
const grpcEndpoint = await getEffectiveRpcEndpoint();
+ const txEngineSettings = await migrationTxEngineSettings(grpcEndpoint);
+ const sourcePublicKey = v0SourcePublicKeyFromMnemonic(mnemonic);
+
let result = await sdkBuildV0MigrationTx(
- mnemonic,
+ sourcePublicKey,
grpcEndpoint,
targetV1Pkh as Digest | undefined,
- { debug }
+ {
+ txEngineSettings,
+ maxNotes: debug ? 1 : undefined,
+ }
);
- if (result.signRawTxPayload) {
+ if (result.v0MigrationTxSignPayload) {
const masterKey = wasm.deriveMasterKeyFromMnemonic(mnemonic, '');
try {
if (!masterKey.privateKey || masterKey.privateKey.byteLength !== 32) {
@@ -93,8 +95,11 @@ export async function buildV0MigrationTx(
}
const privateKey = wasm.PrivateKey.fromBytes(masterKey.privateKey);
try {
- const builder = buildMigrationBuilder(result.signRawTxPayload);
- const feeNicks = await stabilizeMigrationFee(builder, privateKey);
+ const builder = buildV0MigrationTxBuilderFromPayload(
+ result.v0MigrationTxSignPayload,
+ txEngineSettings
+ );
+ const feeNicks = await feeNicksAfterSign(builder, privateKey);
result = {
...result,
fee: feeNicks as BuildV0MigrationTxResult['fee'],
@@ -133,11 +138,12 @@ export async function buildV0MigrationTx(
*/
export async function signAndBroadcastV0Migration(
mnemonic: string,
- signRawTxPayload: { rawTx: any; notes: any[]; spendConditions?: (any | null)[]; refundLock?: any },
+ payload: V0MigrationTxSignPayload,
options?: { debug?: boolean; skipBroadcast?: boolean }
): Promise<{ txId: string; confirmed: boolean; skipped?: boolean }> {
await ensureWasmInitialized();
const grpcEndpoint = await getEffectiveRpcEndpoint();
+ const txEngineSettings = await migrationTxEngineSettings(grpcEndpoint);
const masterKey = wasm.deriveMasterKeyFromMnemonic(mnemonic, '');
if (!masterKey.privateKey || masterKey.privateKey.byteLength !== 32) {
@@ -149,20 +155,24 @@ export async function signAndBroadcastV0Migration(
const skipBroadcast = options?.skipBroadcast ?? debug;
try {
- const { rawTx, notes, spendConditions, refundLock } = signRawTxPayload;
+ const { rawTx, notes, spendConditions, refundLock } = payload;
if (debug) {
+ const dbgTx = rawTx as { id?: string; version?: number; spends?: unknown[] };
console.log('[V0 Migration] Unsigned transaction (before signing):', {
- rawTx: { id: rawTx?.id, version: rawTx?.version, spendsCount: rawTx?.spends?.length ?? 0 },
+ rawTx: { id: dbgTx.id, version: dbgTx.version, spendsCount: dbgTx.spends?.length ?? 0 },
notesCount: notes.length,
- spendConditionsCount: signRawTxPayload.spendConditions?.length ?? 0,
+ spendConditionsCount: spendConditions?.length ?? 0,
fullRawTx: rawTx,
});
}
let builder: wasm.TxBuilder;
try {
- builder = buildMigrationBuilder({ rawTx, notes, spendConditions, refundLock });
+ builder = buildV0MigrationTxBuilderFromPayload(
+ { rawTx, notes, spendConditions, refundLock },
+ txEngineSettings
+ );
} catch (e) {
console.error('[V0 Migration] Failed to reconstruct signer builder from notes:', e);
throw e;
@@ -170,7 +180,7 @@ export async function signAndBroadcastV0Migration(
const privateKey = wasm.PrivateKey.fromBytes(masterKey.privateKey);
try {
- await stabilizeMigrationFee(builder, privateKey);
+ await builder.sign(privateKey);
} catch (e) {
console.error('[V0 Migration] builder.sign failed:', e);
throw e;
From 5752c5ddf2086003fd35b7cd95a18ac8aa87d026 Mon Sep 17 00:00:00 2001
From: Gohlub <62673775+Gohlub@users.noreply.github.com>
Date: Wed, 15 Apr 2026 01:31:38 -0400
Subject: [PATCH 09/12] get rid of redudant skip broadcast and debug argument,
if debug is not it skips broadcast by default
---
.../popup/screens/V0MigrationReviewScreen.tsx | 17 ++++-------------
extension/shared/v0-migration.ts | 13 +++----------
2 files changed, 7 insertions(+), 23 deletions(-)
diff --git a/extension/popup/screens/V0MigrationReviewScreen.tsx b/extension/popup/screens/V0MigrationReviewScreen.tsx
index 19c1d13..0b4034a 100644
--- a/extension/popup/screens/V0MigrationReviewScreen.tsx
+++ b/extension/popup/screens/V0MigrationReviewScreen.tsx
@@ -20,7 +20,7 @@ export function V0MigrationReviewScreen() {
Boolean(v0MigrationDraft.v0Mnemonic) &&
!isSending;
- async function handleSend(skipBroadcast = false) {
+ async function handleSend() {
if (!canSend || !v0MigrationDraft.v0Mnemonic || !v0MigrationDraft.v0MigrationTxSignPayload) return;
setSendError('');
@@ -29,7 +29,8 @@ export function V0MigrationReviewScreen() {
const { txId, confirmed, skipped } = await signAndBroadcastV0Migration(
v0MigrationDraft.v0Mnemonic,
v0MigrationDraft.v0MigrationTxSignPayload,
- { debug: true, skipBroadcast }
+ // TEMP: sign + log only, no broadcast — remove before shipping
+ { debug: true }
);
setV0MigrationDraft({
v0Mnemonic: undefined,
@@ -133,23 +134,13 @@ export function V0MigrationReviewScreen() {
handleSend(false)}
+ onClick={() => handleSend()}
disabled={!canSend}
className="flex-1 h-12 rounded-[14px] text-[16px] font-medium disabled:opacity-50 disabled:cursor-not-allowed"
style={{ backgroundColor: 'var(--color-primary)', color: '#000' }}
>
{isSending ? 'Sending...' : 'Send'}
- handleSend(true)}
- disabled={!canSend}
- title="Sign and log to console, but do not broadcast"
- className="h-12 px-3 rounded-[14px] text-[12px] font-medium disabled:opacity-50 disabled:cursor-not-allowed"
- style={{ backgroundColor: 'var(--color-surface-800)', color: 'var(--color-text-muted)' }}
- >
- Debug
-
diff --git a/extension/shared/v0-migration.ts b/extension/shared/v0-migration.ts
index d15b5f7..fefe7d7 100644
--- a/extension/shared/v0-migration.ts
+++ b/extension/shared/v0-migration.ts
@@ -21,8 +21,6 @@ export type { V0BalanceResult };
const CONFIRM_POLL_INTERVAL_MS = 3000;
const CONFIRM_TIMEOUT_MS = 90_000;
const NOCK_TO_NICKS = 65536;
-/** [TEMPORARY] Set true to log unsigned tx before signing. Remove when migration is validated. */
-const DEBUG_V0_MIGRATION = true;
async function migrationTxEngineSettings(grpcEndpoint: string): Promise