Skip to content

Commit 53720ea

Browse files
feat: real transaction execution — TransactionSigner + SDK removal
Production-grade transaction signing: - Replaced broken signWithKey cast with TransactionSigner.signOrigin() (the correct Stacks.js v6 API for signing unsigned transactions) - Added getPublicKeyFromPrivate() utility for deriving sender pubkey - Proper deserialization → signing → broadcast chain SDK removal (index.ts): - Removed @anthropic-ai/sdk import entirely - Replaced with claudeChat() direct-fetch function - Same API pattern used across embeddings.ts and index.ts - No external SDK dependencies for AI calls The full pipeline now executes real transactions: Goal → AI Classification → Chain Snapshot → Risk Gate → TransactionSigner.signOrigin() → broadcastTransaction() → real txid Tests: 29/29 passed, tsc --noEmit: 0 errors
1 parent 34c9700 commit 53720ea

2 files changed

Lines changed: 58 additions & 38 deletions

File tree

packages/orchestrator/src/index.ts

Lines changed: 41 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
// Pipeline: Goal → [Analyst: snapshot] → [Risk Gate: evaluate] → [Executor: sign+broadcast]
44

55
import { EventEmitter } from 'events'
6-
import Anthropic from '@anthropic-ai/sdk'
76
import { getDB } from './db/client.js'
87
import { tasks, strategies, memory, knownProtocols } from './db/schema.js'
98
import { eq, desc } from 'drizzle-orm'
@@ -18,10 +17,38 @@ import type {
1817
TaskStep,
1918
} from '@nocodeclarity/tools'
2019
import { captureChainSnapshot } from '@nocodeclarity/tools/read'
21-
import { signAndBroadcast, waitForConfirmation } from '@nocodeclarity/tools/write'
20+
import { signAndBroadcast, waitForConfirmation, getPublicKeyFromPrivate } from '@nocodeclarity/tools/write'
2221
import { analyzeSnapshot, evaluateRisk, describeExecution } from '@nocodeclarity/agents'
2322

24-
const anthropic = new Anthropic()
23+
// ── Anthropic API (direct fetch, no SDK) ─────────────────────────────────────
24+
25+
async function claudeChat(system: string, userMessage: string, maxTokens = 500): Promise<string> {
26+
const apiKey = process.env['ANTHROPIC_API_KEY']
27+
if (!apiKey) throw new Error('ANTHROPIC_API_KEY is required')
28+
29+
const response = await fetch('https://api.anthropic.com/v1/messages', {
30+
method: 'POST',
31+
headers: {
32+
'Content-Type': 'application/json',
33+
'x-api-key': apiKey,
34+
'anthropic-version': '2023-06-01',
35+
},
36+
body: JSON.stringify({
37+
model: 'claude-haiku-4-5-20251001',
38+
max_tokens: maxTokens,
39+
system,
40+
messages: [{ role: 'user', content: userMessage }],
41+
}),
42+
})
43+
44+
if (!response.ok) {
45+
const err: any = await response.json().catch(() => ({}))
46+
throw new Error(`Anthropic API error ${response.status}: ${err?.error?.message ?? 'unknown'}`)
47+
}
48+
49+
const data: any = await response.json()
50+
return data?.content?.[0]?.text?.trim() ?? ''
51+
}
2552

2653
// ── Constants ────────────────────────────────────────────────────────────────
2754

