Aegis Wallet 0.4.4 → 0.6.1:
- Vault lifecycle from the wallet gate. The locked / not-yet-created states
now show a master-password form (with optional BIP39 mnemonic on setup)
instead of redirecting users to Settings › Passwords. New
api.vault.lifecycle {status, setup, unlock, lock} in addons-host, gated by
the existing "vault-derive" capability. api.openSettings(section) also
added; settings.html honours a #section hash on open.
- Imported BCH wallets (design M.1a, read-only). Paste a mnemonic + BIP44
path or a WIF; the cashaddr is derived in the add-on, the signer material
goes to a separate wallet-imports.enc via api.vault.imports {list, add,
remove, signer}. Argus password-vault gains createImports / unlockImports /
saveImports with its own KDF salt so the imports key is disjoint from the
passwords key. lib/chain-bch-imported.js is a single-address Electrum
adapter; spend support is deferred to M.1b.
- Opt-in USD prices via CoinGecko (lib/prices.js), off by default, persisted
in add-on storage. Fiat lines under balances, in the wallet picker, and a
portfolio total when 2+ wallets are open. Settings tab is now reachable
while the vault is locked so the toggle is always available.
- WizardConnect wallet-side pairing for BCH wallets (lib/wc.js, lib/wc-sign.js).
@wizardconnect/{core,wallet} are loaded dynamically via api.import to stay
on the right side of LGPL §4d. Sign requests go through approvalModal and
are restricted to P2PKH inputs with SIGHASH_ALL|FORKID|UTXOS.
- DGB adapter load is now soft-fail: when Aegis runs from userData/addons the
bundled ESM can't resolve peer deps, so DGB becomes unavailable instead of
taking the whole add-on down.
95 lines
4 KiB
JavaScript
95 lines
4 KiB
JavaScript
// WizardConnect transaction signing for Aegis.
|
|
//
|
|
// The dapp hands us a full BCH transaction plus its source outputs. Per the
|
|
// WC protocol, we must sign every input with SIGHASH_ALL | FORKID | UTXOS.
|
|
// Any other sighash flag combination MUST be rejected (protocol/security).
|
|
//
|
|
// This module supports P2PKH inputs only. Contract inputs (a source output
|
|
// carrying a `contract` field) are rejected with a clear error — they need
|
|
// script-aware signing that Aegis's BCH runtime doesn't do today.
|
|
|
|
// SIGHASH byte required for this protocol: SIGHASH_ALL | SIGHASH_FORKID | SIGHASH_UTXOS
|
|
// = 0x01 | 0x40 | 0x20 = 0x61.
|
|
const REQUIRED_SIGHASH = 0x61;
|
|
|
|
function toHex(u8) { let s = ""; for (let i = 0; i < u8.length; i++) s += u8[i].toString(16).padStart(2, "0"); return s; }
|
|
function fromHex(h) {
|
|
const s = String(h || "").replace(/^0x/i, "");
|
|
const out = new Uint8Array(s.length / 2);
|
|
for (let i = 0; i < out.length; i++) out[i] = parseInt(s.substr(i * 2, 2), 16);
|
|
return out;
|
|
}
|
|
|
|
function ensureTransaction(txOrHex, libauth) {
|
|
if (typeof txOrHex === "string") {
|
|
const dec = libauth.decodeTransactionCommon
|
|
? libauth.decodeTransactionCommon(fromHex(txOrHex))
|
|
: libauth.decodeTransaction(fromHex(txOrHex));
|
|
if (typeof dec === "string") throw new Error(`wc-sign: bad tx hex — ${dec}`);
|
|
return dec;
|
|
}
|
|
return txOrHex;
|
|
}
|
|
|
|
async function signTx({ request, account, branches, libauth, secp256k1 }) {
|
|
const {
|
|
generateSigningSerializationBCH,
|
|
hash256, encodeTransaction,
|
|
} = libauth;
|
|
|
|
const tx = ensureTransaction(request.transaction, libauth);
|
|
const sourceOutputs = (request.sourceOutputs || []).map((o, i) => {
|
|
if (o.contract) throw new Error(`wc-sign: input ${i} spends a contract — unsupported`);
|
|
return {
|
|
lockingBytecode: o.lockingBytecode instanceof Uint8Array ? o.lockingBytecode : fromHex(o.lockingBytecode),
|
|
valueSatoshis: typeof o.valueSatoshis === "bigint" ? o.valueSatoshis : BigInt(o.valueSatoshis),
|
|
};
|
|
});
|
|
if (sourceOutputs.length !== tx.inputs.length) {
|
|
throw new Error(`wc-sign: sourceOutputs (${sourceOutputs.length}) ≠ inputs (${tx.inputs.length})`);
|
|
}
|
|
|
|
const inputPathMap = new Map(); // inputIndex -> { branch, addressIndex }
|
|
for (const [inputIndex, pathName, addressIndex] of (request.inputPaths || [])) {
|
|
inputPathMap.set(Number(inputIndex), { pathName: String(pathName), addressIndex: Number(addressIndex) });
|
|
}
|
|
|
|
const signedInputs = tx.inputs.map((inp, i) => ({ ...inp }));
|
|
|
|
for (let i = 0; i < tx.inputs.length; i++) {
|
|
const hint = inputPathMap.get(i);
|
|
if (!hint) throw new Error(`wc-sign: no path for input ${i}`);
|
|
const branch = branches[hint.pathName];
|
|
if (!branch) throw new Error(`wc-sign: unknown path "${hint.pathName}"`);
|
|
const node = branch.deriveChild(hint.addressIndex);
|
|
|
|
const preimage = generateSigningSerializationBCH({
|
|
inputIndex: i,
|
|
signingSerializationType: new Uint8Array([REQUIRED_SIGHASH]),
|
|
sourceOutputs,
|
|
transaction: { ...tx, inputs: signedInputs },
|
|
});
|
|
const digest = hash256(preimage);
|
|
const sig = secp256k1.sign(digest, node.privateKey, { prehash: false, lowS: true, format: "der" });
|
|
// signature || sighashType byte
|
|
const sigWithHash = new Uint8Array(sig.length + 1);
|
|
sigWithHash.set(sig, 0); sigWithHash[sig.length] = REQUIRED_SIGHASH;
|
|
|
|
// P2PKH unlocking: <sig+hashtype> <pubkey>
|
|
const pushSig = new Uint8Array(1 + sigWithHash.length);
|
|
pushSig[0] = sigWithHash.length;
|
|
pushSig.set(sigWithHash, 1);
|
|
const pushPk = new Uint8Array(1 + node.publicKey.length);
|
|
pushPk[0] = node.publicKey.length;
|
|
pushPk.set(node.publicKey, 1);
|
|
|
|
const unlocking = new Uint8Array(pushSig.length + pushPk.length);
|
|
unlocking.set(pushSig, 0); unlocking.set(pushPk, pushSig.length);
|
|
signedInputs[i].unlockingBytecode = unlocking;
|
|
}
|
|
|
|
const encoded = encodeTransaction({ ...tx, inputs: signedInputs });
|
|
return { signedTransaction: toHex(encoded) };
|
|
}
|
|
|
|
module.exports = { signTx, REQUIRED_SIGHASH };
|