@aptos-labs/cross-chain-core 5.8.2 → 5.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +26 -0
  2. package/dist/CrossChainCore.d.ts +20 -0
  3. package/dist/CrossChainCore.d.ts.map +1 -1
  4. package/dist/index.d.ts +1 -0
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +580 -275
  7. package/dist/index.js.map +1 -1
  8. package/dist/index.mjs +583 -274
  9. package/dist/index.mjs.map +1 -1
  10. package/dist/providers/wormhole/index.d.ts +2 -0
  11. package/dist/providers/wormhole/index.d.ts.map +1 -1
  12. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts +1 -1
  13. package/dist/providers/wormhole/signers/AptosLocalSigner.d.ts.map +1 -1
  14. package/dist/providers/wormhole/signers/Signer.d.ts +1 -1
  15. package/dist/providers/wormhole/signers/Signer.d.ts.map +1 -1
  16. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts +65 -0
  17. package/dist/providers/wormhole/signers/SolanaLocalSigner.d.ts.map +1 -0
  18. package/dist/providers/wormhole/signers/SolanaSigner.d.ts +12 -20
  19. package/dist/providers/wormhole/signers/SolanaSigner.d.ts.map +1 -1
  20. package/dist/providers/wormhole/signers/solanaUtils.d.ts +68 -0
  21. package/dist/providers/wormhole/signers/solanaUtils.d.ts.map +1 -0
  22. package/dist/providers/wormhole/types.d.ts +43 -0
  23. package/dist/providers/wormhole/types.d.ts.map +1 -1
  24. package/dist/providers/wormhole/utils.d.ts +26 -0
  25. package/dist/providers/wormhole/utils.d.ts.map +1 -0
  26. package/dist/providers/wormhole/wormhole.d.ts +36 -6
  27. package/dist/providers/wormhole/wormhole.d.ts.map +1 -1
  28. package/dist/utils/receiptSerialization.d.ts +38 -0
  29. package/dist/utils/receiptSerialization.d.ts.map +1 -0
  30. package/dist/version.d.ts +1 -1
  31. package/package.json +2 -2
  32. package/src/CrossChainCore.ts +20 -0
  33. package/src/config/mainnet/chains.ts +2 -2
  34. package/src/config/testnet/chains.ts +2 -2
  35. package/src/index.ts +1 -0
  36. package/src/providers/wormhole/index.ts +2 -0
  37. package/src/providers/wormhole/signers/AptosLocalSigner.ts +4 -4
  38. package/src/providers/wormhole/signers/AptosSigner.ts +1 -1
  39. package/src/providers/wormhole/signers/EthereumSigner.ts +3 -3
  40. package/src/providers/wormhole/signers/Signer.ts +4 -4
  41. package/src/providers/wormhole/signers/SolanaLocalSigner.ts +243 -0
  42. package/src/providers/wormhole/signers/SolanaSigner.ts +45 -337
  43. package/src/providers/wormhole/signers/solanaUtils.ts +422 -0
  44. package/src/providers/wormhole/types.ts +68 -0
  45. package/src/providers/wormhole/utils.ts +72 -0
  46. package/src/providers/wormhole/wormhole.ts +182 -120
  47. package/src/utils/receiptSerialization.ts +141 -0
  48. package/src/version.ts +1 -1
