Skip to content
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/publish-sdks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ on:
options:
- all
- connector
- devtools
- connector-debugger

permissions:
contents: read
Expand Down Expand Up @@ -66,8 +66,8 @@ jobs:
working-directory: packages/connector
run: npm publish --access public

- name: Publish @solana/devtools
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.package == 'all' || github.event.inputs.package == 'devtools' }}
- name: Publish @solana/connector-debugger
if: ${{ startsWith(github.ref, 'refs/tags/') || github.event.inputs.package == 'all' || github.event.inputs.package == 'connector-debugger' }}
working-directory: packages/devtools
run: npm publish --access public

Expand Down Expand Up @@ -95,16 +95,16 @@ jobs:
- Version: ${{ github.ref_name }}
- Registry: npm

### @solana/devtools
- Package: `@solana/devtools`
### @solana/connector-debugger
- Package: `@solana/connector-debugger`
- Version: ${{ github.ref_name }}
- Registry: npm

## Installation

```bash
npm install @solana/connector@${{ github.ref_name }}
npm install @solana/devtools@${{ github.ref_name }}
npm install @solana/connector-debugger@${{ github.ref_name }}
```
draft: false
prerelease: ${{ contains(github.ref_name, '-') }}
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ Production-ready Solana wallet infrastructure. A headless, framework-agnostic wa
| Package | Description |
| ----------------------------------------- | ---------------------------------------------------------- |
| [@solana/connector](./packages/connector) | Core wallet connector with React hooks and headless client |
| [@solana/devtools](./packages/devtools) | Framework-agnostic devtools with transaction tracking |
| [@solana/connector-debugger](./packages/devtools) | Framework-agnostic devtools with transaction tracking |

## Why ConnectorKit?

Expand Down Expand Up @@ -77,11 +77,11 @@ See the [connector package docs](./packages/connector/README.md) for full API re
Framework-agnostic devtools that work with any web framework via the imperative DOM API.

```bash
npm install @solana/devtools
npm install @solana/connector-debugger
```

```typescript
import { ConnectorDevtools } from '@solana/devtools';
import { ConnectorDevtools } from '@solana/connector-debugger';

// Create devtools (auto-detects window.__connectorClient from ConnectorProvider)
const devtools = new ConnectorDevtools({
Expand Down Expand Up @@ -114,7 +114,7 @@ export function DevtoolsLoader() {
let devtools: any;
let container: HTMLDivElement;

import('@solana/devtools').then(({ ConnectorDevtools }) => {
import('@solana/connector-debugger').then(({ ConnectorDevtools }) => {
container = document.createElement('div');
document.body.appendChild(container);
devtools = new ConnectorDevtools();
Expand Down
4 changes: 2 additions & 2 deletions connectorkit/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ Details (provider configs + protocol types + security) live in `references/remot

### Devtools (Development Only)

ConnectorKit also ships devtools (`@solana/devtools`) that can be dynamically mounted in development.
ConnectorKit also ships devtools (`@solana/connector-debugger`) that can be dynamically mounted in development.

