import { FastProvider, Signer, TransactionBuilder, toHex } from "@fastxyz/sdk";
import * as fs from "node:fs";
import * as path from "node:path";
import { makeError } from "./errors.js";
import { ok, err } from "./result.js";
/** URL agents should point humans to for Agent Wallet creation + funding. */
export const FAST_APP_URL = "https://app.fast.xyz/";
const KEYFILE_MODE = 0o600;
/**
 * Build a structured funding URL for a Fast address.
 *
 * Per fast-app/docs/agent-payment-modes.md, `/fund` is the recommended default
 * — a single landing that lets the human pick between Cash (card on-ramp),
 * Crypto (multi-chain swap), or an existing fastUSD balance. Agents should
 * prefer this over the bare app URL or a single-source link unless they
 * already know which funding source the human will use.
 *
 * The `amount` is in USD and only added when the agent knows a specific
 * gap (e.g. quote — current balance). Omit it for general "top up the
 * agent wallet" prompts.
 */
export function buildFundUrl(address, amount) {
    const params = new URLSearchParams({ to: address });
    if (amount && /^\d+(\.\d+)?$/.test(amount.trim()) && Number(amount) > 0) {
        params.set("amount", amount.trim());
    }
    return `https://app.fast.xyz/fund?${params.toString()}`;
}
const USDC_DECIMAL_PATTERN = /^\d+(?:\.\d{1,6})?$/;
// Known stablecoin token IDs per Fast network. The SDK 2.0.0 REST proxy does
// NOT have a "return all tokens" mode — `tokenBalancesFilter: null` (or `[]`)
// makes `tokenBalance` come back empty. We MUST pass the explicit token IDs
// we care about. Keys match the network segment of `fast:<network>`.
// Override with FAST_USDC_TOKEN_ID if you're on a non-standard network.
//
// Mainnet = fastUSD (Fast-native; replaced USDC at 2026-04-30).
// Testnet = testUSDC. Both use 6-decimal micro-units.
const KNOWN_USDC_TOKEN_ID_HEX = {
    mainnet: "125b60bb2e805336f0934077d4f9fdb36f45bec9ded8d7b0e637516cc43a86eb",
    testnet: "d73a0679a2be46981e2a8aedecd951c8b6690e7d5f8502b34ed3ff4cc2163b46",
};
/**
 * Default stablecoin symbol per Fast network. Used when the caller doesn't
 * pass an explicit `token` argument and as the user-facing label in receipts.
 */
