commit 5366c29c04720a97d6f48b6f20d43fadfd757a59
parent 8c7f36d53434a15272933e380610f5947b0e534d
Author: Joris Hartog <jorishartog@hotmail.com>
Date: Tue, 8 Sep 2026 21:58:31 +0200
fix(wallet): explain oversized transactions
Diffstat:
3 files changed, 46 insertions(+), 10 deletions(-)
diff --git a/tests/lightweight-wallet.test.cjs b/tests/lightweight-wallet.test.cjs
@@ -41,3 +41,26 @@ test('refuses to sign with a watch-only wallet', async () => {
/watch-only wallet cannot sign/,
);
});
+
+test('explains oversized transactions before submission', () => {
+ const signature = 'aa'.repeat(64);
+ const transaction = {
+ kind: 'transfer',
+ inputs: Array.from({ length: 205 }, (_, index) => ({
+ outpoint: { txid: 'bb'.repeat(32), index },
+ owner: 'cc'.repeat(32),
+ signature,
+ })),
+ outputs: [
+ { address: 'dd'.repeat(32), amount: 1 },
+ { address: 'cc'.repeat(32), amount: 1 },
+ ],
+ fee: 1,
+ signature,
+ };
+
+ assert.throws(
+ () => core.ensureTransactionBodySize(transaction),
+ /205 inputs, 65 KiB; maximum 64 KiB.*smaller amount.*consolidate/s,
+ );
+});
diff --git a/wallet/app.js b/wallet/app.js
@@ -30,7 +30,7 @@ function toast(message, error = false) {
toastElement.textContent = message;
toastElement.className = `toast show${error ? " error" : ""}`;
window.clearTimeout(toastElement.timeout);
- toastElement.timeout = window.setTimeout(() => { toastElement.className = "toast"; }, 3000);
+ toastElement.timeout = window.setTimeout(() => { toastElement.className = "toast"; }, error ? 7000 : 3000);
}
function readJson(key) {
diff --git a/wallet/wallet-core.js b/wallet/wallet-core.js
@@ -4,6 +4,7 @@ export const API_BASE = "https://iuna.jhx.app/v1";
export const STORAGE_KEY = "iuna.wallets.v2";
export const LEGACY_STORAGE_KEY = "iuna.wallet.v1";
export const MICRO_IUNA = 1_000_000n;
+export const MAX_TRANSACTION_BODY_BYTES = 64 * 1024;
export function emptyWalletStore() {
return { version: 2, activeId: null, wallets: [] };
@@ -50,6 +51,16 @@ export function removeWallet(store, id) {
return { version: 2, activeId: wallets[0]?.id || null, wallets };
}
+export function ensureTransactionBodySize(transaction) {
+ const bodyBytes = encoder.encode(JSON.stringify(transaction)).byteLength;
+ if (bodyBytes > MAX_TRANSACTION_BODY_BYTES) {
+ const inputCount = transaction.inputs?.length || 0;
+ const bodyKiB = Math.ceil(bodyBytes / 1024);
+ throw new Error(`Transaction is too large (${inputCount} inputs, ${bodyKiB} KiB; maximum 64 KiB). Try a smaller amount or consolidate this wallet’s UTXOs first.`);
+ }
+ return bodyBytes;
+}
+
export function bytesToHex(bytes) {
return Array.from(bytes, (byte) => byte.toString(16).padStart(2, "0")).join("");
}
@@ -304,16 +315,15 @@ export async function buildSignedTransfer({ wallet, status, utxos, recipientAddr
? transferSigningBytes(status, inputs, outputs, fee)
: encoder.encode(legacyTransferPayload(inputs, outputs, fee));
const signature = bytesToHex(new Uint8Array(await crypto.subtle.sign("Ed25519", wallet.privateKey, signingPayload)));
- return {
- transaction: {
- kind: "transfer",
- inputs: inputs.map((input) => ({ ...input, signature })),
- outputs: outputs.map((output) => ({ ...output, amount: Number(output.amount) })),
- fee: Number(fee),
- signature,
- },
- fee,
+ const transaction = {
+ kind: "transfer",
+ inputs: inputs.map((input) => ({ ...input, signature })),
+ outputs: outputs.map((output) => ({ ...output, amount: Number(output.amount) })),
+ fee: Number(fee),
+ signature,
};
+ ensureTransactionBodySize(transaction);
+ return { transaction, fee };
}
export function parseIuna(value) {
@@ -353,6 +363,9 @@ export async function api(path, options = {}) {
}
let body;
try { body = await response.json(); } catch { body = {}; }
+ if (response.status === 413 && path === "/transactions" && requestOptions.method === "POST") {
+ throw new Error("Transaction is too large for the public endpoint. Try a smaller amount or consolidate this wallet’s UTXOs first.");
+ }
if (!response.ok) throw new Error(body.error || `Endpoint returned status ${response.status}`);
return body;
}