diff --git a/.gitignore b/.gitignore index 60008a8..684384d 100644 --- a/.gitignore +++ b/.gitignore @@ -67,6 +67,9 @@ settings/Testnet.toml secrets.json wallet.json mnemonics.txt +# External-deployer mainnet mnemonic files (24-word secrets) — NEVER commit +mbegu +mbegu2 # ============================================ # Build Artifacts diff --git a/contracts/hk-stx-bitflow-receiver-v1.clar b/contracts/hk-stx-bitflow-receiver-v1.clar new file mode 100644 index 0000000..df2380f --- /dev/null +++ b/contracts/hk-stx-bitflow-receiver-v1.clar @@ -0,0 +1,176 @@ +;; HK STX Bitflow Receiver v1 +;; +;; External-developer flash-loan receiver that executes a REAL DEX round-trip: +;; borrow STX from flashstack-stx-core -> swap STX->stSTX on Bitflow -> +;; swap stSTX->STX back -> repay principal + fee, atomically. +;; +;; Deployed under an EXTERNAL wallet (not the protocol deployer), so every +;; cross-contract reference uses the ABSOLUTE mainnet principal. The `.flashstack-stx-core` +;; sugar used by the in-repo bitflow-arb-receiver would resolve to THIS deployer's +;; address and break for an external deploy. +;; +;; Combines: +;; - hk-stx-real-receiver-v2 : contract-caller gate + absolute principals +;; - bitflow-arb-receiver-v4 : the STX/stSTX Bitflow round-trip +;; +;; Objective is NOT profit. It is: external strategy execution + successful flash +;; loan + real DEX interaction + successful repayment. The repayment assert and the +;; core's own reserve check are the safety gates; min-out=u1 lets the swaps clear. +;; +;; Live contracts used: +;; Core: SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-core +;; Bitflow pool: SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2 +;; stSTX token: SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token +;; Bitflow LP: SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stx-ststx-lp-token-v-1-2 +;; +;; Clarity version: 3 (epoch 3.0 / Nakamoto) + +(impl-trait 'SP3TGRVG7DKGFVRTTVGGS60S59R916FWB4DAB9STZ.stx-flash-receiver-trait.stx-flash-receiver-trait) + +;; Minimal SIP-010 trait for calling the stSTX token +(define-trait sip-010-trait + ( + (transfer (uint principal principal (optional (buff 34))) (response bool uint)) + (get-name () (response (string-ascii 32) uint)) + (get-symbol () (response (string-ascii 32) uint)) + (get-decimals () (response uint uint)) + (get-balance (principal) (response uint uint)) + (get-total-supply () (response uint uint)) + (get-token-uri () (response (optional (string-utf8 256)) uint)) + ) +) + +;; ============================================= +;; Constants +;; ============================================= + +(define-constant CONTRACT-OWNER tx-sender) + +;; The ONLY legitimate caller of execute-stx-flash. Hard-coded (Form A gate). +(define-constant FLASHSTACK-STX-CORE 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-core) + +(define-constant ERR-NOT-OWNER (err u400)) +(define-constant ERR-SWAP-FAILED (err u401)) +(define-constant ERR-WRONG-CALLER (err u403)) +(define-constant ERR-REPAY-FAILED (err u500)) + +;; Bitflow STX/stSTX stableswap pool +(define-constant BITFLOW-POOL 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stableswap-stx-ststx-v-1-2) + +;; stSTX SIP-010 token (y-token in the pool) +(define-constant STSTX 'SP4SZE494VC2YC5JYG7AYFQ44F5Q4PYV7DVMDPBG.ststx-token) + +;; Bitflow STX/stSTX LP token (lp-token parameter) +(define-constant BITFLOW-LP 'SPQC38PW542EQJ5M11CR25P7BS1CA6QT4TBXGB3M.stx-ststx-lp-token-v-1-2) + +;; Slippage tolerance in basis points (default 300 = 3%). Retained as an ops knob; +;; the live legs use min-out=u1 and rely on the repayment assert as the safety gate. +(define-data-var slippage-bp uint u300) + +;; ============================================= +;; Flash Loan Callback +;; ============================================= + +(define-public (execute-stx-flash (amount uint) (core principal)) + (begin + ;; Gate: only the live flashstack-stx-core may invoke this callback. + ;; Closes the direct-drain path a public execute-stx-flash would otherwise expose. + (asserts! (is-eq contract-caller FLASHSTACK-STX-CORE) ERR-WRONG-CALLER) + (let ( + ;; Repayment math - look up the fee dynamically (never hard-code u5). + (fee-bp (unwrap! (contract-call? FLASHSTACK-STX-CORE get-fee-basis-points) ERR-SWAP-FAILED)) + (raw-fee (/ (* amount fee-bp) u10000)) + (fee (if (> raw-fee u0) raw-fee u1)) + (total-owed (+ amount fee)) + + ;; min-out=u1 on both legs - the swap always clears; the repay assert is the gate. + (min-ststx u1) + (min-stx u1) + ) + ;; Leg 1: STX -> stSTX on Bitflow. + ;; as-contract: the borrowed STX sits in THIS contract's balance. + (unwrap! (as-contract (contract-call? BITFLOW-POOL swap-x-for-y + STSTX ;; y-token (stSTX SIP-010) + BITFLOW-LP ;; lp-token + amount ;; STX in (microSTX) + min-ststx ;; min stSTX out + )) ERR-SWAP-FAILED) + + ;; How much stSTX did we receive? + (let ( + (ststx-balance (unwrap! + (contract-call? STSTX get-balance (as-contract tx-sender)) + ERR-SWAP-FAILED)) + ) + (asserts! (> ststx-balance u0) ERR-SWAP-FAILED) + + ;; Leg 2: stSTX -> STX on Bitflow. + (unwrap! (as-contract (contract-call? BITFLOW-POOL swap-y-for-x + STSTX + BITFLOW-LP + ststx-balance ;; all stSTX we hold + min-stx ;; min STX back + )) ERR-SWAP-FAILED) + + ;; Repay STX + fee back to the core. Fail closed if the round-trip came up short. + (let ((stx-now (stx-get-balance (as-contract tx-sender)))) + (asserts! (>= stx-now total-owed) ERR-REPAY-FAILED) + (unwrap! (as-contract (stx-transfer? total-owed tx-sender core)) ERR-REPAY-FAILED) + (print { event: "bitflow-roundtrip", amount: amount, fee: fee, + ststx-mid: ststx-balance, stx-after: stx-now }) + (ok true) + ) + ) + ) + ) +) + +;; ============================================= +;; Admin +;; ============================================= + +(define-public (set-slippage-bp (new-bp uint)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (asserts! (<= new-bp u500) ERR-SWAP-FAILED) ;; max 5% + (ok (var-set slippage-bp new-bp)) + ) +) + +;; Rescue stuck STX (owner only) - escape hatch if a partial round-trip strands STX. +(define-public (rescue-stx (amount uint) (to principal)) + (begin + (asserts! (is-eq tx-sender CONTRACT-OWNER) ERR-NOT-OWNER) + (unwrap! (as-contract (stx-transfer? amount tx-sender to)) ERR-NOT-OWNER) + (ok true) + ) +) + +;; ============================================= +;; Read-only +;; ============================================= + +(define-read-only (get-slippage-bp) + (ok (var-get slippage-bp)) +) + +(define-read-only (get-owner) + (ok CONTRACT-OWNER) +) + +(define-read-only (estimate-repayment (amount uint)) + (let ( + ;; Literal principal (not the constant) so the analyzer can prove this + ;; cross-contract call is read-only inside a define-read-only function. + (fee-bp (unwrap-panic (contract-call? 'SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5.flashstack-stx-core get-fee-basis-points))) + (raw-fee (/ (* amount fee-bp) u10000)) + (fee (if (> raw-fee u0) raw-fee u1)) + ) + (ok { + loan-amount: amount, + fee-to-pay: fee, + total-owed: (+ amount fee), + note: "Round-trip must return >= total-owed or the tx reverts. Seed covers any shortfall." + }) + ) +) diff --git a/scripts/deploy-hk-bitflow-receiver.mjs b/scripts/deploy-hk-bitflow-receiver.mjs new file mode 100644 index 0000000..f73b6b2 --- /dev/null +++ b/scripts/deploy-hk-bitflow-receiver.mjs @@ -0,0 +1,148 @@ +/** + * FlashStack -- Deploy hk-stx-bitflow-receiver-v1 to mainnet + * + * External-developer receiver that runs a REAL Bitflow STX->stSTX->STX round-trip + * inside a flash loan and repays principal + fee atomically. + * + * Deploys from OUR own mainnet wallet (NOT the protocol deployer). The admin-only + * whitelist call (`add-approved-receiver` on flashstack-stx-core) must be done by + * Matt separately before the receiver can borrow. + * + * Usage: + * MAINNET_MNEMONIC="word1 ... word24" node scripts/deploy-hk-bitflow-receiver.mjs + * Or, if ./mbegu2 holds the 24-word mnemonic on a single line: + * node scripts/deploy-hk-bitflow-receiver.mjs + * + * Optional env: + * DRY_RUN=1 Build and print the tx fields but do NOT broadcast. + * DEPLOY_FEE_USTX=... Override the deploy fee (default 500_000 microSTX = 0.5 STX). + */ + +import { makeContractDeploy, PostConditionMode, ClarityVersion, privateKeyToAddress } from "@stacks/transactions"; +import networkPkg from "@stacks/network"; +const { STACKS_MAINNET } = networkPkg; +import walletPkg from "@stacks/wallet-sdk"; +const { generateWallet } = walletPkg; +import { readFileSync, existsSync } from "fs"; + +const NAME = "hk-stx-bitflow-receiver-v1"; +const PATH = "contracts/hk-stx-bitflow-receiver-v1.clar"; +const API = "https://api.hiro.so"; +const EXPLORER = "https://explorer.hiro.so/txid"; +const FEE = Number(process.env.DEPLOY_FEE_USTX ?? 500_000); +const DRY_RUN = process.env.DRY_RUN === "1"; +const network = STACKS_MAINNET; + +function loadMnemonic() { + if (process.env.MAINNET_MNEMONIC) return process.env.MAINNET_MNEMONIC.trim(); + if (existsSync("mbegu2")) return readFileSync("mbegu2", "utf8").trim(); + if (existsSync("mbegu")) return readFileSync("mbegu", "utf8").trim(); + throw new Error("Set MAINNET_MNEMONIC env var, or place 24-word mnemonic in ./mbegu2"); +} + +async function getNonce(addr) { + const res = await fetch(`${API}/v2/accounts/${addr}?proof=0`); + return (await res.json()).nonce; +} + +async function getBalance(addr) { + const res = await fetch(`${API}/extended/v1/address/${addr}/balances`); + return BigInt((await res.json()).stx.balance); +} + +async function broadcast(tx) { + const raw = tx.serialize(); + const body = typeof raw === "string" ? Buffer.from(raw.replace(/^0x/, ""), "hex") : raw; + const res = await fetch(`${API}/v2/transactions`, { + method: "POST", headers: { "Content-Type": "application/octet-stream" }, body, + }); + const text = await res.text(); + let data; try { data = JSON.parse(text); } catch { throw new Error(`Non-JSON: ${text.slice(0, 200)}`); } + if (data?.error) throw new Error(`${data.error} -- ${data.reason ?? ""}`); + const txid = typeof data === "string" ? data : data.txid; + if (!txid) throw new Error(`No txid: ${text.slice(0, 200)}`); + return txid; +} + +async function waitForConfirm(txid, label) { + process.stdout.write(` Waiting for "${label}"`); + for (let i = 0; i < 80; i++) { + await new Promise(r => setTimeout(r, 8000)); + const res = await fetch(`${API}/extended/v1/tx/0x${txid}`); + const data = await res.json(); + if (data.tx_status === "success") { console.log(" confirmed."); return; } + if (data.tx_status?.startsWith("abort")) { + console.log(`\n FAILED: ${data.tx_result?.repr ?? "unknown"}`); + throw new Error(`"${label}" failed`); + } + process.stdout.write("."); + } + throw new Error(`Timeout: "${label}"`); +} + +async function main() { + const mnemonic = loadMnemonic(); + const wc = mnemonic.split(/\s+/).length; + if (wc !== 24) throw new Error(`Expected 24-word mnemonic, got ${wc} words`); + + const wallet = await generateWallet({ secretKey: mnemonic, password: "" }); + const pk = wallet.accounts[0].stxPrivateKey; + const sender = privateKeyToAddress(pk, "mainnet"); + const balance = await getBalance(sender); + const nonce = await getNonce(sender); + + console.log("======================================================="); + console.log(" FlashStack -- Deploy hk-stx-bitflow-receiver-v1 "); + console.log("======================================================="); + console.log(` Sender: ${sender}`); + console.log(` Balance: ${Number(balance) / 1e6} STX`); + console.log(` Nonce: ${nonce}`); + console.log(` Fee: ${FEE} microSTX (${FEE / 1e6} STX)`); + console.log(` Contract: ${sender}.${NAME}`); + console.log(` Source: ${PATH}`); + console.log(` Network: mainnet`); + console.log(` Mode: ${DRY_RUN ? "DRY RUN (no broadcast)" : "LIVE BROADCAST"}`); + console.log(); + + if (balance < BigInt(FEE)) { + throw new Error(`Insufficient balance: ${balance} microSTX < fee ${FEE} microSTX`); + } + + const tx = await makeContractDeploy({ + contractName: NAME, + codeBody: readFileSync(PATH, "utf8"), + senderKey: pk, + network, + clarityVersion: ClarityVersion.Clarity3, + postConditionMode: PostConditionMode.Allow, + anchorMode: 1, + fee: FEE, + nonce, + }); + + if (DRY_RUN) { + console.log(" DRY_RUN=1 -- not broadcasting. Set DRY_RUN=0 (or unset) to broadcast."); + console.log(` Built tx OK. Serialized length: ${tx.serialize().length} bytes-ish.`); + return; + } + + const txid = await broadcast(tx); + console.log(` Broadcast: ${txid}`); + console.log(` Explorer: ${EXPLORER}/${txid}?chain=mainnet`); + await waitForConfirm(txid, "deploy hk-stx-bitflow-receiver-v1"); + + console.log(); + console.log("======================================================="); + console.log(" DEPLOYMENT COMPLETE "); + console.log("======================================================="); + console.log(` Contract: ${sender}.${NAME}`); + console.log(` Tx: ${EXPLORER}/${txid}?chain=mainnet`); + console.log(); + console.log(" Next steps:"); + console.log(` 1. Ask Matt to whitelist ${sender}.${NAME}`); + console.log(" (admin-only: add-approved-receiver on flashstack-stx-core)"); + console.log(" 2. Send >=1 STX to the receiver to cover principal + fee + 2x Bitflow pool fee on repay"); + console.log(" 3. Call flashstack-stx-core.flash-loan(u1000000, receiver) ;; 1 STX round-trip"); +} + +main().catch(e => { console.error("\nFAILED:", e.message); process.exit(1); }); diff --git a/scripts/execute-bitflow-flash-loan.mjs b/scripts/execute-bitflow-flash-loan.mjs new file mode 100644 index 0000000..914ee41 --- /dev/null +++ b/scripts/execute-bitflow-flash-loan.mjs @@ -0,0 +1,141 @@ +/** + * FlashStack -- Execute a flash loan through hk-stx-bitflow-receiver-v1 + * + * Calls flashstack-stx-core.flash-loan(amount, receiver) signed by OUR wallet. + * The receiver must already be (a) deployed and (b) whitelisted by Matt, and + * (c) seeded with >= 1 STX to cover the fee + Bitflow pool fees. + * + * Usage: + * MAINNET_MNEMONIC="word1 ... word24" node scripts/execute-bitflow-flash-loan.mjs + * Or with ./mbegu2 (or ./mbegu) holding the 24-word mnemonic: + * node scripts/execute-bitflow-flash-loan.mjs + * + * Optional env: + * AMOUNT_USTX=1000000 Loan size in microSTX (default 1 STX). + * DRY_RUN=1 Build + print, do NOT broadcast. + * TX_FEE_USTX=... Tx fee (default 300_000 microSTX = 0.3 STX). + */ + +import { makeContractCall, PostConditionMode, Cl, privateKeyToAddress } from "@stacks/transactions"; +import networkPkg from "@stacks/network"; +const { STACKS_MAINNET } = networkPkg; +import walletPkg from "@stacks/wallet-sdk"; +const { generateWallet } = walletPkg; +import { readFileSync, existsSync } from "fs"; + +const CORE_ADDR = "SP20XD46NGAX05ZQZDKFYCCX49A3852BQABNP0VG5"; +const CORE_NAME = "flashstack-stx-core"; +const RECV_NAME = "hk-stx-bitflow-receiver-v1"; +const API = "https://api.hiro.so"; +const EXPLORER = "https://explorer.hiro.so/txid"; +const AMOUNT = BigInt(process.env.AMOUNT_USTX ?? 1_000_000); +const TX_FEE = Number(process.env.TX_FEE_USTX ?? 300_000); +const DRY_RUN = process.env.DRY_RUN === "1"; +const network = STACKS_MAINNET; + +function loadMnemonic() { + if (process.env.MAINNET_MNEMONIC) return process.env.MAINNET_MNEMONIC.trim(); + if (existsSync("mbegu2")) return readFileSync("mbegu2", "utf8").trim(); + if (existsSync("mbegu")) return readFileSync("mbegu", "utf8").trim(); + throw new Error("Set MAINNET_MNEMONIC env var, or place 24-word mnemonic in ./mbegu2"); +} + +async function getNonce(addr) { + return (await (await fetch(`${API}/v2/accounts/${addr}?proof=0`)).json()).nonce; +} +async function getBalance(addr) { + return BigInt((await (await fetch(`${API}/extended/v1/address/${addr}/balances`)).json()).stx.balance); +} +async function readBool(path, fn, args, sender) { + const res = await fetch(`${API}/v2/contracts/call-read/${CORE_ADDR}/${CORE_NAME}/${fn}`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sender, arguments: args }), + }); + return res.json(); +} + +async function broadcast(tx) { + const raw = tx.serialize(); + const body = typeof raw === "string" ? Buffer.from(raw.replace(/^0x/, ""), "hex") : raw; + const res = await fetch(`${API}/v2/transactions`, { + method: "POST", headers: { "Content-Type": "application/octet-stream" }, body, + }); + const text = await res.text(); + let data; try { data = JSON.parse(text); } catch { throw new Error(`Non-JSON: ${text.slice(0,200)}`); } + if (data?.error) throw new Error(`${data.error} -- ${data.reason ?? ""}`); + return typeof data === "string" ? data : data.txid; +} + +async function waitForConfirm(txid, label) { + process.stdout.write(` Waiting for "${label}"`); + for (let i = 0; i < 80; i++) { + await new Promise(r => setTimeout(r, 8000)); + const data = await (await fetch(`${API}/extended/v1/tx/0x${txid}`)).json(); + if (data.tx_status === "success") { console.log(` confirmed. result=${data.tx_result?.repr}`); return data; } + if (data.tx_status?.startsWith("abort")) { + console.log(`\n FAILED: ${data.tx_result?.repr ?? "unknown"}`); + throw new Error(`"${label}" failed: ${data.tx_result?.repr}`); + } + process.stdout.write("."); + } + throw new Error(`Timeout: "${label}"`); +} + +async function main() { + const mnemonic = loadMnemonic(); + const wallet = await generateWallet({ secretKey: mnemonic, password: "" }); + const pk = wallet.accounts[0].stxPrivateKey; + const sender = privateKeyToAddress(pk, "mainnet"); + const receiver = `${sender}.${RECV_NAME}`; + const balance = await getBalance(sender); + const nonce = await getNonce(sender); + + console.log("======================================================="); + console.log(" FlashStack -- flash-loan via hk-stx-bitflow-receiver-v1"); + console.log("======================================================="); + console.log(` Sender: ${sender}`); + console.log(` Balance: ${Number(balance) / 1e6} STX`); + console.log(` Nonce: ${nonce}`); + console.log(` Core: ${CORE_ADDR}.${CORE_NAME}`); + console.log(` Receiver: ${receiver}`); + console.log(` Amount: ${AMOUNT} microSTX (${Number(AMOUNT)/1e6} STX)`); + console.log(` Tx fee: ${TX_FEE} microSTX`); + console.log(` Mode: ${DRY_RUN ? "DRY RUN (no broadcast)" : "LIVE BROADCAST"}`); + console.log(); + + // Preflight read-only checks + const approved = await readBool("is-approved-receiver", "is-approved-receiver", + [Cl.serialize(Cl.contractPrincipal(sender, RECV_NAME))], sender).catch(() => null); + console.log(` Preflight is-approved-receiver -> ${JSON.stringify(approved?.result ?? approved)}`); + const stats = await (await fetch(`${API}/v2/contracts/call-read/${CORE_ADDR}/${CORE_NAME}/get-stats`, { + method: "POST", headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ sender, arguments: [] }) })).json().catch(() => null); + console.log(` Preflight get-stats raw -> ${JSON.stringify(stats?.result ?? stats)?.slice(0,160)}`); + + const tx = await makeContractCall({ + contractAddress: CORE_ADDR, + contractName: CORE_NAME, + functionName: "flash-loan", + functionArgs: [Cl.uint(AMOUNT), Cl.contractPrincipal(sender, RECV_NAME)], + senderKey: pk, + network, + postConditionMode: PostConditionMode.Allow, + anchorMode: 1, + fee: TX_FEE, + nonce, + }); + + if (DRY_RUN) { + console.log("\n DRY_RUN=1 -- not broadcasting. Tx built OK."); + return; + } + + const txid = await broadcast(tx); + console.log(`\n Broadcast: ${txid}`); + console.log(` Explorer: ${EXPLORER}/${txid}?chain=mainnet`); + const res = await waitForConfirm(txid, "flash-loan via bitflow receiver"); + console.log("\n DONE. Save this txid as on-chain evidence."); + console.log(` ${EXPLORER}/${txid}?chain=mainnet`); +} + +main().catch(e => { console.error("\nFAILED:", e.message); process.exit(1); }); diff --git a/scripts/seed-bitflow-receiver.mjs b/scripts/seed-bitflow-receiver.mjs new file mode 100644 index 0000000..f25d830 --- /dev/null +++ b/scripts/seed-bitflow-receiver.mjs @@ -0,0 +1,100 @@ +/** + * FlashStack -- Seed hk-stx-bitflow-receiver-v1 with STX. + * + * The receiver pays the flash-loan fee (and absorbs round-trip slippage) from its + * OWN balance inside the callback, so it must hold STX before the first loan. + * + * Usage: + * node scripts/seed-bitflow-receiver.mjs # reads ./mbegu2 then ./mbegu + * MAINNET_MNEMONIC="w1 ... w24" node scripts/seed-bitflow-receiver.mjs + * + * Optional env: + * SEED_USTX=1000000 Seed size in microSTX (default 1 STX). + * DRY_RUN=1 Build + print, do NOT broadcast. + * TX_FEE_USTX=10000 Tx fee (default 10_000 microSTX = 0.01 STX). + */ +import { makeSTXTokenTransfer, privateKeyToAddress } from "@stacks/transactions"; +import networkPkg from "@stacks/network"; +const { STACKS_MAINNET } = networkPkg; +import walletPkg from "@stacks/wallet-sdk"; +const { generateWallet } = walletPkg; +import { readFileSync, existsSync } from "fs"; + +const RECV_NAME = "hk-stx-bitflow-receiver-v1"; +const API = "https://api.hiro.so"; +const EXPLORER = "https://explorer.hiro.so/txid"; +const SEED = BigInt(process.env.SEED_USTX ?? 1_000_000); +const TX_FEE = Number(process.env.TX_FEE_USTX ?? 10_000); +const DRY_RUN = process.env.DRY_RUN === "1"; +const network = STACKS_MAINNET; + +function loadMnemonic() { + if (process.env.MAINNET_MNEMONIC) return process.env.MAINNET_MNEMONIC.trim(); + if (existsSync("mbegu2")) return readFileSync("mbegu2", "utf8").trim(); + if (existsSync("mbegu")) return readFileSync("mbegu", "utf8").trim(); + throw new Error("Set MAINNET_MNEMONIC env var, or place 24-word mnemonic in ./mbegu2"); +} +async function getNonce(addr) { + return (await (await fetch(`${API}/v2/accounts/${addr}?proof=0`)).json()).nonce; +} +async function getBalance(addr) { + return BigInt((await (await fetch(`${API}/extended/v1/address/${addr}/balances`)).json()).stx.balance); +} +async function broadcast(tx) { + const raw = tx.serialize(); + const body = typeof raw === "string" ? Buffer.from(raw.replace(/^0x/, ""), "hex") : raw; + const res = await fetch(`${API}/v2/transactions`, { + method: "POST", headers: { "Content-Type": "application/octet-stream" }, body }); + const text = await res.text(); + let data; try { data = JSON.parse(text); } catch { throw new Error(`Non-JSON: ${text.slice(0,200)}`); } + if (data?.error) throw new Error(`${data.error} -- ${data.reason ?? ""}`); + return typeof data === "string" ? data : data.txid; +} +async function waitForConfirm(txid, label) { + process.stdout.write(` Waiting for "${label}"`); + for (let i = 0; i < 80; i++) { + await new Promise(r => setTimeout(r, 8000)); + const data = await (await fetch(`${API}/extended/v1/tx/0x${txid}`)).json(); + if (data.tx_status === "success") { console.log(` confirmed. result=${data.tx_result?.repr}`); return data; } + if (data.tx_status?.startsWith("abort")) throw new Error(`"${label}" failed: ${data.tx_result?.repr}`); + process.stdout.write("."); + } + throw new Error(`Timeout: "${label}"`); +} + +async function main() { + const mnemonic = loadMnemonic(); + const wallet = await generateWallet({ secretKey: mnemonic, password: "" }); + const pk = wallet.accounts[0].stxPrivateKey; + const sender = privateKeyToAddress(pk, "mainnet"); + const recipient = `${sender}.${RECV_NAME}`; + const balance = await getBalance(sender); + const nonce = await getNonce(sender); + + console.log("======================================================="); + console.log(" FlashStack -- seed hk-stx-bitflow-receiver-v1"); + console.log("======================================================="); + console.log(` Sender: ${sender}`); + console.log(` Balance: ${Number(balance) / 1e6} STX`); + console.log(` Nonce: ${nonce}`); + console.log(` Recipient: ${recipient}`); + console.log(` Seed: ${SEED} microSTX (${Number(SEED)/1e6} STX)`); + console.log(` Tx fee: ${TX_FEE} microSTX`); + console.log(` Mode: ${DRY_RUN ? "DRY RUN (no broadcast)" : "LIVE BROADCAST"}\n`); + + const tx = await makeSTXTokenTransfer({ + recipient, amount: SEED, senderKey: pk, network, fee: TX_FEE, nonce, + memo: "seed hk-stx-bitflow-receiver-v1", + }); + + if (DRY_RUN) { console.log(" DRY_RUN=1 -- not broadcasting. Tx built OK."); return; } + + const txid = await broadcast(tx); + console.log(` Broadcast: ${txid}`); + console.log(` Explorer: ${EXPLORER}/${txid}?chain=mainnet`); + await waitForConfirm(txid, "seed receiver"); + const recvBal = await getBalance(recipient); + console.log(`\n DONE. Receiver balance now: ${Number(recvBal)/1e6} STX`); + console.log(` Seed txid: ${txid}`); +} +main().catch(e => { console.error("\nFAILED:", e.message); process.exit(1); });