Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
95 changes: 95 additions & 0 deletions apps/demo-app/src/app/direct-mode/connectors/KeplrConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/**
* Example Keplr connector implementation
* Demonstrates how to create a custom connector for browser wallets
*
* This is an example implementation - developers can use this as a reference
* for creating their own connectors for any wallet provider.
*/

import { Buffer } from 'buffer';
import type { Connector, ConnectorConnectionResult, ConnectorMetadata } from '@burnt-labs/abstraxion-core';
import { ConnectorType } from '@burnt-labs/abstraxion-core';
import { AUTHENTICATOR_TYPE } from '@burnt-labs/abstraxion';

/**
* Connector for Keplr wallet
* Example implementation showing how to integrate Cosmos wallets
*/
export class KeplrConnector implements Connector {
public metadata: ConnectorMetadata;
private wallet: any = null;

constructor() {
this.metadata = {
id: 'keplr',
name: 'Keplr',
type: ConnectorType.COSMOS_WALLET,
icon: '🔑',
};
}

async isAvailable(): Promise<boolean> {
if (typeof window === 'undefined') {
return false;
}
return !!(window as any).keplr;
}

async connect(chainId: string): Promise<ConnectorConnectionResult> {
if (!chainId) {
throw new Error('Chain ID is required for Keplr');
}

const keplr = (window as any).keplr;
if (!keplr) {
throw new Error('Keplr wallet not found');
}

await keplr.enable(chainId);
const offlineSigner = await keplr.getOfflineSignerAuto(chainId);
const accounts = await offlineSigner.getAccounts();

if (accounts.length === 0) {
throw new Error('No accounts found in Keplr');
}

const account = accounts[0];
const pubkey = account.pubkey;
const pubkeyHex = Buffer.from(pubkey).toString('hex');
const pubkeyBase64 = Buffer.from(pubkey).toString('base64');
this.wallet = keplr;

const signMessage = async (hexMessage: string): Promise<string> => {
if (!this.wallet) {
throw new Error('Wallet not connected');
}
const plainText = Buffer.from(hexMessage.replace('0x', ''), 'hex').toString('utf8');
const signature = await this.wallet.signArbitrary(chainId, account.address, plainText);

if (typeof signature === 'string') {
const sigBytes = Buffer.from(signature, 'base64');
return sigBytes.toString('hex');
} else {
const sigBytes = Buffer.from((signature as any).signature, 'base64');
return sigBytes.toString('hex');
}
};

return {
authenticator: pubkeyBase64,
displayAddress: account.address,
signMessage,
metadata: {
authenticatorType: AUTHENTICATOR_TYPE.Secp256K1,
walletName: 'keplr',
pubkey: pubkeyHex,
connectionType: 'shuttle',
},
};
}

async disconnect(): Promise<void> {
this.wallet = null;
}
}

87 changes: 87 additions & 0 deletions apps/demo-app/src/app/direct-mode/connectors/MetaMaskConnector.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
/**
* Example MetaMask connector implementation
* Demonstrates how to create a custom connector for Ethereum wallets
*
* This is an example implementation - developers can use this as a reference
* for creating their own connectors for any wallet provider.
*/

import type { Connector, ConnectorConnectionResult, ConnectorMetadata } from '@burnt-labs/abstraxion';
import { ConnectorType } from '@burnt-labs/abstraxion';
import { AUTHENTICATOR_TYPE } from '@burnt-labs/abstraxion';

