// Copyright 2024-2026 Tymofii Pidlisnyi. Apache-2.0 license. See LICENSE.
// ══════════════════════════════════════════════════════════════════
// Wallet Binding — bind / unbind / verify functions
// ══════════════════════════════════════════════════════════════════
import { sign, verify } from '../../crypto/keys.js';
import { canonicalize } from '../../core/canonical.js';
/**
 * Build the canonical payload that the passport private key signs to
 * attest a binding. Verifiers reconstruct this exact payload to check
 * the binding_signature against the passport's public key.
 */
const BASE58_ALPHABET = /^[1-9A-HJ-NP-Za-km-z]+$/;
/**
 * Validate a Solana address: base58-encoded, 32-44 characters (ed25519 pubkey range).
 * Throws with a clear error on mismatch. Other chains are not validated here.
 */
function validateSolanaAddress(address) {
    if (address.length < 32 || address.length > 44) {
        throw new Error(`bindWallet: invalid Solana address — expected 32-44 base58 chars, got ${address.length}`);
    }
    if (!BASE58_ALPHABET.test(address)) {
        throw new Error('bindWallet: invalid Solana address — not valid base58 (0, O, I, l forbidden)');
    }
}
function bindingPayload(opts) {
    return canonicalize({
        passport_id: opts.passport_id,
        chain: opts.chain,
        address: opts.address,
        bound_at: opts.bound_at,
    });
}
function unbindPayload(opts) {
    return canonicalize({
        passport_id: opts.passport_id,
        chain: opts.chain,
        address: opts.address,
        unbound_at: opts.unbound_at,
        event: 'unbind',
    });
}
/**
 * Bind an external wallet to a passport. Produces a new SignedPassport with
 * the BoundWallet appended to bound_wallets and the entire passport re-signed
 * by the agent's private key.
 *
 * The binding_signature on the BoundWallet is independently verifiable: a
 * third party with only the passport's public key can call verify(payload,
 * binding_signature, publicKey) without holding the passport object itself.
 */
export function bindWallet(opts) {
    if (!opts.address || typeof opts.address !== 'string') {
        throw new Error('bindWallet: address must be a non-empty string');
    }
    if (!opts.chain || typeof opts.chain !== 'string') {
        throw new Error('bindWallet: chain must be a non-empty string');
    }
    if (opts.chain === 'solana') {
        validateSolanaAddress(opts.address);
    }
    const bound_at = opts.boundAt ?? new Date().toISOString();
    const payload = bindingPayload({
        passport_id: opts.passport.passport.agentId,
        chain: opts.chain,
        address: opts.address,
        bound_at,
    });
    let binding_signature;
    try {
        binding_signature = sign(payload, opts.privateKey);
    }
    catch (e) {
        throw new Error(`bindWallet: failed to sign binding payload — ${e?.message || String(e)}`);
    }
    // Sanity check: the produced signature must verify against the passport's
    // public key. If the caller passed a wrong private key, surface that here
    // rather than producing a passport with an unverifiable binding.
    if (!verify(payload, binding_signature, opts.passport.passport.publicKey)) {
        throw new Error('bindWallet: binding signature does not verify against passport public key — wrong private key?');
    }
    const bound = {
        chain: opts.chain,
        address: opts.address,
        bound_at,
        binding_signature,
        ...(opts.verificationChallenge ? { verification_challenge: opts.verificationChallenge } : {}),
    };
    const existing = opts.passport.passport.bound_wallets ?? [];
    const updatedPassport = {
        ...opts.passport.passport,
        bound_wallets: [...existing, bound],
    };
    // Re-sign the entire passport so verifyPassport() still passes for the new
    // shape. The binding_signature alone is enough to prove the binding to an
    // external verifier; the re-sign keeps the passport itself self-consistent.
    const passportSignature = sign(canonicalize(updatedPassport), opts.privateKey);
    return {
        ...opts.passport,
        passport: updatedPassport,
        signature: passportSignature,
        signedAt: new Date().toISOString(),
    };
}
/**
 * Unbind a wallet from a passport. Returns:
 *  - passport: a new SignedPassport with the wallet removed from bound_wallets
 *  - unbindEvent: a separately signed UnbindEvent that callers can persist
 *    to preserve the bind/unbind history outside the passport itself.
 *
 * Throws if the (chain, address) is not currently bound.
 */
export function unbindWallet(opts) {
    const existing = opts.passport.passport.bound_wallets ?? [];
    const idx = existing.findIndex((w) => w.chain === opts.chain && w.address === opts.address);
    if (idx === -1) {
        throw new Error(`unbindWallet: no bound wallet matches chain="${opts.chain}" address="${opts.address}"`);
    }
    const unbound_at = opts.unboundAt ?? new Date().toISOString();
    const payload = unbindPayload({
        passport_id: opts.passport.passport.agentId,
        chain: opts.chain,
        address: opts.address,
        unbound_at,
    });
    let unbind_signature;
    try {
        unbind_signature = sign(payload, opts.privateKey);
    }
    catch (e) {
        throw new Error(`unbindWallet: failed to sign unbind payload — ${e?.message || String(e)}`);
    }
    if (!verify(payload, unbind_signature, opts.passport.passport.publicKey)) {
        throw new Error('unbindWallet: unbind signature does not verify against passport public key — wrong private key?');
    }
    const updatedWallets = existing.slice();
    updatedWallets.splice(idx, 1);
    const updatedPassport = {
        ...opts.passport.passport,
        bound_wallets: updatedWallets,
    };
    const passportSignature = sign(canonicalize(updatedPassport), opts.privateKey);
    const unbindEvent = {
        passport_id: opts.passport.passport.agentId,
        chain: opts.chain,
        address: opts.address,
        unbound_at,
        unbind_signature,
    };
    return {
        passport: {
            ...opts.passport,
            passport: updatedPassport,
            signature: passportSignature,
            signedAt: new Date().toISOString(),
        },
        unbindEvent,
    };
}
export function verifyBoundWallet(arg1, chain, address) {
    const asObj = arg1;
    const isObjectForm = !!asObj && typeof asObj === 'object' &&
        !!asObj.passport && !!asObj.chain && !!asObj.address;
    const passport = isObjectForm ? asObj.passport : arg1;
    const c = isObjectForm ? asObj.chain : chain;
    const a = isObjectForm ? asObj.address : address;
    const wallets = passport.passport.bound_wallets;
    if (!wallets || wallets.length === 0)
        return false;
    const bound = wallets.find((w) => w.chain === c && w.address === a);
    if (!bound)
        return false;
    const payload = bindingPayload({
        passport_id: passport.passport.agentId,
        chain: bound.chain,
        address: bound.address,
        bound_at: bound.bound_at,
    });
    try {
        return verify(payload, bound.binding_signature, passport.passport.publicKey);
    }
    catch {
        return false;
    }
}
/**
 * Verify a previously issued unbind event against a passport's public key.
 * Useful when reconstructing wallet history from a stored unbind log.
 */
export function verifyUnbindEvent(event, passportPublicKey) {
    const payload = unbindPayload({
        passport_id: event.passport_id,
        chain: event.chain,
        address: event.address,
        unbound_at: event.unbound_at,
    });
    try {
        return verify(payload, event.unbind_signature, passportPublicKey);
    }
    catch {
        return false;
    }
}
//# sourceMappingURL=bind.js.map