Skip to content

Commit 770fe72

Browse files
feat: add titan transactions for devtools testing
Co-Authored-By: Cursor <cursoragent@cursor.com>
1 parent 61b51c7 commit 770fe72

File tree

7 files changed

+681
-9
lines changed

7 files changed

+681
-9
lines changed
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { NextRequest, NextResponse } from 'next/server';
2+
3+
// Demo endpoints by region
4+
const TITAN_DEMO_URLS: Record<string, string> = {
5+
us1: 'https://us1.api.demo.titan.exchange',
6+
jp1: 'https://jp1.api.demo.titan.exchange',
7+
de1: 'https://de1.api.demo.titan.exchange',
8+
};
9+
10+
/**
11+
* Next.js API route proxy for Titan API.
12+
* Proxies all requests to Titan's API to work around CORS restrictions.
13+
*
14+
* Usage: /api/titan/api/v1/quote/swap?inputMint=...&region=us1
15+
* → proxied to https://us1.api.demo.titan.exchange/api/v1/quote/swap?inputMint=...
16+
*
17+
* Environment:
18+
* - TITAN_API_TOKEN: Your Titan API JWT token (required for demo endpoints)
19+
*
20+
* Query params:
21+
* - region: 'us1' | 'jp1' | 'de1' (default: 'us1') - selects Titan endpoint
22+
* - ...all other params forwarded to Titan
23+
*/
24+
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
25+
const { path } = await params;
26+
const titanPath = '/' + path.join('/');
27+
const searchParams = request.nextUrl.searchParams;
28+
29+
// Extract region (used for routing, not forwarded)
30+
const region = searchParams.get('region') || 'us1';
31+
const baseUrl = TITAN_DEMO_URLS[region] || TITAN_DEMO_URLS.us1;
32+
33+
// Build params to forward (exclude 'region')
34+
const forwardParams = new URLSearchParams();
35+
searchParams.forEach((value, key) => {
36+
if (key !== 'region') {
37+
forwardParams.set(key, value);
38+
}
39+
});
40+
41+
const url = forwardParams.toString() ? `${baseUrl}${titanPath}?${forwardParams}` : `${baseUrl}${titanPath}`;
42+
43+
try {
44+
// Use server-side token from env, or forward client header as fallback
45+
const authToken = process.env.TITAN_API_TOKEN || request.headers.get('Authorization')?.replace('Bearer ', '');
46+
const headers: Record<string, string> = {
47+
Accept: 'application/msgpack',
48+
};
49+
if (authToken) {
50+
headers['Authorization'] = `Bearer ${authToken}`;
51+
}
52+
53+
const response = await fetch(url, { headers });
54+
55+
if (!response.ok) {
56+
const errorText = await response.text().catch(() => 'Unknown error');
57+
console.error(`Titan API error (${response.status}) at ${titanPath}:`, errorText);
58+
return new NextResponse(errorText, {
59+
status: response.status,
60+
headers: { 'Content-Type': 'text/plain' },
61+
});
62+
}
63+
64+
// Return MessagePack binary response as-is
65+
const buffer = await response.arrayBuffer();
66+
return new NextResponse(buffer, {
67+
status: 200,
68+
headers: {
69+
'Content-Type': 'application/msgpack',
70+
},
71+
});
72+
} catch (error) {
73+
console.error('Titan API proxy error:', error);
74+
return new NextResponse(error instanceof Error ? error.message : 'Failed to proxy Titan request', {
75+
status: 500,
76+
headers: { 'Content-Type': 'text/plain' },
77+
});
78+
}
79+
}