```tsx
'use client';
Expand All @@ -299,7 +299,7 @@ export function DevtoolsLoader() {
let devtools: { mount: (el: HTMLElement) => void; unmount: () => void } | undefined;
let container: HTMLDivElement | undefined;

import('@solana/devtools').then(({ ConnectorDevtools }) => {
import('@solana/connector-debugger').then(({ ConnectorDevtools }) => {
container = document.createElement('div');
document.body.appendChild(container);
devtools = new ConnectorDevtools({ config: { position: 'bottom-right', theme: 'dark' } });
Expand Down
79 changes: 79 additions & 0 deletions examples/next-js/app/api/titan/[...path]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import { NextRequest, NextResponse } from 'next/server';

// Demo endpoints by region
const TITAN_DEMO_URLS: Record<string, string> = {
us1: 'https://us1.api.demo.titan.exchange',
jp1: 'https://jp1.api.demo.titan.exchange',
de1: 'https://de1.api.demo.titan.exchange',
};

/**
* Next.js API route proxy for Titan API.
* Proxies all requests to Titan's API to work around CORS restrictions.
*
* Usage: /api/titan/api/v1/quote/swap?inputMint=...&region=us1
* → proxied to https://us1.api.demo.titan.exchange/api/v1/quote/swap?inputMint=...
*
* Environment:
* - TITAN_API_TOKEN: Your Titan API JWT token (required for demo endpoints)
*
* Query params:
* - region: 'us1' | 'jp1' | 'de1' (default: 'us1') - selects Titan endpoint
* - ...all other params forwarded to Titan
*/
export async function GET(request: NextRequest, { params }: { params: Promise<{ path: string[] }> }) {
const { path } = await params;
const titanPath = '/' + path.join('/');
const searchParams = request.nextUrl.searchParams;

// Extract region (used for routing, not forwarded)
const region = searchParams.get('region') || 'us1';
const baseUrl = TITAN_DEMO_URLS[region] || TITAN_DEMO_URLS.us1;

// Build params to forward (exclude 'region')
const forwardParams = new URLSearchParams();
searchParams.forEach((value, key) => {
if (key !== 'region') {
forwardParams.set(key, value);
}
});

const url = forwardParams.toString() ? `${baseUrl}${titanPath}?${forwardParams}` : `${baseUrl}${titanPath}`;

try {
// Use server-side token from env, or forward client header as fallback
const authToken = process.env.TITAN_API_TOKEN || request.headers.get('Authorization')?.replace('Bearer ', '');
const headers: Record<string, string> = {
Accept: 'application/msgpack',
};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}

const response = await fetch(url, { headers });

if (!response.ok) {
const errorText = await response.text().catch(() => 'Unknown error');
console.error(`Titan API error (${response.status}) at ${titanPath}:`, errorText);
return new NextResponse(errorText, {
status: response.status,
headers: { 'Content-Type': 'text/plain' },
});
}

// Return MessagePack binary response as-is
const buffer = await response.arrayBuffer();
return new NextResponse(buffer, {
status: 200,
headers: {
'Content-Type': 'application/msgpack',
},
});
} catch (error) {
console.error('Titan API proxy error:', error);
return new NextResponse(error instanceof Error ? error.message : 'Failed to proxy Titan request', {
status: 500,
headers: { 'Content-Type': 'text/plain' },
});
}
}
2 changes: 1 addition & 1 deletion examples/next-js/app/providers.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ export function Providers({ children }: { children: ReactNode }) {
let container: HTMLDivElement | undefined;

// Dynamic import to avoid bundling in production
import('@solana/devtools').then(({ ConnectorDevtools }) => {
import('@solana/connector-debugger').then(({ ConnectorDevtools }) => {
// Create container for devtools
container = document.createElement('div');
container.id = 'connector-devtools-container';
Expand Down
146 changes: 146 additions & 0 deletions examples/next-js/components/playground/transactions-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import {
LegacySolTransfer,
ModernSolTransfer,
ModernWalletTransfer,
TitanSwap,
titanSwapCode,
KitSignerDemo,
ChainUtilitiesDemo,
ConnectionAbstractionDemo,
Expand Down Expand Up @@ -257,6 +260,149 @@ export function ModernSolTransfer() {
}`,
render: () => <ModernSolTransfer />,
},
{
id: 'modern-wallet-transfer',
name: 'Modern Wallet Transfer',
description: 'Transfer 1 lamport to another wallet using @solana/kit with a kit-compatible signer.',
fileName: 'components/transactions/modern-wallet-transfer.tsx',
code: `'use client';

import { useCallback, useMemo } from 'react';
import {
createSolanaRpc,
pipe,
createTransactionMessage,
setTransactionMessageFeePayerSigner,
setTransactionMessageLifetimeUsingBlockhash,
appendTransactionMessageInstructions,
sendAndConfirmTransactionFactory,
signTransactionMessageWithSigners,
createSolanaRpcSubscriptions,
lamports,
assertIsTransactionWithBlockhashLifetime,
signature as createSignature,
address,
type TransactionSigner,
} from '@solana/kit';
import { getTransferSolInstruction } from '@solana-program/system';
import { useKitTransactionSigner, useCluster, useConnectorClient } from '@solana/connector';
import { PipelineHeaderButton, PipelineVisualization } from '@/components/pipeline';
import { VisualPipeline } from '@/lib/visual-pipeline';
import { useExampleCardHeaderActions } from '@/components/playground/example-card-actions';
import {
getBase58SignatureFromSignedTransaction,
getBase64EncodedWireTransaction,
getWebSocketUrlForRpcUrl,
isRpcProxyUrl,
waitForSignatureConfirmation,
} from './rpc-utils';

// Destination wallet address
const DESTINATION_ADDRESS = address('A7Xmq3qqt4uvw3GELHw9HHNFbwZzHDJNtmk6fe2p5b5s');

export function ModernWalletTransfer() {
const { signer, ready } = useKitTransactionSigner();
const { cluster } = useCluster();
const client = useConnectorClient();

const visualPipeline = useMemo(
() =>
new VisualPipeline('modern-wallet-transfer', [
{ name: 'Build instruction', type: 'instruction' },
{ name: 'Transfer SOL', type: 'transaction' },
]),
[],
);

const getExplorerUrl = useCallback(
(sig: string) => {
const clusterSlug = cluster?.id?.replace('solana:', '');
if (!clusterSlug || clusterSlug === 'mainnet' || clusterSlug === 'mainnet-beta') {
return 'https://explorer.solana.com/tx/' + sig;
}
return 'https://explorer.solana.com/tx/' + sig + '?cluster=' + clusterSlug;
},
[cluster?.id],
);

const executeWalletTransfer = useCallback(async () => {
if (!signer || !client) return;

const rpcUrl = client.getRpcUrl();
if (!rpcUrl) throw new Error('No RPC endpoint configured');
const rpc = createSolanaRpc(rpcUrl);

let signatureBase58: string | null = null;

await visualPipeline.execute(async () => {
visualPipeline.setStepState('Build instruction', { type: 'building' });
visualPipeline.setStepState('Transfer SOL', { type: 'building' });

const { value: latestBlockhash } = await rpc.getLatestBlockhash().send();

// Transfer to another wallet instead of self
const transferInstruction = getTransferSolInstruction({
source: signer as TransactionSigner,
destination: DESTINATION_ADDRESS,
amount: lamports(1n),
});

const transactionMessage = pipe(
createTransactionMessage({ version: 0 }),
tx => setTransactionMessageFeePayerSigner(signer, tx),
tx => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx),
tx => appendTransactionMessageInstructions([transferInstruction], tx),
);

visualPipeline.setStepState('Transfer SOL', { type: 'signing' });

const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
signatureBase58 = getBase58SignatureFromSignedTransaction(signedTransaction);

visualPipeline.setStepState('Build instruction', { type: 'confirmed', signature: signatureBase58, cost: 0 });
visualPipeline.setStepState('Transfer SOL', { type: 'sending' });

assertIsTransactionWithBlockhashLifetime(signedTransaction);

if (isRpcProxyUrl(rpcUrl)) {
const encodedTransaction = getBase64EncodedWireTransaction(signedTransaction);
await rpc.sendTransaction(encodedTransaction, { encoding: 'base64' }).send();
await waitForSignatureConfirmation({
signature: signatureBase58,
commitment: 'confirmed',
getSignatureStatuses: async sig =>
await rpc.getSignatureStatuses([createSignature(sig)]).send(),
});
} else {
const rpcSubscriptions = createSolanaRpcSubscriptions(getWebSocketUrlForRpcUrl(rpcUrl));
await sendAndConfirmTransactionFactory({ rpc, rpcSubscriptions })(signedTransaction, {
commitment: 'confirmed',
});
}

visualPipeline.setStepState('Transfer SOL', { type: 'confirmed', signature: signatureBase58, cost: 0.000005 });
});
}, [client, signer, visualPipeline]);

useExampleCardHeaderActions(
<PipelineHeaderButton visualPipeline={visualPipeline} disabled={!ready || !client} onExecute={executeWalletTransfer} />,
);

return (
<PipelineVisualization visualPipeline={visualPipeline} strategy="sequential" getExplorerUrl={getExplorerUrl} />
);
}`,
render: () => <ModernWalletTransfer />,
},
{
id: 'titan-swap',
name: 'Titan Swap (SOL → USDC)',
description:
'Swap 0.01 SOL for USDC using Titan InstructionPlans and track the transaction(s) in Connector Devtools.',
fileName: 'components/transactions/titan-swap.tsx',
code: titanSwapCode,
render: () => <TitanSwap />,
},
{
id: 'kit-signer',
name: 'Kit Signers',
Expand Down
2 changes: 2 additions & 0 deletions examples/next-js/components/transactions/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export { TransactionDemo } from './transaction-demo';
export { LegacySolTransfer } from './legacy-sol-transfer';
export { ModernSolTransfer } from './modern-sol-transfer';
export { ModernWalletTransfer } from './modern-wallet-transfer';
export { TitanSwap, titanSwapCode } from './titan-swap';
export { TransactionForm } from './transaction-form';
export { TransactionResult } from './transaction-result';
export { KitSignerDemo } from './kit-signer-demo';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,9 @@ export function LegacySolTransfer() {

visualPipeline.setStepState('Self transfer', { type: 'signing' });

// Pre-send devtools preview: emit wire bytes so the debugger can simulate before sending.
client.previewTransaction(transaction);

sig = await walletAdapter.sendTransaction(transaction, connection);
typedSignature = createSignature(sig);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,13 +104,15 @@ export function ModernSolTransfer() {

const signedTransaction = await signTransactionMessageWithSigners(transactionMessage);
signatureBase58 = getBase58SignatureFromSignedTransaction(signedTransaction);
const wireTransactionBase64 = getBase64EncodedWireTransaction(signedTransaction);

// Track transaction in debugger
client.trackTransaction({
signature: createSignature(signatureBase58),
status: 'pending',
method: 'sendTransaction',
feePayer: signer.address,
metadata: { wireTransactionBase64 },
});

visualPipeline.setStepState('Build instruction', {
Expand Down
Loading
Loading