/**
* Connector for MetaMask wallet
* Example implementation showing how to integrate Ethereum wallets
*/
export class MetaMaskConnector implements Connector {
public metadata: ConnectorMetadata;
private ethereumProvider: any = null;
private ethereumAddress: string | null = null;

constructor() {
this.metadata = {
id: 'metamask',
name: 'MetaMask',
type: ConnectorType.ETHEREUM_WALLET,
icon: '🦊',
};
}

async isAvailable(): Promise<boolean> {
if (typeof window === 'undefined') {
return false;
}
return !!(window as any).ethereum;
}

async connect(chainId?: string): Promise<ConnectorConnectionResult> {
const ethereum = (window as any).ethereum;

if (!ethereum) {
throw new Error('MetaMask wallet not found');
}

try {
const accounts = await ethereum.request({ method: 'eth_requestAccounts' });

if (!accounts || accounts.length === 0) {
throw new Error('No accounts found in MetaMask');
}

const address = accounts[0];
this.ethereumProvider = ethereum;
this.ethereumAddress = address;

const signMessage = async (hexMessage: string): Promise<string> => {
if (!this.ethereumProvider || !this.ethereumAddress) {
throw new Error('Wallet not connected');
}
const signature = await this.ethereumProvider.request({
method: 'personal_sign',
params: [hexMessage, this.ethereumAddress],
});
return signature.replace(/^0x/, '');
};

return {
authenticator: address.toLowerCase(),
displayAddress: address,
signMessage,
metadata: {
authenticatorType: AUTHENTICATOR_TYPE.EthWallet,
ethereumAddress: address,
connectionType: 'metamask',
},
};
} catch (error: any) {
throw new Error(`Failed to connect to MetaMask: ${error.message || error}`);
}
}

async disconnect(): Promise<void> {
this.ethereumProvider = null;
this.ethereumAddress = null;
}
}

8 changes: 8 additions & 0 deletions apps/demo-app/src/app/direct-mode/connectors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Example connectors for direct mode
* These demonstrate how developers can implement their own connectors
*/

export { KeplrConnector } from './KeplrConnector';
export { MetaMaskConnector } from './MetaMaskConnector';

165 changes: 165 additions & 0 deletions apps/demo-app/src/app/direct-mode/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
"use client";
import { useMemo, createContext, useContext } from "react";
import {
AbstraxionProvider,
type AbstraxionConfig,
type SignerConfig,
useConnectorSelection,
} from "@burnt-labs/abstraxion";
import { WalletModal } from "../../components/WalletModal";
import { KeplrConnector, MetaMaskConnector } from "./connectors";

// Context to share setShowModal between layout and page
interface DirectModeContextType {
setShowModal: (show: boolean) => void;
}

const DirectModeContext = createContext<DirectModeContextType | null>(null);

export function useDirectMode() {
const context = useContext(DirectModeContext);
if (!context) {
throw new Error("useDirectMode must be used within DirectModeLayout");
}
return context;
}

/**
* Direct Mode Content - manages connectors and modal state
*/
function DirectModeContent({ children }: { children: React.ReactNode }) {
// Create connector instances
const connectors = useMemo(() => [
new KeplrConnector(),
new MetaMaskConnector(),
], []);

// Use connector selection hook - single source of truth for modal state
const connectorSelection = useConnectorSelection({
connectors,
aaApiUrl: process.env.NEXT_PUBLIC_AA_API_URL,
});

return (
<DirectModeContext.Provider value={{ setShowModal: connectorSelection.setShowModal }}>
{children}
{/* Wallet modal - receives all hook results as props */}
<WalletModal
connectors={connectors}
showModal={connectorSelection.showModal}
setShowModal={connectorSelection.setShowModal}
availableConnectors={connectorSelection.availableConnectors}
connect={connectorSelection.connect}
error={connectorSelection.error}
isConnecting={connectorSelection.isConnecting}
/>
</DirectModeContext.Provider>
);
}

