npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@0xsend/external-signing

v0.2.2

Published

TypeScript library for external signing on the Canton Network

Readme

Canton External Signing

A TypeScript library for integrating external signing mechanisms with the Canton Network, enabling secure transaction signing using external key management systems, hardware security modules (HSMs), or software wallets.

Overview

Canton External Signing provides a flexible framework for signing Canton Network transactions without exposing private keys to the application. This is essential for production deployments where key security is paramount.

Key Features

  • 🔐 Secure Key Management - Keep private keys in HSMs, secure enclaves, or external wallets
  • 🔑 Multiple Signer Support - Register and manage multiple signing identities
  • Async Signing - Non-blocking signature operations with timeout support
  • 🛡️ Type Safety - Full TypeScript support with strict typing
  • 🧪 Well Tested - Comprehensive test suite included
  • 🔌 Extensible - Easy to implement custom signers
  • 📝 Prepare/Submit Pattern - Canton handles transaction building, client only signs

Architecture

Overall System Architecture

graph TB
    subgraph "Browser/Client"
        WA[Web App]
        ES[External Signer]
        KS[Key Storage]
    end

    subgraph "Canton Network"
        LED[Ledger API]
        VAL[Validator]
        SYNC[Synchronizer]
    end

    WA -->|1. Prepare Tx| LED
    LED -->|2. Return Hash| WA
    WA -->|3. Sign Hash| ES
    ES -->|4. Access Key| KS
    KS -->|5. Return Signature| ES
    ES -->|6. Return Signature| WA
    WA -->|7. Submit Signed Tx| LED
    LED -->|8. Validate| VAL
    VAL -->|9. Commit| SYNC

Signing Flow Sequence

sequenceDiagram
    participant User
    participant WebApp
    participant Signer
    participant Canton

    User->>WebApp: Initiate Transaction
    WebApp->>Canton: Prepare Command
    Canton->>WebApp: Return Tx Hash
    WebApp->>Signer: Request Signature
    Signer->>Signer: Generate Signature
    Signer->>WebApp: Return Signature
    WebApp->>Canton: Submit Signed Tx
    Canton->>WebApp: Confirmation
    WebApp->>User: Show Result

Component Architecture

graph LR
    subgraph "External Signing Package"
        API[API Client]
        SM[Signer Manager]
        CS[Crypto Services]
        ST[Storage]
    end

    subgraph "Signer Implementations"
        MEM[Memory Signer]
        HSM[HSM Signer]
        HW[Hardware Wallet]
        ED[Ed25519 Signer]
    end

    API --> SM
    SM --> CS
    SM --> ST
    CS --> MEM
    CS --> HSM
    CS --> HW
    CS --> ED

Installation

# Using yarn (recommended for canton-monorepo)
yarn add @0xsend/external-signing

# Using npm
npm install @0xsend/external-signing

Quick Start

Basic Example

import {
  CantonExternalClient,
  InMemorySigner,
  createTestSigner,
} from "@0xsend/external-signing";

// 1. Create a client instance
const client = new CantonExternalClient({
  ledgerApiUrl: "https://canton.example.com/",
  token: "your-jwt-token",
});

// 2. Create and register a signer
const signer = await createTestSigner("my-signer", "ES256");
client.registerSigner(signer);

// 3. Create a DAML command
const command = {
  template: MyTemplate,
  argument: {
    owner: "alice",
    value: 100,
  },
  party: "alice::1234...",
};

// 4. Submit with external signing
const result = await client.submitCreateCommand(command, "my-signer", {
  verifySignature: true,
});

console.log("Transaction ID:", result.transactionId);

Using the Prepare/Submit Pattern

For more control over the signing process, use the prepare/submit pattern:

import { CantonExternalClient, Ed25519Signer } from "@0xsend/external-signing";

// Create client with external party API
const client = new CantonExternalClient({
  validatorUrl: "https://validator.example.com/",
  authToken: "your-auth-token",
});

// Create an Ed25519 signer
const signer = new Ed25519Signer();

// Create external party
const party = await client.createExternalParty("alice", signer);
console.log("Party ID:", party.partyId);

// Execute a transfer with prepare/submit
const transferParams = {
  sender_party_id: party.partyId,
  receiver_party_id: "bob::5678...",
  amount: "100.00",
  transfer_preapproval_contract_id: "contract-id",
};

const result = await client.executeTransferPreapproval(transferParams, signer);

API Reference

CantonExternalClient

The main client for interacting with Canton using external signers.

class CantonExternalClient {
  constructor(config: CantonLedgerConfig);

  // Signer management
  registerSigner(signer: ExternalSigner): void;
  unregisterSigner(signerId: string): boolean;
  getSigner(signerId: string): ExternalSigner | undefined;
  listSigners(): string[];

  // Command submission
  submitCreateCommand<T>(
    command: DamlCommand<T>,
    signerId: string,
    options?: ExternalSigningOptions
  ): Promise<CommandSubmissionResult>;

