@hsuite/smart-engines-sdk 3.0.2 → 3.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.
- package/CHANGELOG.md +28 -0
- package/README.md +43 -12
- package/dist/index.d.ts +348 -34
- package/dist/index.js +642 -20
- package/dist/index.js.map +1 -1
- package/dist/nestjs/index.d.ts +292 -33
- package/dist/nestjs/index.js +598 -20
- package/dist/nestjs/index.js.map +1 -1
- package/package.json +1 -1
package/dist/nestjs/index.js
CHANGED
|
@@ -1022,6 +1022,185 @@ function normalizeRegistryEntry(raw) {
|
|
|
1022
1022
|
return null;
|
|
1023
1023
|
}
|
|
1024
1024
|
|
|
1025
|
+
// src/discovery/cluster-discovery.ts
|
|
1026
|
+
var ClusterDiscoveryClient = class {
|
|
1027
|
+
bootstrap;
|
|
1028
|
+
cacheTtlMs;
|
|
1029
|
+
fetchTimeoutMs;
|
|
1030
|
+
allowInsecure;
|
|
1031
|
+
trustAnchorClient;
|
|
1032
|
+
cache = null;
|
|
1033
|
+
constructor(config) {
|
|
1034
|
+
if (!config.bootstrap || config.bootstrap.length === 0) {
|
|
1035
|
+
throw new Error("ClusterDiscoveryClient: bootstrap must list at least one seed URL");
|
|
1036
|
+
}
|
|
1037
|
+
if (!config.allowInsecure) {
|
|
1038
|
+
for (const seed of config.bootstrap) {
|
|
1039
|
+
if (!seed.startsWith("https://")) {
|
|
1040
|
+
throw new Error(
|
|
1041
|
+
`ClusterDiscoveryClient: bootstrap seed "${seed}" is not HTTPS. Set allowInsecure=true for local development only.`
|
|
1042
|
+
);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
this.bootstrap = config.bootstrap;
|
|
1047
|
+
this.cacheTtlMs = config.cacheTtlMs ?? 6e4;
|
|
1048
|
+
this.fetchTimeoutMs = config.fetchTimeoutMs ?? 5e3;
|
|
1049
|
+
this.allowInsecure = config.allowInsecure ?? false;
|
|
1050
|
+
this.trustAnchorClient = config.trustAnchor ? new ValidatorDiscoveryClient(config.trustAnchor) : null;
|
|
1051
|
+
}
|
|
1052
|
+
/**
|
|
1053
|
+
* Fetch the active-cluster set, with caching.
|
|
1054
|
+
*
|
|
1055
|
+
* Tries each bootstrap seed in order until one returns HTTP 200 with a
|
|
1056
|
+
* parseable cluster list. If all seeds fail, throws — callers can
|
|
1057
|
+
* handle by falling back to a previously-cached value if they want,
|
|
1058
|
+
* or by initializing with multiple seeds.
|
|
1059
|
+
*/
|
|
1060
|
+
async getClusters(forceRefresh = false) {
|
|
1061
|
+
if (!forceRefresh && this.cache && this.isCacheValid()) {
|
|
1062
|
+
return this.cache.clusters;
|
|
1063
|
+
}
|
|
1064
|
+
const fetched = await this.fetchFromFirstAvailableSeed();
|
|
1065
|
+
const verified = this.trustAnchorClient ? await this.verifyAgainstTrustAnchor(fetched) : fetched;
|
|
1066
|
+
this.cache = { clusters: verified, lastUpdated: Date.now() };
|
|
1067
|
+
return verified;
|
|
1068
|
+
}
|
|
1069
|
+
/**
|
|
1070
|
+
* Random pick over the active-cluster set. Returns `null` when no
|
|
1071
|
+
* cluster passes verification (empty registry / all-rejected anchor).
|
|
1072
|
+
*
|
|
1073
|
+
* Uses `crypto.getRandomValues` when available (browsers, Deno, modern
|
|
1074
|
+
* Node) for cryptographic-strength randomness; `Math.random` is a
|
|
1075
|
+
* fallback for older runtimes where `globalThis.crypto` is undefined.
|
|
1076
|
+
* Discovery isn't cryptographically sensitive — load distribution is
|
|
1077
|
+
* the goal here.
|
|
1078
|
+
*/
|
|
1079
|
+
async getRandomCluster(forceRefresh = false) {
|
|
1080
|
+
const clusters = await this.getClusters(forceRefresh);
|
|
1081
|
+
if (clusters.length === 0) return null;
|
|
1082
|
+
return clusters[this.pickRandomIndex(clusters.length)];
|
|
1083
|
+
}
|
|
1084
|
+
/**
|
|
1085
|
+
* Convenience wrapper returning just the gateway URL of a random
|
|
1086
|
+
* active cluster. The single most-used SDK entry point — most
|
|
1087
|
+
* downstream callers want `new SmartEngineClient({ baseUrl: ... })`
|
|
1088
|
+
* and don't care about the rest of the metadata.
|
|
1089
|
+
*/
|
|
1090
|
+
async getRandomGatewayUrl(forceRefresh = false) {
|
|
1091
|
+
const cluster = await this.getRandomCluster(forceRefresh);
|
|
1092
|
+
return cluster?.endpoints.gatewayUrl ?? null;
|
|
1093
|
+
}
|
|
1094
|
+
/** Per-clusterId lookup, useful for "stick the SDK to cluster X" flows. */
|
|
1095
|
+
async getClusterById(clusterId, forceRefresh = false) {
|
|
1096
|
+
const clusters = await this.getClusters(forceRefresh);
|
|
1097
|
+
return clusters.find((c) => c.clusterId === clusterId) ?? null;
|
|
1098
|
+
}
|
|
1099
|
+
clearCache() {
|
|
1100
|
+
this.cache = null;
|
|
1101
|
+
}
|
|
1102
|
+
isCacheValid() {
|
|
1103
|
+
if (!this.cache) return false;
|
|
1104
|
+
return Date.now() - this.cache.lastUpdated < this.cacheTtlMs;
|
|
1105
|
+
}
|
|
1106
|
+
async fetchFromFirstAvailableSeed() {
|
|
1107
|
+
const errors = [];
|
|
1108
|
+
for (const seed of this.bootstrap) {
|
|
1109
|
+
try {
|
|
1110
|
+
return await this.fetchClustersFromSeed(seed);
|
|
1111
|
+
} catch (err) {
|
|
1112
|
+
errors.push(`${seed}: ${err.message}`);
|
|
1113
|
+
}
|
|
1114
|
+
}
|
|
1115
|
+
throw new Error(
|
|
1116
|
+
`ClusterDiscoveryClient: no bootstrap seed reachable. Attempts:
|
|
1117
|
+
${errors.join("\n ")}`
|
|
1118
|
+
);
|
|
1119
|
+
}
|
|
1120
|
+
async fetchClustersFromSeed(seed) {
|
|
1121
|
+
const url = `${seed.replace(/\/$/, "")}/api/v3/discovery/clusters`;
|
|
1122
|
+
const ctrl = new AbortController();
|
|
1123
|
+
const timer = setTimeout(() => ctrl.abort(), this.fetchTimeoutMs);
|
|
1124
|
+
try {
|
|
1125
|
+
const res = await fetch(url, { signal: ctrl.signal });
|
|
1126
|
+
if (!res.ok) {
|
|
1127
|
+
throw new Error(`HTTP ${res.status}`);
|
|
1128
|
+
}
|
|
1129
|
+
const body = await res.json();
|
|
1130
|
+
if (!Array.isArray(body.clusters)) {
|
|
1131
|
+
throw new Error("response missing clusters[]");
|
|
1132
|
+
}
|
|
1133
|
+
return body.clusters.map((c) => this.normalizeClusterEntry(c)).filter((c) => c !== null);
|
|
1134
|
+
} finally {
|
|
1135
|
+
clearTimeout(timer);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
normalizeClusterEntry(raw) {
|
|
1139
|
+
if (typeof raw.clusterId !== "string" || !raw.clusterId) return null;
|
|
1140
|
+
if (!raw.endpoints || typeof raw.endpoints !== "object") return null;
|
|
1141
|
+
const ep = raw.endpoints;
|
|
1142
|
+
if (typeof ep.gatewayUrl !== "string" || !ep.gatewayUrl) return null;
|
|
1143
|
+
if (!this.allowInsecure && !ep.gatewayUrl.startsWith("https://")) {
|
|
1144
|
+
return null;
|
|
1145
|
+
}
|
|
1146
|
+
const nodeIds = Array.isArray(raw.nodeIds) ? raw.nodeIds.filter((n) => typeof n === "string") : [];
|
|
1147
|
+
return {
|
|
1148
|
+
clusterId: raw.clusterId,
|
|
1149
|
+
endpoints: {
|
|
1150
|
+
clusterId: raw.clusterId,
|
|
1151
|
+
gatewayUrl: ep.gatewayUrl,
|
|
1152
|
+
harborUrl: typeof ep.harborUrl === "string" ? ep.harborUrl : void 0,
|
|
1153
|
+
natsUrl: typeof ep.natsUrl === "string" ? ep.natsUrl : void 0,
|
|
1154
|
+
publicIp: typeof ep.publicIp === "string" ? ep.publicIp : void 0,
|
|
1155
|
+
region: typeof ep.region === "string" ? ep.region : void 0
|
|
1156
|
+
},
|
|
1157
|
+
nodeIds
|
|
1158
|
+
};
|
|
1159
|
+
}
|
|
1160
|
+
/**
|
|
1161
|
+
* Cross-check the HTTP-fetched cluster set against the HCS validator
|
|
1162
|
+
* registry. Clusters whose nodeIds are not on-chain are dropped.
|
|
1163
|
+
*
|
|
1164
|
+
* Two failure modes are deliberately silent here:
|
|
1165
|
+
* - the HCS read itself throws → original list is returned
|
|
1166
|
+
* un-verified. Trust-anchor is advisory; a network partition
|
|
1167
|
+
* between SDK and Hedera mirror shouldn't strand the SDK.
|
|
1168
|
+
* - the HCS read returns empty (mirror node lag, wrong topicId)
|
|
1169
|
+
* → original list is returned. Better to keep working off
|
|
1170
|
+
* possibly-stale-but-real data than reject every cluster.
|
|
1171
|
+
*
|
|
1172
|
+
* Both behaviors are why this is called "trust *anchor*", not
|
|
1173
|
+
* "trust gate". Crypto-strong gating is a follow-up.
|
|
1174
|
+
*/
|
|
1175
|
+
async verifyAgainstTrustAnchor(clusters) {
|
|
1176
|
+
if (!this.trustAnchorClient || clusters.length === 0) return clusters;
|
|
1177
|
+
let onChainNodeIds;
|
|
1178
|
+
try {
|
|
1179
|
+
const validators = await this.trustAnchorClient.getValidators();
|
|
1180
|
+
if (validators.length === 0) return clusters;
|
|
1181
|
+
onChainNodeIds = new Set(validators.map((v) => v.nodeId));
|
|
1182
|
+
} catch {
|
|
1183
|
+
return clusters;
|
|
1184
|
+
}
|
|
1185
|
+
return clusters.filter((c) => {
|
|
1186
|
+
if (c.nodeIds.length === 0) {
|
|
1187
|
+
return false;
|
|
1188
|
+
}
|
|
1189
|
+
return c.nodeIds.some((id) => onChainNodeIds.has(id));
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
pickRandomIndex(n) {
|
|
1193
|
+
if (n <= 1) return 0;
|
|
1194
|
+
const g = globalThis.crypto;
|
|
1195
|
+
if (g?.getRandomValues) {
|
|
1196
|
+
const buf = new Uint32Array(1);
|
|
1197
|
+
g.getRandomValues(buf);
|
|
1198
|
+
return buf[0] % n;
|
|
1199
|
+
}
|
|
1200
|
+
return Math.floor(Math.random() * n);
|
|
1201
|
+
}
|
|
1202
|
+
};
|
|
1203
|
+
|
|
1025
1204
|
// src/auth/validator-auth.ts
|
|
1026
1205
|
var SUPPORTED_AUTH_CHAINS = [
|
|
1027
1206
|
"hedera",
|
|
@@ -1354,14 +1533,14 @@ function createHttpClient(config) {
|
|
|
1354
1533
|
throw new SdkHttpError(`Network error: ${err.message}`, 0, error);
|
|
1355
1534
|
}
|
|
1356
1535
|
}
|
|
1357
|
-
async function upload(path, file, filename, metadata) {
|
|
1536
|
+
async function upload(path, file, filename, metadata, fieldName = "file") {
|
|
1358
1537
|
const url = `${config.baseUrl}${path}`;
|
|
1359
1538
|
const controller = new AbortController();
|
|
1360
1539
|
const timeoutId = setTimeout(() => controller.abort(), timeout * 2);
|
|
1361
1540
|
try {
|
|
1362
1541
|
const formData = new FormData();
|
|
1363
1542
|
const blob = file instanceof Blob ? file : new Blob([new Uint8Array(file)]);
|
|
1364
|
-
formData.append(
|
|
1543
|
+
formData.append(fieldName, blob, filename);
|
|
1365
1544
|
if (metadata) {
|
|
1366
1545
|
for (const [key, value] of Object.entries(metadata)) {
|
|
1367
1546
|
formData.append(key, value);
|
|
@@ -1405,7 +1584,7 @@ function createHttpClient(config) {
|
|
|
1405
1584
|
get: (path) => request("GET", path),
|
|
1406
1585
|
put: (path, body) => request("PUT", path, body),
|
|
1407
1586
|
delete: (path) => request("DELETE", path),
|
|
1408
|
-
upload,
|
|
1587
|
+
upload: ((path, file, filename, metadata, fieldName) => upload(path, file, filename, metadata, fieldName)),
|
|
1409
1588
|
setAuthToken
|
|
1410
1589
|
};
|
|
1411
1590
|
return client;
|
|
@@ -1832,6 +2011,167 @@ var SnapshotsClient = class {
|
|
|
1832
2011
|
}
|
|
1833
2012
|
};
|
|
1834
2013
|
|
|
2014
|
+
// src/historical-balance/historical-balance-client.ts
|
|
2015
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
2016
|
+
var ENDPOINT_PATH = "/api/v3/historical-balance/query";
|
|
2017
|
+
var HistoricalBalanceClientError = class extends Error {
|
|
2018
|
+
constructor(message, statusCode, details) {
|
|
2019
|
+
super(message);
|
|
2020
|
+
this.statusCode = statusCode;
|
|
2021
|
+
this.details = details;
|
|
2022
|
+
this.name = "HistoricalBalanceClientError";
|
|
2023
|
+
}
|
|
2024
|
+
statusCode;
|
|
2025
|
+
details;
|
|
2026
|
+
};
|
|
2027
|
+
var HistoricalBalanceClient = class _HistoricalBalanceClient {
|
|
2028
|
+
// Standalone-mode fields (set when the client builds its own fetch).
|
|
2029
|
+
baseUrl;
|
|
2030
|
+
authToken;
|
|
2031
|
+
apiKey;
|
|
2032
|
+
timeoutMs;
|
|
2033
|
+
fetchImpl;
|
|
2034
|
+
// Sub-client-mode field (set when wired via SmartEngineClient).
|
|
2035
|
+
http;
|
|
2036
|
+
constructor(config) {
|
|
2037
|
+
if ("http" in config) {
|
|
2038
|
+
this.http = config.http;
|
|
2039
|
+
this.timeoutMs = DEFAULT_TIMEOUT_MS;
|
|
2040
|
+
return;
|
|
2041
|
+
}
|
|
2042
|
+
if (!config.baseUrl) {
|
|
2043
|
+
throw new HistoricalBalanceClientError(
|
|
2044
|
+
"HistoricalBalanceClient: baseUrl is required for standalone construction",
|
|
2045
|
+
400
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
this.baseUrl = config.baseUrl.replace(/\/+$/, "");
|
|
2049
|
+
this.authToken = config.authToken;
|
|
2050
|
+
this.apiKey = config.apiKey;
|
|
2051
|
+
this.timeoutMs = config.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
2052
|
+
this.fetchImpl = config.fetchImpl;
|
|
2053
|
+
}
|
|
2054
|
+
/**
|
|
2055
|
+
* Factory used by `SmartEngineClient` to wire this sub-client onto the
|
|
2056
|
+
* parent's shared `HttpClient` (which already carries auth + baseUrl + a
|
|
2057
|
+
* `/api/v3` prefix). Routes through `/historical-balance/query` since the
|
|
2058
|
+
* parent's HttpClient is rooted at `/api/v3`.
|
|
2059
|
+
*/
|
|
2060
|
+
static fromHttp(http) {
|
|
2061
|
+
return new _HistoricalBalanceClient({ http });
|
|
2062
|
+
}
|
|
2063
|
+
/**
|
|
2064
|
+
* Query a point-in-time balance.
|
|
2065
|
+
*
|
|
2066
|
+
* The validator returns the on-chain bigint as a base-10 decimal string.
|
|
2067
|
+
* Callers needing arithmetic precision should wrap the result:
|
|
2068
|
+
*
|
|
2069
|
+
* ```ts
|
|
2070
|
+
* const { balance } = await client.historicalBalance.getBalance({ ... });
|
|
2071
|
+
* const value = BigInt(balance);
|
|
2072
|
+
* ```
|
|
2073
|
+
*/
|
|
2074
|
+
async getBalance(params) {
|
|
2075
|
+
this.validateParams(params);
|
|
2076
|
+
if (this.http) {
|
|
2077
|
+
return this.http.post("/historical-balance/query", params);
|
|
2078
|
+
}
|
|
2079
|
+
return this.standaloneFetch(params);
|
|
2080
|
+
}
|
|
2081
|
+
validateParams(params) {
|
|
2082
|
+
if (!params || typeof params !== "object") {
|
|
2083
|
+
throw new HistoricalBalanceClientError(
|
|
2084
|
+
"HistoricalBalanceClient.getBalance: params object is required",
|
|
2085
|
+
400
|
|
2086
|
+
);
|
|
2087
|
+
}
|
|
2088
|
+
const { chain, entityId, account, atTimestamp } = params;
|
|
2089
|
+
if (chain !== "hedera" && chain !== "xrpl" && chain !== "polkadot") {
|
|
2090
|
+
throw new HistoricalBalanceClientError(
|
|
2091
|
+
`HistoricalBalanceClient.getBalance: unsupported chain "${String(chain)}" (expected hedera | xrpl | polkadot)`,
|
|
2092
|
+
400
|
|
2093
|
+
);
|
|
2094
|
+
}
|
|
2095
|
+
if (typeof entityId !== "string" || entityId.length === 0) {
|
|
2096
|
+
throw new HistoricalBalanceClientError(
|
|
2097
|
+
"HistoricalBalanceClient.getBalance: entityId must be a non-empty string",
|
|
2098
|
+
400
|
|
2099
|
+
);
|
|
2100
|
+
}
|
|
2101
|
+
if (typeof account !== "string" || account.length === 0) {
|
|
2102
|
+
throw new HistoricalBalanceClientError(
|
|
2103
|
+
"HistoricalBalanceClient.getBalance: account must be a non-empty string",
|
|
2104
|
+
400
|
|
2105
|
+
);
|
|
2106
|
+
}
|
|
2107
|
+
if (typeof atTimestamp !== "number" || !Number.isInteger(atTimestamp) || atTimestamp <= 0) {
|
|
2108
|
+
throw new HistoricalBalanceClientError(
|
|
2109
|
+
"HistoricalBalanceClient.getBalance: atTimestamp must be a positive integer (unix seconds)",
|
|
2110
|
+
400
|
|
2111
|
+
);
|
|
2112
|
+
}
|
|
2113
|
+
}
|
|
2114
|
+
async standaloneFetch(params) {
|
|
2115
|
+
const url = `${this.baseUrl}${ENDPOINT_PATH}`;
|
|
2116
|
+
const headers = {
|
|
2117
|
+
"Content-Type": "application/json",
|
|
2118
|
+
Accept: "application/json"
|
|
2119
|
+
};
|
|
2120
|
+
if (this.authToken) headers.Authorization = `Bearer ${this.authToken}`;
|
|
2121
|
+
if (this.apiKey) headers["X-API-Key"] = this.apiKey;
|
|
2122
|
+
const fetchFn = this.fetchImpl ?? (typeof fetch !== "undefined" ? fetch : void 0);
|
|
2123
|
+
if (!fetchFn) {
|
|
2124
|
+
throw new HistoricalBalanceClientError(
|
|
2125
|
+
"HistoricalBalanceClient: no fetch implementation available (provide fetchImpl)",
|
|
2126
|
+
500
|
|
2127
|
+
);
|
|
2128
|
+
}
|
|
2129
|
+
const controller = new AbortController();
|
|
2130
|
+
const timer = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
2131
|
+
let response;
|
|
2132
|
+
try {
|
|
2133
|
+
response = await fetchFn(url, {
|
|
2134
|
+
method: "POST",
|
|
2135
|
+
headers,
|
|
2136
|
+
body: JSON.stringify(params),
|
|
2137
|
+
signal: controller.signal
|
|
2138
|
+
});
|
|
2139
|
+
} catch (err) {
|
|
2140
|
+
clearTimeout(timer);
|
|
2141
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2142
|
+
throw new HistoricalBalanceClientError(
|
|
2143
|
+
`HistoricalBalanceClient: transport error: ${message}`,
|
|
2144
|
+
0,
|
|
2145
|
+
err
|
|
2146
|
+
);
|
|
2147
|
+
}
|
|
2148
|
+
clearTimeout(timer);
|
|
2149
|
+
let body;
|
|
2150
|
+
const text = await response.text();
|
|
2151
|
+
if (text.length > 0) {
|
|
2152
|
+
try {
|
|
2153
|
+
body = JSON.parse(text);
|
|
2154
|
+
} catch (err) {
|
|
2155
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2156
|
+
throw new HistoricalBalanceClientError(
|
|
2157
|
+
`HistoricalBalanceClient: non-JSON response (status=${response.status}): ${message}`,
|
|
2158
|
+
response.status,
|
|
2159
|
+
{ raw: text }
|
|
2160
|
+
);
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
if (!response.ok) {
|
|
2164
|
+
const detail = body && typeof body === "object" && "message" in body ? String(body.message) : `HTTP ${response.status}`;
|
|
2165
|
+
throw new HistoricalBalanceClientError(
|
|
2166
|
+
`HistoricalBalanceClient: ${detail}`,
|
|
2167
|
+
response.status,
|
|
2168
|
+
body
|
|
2169
|
+
);
|
|
2170
|
+
}
|
|
2171
|
+
return body;
|
|
2172
|
+
}
|
|
2173
|
+
};
|
|
2174
|
+
|
|
1835
2175
|
// src/settlement/index.ts
|
|
1836
2176
|
var SettlementClient = class {
|
|
1837
2177
|
constructor(http) {
|
|
@@ -1866,6 +2206,53 @@ var SettlementClient = class {
|
|
|
1866
2206
|
}
|
|
1867
2207
|
};
|
|
1868
2208
|
|
|
2209
|
+
// src/governance/governance-client.ts
|
|
2210
|
+
var GovernanceClient = class {
|
|
2211
|
+
constructor(http) {
|
|
2212
|
+
this.http = http;
|
|
2213
|
+
}
|
|
2214
|
+
http;
|
|
2215
|
+
/**
|
|
2216
|
+
* Dry-run a governance proposal/vote/etc. against `GovernanceMolecule.validate(...)`.
|
|
2217
|
+
*
|
|
2218
|
+
* Returns whatever `ValidationResult` the molecule produced. A `false`
|
|
2219
|
+
* `isValid` is **not** an error — it is a successful "this proposal is
|
|
2220
|
+
* invalid" outcome and is surfaced via the normal return value.
|
|
2221
|
+
*
|
|
2222
|
+
* HTTP errors (400 malformed body, 500 unexpected) bubble up as
|
|
2223
|
+
* `SdkHttpError` via the shared `HttpClient` layer.
|
|
2224
|
+
*/
|
|
2225
|
+
async simulate(params) {
|
|
2226
|
+
return this.http.post("/governance/simulate", params);
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
// src/personhood/personhood-client.ts
|
|
2231
|
+
var PersonhoodClient = class {
|
|
2232
|
+
constructor(http) {
|
|
2233
|
+
this.http = http;
|
|
2234
|
+
}
|
|
2235
|
+
http;
|
|
2236
|
+
/**
|
|
2237
|
+
* Verify a personhood proof for `candidate`.
|
|
2238
|
+
*
|
|
2239
|
+
* Returns the issued cert on accept, `null` on clean rejection. All
|
|
2240
|
+
* other failure modes (validation, transport, 5xx) propagate as
|
|
2241
|
+
* `SdkHttpError`.
|
|
2242
|
+
*/
|
|
2243
|
+
async verify(params) {
|
|
2244
|
+
const result = await this.http.post(
|
|
2245
|
+
"/personhood/verify",
|
|
2246
|
+
{
|
|
2247
|
+
candidate: params.candidate,
|
|
2248
|
+
proof: params.proof
|
|
2249
|
+
}
|
|
2250
|
+
);
|
|
2251
|
+
if (result === void 0) return null;
|
|
2252
|
+
return result;
|
|
2253
|
+
}
|
|
2254
|
+
};
|
|
2255
|
+
|
|
1869
2256
|
// src/client.ts
|
|
1870
2257
|
var SmartEngineClient = class _SmartEngineClient {
|
|
1871
2258
|
baseUrl;
|
|
@@ -1886,8 +2273,14 @@ var SmartEngineClient = class _SmartEngineClient {
|
|
|
1886
2273
|
transactions;
|
|
1887
2274
|
/** Token holder snapshot generation and retrieval */
|
|
1888
2275
|
snapshots;
|
|
2276
|
+
/** Historical balance archive reads (chain-native bigint, returned as decimal string) */
|
|
2277
|
+
historicalBalance;
|
|
1889
2278
|
/** Cross-chain settlement operations */
|
|
1890
2279
|
settlement;
|
|
2280
|
+
/** Governance proposal dry-run (simulate-only) */
|
|
2281
|
+
governance;
|
|
2282
|
+
/** Personhood verification (HPP one-human-one-member) */
|
|
2283
|
+
personhood;
|
|
1891
2284
|
constructor(config) {
|
|
1892
2285
|
this.allowInsecure = config.allowInsecure ?? false;
|
|
1893
2286
|
this.baseUrl = validateClientUrl(config.baseUrl, this.allowInsecure);
|
|
@@ -1908,7 +2301,10 @@ var SmartEngineClient = class _SmartEngineClient {
|
|
|
1908
2301
|
this.ipfs = new IPFSClient(this.http);
|
|
1909
2302
|
this.transactions = new TransactionsClient(this.txHttp);
|
|
1910
2303
|
this.snapshots = new SnapshotsClient(this.http);
|
|
2304
|
+
this.historicalBalance = HistoricalBalanceClient.fromHttp(this.http);
|
|
1911
2305
|
this.settlement = new SettlementClient(this.http);
|
|
2306
|
+
this.governance = new GovernanceClient(this.http);
|
|
2307
|
+
this.personhood = new PersonhoodClient(this.http);
|
|
1912
2308
|
}
|
|
1913
2309
|
/**
|
|
1914
2310
|
* Connect to the smart-engines network with auto-discovery and authentication
|
|
@@ -1953,6 +2349,67 @@ var SmartEngineClient = class _SmartEngineClient {
|
|
|
1953
2349
|
});
|
|
1954
2350
|
return { client, validator, session };
|
|
1955
2351
|
}
|
|
2352
|
+
/**
|
|
2353
|
+
* Connect to the smart-engines network via the **service-registry**
|
|
2354
|
+
* (PR-1 of the cluster-discovery arc). Preferred over
|
|
2355
|
+
* {@link connectToNetwork} once the validator pods in the target network
|
|
2356
|
+
* have published their cluster endpoints — the SDK auto-balances across
|
|
2357
|
+
* the active cluster set and rides permissionless cluster join/leave
|
|
2358
|
+
* without code edits.
|
|
2359
|
+
*
|
|
2360
|
+
* Fallback ladder (per `docs/ops/HANDOFF-service-registry-distribution-layer.md` §6):
|
|
2361
|
+
* 1. HTTP fetch `/api/v3/discovery/clusters` from each bootstrap seed.
|
|
2362
|
+
* 2. (Optional) HCS trust-anchor membership cross-check.
|
|
2363
|
+
* 3. Random-pick over the verified set.
|
|
2364
|
+
*
|
|
2365
|
+
* @example
|
|
2366
|
+
* ```ts
|
|
2367
|
+
* const { client, cluster, session } = await SmartEngineClient.connectToCluster({
|
|
2368
|
+
* bootstrap: ['https://sn1.testnet.hsuite.network', 'https://sn2.testnet.hsuite.network'],
|
|
2369
|
+
* chain: 'xrpl',
|
|
2370
|
+
* address: '...',
|
|
2371
|
+
* publicKey: '...',
|
|
2372
|
+
* signFn: async (challenge) => sign(challenge),
|
|
2373
|
+
* });
|
|
2374
|
+
* ```
|
|
2375
|
+
*/
|
|
2376
|
+
static async connectToCluster(config) {
|
|
2377
|
+
const allowInsecure = config.allowInsecure ?? false;
|
|
2378
|
+
const discovery = new ClusterDiscoveryClient({
|
|
2379
|
+
bootstrap: config.bootstrap,
|
|
2380
|
+
allowInsecure,
|
|
2381
|
+
trustAnchor: config.trustAnchor ? {
|
|
2382
|
+
network: config.trustAnchor.network,
|
|
2383
|
+
registryTopicId: config.trustAnchor.registryTopicId,
|
|
2384
|
+
mirrorNodeUrl: config.trustAnchor.mirrorNodeUrl,
|
|
2385
|
+
allowInsecure
|
|
2386
|
+
} : void 0
|
|
2387
|
+
});
|
|
2388
|
+
const cluster = await discovery.getRandomCluster();
|
|
2389
|
+
if (!cluster) {
|
|
2390
|
+
throw new SmartEngineError2(
|
|
2391
|
+
"No active clusters available via bootstrap seeds. Check bootstrap URLs and network reachability.",
|
|
2392
|
+
503
|
|
2393
|
+
);
|
|
2394
|
+
}
|
|
2395
|
+
const gatewayUrl = cluster.endpoints.gatewayUrl;
|
|
2396
|
+
validateClientUrl(gatewayUrl, allowInsecure);
|
|
2397
|
+
const auth = new ValidatorAuthClient({ security: { allowInsecure } });
|
|
2398
|
+
const session = await auth.authenticateWithSigner(
|
|
2399
|
+
gatewayUrl,
|
|
2400
|
+
config.chain,
|
|
2401
|
+
config.address,
|
|
2402
|
+
config.publicKey,
|
|
2403
|
+
config.signFn,
|
|
2404
|
+
config.metadata
|
|
2405
|
+
);
|
|
2406
|
+
const client = new _SmartEngineClient({
|
|
2407
|
+
baseUrl: gatewayUrl,
|
|
2408
|
+
authToken: session.token,
|
|
2409
|
+
allowInsecure
|
|
2410
|
+
});
|
|
2411
|
+
return { client, cluster, session };
|
|
2412
|
+
}
|
|
1956
2413
|
/** Get the current validator URL */
|
|
1957
2414
|
getBaseUrl() {
|
|
1958
2415
|
return this.baseUrl;
|
|
@@ -2730,49 +3187,101 @@ var DeploymentClient = class {
|
|
|
2730
3187
|
}
|
|
2731
3188
|
http;
|
|
2732
3189
|
/**
|
|
2733
|
-
*
|
|
3190
|
+
* Step 1 — allocate appId + receive ephemeral push credentials for
|
|
3191
|
+
* the cluster's per-tenant Harbor project.
|
|
3192
|
+
*
|
|
3193
|
+
* Each app gets its own Harbor project (`hsuite-customers-<appId>`)
|
|
3194
|
+
* isolated by Harbor's RBAC. The push robot returned in
|
|
3195
|
+
* `registry.{username, password}` is scoped to that project only —
|
|
3196
|
+
* it cannot read or write any other tenant's images.
|
|
3197
|
+
*
|
|
3198
|
+
* **Single-use secret discipline:** `registry.password` is returned
|
|
3199
|
+
* exactly once and is NOT persisted server-side. Store it locally
|
|
3200
|
+
* for the `docker push` and discard. To rotate, call `init` again
|
|
3201
|
+
* (issues a new robot under the same project).
|
|
3202
|
+
*
|
|
3203
|
+
* Use the credentials to `docker login` + `docker push`, then call
|
|
3204
|
+
* {@link deploy} with the pushed image tag.
|
|
3205
|
+
*/
|
|
3206
|
+
async init(request) {
|
|
3207
|
+
return this.http.post("/api/deployment/apps/init", request);
|
|
3208
|
+
}
|
|
3209
|
+
/**
|
|
3210
|
+
* Step 3 (optional) — upload the SPA tarball.
|
|
3211
|
+
*
|
|
3212
|
+
* The tarball is content-addressed (SHA-256) and mounted read-only
|
|
3213
|
+
* into the customer's pod alongside the backend container. Returns
|
|
3214
|
+
* the hash + size so the caller can verify the upload.
|
|
3215
|
+
*/
|
|
3216
|
+
async uploadFrontend(appId, bundle, filename = "bundle.tar.gz") {
|
|
3217
|
+
return this.http.upload(
|
|
3218
|
+
`/api/deployment/apps/${encodeURIComponent(appId)}/frontend`,
|
|
3219
|
+
bundle,
|
|
3220
|
+
filename,
|
|
3221
|
+
void 0,
|
|
3222
|
+
"bundle"
|
|
3223
|
+
);
|
|
3224
|
+
}
|
|
3225
|
+
/**
|
|
3226
|
+
* Step 4 — reconcile the runtime to k8s.
|
|
3227
|
+
*
|
|
3228
|
+
* Returns immediately with `status: 'deploying'`. Poll {@link status}
|
|
3229
|
+
* until `runtime.runtimeState === 'RUNNING'` for the URL to be live.
|
|
2734
3230
|
*/
|
|
2735
|
-
async
|
|
2736
|
-
return this.http.post(
|
|
3231
|
+
async deploy(appId, request) {
|
|
3232
|
+
return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/deploy`, request);
|
|
2737
3233
|
}
|
|
2738
3234
|
/**
|
|
2739
|
-
*
|
|
3235
|
+
* Roll back to a previously-deployed image tag (must exist in
|
|
3236
|
+
* `runtime.deploymentHistory[]`).
|
|
3237
|
+
*/
|
|
3238
|
+
async rollback(appId, request) {
|
|
3239
|
+
return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/rollback`, request);
|
|
3240
|
+
}
|
|
3241
|
+
/**
|
|
3242
|
+
* Live combined lifecycle + runtime status of an app.
|
|
3243
|
+
*/
|
|
3244
|
+
async status(appId) {
|
|
3245
|
+
return this.http.get(`/api/deployment/apps/${encodeURIComponent(appId)}/status`);
|
|
3246
|
+
}
|
|
3247
|
+
/**
|
|
3248
|
+
* List all deployed apps for the authenticated developer.
|
|
2740
3249
|
*/
|
|
2741
3250
|
async list() {
|
|
2742
3251
|
return this.http.get("/api/deployment/apps");
|
|
2743
3252
|
}
|
|
2744
3253
|
/**
|
|
2745
|
-
* Get app details
|
|
3254
|
+
* Get app details.
|
|
2746
3255
|
*/
|
|
2747
3256
|
async get(appId) {
|
|
2748
3257
|
return this.http.get(`/api/deployment/apps/${encodeURIComponent(appId)}`);
|
|
2749
3258
|
}
|
|
2750
3259
|
/**
|
|
2751
|
-
* Update app configuration
|
|
3260
|
+
* Update app configuration. Runtime effect lands in PR-H.
|
|
2752
3261
|
*/
|
|
2753
3262
|
async update(appId, updates) {
|
|
2754
3263
|
return this.http.put(`/api/deployment/apps/${encodeURIComponent(appId)}`, updates);
|
|
2755
3264
|
}
|
|
2756
3265
|
/**
|
|
2757
|
-
* Delete an app
|
|
3266
|
+
* Delete an app. Runtime effect (namespace teardown) lands in PR-H.
|
|
2758
3267
|
*/
|
|
2759
3268
|
async delete(appId) {
|
|
2760
3269
|
return this.http.delete(`/api/deployment/apps/${encodeURIComponent(appId)}`);
|
|
2761
3270
|
}
|
|
2762
3271
|
/**
|
|
2763
|
-
* Suspend an app
|
|
3272
|
+
* Suspend an app. Runtime effect (scale to zero) lands in PR-H.
|
|
2764
3273
|
*/
|
|
2765
3274
|
async suspend(appId) {
|
|
2766
3275
|
return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/suspend`, {});
|
|
2767
3276
|
}
|
|
2768
3277
|
/**
|
|
2769
|
-
* Resume a suspended app
|
|
3278
|
+
* Resume a suspended app. Runtime effect (scale back up) lands in PR-H.
|
|
2770
3279
|
*/
|
|
2771
3280
|
async resume(appId) {
|
|
2772
3281
|
return this.http.post(`/api/deployment/apps/${encodeURIComponent(appId)}/resume`, {});
|
|
2773
3282
|
}
|
|
2774
3283
|
/**
|
|
2775
|
-
* Get deployment statistics
|
|
3284
|
+
* Get deployment statistics.
|
|
2776
3285
|
*/
|
|
2777
3286
|
async getStats() {
|
|
2778
3287
|
return this.http.get("/api/deployment/stats");
|
|
@@ -2843,6 +3352,78 @@ var AgentsClient = class {
|
|
|
2843
3352
|
}
|
|
2844
3353
|
};
|
|
2845
3354
|
|
|
3355
|
+
// src/baas/customer-session/index.ts
|
|
3356
|
+
var CustomerSessionClient = class {
|
|
3357
|
+
constructor(baseUrl, timeoutMs = 3e4) {
|
|
3358
|
+
this.baseUrl = baseUrl;
|
|
3359
|
+
this.timeoutMs = timeoutMs;
|
|
3360
|
+
}
|
|
3361
|
+
baseUrl;
|
|
3362
|
+
timeoutMs;
|
|
3363
|
+
/**
|
|
3364
|
+
* Step 1: ask the host to issue a fresh challenge for the customer to sign.
|
|
3365
|
+
*/
|
|
3366
|
+
async challenge(input) {
|
|
3367
|
+
return this.fetch("POST", "/api/customer-session/challenge", input);
|
|
3368
|
+
}
|
|
3369
|
+
/**
|
|
3370
|
+
* Step 2: submit the customer's signed challenge. On success returns a
|
|
3371
|
+
* short-lived bearer JWT scoped to {appId, chain, address}.
|
|
3372
|
+
*/
|
|
3373
|
+
async verify(req) {
|
|
3374
|
+
return this.fetch("POST", "/api/customer-session/verify", req);
|
|
3375
|
+
}
|
|
3376
|
+
/**
|
|
3377
|
+
* Validate a customer bearer + return the decoded session info. Used by
|
|
3378
|
+
* smart-app backends to authorise incoming customer requests.
|
|
3379
|
+
*/
|
|
3380
|
+
async validate(bearer) {
|
|
3381
|
+
return this.fetch("GET", "/api/customer-session/validate", void 0, bearer);
|
|
3382
|
+
}
|
|
3383
|
+
/**
|
|
3384
|
+
* Revoke a customer session. Idempotent.
|
|
3385
|
+
*/
|
|
3386
|
+
async end(bearer) {
|
|
3387
|
+
return this.fetch(
|
|
3388
|
+
"POST",
|
|
3389
|
+
"/api/customer-session/end",
|
|
3390
|
+
void 0,
|
|
3391
|
+
bearer
|
|
3392
|
+
);
|
|
3393
|
+
}
|
|
3394
|
+
async fetch(method, path, body, bearer) {
|
|
3395
|
+
const url = `${this.baseUrl}${path}`;
|
|
3396
|
+
const controller = new AbortController();
|
|
3397
|
+
const timeoutId = setTimeout(() => controller.abort(), this.timeoutMs);
|
|
3398
|
+
try {
|
|
3399
|
+
const headers = { "Content-Type": "application/json" };
|
|
3400
|
+
if (bearer) headers["Authorization"] = `Bearer ${bearer}`;
|
|
3401
|
+
const init = { method, headers, signal: controller.signal };
|
|
3402
|
+
if (body !== void 0) init.body = JSON.stringify(body);
|
|
3403
|
+
const response = await fetch(url, init);
|
|
3404
|
+
clearTimeout(timeoutId);
|
|
3405
|
+
if (!response.ok) {
|
|
3406
|
+
const errBody = await response.json().catch(() => ({}));
|
|
3407
|
+
const err = new Error(
|
|
3408
|
+
errBody.message ?? `customer-session ${path} failed: ${response.status} ${response.statusText}`
|
|
3409
|
+
);
|
|
3410
|
+
err.status = response.status;
|
|
3411
|
+
throw err;
|
|
3412
|
+
}
|
|
3413
|
+
const text = await response.text();
|
|
3414
|
+
if (!text) return void 0;
|
|
3415
|
+
return JSON.parse(text);
|
|
3416
|
+
} catch (error) {
|
|
3417
|
+
clearTimeout(timeoutId);
|
|
3418
|
+
const e = error;
|
|
3419
|
+
if (e.name === "AbortError") {
|
|
3420
|
+
throw new Error(`customer-session ${path} timeout`);
|
|
3421
|
+
}
|
|
3422
|
+
throw error;
|
|
3423
|
+
}
|
|
3424
|
+
}
|
|
3425
|
+
};
|
|
3426
|
+
|
|
2846
3427
|
// src/baas/client.ts
|
|
2847
3428
|
var BaasClient = class {
|
|
2848
3429
|
hostUrl;
|
|
@@ -2867,6 +3448,8 @@ var BaasClient = class {
|
|
|
2867
3448
|
deployment;
|
|
2868
3449
|
/** Autonomous smart agent management */
|
|
2869
3450
|
agents;
|
|
3451
|
+
/** Customer→smart-app session bridge (TokenGate Face B). */
|
|
3452
|
+
customerSession;
|
|
2870
3453
|
constructor(config) {
|
|
2871
3454
|
this.allowInsecure = config.allowInsecure ?? false;
|
|
2872
3455
|
this.hostUrl = validateUrl2(config.hostUrl, this.allowInsecure);
|
|
@@ -2886,6 +3469,7 @@ var BaasClient = class {
|
|
|
2886
3469
|
this.messaging = new MessagingClient(this.http, getAppId);
|
|
2887
3470
|
this.deployment = new DeploymentClient(this.http);
|
|
2888
3471
|
this.agents = new AgentsClient(this.http);
|
|
3472
|
+
this.customerSession = new CustomerSessionClient(baseUrlWithPrefix, this.timeout);
|
|
2889
3473
|
}
|
|
2890
3474
|
/** Set the app ID (for newly registered apps) */
|
|
2891
3475
|
setAppId(appId) {
|
|
@@ -2912,7 +3496,7 @@ var BaasClient = class {
|
|
|
2912
3496
|
requireAppId() {
|
|
2913
3497
|
if (!this.appId) {
|
|
2914
3498
|
throw new BaasError(
|
|
2915
|
-
"App ID required.
|
|
3499
|
+
"App ID required. Provide appId in config (e.g. from a prior deployment.init() call).",
|
|
2916
3500
|
400
|
|
2917
3501
|
);
|
|
2918
3502
|
}
|
|
@@ -2959,12 +3543,6 @@ var BaasClient = class {
|
|
|
2959
3543
|
}
|
|
2960
3544
|
this.authToken = null;
|
|
2961
3545
|
}
|
|
2962
|
-
// ========== App Registration ==========
|
|
2963
|
-
/** Register a new app on the BaaS host */
|
|
2964
|
-
async register(request) {
|
|
2965
|
-
this.requireAuth();
|
|
2966
|
-
return this.post("/api/deployment/apps", request);
|
|
2967
|
-
}
|
|
2968
3546
|
// ========== HTTP Helpers ==========
|
|
2969
3547
|
requireAuth() {
|
|
2970
3548
|
if (!this.authToken) {
|