Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
133 changes: 90 additions & 43 deletions src/rpc/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import {
Contract,
rpc,
xdr,
Transaction,
TransactionBuilder,
Networks,
Account,
Expand All @@ -14,6 +15,53 @@ import {
import { getLogger } from "../logging/index.js";
// CostSummary removed — no longer exported from costs.js

// ── Local helper types to replace `any` casts ──────────────────────────────

/** Error-like shape accessed in catch blocks. */
interface ErrorLike {
code?: string;
message?: string;
response?: { status?: number };
}

/** Soroban meta with optional resource-counter accessors. */
interface SorobanMetaLike {
cpuInstructions?: () => unknown;
memoryBytes?: () => unknown;
}

/** Cost fields present on raw (unparsed) simulation responses. */
interface SimulateTransactionCost {
cpuInsns?: string | number;
memBytes?: string | number;
readBytes?: string | number;
writeBytes?: string | number;
}

/** Simulation success response that may carry raw cost fields. */
interface SimulateWithCost extends rpc.Api.SimulateTransactionSuccessResponse {
cost?: SimulateTransactionCost;
}

/** Send-transaction error response with raw diagnostic fields. */
interface SendTransactionErrorResult {
errorResult?: unknown;
diagnosticEventsXdr?: string;
}

/** Get-transaction response with raw (string-based) fields.
*
* The SDK types parse `resultMetaXdr` as `xdr.TransactionMeta`, but at runtime
* the RPC may still return a base64 string. The `as unknown as
* GetTransactionRawFields` casts in `pollTransaction` handle this mismatch
* intentionally — `extractResourceCosts` expects a raw base64 string. */
interface GetTransactionRawFields {
resultMetaXdr?: string;
feeCharged?: string | number;
ledger?: number;
latestLedger?: number;
}

export function assertSimulationSuccess(sim: rpc.Api.SimulateTransactionResponse): asserts sim is rpc.Api.SimulateTransactionSuccessResponse {
if (rpc.Api.isSimulationError(sim)) {
if (sim.error?.includes("txBadSeq")) {
Expand Down Expand Up @@ -41,9 +89,10 @@ export async function executeWithRetry<T>(action: () => Promise<T>): Promise<T>
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
try {
return await action();
} catch (error: any) {
const isTimeout = error?.code === "ETIMEDOUT" || error?.code === "ECONNRESET" || error?.message?.includes("timeout");
const status = error?.response?.status;
} catch (error: unknown) {
const err = error as ErrorLike;
const isTimeout = err.code === "ETIMEDOUT" || err.code === "ECONNRESET" || err.message?.includes("timeout");
const status = err.response?.status ?? 0;
Comment thread
aymide1ee marked this conversation as resolved.
const isRetryableHttp = status === 429 || (status >= 500 && status < 600);

if ((isTimeout || isRetryableHttp) && attempt < MAX_RETRIES) {
Expand Down Expand Up @@ -201,9 +250,9 @@ export function extractResourceCosts(resultMetaXdrBase64: string): { cpuInstruct
if (v3) {
const sorobanMeta = typeof v3.sorobanMeta === 'function' ? v3.sorobanMeta() : undefined;
if (sorobanMeta) {
const anyMeta = sorobanMeta as any;
const cpuInstructions = typeof anyMeta.cpuInstructions === 'function' ? Number(anyMeta.cpuInstructions()) : undefined;
const memoryBytes = typeof anyMeta.memoryBytes === 'function' ? Number(anyMeta.memoryBytes()) : undefined;
const meta = sorobanMeta as SorobanMetaLike;
const cpuInstructions = typeof meta.cpuInstructions === 'function' ? Number(meta.cpuInstructions()) : undefined;
const memoryBytes = typeof meta.memoryBytes === 'function' ? Number(meta.memoryBytes()) : undefined;

if (cpuInstructions !== undefined && memoryBytes !== undefined) {
return { cpuInstructions, memoryBytes };
Expand Down Expand Up @@ -253,33 +302,31 @@ export class StellarRpcClient {
}

async getCurrentLedger(): Promise<number> {
const serverAny = this.server as any;
if (typeof serverAny.getLatestLedger === "function") {
if (typeof this.server.getLatestLedger === "function") {
try {
const response = await this.withRateLimit(() => serverAny.getLatestLedger());
const seq = (response as any)?.sequence;
const response = await this.withRateLimit(() => this.server.getLatestLedger());
const seq = response?.sequence;
if (typeof seq === "number" && seq > 0) return seq;
} catch (error) {
logger.debug("getLatestLedger failed, falling back to getHealth", error);
}
}

const health = await this.withRateLimit(() => this.server.getHealth());
if (health && typeof (health as any).latestLedger === "number") {
return (health as any).latestLedger;
if (health && typeof health.latestLedger === "number") {
return health.latestLedger;
}

throw new Error("Unable to determine latest ledger from RPC server");
}

async getFeeStats(): Promise<FeeStatsResult> {
return await this.withRateLimit(async () => {
const serverAny = this.server as any;
if (typeof serverAny.getFeeStats !== "function") {
if (typeof this.server.getFeeStats !== "function") {
throw new Error("RPC server does not support getFeeStats");
}

const response = await serverAny.getFeeStats();
const response = await this.server.getFeeStats();
const inclusionFee = response.sorobanInclusionFee ?? response.inclusionFee;
if (!inclusionFee) {
throw new Error("RPC fee stats response did not include inclusion fee data");
Expand Down Expand Up @@ -532,10 +579,10 @@ export class StellarRpcClient {
return {
success: true,
minResourceFee: Number(successSim.minResourceFee ?? 0),
cpuInstructions: Number((successSim as any).cost?.cpuInsns ?? 0),
memoryBytes: Number((successSim as any).cost?.memBytes ?? 0),
readBytes: Number((successSim as any).cost?.readBytes ?? 0),
writeBytes: Number((successSim as any).cost?.writeBytes ?? 0),
cpuInstructions: Number((successSim as SimulateWithCost).cost?.cpuInsns ?? 0),
memoryBytes: Number((successSim as SimulateWithCost).cost?.memBytes ?? 0),
readBytes: Number((successSim as SimulateWithCost).cost?.readBytes ?? 0),
writeBytes: Number((successSim as SimulateWithCost).cost?.writeBytes ?? 0),
};
}

Expand Down Expand Up @@ -627,19 +674,19 @@ export class StellarRpcClient {
retryPrepared.sign(keypair);
const retrySendResult = await this.server.sendTransaction(retryPrepared);
if (retrySendResult.status === "ERROR") {
const diagnostics = (retrySendResult as any).errorResult ?? (retrySendResult as any).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as any).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as any).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
const diagnostics = (retrySendResult as SendTransactionErrorResult).errorResult ?? (retrySendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as SimulateWithCost).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as SimulateWithCost).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
}
const txResult = await this.pollTransaction(retrySendResult.hash);
return txResult.success ? this.addResourcesToSuccess(txResult, retrySim as rpc.Api.SimulateTransactionSuccessResponse) : txResult;
}
const diagnostics = (sendResult as any).errorResult ?? (sendResult as any).diagnosticEventsXdr ?? "";
const diagnostics = (sendResult as SendTransactionErrorResult).errorResult ?? (sendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return {
success: false,
txHash: sendResult.hash,
ledger: 0,
cpuInsns: Number((sim as any).cost?.cpuInsns ?? 0),
memBytes: Number((sim as any).cost?.memBytes ?? 0),
cpuInsns: Number((sim as SimulateWithCost).cost?.cpuInsns ?? 0),
memBytes: Number((sim as SimulateWithCost).cost?.memBytes ?? 0),
error: `Transaction send error: ${diagnostics || sendResult.status}`,
};
}
Expand All @@ -650,7 +697,7 @@ export class StellarRpcClient {

// Helper to add resource usage to a successful transaction result
private addResourcesToSuccess(result: SubmitTransactionResult, sim: rpc.Api.SimulateTransactionSuccessResponse): SubmitTransactionResult {
return { ...result, cpuInsns: Number((sim as any).cost?.cpuInsns ?? 0), memBytes: Number((sim as any).cost?.memBytes ?? 0) };
return { ...result, cpuInsns: Number((sim as SimulateWithCost).cost?.cpuInsns ?? 0), memBytes: Number((sim as SimulateWithCost).cost?.memBytes ?? 0) };
}

/**
Expand Down Expand Up @@ -687,7 +734,7 @@ export class StellarRpcClient {
return { success: false, txHash: "", ledger: 0, cpuInsns: 0, memBytes: 0, error: sim.error ?? "Simulation failed" };
}

const buildAndSignFeeBump = (innerTx: any, simResult: any) => {
const buildAndSignFeeBump = (innerTx: Transaction, simResult: rpc.Api.SimulateTransactionSuccessResponse) => {
const prepared = rpc.assembleTransaction(innerTx, simResult).build();
prepared.sign(keypair);
const feeBump = TransactionBuilder.buildFeeBumpTransaction(
Expand All @@ -714,19 +761,19 @@ export class StellarRpcClient {
const retryFeeBump = buildAndSignFeeBump(retryTx, retrySim);
const retrySendResult = await this.server.sendTransaction(retryFeeBump);
if (retrySendResult.status === "ERROR") {
const diagnostics = (retrySendResult as any).errorResult ?? (retrySendResult as any).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as any).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as any).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
const diagnostics = (retrySendResult as SendTransactionErrorResult).errorResult ?? (retrySendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as SimulateWithCost).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as SimulateWithCost).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
}
const txResult = await this.pollTransaction(retrySendResult.hash);
return txResult.success ? this.addResourcesToSuccess(txResult, retrySim as rpc.Api.SimulateTransactionSuccessResponse) : txResult;
}
const diagnostics = (sendResult as any).errorResult ?? (sendResult as any).diagnosticEventsXdr ?? "";
const diagnostics = (sendResult as SendTransactionErrorResult).errorResult ?? (sendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return {
success: false,
txHash: sendResult.hash,
ledger: 0,
cpuInsns: Number((sim as any).cost?.cpuInsns ?? 0),
memBytes: Number((sim as any).cost?.memBytes ?? 0),
cpuInsns: Number((sim as SimulateWithCost).cost?.cpuInsns ?? 0),
memBytes: Number((sim as SimulateWithCost).cost?.memBytes ?? 0),
error: `Transaction send error: ${diagnostics || sendResult.status}`,
};
}
Expand Down Expand Up @@ -786,19 +833,19 @@ export class StellarRpcClient {
retryPrepared.sign(keypair);
const retrySendResult = await this.server.sendTransaction(retryPrepared);
if (retrySendResult.status === "ERROR") {
const diagnostics = (retrySendResult as any).errorResult ?? (retrySendResult as any).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as any).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as any).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
const diagnostics = (retrySendResult as SendTransactionErrorResult).errorResult ?? (retrySendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return { success: false, txHash: retrySendResult.hash, ledger: 0, cpuInsns: Number((retrySim as SimulateWithCost).cost?.cpuInsns ?? 0), memBytes: Number((retrySim as SimulateWithCost).cost?.memBytes ?? 0), error: `Transaction send error: ${diagnostics || retrySendResult.status}` };
}
const txResult = await this.pollTransaction(retrySendResult.hash);
return txResult.success ? this.addResourcesToSuccess(txResult, retrySim as rpc.Api.SimulateTransactionSuccessResponse) : txResult;
}
const diagnostics = (sendResult as any).errorResult ?? (sendResult as any).diagnosticEventsXdr ?? "";
const diagnostics = (sendResult as SendTransactionErrorResult).errorResult ?? (sendResult as SendTransactionErrorResult).diagnosticEventsXdr ?? "";
return {
success: false,
txHash: sendResult.hash,
ledger: 0,
cpuInsns: Number((sim as any).cost?.cpuInsns ?? 0),
memBytes: Number((sim as any).cost?.memBytes ?? 0),
cpuInsns: Number((sim as SimulateWithCost).cost?.cpuInsns ?? 0),
memBytes: Number((sim as SimulateWithCost).cost?.memBytes ?? 0),
error: `Transaction send error: ${diagnostics || sendResult.status}`,
};
}
Expand Down Expand Up @@ -843,7 +890,7 @@ export class StellarRpcClient {
const sendResult = await this.server.sendTransaction(tx);

if (sendResult.status === "ERROR") {
const diagnostics = (sendResult as any).errorResult ?? "";
const diagnostics = (sendResult as SendTransactionErrorResult).errorResult ?? "";
return {
success: false,
txHash: sendResult.hash,
Expand All @@ -860,14 +907,14 @@ export class StellarRpcClient {
* Returns true if the sendTransaction ERROR response indicates a txBadSeq result code.
* The SDK parses errorResultXdr into `errorResult` as an xdr.TransactionResult.
*/
private isBadSeqError(sendResult: any): boolean {
private isBadSeqError(sendResult: SendTransactionErrorResult): boolean {
try {
const errorResult = sendResult.errorResult;
if (!errorResult) return false;
// errorResult may be a base64 string or a pre-parsed xdr.TransactionResult
const parsed = typeof errorResult === "string"
? xdr.TransactionResult.fromXDR(errorResult, "base64")
: errorResult;
: (errorResult as xdr.TransactionResult);
return parsed.result().switch().name === "txBadSeq";
} catch {
return false;
Expand Down Expand Up @@ -911,7 +958,7 @@ export class StellarRpcClient {
const txResponse = await this.withRateLimit(() => this.server.getTransaction(txHash));

if (txResponse.status === "SUCCESS") {
const resultMetaXdr = (txResponse as any).resultMetaXdr;
const resultMetaXdr = (txResponse as unknown as GetTransactionRawFields).resultMetaXdr;
let cpuInstructions: number | undefined = undefined;
let memoryBytes: number | undefined = undefined;

Expand All @@ -928,13 +975,13 @@ export class StellarRpcClient {
}
}

const rawFee = (txResponse as any).feeCharged;
const rawFee = (txResponse as unknown as GetTransactionRawFields).feeCharged;
const feeCharged = rawFee !== undefined ? Number(rawFee) : undefined;

return {
success: true,
txHash,
ledger: (txResponse as any).ledger ?? txResponse.latestLedger,
ledger: (txResponse as unknown as GetTransactionRawFields).ledger ?? txResponse.latestLedger,
cpuInstructions,
memoryBytes,
feeCharged,
Expand All @@ -945,7 +992,7 @@ export class StellarRpcClient {
return {
success: false,
txHash,
ledger: (txResponse as any).ledger ?? txResponse.latestLedger,
ledger: (txResponse as unknown as GetTransactionRawFields).ledger ?? txResponse.latestLedger,
error: "Transaction failed on-chain",
};
}
Expand Down
Loading