  submitExerciseCommand<T, R>(
    command: DamlExerciseCommand<T, R>,
    signerId: string,
    options?: ExternalSigningOptions
  ): Promise<CommandSubmissionResult>;
}

ExternalSigner Interface

All signers must implement this interface:

interface ExternalSigner {
  readonly id: string;

  sign(data: Uint8Array): Promise<Signature>;
  getPublicKeyHex(): Promise<string>;
  getPublicKey(): Promise<Uint8Array>;

  getSupportedAlgorithms?(): string[];
}

Built-in Signers

InMemorySigner

For development and testing:

const signer = new InMemorySigner("signer-id", "ES256");
await signer.initialize();

Ed25519Signer

For production use with Ed25519 keys:

const signer = new Ed25519Signer();
// Or restore from private key
const signer = Ed25519Signer.fromPrivateKey(privateKeyHex);

MockHSMSigner

Simulates HSM behavior for testing:

const signer = new MockHSMSigner(
  "hsm-signer",
  "key-id",
  "ES256",
  100 // simulated latency in ms
);
await signer.initialize("PIN-CODE");

Core Concepts

Canton's Prepare/Submit Pattern

Canton uses a server-side transaction preparation pattern:

  1. Prepare: Canton builds the transaction and returns a hash
  2. Sign: Client signs only the hash (no serialization needed)
  3. Submit: Client submits the signature back to Canton
// Canton prepares the transaction
const prepared = await api.prepareTransferPreapproval(params);
// prepared = { transaction: "<base64>", tx_hash: "<hex>" }

// Client signs the hash
const signature = await signer.sign(hexToBytes(prepared.tx_hash));

// Submit the signature
await api.submitTransferPreapproval({
  party_id: params.sender_party_id,
  transaction: prepared.transaction,
  signed_tx_hash: signature.value,
  public_key: publicKeyHex,
});

External Party Management

The library provides complete external party lifecycle management:

// Create a new party with generated Ed25519 keys
const alice = await manager.createParty("alice");

// Import existing party from backup
const bob = await manager.importParty(
  "bob",
  privateKeyHex,
  knownPartyId // optional
);

// List all managed parties
const parties = await manager.listParties();

// Export private key for backup
const privateKey = await manager.exportPartyKey(alice.partyId);

Integration Guide

Web Application Integration

React Example

import { useState, useEffect } from "react";
import { CantonExternalClient, Ed25519Signer } from "@0xsend/external-signing";

function WalletComponent() {
  const [client, setClient] = useState<CantonExternalClient>();
  const [signer, setSigner] = useState<Ed25519Signer>();

  useEffect(() => {
    // Initialize client
    const client = new CantonExternalClient({
      ledgerApiUrl: process.env.CANTON_LEDGER_URL,
      token: localStorage.getItem("auth-token"),
    });

    // Create or restore signer
    const signer = new Ed25519Signer();
    client.registerSigner(signer);

    setClient(client);
    setSigner(signer);
  }, []);

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

    const command = {
      template: TransferTemplate,
      argument: {
        /* ... */
      },
      party: getCurrentParty(),
    };

    try {
      const result = await client.submitCreateCommand(command, signer.id);
      console.log("Transfer successful:", result.transactionId);
    } catch (error) {
      console.error("Transfer failed:", error);
    }
  };

  return <button onClick={handleTransfer}>Send Transfer</button>;
}

Next.js Integration

// app/lib/canton-client.ts
import { CantonExternalClient } from "@0xsend/external-signing";

let client: CantonExternalClient;

export function getCantonClient() {
  if (!client) {
    client = new CantonExternalClient({
      ledgerApiUrl: process.env.NEXT_PUBLIC_CANTON_URL!,
      // Token handled by middleware
    });
  }
  return client;
}

// app/actions/transfer.ts
("use server");

import { getCantonClient } from "@/lib/canton-client";
import { Ed25519Signer } from "@0xsend/external-signing";

export async function executeTransfer(amount: string, recipient: string) {
  const client = getCantonClient();
  const signer = new Ed25519Signer();

  // ... implement transfer logic
}

Custom Signer Implementation

Implement your own signer for custom requirements:

import { ExternalSigner, Signature } from "@0xsend/external-signing";

class MyCustomSigner implements ExternalSigner {
  readonly id: string;

  constructor(id: string) {
    this.id = id;
  }

  async sign(data: Uint8Array): Promise<Signature> {
    // Your signing logic here
    // e.g., call to HSM, hardware wallet, etc.

    return {
      algorithm: "ES256",
      value: signatureHex,
      publicKey: publicKeyHex,
      keyId: this.id,
    };
  }

  async getPublicKeyHex(): Promise<string> {
    // Return hex-encoded public key
  }

  async getPublicKey(): Promise<Uint8Array> {
    // Return raw public key bytes
  }