export default function DirectModeLayout({
children,
}: {
children: React.ReactNode;
}): JSX.Element {
// Build indexer config (supports both Numia and Subquery)
const indexerConfig = useMemo(() => {
if (!process.env.NEXT_PUBLIC_INDEXER_URL) return undefined;

// If type is explicitly set to subquery, use Subquery
if (process.env.NEXT_PUBLIC_INDEXER_TYPE === 'subquery') {
if (!process.env.NEXT_PUBLIC_CODE_ID) {
throw new Error('NEXT_PUBLIC_CODE_ID is required when using Subquery indexer');
}
return {
type: 'subquery' as const,
url: process.env.NEXT_PUBLIC_INDEXER_URL,
codeId: parseInt(process.env.NEXT_PUBLIC_CODE_ID),
};
}

// Otherwise, use Numia (default)
if (process.env.NEXT_PUBLIC_INDEXER_TOKEN) {
return {
type: 'numia' as const,
url: process.env.NEXT_PUBLIC_INDEXER_URL,
authToken: process.env.NEXT_PUBLIC_INDEXER_TOKEN,
};
}

return undefined;
}, []);

// Smart account contract configuration (required for signer mode)
const smartAccountContractConfig = useMemo(() => {
if (!process.env.NEXT_PUBLIC_CODE_ID || !process.env.NEXT_PUBLIC_CHECKSUM) {
throw new Error('Smart account contract config is required for direct mode. Please provide NEXT_PUBLIC_CODE_ID and NEXT_PUBLIC_CHECKSUM.');
}
return {
codeId: parseInt(process.env.NEXT_PUBLIC_CODE_ID),
checksum: process.env.NEXT_PUBLIC_CHECKSUM,
addressPrefix: process.env.NEXT_PUBLIC_ADDRESS_PREFIX || 'xion',
};
}, []);

// Configuration for AbstraxionProvider
// Uses signer mode with connectors - the useConnectorSelection hook handles the actual connector connection
const directModeConfig: AbstraxionConfig = useMemo(() => ({
// REQUIRED: Chain ID
chainId: process.env.NEXT_PUBLIC_CHAIN_ID || "xion-testnet-2",

// REQUIRED: RPC URL for blockchain connection
rpcUrl: process.env.NEXT_PUBLIC_RPC_URL!,

// REQUIRED: REST API endpoint
restUrl: process.env.NEXT_PUBLIC_REST_URL!,

// REQUIRED: Gas price
gasPrice: process.env.NEXT_PUBLIC_GAS_PRICE || "0.001uxion",

// Treasury contract address (optional - for dynamic grant configs)
treasury: process.env.NEXT_PUBLIC_TREASURY_ADDRESS,

// Fee granter address (required for grant creation and smart account creation)
feeGranter: process.env.NEXT_PUBLIC_FEE_GRANTER_ADDRESS,

// Signer-mode configuration
authentication: {
type: "signer" as const,

// AA API URL for account creation
aaApiUrl: process.env.NEXT_PUBLIC_AA_API_URL!,

// Function that returns signer configuration
// Note: This is required by the type but useConnectorSelection bypasses it
// by using orchestrator directly. This function should not be called when using connectors.
getSignerConfig: async (): Promise<SignerConfig> => {
throw new Error(
'getSignerConfig should not be called when using connectors via useConnectorSelection. ' +
'The connection flow is handled by the useConnectorSelection hook.'
);
},

// Auto-connect behavior
autoConnect: false, // Manual login - wait for user to click connect

// Smart account contract configuration (codeId, checksum, addressPrefix)
smartAccountContract: smartAccountContractConfig,

// Indexer configuration for account discovery (optional - falls back to RPC if not provided)
indexer: indexerConfig,

// Treasury indexer configuration - for fetching grant configs from DaoDao indexer (fast)
// Optional - falls back to direct RPC queries if not provided
treasuryIndexer: process.env.NEXT_PUBLIC_TREASURY_INDEXER_URL ? {
url: process.env.NEXT_PUBLIC_TREASURY_INDEXER_URL,
} : undefined,
},
}), [indexerConfig, smartAccountContractConfig]);

return (
<AbstraxionProvider config={directModeConfig}>
<DirectModeContent>{children}</DirectModeContent>
</AbstraxionProvider>
);
}
Loading
Loading