diff --git a/apps/wallets/quickstart-devkit/tests/sdk/helpers/mock-device-storage.ts b/apps/wallets/quickstart-devkit/tests/sdk/helpers/mock-device-storage.ts index d1269136e..66325185d 100644 --- a/apps/wallets/quickstart-devkit/tests/sdk/helpers/mock-device-storage.ts +++ b/apps/wallets/quickstart-devkit/tests/sdk/helpers/mock-device-storage.ts @@ -46,8 +46,10 @@ export class MockDeviceSignerKeyStorage extends DeviceSignerKeyStorage { ? (message as `0x${string}`) : (`0x${Buffer.from(message, "base64").toString("hex")}` as `0x${string}`); - // Sign raw digest without additional hashing (message is a pre-computed hash) - const { r, s } = P256.sign({ payload: messageHex, hash: false, privateKey }); + // The server sends authenticatorData || sha256(clientDataJSON) (69 bytes). + // P256 sign with hash: true so the payload is SHA-256'd before signing, + // matching the on-chain WebAuthn signature verification. + const { r, s } = P256.sign({ payload: messageHex, hash: true, privateKey }); return { r: `0x${r.toString(16).padStart(64, "0")}`, s: `0x${s.toString(16).padStart(64, "0")}`, diff --git a/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/evm-latency-benchmark.spec.ts b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/evm-latency-benchmark.spec.ts new file mode 100644 index 000000000..54c77f060 --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/evm-latency-benchmark.spec.ts @@ -0,0 +1,159 @@ +import { test, expect } from "@playwright/test"; +import { CrossmintWallets, createCrossmint, EVMWallet } from "@crossmint/wallets-sdk"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { + AUTH_CONFIG, + TEST_RECIPIENT_WALLET_ADDRESSES, + validateAPITestConfig, +} from "../../../shared/constants/globalConstants"; +import { MockDeviceSignerKeyStorage } from "../../helpers/mock-device-storage"; + +const API_KEY = AUTH_CONFIG.crossmintApiKey; +const BASE_URL = process.env.CROSSMINT_BASE_URL || "https://preview.crossmint.com"; + +function makeSdk() { + return CrossmintWallets.from( + createCrossmint({ + apiKey: API_KEY, + overrideBaseUrl: BASE_URL, + }) + ); +} + +function makeEvmRecovery() { + const admin = privateKeyToAccount(generatePrivateKey()); + return { + type: "external-wallet" as const, + address: admin.address, + onSign: async (payload: string) => admin.signMessage({ message: { raw: payload as `0x${string}` } }), + }; +} + +type PhaseTimings = { + phase: string; + durationMs: number; +}; + +function logTimingTable(testName: string, timings: PhaseTimings[]) { + const totalMs = timings.reduce((sum, t) => sum + t.durationMs, 0); + console.log(`\n${"=".repeat(70)}`); + console.log(`LATENCY BENCHMARK: ${testName}`); + console.log(`${"=".repeat(70)}`); + console.log(`${"Phase".padEnd(50)} ${"Duration".padStart(10)}`); + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + for (const t of timings) { + console.log(`${t.phase.padEnd(50)} ${`${t.durationMs.toFixed(0)}ms`.padStart(10)}`); + } + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + console.log(`${"TOTAL".padEnd(50)} ${`${totalMs.toFixed(0)}ms`.padStart(10)}`); + console.log(`${"=".repeat(70)}\n`); +} + +// Zero-value call to the recipient address — gas is sponsored via paymaster, +// so the wallet doesn't need any token balance. +const ZERO_VALUE_TX_PARAMS = { + to: TEST_RECIPIENT_WALLET_ADDRESSES.evm, + value: BigInt(0), + data: "0x" as `0x${string}`, +}; + +test.describe("EVM Latency Benchmark — Device Signer", { tag: "@latency" }, () => { + test.beforeAll(() => { + validateAPITestConfig(); + console.log(`\nTarget environment: ${BASE_URL}`); + }); + + for (const { chain, bundlerPath } of [ + { chain: "base-sepolia" as const, bundlerPath: "UltraRelay" }, + { chain: "polygon-amoy" as const, bundlerPath: "Pimlico (Traditional)" }, + ]) { + test.describe(`${chain} (${bundlerPath})`, () => { + let sdk: CrossmintWallets; + let storage: MockDeviceSignerKeyStorage; + + test.beforeEach(() => { + sdk = makeSdk(); + storage = new MockDeviceSignerKeyStorage(API_KEY); + }); + + test(`end-to-end: sendTransaction() — full SDK latency`, async () => { + const timings: PhaseTimings[] = []; + + const t0 = performance.now(); + const deviceDesc = await sdk.createDeviceSigner(storage); + timings.push({ phase: "createDeviceSigner()", durationMs: performance.now() - t0 }); + + const t1 = performance.now(); + const wallet = await sdk.createWallet({ + chain, + recovery: makeEvmRecovery(), + signers: [deviceDesc], + options: { deviceSignerKeyStorage: storage }, + owner: `userId:latency-e2e-${chain}-${Date.now()}`, + }); + timings.push({ phase: "createWallet()", durationMs: performance.now() - t1 }); + + expect(wallet.address).toMatch(/^0x[a-fA-F0-9]{40}$/); + + const evmWallet = EVMWallet.from(wallet); + + // Full flow: create tx → device sign → approve → broadcast → poll + const t2 = performance.now(); + const tx = await evmWallet.sendTransaction(ZERO_VALUE_TX_PARAMS); + const sendMs = performance.now() - t2; + timings.push({ + phase: "sendTransaction() [create+sign+approve+poll]", + durationMs: sendMs, + }); + + expect(tx.hash).toMatch(/^0x[a-fA-F0-9]+$/); + expect(tx.transactionId).toBeTruthy(); + + logTimingTable(`${chain} — sendTransaction() end-to-end`, timings); + }); + + test(`phased: prepareOnly + approve — create vs approve+confirm`, async () => { + const timings: PhaseTimings[] = []; + + const deviceDesc = await sdk.createDeviceSigner(storage); + const wallet = await sdk.createWallet({ + chain, + recovery: makeEvmRecovery(), + signers: [deviceDesc], + options: { deviceSignerKeyStorage: storage }, + owner: `userId:latency-phased-${chain}-${Date.now()}`, + }); + + expect(wallet.address).toMatch(/^0x[a-fA-F0-9]{40}$/); + + const evmWallet = EVMWallet.from(wallet); + + // Phase 1: Create transaction only (server-side assembleTransaction) + const t1 = performance.now(); + const preparedTx = await evmWallet.sendTransaction({ + ...ZERO_VALUE_TX_PARAMS, + options: { prepareOnly: true }, + }); + timings.push({ + phase: "Phase 1: sendTransaction(prepareOnly) [create-tx]", + durationMs: performance.now() - t1, + }); + + const txId = preparedTx.transactionId; + expect(txId).toBeTruthy(); + + // Phase 2+3+4: Approve (device sign + submit) → Execute → Confirm + const t2 = performance.now(); + const result = await wallet.approve({ transactionId: txId as string }); + timings.push({ + phase: "Phase 2+3+4: approve() [sign+submit+confirm]", + durationMs: performance.now() - t2, + }); + + expect(result.hash).toMatch(/^0x[a-fA-F0-9]+$/); + + logTimingTable(`${chain} — phased breakdown`, timings); + }); + }); + } +}); diff --git a/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/solana-latency-benchmark.spec.ts b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/solana-latency-benchmark.spec.ts new file mode 100644 index 000000000..767ba2889 --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/solana-latency-benchmark.spec.ts @@ -0,0 +1,183 @@ +import { test, expect } from "@playwright/test"; +import { CrossmintWallets, createCrossmint, SolanaWallet } from "@crossmint/wallets-sdk"; +import { + Connection, + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; +import { + AUTH_CONFIG, + TEST_RECIPIENT_WALLET_ADDRESSES, + validateAPITestConfig, +} from "../../../shared/constants/globalConstants"; + +const API_KEY = AUTH_CONFIG.crossmintApiKey; +const BASE_URL = process.env.CROSSMINT_BASE_URL || "https://preview.crossmint.com"; +const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL || "https://api.devnet.solana.com"; + +function makeSdk() { + return CrossmintWallets.from( + createCrossmint({ + apiKey: API_KEY, + overrideBaseUrl: BASE_URL, + }) + ); +} + +function makeSolanaRecovery() { + const keypair = Keypair.generate(); + return { + type: "external-wallet" as const, + address: keypair.publicKey.toBase58(), + onSign: async (transaction: VersionedTransaction) => { + transaction.sign([keypair]); + return transaction; + }, + }; +} + +type PhaseTimings = { + phase: string; + durationMs: number; +}; + +function logTimingTable(testName: string, timings: PhaseTimings[]) { + const totalMs = timings.reduce((sum, t) => sum + t.durationMs, 0); + console.log(`\n${"=".repeat(70)}`); + console.log(`LATENCY BENCHMARK: ${testName}`); + console.log(`${"=".repeat(70)}`); + console.log(`${"Phase".padEnd(50)} ${"Duration".padStart(10)}`); + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + for (const t of timings) { + console.log(`${t.phase.padEnd(50)} ${`${t.durationMs.toFixed(0)}ms`.padStart(10)}`); + } + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + console.log(`${"TOTAL".padEnd(50)} ${`${totalMs.toFixed(0)}ms`.padStart(10)}`); + console.log(`${"=".repeat(70)}\n`); +} + +async function buildZeroTransferTransaction( + fromAddress: string, + toAddress: string, + connection: Connection +): Promise { + const from = new PublicKey(fromAddress); + const to = new PublicKey(toAddress); + + const { blockhash } = await connection.getLatestBlockhash("confirmed"); + + const instruction = SystemProgram.transfer({ + fromPubkey: from, + toPubkey: to, + lamports: 0, + }); + + const messageV0 = new TransactionMessage({ + payerKey: from, + recentBlockhash: blockhash, + instructions: [instruction], + }).compileToV0Message(); + + return new VersionedTransaction(messageV0); +} + +test.describe("Solana Latency Benchmark", { tag: "@latency" }, () => { + let sdk: CrossmintWallets; + let connection: Connection; + + test.beforeAll(() => { + validateAPITestConfig(); + console.log(`\nTarget environment: ${BASE_URL}`); + console.log(`Solana RPC: ${SOLANA_RPC_URL}`); + }); + + test.beforeEach(() => { + sdk = makeSdk(); + connection = new Connection(SOLANA_RPC_URL, "confirmed"); + }); + + test(`end-to-end: sendTransaction() — full SDK latency`, async () => { + const timings: PhaseTimings[] = []; + + const t1 = performance.now(); + const wallet = await sdk.createWallet({ + chain: "solana", + recovery: makeSolanaRecovery(), + owner: `userId:latency-solana-e2e-${Date.now()}`, + }); + timings.push({ phase: "createWallet()", durationMs: performance.now() - t1 }); + + expect(wallet.address).toBeTruthy(); + + const solanaWallet = SolanaWallet.from(wallet); + + const t2 = performance.now(); + const transaction = await buildZeroTransferTransaction( + wallet.address, + TEST_RECIPIENT_WALLET_ADDRESSES.solana, + connection + ); + timings.push({ phase: "buildTransaction() [client-side]", durationMs: performance.now() - t2 }); + + const t3 = performance.now(); + const tx = await solanaWallet.sendTransaction({ transaction }); + timings.push({ + phase: "sendTransaction() [create+sign+approve+confirm]", + durationMs: performance.now() - t3, + }); + + expect(tx.hash).toBeTruthy(); + expect(tx.transactionId).toBeTruthy(); + + logTimingTable("Solana — sendTransaction() end-to-end", timings); + }); + + test(`phased: prepareOnly + approve — create vs approve+confirm`, async () => { + const timings: PhaseTimings[] = []; + + const wallet = await sdk.createWallet({ + chain: "solana", + recovery: makeSolanaRecovery(), + owner: `userId:latency-solana-phased-${Date.now()}`, + }); + + expect(wallet.address).toBeTruthy(); + + const solanaWallet = SolanaWallet.from(wallet); + + const transaction = await buildZeroTransferTransaction( + wallet.address, + TEST_RECIPIENT_WALLET_ADDRESSES.solana, + connection + ); + + // Phase 1: Create transaction only (server-side assembleTransaction + prepare) + const t1 = performance.now(); + const preparedTx = await solanaWallet.sendTransaction({ + transaction, + options: { prepareOnly: true }, + }); + timings.push({ + phase: "Phase 1: sendTransaction(prepareOnly) [create-tx]", + durationMs: performance.now() - t1, + }); + + const txId = preparedTx.transactionId; + expect(txId).toBeTruthy(); + + // Phase 2+3: Approve (sign) → Execute → Confirm + const t2 = performance.now(); + const result = await wallet.approve({ transactionId: txId as string }); + timings.push({ + phase: "Phase 2+3: approve() [sign+submit+confirm]", + durationMs: performance.now() - t2, + }); + + expect(result.hash).toBeTruthy(); + + logTimingTable("Solana — phased breakdown", timings); + }); +}); diff --git a/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts new file mode 100644 index 000000000..2961026e4 --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts @@ -0,0 +1,150 @@ +import { test, expect } from "@playwright/test"; +import { CrossmintWallets, createCrossmint, StellarWallet } from "@crossmint/wallets-sdk"; +import { Keypair } from "@stellar/stellar-sdk"; +import { + AUTH_CONFIG, + TEST_RECIPIENT_WALLET_ADDRESSES, + validateAPITestConfig, +} from "../../../shared/constants/globalConstants"; + +const API_KEY = AUTH_CONFIG.crossmintApiKey; +const BASE_URL = process.env.CROSSMINT_BASE_URL || "https://preview.crossmint.com"; + +function makeSdk() { + return CrossmintWallets.from( + createCrossmint({ + apiKey: API_KEY, + overrideBaseUrl: BASE_URL, + }) + ); +} + +function makeStellarRecovery() { + const keypair = Keypair.random(); + return { + type: "external-wallet" as const, + address: keypair.publicKey(), + onSign: async (payload: string) => { + const signature = keypair.sign(Buffer.from(payload, "base64")); + return signature.toString("base64"); + }, + }; +} + +type PhaseTimings = { + phase: string; + durationMs: number; +}; + +function logTimingTable(testName: string, timings: PhaseTimings[]) { + const totalMs = timings.reduce((sum, t) => sum + t.durationMs, 0); + console.log(`\n${"=".repeat(70)}`); + console.log(`LATENCY BENCHMARK: ${testName}`); + console.log(`${"=".repeat(70)}`); + console.log(`${"Phase".padEnd(50)} ${"Duration".padStart(10)}`); + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + for (const t of timings) { + console.log(`${t.phase.padEnd(50)} ${`${t.durationMs.toFixed(0)}ms`.padStart(10)}`); + } + console.log(`${"-".repeat(50)} ${"-".repeat(10)}`); + console.log(`${"TOTAL".padEnd(50)} ${`${totalMs.toFixed(0)}ms`.padStart(10)}`); + console.log(`${"=".repeat(70)}\n`); +} + +// XLM SAC (Stellar Asset Contract) on testnet — the native token wrapper. +const STELLAR_XLM_SAC_TESTNET = "CDLZFC3SYJYDZT7K67VZ75HPJVIEUVNIXF47ZG2FB2RMQQVU2HHGCYSC"; + +test.describe("Stellar Latency Benchmark", { tag: "@latency" }, () => { + let sdk: CrossmintWallets; + + test.beforeAll(() => { + validateAPITestConfig(); + console.log(`\nTarget environment: ${BASE_URL}`); + }); + + test.beforeEach(() => { + sdk = makeSdk(); + }); + + test(`end-to-end: sendTransaction() — full SDK latency`, async () => { + const timings: PhaseTimings[] = []; + + const t1 = performance.now(); + const wallet = await sdk.createWallet({ + chain: "stellar", + recovery: makeStellarRecovery(), + owner: `userId:latency-stellar-e2e-${Date.now()}`, + }); + timings.push({ phase: "createWallet()", durationMs: performance.now() - t1 }); + + expect(wallet.address).toMatch(/^[GC][A-Z2-7]{55}$/); + + const stellarWallet = StellarWallet.from(wallet); + + const t2 = performance.now(); + const tx = await stellarWallet.sendTransaction({ + contractId: STELLAR_XLM_SAC_TESTNET, + method: "transfer", + args: { + from: wallet.address, + to: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + amount: "0", + }, + }); + timings.push({ + phase: "sendTransaction() [create+sign+approve+confirm]", + durationMs: performance.now() - t2, + }); + + expect(tx.hash).toBeTruthy(); + expect(tx.transactionId).toBeTruthy(); + + logTimingTable("Stellar — sendTransaction() end-to-end", timings); + }); + + test(`phased: prepareOnly + approve — create vs approve+confirm`, async () => { + const timings: PhaseTimings[] = []; + + const wallet = await sdk.createWallet({ + chain: "stellar", + recovery: makeStellarRecovery(), + owner: `userId:latency-stellar-phased-${Date.now()}`, + }); + + expect(wallet.address).toMatch(/^[GC][A-Z2-7]{55}$/); + + const stellarWallet = StellarWallet.from(wallet); + + // Phase 1: Create transaction only (server-side assembleTransaction + simulate) + const t1 = performance.now(); + const preparedTx = await stellarWallet.sendTransaction({ + contractId: STELLAR_XLM_SAC_TESTNET, + method: "transfer", + args: { + from: wallet.address, + to: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + amount: "0", + }, + options: { prepareOnly: true }, + }); + timings.push({ + phase: "Phase 1: sendTransaction(prepareOnly) [create-tx]", + durationMs: performance.now() - t1, + }); + + const txId = preparedTx.transactionId; + expect(txId).toBeTruthy(); + + // Phase 2+3: Approve (sign) → Execute → Confirm + const t2 = performance.now(); + const result = await wallet.approve({ transactionId: txId as string }); + timings.push({ + phase: "Phase 2+3: approve() [sign+submit+confirm]", + durationMs: performance.now() - t2, + }); + + expect(result.hash).toBeTruthy(); + + logTimingTable("Stellar — phased breakdown", timings); + }); +}); diff --git a/packages/wallets/src/wallets/stellar.ts b/packages/wallets/src/wallets/stellar.ts index 1c6b8d11d..cebe81f00 100644 --- a/packages/wallets/src/wallets/stellar.ts +++ b/packages/wallets/src/wallets/stellar.ts @@ -60,9 +60,15 @@ export class StellarWallet extends Wallet { params: StellarTransactionInput & { options?: T } ): Promise ? true : false>> { walletsLogger.info("stellarWallet.sendTransaction.start"); + const _sdkTotalStart = performance.now(); await this.preAuthIfNeeded(); + + const _createStart = performance.now(); const createdTransaction = await this.createTransaction(params); + console.log( + `[STELLAR LATENCY] sdk.createTransaction: ${(performance.now() - _createStart).toFixed(0)}ms (txId=${createdTransaction.id})` + ); if (params.options?.prepareOnly) { walletsLogger.info("stellarWallet.sendTransaction.prepared", { @@ -77,7 +83,13 @@ export class StellarWallet extends Wallet { const options: ApproveOptions = {}; + const _approveStart = performance.now(); const result = await this.approveTransactionAndWait(createdTransaction.id, options); + console.log(`[STELLAR LATENCY] sdk.approveAndWait: ${(performance.now() - _approveStart).toFixed(0)}ms`); + console.log( + `[STELLAR LATENCY] sdk.sendTransaction.total: ${(performance.now() - _sdkTotalStart).toFixed(0)}ms` + ); + walletsLogger.info("stellarWallet.sendTransaction.success", { transactionId: createdTransaction.id, hash: result.hash, diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 161b44ea9..e3772e484 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1946,9 +1946,18 @@ export class Wallet { } protected async approveTransactionAndWait(transactionId: string, options?: ApproveOptions) { + const _approveInternalStart = performance.now(); await this.approveTransactionInternal(transactionId, options); + console.log( + `[LATENCY][SDKApprove] approveInternal: ${(performance.now() - _approveInternalStart).toFixed(0)}ms | txId=${transactionId}` + ); await this.sleep(1_000); // Rule of thumb: tx won't be confirmed in less than 1 second - return await this.waitForTransaction(transactionId); + const _waitStart = performance.now(); + const result = await this.waitForTransaction(transactionId); + console.log( + `[LATENCY][SDKApprove] waitForTransaction: ${(performance.now() - _waitStart).toFixed(0)}ms | txId=${transactionId}` + ); + return result; } protected async approveSignatureAndWait(signatureId: string, options?: ApproveOptions) { @@ -2023,7 +2032,11 @@ export class Wallet { } protected async approveTransactionInternal(transactionId: string, options?: ApproveOptions) { + const _getTxStart = performance.now(); const transaction = await this.#apiClient.getTransaction(this.walletLocator, transactionId); + console.log( + `[LATENCY][SDKApprove] getTransaction: ${(performance.now() - _getTxStart).toFixed(0)}ms | txId=${transactionId}` + ); if ("error" in transaction) { throw new TransactionNotAvailableError(JSON.stringify(transaction)); @@ -2053,6 +2066,7 @@ export class Wallet { const signers = [...(options?.additionalSigners ?? []), walletSigner]; + const _signStart = performance.now(); const approvals = await Promise.all( pendingApprovals.map(async (pendingApproval) => { const signer = signers.find((s) => s.locator() === pendingApproval.signer.locator); @@ -2078,8 +2092,16 @@ export class Wallet { }; }) ); + console.log( + `[LATENCY][SDKApprove] sign: ${(performance.now() - _signStart).toFixed(0)}ms | signers=${approvals.length} | txId=${transactionId}` + ); - return await this.executeApproveTransactionWithErrorHandling(transactionId, approvals); + const _submitStart = performance.now(); + const result = await this.executeApproveTransactionWithErrorHandling(transactionId, approvals); + console.log( + `[LATENCY][SDKApprove] submitApproval: ${(performance.now() - _submitStart).toFixed(0)}ms | txId=${transactionId}` + ); + return result; } private async executeApproveTransactionWithErrorHandling(transactionId: string, approvals: Approval[]) { @@ -2148,6 +2170,7 @@ export class Wallet { walletsLogger.info("wallet.approve: waiting for transaction confirmation", { transactionId, timeoutMs }); const startTime = Date.now(); let transactionResponse; + let _pollCount = 0; do { if (Date.now() - startTime > timeoutMs) { @@ -2155,7 +2178,14 @@ export class Wallet { throw error; } + _pollCount++; + const _pollStart = performance.now(); transactionResponse = await this.#apiClient.getTransaction(this.walletLocator, transactionId); + const _pollDuration = (performance.now() - _pollStart).toFixed(0); + const _status = "status" in transactionResponse ? transactionResponse.status : "error"; + console.log( + `[LATENCY][SDKPoll] poll #${_pollCount}: ${_pollDuration}ms | status=${_status} | elapsed=${Date.now() - startTime}ms | txId=${transactionId}` + ); if (transactionResponse.error) { throw new TransactionNotAvailableError(JSON.stringify(transactionResponse)); } @@ -2164,6 +2194,17 @@ export class Wallet { initialBackoffMs = Math.min(initialBackoffMs * backoffMultiplier, maxBackoffMs); } while (transactionResponse.status === "pending"); + const _detectedAt = Date.now(); + const _finalStatus = "status" in transactionResponse ? transactionResponse.status : "error"; + const _completedAt = "completedAt" in transactionResponse ? transactionResponse.completedAt : null; + const _pollingOverheadMs = _completedAt != null ? _detectedAt - new Date(String(_completedAt)).getTime() : null; + console.log( + `[LATENCY][SDKPoll] total: ${_detectedAt - startTime}ms | polls=${_pollCount} | finalStatus=${_finalStatus} | txId=${transactionId}` + ); + console.log( + `[LATENCY][PollingOverhead] txId=${transactionId} | serverCompletedAt=${_completedAt} | sdkDetectedAt=${_detectedAt} | pollingOverheadMs=${_pollingOverheadMs}` + ); + if (transactionResponse.status === "failed") { const error = new TransactionSendingFailedError( `Transaction sending failed: ${JSON.stringify(transactionResponse.error)}`