@@ -0,0 +1,422 @@
1
+ /**
2
+ * Shared utilities for Solana transaction handling.
3
+ * Used by both SolanaLocalSigner (server-side) and SolanaSigner (client-side).
4
+ */
5
+
6
+ import {
7
+ ComputeBudgetProgram,
8
+ Connection,
9
+ LAMPORTS_PER_SOL,
10
+ RpcResponseAndContext,
11
+ SignatureResult,
12
+ SimulatedTransactionResponse,
13
+ Transaction,
14
+ TransactionInstruction,
15
+ VersionedTransaction,
16
+ type Commitment,
17
+ } from "@solana/web3.js";
18
+ import {
19
+ determinePriorityFee,
20
+ determinePriorityFeeTritonOne,
21
+ } from "@wormhole-foundation/sdk-solana";
22
+
23
+ // ============================================================================
24
+ // Types
25
+ // ============================================================================
26
+
27
+ /** Configuration for priority fees */
28
+ export interface PriorityFeeConfig {
29
+ /** Percentile of recent fees to use (default: 0.9) */
30
+ percentile?: number;
31
+ /** Multiplier for the percentile fee (default: 1) */
32
+ percentileMultiple?: number;
33
+ /** Minimum fee in microlamports (default: 100_000) */
34
+ min?: number;
35
+ /** Maximum fee in microlamports (default: 100_000_000) */
36
+ max?: number;
37
+ }
38
+
39
+ /** Configuration for sending and confirming transactions */
40
+ export interface SendAndConfirmConfig {
41
+ connection: Connection;
42
+ commitment: Commitment;
43
+ retryIntervalMs?: number;
44
+ /** Enable verbose logging (default: false) */
45
+ verbose?: boolean;
46
+ }
47
+
48
+ /** RPC provider type for priority fee calculation */
49
+ export type SolanaRpcProvider = "triton" | "helius" | "ankr" | "unknown";
50
+
51
+ // ============================================================================
52
+ // Transaction Confirmation
53
+ // ============================================================================
54
+
55
+ /**
56
+ * Sends a serialized transaction and waits for confirmation with automatic retries.
57
+ *
58
+ * @param serializedTx - The serialized transaction bytes
59
+ * @param blockhash - The recent blockhash used in the transaction
60
+ * @param lastValidBlockHeight - The last valid block height for the transaction
61
+ * @param config - Configuration for sending and confirming
62
+ * @returns The transaction signature
63
+ */
64
+ export async function sendAndConfirmTransaction(
65
+ serializedTx: Buffer | Uint8Array,
66
+ blockhash: string,
67
+ lastValidBlockHeight: number,
68
+ config: SendAndConfirmConfig,
69
+ ): Promise<string> {
70
+ const { connection, commitment, retryIntervalMs = 5000, verbose = false } = config;
71
+
72
+ const sendOptions = {
73
+ skipPreflight: true,
74
+ maxRetries: 0,
75
+ preflightCommitment: commitment,
76
+ };
77
+
78
+ const signature = await connection.sendRawTransaction(serializedTx, sendOptions);
79
+
80
+ const confirmTransactionPromise = connection.confirmTransaction(
81
+ { signature, blockhash, lastValidBlockHeight },
82
+ commitment,
83
+ );
84
+
85
+ // Retry loop: resend if not confirmed after interval
86
+ let confirmedTx: RpcResponseAndContext<SignatureResult> | null = null;
87
+ let txSendAttempts = 1;
88
+
89
+ while (!confirmedTx) {
90
+ confirmedTx = await Promise.race([
91
+ confirmTransactionPromise,
92
+ new Promise<null>((resolve) =>
93
+ setTimeout(() => resolve(null), retryIntervalMs),
94
+ ),
95
+ ]);
96
+
97
+ if (confirmedTx) break;
98
+
99
+ if (verbose) {
100
+ console.log(
101
+ `Tx not confirmed after ${retryIntervalMs * txSendAttempts++}ms, resending`,
102
+ );
103
+ }
104
+
105
+ try {
106
+ await connection.sendRawTransaction(serializedTx, sendOptions);
107
+ } catch (e) {
108
+ if (verbose) {
109
+ console.error("Failed to resend transaction:", e);
110
+ }
111
+ // Ignore resend errors, confirmation will handle success/failure
112
+ }
113
+ }
114
+
115
+ if (confirmedTx.value.err) {
116
+ const errorMessage = formatTransactionError(confirmedTx.value.err);
117
+ throw new Error(errorMessage);
118
+ }
119
+
120
+ return signature;
121
+ }
122
+
123
+ /**
124
+ * Formats a transaction error into a readable string.
125
+ */
126
+ export function formatTransactionError(err: unknown): string {
127
+ if (typeof err === "object" && err !== null) {
128
+ try {
129
+ return `Transaction failed: ${JSON.stringify(
130
+ err,
131
+ (_key, value) => (typeof value === "bigint" ? value.toString() : value),
132
+ )}`;
133
+ } catch {
134
+ // Circular reference or other stringify error
135
+ return "Transaction failed: Unknown error";
136
+ }
137
+ }
138
+ return `Transaction failed: ${err}`;
139
+ }
140
+
141
+ // ============================================================================
142
+ // Priority Fees
143
+ // ============================================================================
144
+
145
+ /**
146
+ * Adds priority fee instructions to a transaction.
147
+ * Simulates the transaction to determine compute units and calculates optimal fees.
148
+ *
149
+ * @param connection - Solana RPC connection
150
+ * @param transaction - The transaction to add priority fees to
151
+ * @param priorityFeeConfig - Configuration for priority fees
152
+ * @param verbose - Enable verbose logging
153
+ * @returns The transaction with priority fee instructions added
154
+ */
155
+ export async function addPriorityFeeInstructions(
156
+ connection: Connection,
157
+ transaction: Transaction,
158
+ priorityFeeConfig?: PriorityFeeConfig,
159
+ verbose: boolean = false,
160
+ ): Promise<Transaction> {
161
+ const computeBudgetIxFilter = (ix: TransactionInstruction) =>
162
+ ix.programId.toString() !== "ComputeBudget111111111111111111111111111111";
163
+
164
+ // Remove existing compute budget instructions if they were added by the SDK
165
+ transaction.instructions = transaction.instructions.filter(computeBudgetIxFilter);
166
+
167
+ const instructions = await createPriorityFeeInstructions(
168
+ connection,
169
+ transaction,
170
+ priorityFeeConfig,
171
+ verbose,
172
+ );
173
+
174
+ transaction.add(...instructions);
175
+
176
+ return transaction;
177
+ }
178
+
179
+ /**
180
+ * Creates priority fee instructions based on simulation and fee estimation.
181
+ */
182
+ export async function createPriorityFeeInstructions(
183
+ connection: Connection,
184
+ transaction: Transaction | VersionedTransaction,
185
+ priorityFeeConfig?: PriorityFeeConfig,
186
+ verbose: boolean = false,
187
+ ): Promise<TransactionInstruction[]> {
188
+ // Simulate to get compute units
189
+ const unitsUsed = await simulateAndGetComputeUnits(connection, transaction);
190
+ const unitBudget = Math.floor(unitsUsed * 1.2); // Budget in 20% headroom
191
+
192
+ const instructions: TransactionInstruction[] = [];
193
+ instructions.push(
194
+ ComputeBudgetProgram.setComputeUnitLimit({
195
+ units: unitBudget,
196
+ }),
197
+ );
198
+
199
+ // Calculate priority fee
200
+ const {
201
+ percentile = 0.9,
202
+ percentileMultiple = 1,
203
+ min = 100_000,
204
+ max = 100_000_000,
205
+ } = priorityFeeConfig ?? {};
206
+
207
+ const rpcProvider = determineRpcProvider(connection.rpcEndpoint);
208
+ const { fee, methodUsed } = await calculatePriorityFee(
209
+ connection,
210
+ transaction,
211
+ rpcProvider,
212
+ { percentile, percentileMultiple, min, max },
213
+ );
214
+
215
+ if (verbose) {
216
+ const maxFeeInSol = (fee / 1e6 / LAMPORTS_PER_SOL) * unitBudget;
217
+ console.table({
218
+ "RPC Provider": rpcProvider,
219
+ "Method used": methodUsed,
220
+ "Percentile used": percentile,
221
+ "Multiple used": percentileMultiple,
222
+ "Compute budget": unitBudget,
223
+ "Priority fee": fee,
224
+ "Max fee in SOL": maxFeeInSol,
225
+ });
226
+ }
227
+
228
+ instructions.push(
229
+ ComputeBudgetProgram.setComputeUnitPrice({ microLamports: fee }),
230
+ );
231
+
232
+ return instructions;
233
+ }
234
+
235
+ /**
236
+ * Simulates a transaction and returns the compute units consumed.
237
+ */
238
+ async function simulateAndGetComputeUnits(
239
+ connection: Connection,
240
+ transaction: Transaction | VersionedTransaction,
241
+ ): Promise<number> {
242
+ let unitsUsed = 200_000;
243
+ let simulationAttempts = 0;
244
+
245
+ simulationLoop: while (true) {
246
+ const response = await connection.simulateTransaction(transaction as Transaction);
247
+
248
+ if (response.value.err) {
249
+ if (checkKnownSimulationError(response.value)) {
250
+ // Number of attempts will be at most 5 for known errors
251
+ if (simulationAttempts < 5) {
252
+ simulationAttempts++;
253
+ await sleep(1000);
254
+ continue simulationLoop;
255
+ }
256
+ } else if (simulationAttempts < 3) {
257
+ // Number of attempts will be at most 3 for unknown errors
258
+ simulationAttempts++;
259
+ await sleep(1000);
260
+ continue simulationLoop;
261
+ }
262
+
263
+ // Still failing after multiple attempts
264
+ throw new Error(
265
+ `Simulation failed: ${JSON.stringify(response.value.err)}\nLogs:\n${(
266
+ response.value.logs || []
267
+ ).join("\n ")}`,
268
+ );
269
+ } else {
270
+ // Simulation was successful
271
+ if (response.value.unitsConsumed) {
272
+ unitsUsed = response.value.unitsConsumed;
273
+ }
274
+ break;
275
+ }
276
+ }
277
+
278
+ return unitsUsed;
279
+ }
280
+
281
+ /**
282
+ * Calculates the priority fee based on RPC provider and configuration.
283
+ */
284
+ async function calculatePriorityFee(
285
+ connection: Connection,
286
+ transaction: Transaction | VersionedTransaction,
287
+ rpcProvider: SolanaRpcProvider,
288
+ config: Required<PriorityFeeConfig>,
289
+ ): Promise<{ fee: number; methodUsed: "triton" | "default" | "minimum" }> {
290
+ const { percentile, percentileMultiple, min, max } = config;
291
+
292
+ if (rpcProvider === "triton") {
293
+ // Triton has an experimental RPC method that accepts a percentile parameter
294
+ // and usually gives more accurate fee numbers.
295
+ try {
296
+ const fee = await determinePriorityFeeTritonOne(
297
+ connection,
298
+ transaction,
299
+ percentile,
300
+ percentileMultiple,
301
+ min,
302
+ max,
303
+ );
304
+
305
+ return { fee, methodUsed: "triton" };
306
+ } catch (e) {
307
+ console.warn(`Failed to determine priority fee using Triton RPC:`, e);
308
+ }
309
+ }
310
+
311
+ try {
312
+ // By default, use generic Solana RPC method
313
+ const fee = await determinePriorityFee(
314
+ connection,
315
+ transaction,
316
+ percentile,
317
+ percentileMultiple,
318
+ min,
319
+ max,
320
+ );
321
+
322
+ return { fee, methodUsed: "default" };
323
+ } catch (e) {
324
+ console.warn(`Failed to determine priority fee:`, e);
325
+
326
+ return { fee: min, methodUsed: "minimum" };
327
+ }
328
+ }
329
+
330
+ // ============================================================================
331
+ // Helpers
332
+ // ============================================================================
333
+
334
+ /**
335
+ * Checks response logs for known simulation errors that can be retried.
336
+ */
337
+ function checkKnownSimulationError(response: SimulatedTransactionResponse): boolean {
338
+ const errors: Record<string, string> = {};
339
+
340
+ // This error occurs when the blockhash included in a transaction is not deemed to be valid
341
+ if (response.err === "BlockhashNotFound") {
342
+ errors["BlockhashNotFound"] =
343
+ "Blockhash not found during simulation. Trying again.";
344
+ }
345
+
346
+ // Check the response logs for any known errors
347
+ if (response.logs) {
348
+ for (const line of response.logs) {
349
+ if (line.includes("SlippageToleranceExceeded")) {
350
+ errors["SlippageToleranceExceeded"] =
351
+ "Slippage failure during simulation. Trying again.";
352
+ }
353
+
354
+ if (line.includes("RequireGteViolated")) {
355
+ errors["RequireGteViolated"] =
356
+ "Swap instruction failure during simulation. Trying again.";
357
+ }
358
+ }
359
+ }
360
+
361
+ if (Object.keys(errors).length === 0) {
362
+ return false;
363
+ }
364
+
365
+ console.table(errors);
366
+ return true;
367
+ }
368
+
369
+ /**
370
+ * Checks whether a hostname is exactly the given domain or a subdomain of it.
371
+ * e.g. isHostOrSubdomainOf("api.triton.one", "triton.one") => true
372
+ * isHostOrSubdomainOf("not-triton.com", "triton.one") => false
373
+ */
374
+ function isHostOrSubdomainOf(hostname: string, base: string): boolean {
375
+ return hostname === base || hostname.endsWith(`.${base}`);
376
+ }
377
+
378
+ /**
379
+ * Determines the RPC provider from the endpoint URL.
380
+ */
381
+ export function determineRpcProvider(endpoint: string): SolanaRpcProvider {
382
+ try {
383
+ const url = new URL(endpoint);
384
+ const hostname = url.hostname;
385
+ if (isHostOrSubdomainOf(hostname, "rpcpool.com") || isHostOrSubdomainOf(hostname, "triton.one")) {
386
+ return "triton";
387
+ } else if (isHostOrSubdomainOf(hostname, "helius-rpc.com") || isHostOrSubdomainOf(hostname, "helius.xyz")) {
388
+ return "helius";
389
+ } else if (isHostOrSubdomainOf(hostname, "ankr.com")) {
390
+ return "ankr";
391
+ } else {
392
+ return "unknown";
393
+ }
394
+ } catch {
395
+ return "unknown";
396
+ }
397
+ }
398
+
399
+ /**
400
+ * Sleep for a specified duration.
401
+ */
402
+ export async function sleep(timeout: number): Promise<void> {
403
+ return new Promise((resolve) => setTimeout(resolve, timeout));
404
+ }
405
+
406
+ /**
407
+ * Checks whether an object is empty.
408
+ */
409
+ export const isEmptyObject = (value: object | null | undefined): boolean => {
410
+ if (value === null || value === undefined) {
411
+ return true;
412
+ }
413
+
414
+ for (const key in value) {
415
+ if (Object.prototype.hasOwnProperty.call(value, key)) {
416
+ return false;
417
+ }
418
+ }
419
+
420
+ return true;
421
+ };
422
+
@@ -37,11 +37,18 @@ export interface WormholeTransferRequest {
37
37
  sponsorAccount?: Account;
38
38
  }
