From 5918835db4eb4745e3e808d96c7133566d3ebff9 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Tue, 9 Jun 2026 22:56:58 +0000 Subject: [PATCH 01/15] tests: add EVM latency benchmark spec with device signer hash fix - Add latency benchmark tests for EVM transactions via SDK with device signers - Test both UltraRelay (base-sepolia) and Traditional/Pimlico (polygon-amoy) paths - Measure end-to-end sendTransaction() latency and phased breakdown (create vs approve+confirm) - Fix MockDeviceSignerKeyStorage to use hash: true matching server WebAuthn expectations - All 4 tests pass against preview.crossmint.com Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../tests/sdk/helpers/mock-device-storage.ts | 6 +- .../latency/evm-latency-benchmark.spec.ts | 154 ++++++++++++++++++ 2 files changed, 158 insertions(+), 2 deletions(-) create mode 100644 apps/wallets/quickstart-devkit/tests/sdk/specs/latency/evm-latency-benchmark.spec.ts 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..3d0665c69 --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/evm-latency-benchmark.spec.ts @@ -0,0 +1,154 @@ +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, + }); + + expect(preparedTx.transactionId).toBeTruthy(); + + // Phase 2+3+4: Approve (device sign + submit) → Execute → Confirm + const t2 = performance.now(); + const result = await wallet.approve({ transactionId: preparedTx.transactionId! }); + 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); + }); + }); + } +}); From b174b5ab859e82c0024e7c3590f64a9f0493aa4a Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Tue, 9 Jun 2026 22:58:34 +0000 Subject: [PATCH 02/15] fix: biome formatting and lint (non-null assertion) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/specs/latency/evm-latency-benchmark.spec.ts | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) 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 index 3d0665c69..54c77f060 100644 --- 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 @@ -1,7 +1,11 @@ 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 { + AUTH_CONFIG, + TEST_RECIPIENT_WALLET_ADDRESSES, + validateAPITestConfig, +} from "../../../shared/constants/globalConstants"; import { MockDeviceSignerKeyStorage } from "../../helpers/mock-device-storage"; const API_KEY = AUTH_CONFIG.crossmintApiKey; @@ -135,11 +139,12 @@ test.describe("EVM Latency Benchmark — Device Signer", { tag: "@latency" }, () durationMs: performance.now() - t1, }); - expect(preparedTx.transactionId).toBeTruthy(); + 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: preparedTx.transactionId! }); + const result = await wallet.approve({ transactionId: txId as string }); timings.push({ phase: "Phase 2+3+4: approve() [sign+submit+confirm]", durationMs: performance.now() - t2, From b6f1c9a231612928d929a1fda9d97bf47d7ec15f Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 06:21:18 +0000 Subject: [PATCH 03/15] feat: add Solana latency benchmark alongside EVM benchmark Adds a Playwright spec that measures end-to-end latency of Solana smart wallet transactions via the SDK, mirroring the EVM benchmark structure: - end-to-end: createWallet + buildTransaction + sendTransaction - phased: prepareOnly (Phase 1) vs approve+confirm (Phase 2+3) Uses a zero-lamport SystemProgram.transfer to avoid needing funded wallets. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../latency/solana-latency-benchmark.spec.ts | 180 ++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 apps/wallets/quickstart-devkit/tests/sdk/specs/latency/solana-latency-benchmark.spec.ts 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..a84cce527 --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/solana-latency-benchmark.spec.ts @@ -0,0 +1,180 @@ +import { test, expect } from "@playwright/test"; +import { CrossmintWallets, createCrossmint, SolanaWallet } from "@crossmint/wallets-sdk"; +import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; +import { + Connection, + 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 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`); +} + +async function buildZeroTransferTransaction( + fromAddress: string, + toAddress: string, + connection: Connection +): Promise { + const from = new PublicKey(fromAddress); + const to = new PublicKey(toAddress); + + const { blockhash, lastValidBlockHeight } = 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: makeEvmRecovery(), + 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: makeEvmRecovery(), + 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); + }); +}); From e9f8d9a7239fcf7157c90969b161cc9f1ccfdac7 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 06:23:06 +0000 Subject: [PATCH 04/15] fix: biome formatting for solana benchmark imports Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/specs/latency/solana-latency-benchmark.spec.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) 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 index a84cce527..e4bee3e7a 100644 --- 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 @@ -1,13 +1,7 @@ import { test, expect } from "@playwright/test"; import { CrossmintWallets, createCrossmint, SolanaWallet } from "@crossmint/wallets-sdk"; import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { - Connection, - PublicKey, - SystemProgram, - TransactionMessage, - VersionedTransaction, -} from "@solana/web3.js"; +import { Connection, PublicKey, SystemProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { AUTH_CONFIG, TEST_RECIPIENT_WALLET_ADDRESSES, From 83e6f5db7a25919b75a195b7dc97e8bd0874f883 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 06:26:29 +0000 Subject: [PATCH 05/15] fix: remove unused lastValidBlockHeight destructuring Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../tests/sdk/specs/latency/solana-latency-benchmark.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index e4bee3e7a..11ad74c15 100644 --- 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 @@ -58,7 +58,7 @@ async function buildZeroTransferTransaction( const from = new PublicKey(fromAddress); const to = new PublicKey(toAddress); - const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash("confirmed"); + const { blockhash } = await connection.getLatestBlockhash("confirmed"); const instruction = SystemProgram.transfer({ fromPubkey: from, From cf687d9e06f3658a078035c248ebdc822574ffc9 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 16:49:18 +0000 Subject: [PATCH 06/15] fix: use Solana Keypair.sign() for recovery signer (not EVM string signature) Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../latency/solana-latency-benchmark.spec.ts | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) 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 index 11ad74c15..b25f633f0 100644 --- 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 @@ -1,7 +1,6 @@ import { test, expect } from "@playwright/test"; import { CrossmintWallets, createCrossmint, SolanaWallet } from "@crossmint/wallets-sdk"; -import { generatePrivateKey, privateKeyToAccount } from "viem/accounts"; -import { Connection, PublicKey, SystemProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; +import { Connection, Keypair, PublicKey, SystemProgram, TransactionMessage, VersionedTransaction } from "@solana/web3.js"; import { AUTH_CONFIG, TEST_RECIPIENT_WALLET_ADDRESSES, @@ -21,12 +20,15 @@ function makeSdk() { ); } -function makeEvmRecovery() { - const admin = privateKeyToAccount(generatePrivateKey()); +function makeSolanaRecovery() { + const keypair = Keypair.generate(); return { type: "external-wallet" as const, - address: admin.address, - onSign: async (payload: string) => admin.signMessage({ message: { raw: payload as `0x${string}` } }), + address: keypair.publicKey.toBase58(), + onSign: async (transaction: VersionedTransaction) => { + transaction.sign([keypair]); + return transaction; + }, }; } @@ -96,7 +98,7 @@ test.describe("Solana Latency Benchmark", { tag: "@latency" }, () => { const t1 = performance.now(); const wallet = await sdk.createWallet({ chain: "solana", - recovery: makeEvmRecovery(), + recovery: makeSolanaRecovery(), owner: `userId:latency-solana-e2e-${Date.now()}`, }); timings.push({ phase: "createWallet()", durationMs: performance.now() - t1 }); @@ -131,7 +133,7 @@ test.describe("Solana Latency Benchmark", { tag: "@latency" }, () => { const wallet = await sdk.createWallet({ chain: "solana", - recovery: makeEvmRecovery(), + recovery: makeSolanaRecovery(), owner: `userId:latency-solana-phased-${Date.now()}`, }); From f11821660041a52222b6a5e7925297e8b577d402 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 16:51:04 +0000 Subject: [PATCH 07/15] fix: biome formatting for solana benchmark imports Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/specs/latency/solana-latency-benchmark.spec.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) 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 index b25f633f0..767ba2889 100644 --- 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 @@ -1,6 +1,13 @@ 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 { + Connection, + Keypair, + PublicKey, + SystemProgram, + TransactionMessage, + VersionedTransaction, +} from "@solana/web3.js"; import { AUTH_CONFIG, TEST_RECIPIENT_WALLET_ADDRESSES, From fa00550a7d07e177f3aba03e6bac741e37b5f19a Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 22:57:18 +0000 Subject: [PATCH 08/15] feat: add stellar-latency-benchmark.spec.ts for TX lifecycle measurement Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../latency/stellar-latency-benchmark.spec.ts | 156 ++++++++++++++++++ 1 file changed, 156 insertions(+) create mode 100644 apps/wallets/quickstart-devkit/tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts 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..7883e804e --- /dev/null +++ b/apps/wallets/quickstart-devkit/tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts @@ -0,0 +1,156 @@ +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 (tx: string) => tx, + }; +} + +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`); +} + +// Stellar native token transfer via contract-call to the recipient address. +// The wallet is gas-sponsored, so no balance is needed. +const ZERO_VALUE_TRANSFER_PARAMS = { + contractId: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + method: "transfer", + args: { + from: "", // Will be set dynamically to wallet address + to: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + amount: "0", + }, +}; + +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: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + 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: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + 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); + }); +}); From 3fee90ebfddf51073e0c142f1d3adf481898600b Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 23:00:56 +0000 Subject: [PATCH 09/15] fix: remove unused ZERO_VALUE_TRANSFER_PARAMS constant Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../specs/latency/stellar-latency-benchmark.spec.ts | 12 ------------ 1 file changed, 12 deletions(-) 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 index 7883e804e..bf53c7091 100644 --- 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 @@ -48,18 +48,6 @@ function logTimingTable(testName: string, timings: PhaseTimings[]) { console.log(`${"=".repeat(70)}\n`); } -// Stellar native token transfer via contract-call to the recipient address. -// The wallet is gas-sponsored, so no balance is needed. -const ZERO_VALUE_TRANSFER_PARAMS = { - contractId: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, - method: "transfer", - args: { - from: "", // Will be set dynamically to wallet address - to: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, - amount: "0", - }, -}; - test.describe("Stellar Latency Benchmark", { tag: "@latency" }, () => { let sdk: CrossmintWallets; From a1836716831da5ace4ff802706907f0f5edf7126 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 23:48:04 +0000 Subject: [PATCH 10/15] fix: use XLM SAC testnet contract address instead of recipient wallet address Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/specs/latency/stellar-latency-benchmark.spec.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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 index bf53c7091..c27793a4d 100644 --- 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 @@ -48,6 +48,9 @@ function logTimingTable(testName: string, timings: PhaseTimings[]) { 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; @@ -77,7 +80,7 @@ test.describe("Stellar Latency Benchmark", { tag: "@latency" }, () => { const t2 = performance.now(); const tx = await stellarWallet.sendTransaction({ - contractId: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + contractId: STELLAR_XLM_SAC_TESTNET, method: "transfer", args: { from: wallet.address, @@ -112,7 +115,7 @@ test.describe("Stellar Latency Benchmark", { tag: "@latency" }, () => { // Phase 1: Create transaction only (server-side assembleTransaction + simulate) const t1 = performance.now(); const preparedTx = await stellarWallet.sendTransaction({ - contractId: TEST_RECIPIENT_WALLET_ADDRESSES.stellar, + contractId: STELLAR_XLM_SAC_TESTNET, method: "transfer", args: { from: wallet.address, From c7bf9d7a03553ea6dbb67ea982a5b70e0e7fe26e Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Thu, 11 Jun 2026 23:53:12 +0000 Subject: [PATCH 11/15] fix: properly sign payload in stellar recovery signer Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../sdk/specs/latency/stellar-latency-benchmark.spec.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) 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 index c27793a4d..9b6cfe499 100644 --- 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 @@ -24,7 +24,10 @@ function makeStellarRecovery() { return { type: "external-wallet" as const, address: keypair.publicKey(), - onSign: async (tx: string) => tx, + onSign: async (payload: string) => { + const signature = keypair.sign(Buffer.from(payload, "base64")); + return Buffer.from(signature).toString("base64"); + }, }; } From 9c7e6dabe04367cd028d1794612f947d395b95f6 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Fri, 12 Jun 2026 00:03:29 +0000 Subject: [PATCH 12/15] fix: remove redundant Buffer.from() wrap on keypair.sign() result Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../tests/sdk/specs/latency/stellar-latency-benchmark.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 index 9b6cfe499..2961026e4 100644 --- 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 @@ -26,7 +26,7 @@ function makeStellarRecovery() { address: keypair.publicKey(), onSign: async (payload: string) => { const signature = keypair.sign(Buffer.from(payload, "base64")); - return Buffer.from(signature).toString("base64"); + return signature.toString("base64"); }, }; } From 32048f6a75c3d2789dfb694e0d37305d75c832bf Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Fri, 12 Jun 2026 00:19:59 +0000 Subject: [PATCH 13/15] feat: add [STELLAR LATENCY] logs to SDK sendTransaction/approve/polling flow Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/wallets/src/wallets/stellar.ts | 12 ++++++++++ packages/wallets/src/wallets/wallet.ts | 29 +++++++++++++++++++++++-- 2 files changed, 39 insertions(+), 2 deletions(-) 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..41dd9af25 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1946,9 +1946,16 @@ export class Wallet { } protected async approveTransactionAndWait(transactionId: string, options?: ApproveOptions) { + const _approveInternalStart = performance.now(); await this.approveTransactionInternal(transactionId, options); + console.log( + `[STELLAR LATENCY] sdk.approveInternal: ${(performance.now() - _approveInternalStart).toFixed(0)}ms` + ); 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(`[STELLAR LATENCY] sdk.waitForTransaction: ${(performance.now() - _waitStart).toFixed(0)}ms`); + return result; } protected async approveSignatureAndWait(signatureId: string, options?: ApproveOptions) { @@ -2023,7 +2030,9 @@ 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(`[STELLAR LATENCY] sdk.approve.getTransaction: ${(performance.now() - _getTxStart).toFixed(0)}ms`); if ("error" in transaction) { throw new TransactionNotAvailableError(JSON.stringify(transaction)); @@ -2053,6 +2062,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 +2088,14 @@ export class Wallet { }; }) ); + console.log( + `[STELLAR LATENCY] sdk.approve.sign: ${(performance.now() - _signStart).toFixed(0)}ms (signers=${approvals.length})` + ); - return await this.executeApproveTransactionWithErrorHandling(transactionId, approvals); + const _submitStart = performance.now(); + const result = await this.executeApproveTransactionWithErrorHandling(transactionId, approvals); + console.log(`[STELLAR LATENCY] sdk.approve.submitApproval: ${(performance.now() - _submitStart).toFixed(0)}ms`); + return result; } private async executeApproveTransactionWithErrorHandling(transactionId: string, approvals: Approval[]) { @@ -2148,6 +2164,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 +2172,12 @@ export class Wallet { throw error; } + _pollCount++; + const _pollStart = performance.now(); transactionResponse = await this.#apiClient.getTransaction(this.walletLocator, transactionId); + console.log( + `[STELLAR LATENCY] sdk.poll #${_pollCount}: ${(performance.now() - _pollStart).toFixed(0)}ms (status=${transactionResponse.status}, elapsed=${Date.now() - startTime}ms)` + ); if (transactionResponse.error) { throw new TransactionNotAvailableError(JSON.stringify(transactionResponse)); } @@ -2163,6 +2185,9 @@ export class Wallet { await this.sleep(initialBackoffMs); initialBackoffMs = Math.min(initialBackoffMs * backoffMultiplier, maxBackoffMs); } while (transactionResponse.status === "pending"); + console.log( + `[STELLAR LATENCY] sdk.poll.total: ${Date.now() - startTime}ms (polls=${_pollCount}, finalStatus=${transactionResponse.status})` + ); if (transactionResponse.status === "failed") { const error = new TransactionSendingFailedError( From 8654d6e08d1364848ff76b2895b0c29205bbd232 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Fri, 12 Jun 2026 00:25:46 +0000 Subject: [PATCH 14/15] fix: resolve TS2339 by narrowing GetTransactionResponse union in debug logs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/wallets/src/wallets/wallet.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 41dd9af25..53daa35f3 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -2176,7 +2176,7 @@ export class Wallet { const _pollStart = performance.now(); transactionResponse = await this.#apiClient.getTransaction(this.walletLocator, transactionId); console.log( - `[STELLAR LATENCY] sdk.poll #${_pollCount}: ${(performance.now() - _pollStart).toFixed(0)}ms (status=${transactionResponse.status}, elapsed=${Date.now() - startTime}ms)` + `[STELLAR LATENCY] sdk.poll #${_pollCount}: ${(performance.now() - _pollStart).toFixed(0)}ms (status=${"status" in transactionResponse ? transactionResponse.status : "error"}, elapsed=${Date.now() - startTime}ms)` ); if (transactionResponse.error) { throw new TransactionNotAvailableError(JSON.stringify(transactionResponse)); @@ -2186,7 +2186,7 @@ export class Wallet { initialBackoffMs = Math.min(initialBackoffMs * backoffMultiplier, maxBackoffMs); } while (transactionResponse.status === "pending"); console.log( - `[STELLAR LATENCY] sdk.poll.total: ${Date.now() - startTime}ms (polls=${_pollCount}, finalStatus=${transactionResponse.status})` + `[STELLAR LATENCY] sdk.poll.total: ${Date.now() - startTime}ms (polls=${_pollCount}, finalStatus=${"status" in transactionResponse ? transactionResponse.status : "error"})` ); if (transactionResponse.status === "failed") { From ec0caadf07e14ca379e882baa27c18263cf05b67 Mon Sep 17 00:00:00 2001 From: "daniil.dovgal" Date: Mon, 15 Jun 2026 07:48:21 +0000 Subject: [PATCH 15/15] feat: add generalized latency logs and polling overhead measurement - Replace [STELLAR LATENCY] with [LATENCY][SDKPoll] and [LATENCY][SDKApprove] for all chains - Add [LATENCY][PollingOverhead] log comparing server completedAt vs SDK detection time - Include txId in all log lines for cross-correlation with server logs Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- packages/wallets/src/wallets/wallet.ts | 30 ++++++++++++++++++++------ 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/packages/wallets/src/wallets/wallet.ts b/packages/wallets/src/wallets/wallet.ts index 53daa35f3..e3772e484 100644 --- a/packages/wallets/src/wallets/wallet.ts +++ b/packages/wallets/src/wallets/wallet.ts @@ -1949,12 +1949,14 @@ export class Wallet { const _approveInternalStart = performance.now(); await this.approveTransactionInternal(transactionId, options); console.log( - `[STELLAR LATENCY] sdk.approveInternal: ${(performance.now() - _approveInternalStart).toFixed(0)}ms` + `[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 const _waitStart = performance.now(); const result = await this.waitForTransaction(transactionId); - console.log(`[STELLAR LATENCY] sdk.waitForTransaction: ${(performance.now() - _waitStart).toFixed(0)}ms`); + console.log( + `[LATENCY][SDKApprove] waitForTransaction: ${(performance.now() - _waitStart).toFixed(0)}ms | txId=${transactionId}` + ); return result; } @@ -2032,7 +2034,9 @@ 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(`[STELLAR LATENCY] sdk.approve.getTransaction: ${(performance.now() - _getTxStart).toFixed(0)}ms`); + console.log( + `[LATENCY][SDKApprove] getTransaction: ${(performance.now() - _getTxStart).toFixed(0)}ms | txId=${transactionId}` + ); if ("error" in transaction) { throw new TransactionNotAvailableError(JSON.stringify(transaction)); @@ -2089,12 +2093,14 @@ export class Wallet { }) ); console.log( - `[STELLAR LATENCY] sdk.approve.sign: ${(performance.now() - _signStart).toFixed(0)}ms (signers=${approvals.length})` + `[LATENCY][SDKApprove] sign: ${(performance.now() - _signStart).toFixed(0)}ms | signers=${approvals.length} | txId=${transactionId}` ); const _submitStart = performance.now(); const result = await this.executeApproveTransactionWithErrorHandling(transactionId, approvals); - console.log(`[STELLAR LATENCY] sdk.approve.submitApproval: ${(performance.now() - _submitStart).toFixed(0)}ms`); + console.log( + `[LATENCY][SDKApprove] submitApproval: ${(performance.now() - _submitStart).toFixed(0)}ms | txId=${transactionId}` + ); return result; } @@ -2175,8 +2181,10 @@ export class Wallet { _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( - `[STELLAR LATENCY] sdk.poll #${_pollCount}: ${(performance.now() - _pollStart).toFixed(0)}ms (status=${"status" in transactionResponse ? transactionResponse.status : "error"}, elapsed=${Date.now() - startTime}ms)` + `[LATENCY][SDKPoll] poll #${_pollCount}: ${_pollDuration}ms | status=${_status} | elapsed=${Date.now() - startTime}ms | txId=${transactionId}` ); if (transactionResponse.error) { throw new TransactionNotAvailableError(JSON.stringify(transactionResponse)); @@ -2185,8 +2193,16 @@ export class Wallet { await this.sleep(initialBackoffMs); 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( - `[STELLAR LATENCY] sdk.poll.total: ${Date.now() - startTime}ms (polls=${_pollCount}, finalStatus=${"status" in transactionResponse ? transactionResponse.status : "error"})` + `[LATENCY][PollingOverhead] txId=${transactionId} | serverCompletedAt=${_completedAt} | sdkDetectedAt=${_detectedAt} | pollingOverheadMs=${_pollingOverheadMs}` ); if (transactionResponse.status === "failed") {