Skip to main content

Overview

The AttestProtocol SDK supports custom signer implementations to integrate with external wallets, hardware devices, and delegated authentication systems. This enables flexible key management while maintaining security.

Stellar Custom Signers

StellarCustomSigner Interface

interface StellarCustomSigner {
  signTransaction: (xdr: string) => Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }>;
}

Configuration

const config: StellarConfig = {
  secretKeyOrCustomSigner: customSigner, // Instead of secret key string
  publicKey: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX',
  url: 'https://soroban-testnet.stellar.org'
};

const sdk = await AttestSDK.initializeStellar(config);

Implementation Patterns

Browser Wallet Integration

class FreighterSigner implements StellarCustomSigner {
  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    if (!window.freighterApi) {
      throw new Error('Freighter wallet not installed');
    }

    const signedXdr = await window.freighterApi.signTransaction(
      xdr,
      { 
        networkPassphrase: 'Test SDF Network ; September 2015' 
      }
    );

    return {
      signedTxXdr: signedXdr,
      signerAddress: await window.freighterApi.getPublicKey()
    };
  }
}

// Usage
const signer = new FreighterSigner();
const config: StellarConfig = {
  secretKeyOrCustomSigner: signer,
  publicKey: await window.freighterApi.getPublicKey(),
  url: 'https://soroban-testnet.stellar.org'
};

Hardware Wallet Integration

class LedgerSigner implements StellarCustomSigner {
  private transport: any;

  constructor(transport: any) {
    this.transport = transport;
  }

  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    const StellarApp = await import('@ledgerhq/hw-app-str');
    const app = new StellarApp.default(this.transport);

    const transaction = xdr.Transaction.fromXDR(xdr, 'base64');
    const signature = await app.signTransaction(
      "44'/148'/0'", // BIP44 path
      transaction.hash()
    );

    // Reconstruct signed transaction
    const keypair = /* get public key from Ledger */;
    transaction.addSignature(keypair.publicKey(), signature.signature);

    return {
      signedTxXdr: transaction.toXDR('base64'),
      signerAddress: keypair.publicKey()
    };
  }
}

Multi-Signature Wallet

class MultiSigSigner implements StellarCustomSigner {
  private signers: string[];
  private threshold: number;

  constructor(signers: string[], threshold: number) {
    this.signers = signers;
    this.threshold = threshold;
  }

  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    const transaction = xdr.Transaction.fromXDR(xdr, 'base64');
    let signatureCount = 0;

    // Collect signatures from required signers
    for (const signerSecret of this.signers.slice(0, this.threshold)) {
      const keypair = Keypair.fromSecret(signerSecret);
      transaction.sign(keypair);
      signatureCount++;
    }

    if (signatureCount < this.threshold) {
      throw new Error(`Insufficient signatures: ${signatureCount}/${this.threshold}`);
    }

    return {
      signedTxXdr: transaction.toXDR('base64'),
      signerAddress: this.signers[0] // Primary signer address
    };
  }
}

Delegated Authority Pattern

class DelegatedSigner implements StellarCustomSigner {
  private delegateSecret: string;
  private authorityPublicKey: string;

  constructor(delegateSecret: string, authorityPublicKey: string) {
    this.delegateSecret = delegateSecret;
    this.authorityPublicKey = authorityPublicKey;
  }

  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    // Sign with delegate key but identify as authority
    const delegateKeypair = Keypair.fromSecret(this.delegateSecret);
    const transaction = xdr.Transaction.fromXDR(xdr, 'base64');
    
    transaction.sign(delegateKeypair);

    return {
      signedTxXdr: transaction.toXDR('base64'),
      signerAddress: this.authorityPublicKey // Authority identity
    };
  }
}

// Usage with authority delegation
const config: StellarConfig = {
  secretKeyOrCustomSigner: new DelegatedSigner(
    'SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // Delegate secret
    'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX'  // Authority public
  ),
  publicKey: 'GXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', // Authority public
  url: 'https://soroban-testnet.stellar.org'
};

Solana Wallet Integration

Anchor Wallet Interface

// Solana uses Anchor's Wallet interface
interface AnchorWallet {
  publicKey: PublicKey;
  signTransaction<T extends Transaction | VersionedTransaction>(
    tx: T
  ): Promise<T>;
  signAllTransactions<T extends Transaction | VersionedTransaction>(
    txs: T[]
  ): Promise<T[]>;
}

Configuration

const config: SolanaConfig = {
  walletOrSecretKey: wallet, // Anchor wallet instance
  url: 'https://devnet.helius.com',
  programId: 'BMr9aui54YuxtpBzWXiFNmnr2iH6etRu7rMFJnKxjtpY'
};

const sdk = await AttestSDK.initializeSolana(config);

Phantom Wallet Integration

class PhantomWallet implements AnchorWallet {
  private phantom: any;

  constructor() {
    this.phantom = window.phantom?.solana;
    if (!this.phantom) {
      throw new Error('Phantom wallet not found');
    }
  }

