Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ build configuration, and native integrations.

## Wallet development

Rust workspaces pin the shared `tx-types` 0.2 transaction core and its matching
noun runtime to one Git revision. `packages/wallet-engine` and `nockster-esp`
call the same `tx_types::planner`; their adapters provide RPC note data, wallet envelopes,
and software or hardware signatures. Composer inputs include the actual
`note_data` map and an explicit candidate height for time-locked inputs.

Use Node and npm versions matching `package.json`. Install dependencies from
this directory; npm workspaces share the root lockfile. The Rust engine pins
its toolchain in `packages/wallet-engine/rust-toolchain.toml`; the desktop
Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "nockster-desktop"
version = "1.2.0"
version = "1.3.0"
edition = "2021"
rust-version = "1.90"

Expand Down
2 changes: 1 addition & 1 deletion apps/desktop/src-tauri/tauri.conf.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"$schema": "https://schema.tauri.app/config/2",
"productName": "Nockster",
"version": "1.2.0",
"version": "1.3.0",
"identifier": "io.swps.nockster.desktop",
"build": {
"beforeDevCommand": "npm run dev:web",
Expand Down
31 changes: 26 additions & 5 deletions apps/desktop/src/transactions/Composer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,8 @@
commitments: '',
refundAddress: '',
refundHeight: '',
branch: 'claim'
branch: 'claim',
preimages: ''
};
let outputs = [newOutput(1)];
let nextOutputId = 2;
Expand Down Expand Up @@ -100,7 +101,7 @@
let selectionTimer: ReturnType<typeof setTimeout> | undefined;
const composerWorker = createComposerWorker();
let height = '';
let manual = { first: '', last: '', origin: '', amount: '' };
let manual = { first: '', last: '', origin: '', amount: '', noteData: '{}' };
let notesLoading = false;
let building = false;
let error = '';
Expand Down Expand Up @@ -212,8 +213,9 @@
if (!alive || request !== noteRequest) return;
notes = largestNotesFirst(
loaded
.filter(note => Number(note.version) === 1)
.filter(note => note.version === '1')
.map(note => ({
note_data: note.noteData,
name_first: note.firstName,
name_last: note.lastName,
origin_page: note.originPage,
Expand All @@ -235,6 +237,7 @@
try {
const wasm = await hardwareCrypto();
const note: Note = {
note_data: JSON.parse(manual.noteData),
name_first: manual.first.trim(),
name_last: manual.last.trim(),
origin_page: wholeNumber(manual.origin, 'Origin block height'),
Expand All @@ -248,7 +251,7 @@
throw new Error('This note is reserved by a pending transaction.');
notes = largestNotesFirst([...notes, note]);
if (selectionMode === 'manual') selected = new Set([...selected, noteId(note)]);
manual = { first: '', last: '', origin: '', amount: '' };
manual = { first: '', last: '', origin: '', amount: '', noteData: '{}' };
} catch (failure) {
noteError = failure instanceof Error ? failure.message : String(failure);
}
Expand Down Expand Up @@ -291,6 +294,7 @@
const input: ComposeInput = {
source_pkh: sourceAddress,
notes: inputNotes,
manual_inputs: selectionMode === 'manual',
outputs: [],
current_height: wholeNumber(height, 'Current block height', 1)
};
Expand All @@ -309,7 +313,12 @@
source.refundHeight,
valid
),
spend_branch: source.branch === 'claim' ? 0 : 1
spend_branch: source.branch === 'claim' ? 0 : 1,
preimages: source.preimages
.trim()
.split(/\s+/)
.filter(Boolean)
.map(value => Array.from(atob(value), ch => ch.charCodeAt(0)))
};
input.outputs = outputs.map((output, index) => {
try {
Expand Down Expand Up @@ -499,6 +508,12 @@
<label for="htlc-claim"
>Claim address<input id="htlc-claim" bind:value={source.claimAddress} /></label
>
<label for="htlc-preimages"
>Claim preimages (base64 JAM, one per line)<textarea
id="htlc-preimages"
bind:value={source.preimages}
/></label
>
<label for="htlc-commitments"
>Claim preimage commitments<textarea
id="htlc-commitments"
Expand Down Expand Up @@ -566,6 +581,12 @@
{#if noteError}<p class="transaction-error" role="alert">{noteError}</p>{/if}
<details>
<summary>Add an input note manually</summary>
<label for="note-data"
>On-chain note data (JSON; {'{}'} for an empty map)<textarea
id="note-data"
bind:value={manual.noteData}
/></label
>
<label for="note-first"
>First note hash<input id="note-first" bind:value={manual.first} /></label
>
Expand Down
9 changes: 8 additions & 1 deletion apps/desktop/src/transactions/composer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export type LockForm = {
};
export type OutputForm = LockForm & { id: number; amount: string };
export type Note = {
note_data: Record<string, unknown>;
name_first: string;
name_last: string;
origin_page: number;
Expand All @@ -45,6 +46,7 @@ export type SourceForm = {
refundAddress: string;
refundHeight: string;
branch: 'claim' | 'refund';
preimages: string;
};
export type Output = {
recipient: Recipient;
Expand All @@ -58,10 +60,11 @@ export type Output = {
export type ComposeInput = {
source_pkh: string;
source_multisig?: { m: number; pkhs: string[] };
source_or_lock?: { branches: LockBranch[]; spend_branch: number };
source_or_lock?: { branches: LockBranch[]; spend_branch: number; preimages: number[][] };
notes: Note[];
outputs: Output[];
current_height: number;
manual_inputs?: boolean;
};
export type ComposeSummary = {
total_fees: number;
Expand Down Expand Up @@ -182,6 +185,10 @@ export function validateNotes(notes: Note[], valid: AddressValidator): Note[] {
for (const note of notes) {
address(note.name_first, 'first note hash', valid);
address(note.name_last, 'last note hash', valid);
if (!note.note_data || typeof note.note_data !== 'object' || Array.isArray(note.note_data))
throw new Error(
'Input note data is missing. Refresh the notes or supply the on-chain note data.'
);
if (note.version !== 1) throw new Error('Select V1 notes for this transaction.');
if (
!Number.isSafeInteger(note.assets) ||
Expand Down
37 changes: 10 additions & 27 deletions apps/desktop/src/transactions/composer.worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,39 +2,22 @@ import init, {
compose_tx_v1_min_inputs,
is_valid_pkh
} from '../../../../nockster-esp/crates/nockster-wasm/pkg/nockster_wasm.js';
import { largestNotesFirst, notesForAmount } from './note-selection';
import { validateNotes, type ComposeInput, type ComposeSummary } from './composer';

self.onmessage = async (event: MessageEvent<ComposeInput>) => {
try {
const input = event.data;
const notes = largestNotesFirst(input.notes);
const target = input.outputs.reduce((sum, output) => sum + BigInt(output.amount), 0n);
if (notes.reduce((sum, note) => sum + BigInt(note.assets), 0n) <= target)
throw new Error('Insufficient funds to cover requested outputs and fee.');
await init();
let count = notesForAmount(notes, target + 1n).length;
while (count > 0) {
try {
const result = compose_tx_v1_min_inputs({
...input,
notes: validateNotes(notes.slice(0, count), is_valid_pkh)
});
try {
const bytes = result.wallet_jam;
const summary: ComposeSummary = JSON.parse(result.summary_json);
self.postMessage({ bytes, summary, txId: result.tx_id });
return;
} finally {
result.free();
}
} catch (failure) {
const message = failure instanceof Error ? failure.message : String(failure);
if (!message.includes('insufficient funds') || count === notes.length) throw failure;
count = Math.min(notes.length, Math.max(count + 1, count * 2));
}
const input = event.data;
const result = compose_tx_v1_min_inputs({
...input,
notes: validateNotes(input.notes, is_valid_pkh)
});
try {
const summary: ComposeSummary = JSON.parse(result.summary_json);
self.postMessage({ bytes: result.wallet_jam, summary, txId: result.tx_id });
} finally {
result.free();
}
throw new Error('No spendable notes are available.');
} catch (failure) {
self.postMessage({ error: failure instanceof Error ? failure.message : String(failure) });
}
Expand Down
4 changes: 2 additions & 2 deletions apps/mobile/ios/App/App.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -327,7 +327,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.3.0;
OTHER_SWIFT_FLAGS = "$(inherited) \"-D\" \"COCOAPODS\" \"-DDEBUG\"";
PRODUCT_BUNDLE_IDENTIFIER = io.swps.nockster;
PRODUCT_NAME = "$(TARGET_NAME)";
Expand All @@ -349,7 +349,7 @@
"$(inherited)",
"@executable_path/Frameworks",
);
MARKETING_VERSION = 1.2.0;
MARKETING_VERSION = 1.3.0;
PRODUCT_BUNDLE_IDENTIFIER = io.swps.nockster;
PRODUCT_NAME = "$(TARGET_NAME)";
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "";
Expand Down
Loading
Loading