examples/next-js/components/playground/transactions-section.tsx

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
import {
44
LegacySolTransfer,
55
ModernSolTransfer,
6+
ModernWalletTransfer,
7+
TitanSwap,
8+
titanSwapCode,
69
KitSignerDemo,
710
ChainUtilitiesDemo,
811
ConnectionAbstractionDemo,
@@ -257,6 +260,148 @@ export function ModernSolTransfer() {
257260
}`,
258261
render: () => <ModernSolTransfer />,
259262
},
263+
{
264+
id: 'modern-wallet-transfer',
265+
name: 'Modern Wallet Transfer',
266+
description: 'Transfer 1 lamport to another wallet using @solana/kit with a kit-compatible signer.',
267+
fileName: 'components/transactions/modern-wallet-transfer.tsx',
268+
code: `'use client';
269+
270+
import { useCallback, useMemo } from 'react';
271+
import {
272+
createSolanaRpc,
273+
pipe,
274+
createTransactionMessage,
275+
setTransactionMessageFeePayerSigner,
276+
setTransactionMessageLifetimeUsingBlockhash,
277+
appendTransactionMessageInstructions,
278+
sendAndConfirmTransactionFactory,
279+
signTransactionMessageWithSigners,
280+
createSolanaRpcSubscriptions,
281+
lamports,
282+
assertIsTransactionWithBlockhashLifetime,
283+
signature as createSignature,
284+
address,
285+
type TransactionSigner,
286+
} from '@solana/kit';
287+
import { getTransferSolInstruction } from '@solana-program/system';
288+
import { useKitTransactionSigner, useCluster, useConnectorClient } from '@solana/connector';
289+
import { PipelineHeaderButton, PipelineVisualization } from '@/components/pipeline';
290+
import { VisualPipeline } from '@/lib/visual-pipeline';
291+
import { useExampleCardHeaderActions } from '@/components/playground/example-card-actions';
292+
import {
293+
getBase58SignatureFromSignedTransaction,
294+
getBase64EncodedWireTransaction,
295+
getWebSocketUrlForRpcUrl,
296+
isRpcProxyUrl,
297+
waitForSignatureConfirmation,
298+
} from './rpc-utils';
299+
300+
// Destination wallet address
301+
const DESTINATION_ADDRESS = address('A7Xmq3qqt4uvw3GELHw9HHNFbwZzHDJNtmk6fe2p5b5s');
302+
303+
export function ModernWalletTransfer() {
304+
const { signer, ready } = useKitTransactionSigner();
305+
const { cluster } = useCluster();
306+
const client = useConnectorClient();
307+
308+
const visualPipeline = useMemo(
309+
() =>
310+
new VisualPipeline('modern-wallet-transfer', [
311+
{ name: 'Build instruction', type: 'instruction' },
312+
{ name: 'Transfer SOL', type: 'transaction' },
313+
]),
314+
[],
315+
);
316+
317+
const getExplorerUrl = useCallback(
318+
(sig: string) => {
319+
const clusterSlug = cluster?.id?.replace('solana:', '');
320+
if (!clusterSlug || clusterSlug === 'mainnet' || clusterSlug === 'mainnet-beta') {
321+
return 'https://explorer.solana.com/tx/' + sig;
322+
}
323+
return 'https://explorer.solana.com/tx/' + sig + '?cluster=' + clusterSlug;
324+
},
325+
[cluster?.id],
326+
);
327+
328+
const executeWalletTransfer = useCallback(async () => {
329+
if (!signer || !client) return;
330+
331+
const rpcUrl = client.getRpcUrl();
332+
if (!rpcUrl) throw new Error('No RPC endpoint configured');
333+
const rpc = createSolanaRpc(rpcUrl);
334+
335+
let signatureBase58: string | null = null;
336+
337+
await visualPipeline.execute(async () => {
338+
visualPipeline.setStepState('Build instruction', { type: 'building' });
339+
visualPipeline.setStepState('Transfer SOL', { type: 'building' });
340+
341+
const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();
342+
343+
// Transfer to another wallet instead of self
344+
const transferInstruction = getTransferSolInstruction({
345+
source: signer as TransactionSigner,
346+
destination: DESTINATION_ADDRESS,
347+
amount: lamports(1n),
348+
});
349+
350+
const transactionMessage = pipe(
351+
createTransactionMessage({ version: 0 }),
352+
tx => setTransactionMessageFeePayerSigner(signer, tx),
353+
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
354+
tx => appendTransactionMessageInstructions([transferInstruction], tx),
355+
);
356+
357+
visualPipeline.setStepState('Transfer SOL', { type: 'signing' });
358+
359+
const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
360+
signatureBase58 = getBase58SignatureFromSignedTransaction(signedTransaction);
361+
362+
visualPipeline.setStepState('Build instruction', { type: 'confirmed', signature: signatureBase58, cost: 0 });
363+
visualPipeline.setStepState('Transfer SOL', { type: 'sending' });
364+
365+
assertIsTransactionWithBlockhashLifetime(signedTransaction);
366+
367+
if (isRpcProxyUrl(rpcUrl)) {
368+
const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction);
369+
await rpc.sendTransaction(encodedTransaction, { encoding: 'base64' }).send();
370+
await waitForSignatureConfirmation({
371+
signature: signatureBase58,
372+
commitment: 'confirmed',
373+
getSignatureStatuses: async sig =>
374+
await rpc.getSignatureStatuses([createSignature(sig)]).send(),
375+
});
376+
} else {
377+
const rpcSubscriptions = createSolanaRpcSubscriptions(getWebSocketUrlForRpcUrl(rpcUrl));
378+
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
379+
commitment: 'confirmed',
380+
});
381+
}
382+
383+
visualPipeline.setStepState('Transfer SOL', { type: 'confirmed', signature: signatureBase58, cost: 0.000005 });
384+
});
385+
}, [client, signer, visualPipeline]);
386+
387+
useExampleCardHeaderActions(
388+
<PipelineHeaderButton visualPipeline={visualPipeline} disabled={!ready || !client} onExecute={executeWalletTransfer} />,
389+
);
390+
391+
return (
392+
<PipelineVisualization visualPipeline={visualPipeline} strategy="sequential" getExplorerUrl={getExplorerUrl} />
393+
);
394+
}`,
395+
render: () => <ModernWalletTransfer />,
396+
},
397+
{
398+
id: 'titan-swap',
399+
name: 'Titan Swap (SOL → USDC)',
400+
description: 'Swap 0.01 SOL for USDC using Titan InstructionPlans and track the transaction(s) in Connector Devtools.',
401+
fileName: 'components/transactions/titan-swap.tsx',
402+
code: titanSwapCode,
403+
render: () => <TitanSwap />,
404+
},
260405
{
261406
id: 'kit-signer',
262407
name: 'Kit Signers',

examples/next-js/components/transactions/index.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
export { TransactionDemo } from './transaction-demo';
22
export { LegacySolTransfer } from './legacy-sol-transfer';
33
export { ModernSolTransfer } from './modern-sol-transfer';
4+
export { ModernWalletTransfer } from './modern-wallet-transfer';
5+
export { TitanSwap, titanSwapCode } from './titan-swap';
46
export { TransactionForm } from './transaction-form';
57
export { TransactionResult } from './transaction-result';
68
export { KitSignerDemo } from './kit-signer-demo';

0 commit comments

Comments
 (0)