Skip to content

Commit f499953

Browse files
committed
Improve stackflow-agent workflow coverage and quickstart docs
1 parent 027457f commit f499953

2 files changed

Lines changed: 174 additions & 0 deletions

File tree

packages/stackflow-agent/README.md

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,56 @@ watcher.start();
7575
8. `disputeClosure(...)`
7676
9. `watcher.runOnce()` or `watcher.start()` for hourly checks
7777

78+
## Quick workflow (setup pipe + send + receive)
79+
80+
1. Track pipe locally:
81+
82+
```js
83+
const tracked = agent.trackPipe({
84+
contractId: "ST...stackflow-0-6-0",
85+
pipeKey: { "principal-1": "SP...ME", "principal-2": "SP...THEM", token: null },
86+
localPrincipal: "SP...ME",
87+
counterpartyPrincipal: "SP...THEM",
88+
});
89+
```
90+
91+
2. Open/fund the pipe on-chain:
92+
93+
```js
94+
await agent.openPipe({
95+
contractId: tracked.contractId,
96+
token: null,
97+
amount: "1000",
98+
counterpartyPrincipal: tracked.counterpartyPrincipal,
99+
nonce: "0",
100+
});
101+
```
102+
103+
3. Build an outgoing state update to send to counterparty:
104+
105+
```js
106+
const outgoing = agent.buildOutgoingTransfer({
107+
pipeId: tracked.pipeId,
108+
amount: "25",
109+
actor: tracked.localPrincipal,
110+
});
111+
```
112+
113+
4. Validate + accept incoming counterparty update:
114+
115+
```js
116+
const result = await agent.acceptIncomingTransfer({
117+
pipeId: tracked.pipeId,
118+
payload: {
119+
...outgoing,
120+
actor: tracked.counterpartyPrincipal,
121+
theirSignature: "0x...",
122+
},
123+
});
124+
```
125+
126+
5. Persisted local latest state is now available via `getPipeLatestState(...)`.
127+
78128
## Notes
79129

80130
1. This scaffold intentionally avoids observer endpoints and local chain node.

tests/stackflow-agent.test.ts

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -266,6 +266,130 @@ describe("stackflow agent", () => {
266266
store.close();
267267
});
268268

269+
it("opens a pipe via signer adapter with expected contract call", async () => {
270+
const dbFile = tempDbFile("agent-open");
271+
const store = new AgentStateStore({ dbFile });
272+
273+
const calls: Array<{ contractId: string; functionName: string; functionArgs: unknown[]; network?: string }> = [];
274+
275+
const agent = new StackflowAgentService({
276+
stateStore: store,
277+
signer: {
278+
async submitDispute() {
279+
return { txid: "0x1" };
280+
},
281+
async callContract(input) {
282+
calls.push(input as { contractId: string; functionName: string; functionArgs: unknown[]; network?: string });
283+
return { ok: true, txid: "0xopen" };
284+
},
285+
},
286+
network: "devnet",
287+
});
288+
289+
const result = await agent.openPipe({
290+
contractId: "ST1STACKFLOW.stackflow-0-6-0",
291+
token: null,
292+
amount: "1000",
293+
counterpartyPrincipal: "ST1COUNTERPARTY",
294+
nonce: "0",
295+
});
296+
297+
expect(result.ok).toBe(true);
298+
expect(calls).toHaveLength(1);
299+
expect(calls[0]).toEqual({
300+
contractId: "ST1STACKFLOW.stackflow-0-6-0",
301+
functionName: "fund-pipe",
302+
functionArgs: [null, "1000", "ST1COUNTERPARTY", "0"],
303+
network: "devnet",
304+
});
305+
306+
store.close();
307+
});
308+
309+
it("builds outgoing transfer from tracked state and accepts signed incoming update", async () => {
310+
const dbFile = tempDbFile("agent-send-receive");
311+
const store = new AgentStateStore({ dbFile });
312+
313+
const contractId = "ST1TESTABC.contract";
314+
const pipeKey = {
315+
"principal-1": "ST1LOCAL",
316+
"principal-2": "ST1OTHER",
317+
token: null,
318+
};
319+
const pipeId = buildPipeId({ contractId, pipeKey });
320+
321+
store.upsertTrackedPipe({
322+
pipeId,
323+
contractId,
324+
pipeKey,
325+
localPrincipal: "ST1LOCAL",
326+
counterpartyPrincipal: "ST1OTHER",
327+
token: null,
328+
});
329+
330+
store.upsertSignatureState({
331+
contractId,
332+
pipeKey,
333+
forPrincipal: "ST1LOCAL",
334+
withPrincipal: "ST1OTHER",
335+
token: null,
336+
myBalance: "100",
337+
theirBalance: "0",
338+
nonce: "0",
339+
action: "1",
340+
actor: "ST1LOCAL",
341+
mySignature: "0x" + "11".repeat(65),
342+
theirSignature: "0x" + "22".repeat(65),
343+
secret: null,
344+
validAfter: null,
345+
beneficialOnly: false,
346+
});
347+
348+
const agent = new StackflowAgentService({
349+
stateStore: store,
350+
signer: {
351+
async sip018Sign() {
352+
return "0x" + "44".repeat(65);
353+
},
354+
async submitDispute() {
355+
return { txid: "0x1" };
356+
},
357+
async callContract() {
358+
return { ok: true };
359+
},
360+
},
361+
});
362+
363+
const outgoing = agent.buildOutgoingTransfer({
364+
pipeId,
365+
amount: "25",
366+
actor: "ST1LOCAL",
367+
});
368+
369+
expect(outgoing.myBalance).toBe("75");
370+
expect(outgoing.theirBalance).toBe("25");
371+
expect(outgoing.nonce).toBe("1");
372+
373+
const accepted = await agent.acceptIncomingTransfer({
374+
pipeId,
375+
payload: {
376+
...outgoing,
377+
actor: "ST1OTHER",
378+
theirSignature: "0x" + "33".repeat(65),
379+
},
380+
});
381+
382+
expect(accepted.accepted).toBe(true);
383+
expect(accepted.mySignature).toMatch(/^0x[0-9a-f]+$/);
384+
385+
const latest = store.getLatestSignatureState(pipeId, "ST1LOCAL");
386+
expect(latest?.nonce).toBe("1");
387+
expect(latest?.myBalance).toBe("75");
388+
expect(latest?.theirBalance).toBe("25");
389+
390+
store.close();
391+
});
392+
269393
it("defaults watcher interval to one hour", () => {
270394
const dbFile = tempDbFile("agent-interval");
271395
const store = new AgentStateStore({ dbFile });

0 commit comments

Comments
 (0)