Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.42.0"
".": "0.43.0"
}
174 changes: 174 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

80 changes: 80 additions & 0 deletions challenge-stacks.ts
Original file line number Diff line number Diff line change
@@ -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));
49 changes: 49 additions & 0 deletions challenge-stx.ts
Original file line number Diff line number Diff line change
@@ -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));
70 changes: 70 additions & 0 deletions challenge-stx2.ts
Original file line number Diff line number Diff line change
@@ -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));
15 changes: 15 additions & 0 deletions check-key.ts
Original file line number Diff line number Diff line change
@@ -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");
17 changes: 17 additions & 0 deletions check-key2.ts
Original file line number Diff line number Diff line change
@@ -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");
41 changes: 41 additions & 0 deletions check-key3.ts
Original file line number Diff line number Diff line change
@@ -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);
}
77 changes: 77 additions & 0 deletions claim-beat.ts
Original file line number Diff line number Diff line change
@@ -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 <beat-slug>
*/

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 <beat-slug>");
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));
Loading
Loading