function defaultTokenSymbolForNetwork(network) {
    if (network === "mainnet")
        return "fastUSD";
    if (network === "testnet")
        return "testUSDC";
    return "USDC";
}
function parseUsdcToMicroUnits(amount) {
    if (!USDC_DECIMAL_PATTERN.test(amount))
        return null;
    const [wholePart, fractionalPart = ""] = amount.split(".");
    return BigInt(wholePart) * 1000000n + BigInt(fractionalPart.padEnd(6, "0"));
}
function formatMicroUnits(amount) {
    const whole = amount / 1000000n;
    const fraction = amount % 1000000n;
    if (fraction === 0n)
        return `${whole.toString()}.00`;
    return `${whole.toString()}.${fraction.toString().padStart(6, "0").replace(/0+$/, "")}`;
}
function hexToBytes(hex) {
    const stripped = hex.startsWith("0x") ? hex.slice(2) : hex;
    if (stripped.length % 2 !== 0 || !/^[0-9a-fA-F]*$/.test(stripped)) {
        throw new Error("invalid hex string");
    }
    const out = new Uint8Array(stripped.length / 2);
    for (let i = 0; i < out.length; i++) {
        out[i] = parseInt(stripped.slice(i * 2, i * 2 + 2), 16);
    }
    return out;
}
function toHexTokenId(value) {
    if (typeof value === "string") {
        return value.startsWith("0x") ? value.toLowerCase() : `0x${value.toLowerCase()}`;
    }
    return toHex(value).toLowerCase();
}
export class WalletService {
    keyfilePath;
    provider;
    networkId;
    network;
    constructor(opts) {
        this.keyfilePath = opts.keyfilePath;
        this.provider = new FastProvider({ url: opts.rpcUrl });
        this.network = opts.network;
        this.networkId = `fast:${opts.network}`;
    }
    /**
     * Build the `tokenBalancesFilter` list for `getAccountInfo`. The SDK 2.0.0
     * REST proxy does NOT expose a "return all tokens" mode — empty/null filter
     * comes back with `tokenBalance: []`. We pass the well-known token IDs we
     * need so the response actually includes them.
     */
    knownTokenBalanceFilter() {
        const override = process.env.FAST_USDC_TOKEN_ID?.trim();
        const hex = (override && override.length > 0)
            ? override.replace(/^0x/, "").toLowerCase()
            : KNOWN_USDC_TOKEN_ID_HEX[this.network];
        if (!hex)
            return [];
        try {
            return [hexToBytes(hex)];
        }
        catch {
            return [];
        }
    }
    /**
     * Load an existing wallet from the keyfile. Never generates a new key —
     * wallet material originates from `https://app.fast.xyz/` (human creates
     * the Agent Wallet there and pastes keys via `fast_wallet_import`).
     * Returns `WALLET_NOT_FOUND` if the keyfile is missing so callers can
     * point the user at the setup URL.
     */
    async loadWallet() {
        try {
            if (!fs.existsSync(this.keyfilePath)) {
                return err(makeError("WALLET_NOT_FOUND", "No wallet keyfile found", {
                    details: { setup_url: FAST_APP_URL, keyfilePath: this.keyfilePath },
                }));
            }
            const mode = fs.statSync(this.keyfilePath).mode & 0o777;
            if (mode !== KEYFILE_MODE) {
                fs.chmodSync(this.keyfilePath, KEYFILE_MODE);
            }
            const raw = JSON.parse(fs.readFileSync(this.keyfilePath, "utf8"));
            if (!raw || typeof raw !== "object" || typeof raw.privateKey !== "string") {
                return err(makeError("WALLET_LOAD_FAILED", "Keyfile is missing a readable privateKey"));
            }
            const privateKey = raw.privateKey;
            const signer = new Signer(privateKey);
            const publicKey = toHex(await signer.getPublicKey());
            const address = await signer.getFastAddress();
            const wallet = { signer, publicKey, address };
            return ok(wallet);
        }
        catch (e) {
            const nodeErr = e;
            if (nodeErr.code === "ENOENT" || /not found|no such file/i.test(e.message)) {
                return err(makeError("WALLET_NOT_FOUND", "No wallet keyfile found", {
                    details: { setup_url: FAST_APP_URL, keyfilePath: this.keyfilePath },
                }));
            }
            return err(makeError("WALLET_LOAD_FAILED", `Failed to load wallet: ${e.message}`));
        }
    }
    /**
     * Load the existing wallet. Returns WALLET_NOT_FOUND (with setup_url) if
     * the keyfile doesn't exist yet — the caller should direct the human to
     * app.fast.xyz to create/import an Agent Wallet first.
     */
    async load() {
        const wallet = await this.loadWallet();
        if (!wallet.ok)
            return wallet;
        return ok({ address: wallet.value.address, publicKey: wallet.value.publicKey, keyfilePath: this.keyfilePath });
    }
    /**
     * Persist a private key pasted in from app.fast.xyz. The MCP never
     * generates keys itself — this is the ONLY legitimate way a wallet lands
     * on disk. The address is DERIVED from the private key, not asked for
     * separately (single-paste UX — the human should only ever copy one
     * thing). Input must be a 64-char hex string (with or without 0x prefix).
     *
     * Refuses to overwrite a keyfile that already holds a DIFFERENT key.
     * Re-importing the same key is idempotent.
     */
    async importWallet(params) {
        const raw = (params.privateKey || "").trim();
        const hex = raw.startsWith("0x") ? raw.slice(2) : raw;
        if (!/^[0-9a-fA-F]{64}$/.test(hex)) {
            return err(makeError("INVALID_PRIVATE_KEY", "Private key must be a 64-char hex string (32 bytes). Paste the value from app.fast.xyz exactly.", {
                details: { setup_url: FAST_APP_URL },
            }));
        }
        let derivedPublicKey;
        let derivedAddress;
        try {
            const signer = new Signer(hex);
            derivedPublicKey = toHex(await signer.getPublicKey());
            derivedAddress = await signer.getFastAddress();
        }
        catch (e) {
            return err(makeError("INVALID_PRIVATE_KEY", `Could not derive wallet from key: ${e.message}`));
        }
        // Don't silently overwrite an existing different wallet — that'd strand
        // funds on a wallet whose key we no longer have on disk.
        if (fs.existsSync(this.keyfilePath)) {
            try {
                const existing = JSON.parse(fs.readFileSync(this.keyfilePath, "utf8"));
                if (existing && typeof existing.privateKey === "string" && existing.privateKey.replace(/^0x/, "").toLowerCase() !== hex.toLowerCase()) {
                    return err(makeError("WALLET_ALREADY_EXISTS", `A different wallet is already stored at ${this.keyfilePath}. Remove or back up that file before importing a new key.`, {
                        details: { keyfilePath: this.keyfilePath, existing_address: await new Signer(existing.privateKey).getFastAddress() },
                    }));
                }
            }
            catch {
                // Keyfile is unreadable/corrupt — go ahead and replace.
            }
        }
        fs.mkdirSync(path.dirname(this.keyfilePath), { recursive: true });
        fs.writeFileSync(this.keyfilePath, JSON.stringify({ privateKey: hex }, null, 2), "utf8");
        fs.chmodSync(this.keyfilePath, KEYFILE_MODE);
        return ok({ address: derivedAddress, publicKey: derivedPublicKey, keyfilePath: this.keyfilePath });
    }
    async resolveTokenAmount(token, tokenBalance) {
        const normalizedToken = token.toLowerCase();
        const directMatch = tokenBalance.find(([tokenId]) => toHexTokenId(tokenId) === normalizedToken || toHexTokenId(tokenId) === `0x${normalizedToken}`);
        if (directMatch) {
            return { amount: directMatch[1], resolvedToken: toHexTokenId(directMatch[0]) };
        }
        if (tokenBalance.length === 0) {
            const defaultSymbol = defaultTokenSymbolForNetwork(this.network);
            return token === defaultSymbol ? { amount: 0n, resolvedToken: defaultSymbol } : null;
        }
        const tokenIds = tokenBalance.map(([tokenId]) => tokenId);
        const info = await this.provider.getTokenInfo({ tokenIds });
        for (const [tokenId, metadata] of info.requestedTokenMetadata) {
            if (!metadata)
                continue;
            if (metadata.tokenName === token) {
                const matched = tokenBalance.find(([balanceTokenId]) => toHexTokenId(balanceTokenId) === toHexTokenId(tokenId));
                return { amount: matched ? matched[1] : 0n, resolvedToken: metadata.tokenName };
            }
        }
        const defaultSymbol = defaultTokenSymbolForNetwork(this.network);
        return token === defaultSymbol ? { amount: 0n, resolvedToken: defaultSymbol } : null;
    }
    async resolveTokenId(token, tokenBalance) {
        const tokenData = await this.resolveTokenAmount(token, tokenBalance);
        if (!tokenData)
            return null;
        const normalizedToken = token.toLowerCase();
        const directMatch = tokenBalance.find(([tokenId]) => toHexTokenId(tokenId) === normalizedToken || toHexTokenId(tokenId) === `0x${normalizedToken}`);
        if (directMatch)
            return toHexTokenId(directMatch[0]);
        const tokenIds = tokenBalance.map(([tokenId]) => tokenId);
        const info = await this.provider.getTokenInfo({ tokenIds });
        for (const [tokenId, metadata] of info.requestedTokenMetadata) {
            if (!metadata)
                continue;
            if (metadata.tokenName === token) {
                return toHexTokenId(tokenId);
            }
        }
        return null;
    }
    async getBalance(token) {
        const resolvedToken = token ?? defaultTokenSymbolForNetwork(this.network);
        const wallet = await this.loadWallet();
        if (!wallet.ok)
            return wallet;
        try {
            const account = await this.provider.getAccountInfo({
                address: wallet.value.publicKey,
                tokenBalancesFilter: this.knownTokenBalanceFilter(),
            });
            const tokenBalance = account.tokenBalance;
            const tokenAmount = await this.resolveTokenAmount(resolvedToken, tokenBalance);
            if (!tokenAmount) {
                return err(makeError("WALLET_OPERATION_FAILED", `Token ${resolvedToken} was not found for this wallet`));
            }
            return ok({
                address: wallet.value.address,
                amount: formatMicroUnits(tokenAmount.amount),
                token: tokenAmount.resolvedToken,
            });
        }
        catch (error) {
            return err(makeError("WALLET_OPERATION_FAILED", `Failed to get balance: ${error.message}`));
        }
    }
    async sendUsdc(params) {
        const wallet = await this.loadWallet();
        if (!wallet.ok)
            return wallet;
        try {
            const account = await this.provider.getAccountInfo({
                address: wallet.value.publicKey,
                tokenBalancesFilter: this.knownTokenBalanceFilter(),
            });
            const tokenBalance = account.tokenBalance;
            const tokenSymbol = defaultTokenSymbolForNetwork(this.network);
            const usdcTokenId = await this.resolveTokenId(tokenSymbol, tokenBalance);
            if (!usdcTokenId) {
                return err(makeError("WALLET_OPERATION_FAILED", `${tokenSymbol} token was not found for this wallet`));
            }
            const amount = parseUsdcToMicroUnits(params.amount);
            if (amount === null) {
                return err(makeError("WALLET_OPERATION_FAILED", `Invalid ${tokenSymbol} amount: ${params.amount}`));
            }
            const envelope = await new TransactionBuilder({
                networkId: this.networkId,
                signer: wallet.value.signer,
                nonce: account.nextNonce,
            })
                .addTokenTransfer({
                tokenId: usdcTokenId,
                recipient: params.to,
                amount,
                userData: null,
            })
                .sign();
            const result = await this.provider.submitTransaction(envelope);
            if (result.type !== "Success") {
                return err(makeError("WALLET_OPERATION_FAILED", `Failed to submit transaction: ${result.type}`));
            }
            const txHash = result.value.signatures[0]
                ? toHex(result.value.signatures[0][1])
                : "";
            return ok({
                from: wallet.value.address,
                to: params.to,
                amount: params.amount,
                token: tokenSymbol,
                txHash,
                explorerUrl: txHash ? `https://explorer.fast.xyz/tx/${txHash}` : "",
            });
        }
        catch (error) {
            return err(makeError("WALLET_OPERATION_FAILED", `Failed to send USDC: ${error.message}`));
        }
    }
    async signMessage(message) {
        const wallet = await this.loadWallet();
        if (!wallet.ok)
            return wallet;
        try {
            const signature = await wallet.value.signer.signMessage(new TextEncoder().encode(message));
            return ok({ signature: toHex(signature), address: wallet.value.address });
        }
        catch (error) {
            return err(makeError("WALLET_OPERATION_FAILED", `Failed to sign message: ${error.message}`));
        }
    }
    async exportKeys() {
        const wallet = await this.loadWallet();
        if (!wallet.ok)
            return wallet;
        return ok({ publicKey: wallet.value.publicKey, address: wallet.value.address });
    }
}
//# sourceMappingURL=wallet.js.map