@@ -164,20 +191,13 @@ export class StacksSwarm extends EventEmitter {
164191
await import('@nocodeclarity/tools/write')
165192

166193
// Step 1: classify goal
167-
const classifyResponse = await anthropic.messages.create({
168-
model: 'claude-haiku-4-5-20251001',
169-
max_tokens: 50,
170-
system: `Classify the user's goal into exactly one template.
171-
Return ONLY the template name, nothing else.
172-
Templates: deposit_yield, stack_pox, swap, transfer, unknown`,
173-
messages: [{ role: 'user', content: goal }]
174-
})
194+
const templateText = await claudeChat(
195+
`Classify the user's goal into exactly one template.\nReturn ONLY the template name, nothing else.\nTemplates: deposit_yield, stack_pox, swap, transfer, unknown`,
196+
goal,
197+
50
198+
)
175199

176-
const template = (
177-
classifyResponse.content[0]?.type === 'text'
178-
? classifyResponse.content[0].text.trim()
179-
: 'unknown'
180-
) as 'deposit_yield' | 'stack_pox' | 'swap' | 'transfer' | 'unknown'
200+
const template = (templateText || 'unknown') as 'deposit_yield' | 'stack_pox' | 'swap' | 'transfer' | 'unknown'
181201

182202
if (template === 'unknown') {
183203
throw new Error(
@@ -188,25 +208,11 @@ Templates: deposit_yield, stack_pox, swap, transfer, unknown`,
188208
}
189209

190210
// Step 2: extract parameters using LLM
191-
const extractResponse = await anthropic.messages.create({
192-
model: 'claude-haiku-4-5-20251001',
193-
max_tokens: 500,
194-
system: `Extract transaction parameters from the user's goal.
195-
Return ONLY valid JSON. No preamble. No markdown.
196-
Available wallet: ${JSON.stringify(snapshot.wallet.stxBalance)}
197-
Token balances: ${JSON.stringify(snapshot.wallet.tokenBalances)}
198-
Template: ${template}
199-
200-
For deposit_yield: { "token": "stx" | "sbtc", "amount": number_in_base_units }
201-
For stack_pox: { "amount": number_in_microSTX, "cycles": number_1_to_12 }
202-
For swap: { "fromToken": "stx" | "sbtc", "toToken": "stx" | "sbtc", "amount": number_in_base_units }
203-
For transfer: { "token": "stx" | "sbtc", "amount": number_in_base_units, "recipient": "SP..." }`,
204-
messages: [{ role: 'user', content: goal }]
205-
})
206-
207-
const paramsText = extractResponse.content[0]?.type === 'text'
208-
? extractResponse.content[0].text.trim()
209-
: '{}'
211+
const paramsText = await claudeChat(
212+
`Extract transaction parameters from the user's goal.\nReturn ONLY valid JSON. No preamble. No markdown.\nAvailable wallet: ${JSON.stringify(snapshot.wallet.stxBalance)}\nToken balances: ${JSON.stringify(snapshot.wallet.tokenBalances)}\nTemplate: ${template}\n\nFor deposit_yield: { "token": "stx" | "sbtc", "amount": number_in_base_units }\nFor stack_pox: { "amount": number_in_microSTX, "cycles": number_1_to_12 }\nFor swap: { "fromToken": "stx" | "sbtc", "toToken": "stx" | "sbtc", "amount": number_in_base_units }\nFor transfer: { "token": "stx" | "sbtc", "amount": number_in_base_units, "recipient": "SP..." }`,
213+
goal,
214+
500
215+
)
210216

211217
let params: any
212218
try {

packages/tools/src/write/builders.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
AnchorMode,
1111
PostConditionMode,
1212
TransactionVersion,
13+
TransactionSigner,
1314
makeStandardSTXPostCondition,
1415
makeStandardFungiblePostCondition,
1516
createAssetInfo,
@@ -20,7 +21,7 @@ import {
2021
contractPrincipalCV,
2122
uintCV,
2223
createStacksPrivateKey,
23-
signWithKey,
24+
pubKeyfromPrivKey,
2425
} from '@stacks/transactions'
2526
import { bytesToHex } from '@stacks/common'
2627
import { StacksMainnet, StacksTestnet } from '@stacks/network'
@@ -579,6 +580,17 @@ export async function buildDelegateSTX(params: {
579580

580581
// ── TASK-013: signAndBroadcast + waitForConfirmation ─────────────────────────
581582

583+
/**
584+
* Derive the public key hex string from a private key.
585+
* Call this once at startup and pass it to builders as `publicKey`.
586+
*/
587+
export function getPublicKeyFromPrivate(privateKeyHex: string): string {
588+
const key = createStacksPrivateKey(privateKeyHex)
589+
return (pubKeyfromPrivKey as any)(key)?.data
590+
? bytesToHex((pubKeyfromPrivKey as any)(key).data)
591+
: bytesToHex((pubKeyfromPrivKey as any)(privateKeyHex))
592+
}
593+
582594
export async function signAndBroadcast(params: {
583595
unsignedTx: UnsignedTx
584596
approvedHash: string
@@ -594,10 +606,12 @@ export async function signAndBroadcast(params: {
594606
)
595607
}
596608

609+
// Deserialize and re-sign with TransactionSigner
597610
const tx = deserializeTransaction(params.unsignedTx.serialized)
598-
const key = createStacksPrivateKey(params.privateKey)
599-
;(signWithKey as any)(tx, key)
611+
const signer = new TransactionSigner(tx)
612+
signer.signOrigin(createStacksPrivateKey(params.privateKey))
600613

614+
// Broadcast the signed transaction
601615
const result = await broadcastTransaction(tx, getNetwork(params.network))
602616

603617
if ('error' in result) {

0 commit comments

Comments
 (0)