Skip to content

Commit 5577867

Browse files
feat(sdk): add round-status predicates and human-readable label helper
1 parent 42b3444 commit 5577867

5 files changed

Lines changed: 230 additions & 1 deletion

File tree

packages/sdk/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
".": "./src/index.ts"
2525
},
2626
"scripts": {
27-
"test": "node --import tsx --test src/client.test.ts src/encoding.test.ts src/errors.test.ts src/ids.test.ts src/mainnet-readiness.test.ts src/network.test.ts src/preflight.test.ts src/public-api-snapshot.test.ts src/redact.test.ts src/verify.test.ts",
27+
"test": "node --import tsx --test src/client.test.ts src/encoding.test.ts src/errors.test.ts src/ids.test.ts src/mainnet-readiness.test.ts src/network.test.ts src/preflight.test.ts src/public-api-snapshot.test.ts src/redact.test.ts src/round-status.test.ts src/verify.test.ts",
2828
"typecheck": "tsc --noEmit -p tsconfig.json"
2929
},
3030
"dependencies": {

packages/sdk/src/index.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,23 @@ export {
110110
type Severity,
111111
} from "./verify.js";
112112

113+
// Round-status predicates and human-readable labels. Mirror
114+
// services/keeper/src/status.ts status vocab.
115+
export {
116+
ACTIVE_ROUND_STATUSES,
117+
TERMINAL_ROUND_STATUSES,
118+
ERROR_ROUND_STATUSES,
119+
type RoundStatusClass,
120+
classifyRoundStatus,
121+
isActiveRoundStatus,
122+
isTerminalRoundStatus,
123+
isErrorRoundStatus,
124+
roundStatusLabel,
125+
isKeeperRoundActive,
126+
isKeeperRoundTerminal,
127+
isKeeperRoundSettlementPending,
128+
} from "./round-status.js";
129+
113130
// Keeper status-API response shapes. Mirror services/keeper/src/status.ts.
114131
export {
115132
type RoundStatus,

packages/sdk/src/public-api-snapshot.test.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ import * as sdk from "./index.js";
55
const EXPECTED_EXPORTS = [
66
"ASSET_FIXTURES",
77
"AssetConfigError",
8+
"ACTIVE_ROUND_STATUSES",
9+
"ERROR_ROUND_STATUSES",
810
"KeeperStatusClient",
911
"MAINNET_ARTIFACTS",
1012
"MAINNET_CONFIRM_PHRASE",
@@ -25,9 +27,11 @@ const EXPECTED_EXPORTS = [
2527
"SubRosaSubmitError",
2628
"SubRosaTimeoutError",
2729
"SubRosaTransactionError",
30+
"TERMINAL_ROUND_STATUSES",
2831
"assertMainnetConfirmed",
2932
"assertMicroAmounts",
3033
"assertReadinessForExecute",
34+
"classifyRoundStatus",
3135
"contractErrorCode",
3236
"createOzChannelsSubmitter",
3337
"createOzChannelsSubmitterFromEnv",
@@ -38,12 +42,19 @@ const EXPECTED_EXPORTS = [
3842
"fetchKeeperStatus",
3943
"formatReadinessReport",
4044
"hasBlockingFailures",
45+
"isActiveRoundStatus",
46+
"isErrorRoundStatus",
47+
"isKeeperRoundActive",
48+
"isKeeperRoundSettlementPending",
49+
"isKeeperRoundTerminal",
50+
"isTerminalRoundStatus",
4151
"nativeXlmSacId",
4252
"networkFingerprint",
4353
"normalizeRoundId",
4454
"normalizeSorobanContractId",
4555
"parseReceipt",
4656
"redactReceipt",
57+
"roundStatusLabel",
4758
"runMainnetReadiness",
4859
"serializeReceipt",
4960
"tryDecodeBase64",
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import assert from "node:assert/strict";
2+
import { describe, it } from "node:test";
3+
4+
import type { KeeperRoundStatusView, RoundStatus } from "./status.js";
5+
import {
6+
ACTIVE_ROUND_STATUSES,
7+
ERROR_ROUND_STATUSES,
8+
TERMINAL_ROUND_STATUSES,
9+
classifyRoundStatus,
10+
isActiveRoundStatus,
11+
isErrorRoundStatus,
12+
isKeeperRoundActive,
13+
isKeeperRoundSettlementPending,
14+
isKeeperRoundTerminal,
15+
isTerminalRoundStatus,
16+
roundStatusLabel,
17+
} from "./round-status.js";
18+
19+
const ALL_STATUSES: RoundStatus[] = [
20+
"Unknown",
21+
"Open",
22+
"Revealing",
23+
"Cleared",
24+
"Settled",
25+
"Voided",
26+
"NotFound",
27+
];
28+
29+
function viewFor(status: RoundStatus, settlement: KeeperRoundStatusView["settlement"] = "none"): KeeperRoundStatusView {
30+
return {
31+
roundId: "1",
32+
status,
33+
phase: "complete",
34+
nextAction: "none",
35+
commitDeadline: null,
36+
revealDeadline: null,
37+
revealRound: null,
38+
revealReady: false,
39+
commitClosed: false,
40+
revealWindowOpen: false,
41+
voidableAfter: null,
42+
bidderCount: null,
43+
revealedCount: null,
44+
winner: null,
45+
winningValue: null,
46+
clearingRule: null,
47+
settlement,
48+
lastKeeperAction: null,
49+
lastError: null,
50+
retryCount: 0,
51+
updatedAt: "2026-01-01T00:00:00.000Z",
52+
};
53+
}
54+
55+
describe("round-status classification", () => {
56+
it("partitions every status into exactly one class", () => {
57+
const covered = new Set([
58+
...ACTIVE_ROUND_STATUSES,
59+
...TERMINAL_ROUND_STATUSES,
60+
...ERROR_ROUND_STATUSES,
61+
]);
62+
assert.equal(covered.size, ALL_STATUSES.length);
63+
for (const status of ALL_STATUSES) {
64+
assert.ok(covered.has(status), `status ${status} must be classified`);
65+
}
66+
});
67+
68+
it("classifies active statuses", () => {
69+
for (const status of ["Open", "Revealing", "Cleared"] as RoundStatus[]) {
70+
assert.equal(isActiveRoundStatus(status), true);
71+
assert.equal(classifyRoundStatus(status), "active");
72+
}
73+
});
74+
75+
it("classifies terminal statuses", () => {
76+
for (const status of ["Settled", "Voided"] as RoundStatus[]) {
77+
assert.equal(isTerminalRoundStatus(status), true);
78+
assert.equal(classifyRoundStatus(status), "terminal");
79+
}
80+
});
81+
82+
it("classifies error statuses", () => {
83+
for (const status of ["Unknown", "NotFound"] as RoundStatus[]) {
84+
assert.equal(isErrorRoundStatus(status), true);
85+
assert.equal(classifyRoundStatus(status), "error");
86+
}
87+
});
88+
89+
it("does not double-count across buckets", () => {
90+
for (const status of ALL_STATUSES) {
91+
const hits =
92+
Number(isActiveRoundStatus(status)) +
93+
Number(isTerminalRoundStatus(status)) +
94+
Number(isErrorRoundStatus(status));
95+
assert.equal(hits, 1, `status ${status} should match exactly one predicate`);
96+
}
97+
});
98+
});
99+
100+
describe("roundStatusLabel", () => {
101+
it("returns a human-readable label for every status", () => {
102+
for (const status of ALL_STATUSES) {
103+
const label = roundStatusLabel(status);
104+
assert.ok(typeof label === "string" && label.length > 0);
105+
assert.ok(label.toLowerCase().startsWith(status.toLowerCase()));
106+
}
107+
});
108+
});
109+
110+
describe("keeper round view helpers", () => {
111+
it("mirror status classification", () => {
112+
assert.equal(isKeeperRoundActive(viewFor("Open")), true);
113+
assert.equal(isKeeperRoundActive(viewFor("Settled")), false);
114+
assert.equal(isKeeperRoundTerminal(viewFor("Voided")), true);
115+
assert.equal(isKeeperRoundTerminal(viewFor("Revealing")), false);
116+
});
117+
118+
it("treats pending and submitted settlement as pending", () => {
119+
assert.equal(isKeeperRoundSettlementPending(viewFor("Cleared", "pending")), true);
120+
assert.equal(isKeeperRoundSettlementPending(viewFor("Cleared", "submitted")), true);
121+
assert.equal(isKeeperRoundSettlementPending(viewFor("Cleared", "none")), false);
122+
assert.equal(isKeeperRoundSettlementPending(viewFor("Settled", "terminal")), false);
123+
});
124+
});

packages/sdk/src/round-status.ts

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// Pure helpers for reasoning about keeper round statuses without depending on
2+
// the keeper service itself. These classify a `RoundStatus` into coarse
3+
// buckets (active / terminal / error) and turn a status into a human-readable
4+
// label suitable for dashboards, operator CLIs and alert copy.
5+
//
6+
// Keep the status vocab in lockstep with `services/keeper/src/status.ts`.
7+
8+
import type {
9+
KeeperRoundStatusView,
10+
RoundStatus,
11+
SettlementIndicator,
12+
} from "./status.js";
13+
14+
export const ACTIVE_ROUND_STATUSES: readonly RoundStatus[] = [
15+
"Open",
16+
"Revealing",
17+
"Cleared",
18+
];
19+
20+
export const TERMINAL_ROUND_STATUSES: readonly RoundStatus[] = [
21+
"Settled",
22+
"Voided",
23+
];
24+
25+
export const ERROR_ROUND_STATUSES: readonly RoundStatus[] = [
26+
"Unknown",
27+
"NotFound",
28+
];
29+
30+
export type RoundStatusClass = "active" | "terminal" | "error";
31+
32+
export function classifyRoundStatus(status: RoundStatus): RoundStatusClass {
33+
if (isActiveRoundStatus(status)) return "active";
34+
if (isTerminalRoundStatus(status)) return "terminal";
35+
return "error";
36+
}
37+
38+
export function isActiveRoundStatus(status: RoundStatus): boolean {
39+
return (ACTIVE_ROUND_STATUSES as readonly string[]).includes(status);
40+
}
41+
42+
export function isTerminalRoundStatus(status: RoundStatus): boolean {
43+
return (TERMINAL_ROUND_STATUSES as readonly string[]).includes(status);
44+
}
45+
46+
export function isErrorRoundStatus(status: RoundStatus): boolean {
47+
return (ERROR_ROUND_STATUSES as readonly string[]).includes(status);
48+
}
49+
50+
const ROUND_STATUS_LABELS: Record<RoundStatus, string> = {
51+
Unknown: "Unknown — keeper has not resolved the round yet",
52+
Open: "Open — accepting commitments",
53+
Revealing: "Revealing — accepting reveals",
54+
Cleared: "Cleared — awaiting settlement",
55+
Settled: "Settled — round complete",
56+
Voided: "Voided — escrow refunded",
57+
NotFound: "NotFound — round does not exist on-chain",
58+
};
59+
60+
export function roundStatusLabel(status: RoundStatus): string {
61+
return ROUND_STATUS_LABELS[status];
62+
}
63+
64+
export function isKeeperRoundActive(view: KeeperRoundStatusView): boolean {
65+
return isActiveRoundStatus(view.status);
66+
}
67+
68+
export function isKeeperRoundTerminal(view: KeeperRoundStatusView): boolean {
69+
return isTerminalRoundStatus(view.status);
70+
}
71+
72+
export function isKeeperRoundSettlementPending(
73+
view: KeeperRoundStatusView,
74+
): boolean {
75+
const pending: readonly SettlementIndicator[] = ["pending", "submitted"];
76+
return (pending as readonly string[]).includes(view.settlement);
77+
}

0 commit comments

Comments
 (0)