@hashgraphonline/standards-sdk
Version:
The Hashgraph Online Standards SDK provides a complete implementation of the Hashgraph Consensus Standards (HCS), giving developers all the tools needed to build applications on Hedera.
890 lines (889 loc) • 34.2 kB
JavaScript
import { Logger } from "./standards-sdk.es22.js";
class ConnectionsManager {
/**
* Creates a new ConnectionsManager instance
*/
constructor(options) {
this.connections = /* @__PURE__ */ new Map();
this.pendingRequests = /* @__PURE__ */ new Map();
this.profileCache = /* @__PURE__ */ new Map();
this.filterPendingAccountIds = /* @__PURE__ */ new Set();
const loggerOptions = {
module: "ConnectionsManager",
level: options?.logLevel || "info",
prettyPrint: true,
silent: options?.silent
};
this.logger = new Logger(loggerOptions);
if (options?.filterPendingAccountIds) {
this.filterPendingAccountIds = new Set(options.filterPendingAccountIds);
}
if (!options.baseClient) {
throw new Error("ConnectionsManager requires a baseClient to operate");
}
this.baseClient = options.baseClient;
}
/**
* Fetches and processes connection data using the configured client
* @param accountId - The account ID to fetch connection data for
* @returns A promise that resolves to an array of Connection objects
*/
async fetchConnectionData(accountId) {
try {
const topicInfo = await this.baseClient.retrieveCommunicationTopics(accountId);
const isValidTopicId = (topicId) => {
return Boolean(topicId) && !topicId.includes(":");
};
if (!isValidTopicId(topicInfo.inboundTopic) || !isValidTopicId(topicInfo.outboundTopic)) {
this.logger.warn(
"Invalid topic IDs detected in retrieved communication topics"
);
return this.getAllConnections();
}
const [outboundMessagesResult, inboundMessagesResult] = await Promise.all(
[
this.baseClient.getMessages(topicInfo.outboundTopic),
this.baseClient.getMessages(topicInfo.inboundTopic)
]
);
this.processOutboundMessages(
outboundMessagesResult.messages || [],
accountId
);
this.processInboundMessages(inboundMessagesResult.messages || []);
const pendingCount = Array.from(this.connections.values()).filter(
(conn) => conn.status === "pending" || conn.isPending
).length;
this.logger.debug(
`Processed ${outboundMessagesResult.messages?.length || 0} outbound and ${inboundMessagesResult.messages?.length || 0} inbound messages. Found ${pendingCount} pending connections.`
);
await this.checkTargetInboundTopicsForConfirmations();
await this.checkOutboundRequestsForConfirmations();
await this.fetchProfilesForConnections();
await this.fetchConnectionActivity();
return this.getAllConnections();
} catch (error) {
this.logger.error("Error fetching connection data:", error);
return this.getAllConnections();
}
}
/**
* Checks target agent inbound topics to find confirmations for pending requests
* that might not be visible in our local messages
*/
async checkTargetInboundTopicsForConfirmations() {
const pendingConnections = Array.from(this.connections.values()).filter(
(conn) => (conn.isPending || conn.status === "pending") && conn.targetInboundTopicId
);
if (pendingConnections.length === 0) {
return;
}
const pendingRequestsByTarget = /* @__PURE__ */ new Map();
pendingConnections.forEach((conn) => {
if (conn.targetInboundTopicId) {
const requests = pendingRequestsByTarget.get(conn.targetInboundTopicId) || [];
requests.push(conn);
pendingRequestsByTarget.set(conn.targetInboundTopicId, requests);
}
});
const MAX_FETCH_ATTEMPTS = 2;
const FETCH_DELAY_MS = 500;
for (const [
targetInboundTopicId,
requests
] of pendingRequestsByTarget.entries()) {
for (let attempt = 1; attempt <= MAX_FETCH_ATTEMPTS; attempt++) {
try {
const targetMessagesResult = await this.baseClient.getMessages(targetInboundTopicId);
const targetMessages = targetMessagesResult.messages || [];
let confirmedAny = false;
for (const conn of requests) {
const requestId = conn.connectionRequestId;
if (!requestId) {
continue;
}
const confirmationMsg = targetMessages.find((msg) => {
if (msg.op !== "connection_created" || !msg.connection_topic_id) {
return false;
}
if (msg.connection_id !== requestId) {
return false;
}
if (conn.uniqueRequestKey) {
const keyParts = conn.uniqueRequestKey.split(":");
if (keyParts.length > 1) {
const operatorIdPart = keyParts[1];
if (msg.operator_id && msg.operator_id === operatorIdPart) {
return true;
}
if (msg.connected_account_id === conn.targetAccountId) {
return true;
}
}
}
return true;
});
if (confirmationMsg?.connection_topic_id) {
confirmedAny = true;
const connectionTopicId = confirmationMsg.connection_topic_id;
let pendingKey = conn.uniqueRequestKey;
const newConnection = {
connectionTopicId,
targetAccountId: conn.targetAccountId,
targetAgentName: conn.targetAgentName,
targetInboundTopicId: conn.targetInboundTopicId,
status: "established",
isPending: false,
needsConfirmation: false,
created: new Date(confirmationMsg.created || conn.created),
profileInfo: conn.profileInfo,
connectionRequestId: requestId,
uniqueRequestKey: conn.uniqueRequestKey,
originTopicId: conn.originTopicId,
processed: conn.processed,
memo: conn.memo
};
this.connections.set(connectionTopicId, newConnection);
if (pendingKey) {
this.connections.delete(pendingKey);
}
this.logger.debug(
`Confirmed connection in target inbound topic: ${connectionTopicId}`
);
}
}
if (confirmedAny || attempt === MAX_FETCH_ATTEMPTS) {
break;
}
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY_MS));
} catch (error) {
this.logger.debug(
`Error fetching target inbound topic ${targetInboundTopicId}:`,
error
);
if (attempt === MAX_FETCH_ATTEMPTS) {
break;
}
await new Promise((resolve) => setTimeout(resolve, FETCH_DELAY_MS));
}
}
}
}
/**
* Checks target agents' inbound topics for confirmations of our outbound connection requests
* This complements checkTargetInboundTopicsForConfirmations by looking for confirmations
* that might have been sent to the target agent's inbound topic rather than our own
*/
async checkOutboundRequestsForConfirmations() {
const allConnections = Array.from(this.connections.values());
this.logger.info(`Total connections in map: ${allConnections.length}`);
const pendingByStatus = allConnections.filter(
(conn) => conn.status === "pending"
);
this.logger.info(
`Connections with status='pending': ${pendingByStatus.length}`
);
const pendingConnections = allConnections.filter(
(conn) => conn.status === "pending"
);
if (!Boolean(pendingConnections?.length)) {
this.logger.info("No pending connections found");
return;
}
for (const conn of pendingConnections) {
this.logger.debug(
`Processing pending connection: ${conn.connectionTopicId}`
);
if (!conn.targetAccountId) {
this.logger.debug(
`Skipping connection ${conn.connectionTopicId} - no targetAccountId`
);
continue;
}
let targetInboundTopicId = conn.targetInboundTopicId;
if (!targetInboundTopicId) {
try {
const profileResponse = await this.baseClient.retrieveProfile(
conn.targetAccountId
);
if (profileResponse?.profile?.inboundTopicId) {
targetInboundTopicId = profileResponse.profile.inboundTopicId;
this.connections.set(conn.connectionTopicId, {
...conn,
targetInboundTopicId
});
this.logger.debug(
`Updated connection ${conn.connectionTopicId} with inbound topic ID: ${targetInboundTopicId}`
);
} else {
this.logger.debug(
`Couldn't get inbound topic ID for account ${conn.targetAccountId}`
);
continue;
}
} catch (error) {
this.logger.debug(
`Error fetching profile for ${conn.targetAccountId}: ${error}`
);
continue;
}
}
if (!targetInboundTopicId || targetInboundTopicId.includes(":")) {
this.logger.debug(
`Skipping invalid inbound topic format: ${targetInboundTopicId}`
);
continue;
}
const requestId = conn.connectionRequestId || conn.inboundRequestId;
if (!requestId) {
this.logger.debug(
`Skipping connection ${conn.connectionTopicId} - no request ID`
);
continue;
}
try {
this.logger.debug(
`Checking for confirmations on topic ${targetInboundTopicId} for request ID ${requestId}`
);
const targetMessagesResult = await this.baseClient.getMessages(targetInboundTopicId);
const targetMessages = targetMessagesResult.messages || [];
const confirmationMsg = targetMessages.find(
(msg) => msg.op === "connection_created" && msg.connection_id === requestId && msg.connection_topic_id
);
if (confirmationMsg?.connection_topic_id) {
const connectionTopicId = confirmationMsg.connection_topic_id;
this.logger.info(
`Found confirmation for request #${requestId} to ${conn.targetAccountId} on their inbound topic`
);
this.connections.set(conn.connectionTopicId, {
...conn,
connectionTopicId,
status: "established",
isPending: false,
needsConfirmation: false,
created: new Date(confirmationMsg.created || conn.created),
lastActivity: new Date(confirmationMsg.created || conn.created)
});
} else {
this.logger.debug(
`No confirmation found for request ID ${requestId} on topic ${targetInboundTopicId}`
);
}
} catch (error) {
this.logger.warn(
`Error checking for confirmations on target inbound topic for ${conn.targetAccountId}: ${error}`
);
}
}
}
/**
* Fetches profiles for all connected accounts
* @param accountId - The account ID making the request
*/
async fetchProfilesForConnections() {
const targetAccountIds = /* @__PURE__ */ new Set();
for (const connection of this.connections.values()) {
if (connection.targetAccountId && !this.profileCache.has(connection.targetAccountId)) {
targetAccountIds.add(connection.targetAccountId);
}
}
const accountIdPromises = Array.from(targetAccountIds).map(
async (targetId) => {
try {
const profileResponse = await this.baseClient.retrieveProfile(targetId);
if (profileResponse.success && profileResponse.profile) {
this.addProfileInfo(targetId, profileResponse.profile);
this.updatePendingConnectionsWithProfileInfo(
targetId,
profileResponse.profile
);
}
} catch (error) {
this.logger.debug(`Failed to fetch profile for ${targetId}:`, error);
}
}
);
await Promise.allSettled(accountIdPromises);
}
/**
* Updates pending connections with inbound topic IDs from profile info
* @param accountId - The account ID to update connections for
* @param profile - The profile containing the inbound topic ID
*/
updatePendingConnectionsWithProfileInfo(accountId, profile) {
const pendingConnections = Array.from(this.connections.values()).filter(
(conn) => conn.targetAccountId === accountId && (conn.isPending || conn.needsConfirmation) && !conn.targetInboundTopicId
);
if (pendingConnections.length > 0 && profile.inboundTopicId) {
for (const conn of pendingConnections) {
const updatedConn = {
...conn,
targetInboundTopicId: profile.inboundTopicId
};
this.connections.set(conn.connectionTopicId, updatedConn);
}
}
}
/**
* Fetches activity from active connection topics
* Updates the lastActivity timestamp for each connection based on latest messages
* @returns Promise that resolves when all activity has been fetched
*/
async fetchConnectionActivity() {
const activeConnections = this.getActiveConnections();
const validConnections = activeConnections.filter((connection) => {
const topicId = connection.connectionTopicId;
if (!topicId || topicId.includes(":") || !topicId.match(/^0\.0\.\d+$/)) {
this.logger.debug(
`Skipping activity fetch for invalid topic ID format: ${topicId}`
);
return false;
}
return true;
});
const activityPromises = validConnections.map(async (connection) => {
try {
const topicId = connection.connectionTopicId;
const messagesResult = await this.baseClient.getMessages(topicId);
if (messagesResult?.messages?.length > 0) {
this.processConnectionMessages(topicId, messagesResult.messages);
}
} catch (error) {
this.logger.debug(
`Failed to fetch activity for ${connection.connectionTopicId}:`,
error
);
}
});
await Promise.allSettled(activityPromises);
}
/**
* Checks if an account should be filtered, taking into account existing established connections
* @param accountId - The account ID to check
* @returns True if the account should be filtered, false otherwise
*/
shouldFilterAccount(accountId) {
if (!this.filterPendingAccountIds.has(accountId)) {
return false;
}
if (this.hasEstablishedConnectionWithAccount(accountId)) {
return false;
}
return true;
}
/**
* Process outbound messages to track connection requests and confirmations
* @param messages - The messages to process
* @param accountId - The account ID that sent the messages
* @returns Array of connections after processing
*/
processOutboundMessages(messages, accountId) {
if (!Boolean(messages?.length)) {
return Array.from(this.connections.values());
}
const requestMessages = messages.filter(
(msg) => msg.op === "connection_request" && msg.connection_request_id
);
for (const msg of requestMessages) {
const requestId = msg.connection_request_id;
const operatorId = msg.operator_id || "";
const targetAccountId = this.baseClient.extractAccountFromOperatorId(operatorId);
const targetInboundTopicId = this.baseClient.extractTopicFromOperatorId(operatorId);
if (this.shouldFilterAccount(targetAccountId)) {
this.logger.debug(
`Filtering out outbound request to account: ${targetAccountId}`
);
continue;
}
const isAlreadyConfirmed = Array.from(this.connections.values()).some(
(conn) => conn.connectionRequestId === requestId && !conn.isPending && conn.targetAccountId === targetAccountId
);
const pendingKey = `req-${requestId}:${operatorId}`;
if (!isAlreadyConfirmed && !this.pendingRequests.has(pendingKey)) {
const pendingRequest = {
id: requestId,
requesterId: accountId,
requesterTopicId: msg.outbound_topic_id || "",
targetAccountId,
targetTopicId: targetInboundTopicId,
operatorId,
sequenceNumber: msg.sequence_number,
created: msg.created || /* @__PURE__ */ new Date(),
memo: msg.m,
status: "pending"
};
this.pendingRequests.set(pendingKey, pendingRequest);
if (!this.connections.has(pendingKey)) {
const pendingConnection = {
connectionTopicId: pendingKey,
targetAccountId,
targetInboundTopicId,
status: "pending",
isPending: true,
needsConfirmation: false,
created: msg.created || /* @__PURE__ */ new Date(),
connectionRequestId: requestId,
uniqueRequestKey: pendingKey,
originTopicId: msg.outbound_topic_id || "",
processed: false,
memo: msg.m
};
this.connections.set(pendingKey, pendingConnection);
}
}
}
const confirmationMessages = messages.filter(
(msg) => msg.op === "connection_created" && msg.connection_topic_id && msg.connection_request_id
);
for (const msg of confirmationMessages) {
const requestId = msg.connection_request_id;
const connectionTopicId = msg.connection_topic_id;
const targetAccountId = this.baseClient.extractAccountFromOperatorId(
msg.operator_id || ""
);
if (this.shouldFilterAccount(targetAccountId)) {
this.logger.debug(
`Filtering out outbound confirmation to account: ${targetAccountId}`
);
continue;
}
const pendingKey = `req-${requestId}:${msg.operator_id}`;
const pendingRequest = this.pendingRequests.get(pendingKey);
if (pendingRequest) {
pendingRequest.status = "confirmed";
}
if (this.connections.has(pendingKey)) {
this.connections.delete(pendingKey);
}
if (!this.connections.has(connectionTopicId)) {
this.connections.set(connectionTopicId, {
connectionTopicId,
targetAccountId,
status: "established",
isPending: false,
needsConfirmation: false,
created: msg.created || /* @__PURE__ */ new Date(),
connectionRequestId: requestId,
confirmedRequestId: msg.confirmed_request_id,
requesterOutboundTopicId: msg.outbound_topic_id,
uniqueRequestKey: pendingKey,
originTopicId: msg.outbound_topic_id || "",
processed: false,
memo: msg.m
});
} else {
const conn = this.connections.get(connectionTopicId);
this.connections.set(connectionTopicId, {
...conn,
status: "established",
isPending: false,
needsConfirmation: false,
connectionRequestId: requestId,
confirmedRequestId: msg.confirmed_request_id,
requesterOutboundTopicId: msg.outbound_topic_id,
uniqueRequestKey: pendingKey,
originTopicId: msg.outbound_topic_id || "",
processed: false,
memo: msg.m
});
}
}
const closedMessages = messages.filter(
(msg) => msg.op === "connection_closed" || msg.op === "close_connection" && msg.connection_topic_id
);
for (const msg of closedMessages) {
const connectionTopicId = msg.connection_topic_id;
if (this.connections.has(connectionTopicId)) {
const conn = this.connections.get(connectionTopicId);
if (this.shouldFilterAccount(conn.targetAccountId) && conn.status !== "established") {
continue;
}
const uniqueKey = msg.connection_request_id && msg.operator_id ? `req-${msg.connection_request_id}:${msg.operator_id}` : void 0;
this.connections.set(connectionTopicId, {
...conn,
status: "closed",
isPending: false,
needsConfirmation: false,
lastActivity: msg.created || /* @__PURE__ */ new Date(),
closedReason: msg.reason,
closeMethod: msg.close_method,
uniqueRequestKey: uniqueKey,
originTopicId: conn.originTopicId,
processed: false,
memo: msg.m
});
}
}
return Array.from(this.connections.values()).filter(
(conn) => conn.status === "established" || conn.status === "closed" || !this.filterPendingAccountIds.has(conn.targetAccountId)
);
}
/**
* Process inbound messages to track connection requests and confirmations
* @param messages - The messages to process
* @returns Array of connections after processing
*/
processInboundMessages(messages) {
if (!Boolean(messages?.length)) {
return Array.from(this.connections.values());
}
const requestMessages = messages.filter(
(msg) => msg.op === "connection_request" && msg.sequence_number
);
const confirmationMessages = messages.filter(
(msg) => msg.op === "connection_created" && msg.connection_topic_id && msg.connection_id
);
for (const msg of requestMessages) {
const sequenceNumber = msg.sequence_number;
const operatorId = msg.operator_id || "";
const requestorAccountId = this.baseClient.extractAccountFromOperatorId(operatorId);
const requestorTopicId = this.baseClient.extractTopicFromOperatorId(operatorId);
if (this.shouldFilterAccount(requestorAccountId)) {
this.logger.debug(
`Filtering out request from account: ${requestorAccountId}`
);
continue;
}
const needsConfirmKey = `inb-${sequenceNumber}:${operatorId}`;
const hasCreated = confirmationMessages.some(
(m) => m.connection_id === sequenceNumber
);
if (hasCreated) {
this.logger.debug(
`Skipping request from ${requestorAccountId} as it has already been confirmed`
);
continue;
}
if (!this.connections.has(needsConfirmKey)) {
this.connections.set(needsConfirmKey, {
connectionTopicId: needsConfirmKey,
targetAccountId: requestorAccountId,
targetInboundTopicId: requestorTopicId,
status: "needs_confirmation",
isPending: false,
needsConfirmation: true,
created: msg.created || /* @__PURE__ */ new Date(),
inboundRequestId: sequenceNumber,
uniqueRequestKey: needsConfirmKey,
originTopicId: requestorTopicId,
processed: false,
memo: msg.m
});
}
}
for (const msg of confirmationMessages) {
const sequenceNumber = msg.connection_id;
const connectionTopicId = msg.connection_topic_id;
const connectedAccountId = msg.connected_account_id || "";
const operatorId = msg.operator_id || "";
if (this.shouldFilterAccount(connectedAccountId)) {
this.logger.debug(
`Filtering out confirmation for account: ${connectedAccountId}`
);
continue;
}
const needsConfirmKey = `inb-${sequenceNumber}:${operatorId}`;
if (this.connections.has(needsConfirmKey)) {
this.connections.delete(needsConfirmKey);
}
if (!this.connections.has(connectionTopicId)) {
this.connections.set(connectionTopicId, {
connectionTopicId,
targetAccountId: connectedAccountId,
status: "established",
isPending: false,
needsConfirmation: false,
created: msg.created || /* @__PURE__ */ new Date(),
inboundRequestId: sequenceNumber,
uniqueRequestKey: needsConfirmKey,
originTopicId: msg.connection_topic_id,
processed: false,
memo: msg.m
});
} else {
const conn = this.connections.get(connectionTopicId);
this.connections.set(connectionTopicId, {
...conn,
status: "established",
isPending: false,
needsConfirmation: false,
inboundRequestId: sequenceNumber,
uniqueRequestKey: needsConfirmKey,
originTopicId: msg.connection_topic_id,
processed: false,
memo: msg.m
});
}
}
return Array.from(this.connections.values()).filter(
(conn) => conn.status === "established" || conn.status === "closed" || !this.filterPendingAccountIds.has(conn.targetAccountId)
);
}
/**
* Process connection topic messages to update last activity time
* @param connectionTopicId - The topic ID of the connection
* @param messages - The messages to process
* @returns The updated connection or undefined if not found
*/
processConnectionMessages(connectionTopicId, messages) {
if (!messages || messages.length === 0 || !this.connections.has(connectionTopicId)) {
return this.connections.get(connectionTopicId);
}
const latestMessage = messages.filter((m) => m.created).sort((a, b) => {
const dateA = a.created ? new Date(a.created).getTime() : 0;
const dateB = b.created ? new Date(b.created).getTime() : 0;
return dateB - dateA;
})[0];
if (latestMessage?.created) {
const conn = this.connections.get(connectionTopicId);
this.connections.set(connectionTopicId, {
...conn,
lastActivity: latestMessage.created
});
}
const closeMessage = messages.find((msg) => msg.op === "close_connection");
if (closeMessage) {
const conn = this.connections.get(connectionTopicId);
this.connections.set(connectionTopicId, {
...conn,
status: "closed",
lastActivity: closeMessage.created || /* @__PURE__ */ new Date(),
closedReason: closeMessage.reason,
closeMethod: "explicit"
});
}
return this.connections.get(connectionTopicId);
}
/**
* Adds or updates profile information for a connection
* @param accountId - The account ID to add profile info for
* @param profile - The profile information
*/
addProfileInfo(accountId, profile) {
this.profileCache.set(accountId, profile);
const matchingConnections = Array.from(this.connections.values()).filter(
(conn) => conn.targetAccountId === accountId
);
for (const conn of matchingConnections) {
this.connections.set(conn.connectionTopicId, {
...conn,
profileInfo: profile,
targetAgentName: profile.display_name,
targetInboundTopicId: profile.inboundTopicId,
targetOutboundTopicId: profile.outboundTopicId
});
}
}
/**
* Gets all connections
* @returns Array of all connections that should be visible
*/
getAllConnections() {
const connections = Array.from(this.connections.values()).filter(
(conn) => conn.status === "established" || conn.status === "closed" || !this.filterPendingAccountIds.has(conn.targetAccountId)
);
return connections;
}
/**
* Gets all pending connection requests
* @returns Array of pending connection requests
*/
getPendingRequests() {
const pendingConnections = Array.from(this.connections.values()).filter(
(conn) => {
return conn.isPending && !this.filterPendingAccountIds.has(conn.targetAccountId);
}
);
return pendingConnections;
}
/**
* Helper method to check if there's an established connection with an account
* @param accountId - The account ID to check
* @returns True if there's an established connection, false otherwise
*/
hasEstablishedConnectionWithAccount(accountId) {
return Array.from(this.connections.values()).some(
(conn) => conn.targetAccountId === accountId && conn.status === "established"
);
}
/**
* Gets all active (established) connections
* @returns Array of active connections
*/
getActiveConnections() {
return Array.from(this.connections.values()).filter(
(conn) => conn.status === "established"
);
}
/**
* Gets all connections needing confirmation
* @returns Array of connections needing confirmation
*/
getConnectionsNeedingConfirmation() {
return Array.from(this.connections.values()).filter(
(conn) => conn.needsConfirmation && !this.filterPendingAccountIds.has(conn.targetAccountId)
);
}
/**
* Gets a connection by its topic ID
* @param connectionTopicId - The topic ID to look up
* @returns The connection with the given topic ID, or undefined if not found
*/
getConnectionByTopicId(connectionTopicId) {
return this.connections.get(connectionTopicId);
}
/**
* Gets a connection by account ID
* @param accountId - The account ID to look up
* @returns The connection with the given account ID, or undefined if not found
*/
getConnectionByAccountId(accountId) {
return Array.from(this.connections.values()).find(
(conn) => conn.targetAccountId === accountId && conn.status === "established"
);
}
/**
* Gets all connections for a specific account ID
* @param accountId - The account ID to look up
* @returns Array of connections for the given account ID
*/
getConnectionsByAccountId(accountId) {
return Array.from(this.connections.values()).filter(
(conn) => conn.targetAccountId === accountId
);
}
/**
* Updates or adds a connection
* @param connection - The connection to update or add
*/
updateOrAddConnection(connection) {
this.connections.set(connection.connectionTopicId, connection);
}
/**
* Clears all tracked connections and requests
*/
clearAll() {
this.connections.clear();
this.pendingRequests.clear();
}
/**
* Checks if a given connection request has been processed already
* This uses a combination of topic ID and request ID to uniquely identify requests
*
* @param inboundTopicId - The inbound topic ID where the request was received
* @param requestId - The sequence number (request ID)
* @returns True if this specific request has been processed, false otherwise
*/
isConnectionRequestProcessed(inboundTopicId, requestId) {
for (const conn of this.connections.values()) {
if (conn.originTopicId === inboundTopicId && conn.inboundRequestId === requestId && conn.processed) {
return true;
}
if (conn.originTopicId === inboundTopicId && conn.connectionRequestId === requestId && conn.processed) {
return true;
}
}
return false;
}
/**
* Marks a specific connection request as processed
*
* @param inboundTopicId - The inbound topic ID where the request was received
* @param requestId - The sequence number (request ID)
* @returns True if a matching connection was found and marked, false otherwise
*/
markConnectionRequestProcessed(inboundTopicId, requestId) {
let found = false;
for (const [key, conn] of this.connections.entries()) {
if (conn.originTopicId === inboundTopicId && conn.inboundRequestId === requestId) {
this.connections.set(key, {
...conn,
processed: true
});
found = true;
this.logger.debug(
`Marked inbound connection request #${requestId} on topic ${inboundTopicId} as processed`
);
}
if (conn.originTopicId === inboundTopicId && conn.connectionRequestId === requestId) {
this.connections.set(key, {
...conn,
processed: true
});
found = true;
this.logger.debug(
`Marked outbound connection request #${requestId} on topic ${inboundTopicId} as processed`
);
}
}
return found;
}
/**
* Gets pending transactions from a specific connection
* @param connectionTopicId - The connection topic ID to check for transactions
* @param options - Optional filtering and retrieval options
* @returns Array of pending transaction messages sorted by timestamp (newest first)
*/
async getPendingTransactions(connectionTopicId, options) {
try {
const transactMessages = await this.baseClient.getTransactionRequests(
connectionTopicId,
options ? { ...options } : void 0
);
const pendingTransactions = [];
for (const transaction of transactMessages) {
try {
const status = await this.baseClient.mirrorNode.getScheduledTransactionStatus(
transaction.schedule_id
);
if (!status.executed && !status.deleted) {
pendingTransactions.push(transaction);
}
} catch (error) {
this.logger.error(`Error checking transaction status: ${error}`);
pendingTransactions.push(transaction);
}
}
return pendingTransactions;
} catch (error) {
this.logger.error(`Error getting pending transactions: ${error}`);
return [];
}
}
/**
* Gets the status of a scheduled transaction
* @param scheduleId - The schedule ID to check
* @returns Status of the scheduled transaction
*/
getScheduledTransactionStatus(scheduleId) {
return this.baseClient.mirrorNode.getScheduledTransactionStatus(scheduleId);
}
/**
* Gets the timestamp of the last message sent by the specified operator on the connection topic
* @param connectionTopicId - The topic ID to check
* @param operatorAccountId - The account ID of the operator
* @returns The timestamp of the last message or undefined if no messages found
*/
async getLastOperatorActivity(connectionTopicId, operatorAccountId) {
try {
const messages = await this.baseClient.getMessageStream(connectionTopicId);
const filteredMessages = messages.messages.filter(
(msg) => msg.operator_id && msg.operator_id.includes(operatorAccountId) && msg.created
);
if (filteredMessages.length === 0) {
return void 0;
}
filteredMessages.sort(
(a, b) => b.created.getTime() - a.created.getTime()
);
return filteredMessages[0].created;
} catch (error) {
this.logger.error(`Error getting last operator activity: ${error}`);
return void 0;
}
}
}
export {
ConnectionsManager
};
//# sourceMappingURL=standards-sdk.es10.js.map