@stellaris-lab/por-sdk 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,199 @@
1
+ /**
2
+ * Domain model for the Stellaris proof-of-reserves SDK.
3
+ *
4
+ * This file intentionally has no Stellar SDK or snarkjs imports. Mature SDKs keep
5
+ * domain primitives independent from transport/proving so they can be reused by
6
+ * CLIs, backend services, browser apps, tests, and indexers.
7
+ */
8
+ export type DecimalString = string;
9
+ export type PublicKey = string;
10
+ export type ContractId = string;
11
+ export type UnixTimestamp = bigint;
12
+ export type PeriodId = bigint;
13
+ export type FieldElement = DecimalString;
14
+ export interface ReserveAccount {
15
+ readonly label: string;
16
+ readonly balance: bigint;
17
+ readonly asset?: string;
18
+ readonly sourceRef?: string;
19
+ }
20
+ export interface LiabilityStatement {
21
+ readonly total: bigint;
22
+ readonly asset?: string;
23
+ readonly sourceRef?: string;
24
+ }
25
+ export interface ReserveSnapshot {
26
+ readonly periodId: PeriodId;
27
+ readonly accounts: readonly ReserveAccount[];
28
+ readonly liabilities: LiabilityStatement;
29
+ readonly salt: bigint;
30
+ readonly metadata?: SnapshotMetadata;
31
+ }
32
+ export interface SnapshotMetadata {
33
+ readonly issuerName?: string;
34
+ readonly ledger?: bigint;
35
+ readonly generatedAt?: UnixTimestamp;
36
+ readonly externalReportUri?: string;
37
+ readonly notes?: readonly string[];
38
+ }
39
+ export interface NormalizedReserveSnapshot {
40
+ readonly periodId: PeriodId;
41
+ readonly balances: readonly bigint[];
42
+ readonly liabilities: bigint;
43
+ readonly salt: bigint;
44
+ readonly accounts: readonly ReserveAccount[];
45
+ readonly metadata?: SnapshotMetadata;
46
+ }
47
+ export interface PublicSignals {
48
+ readonly solvent: boolean;
49
+ readonly commitment: FieldElement;
50
+ readonly liabilities: bigint;
51
+ readonly periodId: PeriodId;
52
+ }
53
+ export interface ProofBundle {
54
+ readonly proof: SnarkJsGroth16Proof;
55
+ readonly publicSignals: readonly FieldElement[];
56
+ readonly parsed: PublicSignals;
57
+ }
58
+ /** v2 public signals: [solvent, reserveCommitment, liabRoot, liabTotal, period]. */
59
+ export interface PublicSignalsV2 {
60
+ readonly solvent: boolean;
61
+ readonly reserveCommitment: FieldElement;
62
+ readonly liabRoot: FieldElement;
63
+ readonly liabTotal: bigint;
64
+ readonly periodId: PeriodId;
65
+ }
66
+ export interface ProofBundleV2 {
67
+ readonly proof: SnarkJsGroth16Proof;
68
+ readonly publicSignals: readonly FieldElement[];
69
+ readonly parsed: PublicSignalsV2;
70
+ }
71
+ /**
72
+ * v3 public signals (multi-asset): [aggregateSolvent, reserveCommitment,
73
+ * priceCommitment, assetSolvent[0..n-1], period]. `assetSolvent` is the
74
+ * per-asset flag vector; `aggregateSolvent` is the oracle-priced aggregate.
75
+ */
76
+ export interface PublicSignalsV3 {
77
+ readonly aggregateSolvent: boolean;
78
+ readonly reserveCommitment: FieldElement;
79
+ readonly priceCommitment: FieldElement;
80
+ readonly assetSolvent: readonly boolean[];
81
+ readonly periodId: PeriodId;
82
+ }
83
+ export interface ProofBundleV3 {
84
+ readonly proof: SnarkJsGroth16Proof;
85
+ readonly publicSignals: readonly FieldElement[];
86
+ readonly parsed: PublicSignalsV3;
87
+ }
88
+ /** A G2 point pair as emitted by snarkjs (two field-element coordinates). */
89
+ export type Groth16G2Pair = readonly [FieldElement, FieldElement];
90
+ export interface SnarkJsGroth16Proof {
91
+ readonly pi_a: readonly [FieldElement, FieldElement, FieldElement?];
92
+ readonly pi_b: readonly [Groth16G2Pair, Groth16G2Pair, Groth16G2Pair?];
93
+ readonly pi_c: readonly [FieldElement, FieldElement, FieldElement?];
94
+ readonly protocol?: string;
95
+ readonly curve?: string;
96
+ }
97
+ export interface Attestation {
98
+ readonly commitment: FieldElement;
99
+ readonly liabilities: bigint;
100
+ readonly solvent: boolean;
101
+ readonly ledgerTs: UnixTimestamp;
102
+ readonly periodId: PeriodId;
103
+ readonly issuer: PublicKey;
104
+ }
105
+ export interface AttestationReceipt {
106
+ readonly attestation: Attestation;
107
+ readonly transactionHash?: string;
108
+ readonly ledger?: bigint;
109
+ readonly networkPassphrase: string;
110
+ readonly contractId: ContractId;
111
+ }
112
+ /**
113
+ * v2 attestation: solvency with a SNARK-proven liability total. Unlike v1, the
114
+ * liability figure (`liabTotal`) is not a trusted scalar — it is the constrained
115
+ * total of a Merkle-sum liability tree, committed to by `liabRoot`.
116
+ */
117
+ export interface AttestationV2 {
118
+ readonly reserveCommitment: FieldElement;
119
+ readonly liabRoot: FieldElement;
120
+ readonly liabTotal: bigint;
121
+ readonly solvent: boolean;
122
+ readonly ledgerTs: UnixTimestamp;
123
+ readonly periodId: PeriodId;
124
+ readonly issuer: PublicKey;
125
+ }
126
+ export interface AttestationReceiptV2 {
127
+ readonly attestation: AttestationV2;
128
+ readonly transactionHash?: string;
129
+ readonly ledger?: bigint;
130
+ readonly networkPassphrase: string;
131
+ readonly contractId: ContractId;
132
+ }
133
+ /**
134
+ * v3 multi-asset attestation: oracle-priced aggregate solvency plus per-asset
135
+ * flags. `aggregateSolvent` is the priced cross-asset solvency; `assetSolvent`
136
+ * is the per-asset breakdown (an asset may be underwater while the aggregate is
137
+ * solvent). `priceCommitment` binds the price vector used (trust boundary: the
138
+ * prices' authenticity is a C3 signed-feed concern).
139
+ */
140
+ export interface AttestationV3 {
141
+ readonly aggregateSolvent: boolean;
142
+ readonly reserveCommitment: FieldElement;
143
+ readonly priceCommitment: FieldElement;
144
+ readonly assetSolvent: readonly boolean[];
145
+ /**
146
+ * C3 provenance: true iff a designated-oracle price commitment was published
147
+ * for this period AND the attested priceCommitment matched it. When false the
148
+ * prices are issuer-chosen; a consumer requiring oracle-bound pricing MUST
149
+ * check this flag.
150
+ */
151
+ readonly oracleBound: boolean;
152
+ /**
153
+ * C2 provenance: true iff a designated custodian's BLS12-381 signature over the
154
+ * reserveCommitment was presented and verified on-chain (via `attestV3Signed`).
155
+ * When false the reserves are not custodian-attested; a consumer requiring a
156
+ * named-custodian signature MUST check this flag.
157
+ */
158
+ readonly custodianBound: boolean;
159
+ readonly ledgerTs: UnixTimestamp;
160
+ readonly periodId: PeriodId;
161
+ readonly issuer: PublicKey;
162
+ }
163
+ export interface AttestationReceiptV3 {
164
+ readonly attestation: AttestationV3;
165
+ readonly transactionHash?: string;
166
+ readonly ledger?: bigint;
167
+ readonly networkPassphrase: string;
168
+ readonly contractId: ContractId;
169
+ }
170
+ export interface VerificationKeyDocument {
171
+ readonly protocol?: string;
172
+ readonly curve?: string;
173
+ readonly nPublic?: number;
174
+ readonly vk_alpha_1?: unknown;
175
+ readonly vk_beta_2?: unknown;
176
+ readonly vk_gamma_2?: unknown;
177
+ readonly vk_delta_2?: unknown;
178
+ readonly IC?: unknown[];
179
+ readonly [key: string]: unknown;
180
+ }
181
+ export interface ProvingArtifacts {
182
+ readonly wasmUrl: string;
183
+ readonly zkeyUrl: string;
184
+ readonly verificationKey?: VerificationKeyDocument;
185
+ }
186
+ export interface ContractDeployment {
187
+ readonly contractId: ContractId;
188
+ readonly networkPassphrase: string;
189
+ readonly rpcUrl: string;
190
+ }
191
+ export declare function normalizeSnapshot(snapshot: ReserveSnapshot): NormalizedReserveSnapshot;
192
+ export declare function totalReserves(snapshot: Pick<NormalizedReserveSnapshot, "balances">): bigint;
193
+ export declare function isSolvent(snapshot: Pick<NormalizedReserveSnapshot, "balances" | "liabilities">): boolean;
194
+ export declare function toReserveInput(snapshot: NormalizedReserveSnapshot): {
195
+ balances: bigint[];
196
+ salt: bigint;
197
+ liabilities: bigint;
198
+ periodId: bigint;
199
+ };
package/dist/domain.js ADDED
@@ -0,0 +1,57 @@
1
+ /**
2
+ * Domain model for the Stellaris proof-of-reserves SDK.
3
+ *
4
+ * This file intentionally has no Stellar SDK or snarkjs imports. Mature SDKs keep
5
+ * domain primitives independent from transport/proving so they can be reused by
6
+ * CLIs, backend services, browser apps, tests, and indexers.
7
+ */
8
+ import { MAX_RESERVE, N_RESERVES } from "./constants.js";
9
+ import { StellarisError } from "./errors.js";
10
+ export function normalizeSnapshot(snapshot) {
11
+ if (snapshot.accounts.length === 0) {
12
+ throw StellarisError.validation("reserve snapshot must include at least one account");
13
+ }
14
+ if (snapshot.accounts.length > N_RESERVES) {
15
+ throw StellarisError.validation(`reserve snapshot has ${snapshot.accounts.length} accounts; max is ${N_RESERVES}`);
16
+ }
17
+ if (snapshot.periodId < 0n) {
18
+ throw StellarisError.validation("periodId must be non-negative");
19
+ }
20
+ if (snapshot.salt < 0n) {
21
+ throw StellarisError.validation("salt must be non-negative");
22
+ }
23
+ if (snapshot.liabilities.total < 0n) {
24
+ throw StellarisError.validation("liabilities must be non-negative");
25
+ }
26
+ const balances = snapshot.accounts.map((account, index) => {
27
+ if (!account.label || account.label.trim().length === 0) {
28
+ throw StellarisError.validation(`reserve account ${index} is missing a label`);
29
+ }
30
+ if (account.balance < 0n || account.balance > MAX_RESERVE) {
31
+ throw StellarisError.validation(`reserve account ${account.label} balance is outside uint64 range`);
32
+ }
33
+ return account.balance;
34
+ });
35
+ return {
36
+ periodId: snapshot.periodId,
37
+ balances,
38
+ liabilities: snapshot.liabilities.total,
39
+ salt: snapshot.salt,
40
+ accounts: snapshot.accounts,
41
+ ...(snapshot.metadata === undefined ? {} : { metadata: snapshot.metadata }),
42
+ };
43
+ }
44
+ export function totalReserves(snapshot) {
45
+ return snapshot.balances.reduce((sum, balance) => sum + balance, 0n);
46
+ }
47
+ export function isSolvent(snapshot) {
48
+ return totalReserves(snapshot) >= snapshot.liabilities;
49
+ }
50
+ export function toReserveInput(snapshot) {
51
+ return {
52
+ balances: [...snapshot.balances],
53
+ salt: snapshot.salt,
54
+ liabilities: snapshot.liabilities,
55
+ periodId: snapshot.periodId,
56
+ };
57
+ }
@@ -0,0 +1,67 @@
1
+ /**
2
+ * BLS12-381 byte encoding for the Stellaris contract ABI.
3
+ *
4
+ * This is the SINGLE shared converter required by the cross-file proof-encoding
5
+ * invariant: the bytes produced here MUST be identical to what the on-chain
6
+ * Soroban verifier (contracts/stellaris) consumes. The contract takes:
7
+ *
8
+ * - G1 points (proof.a, proof.c, vk.alpha, vk.ic[i]) as 96-byte uncompressed
9
+ * big-endian: X(48) || Y(48)
10
+ * - G2 points (proof.b, vk.beta/gamma/delta) as 192-byte uncompressed
11
+ * big-endian: X.c1(48) || X.c0(48) || Y.c1(48) || Y.c0(48)
12
+ * - Public signals (U256) as 32-byte big-endian.
13
+ *
14
+ * snarkjs emits each Fp2 as [c0, c1]. The arkworks / Soroban uncompressed
15
+ * serialization writes the HIGH coefficient (c1) first, so the G2 encoder SWAPS
16
+ * within each Fp2 pair. This was confirmed byte-for-byte against the Rust
17
+ * on-chain pairing path (ark-bls12-381 `serialize_uncompressed` feeding the
18
+ * `G2Affine` the verifier accepts) and the official stellar/soroban-examples
19
+ * groth16_verifier. Do NOT reorder without re-running the byte-equality test.
20
+ *
21
+ * This module has ZERO crypto dependencies: it only does fixed-width big-endian
22
+ * serialization of already-reduced field elements. Curve membership is checked
23
+ * on-chain by the verifier, not here.
24
+ */
25
+ import { SnarkJsGroth16Proof } from "./domain.js";
26
+ /** Bytes per BLS12-381 base-field element (Fp), uncompressed big-endian. */
27
+ export declare const FP_SIZE = 48;
28
+ /** Bytes per G1 affine point: X(48) || Y(48). */
29
+ export declare const G1_SIZE = 96;
30
+ /** Bytes per G2 affine point: X.c1(48) || X.c0(48) || Y.c1(48) || Y.c0(48). */
31
+ export declare const G2_SIZE = 192;
32
+ /** Bytes per public signal (U256), big-endian. */
33
+ export declare const U256_SIZE = 32;
34
+ /**
35
+ * Serialize a non-negative decimal string into a fixed-width big-endian buffer.
36
+ * Throws if the value is malformed or overflows `size` bytes.
37
+ */
38
+ export declare function decimalToBytes(decimal: string, size: number): Uint8Array;
39
+ /** Serialize one Fp field element to 48 big-endian bytes. */
40
+ export declare function fpToBytes(decimal: string): Uint8Array;
41
+ /** Serialize a G1 affine point [x, y] to 96 uncompressed big-endian bytes: X || Y. */
42
+ export declare function g1ToBytes(x: string, y: string): Uint8Array;
43
+ /**
44
+ * Serialize a G2 affine point to 192 uncompressed big-endian bytes.
45
+ * Inputs are in snarkjs [c0, c1] order; output writes c1 || c0 per Fp2 (the
46
+ * high coefficient first), matching the on-chain layout.
47
+ */
48
+ export declare function g2ToBytes(xC0: string, xC1: string, yC0: string, yC1: string): Uint8Array;
49
+ /** Serialize a public signal (decimal U256) to 32 big-endian bytes. */
50
+ export declare function signalToBytes(decimal: string): Uint8Array;
51
+ /** A Groth16 proof serialized to the contract's byte layout. */
52
+ export interface ContractProofBytes {
53
+ /** proof.a — G1, 96 bytes. */
54
+ readonly a: Uint8Array;
55
+ /** proof.b — G2, 192 bytes. */
56
+ readonly b: Uint8Array;
57
+ /** proof.c — G1, 96 bytes. */
58
+ readonly c: Uint8Array;
59
+ }
60
+ /**
61
+ * Serialize a snarkjs Groth16 proof to the contract's byte layout (G1/G2
62
+ * uncompressed). This is the byte-exact counterpart to `proofToContractArgs`,
63
+ * which keeps the decimal-string shape for inspection/logging.
64
+ */
65
+ export declare function proofToBytes(proof: SnarkJsGroth16Proof): ContractProofBytes;
66
+ /** Lowercase hex of a byte buffer (no 0x prefix) — for tests / debugging. */
67
+ export declare function bytesToHex(bytes: Uint8Array): string;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * BLS12-381 byte encoding for the Stellaris contract ABI.
3
+ *
4
+ * This is the SINGLE shared converter required by the cross-file proof-encoding
5
+ * invariant: the bytes produced here MUST be identical to what the on-chain
6
+ * Soroban verifier (contracts/stellaris) consumes. The contract takes:
7
+ *
8
+ * - G1 points (proof.a, proof.c, vk.alpha, vk.ic[i]) as 96-byte uncompressed
9
+ * big-endian: X(48) || Y(48)
10
+ * - G2 points (proof.b, vk.beta/gamma/delta) as 192-byte uncompressed
11
+ * big-endian: X.c1(48) || X.c0(48) || Y.c1(48) || Y.c0(48)
12
+ * - Public signals (U256) as 32-byte big-endian.
13
+ *
14
+ * snarkjs emits each Fp2 as [c0, c1]. The arkworks / Soroban uncompressed
15
+ * serialization writes the HIGH coefficient (c1) first, so the G2 encoder SWAPS
16
+ * within each Fp2 pair. This was confirmed byte-for-byte against the Rust
17
+ * on-chain pairing path (ark-bls12-381 `serialize_uncompressed` feeding the
18
+ * `G2Affine` the verifier accepts) and the official stellar/soroban-examples
19
+ * groth16_verifier. Do NOT reorder without re-running the byte-equality test.
20
+ *
21
+ * This module has ZERO crypto dependencies: it only does fixed-width big-endian
22
+ * serialization of already-reduced field elements. Curve membership is checked
23
+ * on-chain by the verifier, not here.
24
+ */
25
+ import { StellarisError } from "./errors.js";
26
+ /** Bytes per BLS12-381 base-field element (Fp), uncompressed big-endian. */
27
+ export const FP_SIZE = 48;
28
+ /** Bytes per G1 affine point: X(48) || Y(48). */
29
+ export const G1_SIZE = 96;
30
+ /** Bytes per G2 affine point: X.c1(48) || X.c0(48) || Y.c1(48) || Y.c0(48). */
31
+ export const G2_SIZE = 192;
32
+ /** Bytes per public signal (U256), big-endian. */
33
+ export const U256_SIZE = 32;
34
+ const DECIMAL = /^(0|[1-9][0-9]*)$/;
35
+ /**
36
+ * Serialize a non-negative decimal string into a fixed-width big-endian buffer.
37
+ * Throws if the value is malformed or overflows `size` bytes.
38
+ */
39
+ export function decimalToBytes(decimal, size) {
40
+ if (!DECIMAL.test(decimal)) {
41
+ throw StellarisError.encoding("field element must be a non-negative decimal string", {
42
+ value: decimal,
43
+ });
44
+ }
45
+ let value = BigInt(decimal);
46
+ const out = new Uint8Array(size);
47
+ for (let i = size - 1; i >= 0; i--) {
48
+ out[i] = Number(value & 0xffn);
49
+ value >>= 8n;
50
+ }
51
+ if (value !== 0n) {
52
+ throw StellarisError.encoding(`value exceeds ${size} bytes (serialization overflow)`, {
53
+ value: decimal,
54
+ });
55
+ }
56
+ return out;
57
+ }
58
+ /** Serialize one Fp field element to 48 big-endian bytes. */
59
+ export function fpToBytes(decimal) {
60
+ return decimalToBytes(decimal, FP_SIZE);
61
+ }
62
+ /** Serialize a G1 affine point [x, y] to 96 uncompressed big-endian bytes: X || Y. */
63
+ export function g1ToBytes(x, y) {
64
+ const out = new Uint8Array(G1_SIZE);
65
+ out.set(fpToBytes(x), 0);
66
+ out.set(fpToBytes(y), FP_SIZE);
67
+ return out;
68
+ }
69
+ /**
70
+ * Serialize a G2 affine point to 192 uncompressed big-endian bytes.
71
+ * Inputs are in snarkjs [c0, c1] order; output writes c1 || c0 per Fp2 (the
72
+ * high coefficient first), matching the on-chain layout.
73
+ */
74
+ export function g2ToBytes(xC0, xC1, yC0, yC1) {
75
+ const out = new Uint8Array(G2_SIZE);
76
+ out.set(fpToBytes(xC1), 0);
77
+ out.set(fpToBytes(xC0), FP_SIZE);
78
+ out.set(fpToBytes(yC1), FP_SIZE * 2);
79
+ out.set(fpToBytes(yC0), FP_SIZE * 3);
80
+ return out;
81
+ }
82
+ /** Serialize a public signal (decimal U256) to 32 big-endian bytes. */
83
+ export function signalToBytes(decimal) {
84
+ return decimalToBytes(decimal, U256_SIZE);
85
+ }
86
+ /**
87
+ * Serialize a snarkjs Groth16 proof to the contract's byte layout (G1/G2
88
+ * uncompressed). This is the byte-exact counterpart to `proofToContractArgs`,
89
+ * which keeps the decimal-string shape for inspection/logging.
90
+ */
91
+ export function proofToBytes(proof) {
92
+ if (!Array.isArray(proof.pi_a) || !Array.isArray(proof.pi_b) || !Array.isArray(proof.pi_c)) {
93
+ throw StellarisError.encoding("invalid snarkjs proof shape");
94
+ }
95
+ const a0 = proof.pi_a[0];
96
+ const a1 = proof.pi_a[1];
97
+ const c0 = proof.pi_c[0];
98
+ const c1 = proof.pi_c[1];
99
+ const bx = proof.pi_b[0];
100
+ const by = proof.pi_b[1];
101
+ if (a0 === undefined || a1 === undefined || c0 === undefined || c1 === undefined) {
102
+ throw StellarisError.encoding("proof pi_a / pi_c missing coordinates");
103
+ }
104
+ if (!Array.isArray(bx) || !Array.isArray(by) || bx.length < 2 || by.length < 2) {
105
+ throw StellarisError.encoding("proof pi_b must be [[x.c0,x.c1],[y.c0,y.c1]]");
106
+ }
107
+ return {
108
+ a: g1ToBytes(a0, a1),
109
+ b: g2ToBytes(bx[0], bx[1], by[0], by[1]),
110
+ c: g1ToBytes(c0, c1),
111
+ };
112
+ }
113
+ /** Lowercase hex of a byte buffer (no 0x prefix) — for tests / debugging. */
114
+ export function bytesToHex(bytes) {
115
+ let s = "";
116
+ for (const b of bytes) {
117
+ s += b.toString(16).padStart(2, "0");
118
+ }
119
+ return s;
120
+ }
@@ -0,0 +1,25 @@
1
+ /**
2
+ * Structured SDK errors.
3
+ *
4
+ * The SDK uses stable error codes instead of ad-hoc Error messages so API
5
+ * consumers can branch safely, similar to mature protocol SDKs.
6
+ */
7
+ export type StellarisErrorKind = "validation" | "proving" | "verification" | "encoding" | "transport" | "contract" | "configuration";
8
+ export interface StellarisErrorDetails {
9
+ readonly cause?: unknown;
10
+ readonly context?: Record<string, unknown>;
11
+ }
12
+ export type StellarisErrorContext = Record<string, unknown> | StellarisErrorDetails;
13
+ export declare class StellarisError extends Error {
14
+ readonly kind: StellarisErrorKind;
15
+ readonly context?: Record<string, unknown>;
16
+ constructor(kind: StellarisErrorKind, message: string, details?: StellarisErrorDetails);
17
+ static validation(message: string, context?: Record<string, unknown>): StellarisError;
18
+ static proving(message: string, details?: StellarisErrorContext): StellarisError;
19
+ static verification(message: string, details?: StellarisErrorContext): StellarisError;
20
+ static encoding(message: string, details?: StellarisErrorContext): StellarisError;
21
+ static transport(message: string, details?: StellarisErrorContext): StellarisError;
22
+ static contract(message: string, details?: StellarisErrorContext): StellarisError;
23
+ static configuration(message: string, details?: StellarisErrorContext): StellarisError;
24
+ }
25
+ export declare function wrapUnknown(kind: StellarisErrorKind, message: string, cause: unknown): StellarisError;
package/dist/errors.js ADDED
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Structured SDK errors.
3
+ *
4
+ * The SDK uses stable error codes instead of ad-hoc Error messages so API
5
+ * consumers can branch safely, similar to mature protocol SDKs.
6
+ */
7
+ export class StellarisError extends Error {
8
+ kind;
9
+ context;
10
+ constructor(kind, message, details) {
11
+ super(`[${kind}] ${message}`);
12
+ this.name = "StellarisError";
13
+ this.kind = kind;
14
+ if (details?.context !== undefined) {
15
+ this.context = details.context;
16
+ }
17
+ if (details?.cause !== undefined) {
18
+ this.cause = details.cause;
19
+ }
20
+ }
21
+ static validation(message, context) {
22
+ return new StellarisError("validation", message, normalizeDetails(context));
23
+ }
24
+ static proving(message, details) {
25
+ return new StellarisError("proving", message, normalizeDetails(details));
26
+ }
27
+ static verification(message, details) {
28
+ return new StellarisError("verification", message, normalizeDetails(details));
29
+ }
30
+ static encoding(message, details) {
31
+ return new StellarisError("encoding", message, normalizeDetails(details));
32
+ }
33
+ static transport(message, details) {
34
+ return new StellarisError("transport", message, normalizeDetails(details));
35
+ }
36
+ static contract(message, details) {
37
+ return new StellarisError("contract", message, normalizeDetails(details));
38
+ }
39
+ static configuration(message, details) {
40
+ return new StellarisError("configuration", message, normalizeDetails(details));
41
+ }
42
+ }
43
+ export function wrapUnknown(kind, message, cause) {
44
+ return new StellarisError(kind, message, { cause });
45
+ }
46
+ function normalizeDetails(details) {
47
+ if (details === undefined) {
48
+ return undefined;
49
+ }
50
+ if ("context" in details || "cause" in details) {
51
+ return details;
52
+ }
53
+ return { context: details };
54
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Typed registry/indexer events.
3
+ *
4
+ * Backend consumers should subscribe to stable events instead of reaching into
5
+ * reconciler internals. This mirrors mature SDK event surfaces: typed payloads,
6
+ * bounded replay, and composable sinks.
7
+ */
8
+ import { PublicKey } from "./domain.js";
9
+ import { RegistryDiagnostics, RegistrySnapshot } from "./registry.js";
10
+ import { ReconciliationFailure, ReconciliationResult } from "./reconciler.js";
11
+ export type RegistryEventKind = "reconciler.started" | "issuer.refresh.started" | "issuer.refresh.succeeded" | "issuer.refresh.failed" | "checkpoint.written" | "reconciler.completed";
12
+ export type RegistryEvent = ReconcilerStartedEvent | IssuerRefreshStartedEvent | IssuerRefreshSucceededEvent | IssuerRefreshFailedEvent | CheckpointWrittenEvent | ReconcilerCompletedEvent;
13
+ export interface RegistryEventBase<Kind extends RegistryEventKind> {
14
+ readonly kind: Kind;
15
+ readonly id: string;
16
+ readonly runId: string;
17
+ readonly at: string;
18
+ }
19
+ export interface ReconcilerStartedEvent extends RegistryEventBase<"reconciler.started"> {
20
+ readonly targetCount: number;
21
+ }
22
+ export interface IssuerRefreshStartedEvent extends RegistryEventBase<"issuer.refresh.started"> {
23
+ readonly issuer: PublicKey;
24
+ readonly attempt: number;
25
+ }
26
+ export interface IssuerRefreshSucceededEvent extends RegistryEventBase<"issuer.refresh.succeeded"> {
27
+ readonly issuer: PublicKey;
28
+ readonly attempt: number;
29
+ readonly periodCount: number;
30
+ readonly diagnostics: RegistryDiagnostics;
31
+ }
32
+ export interface IssuerRefreshFailedEvent extends RegistryEventBase<"issuer.refresh.failed"> {
33
+ readonly issuer: PublicKey;
34
+ readonly failure: ReconciliationFailure;
35
+ }
36
+ export interface CheckpointWrittenEvent extends RegistryEventBase<"checkpoint.written"> {
37
+ readonly issuerCount: number;
38
+ readonly recordCount: number;
39
+ }
40
+ export interface ReconcilerCompletedEvent extends RegistryEventBase<"reconciler.completed"> {
41
+ readonly status: ReconciliationResult["status"];
42
+ readonly attempts: number;
43
+ readonly snapshots: number;
44
+ readonly failures: number;
45
+ }
46
+ export interface RegistryEventSink {
47
+ emit(event: RegistryEvent): void | Promise<void>;
48
+ }
49
+ export interface RegistryEventSubscription {
50
+ unsubscribe(): void;
51
+ }
52
+ export type RegistryEventHandler = (event: RegistryEvent) => void | Promise<void>;
53
+ /** Distributive Omit so each union member keeps its own discriminated fields. */
54
+ type DistributiveOmit<T, K extends keyof never> = T extends unknown ? Omit<T, K> : never;
55
+ export declare class InMemoryRegistryEventLog implements RegistryEventSink {
56
+ private readonly events;
57
+ emit(event: RegistryEvent): void;
58
+ replay(handler: RegistryEventHandler, filter?: Partial<{
59
+ readonly kind: RegistryEventKind;
60
+ readonly runId: string;
61
+ }>): void;
62
+ snapshot(): readonly RegistryEvent[];
63
+ clear(): void;
64
+ }
65
+ export declare class RegistryEventBus implements RegistryEventSink {
66
+ private readonly handlers;
67
+ private readonly replayLog?;
68
+ constructor(input?: {
69
+ readonly replayLog?: InMemoryRegistryEventLog;
70
+ });
71
+ subscribe(handler: RegistryEventHandler): RegistryEventSubscription;
72
+ emit(event: RegistryEvent): Promise<void>;
73
+ }
74
+ export declare function makeRegistryEvent(input: DistributiveOmit<RegistryEvent, "id" | "at"> & {
75
+ readonly at?: string;
76
+ }): RegistryEvent;
77
+ export declare function snapshotToRefreshEvent(runId: string, snapshot: RegistrySnapshot, attempt: number): IssuerRefreshSucceededEvent;
78
+ export {};
package/dist/events.js ADDED
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Typed registry/indexer events.
3
+ *
4
+ * Backend consumers should subscribe to stable events instead of reaching into
5
+ * reconciler internals. This mirrors mature SDK event surfaces: typed payloads,
6
+ * bounded replay, and composable sinks.
7
+ */
8
+ export class InMemoryRegistryEventLog {
9
+ events = [];
10
+ emit(event) {
11
+ this.events.push(event);
12
+ }
13
+ replay(handler, filter) {
14
+ for (const event of this.events) {
15
+ if (filter?.kind !== undefined && event.kind !== filter.kind) {
16
+ continue;
17
+ }
18
+ if (filter?.runId !== undefined && event.runId !== filter.runId) {
19
+ continue;
20
+ }
21
+ void handler(event);
22
+ }
23
+ }
24
+ snapshot() {
25
+ return [...this.events];
26
+ }
27
+ clear() {
28
+ this.events.length = 0;
29
+ }
30
+ }
31
+ export class RegistryEventBus {
32
+ handlers = new Set();
33
+ replayLog;
34
+ constructor(input = {}) {
35
+ if (input.replayLog !== undefined) {
36
+ this.replayLog = input.replayLog;
37
+ }
38
+ }
39
+ subscribe(handler) {
40
+ this.handlers.add(handler);
41
+ return {
42
+ unsubscribe: () => {
43
+ this.handlers.delete(handler);
44
+ },
45
+ };
46
+ }
47
+ async emit(event) {
48
+ this.replayLog?.emit(event);
49
+ await Promise.all([...this.handlers].map((handler) => handler(event)));
50
+ }
51
+ }
52
+ export function makeRegistryEvent(input) {
53
+ const at = input.at ?? new Date().toISOString();
54
+ const id = `${input.runId}:${input.kind}:${at}`;
55
+ return { ...input, id, at };
56
+ }
57
+ export function snapshotToRefreshEvent(runId, snapshot, attempt) {
58
+ return makeRegistryEvent({
59
+ kind: "issuer.refresh.succeeded",
60
+ runId,
61
+ issuer: snapshot.issuer,
62
+ attempt,
63
+ periodCount: snapshot.periods.length,
64
+ diagnostics: snapshot.diagnostics,
65
+ });
66
+ }