Algorand MCP
by GoPlausible
import { PeraWalletConnect } from '@perawallet/connect';
import { DeflyWalletConnect } from '@blockshake/defly-connect';
import { DaffiWalletConnect } from '@daffiwallet/connect';
import algosdk, { Transaction } from 'algosdk';
import { LocalWallet } from './LocalWallet.js';
export type WalletType = 'pera' | 'defly' | 'daffi' | 'local';
export interface WalletConnectOptions {
network: 'mainnet' | 'testnet';
}
interface WalletProvider {
connect(): Promise<string[]>;
reconnectSession(): Promise<string[]>;
disconnect(): Promise<void>;
signTransactions(transactions: { txn: Transaction; message?: string }[][]): Promise<Uint8Array[][]>;
}
export class AlgorandMcpClient {
private peraWallet: PeraWalletConnect;
private deflyWallet: DeflyWalletConnect;
private daffiWallet: DaffiWalletConnect;
private activeWallet: WalletProvider | null = null;
private localWallet: LocalWallet;
private connectedAccounts: string[] = [];
private activeWalletType: WalletType | null = null;
constructor(options: WalletConnectOptions) {
this.localWallet = new LocalWallet();
this.peraWallet = new PeraWalletConnect({
chainId: options.network === 'mainnet' ? 416001 : 416002
});
this.deflyWallet = new DeflyWalletConnect({
chainId: options.network === 'mainnet' ? 416001 : 416002
});
this.daffiWallet = new DaffiWalletConnect({
chainId: options.network === 'mainnet' ? 416001 : 416002
});
}
async connect(walletType: WalletType): Promise<string[]> {
try {
let accounts: string[] = [];
switch (walletType) {
case 'local':
accounts = await this.localWallet.connect();
this.activeWallet = this.localWallet;
break;
case 'pera':
accounts = await this.peraWallet.connect();
this.activeWallet = this.peraWallet as unknown as WalletProvider;
break;
case 'defly':
accounts = await this.deflyWallet.connect();
this.activeWallet = this.deflyWallet as unknown as WalletProvider;
break;
case 'daffi':
accounts = await this.daffiWallet.connect();
this.activeWallet = this.daffiWallet as unknown as WalletProvider;
break;
default:
throw new Error('Unsupported wallet type');
}
this.connectedAccounts = accounts;
this.activeWalletType = walletType;
return accounts;
} catch (error: unknown) {
throw new Error(`Failed to connect wallet: ${error instanceof Error ? error.message : String(error)}`);
}
}
makeTransactionSigner = async (txnGroup: Uint8Array[], indexesToSign: number[]) => {
if (!this.activeWallet || !this.activeWalletType) {
throw new Error('No active wallet');
}
if (this.activeWalletType === 'local') {
const signer = await this.localWallet.makeTransactionSigner();
const decodedTxns = txnGroup.map(txn => algosdk.decodeUnsignedTransaction(txn));
return () => signer(decodedTxns, indexesToSign);
}
return async () => {
if (!this.activeWallet) {
throw new Error('Wallet not connected');
}
try {
// Decode transactions from Uint8Array
const decodedTxns = txnGroup.map(txn => algosdk.decodeUnsignedTransaction(txn));
// Format transactions for signing
const txnsToSign = decodedTxns.map(txn => ({
txn: txn,
message: 'Please sign the transaction'
}));
const result = await this.activeWallet.signTransactions([txnsToSign]);
return result[0];
} catch (error: any) {
// Handle specific error cases
if (error?.data?.type === "SIGN_MODAL_CLOSED") {
console.error('Transaction signing cancelled');
return new Array(indexesToSign.length).fill(null);
}
const msg = error?.message || "Failed to sign transaction";
console.error(`Wallet signer error: ${msg}`);
return new Array(indexesToSign.length).fill(null);
}
};
}
getAccounts(): string[] {
return this.connectedAccounts;
}
async signTransactions(transactions: { txn: Transaction; message?: string }[][]): Promise<Uint8Array[][]> {
if (!this.activeWallet) {
throw new Error('No active wallet connection. Call connect() first.');
}
try {
return await this.activeWallet.signTransactions(transactions);
} catch (error: unknown) {
throw new Error(`Failed to sign transactions: ${error instanceof Error ? error.message : String(error)}`);
}
}
// Backward compatibility
async signTransaction(txn: Transaction): Promise<Uint8Array> {
const result = await this.signTransactions([[{ txn, message: 'Transaction to sign' }]]);
return result[0][0];
}
async disconnect(): Promise<void> {
if (this.activeWallet) {
await this.activeWallet.disconnect();
this.activeWallet = null;
this.connectedAccounts = [];
}
}
async reconnectSession(walletType: WalletType): Promise<string[]> {
try {
let accounts: string[] = [];
switch (walletType) {
case 'local':
accounts = await this.localWallet.reconnectSession();
this.activeWallet = this.localWallet;
break;
case 'pera':
accounts = await this.peraWallet.reconnectSession();
this.activeWallet = this.peraWallet as unknown as WalletProvider;
break;
case 'defly':
accounts = await this.deflyWallet.reconnectSession();
this.activeWallet = this.deflyWallet as unknown as WalletProvider;
break;
case 'daffi':
accounts = await this.daffiWallet.reconnectSession();
this.activeWallet = this.daffiWallet as unknown as WalletProvider;
break;
default:
throw new Error('Unsupported wallet type');
}
this.connectedAccounts = accounts;
this.activeWalletType = walletType;
return accounts;
} catch (error: unknown) {
throw new Error(`Failed to reconnect session: ${error instanceof Error ? error.message : String(error)}`);
}
}
}