Skip to content
Open
Show file tree
Hide file tree
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
File renamed without changes.
File renamed without changes.
File renamed without changes.
29 changes: 20 additions & 9 deletions src/services/stellarService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
xdr,
Account,
} from "@stellar/stellar-sdk";
import logger from "../utils/logger";
import stellarProvider from "../lib/stellarProvider";
import { sequenceManager } from "./sequence-manager";
import { assertSigningAllowed } from "../state/appState";
Expand Down Expand Up @@ -81,7 +82,7 @@ export class StellarService {
baseFee,
);

console.info(`✅ Price update for ${currency} confirmed. Hash: ${result.hash}`);
logger.info(`✅ Price update for ${currency} confirmed. Hash: ${result.hash}`);
return result.hash;
}

Expand Down Expand Up @@ -123,7 +124,7 @@ export class StellarService {
);

const currencies = updates.map((u) => u.currency).join(", ");
console.info(`✅ Batched price update for [${currencies}] confirmed. Hash: ${result.hash}`);
logger.info(`✅ Batched price update for [${currencies}] confirmed. Hash: ${result.hash}`);
return result.hash;
}

Expand Down Expand Up @@ -161,7 +162,7 @@ export class StellarService {
baseFee,
);

console.info(`✅ Multi-signed price update for ${currency} confirmed. Hash: ${result.hash}`);
logger.info(`✅ Multi-signed price update for ${currency} confirmed. Hash: ${result.hash}`);
return result.hash;
}

Expand Down Expand Up @@ -207,20 +208,25 @@ export class StellarService {
})
);

return await this.server.submitTransaction(transaction);
const result = await this.server.submitTransaction(transaction);

// Telemetry Sink: Clean point-and-click StellarExpert URL for Testnet
logger.info(`[StellarService] Transaction Broadcast Successful. View on StellarExpert: https://testnet.stellarexpert.org/tx/${result.hash}`);

return result;
} catch (error: any) {
const resultCode = error.response?.data?.extras?.result_codes?.transaction;

if (resultCode === "tx_bad_seq") {
console.warn("⚠️ SequenceManager: tx_bad_seq detected. Invalidating sequence and retrying...");
logger.warn("⚠️ SequenceManager: tx_bad_seq detected. Invalidating sequence and retrying...");
sequenceManager.invalidate(await this.getPublicKey());
}

attempt++;
stellarProvider.reportFailure(error);

if (this.isStuckError(error) && attempt <= maxRetries) {
console.warn(`⚠️ Transaction stuck or fee too low (Attempt ${attempt}). Bumping fee and retrying...`);
logger.warn(`⚠️ Transaction stuck or fee too low (Attempt ${attempt}). Bumping fee and retrying...`);
await new Promise((resolve) => setTimeout(resolve, this.RETRY_DELAY_MS));
continue;
}
Expand Down Expand Up @@ -288,16 +294,21 @@ export class StellarService {

transaction.signatures.push(decoratedSignature);
} catch (error) {
console.error(`[StellarService] Failed to add signature for ${sig.signerPublicKey}:`, error);
logger.error(`[StellarService] Failed to add signature for ${sig.signerPublicKey}:`, error);
}
}

return await this.server.submitTransaction(transaction);
const result = await this.server.submitTransaction(transaction);

// Telemetry Sink: Clean point-and-click StellarExpert URL for Testnet
logger.info(`[StellarService] Transaction Broadcast Successful. View on StellarExpert: https://testnet.stellarexpert.org/tx/${result.hash}`);

return result;
} catch (error: any) {
const resultCode = error.response?.data?.extras?.result_codes?.transaction;

if (resultCode === "tx_bad_seq") {
console.warn("⚠️ SequenceManager: tx_bad_seq detected in multi-sig. Invalidating sequence...");
logger.warn("⚠️ SequenceManager: tx_bad_seq detected in multi-sig. Invalidating sequence...");
sequenceManager.invalidate(await this.getPublicKey());
}

Expand Down
50 changes: 50 additions & 0 deletions test/stellarExpertTelemetry.jest.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { StellarService } from '../src/services/stellarService';
import { TransactionBuilder, Networks, Keypair } from '@stellar/stellar-sdk';
import logger from '../src/utils/logger';
import { jest, describe, it, expect, beforeEach, afterEach } from '@jest/globals';

// Mock the native logger module
jest.mock('../src/utils/logger', () => ({
info: jest.fn(),
warn: jest.fn(),
error: jest.fn(),
}));

describe('StellarService Telemetry', () => {
let stellarService: StellarService;
const mockHash = 'abc123transactionhash';

beforeEach(() => {
stellarService = new StellarService();

// Mock the horizon server response
(stellarService as any).server = {
submitTransaction: jest.fn().mockResolvedValue({
hash: mockHash,
successful: true
})
};

// Mock signer to avoid real cryptography in test
jest.spyOn(stellarService as any, 'getPublicKey').mockResolvedValue(Keypair.random().publicKey());
});

afterEach(() => {
jest.clearAllMocks();
});

it('should log a clean StellarExpert URL at INFO level upon successful broadcast', async () => {
const mockSource = Keypair.random();
const tx = new TransactionBuilder(mockSource, { fee: '100', networkPassphrase: Networks.TESTNET })
.addOperation({} as any)
.setTimeout(30)
.build();

await stellarService.submitTransactionWithRetries(() => tx, 0, 100);

const expectedUrl = `https://testnet.stellarexpert.org/tx/${mockHash}`;
expect(logger.info).toHaveBeenCalledWith(
expect.stringContaining(`[StellarService] Transaction Broadcast Successful. View on StellarExpert: ${expectedUrl}`)
);
});
});