  get publicKey(): PublicKey {
    return this.phantom.publicKey;
  }

  async signTransaction<T extends Transaction | VersionedTransaction>(
    transaction: T
  ): Promise<T> {
    return await this.phantom.signTransaction(transaction);
  }

  async signAllTransactions<T extends Transaction | VersionedTransaction>(
    transactions: T[]
  ): Promise<T[]> {
    return await this.phantom.signAllTransactions(transactions);
  }
}

// Usage
const wallet = new PhantomWallet();
await wallet.phantom.connect();

const config: SolanaConfig = {
  walletOrSecretKey: wallet,
  url: 'https://api.devnet.solana.com'
};

Security Considerations

Key Management

PatternSecurity LevelUse Case
Direct Secret KeyHighServer-side, automated systems
Browser WalletMediumUser-facing applications
Hardware WalletVery HighHigh-value operations
Multi-SignatureVery HighShared authority, governance
Delegated AuthorityMediumService accounts, automation

Implementation Guidelines

Do:
  • Validate signatures before submission
  • Implement proper error handling for wallet disconnections
  • Use secure communication channels for signature requests
  • Store minimal key material in memory
  • Implement timeout mechanisms for signature requests
Don’t:
  • Log or store signed transactions
  • Expose private keys in browser environments
  • Trust client-side signature validation alone
  • Hardcode secrets in application code
  • Skip signature verification

Error Handling

class RobustSigner implements StellarCustomSigner {
  private fallbackSigner?: StellarCustomSigner;

  constructor(private primarySigner: StellarCustomSigner, fallback?: StellarCustomSigner) {
    this.fallbackSigner = fallback;
  }

  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    try {
      return await this.primarySigner.signTransaction(xdr);
    } catch (error) {
      if (this.fallbackSigner && this.shouldRetryWithFallback(error)) {
        return await this.fallbackSigner.signTransaction(xdr);
      }
      throw error;
    }
  }

  private shouldRetryWithFallback(error: any): boolean {
    return error.message?.includes('wallet disconnected') ||
           error.message?.includes('user rejected') ||
           error.message?.includes('timeout');
  }
}

Testing Custom Signers

Mock Signer for Testing

class MockStellarSigner implements StellarCustomSigner {
  private keypair: Keypair;

  constructor(secretKey?: string) {
    this.keypair = secretKey 
      ? Keypair.fromSecret(secretKey)
      : Keypair.random();
  }

  async signTransaction(xdr: string): Promise<{
    signedTxXdr: string;
    signerAddress?: string;
  }> {
    const transaction = xdr.Transaction.fromXDR(xdr, 'base64');
    transaction.sign(this.keypair);

    return {
      signedTxXdr: transaction.toXDR('base64'),
      signerAddress: this.keypair.publicKey()
    };
  }

  getPublicKey(): string {
    return this.keypair.publicKey();
  }
}

// Test usage
const mockSigner = new MockStellarSigner();
const config: StellarConfig = {
  secretKeyOrCustomSigner: mockSigner,
  publicKey: mockSigner.getPublicKey(),
  url: 'https://soroban-testnet.stellar.org'
};

Integration Tests

describe('Custom Signer Integration', () => {
  it('should work with custom signer', async () => {
    const signer = new MockStellarSigner();
    const sdk = await AttestSDK.initializeStellar({
      secretKeyOrCustomSigner: signer,
      publicKey: signer.getPublicKey(),
      url: 'https://soroban-testnet.stellar.org'
    });

    const result = await sdk.registerAuthority();
    expect(result.error).toBeUndefined();
    expect(result.data).toBeDefined();
  });
});

Framework Integration Examples

React Hook

function useCustomSigner() {
  const [signer, setSigner] = useState<StellarCustomSigner | null>(null);
  const [isConnected, setIsConnected] = useState(false);

  const connectWallet = async () => {
    try {
      if (window.freighterApi) {
        const publicKey = await window.freighterApi.getPublicKey();
        const customSigner = new FreighterSigner();
        
        setSigner(customSigner);
        setIsConnected(true);
        return publicKey;
      }
    } catch (error) {
      console.error('Failed to connect wallet:', error);
    }
    return null;
  };

  const disconnect = () => {
    setSigner(null);
    setIsConnected(false);
  };

  return { signer, isConnected, connectWallet, disconnect };
}

Express.js Middleware

function createSignerMiddleware(signer: StellarCustomSigner) {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      const sdk = await AttestSDK.initializeStellar({
        secretKeyOrCustomSigner: signer,
        publicKey: req.headers['x-public-key'] as string,
        url: process.env.STELLAR_RPC_URL
      });

      req.attestSDK = sdk;
      next();
    } catch (error) {
      res.status(500).json({ error: 'Failed to initialize SDK with custom signer' });
    }
  };
}
Custom signers provide the flexibility to integrate the AttestProtocol SDK with any wallet or key management system while maintaining security and user experience standards.