diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 13c82144..84dd9173 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,14 @@ jobs: - name: TypeScript typecheck run: bun run typecheck + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.12' + + - name: Install skills-ref (required by bun run validate for tier-1 spec checks) + run: pip install skills-ref==0.1.1 + - name: Validate skill frontmatter run: bun run validate diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 57953081..2ff8218f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.42.0" + ".": "0.43.0" } diff --git a/CHANGELOG.md b/CHANGELOG.md index e3e7384c..5562a2c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,179 @@ # Changelog +## [0.43.0](https://github.com/gregoryford963-sys/skills/compare/skills-v0.42.0...skills-v0.43.0) (2026-05-18) + + +### Features + +* add aibtc-news and aibtc-news-protocol skills ([#46](https://github.com/gregoryford963-sys/skills/issues/46)) ([9a361b7](https://github.com/gregoryford963-sys/skills/commit/9a361b7fb77509a540c5a7182385841f7ca255cf)) +* add author attribution to skill frontmatter ([#90](https://github.com/gregoryford963-sys/skills/issues/90)) ([f90b996](https://github.com/gregoryford963-sys/skills/commit/f90b9966a63265fffef583d3a487311d8ce83797)) +* add bitflow-hodlmm-deposit (BFF Skills Comp Day 21 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#357](https://github.com/gregoryford963-sys/skills/issues/357)) ([eff01e1](https://github.com/gregoryford963-sys/skills/commit/eff01e1fe1dda7c9e8dd9d9ae0a4b4771a4f1c6d)) +* add bitflow-hodlmm-withdraw (BFF Skills Comp Day 22 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#356](https://github.com/gregoryford963-sys/skills/issues/356)) ([db5bdb8](https://github.com/gregoryford963-sys/skills/commit/db5bdb868fcf6c62d03ab86b97d818bc2a0dd679)) +* add bitflow-hodlmm-zest-yield-loop (BFF Skills Comp Day 27 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([920d127](https://github.com/gregoryford963-sys/skills/commit/920d1270c3eb622895c1931248a379544def860d)) +* add bitflow-limit-order (BFF Skills Comp Day 18 winner by @ClankOS) ([#329](https://github.com/gregoryford963-sys/skills/issues/329)) ([8aa5d3f](https://github.com/gregoryford963-sys/skills/commit/8aa5d3f93bfff9c778b35904c80c8de5b92e8a8e)) +* add bitflow-swap-aggregator (BFF Skills Comp Day 25 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#360](https://github.com/gregoryford963-sys/skills/issues/360)) ([ddbb9cb](https://github.com/gregoryford963-sys/skills/commit/ddbb9cb0ff407e79e09ccdef9c360345eb699b9e)) +* add bitflow-zest-sbtc-leverage-cycle (BFF Skills Comp Day 26 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#374](https://github.com/gregoryford963-sys/skills/issues/374)) ([c8e8fc9](https://github.com/gregoryford963-sys/skills/commit/c8e8fc95d39fb82736c7747c2b8bd00b1d2517e1)) +* add child-inscription-builder module ([#204](https://github.com/gregoryford963-sys/skills/issues/204)) ([f892825](https://github.com/gregoryford963-sys/skills/commit/f8928255294026042cfd3bbeb0f466393e1c97d5)) +* add contract-preflight skill (BFF Skills Comp Day 17 winner) ([#327](https://github.com/gregoryford963-sys/skills/issues/327)) ([4a284c7](https://github.com/gregoryford963-sys/skills/commit/4a284c79f432bba987669f89a8a335976087b6a7)) +* add CONTRIBUTING.md, wallet import docs, and Result type ([#6](https://github.com/gregoryford963-sys/skills/issues/6)) ([831c810](https://github.com/gregoryford963-sys/skills/commit/831c81070e70f877129c8b5725e2b0e6f915c149)) +* add dog-intelligence (BFF Skills Comp Day 30 winner by @LimaDevBTC) ([b8fa740](https://github.com/gregoryford963-sys/skills/commit/b8fa740ed870fe11ad431370bfb17b3aac4c3637)) +* add hodlmm-arb-executor (BFF Skills Comp Day 20 winner by [@ronkenx9](https://github.com/ronkenx9)) ([#326](https://github.com/gregoryford963-sys/skills/issues/326)) ([5472699](https://github.com/gregoryford963-sys/skills/commit/54726995fc9b60a17177f1d433d064cb970428c6)) +* add hodlmm-flow (BFF Skills Comp Day 19 winner by @ClankOS) ([#328](https://github.com/gregoryford963-sys/skills/issues/328)) ([7839339](https://github.com/gregoryford963-sys/skills/commit/7839339f70823eb0ab9e7adf62669571a9a193fc)) +* add hodlmm-inventory-balancer (BFF Skills Comp Day 24 winner by [@cliqueengagements](https://github.com/cliqueengagements)) ([9debfad](https://github.com/gregoryford963-sys/skills/commit/9debfad29cd83e90f39fa37b55a6d3fdef6a2495)) +* add hodlmm-move-liquidity skill (BFF Skills Comp Day 14 winner) ([#317](https://github.com/gregoryford963-sys/skills/issues/317)) ([daac65d](https://github.com/gregoryford963-sys/skills/commit/daac65d81b85a9a33da163186d1f3e6717d969d9)) +* add sbtc-yield-maximizer (BFF Skills Comp Day 16 winner by @Ololadestephen) ([849f122](https://github.com/gregoryford963-sys/skills/commit/849f1224ff3b6e472a809ca1869fdc9809449581)) +* add skill scaffolding script for new contributors ([#89](https://github.com/gregoryford963-sys/skills/issues/89)) ([a0def2c](https://github.com/gregoryford963-sys/skills/commit/a0def2cee5dbb2853d6b49514b605f40254a5e20)) +* add stacking-delegation skill (BFF Skills Comp Day 15 winner) ([#330](https://github.com/gregoryford963-sys/skills/issues/330)) ([bf5c7e1](https://github.com/gregoryford963-sys/skills/commit/bf5c7e1a801f531bee1248767923f69f95332e6b)) +* add stacks-alpha-engine (BFF Skills Comp Day 13 winner by [@cliqueengagements](https://github.com/cliqueengagements)) ([#339](https://github.com/gregoryford963-sys/skills/issues/339)) ([7c5ca44](https://github.com/gregoryford963-sys/skills/commit/7c5ca4479fd0ea712837a57626d351d2585d7487)) +* add yield-dashboard skill — cross-protocol DeFi yield aggregator ([#82](https://github.com/gregoryford963-sys/skills/issues/82)) ([979bd69](https://github.com/gregoryford963-sys/skills/commit/979bd69eddb755d903e27a8d07586d58f7d3726e)) +* add zest-asset-deposit-primitive (BFF Skills Comp Day 23 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#358](https://github.com/gregoryford963-sys/skills/issues/358)) ([d7b116f](https://github.com/gregoryford963-sys/skills/commit/d7b116f0ea23a340eb7c570dca1f8315a9913568)) +* add zest-borrow-asset-primitive (BFF Skills Comp Day 24 winner by [@macbotmini-eng](https://github.com/macbotmini-eng)) ([#359](https://github.com/gregoryford963-sys/skills/issues/359)) ([f5f831b](https://github.com/gregoryford963-sys/skills/commit/f5f831badb4137110264bafbd5e5f5565b9aeaab)) +* **agent-lookup:** add agent-lookup skill ([#123](https://github.com/gregoryford963-sys/skills/issues/123)) ([12af974](https://github.com/gregoryford963-sys/skills/commit/12af9742d15bc5e5f77fb55b6b74c28710d0cb5a)) +* **aibtc-agents:** add amber-otter agent configuration ([#371](https://github.com/gregoryford963-sys/skills/issues/371)) ([fc0d99a](https://github.com/gregoryford963-sys/skills/commit/fc0d99a671784f4b9d6f4276ea1b2ee0fea3d079)) +* **aibtc-agents:** add iris0btc, loom0btc, and forge0btc agent configs ([#106](https://github.com/gregoryford963-sys/skills/issues/106)) ([e0eb01e](https://github.com/gregoryford963-sys/skills/commit/e0eb01e5133d3d9c1ad149397217423fd48e6a04)) +* **aibtc-agents:** add secret-mars agent config ([#18](https://github.com/gregoryford963-sys/skills/issues/18)) ([f80ddd7](https://github.com/gregoryford963-sys/skills/commit/f80ddd7f0524c26d9592c446d5aeb6a2f3923408)) +* **aibtc-agents:** add SKILL.md and update manifest ([#120](https://github.com/gregoryford963-sys/skills/issues/120)) ([221e4b5](https://github.com/gregoryford963-sys/skills/commit/221e4b5a79cd58ad499aa0b04cb0596739a09b21)) +* **aibtc-agents:** add spark0btc agent config ([#61](https://github.com/gregoryford963-sys/skills/issues/61)) ([5e83bf4](https://github.com/gregoryford963-sys/skills/commit/5e83bf4d532da95eab687b815db7019a504d20cd)) +* **aibtc-agents:** add tiny-marten config and two workflow guides ([#42](https://github.com/gregoryford963-sys/skills/issues/42)) ([e028c02](https://github.com/gregoryford963-sys/skills/commit/e028c02430adbc51b9be00a70cf51a252f8b6936)) +* **aibtc-news:** add front-page, status filter, disclosure field (closes [#171](https://github.com/gregoryford963-sys/skills/issues/171)) ([#172](https://github.com/gregoryford963-sys/skills/issues/172)) ([5c73fcd](https://github.com/gregoryford963-sys/skills/commit/5c73fcd4b1c6aec89b2590b5817d55b3a8e93d40)) +* **aibtc-news:** add leaderboard, review-signal; add corrections to classifieds (closes [#171](https://github.com/gregoryford963-sys/skills/issues/171)) ([#177](https://github.com/gregoryford963-sys/skills/issues/177)) ([6488633](https://github.com/gregoryford963-sys/skills/commit/6488633cd92846a033fbd27dabc8397d046085c5)) +* **aibtc-news:** add reset-leaderboard subcommand for publisher ([#212](https://github.com/gregoryford963-sys/skills/issues/212)) ([0f09682](https://github.com/gregoryford963-sys/skills/commit/0f09682a78f9b6b6f73db9a3fb8e7e91b8e8dc75)) +* **aibtc-news:** add x402 payment flow to file-signal subcommand ([#264](https://github.com/gregoryford963-sys/skills/issues/264)) ([5c11c53](https://github.com/gregoryford963-sys/skills/commit/5c11c5319b60ce7f0a09b09aa84aa6ebc58e66aa)) +* AX/UX audit phases 1-5 (skills repo) ([#12](https://github.com/gregoryford963-sys/skills/issues/12)) ([680dcf5](https://github.com/gregoryford963-sys/skills/commit/680dcf593c57aa55cd6140e4590434e5e8de38a3)) +* **bitflow:** unify SDK and HODLMM routing and ranking ([#111](https://github.com/gregoryford963-sys/skills/issues/111)) ([fea9df6](https://github.com/gregoryford963-sys/skills/commit/fea9df6940e751a2c2d825e924a7edb6120c5156)) +* **bounty-scanner:** autonomous bounty hunting skill ([#91](https://github.com/gregoryford963-sys/skills/issues/91)) ([e5d7b12](https://github.com/gregoryford963-sys/skills/commit/e5d7b1249b7b4892239a6c0b6fb5d3710efb0856)) +* **business-dev:** add business development skill ([#65](https://github.com/gregoryford963-sys/skills/issues/65)) ([eaf8e3f](https://github.com/gregoryford963-sys/skills/commit/eaf8e3f46c3bb54052c1bdc1c237b6d92d89cc49)) +* **ceo:** add CEO operating manual skill for agent guidance ([#67](https://github.com/gregoryford963-sys/skills/issues/67)) ([9cf5d1f](https://github.com/gregoryford963-sys/skills/commit/9cf5d1fc397658e6c0f7a6bed7ed5260c819843d)) +* **child-inscription:** add parent-child Ordinals inscription skill (closes [#142](https://github.com/gregoryford963-sys/skills/issues/142)) ([#152](https://github.com/gregoryford963-sys/skills/issues/152)) ([72e8ad6](https://github.com/gregoryford963-sys/skills/commit/72e8ad657444f199c593ad02897dd5c3753acfd4)) +* **clarity:** add Clarity development skills ([#222](https://github.com/gregoryford963-sys/skills/issues/222)) ([55bbc20](https://github.com/gregoryford963-sys/skills/commit/55bbc201c7fcd1de2b237990164ba01a2a8bad5d)) +* **contract:** add contract deployment and interaction skill (closes [#138](https://github.com/gregoryford963-sys/skills/issues/138)) ([#160](https://github.com/gregoryford963-sys/skills/issues/160)) ([0b88f36](https://github.com/gregoryford963-sys/skills/commit/0b88f36d63814bf7bdadafdc7e96bbc1575aabcb)) +* convert MCP server to Claude Code skills ([fae4417](https://github.com/gregoryford963-sys/skills/commit/fae4417e81aa88d7e600b590a2cce5567d0926b3)) +* **dca:** add BFF Skills Comp Day 10 winner by [@k9dreamermacmini-coder](https://github.com/k9dreamermacmini-coder) ([#299](https://github.com/gregoryford963-sys/skills/issues/299)) ([306fdfe](https://github.com/gregoryford963-sys/skills/commit/306fdfea45daf8de905f33db32bd204f73358dc4)) +* **defi-portfolio-scanner:** add cross-protocol DeFi position aggregator (BFF Day 7 winner) ([f6d6a81](https://github.com/gregoryford963-sys/skills/commit/f6d6a81661e6601935d22b04b557bef1da4e0a8b)) +* **dual-stacking:** add dual stacking enrollment skill ([#76](https://github.com/gregoryford963-sys/skills/issues/76)) ([07ca9fb](https://github.com/gregoryford963-sys/skills/commit/07ca9fb7136be0c790d8878a15e4b9f9943c84d0)) +* **erc8004:** add ERC-8004 on-chain agent identity skill (closes [#141](https://github.com/gregoryford963-sys/skills/issues/141)) ([#156](https://github.com/gregoryford963-sys/skills/issues/156)) ([322271e](https://github.com/gregoryford963-sys/skills/commit/322271e0f2e3c1306f63750ec5ac5f5b8e798801)) +* full ERC-8004 support — split identity into identity, reputation, validation skills ([#45](https://github.com/gregoryford963-sys/skills/issues/45)) ([d68329d](https://github.com/gregoryford963-sys/skills/commit/d68329d71fec0ef5f456639708990f29ac575f22)) +* **hermetica-yield-rotator:** add BFF Skills Comp Day 4 winner by [@cliqueengagements](https://github.com/cliqueengagements) ([#273](https://github.com/gregoryford963-sys/skills/issues/273)) ([f6e0e5d](https://github.com/gregoryford963-sys/skills/commit/f6e0e5dfb89c9d38a84981e92949743c62f8d0b0)) +* **hodlmm-bin-guardian:** add BFF Skills Comp Day 3 winner by [@cliqueengagements](https://github.com/cliqueengagements) ([#265](https://github.com/gregoryford963-sys/skills/issues/265)) ([f9dc292](https://github.com/gregoryford963-sys/skills/commit/f9dc292150e05cc32dcb8c242e483f444bcd86fd)) +* **hodlmm-pulse:** add BFF Skills Comp Day 6 winner by [@ghislo749](https://github.com/ghislo749) ([#281](https://github.com/gregoryford963-sys/skills/issues/281)) ([ffe20a1](https://github.com/gregoryford963-sys/skills/commit/ffe20a1a530fe0e800c8ce9d3e115b5a606f6566)) +* **hodlmm-range-keeper:** add BFF Skills Comp Day 12 winner by [@tearful-saw](https://github.com/tearful-saw) ([#308](https://github.com/gregoryford963-sys/skills/issues/308)) ([61f8ac1](https://github.com/gregoryford963-sys/skills/commit/61f8ac12f51f6c7ab7863d3b4f3113670cd0149b)) +* **hodlmm-risk:** add HODLMM volatility risk monitoring skill ([#251](https://github.com/gregoryford963-sys/skills/issues/251)) ([39bb078](https://github.com/gregoryford963-sys/skills/commit/39bb07849cf9485eb34e349ca790f22921edbf6b)) +* **hodlmm-signal-allocator:** add BFF Skills Comp Day 11 winner by @IamHarrie-Labs ([#303](https://github.com/gregoryford963-sys/skills/issues/303)) ([2b12f5a](https://github.com/gregoryford963-sys/skills/commit/2b12f5ae70ee3a7be78db98da475aa8af7f76cf2)) +* **inbox:** add x402-gated inbox skill (closes [#146](https://github.com/gregoryford963-sys/skills/issues/146)) ([#149](https://github.com/gregoryford963-sys/skills/issues/149)) ([387b221](https://github.com/gregoryford963-sys/skills/commit/387b2214dd2dc12e8235def935b9f3095548b5fe)) +* initial project scaffold ([d3c2928](https://github.com/gregoryford963-sys/skills/commit/d3c2928cf05a942d044fa59a7329538d3902c6bd)) +* **jingswap-cycle-agent:** add BFF Skills Comp Day 9 winner by [@teflonmusk](https://github.com/teflonmusk) ([#294](https://github.com/gregoryford963-sys/skills/issues/294)) ([237b5fd](https://github.com/gregoryford963-sys/skills/commit/237b5fdcdfbf9b972e39c86d25658e82fc547e1d)) +* **jingswap-v2:** add V2 limit-price auction skill ([#325](https://github.com/gregoryford963-sys/skills/issues/325)) ([52e6c3c](https://github.com/gregoryford963-sys/skills/commit/52e6c3c589a3e682d253d481a0701e8e2c40cc8d)) +* **jingswap:** add blind batch auction skill for STX/sBTC ([#162](https://github.com/gregoryford963-sys/skills/issues/162)) ([df91241](https://github.com/gregoryford963-sys/skills/commit/df9124161a18741c2ef9ceb73f3bbe102c27a517)) +* **jingswap:** add multi-market support (sbtc-stx + sbtc-usdcx) ([#166](https://github.com/gregoryford963-sys/skills/issues/166)) ([6a64491](https://github.com/gregoryford963-sys/skills/commit/6a644918350fecc1a0a996797c0af2205ea94d33)) +* **jingswap:** update contract names to sbtc-stx-jing / sbtc-usdcx-jing ([#199](https://github.com/gregoryford963-sys/skills/issues/199)) ([1d640f7](https://github.com/gregoryford963-sys/skills/commit/1d640f74e1733d3ca8826bb63d0f16d02955fbdc)) +* **lunarcrush:** add LunarCrush social-intelligence skill ([92119d6](https://github.com/gregoryford963-sys/skills/commit/92119d68421182c1da72a4deebe72d58198f7c44)) +* **maximumsats-wot:** add WoT trust scoring skill ([#183](https://github.com/gregoryford963-sys/skills/issues/183)) ([297f6c5](https://github.com/gregoryford963-sys/skills/commit/297f6c566629330b95e2c75298ddaabb30ff57ee)) +* **mempool-watch:** add mempool-watch skill ([#105](https://github.com/gregoryford963-sys/skills/issues/105)) ([a803503](https://github.com/gregoryford963-sys/skills/commit/a803503f78cb42230818b7ae9ffe0fa74557aee7)) +* multi-author support and full author attribution ([#97](https://github.com/gregoryford963-sys/skills/issues/97)) ([fc82ef7](https://github.com/gregoryford963-sys/skills/commit/fc82ef70bd8c9a31fcf27f091636304f33e94ecf)) +* **news:** add beat editor skill ([#306](https://github.com/gregoryford963-sys/skills/issues/306)) ([d7dd2dc](https://github.com/gregoryford963-sys/skills/commit/d7dd2dc77ec5ab2ef9d71d9df1819ed1c1ea8425)) +* **nonce-manager:** add cross-process nonce oracle with acquire/release lifecycle ([#250](https://github.com/gregoryford963-sys/skills/issues/250)) ([33f6e0b](https://github.com/gregoryford963-sys/skills/commit/33f6e0bc497bc1a76cf42840520357d7838e61cb)) +* **nostr:** add amplify-signal and amplify-text subcommands ([#92](https://github.com/gregoryford963-sys/skills/issues/92)) ([7100cd6](https://github.com/gregoryford963-sys/skills/commit/7100cd6e66d435586baf1ff9e438e03ee43d2ec0)) +* **nostr:** add nostr skill ([#73](https://github.com/gregoryford963-sys/skills/issues/73)) ([2a03684](https://github.com/gregoryford963-sys/skills/commit/2a03684d44f80ce8752251b6eff04f3099e4a5ba)) +* **nostr:** NIP-06 derivation as default, add --key-source flag ([#187](https://github.com/gregoryford963-sys/skills/issues/187)) ([6f796be](https://github.com/gregoryford963-sys/skills/commit/6f796bedd6ab50d7f69bca54aecac7dd21782018)), closes [#86](https://github.com/gregoryford963-sys/skills/issues/86) +* **onboarding:** add agent onboarding automation skill ([#81](https://github.com/gregoryford963-sys/skills/issues/81)) ([867f8f8](https://github.com/gregoryford963-sys/skills/commit/867f8f877f209033d04086c7a484389ef09a2f23)) +* **openrouter:** add OpenRouter AI integration skill ([#148](https://github.com/gregoryford963-sys/skills/issues/148)) ([bc644b0](https://github.com/gregoryford963-sys/skills/commit/bc644b0d2088025510e8d657588af8565087fc79)) +* **ordinals-p2p:** P2P ordinals trading skill ([#79](https://github.com/gregoryford963-sys/skills/issues/79)) ([a138596](https://github.com/gregoryford963-sys/skills/commit/a138596b7867949dee77e93beff206ecc1ab1865)) +* **paperboy:** add paid signal distribution skill (docs-only) ([530861a](https://github.com/gregoryford963-sys/skills/commit/530861a4e611108d368a9d1b303f0ed7d46568d2)), closes [#221](https://github.com/gregoryford963-sys/skills/issues/221) +* **paperboy:** add paid signal distribution skill (docs-only) ([#237](https://github.com/gregoryford963-sys/skills/issues/237)) ([530861a](https://github.com/gregoryford963-sys/skills/commit/530861a4e611108d368a9d1b303f0ed7d46568d2)) +* **psbt:** add PSBT construction and signing skill (closes [#144](https://github.com/gregoryford963-sys/skills/issues/144)) ([#153](https://github.com/gregoryford963-sys/skills/issues/153)) ([8010455](https://github.com/gregoryford963-sys/skills/commit/8010455f2874c1ca2cb7af174be3a939142fadc1)) +* **relay-diagnostic:** add sponsor relay health and nonce recovery skill (closes [#140](https://github.com/gregoryford963-sys/skills/issues/140)) ([#150](https://github.com/gregoryford963-sys/skills/issues/150)) ([6903de2](https://github.com/gregoryford963-sys/skills/commit/6903de2444ce7fdf8c5f795850db6cc05ffa38e1)) +* **runes:** migrate to Unisat API, add Runes support and inscription/rune transfers ([#170](https://github.com/gregoryford963-sys/skills/issues/170)) ([e77d006](https://github.com/gregoryford963-sys/skills/commit/e77d006482d349e2734da0930360732b8c4007fe)) +* **sbtc-auto-funnel:** add BFF Skills Comp Day 5 winner by [@secret-mars](https://github.com/secret-mars) ([#278](https://github.com/gregoryford963-sys/skills/issues/278)) ([05e758f](https://github.com/gregoryford963-sys/skills/commit/05e758fd152a1c5cdc1f251d4391aba91aa49b06)) +* **sbtc-yield-maximizer:** complete HODLMM routing leg ([#340](https://github.com/gregoryford963-sys/skills/issues/340)) ([58a0ab9](https://github.com/gregoryford963-sys/skills/commit/58a0ab959a424eb7680f02d8b76e7955b16f63e3)) +* **settings:** add check-relay-health subcommand (closes [#51](https://github.com/gregoryford963-sys/skills/issues/51)) ([#56](https://github.com/gregoryford963-sys/skills/issues/56)) ([6ef5b34](https://github.com/gregoryford963-sys/skills/commit/6ef5b34b6f2055e02b9403ff6f6095e005f4b95e)) +* **signing:** add BIP-322 support for bc1q and bc1p addresses ([#32](https://github.com/gregoryford963-sys/skills/issues/32)) ([d70d07c](https://github.com/gregoryford963-sys/skills/commit/d70d07c6f312dc6bca336c91515f723853b9878b)) +* skill metadata manifest and agent experience improvements ([#10](https://github.com/gregoryford963-sys/skills/issues/10)) ([235c7cf](https://github.com/gregoryford963-sys/skills/commit/235c7cfe7939711f3fbd832457baf2bb802a23b9)) +* skill-discovery-layer — workflow guides, agent configs, AGENT.md convention, credential store ([#4](https://github.com/gregoryford963-sys/skills/issues/4)) ([7f62fe5](https://github.com/gregoryford963-sys/skills/commit/7f62fe57aa156081dd464127ec4645b028a5acbb)) +* skill-mcp parity — new skills + cross-ref audit ([#243](https://github.com/gregoryford963-sys/skills/issues/243)) ([a0852fb](https://github.com/gregoryford963-sys/skills/commit/a0852fbee84a59bb8a27e7b0b8907c183d157f76)) +* **skills:** add jingswap to skills.json manifest ([#168](https://github.com/gregoryford963-sys/skills/issues/168)) ([56144be](https://github.com/gregoryford963-sys/skills/commit/56144beb14a36840064d66f49f2694007824b09a)), closes [#165](https://github.com/gregoryford963-sys/skills/issues/165) +* **skills:** add mcp-tools field to SKILL.md frontmatter ([#128](https://github.com/gregoryford963-sys/skills/issues/128)) ([#129](https://github.com/gregoryford963-sys/skills/issues/129)) ([28d8d83](https://github.com/gregoryford963-sys/skills/commit/28d8d835ca62b549361cbea596b29760519b213e)) +* **skills:** add nostr-wot, arxiv-research; update arc0btc config to v5 ([#188](https://github.com/gregoryford963-sys/skills/issues/188)) ([fa784c7](https://github.com/gregoryford963-sys/skills/commit/fa784c73fe790ffb812486aae18738208406c274)) +* **skills:** migrate to agentskills.io spec compliance ([#135](https://github.com/gregoryford963-sys/skills/issues/135)) ([03adaab](https://github.com/gregoryford963-sys/skills/commit/03adaab6a0795727fd668a9aa895998387889365)) +* **souldinals:** add soul.md inscription and collection management skill ([#159](https://github.com/gregoryford963-sys/skills/issues/159)) ([8d5f0c2](https://github.com/gregoryford963-sys/skills/commit/8d5f0c2cb72509e4904aeea203b2a9152643e891)) +* **src:** add normalized state file envelope ([#132](https://github.com/gregoryford963-sys/skills/issues/132)) ([56c1346](https://github.com/gregoryford963-sys/skills/commit/56c1346a1be4271c5a6ac8eed4edc0ef065e2c26)) +* **stacking-lottery:** add stacking-lottery skill ([#185](https://github.com/gregoryford963-sys/skills/issues/185)) ([dd0c912](https://github.com/gregoryford963-sys/skills/commit/dd0c9127ef00dd90293ec0497f1e3e319b62c158)), closes [#184](https://github.com/gregoryford963-sys/skills/issues/184) +* **stacks-market:** add prediction market skill ([#30](https://github.com/gregoryford963-sys/skills/issues/30)) ([45067ea](https://github.com/gregoryford963-sys/skills/commit/45067ea946cc291e14142403551441f9d2889048)) +* **stackspot:** add stacking lottery skill ([#31](https://github.com/gregoryford963-sys/skills/issues/31)) ([569fb3c](https://github.com/gregoryford963-sys/skills/commit/569fb3cc6c51037f8f3cdff1e859e638122f0416)) +* **styx:** add BTC→sBTC conversion skill via Styx protocol ([#78](https://github.com/gregoryford963-sys/skills/issues/78)) ([db36f98](https://github.com/gregoryford963-sys/skills/commit/db36f980cfc82cb57fbb57ef7c4684fc11347a8f)) +* sync with mcp-server v1.25.0 — Schnorr signing, Bitflow DEX, public API ([cf2ab75](https://github.com/gregoryford963-sys/skills/commit/cf2ab75723f1ed51909205ba241a9732dc65160d)) +* **taproot-multisig:** add Taproot M-of-N multisig coordination skill ([#71](https://github.com/gregoryford963-sys/skills/issues/71)) ([60a0ccc](https://github.com/gregoryford963-sys/skills/commit/60a0ccc94bc82d8aeb84441cdbed2ef79469d52b)) +* **tenero:** add tenero market analytics skill ([#125](https://github.com/gregoryford963-sys/skills/issues/125)) ([#130](https://github.com/gregoryford963-sys/skills/issues/130)) ([93cc20d](https://github.com/gregoryford963-sys/skills/commit/93cc20d8e5ff9a925370906cb1bdc81b1fc063da)) +* **test:** add unit tests for config, transactions, and services (77 tests) ([d17dda5](https://github.com/gregoryford963-sys/skills/commit/d17dda5f95900b8edeaef3fd8af49c588a13a887)) +* **transactions:** add nonce-tracker integration and retry logic to sponsor-builder ([#312](https://github.com/gregoryford963-sys/skills/issues/312)) ([ef69b08](https://github.com/gregoryford963-sys/skills/commit/ef69b08b4af9f38824c862acbae38a154f662193)) +* **transfer:** add STX, token, and NFT transfer skill (closes [#139](https://github.com/gregoryford963-sys/skills/issues/139)) ([#151](https://github.com/gregoryford963-sys/skills/issues/151)) ([530dc8c](https://github.com/gregoryford963-sys/skills/commit/530dc8cc65cb3ac41bb4d2c2029807d7c07f6220)) +* **what-to-do:** add autonomy workflows and update registration flow ([#37](https://github.com/gregoryford963-sys/skills/issues/37)) ([a7a0bd6](https://github.com/gregoryford963-sys/skills/commit/a7a0bd6ac3cb6a880ff9ecd08fc74e1335661d70)) +* **what-to-do:** add project board scanning workflow ([#74](https://github.com/gregoryford963-sys/skills/issues/74)) ([6bb4904](https://github.com/gregoryford963-sys/skills/commit/6bb490491584b508b98962dbbf612f74333bd2b5)), closes [#28](https://github.com/gregoryford963-sys/skills/issues/28) +* **what-to-do:** add setup-autonomous-loop workflow + update secret-mars config ([#22](https://github.com/gregoryford963-sys/skills/issues/22)) ([c9565f7](https://github.com/gregoryford963-sys/skills/commit/c9565f75f9a3e68e9718c2fcb0a50509b290a772)) +* **x402:** add --headers flag to execute-endpoint ([#211](https://github.com/gregoryford963-sys/skills/issues/211)) ([6b8b93a](https://github.com/gregoryford963-sys/skills/commit/6b8b93a2376fb272a00c5c06af6c0eb5aa067360)) +* **x402:** add nonce tracking and retry logic for inbox messages ([#247](https://github.com/gregoryford963-sys/skills/issues/247)) ([e2de2b1](https://github.com/gregoryford963-sys/skills/commit/e2de2b1b0c55959afe04363394a0387f97579608)), closes [#240](https://github.com/gregoryford963-sys/skills/issues/240) +* **zest-yield-manager:** add Zest yield management skill ([#246](https://github.com/gregoryford963-sys/skills/issues/246)) ([07f0df0](https://github.com/gregoryford963-sys/skills/commit/07f0df0dceb76102ae7473335759a5c43e5a6568)) + + +### Bug Fixes + +* **aibtc-news-classifieds:** handle pending_review status from POST /api/classifieds ([#202](https://github.com/gregoryford963-sys/skills/issues/202)) ([94d1977](https://github.com/gregoryford963-sys/skills/commit/94d1977b0d41e52dae0648e1573a0d4a7becc167)) +* **aibtc-news:** inherit process.env in signing subprocess + support signatureBase64 ([#362](https://github.com/gregoryford963-sys/skills/issues/362)) ([70575ee](https://github.com/gregoryford963-sys/skills/commit/70575eebb2583f9f76f98a35c07f84deb177bdde)) +* **aibtc-news:** migrate to v2 API auth headers and snake_case bodies ([#127](https://github.com/gregoryford963-sys/skills/issues/127)) ([acb4c75](https://github.com/gregoryford963-sys/skills/commit/acb4c7555d8de2972b54d7e2919e65049a5e4858)) +* align relay payment polling contract with tx-schemas ([#290](https://github.com/gregoryford963-sys/skills/issues/290)) ([8a0b532](https://github.com/gregoryford963-sys/skills/commit/8a0b532b16684417c230150a1d3fef7d3f8a0d7a)) +* align sip018-sign/hash domain params with MCP ([#50](https://github.com/gregoryford963-sys/skills/issues/50)) ([#58](https://github.com/gregoryford963-sys/skills/issues/58)) ([60ca60c](https://github.com/gregoryford963-sys/skills/commit/60ca60cf7cd6e11a41b4e481990fe0af76bf21a5)) +* **bitflow-limit-order:** address post-merge audit — fee, atomic writes, pair normalization, slippage retry ([#355](https://github.com/gregoryford963-sys/skills/issues/355)) ([7f9d804](https://github.com/gregoryford963-sys/skills/commit/7f9d804feb16c73d99a8f45d8b0a79fe0148553a)) +* **bitflow:** correct amount-in docs — human-readable decimal, not smallest units ([#26](https://github.com/gregoryford963-sys/skills/issues/26)) ([2236cc4](https://github.com/gregoryford963-sys/skills/commit/2236cc46b09f5ea2290958fc7946719c0a3c583a)) +* **bitflow:** default USDC references to USDCx ([#134](https://github.com/gregoryford963-sys/skills/issues/134)) ([852a8a9](https://github.com/gregoryford963-sys/skills/commit/852a8a9be256c87bad511322651b8554bb1bb79f)) +* **bounty-scanner:** align with bounty.drx4.xyz API ([#178](https://github.com/gregoryford963-sys/skills/issues/178)) ([8ad4248](https://github.com/gregoryford963-sys/skills/commit/8ad424855eb497ab1265be00aa166a4b6ca936cf)) +* **build:** add --target bun to fix node: built-in imports ([b2bdb4b](https://github.com/gregoryford963-sys/skills/commit/b2bdb4b1e0570a5c919939d44f3c1652a088ff5c)) +* **child-inscription:** add tapInternalKey to parent input in buildChildRevealTransaction ([#207](https://github.com/gregoryford963-sys/skills/issues/207)) ([5bfc907](https://github.com/gregoryford963-sys/skills/commit/5bfc907081f515f78fc4a895b7b88fecac1bfce2)) +* **ci:** remove pip cache — no requirements.txt in repo ([27ca92b](https://github.com/gregoryford963-sys/skills/commit/27ca92b56251fb7f19db72d3836d6231409cfe47)) +* **ci:** rename skills-ref binary to agentskills in validate-frontmatter.ts ([c1f6968](https://github.com/gregoryford963-sys/skills/commit/c1f696804d8e8acf9e743aa629cd3b8542b59358)) +* **competition-swap:** update quote from 1691 to 1596 sats (2026-05-15 market rate) ([8306dd9](https://github.com/gregoryford963-sys/skills/commit/8306dd91df6225b3576def2ff953a2556b1d6085)) +* **contract-preflight:** swap hardcoded --sender for <YOUR_STACKS_ADDRESS> placeholder ([#343](https://github.com/gregoryford963-sys/skills/issues/343)) ([7b47804](https://github.com/gregoryford963-sys/skills/commit/7b47804d34cdfc8e253d2d756a3c217061c484b4)) +* **contracts:** update ZEST_BORROW_HELPER to borrow-helper-v2-1-7 ([#341](https://github.com/gregoryford963-sys/skills/issues/341)) ([2562a62](https://github.com/gregoryford963-sys/skills/commit/2562a6266530abc67f38313bdf8e94557fbd13ed)) +* **defi:** read Zest supply from LP token balance, not reserve data ([#117](https://github.com/gregoryford963-sys/skills/issues/117)) ([6b2c2a6](https://github.com/gregoryford963-sys/skills/commit/6b2c2a6133cde485e9b05c2897cc9b45bf34e47b)) +* **erc8004:** add NFT post-condition to transferIdentity ([#109](https://github.com/gregoryford963-sys/skills/issues/109)) ([11d662d](https://github.com/gregoryford963-sys/skills/commit/11d662d221aeb7ace11a56e83653b7685e9ce1f3)) +* hermetica-yield-rotator unstake/withdraw calls wrong function names ([#314](https://github.com/gregoryford963-sys/skills/issues/314)) ([0714e58](https://github.com/gregoryford963-sys/skills/commit/0714e5880e937e7bbbbb46f4670be9f98525029e)) +* **hodlmm-flow:** detect liquidations by sender-address prefix ([#369](https://github.com/gregoryford963-sys/skills/issues/369)) ([b9a7817](https://github.com/gregoryford963-sys/skills/commit/b9a78177233c83b2533ff7e5f3b412c1cd5b0735)) +* **hodlmm-flow:** SWAP_FUNCTIONS coverage + liquidation + 429 partial results [blocking [#348](https://github.com/gregoryford963-sys/skills/issues/348)] ([#350](https://github.com/gregoryford963-sys/skills/issues/350)) ([88684c2](https://github.com/gregoryford963-sys/skills/commit/88684c2fd8cc7fe9e6e9c56be31f3528a199f18d)) +* **hodlmm-move-liquidity:** add settings to requires field on write-tagged skill ([#377](https://github.com/gregoryford963-sys/skills/issues/377)) ([b4c8f7a](https://github.com/gregoryford963-sys/skills/commit/b4c8f7aebd5c690703b89016e022db55459dd6f6)) +* **hodlmm-move-liquidity:** Apr 2026 API migration, 208→220 list cap, min-dlp cross-bin, fee floor ([#338](https://github.com/gregoryford963-sys/skills/issues/338)) ([c1026f8](https://github.com/gregoryford963-sys/skills/commit/c1026f856f44846c7f15ecf48c879d76274853bd)) +* **hodlmm-pulse:** remove unused computeTrend() and regenerate manifest ([#311](https://github.com/gregoryford963-sys/skills/issues/311)) ([0762ec0](https://github.com/gregoryford963-sys/skills/commit/0762ec0d4bae22dba9891efede9385761990a643)) +* **hodlmm-signal-allocator:** declare requires field on write-tagged skill ([#376](https://github.com/gregoryford963-sys/skills/issues/376)) ([8754df6](https://github.com/gregoryford963-sys/skills/commit/8754df6132bce521f31899f4b4828df6cf4f5f7c)) +* **inscription-builder:** fix 3 bugs in buildRevealTransaction ([#216](https://github.com/gregoryford963-sys/skills/issues/216)) ([d6648e9](https://github.com/gregoryford963-sys/skills/commit/d6648e9fa7927c982763fdb01d108785872fa2d8)) +* **lib:** add node: prefix to bare stdlib imports in src/lib ([#103](https://github.com/gregoryford963-sys/skills/issues/103)) ([615f3bf](https://github.com/gregoryford963-sys/skills/commit/615f3bf2350cc0c4eba838eea46b7aacafd4e95c)), closes [#94](https://github.com/gregoryford963-sys/skills/issues/94) +* **mempool-watch:** update mcp-tools refs for renamed btc tools ([#161](https://github.com/gregoryford963-sys/skills/issues/161)) ([879e668](https://github.com/gregoryford963-sys/skills/commit/879e668cc293495bfd9ecf9cc2f701e49f04570f)) +* pass msgBytes directly to taggedHash, removing the encodeVarInt call. ([720c90c](https://github.com/gregoryford963-sys/skills/commit/720c90cf7cdddc8c9e8ee6f713e6d0db663bc520)) +* persist wallet session across process boundaries (closes [#87](https://github.com/gregoryford963-sys/skills/issues/87)) ([#107](https://github.com/gregoryford963-sys/skills/issues/107)) ([992e0c1](https://github.com/gregoryford963-sys/skills/commit/992e0c1f993eaa4f630d7c29b1106c50930fb6dd)) +* **relay-health:** consume pool state and unify diagnostics ([#321](https://github.com/gregoryford963-sys/skills/issues/321)) ([502d60c](https://github.com/gregoryford963-sys/skills/commit/502d60cd31eb90da7b136894687c70e67d62f211)) +* replace bare catch-return-null with selective 404 guards ([#155](https://github.com/gregoryford963-sys/skills/issues/155)) ([b7d1313](https://github.com/gregoryford963-sys/skills/commit/b7d1313d570896c3397ef7b8bfd4851cc253d9db)), closes [#154](https://github.com/gregoryford963-sys/skills/issues/154) +* **rune-transfer-builder:** only set changeOutput pointer when change output is included ([0a0fd63](https://github.com/gregoryford963-sys/skills/commit/0a0fd63077f887ec84f49bd91642c17fcc22292f)), closes [#229](https://github.com/gregoryford963-sys/skills/issues/229) +* **rune-transfer-builder:** only set changeOutput pointer when change output is included ([#230](https://github.com/gregoryford963-sys/skills/issues/230)) ([0a0fd63](https://github.com/gregoryford963-sys/skills/commit/0a0fd63077f887ec84f49bd91642c17fcc22292f)) +* **runes:** add erc8004 AGENT.md and fix silent rune burn on partial transfers ([#244](https://github.com/gregoryford963-sys/skills/issues/244)) ([e3b4fd3](https://github.com/gregoryford963-sys/skills/commit/e3b4fd36d0e3f18682b3289c32d1a0b8f5a911ac)) +* **signing:** align SIP-018 domain parsing with MCP shape ([#75](https://github.com/gregoryford963-sys/skills/issues/75)) ([e91309f](https://github.com/gregoryford963-sys/skills/commit/e91309fcf5ee3051aaeb38c9660807ca0c77abb1)) +* **signing:** remove varint prepend from bip322TaggedHash ([#69](https://github.com/gregoryford963-sys/skills/issues/69)) ([720c90c](https://github.com/gregoryford963-sys/skills/commit/720c90cf7cdddc8c9e8ee6f713e6d0db663bc520)) +* **skills:** correct validation failures in openrouter and relay-diagnostic SKILL.md ([#158](https://github.com/gregoryford963-sys/skills/issues/158)) ([3247563](https://github.com/gregoryford963-sys/skills/commit/3247563d6ce7d2c77ec75764925afc74ac3e1e78)) +* **sponsor-builder:** add 0x prefix to serialized tx before relay submission ([#292](https://github.com/gregoryford963-sys/skills/issues/292)) ([e5c3140](https://github.com/gregoryford963-sys/skills/commit/e5c3140909e67d8f95973389f173bf386febb945)), closes [#268](https://github.com/gregoryford963-sys/skills/issues/268) +* **src:** bind INBOX_BASE to NETWORK + clarify HODLMM_API_BASE override ([#368](https://github.com/gregoryford963-sys/skills/issues/368)) ([8657129](https://github.com/gregoryford963-sys/skills/commit/8657129dc5bf2f1bca44d4311f42ef468edc7dbc)) +* **src:** surface SENDER_NONCE_GAP and queue management in sponsor relay ([#266](https://github.com/gregoryford963-sys/skills/issues/266)) ([2749dd9](https://github.com/gregoryford963-sys/skills/commit/2749dd9528573c6fd55f7853d93c53f29320058c)) +* **stacking-lottery:** use stacking_lottery mcp-tools instead of stackspot names ([#194](https://github.com/gregoryford963-sys/skills/issues/194)) ([4e280b3](https://github.com/gregoryford963-sys/skills/commit/4e280b33d00cb47b8c039f5f59803951f763b5a3)), closes [#190](https://github.com/gregoryford963-sys/skills/issues/190) +* **stacks-alpha-engine:** post-[#339](https://github.com/gregoryford963-sys/skills/issues/339) audit sweep (rebased) ([#367](https://github.com/gregoryford963-sys/skills/issues/367)) ([4e02afe](https://github.com/gregoryford963-sys/skills/commit/4e02afec1d2df8d9b01209113e4e008f6ec3eacf)) +* **stackspot:** accept fully qualified contract identifiers ([#44](https://github.com/gregoryford963-sys/skills/issues/44)) ([4b56455](https://github.com/gregoryford963-sys/skills/commit/4b564552e0a56dec30b9a3f34878d83a07f33187)) +* **stackspot:** add mcp-tools metadata field (closes [#145](https://github.com/gregoryford963-sys/skills/issues/145)) ([#147](https://github.com/gregoryford963-sys/skills/issues/147)) ([3790d66](https://github.com/gregoryford963-sys/skills/commit/3790d6660dcbade354283255cf471c02bd6df867)) +* **styx:** move OP_RETURN to output index 0 for Styx protocol compliance ([#85](https://github.com/gregoryford963-sys/skills/issues/85)) ([9f7c6a6](https://github.com/gregoryford963-sys/skills/commit/9f7c6a634bdde0adc9b4fa80fea68b31d152745a)) +* update scaffold, docs, and bounty-scanner for nested metadata format ([1eb033d](https://github.com/gregoryford963-sys/skills/commit/1eb033d477bf7fee87f915d0fd82f0b9559170c2)) +* update scaffold, docs, and bounty-scanner for nested metadata format ([#232](https://github.com/gregoryford963-sys/skills/issues/232)) ([1eb033d](https://github.com/gregoryford963-sys/skills/commit/1eb033d477bf7fee87f915d0fd82f0b9559170c2)) +* use v2 header name payment-required instead of x-payment-required in send-inbox-message ([9a2f3f8](https://github.com/gregoryford963-sys/skills/commit/9a2f3f8010a0575910119605ba683bffd2a5b9dd)) +* **what-to-do:** correct aibtc.com API alignment in workflow guides ([#52](https://github.com/gregoryford963-sys/skills/issues/52)) ([884ce0b](https://github.com/gregoryford963-sys/skills/commit/884ce0b9729d7394c55a042a4e5c01ab4fbeffe5)) +* **x402-retry:** track pending payment ids ([#279](https://github.com/gregoryford963-sys/skills/issues/279)) ([92ece39](https://github.com/gregoryford963-sys/skills/commit/92ece391dd73fb28b8796a4141a44a4b23216b9c)) +* **x402:** correct payment-status fallback URL to relay's /payment/{id} route ([#372](https://github.com/gregoryford963-sys/skills/issues/372)) ([e11cbd1](https://github.com/gregoryford963-sys/skills/commit/e11cbd1d54d5bf26c33aba180335705394a883ba)) +* **x402:** detect sbtc-token contract identifier in detectTokenType ([#101](https://github.com/gregoryford963-sys/skills/issues/101)) ([f6b383e](https://github.com/gregoryford963-sys/skills/commit/f6b383e59476f40221988012befc779d9a6d46ea)) +* **x402:** migrate wrangler.jsonc template to use JSONC comments ([#124](https://github.com/gregoryford963-sys/skills/issues/124)) ([431e727](https://github.com/gregoryford963-sys/skills/commit/431e727a591837f888a2706c18c73332fdba7270)), closes [#115](https://github.com/gregoryford963-sys/skills/issues/115) +* **x402:** use v2 header name for payment-required in send-inbox-message ([#59](https://github.com/gregoryford963-sys/skills/issues/59)) ([9a2f3f8](https://github.com/gregoryford963-sys/skills/commit/9a2f3f8010a0575910119605ba683bffd2a5b9dd)) +* **yield-dashboard:** read ALEX LP token balance for user positions ([#203](https://github.com/gregoryford963-sys/skills/issues/203)) ([cb51ad3](https://github.com/gregoryford963-sys/skills/commit/cb51ad362cabed164c6add10ca28d052a9f12184)) +* **zest-auto-repay:** address all 3 blocking issues from arc0btc review ([ee4bffb](https://github.com/gregoryford963-sys/skills/commit/ee4bffbd43bfad542c24428a6aa54601315c112e)) + ## [0.42.0](https://github.com/aibtcdev/skills/compare/skills-v0.41.0...skills-v0.42.0) (2026-05-11) diff --git a/challenge-stacks.ts b/challenge-stacks.ts new file mode 100644 index 00000000..6c33024e --- /dev/null +++ b/challenge-stacks.ts @@ -0,0 +1,80 @@ +import { signMessageHashRsv } from "@stacks/transactions"; +import { bytesToHex, hexToBytes } from "@stacks/common"; +import { hashSha256Sync } from "@stacks/encryption"; + +const STACKS_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const STX_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +// Step 1: Get a challenge for btcAddress to update-owner +// (so we can set our X handle, making the viral tweet easier to verify) +const challengeResp = await fetch( + `https://aibtc.com/api/challenge?address=${BTC_ADDRESS}&action=update-owner` +); +const challengeData = await challengeResp.json() as any; +console.log("Challenge:", JSON.stringify(challengeData, null, 2)); + +if (!challengeData.challenge) { + console.log("No challenge received"); + process.exit(1); +} + +const challengeMsg = challengeData.challenge.message; +console.log("\nChallenge message:", challengeMsg); + +// Step 2: Try signing with Stacks key (the STX address is also registered) +// The Stacks RSV signature format +const normalizedKey = STACKS_PRIVATE_KEY_HEX.endsWith("01") + ? STACKS_PRIVATE_KEY_HEX + : STACKS_PRIVATE_KEY_HEX + "01"; + +// Hash the message Stacks-style +function hashStacksMessage(message: string): string { + const msgBytes = new TextEncoder().encode(message); + const prefix = new TextEncoder().encode("\x17Stacks Signed Message:\n"); + const varintBytes = msgBytes.length < 253 + ? new Uint8Array([msgBytes.length]) + : new Uint8Array([0xfd, msgBytes.length & 0xff, (msgBytes.length >> 8) & 0xff]); + const combined = new Uint8Array([...prefix, ...varintBytes, ...msgBytes]); + return bytesToHex(hashSha256Sync(hashSha256Sync(combined))); +} + +// Actually use the proper Stacks signing approach +// The Stacks signature is: RSV format, hex encoded +// Based on how it was done in registration +const messageHash = hashStacksMessage(challengeMsg); +const stacksSig = signMessageHashRsv({ + messageHash, + privateKey: normalizedKey +}); +console.log("Stacks signature:", stacksSig); + +// Step 3: Submit the challenge with Stacks address + Stacks signature +const submitResp = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: STX_ADDRESS, + challenge: challengeMsg, + signature: stacksSig, + action: "update-owner", + params: { owner: "369sunray" }, + }), +}); +const submitData = await submitResp.json(); +console.log("\nChallenge submit response:", JSON.stringify(submitData, null, 2)); + +// Also try with BTC address (btcPublicKey is "" but maybe Stacks sig works too) +const submitResp2 = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: BTC_ADDRESS, + challenge: challengeMsg, + signature: stacksSig, + action: "update-owner", + params: { owner: "369sunray" }, + }), +}); +const submitData2 = await submitResp2.json(); +console.log("\nChallenge submit (btcAddress) response:", JSON.stringify(submitData2, null, 2)); diff --git a/challenge-stx.ts b/challenge-stx.ts new file mode 100644 index 00000000..e2692193 --- /dev/null +++ b/challenge-stx.ts @@ -0,0 +1,49 @@ +import { signMessageHashRsv } from "@stacks/transactions"; +import { bytesToHex } from "@stacks/common"; +import { hashSha256Sync } from "@stacks/encryption"; + +const STACKS_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const STX_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; + +// Step 1: Get challenge for STX address +const challengeResp = await fetch( + `https://aibtc.com/api/challenge?address=${STX_ADDRESS}&action=update-owner` +); +const challengeData = await challengeResp.json() as any; +console.log("Challenge:", JSON.stringify(challengeData, null, 2)); +const challengeMsg = challengeData.challenge?.message; +if (!challengeMsg) process.exit(1); + +// Step 2: Sign with Stacks key +const normalizedKey = STACKS_PRIVATE_KEY_HEX.endsWith("01") + ? STACKS_PRIVATE_KEY_HEX + : STACKS_PRIVATE_KEY_HEX + "01"; + +function hashStacksMessage(message: string): string { + const msgBytes = new TextEncoder().encode(message); + const prefix = new TextEncoder().encode("\x17Stacks Signed Message:\n"); + const varintBytes = msgBytes.length < 253 + ? new Uint8Array([msgBytes.length]) + : new Uint8Array([0xfd, msgBytes.length & 0xff, (msgBytes.length >> 8) & 0xff]); + const combined = new Uint8Array([...prefix, ...varintBytes, ...msgBytes]); + return bytesToHex(hashSha256Sync(hashSha256Sync(combined))); +} + +const messageHash = hashStacksMessage(challengeMsg); +const stacksSig = signMessageHashRsv({ messageHash, privateKey: normalizedKey }); +console.log("Stacks signature:", stacksSig); + +// Step 3: Submit challenge with STX address and Stacks signature +const submitResp = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: STX_ADDRESS, + challenge: challengeMsg, + signature: stacksSig, + action: "update-owner", + params: { owner: "369sunray" }, + }), +}); +const submitData = await submitResp.json(); +console.log("\nChallenge submit response:", JSON.stringify(submitData, null, 2)); diff --git a/challenge-stx2.ts b/challenge-stx2.ts new file mode 100644 index 00000000..008f7049 --- /dev/null +++ b/challenge-stx2.ts @@ -0,0 +1,70 @@ +import { signMessageHashRsv } from "@stacks/transactions"; +import { bytesToHex } from "@stacks/common"; +import { hashMessage } from "@stacks/encryption"; + +const STACKS_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const STX_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +// Get challenge for STX address +const challengeResp = await fetch( + `https://aibtc.com/api/challenge?address=${STX_ADDRESS}&action=update-owner` +); +const { challenge } = await challengeResp.json() as any; +const challengeMsg = challenge.message; +console.log("Challenge:", challengeMsg); + +// Sign with correct Stacks message hashing (using @stacks/encryption hashMessage) +const normalizedKey = STACKS_PRIVATE_KEY_HEX.endsWith("01") + ? STACKS_PRIVATE_KEY_HEX + : STACKS_PRIVATE_KEY_HEX + "01"; + +const msgHash = hashMessage(challengeMsg); +const msgHashHex = bytesToHex(msgHash); +console.log("Message hash:", msgHashHex); + +const signature = signMessageHashRsv({ messageHash: msgHashHex, privateKey: normalizedKey }); +console.log("Stacks signature:", signature); + +// Submit challenge to update X handle (owner) +const submitResp = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: STX_ADDRESS, + challenge: challengeMsg, + signature, + action: "update-owner", + params: { owner: "369sunray" }, + }), +}); +const result = await submitResp.json(); +console.log("\nChallenge result:", JSON.stringify(result, null, 2)); + +// Also try to get a challenge for the BTC address +// and sign with the Stacks key as a workaround (in case server accepts Stacks sig) +console.log("\n--- Trying BTC challenge with Stacks sig ---"); +const btcChallengeResp = await fetch( + `https://aibtc.com/api/challenge?address=${BTC_ADDRESS}&action=update-owner` +); +const { challenge: btcChallenge } = await btcChallengeResp.json() as any; +const btcChallengeMsg = btcChallenge.message; +console.log("BTC Challenge:", btcChallengeMsg); + +const btcMsgHash = hashMessage(btcChallengeMsg); +const btcMsgHashHex = bytesToHex(btcMsgHash); +const btcSignature = signMessageHashRsv({ messageHash: btcMsgHashHex, privateKey: normalizedKey }); + +const btcSubmitResp = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: BTC_ADDRESS, + challenge: btcChallengeMsg, + signature: btcSignature, + action: "update-owner", + params: { owner: "369sunray" }, + }), +}); +const btcResult = await btcSubmitResp.json(); +console.log("BTC challenge with Stacks sig:", JSON.stringify(btcResult, null, 2)); diff --git a/check-key.ts b/check-key.ts new file mode 100644 index 00000000..1e93df06 --- /dev/null +++ b/check-key.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env bun +// Check address from mnemonic to verify CLIENT_PRIVATE_KEY matches our STX address +import { generateWallet, getStxAddress } from "@stacks/wallet-sdk"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; + +const wallet = await generateWallet({ secretKey: MNEMONIC, password: "" }); +const account = wallet.accounts[0]; +const addrMainnet = getStxAddress(account, "mainnet"); + +console.log("STX address (mainnet):", addrMainnet); +console.log("Expected: ", "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"); +console.log("Match:", addrMainnet === "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"); +console.log("STX private key:", account.stxPrivateKey); +console.log("CLIENT_PRIVATE_KEY: ", "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"); diff --git a/check-key2.ts b/check-key2.ts new file mode 100644 index 00000000..40d9f805 --- /dev/null +++ b/check-key2.ts @@ -0,0 +1,17 @@ +#!/usr/bin/env bun +// Check what STX address CLIENT_PRIVATE_KEY corresponds to +import { generateWallet, getStxAddress } from "@stacks/wallet-sdk"; + +// Also check: derive from mnemonic using different account index +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const wallet = await generateWallet({ secretKey: MNEMONIC, password: "" }); + +// Check first few accounts +for (let i = 0; i < 5; i++) { + const acc = wallet.accounts[i] || { stxPrivateKey: "N/A" }; + const addr = acc.stxPrivateKey !== "N/A" ? getStxAddress(acc, "mainnet") : "N/A"; + console.log(`Account ${i}: ${addr}`); +} + +console.log("\nOur expected: SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"); +console.log("CLIENT_PRIVATE_KEY: 9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"); diff --git a/check-key3.ts b/check-key3.ts new file mode 100644 index 00000000..064d4024 --- /dev/null +++ b/check-key3.ts @@ -0,0 +1,41 @@ +#!/usr/bin/env bun +// Derive STX address from CLIENT_PRIVATE_KEY +// Use @stacks/wallet-sdk with raw key approach +import { generateWallet, getStxAddress } from "@stacks/wallet-sdk"; +import { makeContractCall, uintCV, principalCV, noneCV, PostConditionMode } from "@stacks/transactions"; + +// The CLIENT_PRIVATE_KEY from .env (as used in send-inbox.ts) +const key = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; + +// Try to build a simple STX transfer and see what address it comes from +// by checking the serialized tx +try { + const tx = await makeContractCall({ + contractAddress: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4", + contractName: "sbtc-token", + functionName: "transfer", + functionArgs: [uintCV(1n), principalCV("SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"), principalCV("SP1GFVV54QHZV32TD87PG7JN8J2X4WP1WB363QVHE"), noneCV()], + senderKey: key, + network: "mainnet", + postConditionMode: PostConditionMode.Allow, + fee: 1000n, + nonce: 0n, + }); + const hex = tx.serialize(); + console.log("Transaction serialized successfully"); + console.log("Hex starts with:", hex.slice(0, 80)); + + // Decode the origin address from the hex (bytes 5 onwards for version+hash) + // STX auth field starts at byte 5 + // tx format: version(1) chain_id(4) auth_type(1) ... + // For standard auth: auth_type(1) hash_mode(1) signer_hash(20) nonce(8) fee(8) ... + const authStart = 5; // skip version + chain_id + const authType = hex.slice(authStart*2, (authStart+1)*2); + const hashMode = hex.slice((authStart+1)*2, (authStart+2)*2); + const signerHash = hex.slice((authStart+2)*2, (authStart+22)*2); + console.log("Auth type:", authType); + console.log("Hash mode:", hashMode); + console.log("Signer hash160:", signerHash); +} catch (e) { + console.error("Error:", e.message); +} diff --git a/claim-beat.ts b/claim-beat.ts new file mode 100644 index 00000000..ab661ae3 --- /dev/null +++ b/claim-beat.ts @@ -0,0 +1,77 @@ +#!/usr/bin/env bun +/** + * claim-beat.ts — Claim a beat on aibtc.news using BIP-137 signature + * Usage: bun run claim-beat.ts + */ + +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const NEWS_BASE = "https://aibtc.news"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; + +const [, , beatSlug] = process.argv; +if (!beatSlug) { + console.error("Usage: bun run claim-beat.ts "); + process.exit(1); +} + +const timestamp = Math.floor(Date.now() / 1000); +const MESSAGE = `PATCH /api/beats/${beatSlug}:${timestamp}`; +console.log("Signing:", MESSAGE); + +// BIP-137 signing +const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; +function varInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; b[1] = n & 0xff; b[2] = (n >> 8) & 0xff; + return b; +} +const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; +}; +const msgBytes = new TextEncoder().encode(MESSAGE); +const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); +const lengthBytes = varInt(msgBytes.length); +const formattedMsg = concat(prefixBytes, lengthBytes, msgBytes); +const msgHash = sha256(sha256(formattedMsg)); + +const sigResult = secp256k1.sign(msgHash, privKeyBytes, { prehash: false, lowS: true, format: "recovered" }) as Uint8Array; +const recId = sigResult[0]; +const header = 39 + recId; // P2WPKH native segwit +const bip137Sig = new Uint8Array(65); +bip137Sig[0] = header; +bip137Sig.set(sigResult.slice(1, 33), 1); +bip137Sig.set(sigResult.slice(33, 65), 33); +const signature = Buffer.from(bip137Sig).toString("base64"); +console.log("Signature (65 bytes, BIP-137):", signature.slice(0, 20) + "..."); + +// PATCH the beat to claim it +const res = await fetch(`${NEWS_BASE}/api/beats/${beatSlug}`, { + method: "PATCH", + headers: { + "Content-Type": "application/json", + "X-BTC-Address": BTC_ADDRESS, + "X-BTC-Signature": signature, + "X-BTC-Timestamp": String(timestamp), + }, + body: JSON.stringify({ btc_address: BTC_ADDRESS }), +}); + +const data = await res.json(); +console.log("Status:", res.status); +console.log(JSON.stringify(data, null, 2)); diff --git a/claim-beat2.ts b/claim-beat2.ts new file mode 100644 index 00000000..8874a899 --- /dev/null +++ b/claim-beat2.ts @@ -0,0 +1,74 @@ +#!/usr/bin/env bun +/** + * claim-beat2.ts — Claim a beat via POST on aibtc.news + * Quickstart says "POST /api/beats to claim an unclaimed beat" + */ + +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const NEWS_BASE = "https://aibtc.news"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; + +const [, , beatSlug] = process.argv; +if (!beatSlug) { console.error("Usage: bun run claim-beat2.ts "); process.exit(1); } + +const timestamp = Math.floor(Date.now() / 1000); +const MESSAGE = `POST /api/beats:${timestamp}`; +console.log("Signing:", MESSAGE); + +function sign137(message: string): string { + const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; + const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; + }; + const msgBytes = new TextEncoder().encode(message); + const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); + const lenByte = new Uint8Array([msgBytes.length]); + const formattedMsg = concat(prefixBytes, lenByte, msgBytes); + const msgHash = sha256(sha256(formattedMsg)); + const sigResult = secp256k1.sign(msgHash, privKeyBytes, { prehash: false, lowS: true, format: "recovered" }) as Uint8Array; + const recId = sigResult[0]; + const header = 39 + recId; + const bip137Sig = new Uint8Array(65); + bip137Sig[0] = header; + bip137Sig.set(sigResult.slice(1, 33), 1); + bip137Sig.set(sigResult.slice(33, 65), 33); + return Buffer.from(bip137Sig).toString("base64"); +} + +const signature = sign137(MESSAGE); +console.log("Signature:", signature.slice(0, 20) + "..."); + +const res = await fetch(`${NEWS_BASE}/api/beats`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-BTC-Address": BTC_ADDRESS, + "X-BTC-Signature": signature, + "X-BTC-Timestamp": String(timestamp), + }, + body: JSON.stringify({ + slug: beatSlug, + name: "Security", + description: "Vulnerability disclosures, protocol exploits, wallet/key security events, contract audit findings, agent-targeted social engineering, and threat intelligence relevant to Bitcoin and Stacks.", + color: "#E53935", + created_by: BTC_ADDRESS, + }), +}); + +const data = await res.json(); +console.log("Status:", res.status); +console.log(JSON.stringify(data, null, 2)); diff --git a/dao-launch.ts b/dao-launch.ts new file mode 100644 index 00000000..6159cea0 --- /dev/null +++ b/dao-launch.ts @@ -0,0 +1,392 @@ +#!/usr/bin/env bun +/** + * dao-launch.ts -- Prompt-to-DAO launcher for AIBTC Bounty #31 + * + * Deploys dao-template.clar to Stacks mainnet from a natural-language prompt. + * + * Usage: + * bun run dao-launch.ts "" # deploy + initialize + * bun run dao-launch.ts deploy "" # explicit deploy + * bun run dao-launch.ts add-member + * bun run dao-launch.ts deposit + * bun run dao-launch.ts propose "" "<desc>" <amount-sats> <recipient> + * bun run dao-launch.ts vote <contract> <proposal-id> <yes|no> + * bun run dao-launch.ts execute <contract> <proposal-id> + * bun run dao-launch.ts info <contract> + */ + +import { readFileSync } from "node:fs"; +import { resolve, dirname } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + makeContractDeploy, + makeContractCall, + broadcastTransaction, + stringAsciiCV, + uintCV, + principalCV, + boolCV, + PostConditionMode, + AnchorMode, +} from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +// --------------------------------------------------------------------------- +// Config +// --------------------------------------------------------------------------- + +const PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY ?? ""; +const DEPLOYER_ADDRESS = process.env.DEPLOYER_ADDRESS ?? ""; +const __dirname = dirname(fileURLToPath(import.meta.url)); +const TEMPLATE_PATH = resolve(__dirname, "dao-template.clar"); +const HIRO_API = "https://api.mainnet.hiro.so"; + +if (!PRIVATE_KEY) { + console.error( + JSON.stringify({ error: "CLIENT_PRIVATE_KEY not set in environment" }) + ); + process.exit(1); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +/** + * Slugify the first 3 words of the prompt, prefix with "dao-", max 40 chars. + */ +function promptToContractName(prompt: string): string { + const words = prompt.trim().toLowerCase().split(/\s+/).slice(0, 3); + const slug = words.join("-").replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, ""); + const name = `dao-${slug}`.slice(0, 40); + return name; +} + +/** + * Extract a human-friendly DAO name (first 3-4 words, title-cased) from prompt. + */ +function promptToDaoName(prompt: string): string { + return prompt.trim().split(/\s+/).slice(0, 4).map( + (w) => w.charAt(0).toUpperCase() + w.slice(1) + ).join(" ").slice(0, 64); +} + +function sleep(ms: number): Promise<void> { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +/** Split "SP123.my-contract" into [address, name]. */ +function parseContractId(contractId: string): [string, string] { + const dot = contractId.lastIndexOf("."); + if (dot === -1) throw new Error(`Invalid contract ID: ${contractId}`); + return [contractId.slice(0, dot), contractId.slice(dot + 1)]; +} + +/** Call a read-only function via Hiro API (no wallet needed). */ +async function callReadOnly( + contractAddress: string, + contractName: string, + functionName: string, + args: string[] = [], + sender?: string +): Promise<unknown> { + const url = `${HIRO_API}/v2/contracts/call-read/${contractAddress}/${contractName}/${functionName}`; + const res = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + sender: sender ?? contractAddress, + arguments: args, + }), + }); + if (!res.ok) { + throw new Error(`Read-only call failed: ${res.status} ${await res.text()}`); + } + return res.json(); +} + +// --------------------------------------------------------------------------- +// Core operations +// --------------------------------------------------------------------------- + +async function deployDao(prompt: string): Promise<void> { + const contractName = promptToContractName(prompt); + const daoName = promptToDaoName(prompt); + const daoPurpose = prompt.slice(0, 256); + const codeBody = readFileSync(TEMPLATE_PATH, "utf-8"); + + // -- Deploy -- + const deployTx = await makeContractDeploy({ + contractName, + codeBody, + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const deployBroadcast = await broadcastTransaction({ + transaction: deployTx, + network: STACKS_MAINNET, + }); + + if ("error" in deployBroadcast) { + throw new Error( + `Deploy broadcast failed: ${deployBroadcast.error} -- ${String((deployBroadcast as { reason?: unknown }).reason ?? "")}` + ); + } + + const deployTxid = deployBroadcast.txid; + const contractId = `${DEPLOYER_ADDRESS}.${contractName}`; + + // Wait for nonce increment before sending initialize tx + await sleep(1000); + + // -- Initialize -- + const initTx = await makeContractCall({ + contractAddress: DEPLOYER_ADDRESS, + contractName, + functionName: "initialize", + functionArgs: [stringAsciiCV(daoName), stringAsciiCV(daoPurpose)], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const initBroadcast = await broadcastTransaction({ + transaction: initTx, + network: STACKS_MAINNET, + }); + + if ("error" in initBroadcast) { + throw new Error( + `Initialize broadcast failed: ${initBroadcast.error} -- ${String((initBroadcast as { reason?: unknown }).reason ?? "")}` + ); + } + + console.log( + JSON.stringify( + { + contractId, + deployTxid, + initTxid: initBroadcast.txid, + daoName, + daoPurpose, + instructions: [ + `Contract deployed as ${contractId}`, + `Deploy tx: https://explorer.hiro.so/txid/${deployTxid}?chain=mainnet`, + `Init tx: https://explorer.hiro.so/txid/${initBroadcast.txid}?chain=mainnet`, + "Wait for both transactions to confirm before interacting.", + `Add members: bun run dao-launch.ts add-member ${contractId} <stx-address>`, + `Deposit sBTC: bun run dao-launch.ts deposit ${contractId} <amount-sats>`, + ], + }, + null, + 2 + ) + ); +} + +async function addMember(contractId: string, newMember: string): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + + const tx = await makeContractCall({ + contractAddress, + contractName, + functionName: "add-member", + functionArgs: [principalCV(newMember)], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const broadcast = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if ("error" in broadcast) throw new Error(`Broadcast failed: ${broadcast.error}`); + + console.log(JSON.stringify({ txid: broadcast.txid, member: newMember, contractId }, null, 2)); +} + +async function depositSbtc(contractId: string, amountSats: number): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + + const tx = await makeContractCall({ + contractAddress, + contractName, + functionName: "deposit-sbtc", + functionArgs: [uintCV(amountSats)], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const broadcast = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if ("error" in broadcast) throw new Error(`Broadcast failed: ${broadcast.error}`); + + console.log(JSON.stringify({ txid: broadcast.txid, amountSats, contractId }, null, 2)); +} + +async function createProposal( + contractId: string, + title: string, + description: string, + amountSats: number, + recipient: string +): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + + const tx = await makeContractCall({ + contractAddress, + contractName, + functionName: "create-proposal", + functionArgs: [ + stringAsciiCV(title.slice(0, 128)), + stringAsciiCV(description.slice(0, 512)), + uintCV(amountSats), + principalCV(recipient), + ], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const broadcast = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if ("error" in broadcast) throw new Error(`Broadcast failed: ${broadcast.error}`); + + console.log(JSON.stringify({ txid: broadcast.txid, title, amountSats, recipient, contractId }, null, 2)); +} + +async function castVote(contractId: string, proposalId: number, voteYes: boolean): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + + const tx = await makeContractCall({ + contractAddress, + contractName, + functionName: "vote", + functionArgs: [uintCV(proposalId), boolCV(voteYes)], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const broadcast = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if ("error" in broadcast) throw new Error(`Broadcast failed: ${broadcast.error}`); + + console.log(JSON.stringify({ txid: broadcast.txid, proposalId, vote: voteYes ? "yes" : "no", contractId }, null, 2)); +} + +async function executeProposal(contractId: string, proposalId: number): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + + const tx = await makeContractCall({ + contractAddress, + contractName, + functionName: "execute-proposal", + functionArgs: [uintCV(proposalId)], + senderKey: PRIVATE_KEY, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + postConditionMode: PostConditionMode.Allow, + }); + + const broadcast = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + if ("error" in broadcast) throw new Error(`Broadcast failed: ${broadcast.error}`); + + console.log(JSON.stringify({ txid: broadcast.txid, proposalId, contractId }, null, 2)); +} + +async function getInfo(contractId: string): Promise<void> { + const [contractAddress, contractName] = parseContractId(contractId); + const result = await callReadOnly(contractAddress, contractName, "get-info", [], contractAddress); + console.log(JSON.stringify({ contractId, info: result }, null, 2)); +} + +// --------------------------------------------------------------------------- +// CLI dispatch +// --------------------------------------------------------------------------- + +async function main(): Promise<void> { + const args = process.argv.slice(2); + + if (args.length === 0) { + console.log( + JSON.stringify( + { + error: "No arguments provided", + usage: [ + 'bun run dao-launch.ts "<prompt>"', + 'bun run dao-launch.ts deploy "<prompt>"', + "bun run dao-launch.ts add-member <contract> <stx-address>", + "bun run dao-launch.ts deposit <contract> <amount-sats>", + 'bun run dao-launch.ts propose <contract> "<title>" "<desc>" <amount-sats> <recipient>', + "bun run dao-launch.ts vote <contract> <proposal-id> <yes|no>", + "bun run dao-launch.ts execute <contract> <proposal-id>", + "bun run dao-launch.ts info <contract>", + ], + }, + null, + 2 + ) + ); + process.exit(1); + } + + const subcommand = args[0]; + + // If the first arg looks like a prompt (not a known subcommand), treat as deploy + const knownSubcommands = ["deploy", "add-member", "deposit", "propose", "vote", "execute", "info"]; + if (!knownSubcommands.includes(subcommand)) { + await deployDao(args.join(" ")); + return; + } + + switch (subcommand) { + case "deploy": { + if (args.length < 2) throw new Error("deploy requires a prompt argument"); + await deployDao(args.slice(1).join(" ")); + break; + } + case "add-member": { + if (args.length < 3) throw new Error("add-member requires <contract> <stx-address>"); + await addMember(args[1], args[2]); + break; + } + case "deposit": { + if (args.length < 3) throw new Error("deposit requires <contract> <amount-sats>"); + await depositSbtc(args[1], parseInt(args[2], 10)); + break; + } + case "propose": { + if (args.length < 6) throw new Error("propose requires <contract> <title> <desc> <amount-sats> <recipient>"); + await createProposal(args[1], args[2], args[3], parseInt(args[4], 10), args[5]); + break; + } + case "vote": { + if (args.length < 4) throw new Error("vote requires <contract> <proposal-id> <yes|no>"); + const voteYes = args[3].toLowerCase() === "yes"; + await castVote(args[1], parseInt(args[2], 10), voteYes); + break; + } + case "execute": { + if (args.length < 3) throw new Error("execute requires <contract> <proposal-id>"); + await executeProposal(args[1], parseInt(args[2], 10)); + break; + } + case "info": { + if (args.length < 2) throw new Error("info requires <contract>"); + await getInfo(args[1]); + break; + } + default: + throw new Error(`Unknown subcommand: ${subcommand}`); + } +} + +main().catch((err) => { + console.log(JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2)); + process.exit(1); +}); diff --git a/dao-template.clar b/dao-template.clar new file mode 100644 index 00000000..089e3a1b --- /dev/null +++ b/dao-template.clar @@ -0,0 +1,260 @@ +;; dao-template.clar +;; Treasury + membership + voting DAO contract template. +;; Deploy with a unique contract-name per DAO. +;; Admin = tx-sender at deploy time. + +;; --------------------------------------------------------------------------- +;; External contracts +;; --------------------------------------------------------------------------- + +(define-constant SBTC-CONTRACT 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token) + +;; --------------------------------------------------------------------------- +;; Error constants +;; --------------------------------------------------------------------------- + +(define-constant ERR-NOT-MEMBER (err u1)) +(define-constant ERR-ALREADY-MEMBER (err u2)) +(define-constant ERR-NOT-ADMIN (err u3)) +(define-constant ERR-PROPOSAL-NOT-FOUND (err u4)) +(define-constant ERR-ALREADY-VOTED (err u5)) +(define-constant ERR-VOTING-CLOSED (err u6)) +(define-constant ERR-ALREADY-EXECUTED (err u7)) +(define-constant ERR-QUORUM-NOT-MET (err u8)) +(define-constant ERR-NOT-PASSED (err u9)) +(define-constant ERR-INSUFFICIENT-FUNDS (err u10)) + +;; ~1 day in blocks (~144 blocks at 10min each) +(define-constant VOTING-PERIOD u144) + +;; --------------------------------------------------------------------------- +;; Data vars +;; --------------------------------------------------------------------------- + +(define-data-var dao-name (string-ascii 64) "") +(define-data-var dao-purpose (string-ascii 256) "") +(define-data-var admin principal 'SP1C7XGRFPDHRSZECMGDEYJ7TWHFFQ03JMKE3NHCR) +(define-data-var member-count uint u0) +(define-data-var proposal-count uint u0) + +;; --------------------------------------------------------------------------- +;; Maps +;; --------------------------------------------------------------------------- + +(define-map Members + { member: principal } + { joined-at: uint, active: bool } +) + +(define-map Proposals + { proposal-id: uint } + { + proposer: principal, + title: (string-ascii 128), + description: (string-ascii 512), + amount-sats: uint, + recipient: principal, + votes-yes: uint, + votes-no: uint, + executed: bool, + cancelled: bool, + created-at: uint, + voting-ends-at: uint, + } +) + +(define-map Votes + { proposal-id: uint, voter: principal } + { vote: bool } +) + +;; --------------------------------------------------------------------------- +;; Private helpers +;; --------------------------------------------------------------------------- + +(define-private (is-admin) + (is-eq tx-sender (var-get admin)) +) + +(define-private (is-active-member (who principal)) + (match (map-get? Members { member: who }) + entry (get active entry) + false + ) +) + +;; --------------------------------------------------------------------------- +;; Public: initialize +;; --------------------------------------------------------------------------- + +(define-public (initialize + (name (string-ascii 64)) + (purpose (string-ascii 256))) + (begin + (asserts! (is-admin) ERR-NOT-ADMIN) + ;; Only callable once -- if name already set, reject + (asserts! (is-eq (var-get dao-name) "") ERR-ALREADY-MEMBER) + (var-set dao-name name) + (var-set dao-purpose purpose) + ;; Add admin as first member + (map-set Members { member: tx-sender } { joined-at: stacks-block-height, active: true }) + (var-set member-count u1) + (ok true) + ) +) + +;; --------------------------------------------------------------------------- +;; Public: add-member (admin only) +;; --------------------------------------------------------------------------- + +(define-public (add-member (new-member principal)) + (begin + (asserts! (is-admin) ERR-NOT-ADMIN) + (asserts! (is-none (map-get? Members { member: new-member })) ERR-ALREADY-MEMBER) + (map-set Members { member: new-member } { joined-at: stacks-block-height, active: true }) + (var-set member-count (+ (var-get member-count) u1)) + (ok true) + ) +) + +;; --------------------------------------------------------------------------- +;; Public: join-dao +;; Currently restricted -- membership is granted by admin only. +;; This function is present for future open-membership extension. +;; --------------------------------------------------------------------------- + +(define-public (join-dao) + (begin + (asserts! false ERR-NOT-ADMIN) + (ok true)) +) + +;; --------------------------------------------------------------------------- +;; Public: deposit-sbtc +;; --------------------------------------------------------------------------- + +(define-public (deposit-sbtc (amount uint)) + (begin + (try! (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount tx-sender (as-contract tx-sender) none)) + (ok true) + ) +) + +;; --------------------------------------------------------------------------- +;; Public: create-proposal (members only) +;; --------------------------------------------------------------------------- + +(define-public (create-proposal + (title (string-ascii 128)) + (description (string-ascii 512)) + (amount-sats uint) + (recipient principal)) + (let ((pid (+ (var-get proposal-count) u1))) + (asserts! (is-active-member tx-sender) ERR-NOT-MEMBER) + (map-set Proposals + { proposal-id: pid } + { + proposer: tx-sender, + title: title, + description: description, + amount-sats: amount-sats, + recipient: recipient, + votes-yes: u0, + votes-no: u0, + executed: false, + cancelled: false, + created-at: stacks-block-height, + voting-ends-at: (+ stacks-block-height VOTING-PERIOD), + } + ) + (var-set proposal-count pid) + (ok pid) + ) +) + +;; --------------------------------------------------------------------------- +;; Public: vote (members only, within voting period) +;; --------------------------------------------------------------------------- + +(define-public (vote (proposal-id uint) (vote-yes bool)) + (let ( + (proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) ERR-PROPOSAL-NOT-FOUND)) + ) + (asserts! (is-active-member tx-sender) ERR-NOT-MEMBER) + (asserts! (<= stacks-block-height (get voting-ends-at proposal)) ERR-VOTING-CLOSED) + (asserts! (is-none (map-get? Votes { proposal-id: proposal-id, voter: tx-sender })) ERR-ALREADY-VOTED) + (map-set Votes { proposal-id: proposal-id, voter: tx-sender } { vote: vote-yes }) + (if vote-yes + (map-set Proposals { proposal-id: proposal-id } + (merge proposal { votes-yes: (+ (get votes-yes proposal) u1) })) + (map-set Proposals { proposal-id: proposal-id } + (merge proposal { votes-no: (+ (get votes-no proposal) u1) })) + ) + (ok true) + ) +) + +;; --------------------------------------------------------------------------- +;; Public: execute-proposal +;; Anyone may call after voting ends. Marks executed=true to prevent replay. +;; --------------------------------------------------------------------------- + +(define-public (execute-proposal (proposal-id uint)) + (let ( + (proposal (unwrap! (map-get? Proposals { proposal-id: proposal-id }) ERR-PROPOSAL-NOT-FOUND)) + (yes-votes (get votes-yes proposal)) + (no-votes (get votes-no proposal)) + (total-votes (+ yes-votes no-votes)) + (quorum (+ (/ (var-get member-count) u2) u1)) + (amount (get amount-sats proposal)) + (recipient (get recipient proposal)) + ) + (asserts! (not (get executed proposal)) ERR-ALREADY-EXECUTED) + (asserts! (> stacks-block-height (get voting-ends-at proposal)) ERR-VOTING-CLOSED) + ;; Mark executed first to prevent re-entrancy + (map-set Proposals { proposal-id: proposal-id } + (merge proposal { executed: true })) + ;; Quorum and majority check + (asserts! (>= total-votes quorum) ERR-QUORUM-NOT-MET) + (asserts! (> yes-votes no-votes) ERR-NOT-PASSED) + ;; Transfer sBTC if amount > 0 (transfer itself will fail if balance insufficient) + (if (> amount u0) + (begin + (try! (as-contract (contract-call? 'SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4.sbtc-token + transfer amount (as-contract tx-sender) recipient none))) + (ok true) + ) + (ok true) + ) + ) +) + +;; --------------------------------------------------------------------------- +;; Read-only: get-proposal +;; --------------------------------------------------------------------------- + +(define-read-only (get-proposal (proposal-id uint)) + (map-get? Proposals { proposal-id: proposal-id }) +) + +;; --------------------------------------------------------------------------- +;; Read-only: get-member +;; --------------------------------------------------------------------------- + +(define-read-only (get-member (member principal)) + (map-get? Members { member: member }) +) + +;; --------------------------------------------------------------------------- +;; Read-only: get-info +;; --------------------------------------------------------------------------- + +(define-read-only (get-info) + { + dao-name: (var-get dao-name), + dao-purpose: (var-get dao-purpose), + member-count: (var-get member-count), + proposal-count: (var-get proposal-count), + } +) diff --git a/decode-addr.ts b/decode-addr.ts new file mode 100644 index 00000000..5d1c9a01 --- /dev/null +++ b/decode-addr.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env bun +// Convert hash160 to Stacks address using c32check encoding embedded in @stacks/transactions +import { addressToString } from "@stacks/transactions"; + +// Hash from CLIENT_PRIVATE_KEY signed transaction +const hash = "ea41bef47ec2fa26d25e2c5456330b260c800c97"; + +// Manually do c32 encoding +// version 22 = mainnet p2pkh = "SP" prefix +// The Stacks address format uses c32check with version byte + +// Use a helper from @stacks/transactions internals +// Actually, let's just check our SP3 address hash +// SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW +// Base-check decode to get hash160... + +// Simpler: decode from known address +// SP = version 22 (0x16) +// Use the address in a tx and extract +import { makeSTXTokenTransfer } from "@stacks/transactions"; +const tx = await makeSTXTokenTransfer({ + recipient: "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW", + amount: 1n, + senderKey: "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab", + network: "mainnet", + fee: 1000n, + nonce: 0n, +}); +const hex = tx.serialize(); +// sender hash160 is at bytes 6-25 (after version+chainId+authType+hashMode) +const senderHash = hex.slice(12, 52); // 2 bytes per hex char, offset 6 bytes = 12 chars +console.log("Sender hash160:", senderHash); + +// The recipient hash160 is in the outputs +// For STX transfer, check after the auth fields +// Let's just print the full hex and manually inspect +console.log("Full hex:", hex); diff --git a/deploy-dao-minimal.ts b/deploy-dao-minimal.ts new file mode 100644 index 00000000..ff7693f5 --- /dev/null +++ b/deploy-dao-minimal.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env bun +import { readFileSync } from "fs"; +import { makeContractDeploy, AnchorMode } from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +const privateKey = process.env.CLIENT_PRIVATE_KEY; +if (!privateKey) { console.error("CLIENT_PRIVATE_KEY not set"); process.exit(1); } + +const contractName = process.argv[2] || "dao-minimal-test"; +const contractFile = process.argv[3] || "/tmp/dao-minimal.clar"; +const codeBody = readFileSync(contractFile, "utf-8"); +console.error("Contract:", contractName, "| Size:", codeBody.length); + +const tx = await makeContractDeploy({ + contractName, + codeBody, + senderKey: privateKey, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + fee: 100000n, +}); + +const serialized = tx.serialize(); +const resp = await fetch("https://api.mainnet.hiro.so/v2/transactions", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ tx: serialized }), +}); + +const text = await resp.text(); +if (!resp.ok) { + console.log(JSON.stringify({ error: text }, null, 2)); + process.exit(1); +} + +const txid = JSON.parse(text); +console.log(JSON.stringify({ success: true, txid, contract: `SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW.${contractName}` }, null, 2)); diff --git a/deploy-dao-relay.ts b/deploy-dao-relay.ts new file mode 100644 index 00000000..384f110d --- /dev/null +++ b/deploy-dao-relay.ts @@ -0,0 +1,37 @@ +#!/usr/bin/env bun +// Deploy via /relay endpoint (no auth required) +import { readFileSync } from "fs"; +import { makeContractDeploy, AnchorMode } from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +const privateKey = process.env.CLIENT_PRIVATE_KEY; +if (!privateKey) { console.error("CLIENT_PRIVATE_KEY not set"); process.exit(1); } + +const contractName = process.argv[2] || "dao-template-v4"; +const contractFile = process.argv[3] || "/home/gregoryford963/aibtcdev-skills/dao-template.clar"; +const codeBody = readFileSync(contractFile, "utf-8"); +console.error("Contract:", contractName, "| Size:", codeBody.length); + +const tx = await makeContractDeploy({ + contractName, + codeBody, + senderKey: privateKey, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + fee: 0n, + sponsored: true, +}); + +const serialized = tx.serialize(); +console.error("Serialized tx length:", serialized.length); + +// Try /relay endpoint — no auth needed +const resp = await fetch("https://x402-relay.aibtc.com/relay", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ transaction: serialized }), +}); + +const text = await resp.text(); +console.error("Status:", resp.status); +console.log(text); diff --git a/deploy-dao-sponsored.ts b/deploy-dao-sponsored.ts new file mode 100644 index 00000000..05c2f75b --- /dev/null +++ b/deploy-dao-sponsored.ts @@ -0,0 +1,48 @@ +#!/usr/bin/env bun +import { readFileSync } from "fs"; +import { makeContractDeploy, AnchorMode, ClarityVersion } from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +const privateKey = process.env.CLIENT_PRIVATE_KEY; +if (!privateKey) { console.error("CLIENT_PRIVATE_KEY not set"); process.exit(1); } + +const contractName = process.argv[2] || "dao-template-v4"; +const contractFile = process.argv[3] || "/home/gregoryford963/aibtcdev-skills/dao-template.clar"; +const apiKey = process.env.SPONSOR_API_KEY || ""; +const codeBody = readFileSync(contractFile, "utf-8"); +console.error("Contract:", contractName, "| Size:", codeBody.length, "| Sponsored:", !apiKey ? "no-key" : "yes"); + +// Get current account nonce from chain to avoid relay nonce gaps +const nonceResp = await fetch(`https://api.mainnet.hiro.so/v2/accounts/${process.argv[4] || "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"}?proof=0`); +const nonceData = await nonceResp.json() as { nonce: number }; +const nonce = nonceData.nonce; +console.error("Chain nonce:", nonce); + +const tx = await makeContractDeploy({ + contractName, + codeBody, + senderKey: privateKey, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + fee: 0n, + sponsored: true, + clarityVersion: ClarityVersion.Clarity3, + nonce, +}); + +const serialized = tx.serialize(); +console.error("Serialized tx length:", serialized.length); + +const relayUrl = "https://x402-relay.aibtc.com"; +const headers: Record<string, string> = { "Content-Type": "application/json" }; +if (apiKey) headers["Authorization"] = `Bearer ${apiKey}`; + +const resp = await fetch(`${relayUrl}/sponsor`, { + method: "POST", + headers, + body: JSON.stringify({ transaction: serialized }), +}); + +const text = await resp.text(); +console.error("Relay status:", resp.status); +console.log(text); diff --git a/deploy-dao-v3.ts b/deploy-dao-v3.ts new file mode 100644 index 00000000..f6c50330 --- /dev/null +++ b/deploy-dao-v3.ts @@ -0,0 +1,54 @@ +#!/usr/bin/env bun +/** + * deploy-dao-v3.ts — Deploy dao-template-v3 for Bounty #31 (Prompt-to-DAO) + */ + +import { readFileSync } from "fs"; +import { + makeContractDeploy, + broadcastTransaction, + AnchorMode, +} from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +const privateKey = process.env.CLIENT_PRIVATE_KEY; +if (!privateKey) { + console.error(JSON.stringify({ error: "CLIENT_PRIVATE_KEY not set in .env" })); + process.exit(1); +} + +const codeBody = readFileSync( + new URL("./dao-template.clar", import.meta.url).pathname, + "utf-8" +); + +console.error("Contract size:", codeBody.length, "chars"); + +const tx = await makeContractDeploy({ + contractName: "dao-template-v3", + codeBody, + senderKey: privateKey, + network: STACKS_MAINNET, + anchorMode: AnchorMode.Any, + fee: 3000000n, +}); + +const response = await broadcastTransaction({ + transaction: tx, + network: STACKS_MAINNET, +}); + +if ("error" in response) { + console.log(JSON.stringify({ + error: response.error, + reason: (response as any).reason, + reason_data: (response as any).reason_data, + }, null, 2)); + process.exit(1); +} + +console.log(JSON.stringify({ + success: true, + txid: response.txid, + contract: `SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW.dao-template-v3`, +}, null, 2)); diff --git a/derive-stx-key.ts b/derive-stx-key.ts new file mode 100644 index 00000000..41df09f1 --- /dev/null +++ b/derive-stx-key.ts @@ -0,0 +1,15 @@ +#!/usr/bin/env bun +/** + * Derive Stacks private key from mnemonic + */ +import { generateWallet, getStxAddress } from "@stacks/wallet-sdk"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; + +const wallet = await generateWallet({ secretKey: MNEMONIC, password: "" }); +const account = wallet.accounts[0]; +// 1 = mainnet +const stxAddress = getStxAddress({ account, transactionVersion: 1 }); +console.log("STX Address:", stxAddress); +console.log("Private Key:", account.stxPrivateKey); +console.log("Data Private Key:", account.dataPrivateKey); diff --git a/file-signal-direct.ts b/file-signal-direct.ts new file mode 100644 index 00000000..28beaec6 --- /dev/null +++ b/file-signal-direct.ts @@ -0,0 +1,131 @@ +#!/usr/bin/env bun +/** + * Direct signal filing script — bypasses wallet CLI subprocess issue. + * Signs BIP-322 inline from mnemonic (same approach as heartbeat3.ts). + * + * Usage: bun run file-signal-direct.ts --beat <slug> --headline <text> --content <text> [--sources <json>] [--tags <json>] [--disclosure <text>] + */ +import { p2wpkh, Transaction, RawTx, RawWitness, Script } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const NEWS_API_BASE = "https://aibtc.news/api"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +const p2wpkhOutput = p2wpkh(pubKeyBytes); +const scriptPubKey = p2wpkhOutput.script; + +function taggedHash(tag: string, data: Uint8Array): Uint8Array { + const tagHash = hashSha256Sync(new TextEncoder().encode(tag)); + return hashSha256Sync(concatBytes(tagHash, tagHash, data)); +} + +function bip322TaggedHash(message: string): Uint8Array { + return taggedHash("BIP0322-signed-message", new TextEncoder().encode(message)); +} + +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} + +function bip322BuildToSpendTxId(message: string, spk: Uint8Array): Uint8Array { + const msgHash = bip322TaggedHash(message); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + const rawTx = RawTx.encode({ + version: 0, + segwitFlag: false, + inputs: [{ txid: new Uint8Array(32), index: 0xffffffff, finalScriptSig: scriptSig, sequence: 0 }], + outputs: [{ amount: 0n, script: spk }], + witnesses: [], + lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +function signBip322(message: string): string { + const toSpendTxid = bip322BuildToSpendTxId(message, scriptPubKey); + const toSignTx = new Transaction({ version: 0, lockTime: 0, allowUnknownOutputs: true }); + toSignTx.addInput({ + txid: toSpendTxid, index: 0, + sequence: 0, + witnessUtxo: { amount: 0n, script: scriptPubKey }, + }); + toSignTx.addOutput({ script: Script.encode(["RETURN"]), amount: 0n }); + toSignTx.signIdx(privKeyBytes, 0); + toSignTx.finalizeIdx(0); + const input = toSignTx.getInput(0); + if (!input.finalScriptWitness) throw new Error("No witness produced"); + const encodedWitness = RawWitness.encode(input.finalScriptWitness); + return Buffer.from(encodedWitness).toString("base64"); +} + +// Parse args +const args = process.argv.slice(2); +function getArg(flag: string): string | undefined { + const idx = args.indexOf(flag); + return idx !== -1 ? args[idx + 1] : undefined; +} + +const beat = getArg("--beat"); +const headline = getArg("--headline"); +const content = getArg("--content"); +const sourcesRaw = getArg("--sources") ?? "[]"; +const tagsRaw = getArg("--tags") ?? "[]"; +const disclosureRaw = getArg("--disclosure"); + +if (!beat || !headline || !content) { + console.error("Usage: bun run file-signal-direct.ts --beat <slug> --headline <text> --content <text>"); + process.exit(1); +} + +const timestamp = Math.floor(Date.now() / 1000); +const message = `POST /api/signals:${timestamp}`; +console.log(`Signing message: ${message}`); + +const signature = signBip322(message); +console.log(`Signature: ${signature.substring(0, 40)}...`); + +const body: Record<string, unknown> = { + beat_slug: beat, + btc_address: BTC_ADDRESS, + headline, + content, + sources: JSON.parse(sourcesRaw), + tags: JSON.parse(tagsRaw), +}; + +if (disclosureRaw) body.disclosure = disclosureRaw; + +console.log("\nRequest body:", JSON.stringify(body, null, 2)); + +console.log(`\nFiling signal to beat: ${beat}`); +console.log(`Headline: ${headline}`); +console.log(`Content length: ${content.length} chars`); + +const res = await fetch(`${NEWS_API_BASE}/signals`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-BTC-Address": BTC_ADDRESS, + "X-BTC-Signature": signature, + "X-BTC-Timestamp": String(timestamp), + }, + body: JSON.stringify(body), +}); + +const text = await res.text(); +console.log(`\nResponse ${res.status}:`); +try { + console.log(JSON.stringify(JSON.parse(text), null, 2)); +} catch { + console.log(text); +} diff --git a/file-signal.ts b/file-signal.ts new file mode 100644 index 00000000..32c67d60 --- /dev/null +++ b/file-signal.ts @@ -0,0 +1,80 @@ +#!/usr/bin/env bun +/** + * file-signal.ts — File a signal on aibtc.news + * Usage: bun run file-signal.ts <beat-slug> "<headline>" "<body>" "<tag1,tag2>" + */ + +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const NEWS_BASE = "https://aibtc.news"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; + +const [, , beatSlug, headline, body, tagsStr, sourceUrl, sourceTitle] = process.argv; +if (!beatSlug || !headline) { + console.error("Usage: bun run file-signal.ts <beat-slug> '<headline>' '[body]' '[tag1,tag2]' '[sourceUrl]' '[sourceTitle]'"); + process.exit(1); +} + +function sign137(message: string): string { + const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; + const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; + }; + const msgBytes = new TextEncoder().encode(message); + const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); + const lenByte = new Uint8Array([msgBytes.length]); + const formattedMsg = concat(prefixBytes, lenByte, msgBytes); + const msgHash = sha256(sha256(formattedMsg)); + const sigResult = secp256k1.sign(msgHash, privKeyBytes, { prehash: false, lowS: true, format: "recovered" }) as Uint8Array; + const recId = sigResult[0]; + const header = 39 + recId; + const bip137Sig = new Uint8Array(65); + bip137Sig[0] = header; + bip137Sig.set(sigResult.slice(1, 33), 1); + bip137Sig.set(sigResult.slice(33, 65), 33); + return Buffer.from(bip137Sig).toString("base64"); +} + +const timestamp = Math.floor(Date.now() / 1000); +const MESSAGE = `POST /api/signals:${timestamp}`; +const signature = sign137(MESSAGE); + +const tags = tagsStr ? tagsStr.split(",").map(t => t.trim()) : ["security"]; +const payload = { + beat_slug: beatSlug, + btc_address: BTC_ADDRESS, + headline, + ...(body && { body }), + sources: [{ url: sourceUrl || "https://aibtc.com", title: sourceTitle || "AIBTC Network" }], + tags, + disclosure: "Claude claude-sonnet-4-6, aibtc-skills", + signature, +}; + +const res = await fetch(`${NEWS_BASE}/api/signals`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-BTC-Address": BTC_ADDRESS, + "X-BTC-Signature": signature, + "X-BTC-Timestamp": String(timestamp), + }, + body: JSON.stringify(payload), +}); + +const data = await res.json(); +console.log("Status:", res.status); +console.log(JSON.stringify(data, null, 2)); diff --git a/get-claim-bip137.ts b/get-claim-bip137.ts new file mode 100644 index 00000000..9b89db40 --- /dev/null +++ b/get-claim-bip137.ts @@ -0,0 +1,43 @@ +// Try claim code with BIP-137 signature +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +console.log("Address:", p2wpkh(pubKeyBytes).address); + +// BIP-137 format +const prefix = new TextEncoder().encode("\x18Bitcoin Signed Message:\n"); +const msgBytes = new TextEncoder().encode(MESSAGE); +const varint = msgBytes.length < 0xfd ? new Uint8Array([msgBytes.length]) : new Uint8Array([0xfd, msgBytes.length & 0xff, (msgBytes.length >> 8) & 0xff]); +const combined = new Uint8Array([...prefix, ...varint, ...msgBytes]); +const msgHash = sha256(sha256(combined)); + +const sig = secp256k1.sign(msgHash, privKeyBytes, { prehash: false, lowS: true, format: "recovered" }) as Uint8Array; +const recId = sig[0]; +const header = 39 + recId; // P2WPKH base +const bip137 = new Uint8Array(65); +bip137[0] = header; +bip137.set(sig.slice(1, 33), 1); +bip137.set(sig.slice(33, 65), 33); + +const sigB64 = Buffer.from(bip137).toString("base64"); +console.log("Signature (BIP-137):", sigB64); + +const resp = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ btcAddress: BTC_ADDRESS, bitcoinSignature: sigB64 }), +}); +const data = await resp.json(); +console.log("\nResponse:", JSON.stringify(data, null, 2)); diff --git a/get-claim-code.ts b/get-claim-code.ts new file mode 100644 index 00000000..19d9d8b8 --- /dev/null +++ b/get-claim-code.ts @@ -0,0 +1,157 @@ +import { hex } from "@scure/base"; +import { p2wpkh, Transaction } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; + +const BTC_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +function doubleSha256(data: Uint8Array): Uint8Array { + return sha256(sha256(data)); +} + +function taggedHash(tag: string, data: Uint8Array): Uint8Array { + const tagHash = sha256(new TextEncoder().encode(tag)); + const combined = new Uint8Array(tagHash.length * 2 + data.length); + combined.set(tagHash, 0); + combined.set(tagHash, tagHash.length); + combined.set(data, tagHash.length * 2); + return sha256(combined); +} + +function varInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + if (n <= 0xffff) { + const b = new Uint8Array(3); + b[0] = 0xfd; new DataView(b.buffer).setUint16(1, n, true); + return b; + } + const b = new Uint8Array(5); + b[0] = 0xfe; new DataView(b.buffer).setUint32(1, n, true); + return b; +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgBytes = new TextEncoder().encode(message); + const msgHash = taggedHash("BIP0322-signed-message", msgBytes); + + const scriptSig = new Uint8Array([ + 0x00, // OP_0 + 0x20, // push 32 bytes + ...msgHash, + ]); + + const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; + }; + + const raw = concat( + // version = 0 + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + // vin count = 1 + new Uint8Array([0x01]), + // prev txid = 32 zero bytes + new Uint8Array(32), + // prev vout = 0xFFFFFFFF + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + // scriptSig + varInt(scriptSig.length), scriptSig, + // sequence = 0 + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + // vout count = 1 + new Uint8Array([0x01]), + // value = 0 (8 bytes LE) + new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + // scriptPubKey + varInt(scriptPubKey.length), scriptPubKey, + // locktime = 0 + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + ); + + const txid = doubleSha256(raw); + txid.reverse(); + return txid; +} + +const privKeyBytes = hex.decode(BTC_PRIVATE_KEY_HEX); +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +const p2wpkhOutput = p2wpkh(pubKeyBytes); +const scriptPubKey = p2wpkhOutput.script; + +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); + +// Build to_sign transaction +const toSignTx = new Transaction({ allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, + index: 0, + witnessUtxo: { script: scriptPubKey, amount: BigInt(0) }, + sequence: 0, +}); +toSignTx.addOutput({ script: new Uint8Array([0x6a]), amount: BigInt(0) }); // OP_RETURN + +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalize(); + +// Extract witness items +const input = toSignTx.getInput(0); +const witness = input.finalScriptWitness as Uint8Array[]; +if (!witness || witness.length === 0) { + throw new Error("No witness found after signing"); +} + +function serializeWitness(items: Uint8Array[]): Uint8Array { + const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; + }; + const parts: Uint8Array[] = [varInt(items.length)]; + for (const item of items) { + parts.push(varInt(item.length)); + parts.push(item); + } + return concat(...parts); +} + +const witnessBytes = serializeWitness(witness); +const signatureBase64 = Buffer.from(witnessBytes).toString("base64"); + +console.log("Message:", MESSAGE); +console.log("BIP-322 signature (base64):", signatureBase64); + +// Try different field names +const url = `https://aibtc.com/api/claims/code`; + +// Try 1: body only with both fields +const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + btcAddress: BTC_ADDRESS, + bitcoinSignature: signatureBase64, + }), +}); +const data1 = await response.json(); +console.log("Attempt 1 (body only):", JSON.stringify(data1)); + +// Try 2: query param + body +const url2 = `https://aibtc.com/api/claims/code?btcAddress=${encodeURIComponent(BTC_ADDRESS)}&address=${encodeURIComponent(BTC_ADDRESS)}`; +const response2 = await fetch(url2, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + bitcoinSignature: signatureBase64, + }), +}); +const data2 = await response2.json(); +console.log("Attempt 2 (query+body no btcAddress in body):", JSON.stringify(data2)); + diff --git a/get-claim-code2.ts b/get-claim-code2.ts new file mode 100644 index 00000000..3cb474e7 --- /dev/null +++ b/get-claim-code2.ts @@ -0,0 +1,58 @@ +// Regenerate claim code using correct BIP-322 (segwitFlag:false) +import { p2wpkh, Transaction, RawTx, RawWitness, Script } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +const scriptPubKey = p2wpkh(pubKeyBytes).script; + +function taggedHash(tag: string, data: Uint8Array): Uint8Array { + const tagHash = hashSha256Sync(new TextEncoder().encode(tag)); + return hashSha256Sync(concatBytes(tagHash, tagHash, data)); +} +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = taggedHash("BIP0322-signed-message", new TextEncoder().encode(message)); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + const rawTx = RawTx.encode({ + version: 0, segwitFlag: false, + inputs: [{ txid: new Uint8Array(32), index: 0xffffffff, finalScriptSig: scriptSig, sequence: 0 }], + outputs: [{ amount: 0n, script: scriptPubKey }], + witnesses: [], lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); +const toSignTx = new Transaction({ version: 0, lockTime: 0, allowUnknownOutputs: true }); +toSignTx.addInput({ txid: toSpendTxid, index: 0, sequence: 0, witnessUtxo: { amount: 0n, script: scriptPubKey } }); +toSignTx.addOutput({ script: Script.encode(["RETURN"]), amount: 0n }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalizeIdx(0); + +const input = toSignTx.getInput(0); +if (!input.finalScriptWitness) throw new Error("No witness"); +const sig = Buffer.from(RawWitness.encode(input.finalScriptWitness)).toString("base64"); +console.log("Message:", MESSAGE); +console.log("Signature (BIP-322):", sig); + +const resp = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ btcAddress: BTC_ADDRESS, bitcoinSignature: sig }), +}); +const data = await resp.json(); +console.log("\nResponse:", JSON.stringify(data, null, 2)); diff --git a/heartbeat.ts b/heartbeat.ts new file mode 100644 index 00000000..e4e31da8 --- /dev/null +++ b/heartbeat.ts @@ -0,0 +1,100 @@ +// Send AIBTC heartbeat using BIP-322 BTC signature +import { hex } from "@scure/base"; +import { p2wpkh, Transaction, RawTx } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +// Derive BTC key +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +const p2wpkhOutput = p2wpkh(pubKeyBytes); +const scriptPubKey = p2wpkhOutput.script; + +// Generate fresh timestamp +const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z"); +const MESSAGE = `AIBTC Check-In | ${timestamp}`; +console.log("Message:", MESSAGE); + +// BIP-322 signing with MCP server's varint approach +function encodeVarInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const buf = new Uint8Array(3); + buf[0] = 0xfd; buf[1] = n & 0xff; buf[2] = (n >> 8) & 0xff; + return buf; +} + +function bip322TaggedHash(message: string): Uint8Array { + const tagBytes = new TextEncoder().encode("BIP0322-signed-message"); + const tagHash = hashSha256Sync(tagBytes); + const msgBytes = new TextEncoder().encode(message); + const varint = encodeVarInt(msgBytes.length); + const msgPart = concatBytes(varint, msgBytes); + return hashSha256Sync(concatBytes(tagHash, tagHash, msgPart)); +} + +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = bip322TaggedHash(message); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + const rawTx = RawTx.encode({ + version: 0, + inputs: [{ txid: new Uint8Array(32), index: 0xffffffff, finalScriptSig: scriptSig, sequence: 0 }], + outputs: [{ amount: 0n, script: scriptPubKey }], + lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); + +const toSignTx = new Transaction({ allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, index: 0, + witnessUtxo: { script: scriptPubKey, amount: BigInt(0) }, + sequence: 0, +}); +toSignTx.addOutput({ script: new Uint8Array([0x6a]), amount: BigInt(0) }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalize(); + +const witness = toSignTx.getInput(0).finalScriptWitness as Uint8Array[]; +function serializeWitness(items: Uint8Array[]): Uint8Array { + const parts: Uint8Array[] = [encodeVarInt(items.length)]; + for (const item of items) { parts.push(encodeVarInt(item.length)); parts.push(item); } + const total = parts.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const p of parts) { result.set(p, off); off += p.length; } + return result; +} + +const signatureBase64 = Buffer.from(serializeWitness(witness)).toString("base64"); +console.log("Signature:", signatureBase64); + +// POST heartbeat +const response = await fetch("https://aibtc.com/api/heartbeat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + signature: signatureBase64, + timestamp, + btcAddress: BTC_ADDRESS, + }), +}); + +const data = await response.json(); +console.log("\nHeartbeat response:", JSON.stringify(data, null, 2)); diff --git a/heartbeat2.ts b/heartbeat2.ts new file mode 100644 index 00000000..551e08d2 --- /dev/null +++ b/heartbeat2.ts @@ -0,0 +1,89 @@ +// Heartbeat with standard BIP-322 (no varint in tagged hash) +import { p2wpkh, Transaction, RawTx } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +const p2wpkhOutput = p2wpkh(pubKeyBytes); +const scriptPubKey = p2wpkhOutput.script; + +const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z"); +const MESSAGE = `AIBTC Check-In | ${timestamp}`; +console.log("Message:", MESSAGE); + +function encodeVarInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const buf = new Uint8Array(3); + buf[0] = 0xfd; buf[1] = n & 0xff; buf[2] = (n >> 8) & 0xff; + return buf; +} + +// Standard BIP-322 tagged hash (NO varint prefix) +function bip322TaggedHashStandard(message: string): Uint8Array { + const tagBytes = new TextEncoder().encode("BIP0322-signed-message"); + const tagHash = hashSha256Sync(tagBytes); + const msgBytes = new TextEncoder().encode(message); + return hashSha256Sync(concatBytes(tagHash, tagHash, msgBytes)); +} + +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = bip322TaggedHashStandard(message); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + const rawTx = RawTx.encode({ + version: 0, + inputs: [{ txid: new Uint8Array(32), index: 0xffffffff, finalScriptSig: scriptSig, sequence: 0 }], + outputs: [{ amount: 0n, script: scriptPubKey }], + lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); +const toSignTx = new Transaction({ allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, index: 0, + witnessUtxo: { script: scriptPubKey, amount: BigInt(0) }, + sequence: 0, +}); +toSignTx.addOutput({ script: new Uint8Array([0x6a]), amount: BigInt(0) }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalize(); + +const witness = toSignTx.getInput(0).finalScriptWitness as Uint8Array[]; +function serializeWitness(items: Uint8Array[]): Uint8Array { + const parts: Uint8Array[] = [encodeVarInt(items.length)]; + for (const item of items) { parts.push(encodeVarInt(item.length)); parts.push(item); } + const total = parts.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const p of parts) { result.set(p, off); off += p.length; } + return result; +} + +const signatureBase64 = Buffer.from(serializeWitness(witness)).toString("base64"); +console.log("Signature (standard, no varint):", signatureBase64); + +const response = await fetch("https://aibtc.com/api/heartbeat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ signature: signatureBase64, timestamp, btcAddress: BTC_ADDRESS }), +}); + +const data = await response.json(); +console.log("\nHeartbeat response:", JSON.stringify(data, null, 2)); diff --git a/heartbeat3.ts b/heartbeat3.ts new file mode 100644 index 00000000..215d54b7 --- /dev/null +++ b/heartbeat3.ts @@ -0,0 +1,79 @@ +// Heartbeat using exact BIP-322 pattern from signing/signing.ts +import { p2wpkh, Transaction, RawTx, RawWitness, Script } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +const p2wpkhOutput = p2wpkh(pubKeyBytes); +const scriptPubKey = p2wpkhOutput.script; + +const timestamp = new Date().toISOString().replace(/\.\d{3}Z$/, ".000Z"); +const MESSAGE = `AIBTC Check-In | ${timestamp}`; +console.log("Message:", MESSAGE); + +// BIP-322 tagged hash (no varint — correct per spec) +function taggedHash(tag: string, data: Uint8Array): Uint8Array { + const tagHash = hashSha256Sync(new TextEncoder().encode(tag)); + return hashSha256Sync(concatBytes(tagHash, tagHash, data)); +} + +function bip322TaggedHash(message: string): Uint8Array { + return taggedHash("BIP0322-signed-message", new TextEncoder().encode(message)); +} + +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = bip322TaggedHash(message); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + // segwitFlag: false forces legacy serialization (no 0x00 0x01 marker bytes) + const rawTx = RawTx.encode({ + version: 0, + segwitFlag: false, + inputs: [{ txid: new Uint8Array(32), index: 0xffffffff, finalScriptSig: scriptSig, sequence: 0 }], + outputs: [{ amount: 0n, script: scriptPubKey }], + witnesses: [], + lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); +const toSignTx = new Transaction({ version: 0, lockTime: 0, allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, index: 0, + sequence: 0, + witnessUtxo: { amount: 0n, script: scriptPubKey }, +}); +toSignTx.addOutput({ script: Script.encode(["RETURN"]), amount: 0n }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalizeIdx(0); + +const input = toSignTx.getInput(0); +if (!input.finalScriptWitness) throw new Error("No witness produced"); + +const encodedWitness = RawWitness.encode(input.finalScriptWitness); +const signatureBase64 = Buffer.from(encodedWitness).toString("base64"); +console.log("Signature:", signatureBase64); + +const response = await fetch("https://aibtc.com/api/heartbeat", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ signature: signatureBase64, timestamp, btcAddress: BTC_ADDRESS }), +}); + +const data = await response.json(); +console.log("\nHeartbeat response:", JSON.stringify(data, null, 2)); diff --git a/package.json b/package.json index 01c66c7e..2ad10b6b 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "@aibtc/skills", - "version": "0.42.0", + "version": "0.43.0", "description": "Claude Code skills for Bitcoin/Stacks blockchain operations — flat SKILL.md + colocated TypeScript CLI scripts.", "type": "module", "scripts": { - "build": "bun build src/lib/index.ts --outdir dist", + "build": "bun build src/lib/index.ts --outdir dist --target bun", "typecheck": "tsc --noEmit", "manifest": "bun run scripts/generate-manifest.ts", "validate": "bun run scripts/validate-frontmatter.ts", diff --git a/probe-inbox.ts b/probe-inbox.ts new file mode 100644 index 00000000..ba5fc0e8 --- /dev/null +++ b/probe-inbox.ts @@ -0,0 +1,25 @@ +#!/usr/bin/env bun +// Probe inbox 402 challenge + +const recipientBtcAddress = "bc1qjj6nnd4ngpw2l84fynhal0wzwxfzmnltuw2884"; +const body = { + toBtcAddress: recipientBtcAddress, + toStxAddress: "SP1GFVV54QHZV32TD87PG7JN8J2X4WP1WB363QVHE", + content: "test", +}; + +const res = await fetch(`https://aibtc.com/api/inbox/${recipientBtcAddress}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), +}); + +console.log("Status:", res.status); +const paymentHeader = res.headers.get("payment-required"); +if (paymentHeader) { + const decoded = JSON.parse(Buffer.from(paymentHeader, "base64").toString("utf-8")); + console.log("Payment required:", JSON.stringify(decoded, null, 2)); +} else { + const text = await res.text(); + console.log("Response:", text.slice(0, 500)); +} diff --git a/provision-relay-key.ts b/provision-relay-key.ts new file mode 100644 index 00000000..04b397e2 --- /dev/null +++ b/provision-relay-key.ts @@ -0,0 +1,50 @@ +#!/usr/bin/env bun +// Provision a sponsor relay API key via Stacks signature +import { signWithKey, privateKeyToPublic, hexToBytes } from "@stacks/transactions"; + +const privateKey = process.env.CLIENT_PRIVATE_KEY; +if (!privateKey) { console.error("CLIENT_PRIVATE_KEY not set"); process.exit(1); } + +const stxAddress = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const timestamp = new Date().toISOString(); +const message = `Bitcoin will be the currency of AIs | ${timestamp}`; + +console.error("Message:", message); + +// Sign the message using Stacks signing (structured message hash) +// Stacks signature: sha256(sha256("Stacks Signed Message:\n" + message)) +import { createHash } from "crypto"; + +function hashMessage(msg: string): Uint8Array { + const prefix = "Stacks Signed Message:\n"; + const prefixed = prefix + msg; + const buf = Buffer.from(prefixed, "utf-8"); + const h1 = createHash("sha256").update(buf).digest(); + return createHash("sha256").update(h1).digest(); +} + +const msgHash = hashMessage(message); +const msgHashHex = Buffer.from(msgHash).toString("hex"); +console.error("Message hash:", msgHashHex); + +// Sign with private key +const sig = signWithKey({ data: msgHashHex, type: "secp256k1" } as any, privateKey); +const sigHex = typeof sig === "string" ? sig : (sig as any).data; +console.error("Signature:", sigHex.slice(0, 20) + "..."); + +const body = { + stxAddress, + signature: "0x" + sigHex, + message, +}; + +console.error("POSTing to relay..."); +const resp = await fetch("https://x402-relay.aibtc.com/keys/provision-stx", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), +}); + +const text = await resp.text(); +console.error("Status:", resp.status); +console.log(text); diff --git a/register-identity.ts b/register-identity.ts new file mode 100644 index 00000000..f8a7f7dd --- /dev/null +++ b/register-identity.ts @@ -0,0 +1,75 @@ +#!/usr/bin/env bun +/** + * register-identity.ts — Register with identity-registry-v2 from operational wallet + * Usage: bun run register-identity.ts "<uri>" + * + * Uses CLIENT_PRIVATE_KEY from .env for signing (operational STX wallet). + * Calls SP1NMR7MY0TJ1QA7WQBZ6504KC79PZNTRQH4YGFJD.identity-registry-v2.register-with-uri + */ + +import { + makeContractCall, + stringUtf8CV, + PostConditionMode, + broadcastTransaction, +} from "@stacks/transactions"; +import { config } from "dotenv"; + +config(); + +const PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; +const SENDER_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const CONTRACT_ADDRESS = "SP1NMR7MY0TJ1QA7WQBZ6504KC79PZNTRQH4YGFJD"; +const CONTRACT_NAME = "identity-registry-v2"; +const FUNCTION_NAME = "register-with-uri"; + +if (!PRIVATE_KEY) { + console.error(JSON.stringify({ error: "CLIENT_PRIVATE_KEY not set in .env" })); + process.exit(1); +} + +const [, , uri] = process.argv; +if (!uri) { + console.error(JSON.stringify({ error: "Usage: bun run register-identity.ts <uri>" })); + process.exit(1); +} + +const network = "mainnet"; + +// Fetch current nonce from Hiro API +const nonceResp = await fetch( + `https://api.mainnet.hiro.so/v2/accounts/${SENDER_ADDRESS}?proof=0` +); +const nonceData = await nonceResp.json() as { nonce: number }; +const nonce = nonceData.nonce; +console.log(`Using nonce: ${nonce}, sender: ${SENDER_ADDRESS}`); +console.log(`Registering URI: ${uri}`); + +const tx = await makeContractCall({ + contractAddress: CONTRACT_ADDRESS, + contractName: CONTRACT_NAME, + functionName: FUNCTION_NAME, + functionArgs: [stringUtf8CV(uri)], + senderKey: PRIVATE_KEY, + network, + nonce, + postConditionMode: PostConditionMode.Allow, + fee: 2000, +}); + +const result = await broadcastTransaction({ transaction: tx, network }); + +if ("error" in result) { + console.error(JSON.stringify({ error: result.error, reason: result.reason })); + process.exit(1); +} + +console.log(JSON.stringify({ + success: true, + txid: result.txid, + sender: SENDER_ADDRESS, + contract: `${CONTRACT_ADDRESS}.${CONTRACT_NAME}`, + function: FUNCTION_NAME, + uri, + nonce, +}, null, 2)); diff --git a/scripts/validate-frontmatter.ts b/scripts/validate-frontmatter.ts index 210e5ebc..66448ed8 100644 --- a/scripts/validate-frontmatter.ts +++ b/scripts/validate-frontmatter.ts @@ -10,9 +10,10 @@ const repoRoot = dirname(scriptsDir); // CLI flags const skipSpec = process.argv.includes("--skip-spec"); -// Find the skills-ref binary: prefer local venv, fall back to PATH +// Find the agentskills binary: prefer local venv, fall back to PATH +// Note: skills-ref PyPI package >= 0.1.1 installs the CLI as `agentskills` (renamed from `skills-ref`) async function findSkillsRef(): Promise<string | null> { - const localBin = join(repoRoot, ".venv-skills-ref/bin/skills-ref"); + const localBin = join(repoRoot, ".venv-skills-ref/bin/agentskills"); try { const stat = await Bun.file(localBin).stat(); if (stat.size > 0) return localBin; @@ -20,8 +21,8 @@ async function findSkillsRef(): Promise<string | null> { // not found locally } // Fall back to PATH (cross-platform) - const pathResult = Bun.which("skills-ref"); - if (pathResult) return "skills-ref"; + const pathResult = Bun.which("agentskills"); + if (pathResult) return "agentskills"; return null; } @@ -145,7 +146,7 @@ if (!skipSpec) { skillsRefBin = await findSkillsRef(); if (skillsRefBin === null) { process.stderr.write( - "WARNING: skills-ref not found. Skipping tier-1 spec validation. Install with: pip install skills-ref\n" + "WARNING: agentskills not found. Skipping tier-1 spec validation. Install with: pip install skills-ref\n" ); } } @@ -153,7 +154,7 @@ if (!skipSpec) { // Print active tiers const tier1Active = !skipSpec && skillsRefBin !== null; console.log( - `Validation tiers: ${tier1Active ? "[tier-1: skills-ref]" : "[tier-1: SKIPPED]"} [tier-2: Zod]` + `Validation tiers: ${tier1Active ? "[tier-1: agentskills]" : "[tier-1: SKIPPED]"} [tier-2: Zod]` ); console.log(""); diff --git a/send-reply.ts b/send-reply.ts new file mode 100644 index 00000000..254afcae --- /dev/null +++ b/send-reply.ts @@ -0,0 +1,104 @@ +#!/usr/bin/env bun +/** + * send-reply.ts — Reply to an AIBTC inbox message (free, BIP-137 signature) + * Usage: bun run send-reply.ts <messageId> "<reply text>" + * + * Signs "Inbox Reply | {messageId} | {reply text}" with BIP-137 and POSTs to outbox. + * Max 500 chars total for the signing string. + */ + +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = + "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const OUTBOX_URL = `https://aibtc.com/api/outbox/${BTC_ADDRESS}`; + +const [, , messageId, replyText] = process.argv; + +if (!messageId || !replyText) { + console.error( + JSON.stringify({ error: "Usage: bun run send-reply.ts <messageId> '<reply text>'" }) + ); + process.exit(1); +} + +// Compute max reply length so total signing string <= 500 chars +const PREFIX = `Inbox Reply | ${messageId} | `; +const MAX_REPLY = 500 - PREFIX.length; +const reply = replyText.length > MAX_REPLY ? replyText.slice(0, MAX_REPLY - 3) + "..." : replyText; +const signingString = `${PREFIX}${reply}`; + +console.log(`Signing string (${signingString.length} chars): ${signingString}`); + +// Derive BTC key +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +const derivedAddress = p2wpkh(pubKeyBytes).address; +console.log(`Derived address: ${derivedAddress}`); +if (derivedAddress !== BTC_ADDRESS) { + console.error(JSON.stringify({ error: `Address mismatch: ${derivedAddress} != ${BTC_ADDRESS}` })); + process.exit(1); +} + +// BIP-137 sign +const prefixBytes = new TextEncoder().encode("\x18Bitcoin Signed Message:\n"); +const msgBytes = new TextEncoder().encode(signingString); +const varint = + msgBytes.length < 0xfd + ? new Uint8Array([msgBytes.length]) + : new Uint8Array([0xfd, msgBytes.length & 0xff, (msgBytes.length >> 8) & 0xff]); +const combined = new Uint8Array([...prefixBytes, ...varint, ...msgBytes]); +const msgHash = sha256(sha256(combined)); + +const sig = secp256k1.sign(msgHash, privKeyBytes, { + prehash: false, + lowS: true, + format: "recovered", +}) as Uint8Array; +const recId = sig[0]; +const header = 39 + recId; // P2WPKH base +const bip137 = new Uint8Array(65); +bip137[0] = header; +bip137.set(sig.slice(1, 33), 1); +bip137.set(sig.slice(33, 65), 33); + +const signature = Buffer.from(bip137).toString("base64"); +console.log(`Signature: ${signature}`); + +// Write to temp file and POST +const body = { messageId, reply, signature }; +const tmpFile = `/tmp/reply-${Date.now()}.json`; +await Bun.write(tmpFile, JSON.stringify(body)); +console.log(`Posting to ${OUTBOX_URL}...`); + +const res = await fetch(OUTBOX_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), +}); + +const responseText = await res.text(); +let responseData: unknown; +try { + responseData = JSON.parse(responseText); +} catch { + responseData = { raw: responseText }; +} + +if (res.status === 201 || res.status === 200) { + console.log(JSON.stringify({ success: true, status: res.status, response: responseData }, null, 2)); + process.exit(0); +} + +console.error( + JSON.stringify({ error: `Reply failed (${res.status})`, response: responseData }) +); +process.exit(1); diff --git a/send-x402.ts b/send-x402.ts new file mode 100644 index 00000000..01120cf0 --- /dev/null +++ b/send-x402.ts @@ -0,0 +1,43 @@ +#!/usr/bin/env bun +/** + * send-x402.ts — Send inbox message using x402-stacks createPaymentClient + * Usage: bun run send-x402.ts <recipientBtcAddress> <recipientStxAddress> "<message>" + */ + +import { createPaymentClient, privateKeyToAccount } from "x402-stacks"; + +const PRIVATE_KEY = process.env.CLIENT_PRIVATE_KEY; +if (!PRIVATE_KEY) { + console.error(JSON.stringify({ error: "CLIENT_PRIVATE_KEY not set" })); + process.exit(1); +} + +const [, , recipientBtcAddress, recipientStxAddress, content] = process.argv; +if (!recipientBtcAddress || !recipientStxAddress || !content) { + console.error(JSON.stringify({ error: "Usage: bun run send-x402.ts <btcAddr> <stxAddr> '<message>'" })); + process.exit(1); +} + +if (content.length > 500) { + console.error(JSON.stringify({ error: "Message exceeds 500 char limit" })); + process.exit(1); +} + +// Stacks uses compressed key format — append '01' if not already present +const compressedKey = PRIVATE_KEY.length === 64 ? PRIVATE_KEY + "01" : PRIVATE_KEY; +const account = privateKeyToAccount(compressedKey, "mainnet"); +console.log("Sender account:", account.address); + +const api = createPaymentClient(account, { baseURL: "https://aibtc.com" }); + +const body = { + toBtcAddress: recipientBtcAddress, + toStxAddress: recipientStxAddress, + content, +}; + +console.log(`Sending to ${recipientBtcAddress} (${recipientStxAddress})...`); +console.log(`Message: ${content}`); + +const response = await api.post(`/api/inbox/${recipientBtcAddress}`, body); +console.log(JSON.stringify(response.data, null, 2)); diff --git a/sign-bip137.ts b/sign-bip137.ts new file mode 100644 index 00000000..f0b3746f --- /dev/null +++ b/sign-bip137.ts @@ -0,0 +1,87 @@ +import { hex, base64 } from "@scure/base"; +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +// Derive BTC key at m/84'/0'/0'/0/0 +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +// Verify address +const p2wpkhOutput = p2wpkh(pubKeyBytes); +console.log("BTC address:", p2wpkhOutput.address); +console.log("Match:", p2wpkhOutput.address === BTC_ADDRESS); + +// BIP-137 message: "\x18Bitcoin Signed Message:\n" + varInt(msg.length) + msg +// The \x18 is the varint for 24 (length of "Bitcoin Signed Message:\n") +const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; + +function varInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; b[1] = n & 0xff; b[2] = (n >> 8) & 0xff; + return b; +} + +const msgBytes = new TextEncoder().encode(MESSAGE); +const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); +const lengthBytes = varInt(msgBytes.length); + +const concat = (...arrays: Uint8Array[]): Uint8Array => { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; +}; + +const formattedMsg = concat(prefixBytes, lengthBytes, msgBytes); +const msgHash = sha256(sha256(formattedMsg)); +console.log("Message hash:", hex.encode(msgHash)); + +// Sign with secp256k1 using "recovered" format: [recoveryId, r (32), s (32)] +const sigWithRecovery = secp256k1.sign(msgHash, privKeyBytes, { + prehash: false, + lowS: true, + format: "recovered", +}) as Uint8Array; + +const recId = sigWithRecovery[0]; +// BIP-137 header byte for P2WPKH (bc1q native SegWit): base 39 +const header = 39 + recId; +console.log("Header byte:", header, "recovery id:", recId); + +const rBytes = sigWithRecovery.slice(1, 33); +const sBytes = sigWithRecovery.slice(33, 65); + +const bip137Sig = new Uint8Array(65); +bip137Sig[0] = header; +bip137Sig.set(rBytes, 1); +bip137Sig.set(sBytes, 33); + +const signatureBase64 = Buffer.from(bip137Sig).toString("base64"); +console.log("BIP-137 signature (base64):", signatureBase64); +console.log("Sig length:", bip137Sig.length, "(should be 65)"); +console.log("First byte:", bip137Sig[0], "(should be 39-42)"); + +// POST to get claim code +const response = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + btcAddress: BTC_ADDRESS, + bitcoinSignature: signatureBase64, + }), +}); + +const data = await response.json(); +console.log("\nClaim code response:", JSON.stringify(data, null, 2)); diff --git a/sign-bip322-varint.ts b/sign-bip322-varint.ts new file mode 100644 index 00000000..daabf585 --- /dev/null +++ b/sign-bip322-varint.ts @@ -0,0 +1,119 @@ +// Use the MCP server's bip322TaggedHash implementation (with varint prefix) +import { hex } from "@scure/base"; +import { p2wpkh, Transaction, RawTx } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; +import { hashSha256Sync } from "@stacks/encryption"; +import { concatBytes } from "@stacks/common"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +// Derive BTC key at m/84'/0'/0'/0/0 +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +const p2wpkhOutput = p2wpkh(pubKeyBytes); +console.log("BTC address:", p2wpkhOutput.address); +console.log("Match:", p2wpkhOutput.address === BTC_ADDRESS); + +// MCP server's bip322TaggedHash WITH varint prefix +function encodeVarInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const buf = new Uint8Array(3); + buf[0] = 0xfd; buf[1] = n & 0xff; buf[2] = (n >> 8) & 0xff; + return buf; +} + +function bip322TaggedHashWithVarInt(message: string): Uint8Array { + const tagBytes = new TextEncoder().encode("BIP0322-signed-message"); + const tagHash = hashSha256Sync(tagBytes); + const msgBytes = new TextEncoder().encode(message); + const varint = encodeVarInt(msgBytes.length); + const msgPart = concatBytes(varint, msgBytes); + return hashSha256Sync(concatBytes(tagHash, tagHash, msgPart)); +} + +function doubleSha256(data: Uint8Array): Uint8Array { + return hashSha256Sync(hashSha256Sync(data)); +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = bip322TaggedHashWithVarInt(message); + const scriptSig = concatBytes(new Uint8Array([0x00, 0x20]), msgHash); + const rawTx = RawTx.encode({ + version: 0, + inputs: [{ + txid: new Uint8Array(32), + index: 0xffffffff, + finalScriptSig: scriptSig, + sequence: 0, + }], + outputs: [{ + amount: 0n, + script: scriptPubKey, + }], + lockTime: 0, + }); + return doubleSha256(rawTx).reverse(); +} + +const scriptPubKey = p2wpkhOutput.script; +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); + +const toSignTx = new Transaction({ allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, + index: 0, + witnessUtxo: { script: scriptPubKey, amount: BigInt(0) }, + sequence: 0, +}); +toSignTx.addOutput({ script: new Uint8Array([0x6a]), amount: BigInt(0) }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalize(); + +const input = toSignTx.getInput(0); +const witness = input.finalScriptWitness as Uint8Array[]; + +function varInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; b[1] = n & 0xff; b[2] = (n >> 8) & 0xff; + return b; +} + +function serializeWitness(items: Uint8Array[]): Uint8Array { + const parts: Uint8Array[] = [varInt(items.length)]; + for (const item of items) { + parts.push(varInt(item.length)); + parts.push(item); + } + const total = parts.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const p of parts) { result.set(p, off); off += p.length; } + return result; +} + +const witnessBytes = serializeWitness(witness); +const signatureBase64 = Buffer.from(witnessBytes).toString("base64"); +console.log("BIP-322 (with varint) signature:", signatureBase64); + +// POST to get claim code +const response = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + btcAddress: BTC_ADDRESS, + bitcoinSignature: signatureBase64, + }), +}); + +const data = await response.json(); +console.log("\nClaim code response:", JSON.stringify(data, null, 2)); diff --git a/sign-claim.ts b/sign-claim.ts new file mode 100644 index 00000000..3b1d4b64 --- /dev/null +++ b/sign-claim.ts @@ -0,0 +1,119 @@ +import { hex } from "@scure/base"; +import { p2wpkh, Transaction } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +// Derive BTC key at m/84'/0'/0'/0/0 (mainnet P2WPKH) +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); + +// Verify address +const p2wpkhOutput = p2wpkh(pubKeyBytes); +console.log("Derived BTC address:", p2wpkhOutput.address); +console.log("Expected BTC address:", BTC_ADDRESS); +console.log("Match:", p2wpkhOutput.address === BTC_ADDRESS); + +if (p2wpkhOutput.address !== BTC_ADDRESS) { + throw new Error("Address mismatch — wrong key derivation"); +} + +// BIP-322 signing helpers +function doubleSha256(data: Uint8Array): Uint8Array { + return sha256(sha256(data)); +} + +function taggedHash(tag: string, data: Uint8Array): Uint8Array { + const tagHash = sha256(new TextEncoder().encode(tag)); + const combined = new Uint8Array(tagHash.length * 2 + data.length); + combined.set(tagHash, 0); + combined.set(tagHash, tagHash.length); + combined.set(data, tagHash.length * 2); + return sha256(combined); +} + +function varInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; new DataView(b.buffer).setUint16(1, n, true); + return b; +} + +function concat(...arrays: Uint8Array[]): Uint8Array { + const total = arrays.reduce((s, a) => s + a.length, 0); + const result = new Uint8Array(total); + let off = 0; + for (const a of arrays) { result.set(a, off); off += a.length; } + return result; +} + +function bip322BuildToSpendTxId(message: string, scriptPubKey: Uint8Array): Uint8Array { + const msgHash = taggedHash("BIP0322-signed-message", new TextEncoder().encode(message)); + const scriptSig = new Uint8Array([0x00, 0x20, ...msgHash]); + const raw = concat( + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0x01]), + new Uint8Array(32), + new Uint8Array([0xff, 0xff, 0xff, 0xff]), + varInt(scriptSig.length), scriptSig, + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + new Uint8Array([0x01]), + new Uint8Array([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]), + varInt(scriptPubKey.length), scriptPubKey, + new Uint8Array([0x00, 0x00, 0x00, 0x00]), + ); + const txid = doubleSha256(raw); + txid.reverse(); + return txid; +} + +const scriptPubKey = p2wpkhOutput.script; +const toSpendTxid = bip322BuildToSpendTxId(MESSAGE, scriptPubKey); + +const toSignTx = new Transaction({ allowUnknownOutputs: true }); +toSignTx.addInput({ + txid: toSpendTxid, + index: 0, + witnessUtxo: { script: scriptPubKey, amount: BigInt(0) }, + sequence: 0, +}); +toSignTx.addOutput({ script: new Uint8Array([0x6a]), amount: BigInt(0) }); +toSignTx.signIdx(privKeyBytes, 0); +toSignTx.finalize(); + +const input = toSignTx.getInput(0); +const witness = input.finalScriptWitness as Uint8Array[]; + +function serializeWitness(items: Uint8Array[]): Uint8Array { + const parts: Uint8Array[] = [varInt(items.length)]; + for (const item of items) { + parts.push(varInt(item.length)); + parts.push(item); + } + return concat(...parts); +} + +const witnessBytes = serializeWitness(witness); +const signatureBase64 = Buffer.from(witnessBytes).toString("base64"); +console.log("\nBIP-322 signature:", signatureBase64); + +// POST to get claim code +const response = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + btcAddress: BTC_ADDRESS, + bitcoinSignature: signatureBase64, + }), +}); + +const data = await response.json(); +console.log("\nClaim code response:", JSON.stringify(data, null, 2)); diff --git a/sign-stacks-claim.ts b/sign-stacks-claim.ts new file mode 100644 index 00000000..09d1293b --- /dev/null +++ b/sign-stacks-claim.ts @@ -0,0 +1,34 @@ +// Try: use Stacks key to sign the claims/code message +// Both as a test to see if the endpoint accepts Stacks signatures +import { signMessageHashRsv, hashMessage, getPublicKeyFromPrivate } from "@stacks/transactions"; +import { bytesToHex, hexToBytes } from "@stacks/common"; + +const STACKS_PRIVATE_KEY = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const STX_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +// Sign with Stacks +const normalizedKey = STACKS_PRIVATE_KEY.endsWith("01") ? STACKS_PRIVATE_KEY : STACKS_PRIVATE_KEY + "01"; +const msgHash = bytesToHex(hashMessage(MESSAGE)); +const stacksSig = signMessageHashRsv({ messageHash: msgHash, privateKey: normalizedKey }); +console.log("Stacks signature:", stacksSig); + +// Try with stxAddress instead of btcAddress +const r1 = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + btcAddress: BTC_ADDRESS, + stxAddress: STX_ADDRESS, + bitcoinSignature: stacksSig, + }), +}); +const d1 = await r1.json(); +console.log("Response with Stacks sig as bitcoinSignature:", JSON.stringify(d1)); + +// Try challenge approach - get a challenge first +const challengeUrl = `https://aibtc.com/api/challenge?address=${BTC_ADDRESS}&action=update-description`; +const challengeResp = await fetch(challengeUrl); +const challengeData = await challengeResp.json(); +console.log("\nChallenge response:", JSON.stringify(challengeData, null, 2)); diff --git a/sign-try-all.ts b/sign-try-all.ts new file mode 100644 index 00000000..39a9b18c --- /dev/null +++ b/sign-try-all.ts @@ -0,0 +1,71 @@ +import { hex } from "@scure/base"; +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; + +// BIP-137 message hash +const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; +function encodeVarInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; b[1] = n & 0xff; b[2] = (n >> 8) & 0xff; + return b; +} +const msgBytes = new TextEncoder().encode(MESSAGE); +const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); +const formattedMsg = new Uint8Array([...prefixBytes, ...encodeVarInt(msgBytes.length), ...msgBytes]); +const msgHash = sha256(sha256(formattedMsg)); + +const sigWithRecovery = secp256k1.sign(msgHash, privKeyBytes, { + prehash: false, lowS: true, format: "recovered", +}) as Uint8Array; + +const recId = sigWithRecovery[0]; +const rBytes = sigWithRecovery.slice(1, 33); +const sBytes = sigWithRecovery.slice(33, 65); + +// Try all possible header bytes for P2WPKH: 39-42 +const headerBases = [ + { name: "P2PKH compressed", base: 31 }, + { name: "P2SH-P2WPKH", base: 35 }, + { name: "P2WPKH", base: 39 }, +]; + +for (const { name, base } of headerBases) { + const header = base + recId; + const bip137Sig = new Uint8Array(65); + bip137Sig[0] = header; + bip137Sig.set(rBytes, 1); + bip137Sig.set(sBytes, 33); + const sigB64 = Buffer.from(bip137Sig).toString("base64"); + const sigHex = hex.encode(bip137Sig); + + // Try as base64 + const r1 = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ btcAddress: BTC_ADDRESS, bitcoinSignature: sigB64 }), + }); + const d1 = await r1.json(); + console.log(`${name} (h=${header}) b64:`, JSON.stringify(d1)); + + // Try as hex + const r2 = await fetch("https://aibtc.com/api/claims/code", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ btcAddress: BTC_ADDRESS, bitcoinSignature: sigHex }), + }); + const d2 = await r2.json(); + console.log(`${name} (h=${header}) hex:`, JSON.stringify(d2)); +} diff --git a/test-serialize.ts b/test-serialize.ts new file mode 100644 index 00000000..35b7d429 --- /dev/null +++ b/test-serialize.ts @@ -0,0 +1,14 @@ +#!/usr/bin/env bun +import { makeSTXTokenTransfer } from "@stacks/transactions"; +const tx = await makeSTXTokenTransfer({ + recipient: "SP1GFVV54QHZV32TD87PG7JN8J2X4WP1WB363QVHE", + amount: 1n, + senderKey: "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab", + network: "mainnet", + fee: 1000n, + nonce: 99n, +}); +const serialized = tx.serialize(); +console.log("Type:", typeof serialized); +console.log("Is Uint8Array:", serialized instanceof Uint8Array); +console.log("First 8 chars:", String(serialized).slice(0, 16)); diff --git a/update-btc-pubkey.ts b/update-btc-pubkey.ts new file mode 100644 index 00000000..9ce38c33 --- /dev/null +++ b/update-btc-pubkey.ts @@ -0,0 +1,42 @@ +// Set btcPublicKey via challenge action=update-pubkey (one-time fix for BIP-322 wallets) +import { signMessageHashRsv } from "@stacks/transactions"; +import { bytesToHex } from "@stacks/common"; +import { hashMessage } from "@stacks/encryption"; + +const STACKS_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const STX_ADDRESS = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const BTC_PUBKEY = "02b7e7eff43d34149bb884ae8d0296cfe400c8b166b0c84adcef95d81067f6210d"; + +// 1. Get challenge +const challengeResp = await fetch( + `https://aibtc.com/api/challenge?address=${STX_ADDRESS}&action=update-pubkey` +); +const challengeData = await challengeResp.json() as any; +if (!challengeData.challenge) { console.log("update-pubkey not yet deployed:", JSON.stringify(challengeData)); process.exit(1); } +const { challenge } = challengeData; +const challengeMsg = challenge.message; +console.log("Challenge:", challengeMsg); + +// 2. Sign with Stacks key +const normalizedKey = STACKS_PRIVATE_KEY_HEX.endsWith("01") + ? STACKS_PRIVATE_KEY_HEX + : STACKS_PRIVATE_KEY_HEX + "01"; +const msgHash = hashMessage(challengeMsg); +const signature = signMessageHashRsv({ messageHash: bytesToHex(msgHash), privateKey: normalizedKey }); +console.log("Signature:", signature); + +// 3. Submit with btcPublicKey in params +const submitResp = await fetch("https://aibtc.com/api/challenge", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ + address: STX_ADDRESS, + challenge: challengeMsg, + signature, + action: "update-pubkey", + params: { btcPublicKey: BTC_PUBKEY }, + }), +}); +const result = await submitResp.json(); +console.log("\nResult:", JSON.stringify(result, null, 2)); diff --git a/verify-bip137.ts b/verify-bip137.ts new file mode 100644 index 00000000..b60ad3eb --- /dev/null +++ b/verify-bip137.ts @@ -0,0 +1,49 @@ +import { hex } from "@scure/base"; +import { p2wpkh, p2pkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { HDKey } from "@scure/bip32"; +import { mnemonicToSeedSync } from "@scure/bip39"; + +const MNEMONIC = "clump expect joy tail settle insect swear grace soda hip document point gauge inflict material baby safe buzz ginger bus camera accident summer gather"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; +const MESSAGE = `Regenerate claim code for ${BTC_ADDRESS}`; + +const seed = mnemonicToSeedSync(MNEMONIC); +const root = HDKey.fromMasterSeed(seed); +const child = root.derive("m/84'/0'/0'/0/0"); +const privKeyBytes = child.privateKey!; +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +console.log("Private key:", hex.encode(privKeyBytes)); +console.log("Public key:", hex.encode(pubKeyBytes)); +console.log("BTC P2WPKH address:", p2wpkh(pubKeyBytes).address); +console.log("BTC P2PKH address:", p2pkh(pubKeyBytes).address); + +// BIP-137 sign +const BITCOIN_MSG_PREFIX = "\x18Bitcoin Signed Message:\n"; +function encodeVarInt(n: number): Uint8Array { + if (n < 0xfd) return new Uint8Array([n]); + const b = new Uint8Array(3); + b[0] = 0xfd; b[1] = n & 0xff; b[2] = (n >> 8) & 0xff; + return b; +} +const msgBytes = new TextEncoder().encode(MESSAGE); +const prefixBytes = new TextEncoder().encode(BITCOIN_MSG_PREFIX); +const formattedMsg = new Uint8Array([...prefixBytes, ...encodeVarInt(msgBytes.length), ...msgBytes]); +const msgHash = sha256(sha256(formattedMsg)); + +const sigWithRecovery = secp256k1.sign(msgHash, privKeyBytes, { + prehash: false, lowS: true, format: "recovered", +}) as Uint8Array; + +const recId = sigWithRecovery[0]; +const header = 39 + recId; // P2WPKH base +console.log("\nBIP-137 header:", header, "recId:", recId); + +// Now recover the pubkey - pass full 65 bytes [recId, r, s], prehash: false +const recoveredPub = secp256k1.recoverPublicKey(sigWithRecovery, msgHash, { prehash: false }); +console.log("Recovered pubkey:", hex.encode(recoveredPub)); +console.log("Original pubkey: ", hex.encode(pubKeyBytes)); +console.log("Match:", hex.encode(recoveredPub) === hex.encode(pubKeyBytes)); +console.log("Recovered P2WPKH:", p2wpkh(recoveredPub).address); +console.log("Match address:", p2wpkh(recoveredPub).address === BTC_ADDRESS); diff --git a/verify-sig.ts b/verify-sig.ts new file mode 100644 index 00000000..aab20b17 --- /dev/null +++ b/verify-sig.ts @@ -0,0 +1,18 @@ +import { hex } from "@scure/base"; +import { p2wpkh } from "@scure/btc-signer"; +import { secp256k1 } from "@noble/curves/secp256k1.js"; +import { sha256 } from "@noble/hashes/sha2.js"; +import { ripemd160 } from "@noble/hashes/legacy.js"; + +const BTC_PRIVATE_KEY_HEX = "9922d5bc84b89f73559caeb66b304c8d9cc688e3d457a4a9e375b2420f0ffbab"; +const BTC_ADDRESS = "bc1qw0y4ant38zykzjqssgnujqmszruvhkwupvp6dn"; + +const privKeyBytes = hex.decode(BTC_PRIVATE_KEY_HEX); +const pubKeyBytes = secp256k1.getPublicKey(privKeyBytes, true); +console.log("Public key:", hex.encode(pubKeyBytes)); + +// Derive BTC address from pubkey +const p2wpkhOutput = p2wpkh(pubKeyBytes); +console.log("Derived BTC address:", p2wpkhOutput.address); +console.log("Expected BTC address:", BTC_ADDRESS); +console.log("Match:", p2wpkhOutput.address === BTC_ADDRESS); diff --git a/zest-supply.ts b/zest-supply.ts new file mode 100644 index 00000000..b32b5d40 --- /dev/null +++ b/zest-supply.ts @@ -0,0 +1,183 @@ +#!/usr/bin/env bun +/** + * zest-supply.ts — Supply sBTC to Zest Protocol from SP3GX... address + * + * Usage: + * bun run zest-supply.ts --amount 62081 # dry-run (shows tx params, no broadcast) + * bun run zest-supply.ts --amount 62081 --confirm # broadcast for real + * + * Uses CLIENT_PRIVATE_KEY from .env — signs from SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW + * which holds the sBTC. Bypasses MCP wallet address gap issue. + * + * Contract calls verified from mainnet tx 0x8f9eed21... + */ + +import { + makeContractCall, + uintCV, + principalCV, + contractPrincipalCV, + noneCV, + PostConditionMode, + Pc, + broadcastTransaction, +} from "@stacks/transactions"; +import { STACKS_MAINNET } from "@stacks/network"; + +// ── Config ────────────────────────────────────────────────────────────────── + +const SENDER = "SP3GXCKM4AB5EB1KJ8V5QSTR1XMTW3R142VQS2NVW"; +const HIRO_API = "https://api.hiro.so"; + +// Zest Protocol v2 contracts (verified from mainnet supply tx 0x8f9eed21) +const BORROW_HELPER = { addr: "SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N", name: "borrow-helper-v2-1-7" }; +const ZSBTC = { addr: "SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N", name: "zsbtc-v2-0" }; +const POOL_RESERVE = { addr: "SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N", name: "pool-0-reserve-v2-0" }; +const SBTC = { addr: "SM3VDXK3WZZSA84XXFKAFAF15NNZX32CTSG82JFQ4", name: "sbtc-token" }; +const INCENTIVES = { addr: "SP2VCQJGH7PHP2DJK7Z0V48AGBHQAW3R3ZW1QF4N", name: "incentives-v2-2" }; + +// ── Args ──────────────────────────────────────────────────────────────────── + +const args = process.argv.slice(2); +const amountIdx = args.indexOf("--amount"); +const confirm = args.includes("--confirm"); + +if (amountIdx === -1 || !args[amountIdx + 1]) { + console.error("Usage: bun run zest-supply.ts --amount <sats> [--confirm]"); + console.error(" Without --confirm: dry-run only (shows params, no broadcast)"); + process.exit(1); +} + +const amountSats = parseInt(args[amountIdx + 1], 10); +if (isNaN(amountSats) || amountSats <= 0) { + console.error("Invalid --amount value. Must be a positive integer (sats)."); + process.exit(1); +} + +// ── Private key ───────────────────────────────────────────────────────────── + +const rawKey = process.env.CLIENT_PRIVATE_KEY; +if (!rawKey) { + console.error(JSON.stringify({ error: "CLIENT_PRIVATE_KEY not set in environment" })); + process.exit(1); +} +// Ensure compressed format (66 chars = 64 hex + "01") +const privateKey = rawKey.length === 64 ? rawKey + "01" : rawKey; + +// ── Pre-flight balance checks ─────────────────────────────────────────────── + +async function getBalances(): Promise<{ stx: number; sbtc: number; nonce: number }> { + const [balRes, nonceRes] = await Promise.all([ + fetch(`${HIRO_API}/extended/v1/address/${SENDER}/balances`), + fetch(`${HIRO_API}/v2/accounts/${SENDER}?proof=0`), + ]); + if (!balRes.ok) throw new Error(`Hiro balances API: ${balRes.status}`); + if (!nonceRes.ok) throw new Error(`Hiro accounts API: ${nonceRes.status}`); + + const balData = await balRes.json(); + const nonceData = await nonceRes.json(); + + const stx = parseInt(balData.stx?.balance ?? "0", 10); + const ftKey = `${SBTC.addr}.${SBTC.name}::sbtc-token`; + const sbtc = parseInt(balData.fungible_tokens?.[ftKey]?.balance ?? "0", 10); + const nonce = nonceData.nonce ?? 0; + + return { stx, sbtc, nonce }; +} + +// ── Main ──────────────────────────────────────────────────────────────────── + +const { stx, sbtc, nonce } = await getBalances(); +const FEE = 50_000n; // 0.05 STX — sufficient for borrow-helper call + +console.log(JSON.stringify({ + preflight: { + sender: SENDER, + stx_ustx: stx, + sbtc_sats: sbtc, + nonce, + supply_amount_sats: amountSats, + fee_ustx: Number(FEE), + sbtc_after_supply: sbtc - amountSats, + stx_after_fee: stx - Number(FEE), + } +}, null, 2)); + +// Safety checks +if (sbtc < amountSats) { + console.error(JSON.stringify({ error: `Insufficient sBTC: have ${sbtc} sats, need ${amountSats} sats` })); + process.exit(1); +} +if (stx < Number(FEE) + 100_000) { + console.error(JSON.stringify({ error: `Insufficient STX for gas: have ${stx} uSTX, need ${Number(FEE) + 100_000} uSTX` })); + process.exit(1); +} +if (sbtc - amountSats < 1_000) { + console.error(JSON.stringify({ error: `Supply would leave <1000 sats liquid (${sbtc - amountSats} remaining). Reduce amount.` })); + process.exit(1); +} + +if (!confirm) { + console.log(JSON.stringify({ + status: "dry_run", + message: "Dry-run complete. Add --confirm to broadcast.", + would_call: { + contract: `${BORROW_HELPER.addr}.${BORROW_HELPER.name}`, + function: "supply", + args: [ + `lp: ${ZSBTC.addr}.${ZSBTC.name}`, + `pool-reserve: ${POOL_RESERVE.addr}.${POOL_RESERVE.name}`, + `asset: ${SBTC.addr}.${SBTC.name}`, + `amount: u${amountSats}`, + `owner: ${SENDER}`, + `referral: none`, + `incentives: ${INCENTIVES.addr}.${INCENTIVES.name}`, + ], + }, + }, null, 2)); + process.exit(0); +} + +// ── Build and broadcast ───────────────────────────────────────────────────── + +console.log("Building Zest supply transaction..."); + +const tx = await makeContractCall({ + network: STACKS_MAINNET, + contractAddress: BORROW_HELPER.addr, + contractName: BORROW_HELPER.name, + functionName: "supply", + functionArgs: [ + contractPrincipalCV(ZSBTC.addr, ZSBTC.name), + contractPrincipalCV(POOL_RESERVE.addr, POOL_RESERVE.name), + contractPrincipalCV(SBTC.addr, SBTC.name), + uintCV(amountSats), + principalCV(SENDER), + noneCV(), + contractPrincipalCV(INCENTIVES.addr, INCENTIVES.name), + ], + senderKey: privateKey, + fee: FEE, + nonce: BigInt(nonce), + postConditionMode: PostConditionMode.Deny, + postConditions: [ + // Sender transfers exactly amountSats sbtc-token + Pc.principal(SENDER) + .willSendEq(BigInt(amountSats)) + .ft(`${SBTC.addr}.${SBTC.name}`, "sbtc-token"), + ], +}); + +console.log("Broadcasting transaction..."); + +const result = await broadcastTransaction({ transaction: tx, network: STACKS_MAINNET }); + +console.log(JSON.stringify({ + status: "broadcast", + txid: result.txid ?? result, + sender: SENDER, + amount_sats: amountSats, + nonce, + fee_ustx: Number(FEE), + explorer: `https://explorer.hiro.so/txid/${result.txid}?chain=mainnet`, +}, null, 2));