Observed since 21 Oct, 09:45 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
TypeScript library for external signing on the Canton Network
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.
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.
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
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
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
# Using yarn (recommended for canton-monorepo)
yarn add @0xsend/external-signing
# Using npm
npm install @0xsend/external-signing
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);
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);
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>;
}
All signers must implement this interface:
interface ExternalSigner {
readonly id: string;
sign(data: Uint8Array): Promise<Signature>;
getPublicKeyHex(): Promise<string>;
getPublicKey(): Promise<Uint8Array>;
getSupportedAlgorithms?(): string[];
}
For development and testing:
const signer = new InMemorySigner("signer-id", "ES256");
await signer.initialize();
For production use with Ed25519 keys:
const signer = new Ed25519Signer();
// Or restore from private key
const signer = Ed25519Signer.fromPrivateKey(privateKeyHex);
Simulates HSM behavior for testing:
const signer = new MockHSMSigner(
"hsm-signer",
"key-id",
"ES256",
100 // simulated latency in ms
);
await signer.initialize("PIN-CODE");
Canton uses a server-side transaction preparation pattern:
// 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,
});
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);
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>;
}
// 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
}
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"];
}
}
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
}
}
}
External party management endpoints require admin authentication. Regular user tokens will receive 401 Unauthorized errors.
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
);
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;
}
# Unit tests
yarn test src/testxsend/external-signing.test.ts
# Integration tests (requires Canton localnet)
yarn test:integration
# Watch mode
yarn test:watch
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
});
});
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();
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
# Type checking
yarn typecheck
# Run linter
yarn lint
# Build (if needed)
yarn build
git checkout -b feature/amazing-feature)git commit -m 'Add amazing feature')git push origin feature/amazing-feature)This package is part of the Canton monorepo and follows the same license terms.
For issues and questions:
Download counts and popularity will appear here as the collector receives them.
This describes our collected data; it is not a security assessment.
Shared repository data · GitHub
Observed since 21 Oct, 09:45 UTC
No superseded metadata states collected. Unchanged observations do not create new revisions.
package:aa279a2bd2b6a39d50f0eb9cee4db7ce3fd72229e795d9b981c040ffe61c41d1