39
39
 
40
+ export type WithdrawPhase =
41
+ | "initiating" // User signing Aptos burn transaction
42
+ | "tracking" // Waiting for Wormhole attestation (~60s)
43
+ | "claiming"; // Claiming on destination chain
44
+
40
45
  export interface WormholeWithdrawRequest {
41
46
  sourceChain: Chain;
42
47
  wallet: AdapterWallet;
43
48
  destinationAddress: AccountAddressInput;
44
49
  sponsorAccount?: Account | GasStationApiKey;
50
+ /** Optional callback fired when the withdraw progresses to a new phase. */
51
+ onPhaseChange?: (phase: WithdrawPhase) => void;
45
52
  }
46
53
 
47
54
  export interface WormholeSubmitTransferRequest {
@@ -70,3 +77,64 @@ export interface WormholeStartTransferResponse {
70
77
  originChainTxnId: string;
71
78
  receipt: routes.Receipt<AttestationReceipt>;
72
79
  }
80
+
81
+ // --- Split withdraw flow types ---
82
+
83
+ export interface WormholeInitiateWithdrawRequest {
84
+ wallet: AdapterWallet;
85
+ destinationAddress: AccountAddressInput;
86
+ sponsorAccount?: Account | GasStationApiKey;
87
+ }
88
+
89
+ export interface WormholeInitiateWithdrawResponse {
90
+ originChainTxnId: string;
91
+ receipt: routes.Receipt<AttestationReceipt>;
92
+ }
93
+
94
+ export interface WormholeClaimWithdrawRequest {
95
+ sourceChain: Chain;
96
+ destinationAddress: string;
97
+ receipt: routes.Receipt<AttestationReceipt>;
98
+ // Required for wallet-based claim (non-Solana chains, or Solana without serverClaimUrl).
99
+ // Not needed when the SDK uses the configured serverClaimUrl for Solana claims.
100
+ wallet?: AdapterWallet;
101
+ }
102
+
103
+ export interface WormholeClaimWithdrawResponse {
104
+ destinationChainTxnId: string;
105
+ }
106
+
107
+ /**
108
+ * Error thrown when the withdraw flow fails *after* the Aptos burn
109
+ * transaction has already been submitted (i.e. during attestation tracking
110
+ * or destination-chain claiming).
111
+ *
112
+ * Consumers should check `instanceof WithdrawError` in their catch block
113
+ * to recover the `originChainTxnId` and display an explorer link so the
114
+ * user can verify their burn on-chain.
115
+ */
116
+ export class WithdrawError extends Error {
117
+ /** Aptos burn transaction hash — always available when this error is thrown. */
118
+ readonly originChainTxnId: string;
119
+ /** The withdraw phase that failed ("tracking" or "claiming"). */
120
+ readonly phase: WithdrawPhase;
121
+ /**
122
+ * The underlying error that caused this failure.
123
+ * Mirrors ES2022 Error.cause — declared explicitly because the project's
124
+ * TypeScript lib target does not include ES2022 ErrorOptions.
125
+ */
126
+ readonly cause?: unknown;
127
+
128
+ constructor(
129
+ message: string,
130
+ originChainTxnId: string,
131
+ phase: WithdrawPhase,
132
+ cause?: unknown,
133
+ ) {
134
+ super(message);
135
+ this.name = "WithdrawError";
136
+ this.originChainTxnId = originChainTxnId;
137
+ this.phase = phase;
138
+ this.cause = cause;
139
+ }
140
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Utility functions for Wormhole CCTP route creation.
3
+ * These helpers can be used both by WormholeProvider and server-side claim endpoints.
4
+ */
5
+
6
+ import {
7
+ chainToPlatform,
8
+ routes,
9
+ Wormhole,
10
+ TokenId,
11
+ } from "@wormhole-foundation/sdk";
12
+ import { Chain } from "../../CrossChainCore";
13
+ import { TokenConfig } from "../../config";
14
+
15
+ export interface CCTPRouteResult {
16
+ route: routes.ManualRoute<"Mainnet" | "Testnet">;
17
+ request: routes.RouteTransferRequest<"Mainnet" | "Testnet">;
18
+ }
19
+
20
+ /**
21
+ * Creates a CCTP route for transferring USDC between two chains.
22
+ *
23
+ * This is a standalone helper that can be used both by WormholeProvider
24
+ * (client-side) and server-side claim endpoints to avoid code duplication.
25
+ *
26
+ * @param wh - Initialized Wormhole context
27
+ * @param sourceChain - Source chain (e.g., "Aptos")
28
+ * @param destChain - Destination chain (e.g., "Solana")
29
+ * @param tokens - Token configuration mapping (mainnetTokens or testnetTokens)
30
+ * @returns The CCTP route and request for completing transfers
31
+ * @throws Error if no valid CCTP route is found
32
+ */
33
+ export async function createCCTPRoute(
34
+ wh: Wormhole<"Mainnet" | "Testnet">,
35
+ sourceChain: Chain,
36
+ destChain: Chain,
37
+ tokens: Record<string, TokenConfig>,
38
+ ): Promise<CCTPRouteResult> {
39
+ const sourceToken: TokenId = Wormhole.tokenId(
40
+ sourceChain,
41
+ tokens[sourceChain].tokenId.address,
42
+ );
43
+ const destToken: TokenId = Wormhole.tokenId(
44
+ destChain,
45
+ tokens[destChain].tokenId.address,
46
+ );
47
+
48
+ const destContext = wh
49
+ .getPlatform(chainToPlatform(destChain))
50
+ .getChain(destChain);
51
+ const sourceContext = wh
52
+ .getPlatform(chainToPlatform(sourceChain))
53
+ .getChain(sourceChain);
54
+
55
+ const request = await routes.RouteTransferRequest.create(
56
+ wh,
57
+ { source: sourceToken, destination: destToken },
58
+ sourceContext,
59
+ destContext,
60
+ );
61
+
62
+ const resolver = wh.resolver([routes.CCTPRoute]);
63
+ const foundRoutes = await resolver.findRoutes(request);
64
+ const cctpRoute = foundRoutes[0];
65
+
66
+ if (!cctpRoute || !routes.isManual(cctpRoute)) {
67
+ throw new Error("Expected manual CCTP route");
68
+ }
69
+
70
+ return { route: cctpRoute, request };
71
+ }
72
+