  getSupportedAlgorithms(): string[] {
    return ["ES256", "ES384"];
  }
}

Error Handling

The library provides specific error types for different scenarios:

import {
  ExternalSigningError,
  ExternalSigningErrorCode,
} from "@0xsend/external-signing";

try {
  await client.submitCreateCommand(command, signerId);
} catch (error) {
  if (error instanceof ExternalSigningError) {
    switch (error.code) {
      case ExternalSigningErrorCode.SIGNER_NOT_FOUND:
        console.error("Signer not registered");
        break;
      case ExternalSigningErrorCode.SIGNING_FAILED:
        console.error("Signing operation failed");
        break;
      case ExternalSigningErrorCode.TIMEOUT:
        console.error("Signing timed out");
        break;
      // ... handle other error codes
    }
  }
}

Authentication and API Requirements

Important: Admin Authentication Required

External party management endpoints require admin authentication. Regular user tokens will receive 401 Unauthorized errors.

JWT Token Requirements

For Canton localnet with JWKS authentication:

// Get admin token from Keycloak using client credentials
async function getAdminToken(): Promise<string> {
  const response = await fetch(
    "http://auth-dev.cantonwallet.com/realms/localnet/protocol/openid-connect/token",
    {
      method: "POST",
      headers: { "Content-Type": "application/x-www-form-urlencoded" },
      body: new URLSearchParams({
        grant_type: "client_credentials",
        client_id: "localnet-validator",
        client_secret: process.env.KEYCLOAK_CLIENT_SECRET!,
        audience:
          "https://ledger-api.canton.local https://canton.network.global",
      }),
    }
  );

  const data = await response.json();
  return data.access_token;
}

// Use admin token for external party operations
const adminToken = await getAdminToken();
const manager = new ExternalPartyManager(
  {
    validatorUrl: "http://localhost:45003",
    authToken: adminToken,
  },
  storage
);

Legacy HMAC Authentication (Development Only)

For backwards compatibility with unsafe HMAC tokens:

import { SignJWT } from "jose";

// Only for development environments with SPLICE_APP_UI_UNSAFE=true
async function generateUnsafeAdminToken(): Promise<string> {
  const secret = new TextEncoder().encode("unsafe");

  const token = await new SignJWT({
    sub: "ledger-api-user",
    aud: ["https://ledger-api.canton.local", "https://canton.network.global"],
    scope: "daml_ledger_api",
  })
    .setProtectedHeader({ alg: "HS256" })
    .setIssuedAt()
    .setExpirationTime("24h")
    .sign(secret);

  return token;
}

Security Considerations

Best Practices

  1. Never expose private keys - Keep them in secure storage
  2. Use hardware security - HSMs or secure enclaves for production
  3. Implement key rotation - Regular key updates
  4. Audit signing requests - Log all signature operations
  5. Validate inputs - Always validate transaction data before signing

Production Checklist

  • [ ] Private keys stored in HSM or secure enclave
  • [ ] Signing operations require authentication
  • [ ] Transaction limits implemented
  • [ ] Audit logging enabled
  • [ ] Key backup and recovery procedures
  • [ ] Regular security audits

Testing

Running Tests

# Unit tests
yarn test src/testxsend/external-signing.test.ts

# Integration tests (requires Canton localnet)
yarn test:integration

# Watch mode
yarn test:watch

Writing Tests

import { createTestSigner } from "@0xsend/external-signing";

describe("My Canton Integration", () => {
  it("should sign and submit transaction", async () => {
    const signer = await createTestSigner("test-signer");
    const client = new CantonExternalClient({
      ledgerApiUrl: "http://localhost:5001/",
    });

    client.registerSigner(signer);

    // ... test your integration
  });
});

Troubleshooting

Common Issues

Ledger URL must end with '/'

// ❌ Wrong
ledgerApiUrl: "http://localhost:5001";

// ✅ Correct
ledgerApiUrl: "http://localhost:5001/";

Signer not found error

// Make sure to register the signer first
client.registerSigner(signer);

Timeout errors

// Increase timeout for slow signers
await client.submitCreateCommand(command, signerId, {
  signingTimeoutMs: 30000, // 30 seconds
});

401 Unauthorized for external party operations

// Use admin token with 'ledger-api-user' subject
const adminToken = await generateAdminToken();

Development

Project Structure

packages/canton-external-signing/
├── src/
│   ├── api/              # Canton API integrations
│   ├── client/           # Client implementations
│   ├── party/            # External party management
│   ├── signers/          # Signer implementations
│   ├── storage/          # Storage interfaces
│   ├── types/            # TypeScript types
│   ├── utils/            # Utilities
│   └── test/             # Test files
├── README.md
└── package.json

Building

# Type checking
yarn typecheck

# Run linter
yarn lint

# Build (if needed)
yarn build

Contributing

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

License

This package is part of the Canton monorepo and follows the same license terms.

Support

For issues and questions: