Customize the WebSocket client
Overview
The CloudWebSocketClient class is a protocol-aware WebSocket client designed specifically for MPC (Multi-Party Computation) operations. It serves as a thin wrapper around the SilentShardWebSocketClient which implements IWebSocketClient from @silencelaboratories/silent-shard-sdk.
1. CloudWebSocketClient
- Role: Protocol-aware MPC client
- Responsibility:
- MPC-specific URL construction
- Operation routing (keygen/sign/refresh)
- Customization Use Cases:
- Add authentication headers
2. SilentShardWebSocketClient implements IWebSocketClient
- Role: Low-level WebSocket implementation
- Responsibility:
- Raw WebSocket connection management
- Message queueing and asynchronous I/O
- Binary/text data transmission
- Customization Use Cases:
- Replace WebSocket library (e.g., Socket.IO, different WebSocket impl)
- Modify low-level message queueing logic
Example adding authentication headers to WebSocket connection
import { CloudWebSocketClient } from '@silencelaboratories/silent-shard-sdk';
import { createEcdsaDuoSession } from '@silencelaboratories/silent-shard-sdk/ecdsa';
import type { WebSocketAction } from '@/silent-shard-sdk/src/network/WebSocketAction';
class CustomCloudClient extends CloudWebSocketClient {
connect(opt: WebSocketAction): Promise<void> {
// You can read your client ID and access token from environment variables or a secure store.
const token = 'client-access-token-here';
const headers = {
'client-id': 'your-client-id-here',
'access-token': token,
};
return super.connect(opt, { headers });
}
}
// Then you can use the CustomCloudClient in Session objects
const customClient = new CustomCloudClient('CLOUD_NODE_URL', true); // true for WSS, false for WS
createEcdsaDuoSession({
client: customClient,
cloudVerifyingKey: '',
});
Overview
The CloudClient class is a protocol-aware WebSocket client designed specifically for MPC (Multi-Party Computation) operations. It serves as a thin wrapper around the WebSocketClient which implements WebSocketClientInterface from silent-shard-sdk.
1. CloudClient
- Role: Protocol-aware MPC client
- Responsibility:
- MPC-specific URL construction
- Operation routing (keygen/sign/refresh)
- Customization Use Cases:
- Add authentication headers
2. WebSocketClient implements WebSocketClientInterface
- Role: Low-level WebSocket implementation
- Responsibility:
- Raw WebSocket connection management
- Message queueing and asynchronous I/O
- Binary/text data transmission
- Customization Use Cases:
- Replace WebSocket library (e.g., Socket.IO, different WebSocket impl)
- Modify low-level message queueing logic
Example adding authentication headers to WebSocket connection
import 'package:silent_shard_sdk/silent_shard_sdk.dart' as sdk;
// To add custom header with each connection
class CloudSocketClient extends sdk.CloudClient {
final String userToken;
CloudSocketClient(
{required super.baseUri,
required this.userToken,
required super.isSecure,
super.webSocketClient})
: super();
Future<void> connect(String uri, [String protocol = 'duo-instance']) {
return webSocketClient.connect(uri: uri, protocol: protocol, headers: {
'Authorization': 'Bearer $userToken',
});
}
}
Future<void> setup() async {
final customCloudClient = CloudSocketClient(
baseUri: 'wss://cloud.silent-shard.com',
isSecure: true,
userToken: 'your-token',
);
final session = await sdk.createDuoEcdsaSession(
cloudClient: customCloudClient,
cloudVerifyingKeyHex: 'SERVER_PUBLIC_KEY_HEX',
storageClient: sdk.SimpleStorageClient(),
);
final keyshare = await session.keygen();
print('Keyshare: $keyshare');
}
Overview
Protocol traffic between your app and the cloud peers flows through a NetworkClient — a small transport contract the SDK drives to run each MPC step. The SDK ships a WebSocket implementation, TrioNetworkClient, and uses it automatically when you pass a WebsocketConfig to trioInitiator. You only need this guide if you want to customize that transport.
The NetworkClient contract is a fixed rhythm the session repeats for every protocol step:
| Method | Responsibility |
|---|---|
connect(action, params) | Open a channel for one step. action carries the routing (which operation, which algorithm); params is optional detail such as a keyId. |
send(bytes) / send(text) | Send one framed message to the peer. |
read() | Suspend until the next message arrives, and return its bytes. |
disconnect() | Close the channel. The client stays reusable for the next connect. |
TrioNetworkClient implements this contract over Ktor WebSockets. On connect it:
- opens a session at
wss://<host>/v3/{path}/{action}— for example/v3/trio/keygen; - negotiates the step's WebSocket subprotocol (
triofor keygen); - sends
WebsocketConfig.authenticationTokenas the first message, if you set one.
The underlying HTTP engine is created on connect and released on disconnect, so an idle client holds no transport resources.
Three ways to provide a transport
trioInitiator has three overloads, from zero-config to full control. Pick the one that matches how much of the transport you need to own.
| # | Approach | When to use |
|---|---|---|
| 1 | WebsocketConfig | The default. The SDK builds and owns the WebSocket client. |
| 2 | Subclass TrioNetworkClient | Keep the WebSocket transport, but hook connect / send / read — logging, headers, retry, or route rewriting. |
| 3 | Custom NetworkClient + TrioNetworkActionProvider | Route protocol traffic over a different transport entirely. |
1. Default WebSocket — pass a WebsocketConfig
Give the factory a WebsocketConfig and the SDK builds and owns the default TrioNetworkClient. There is nothing to implement — this is the standard path used in the session setup.
// The default transport. Pass a WebsocketConfig and the SDK builds and owns the
// default TrioNetworkClient (a Ktor WebSocket client) for you — nothing to implement.
val websocketConfig = WebsocketConfig(url = "wss://your-server.example.com")
val trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner = messageSigner,
cloudVerifyingKey = cloudPublicKey,
websocketConfig = websocketConfig,
storageClient = storageClient,
)
2. Customize the WebSocket — subclass TrioNetworkClient
Subclass TrioNetworkClient and override connect (or send / read) to add logging, inject headers, add retry/back-off, or rewrite a route — while reusing the default framing, /v3/{path}/{action} routing, subprotocol negotiation, and engine lifecycle. Pass your subclass to the trioInitiator overload that accepts a TrioNetworkClient.
Import TrioNetworkClient and the TrioNetworkAction types from com.silencelaboratories.silentshard.trio.initiator.network.
import com.silencelaboratories.silentshard.network.websocket.WebsocketConfig
import com.silencelaboratories.silentshard.trio.initiator.network.TrioNetworkAction
import com.silencelaboratories.silentshard.trio.initiator.network.TrioNetworkClient
// Subclass the default client to hook connect / send / read while reusing its framing
// and /v3/{path}/{action} routing. Here we rewrite the keygen route as an example
// and delegate every other action unchanged; you could just as easily log, add headers,
// or wrap the call in retry/back-off before calling super.
class CustomNetworkClient(websocketConfig: WebsocketConfig) : TrioNetworkClient(websocketConfig) {
override suspend fun connect(action: TrioNetworkAction, params: String?) {
val routed = when (action) {
is TrioNetworkAction.ECDSA.Keygen ->
TrioNetworkAction.ECDSA.Keygen(path = "custom-path-for-keygen")
else -> action
}
super.connect(routed, params)
}
}
// Pass your subclass to the TrioNetworkClient overload of trioInitiator.
val trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner = messageSigner,
cloudVerifyingKey = cloudPublicKey,
networkClient = CustomNetworkClient(websocketConfig),
storageClient = storageClient,
)
3. Bring your own transport — implement NetworkClient
The most control: carry protocol traffic over any channel you like. Because the transport only has to move framed bytes between the parties, this can be a shared app socket, a native bridge, or an already-open host connection — or a peer-to-peer link such as Bluetooth Low Energy (BLE), mapping connect / send / read / disconnect onto GATT characteristic writes and notifications.
A custom transport has two halves, and you supply both to the trioInitiator overload that accepts a NetworkClient + TrioNetworkActionProvider.
a. The transport — implement NetworkClient
How bytes move. The SDK drives your client with connect → send/read → disconnect for each step; the action it hands connect is the step your routing selected.
import com.silencelaboratories.silentshard.network.NetworkAction
import com.silencelaboratories.silentshard.network.NetworkClient
// How bytes move. The SDK drives your client with the same rhythm for every step:
// connect → send/read → disconnect. `action` is the concrete step your routing selected.
class CustomTransport : NetworkClient {
override suspend fun connect(action: NetworkAction, params: String?) {
// `action` is the step the provider chose; `params` may carry a keyId.
// Open a channel for it over your transport (WebSocket, app socket, BLE, …).
}
override suspend fun send(bytes: ByteArray) { /* send framed bytes to the peer */ }
override suspend fun send(text: String) { /* send framed text to the peer */ }
override suspend fun read(): ByteArray = TODO("suspend until the next peer message, then return its bytes")
override suspend fun disconnect() { /* close the channel; stay reusable for the next connect */ }
}
b. The routing — implement TrioNetworkActionProvider
How each step is addressed. It exposes one NetworkAction per operation (keygen, reconcile, refresh, hardDerivation, sign, preSign, preSignFinal, recover, export, import) — hardDerivation is ECDSA-only — and the action it supplies is exactly what your client's connect receives. Take TrioNetworkActionProvider.Default.ECDSA / .EdDSA for the standard /v3/{path}/{action} routing, or implement it yourself — every TrioNetworkAction lets you override path / action / protocol / algorithm.
import com.silencelaboratories.silentshard.trio.initiator.network.TrioNetworkAction
import com.silencelaboratories.silentshard.trio.initiator.network.TrioNetworkActionProvider
// How each step is addressed. Every TrioNetworkAction constructor takes
// path / action / protocol / algorithm — override whichever fields you want: pass one,
// spell them all out, or take the defaults, per step.
class CustomActions : TrioNetworkActionProvider {
// Override a single field (here, the route's `path`).
override val keygen = TrioNetworkAction.ECDSA.Keygen(path = "my-keygen-route")
// Spell out every field explicitly.
override val reconcile = TrioNetworkAction.ECDSA.Reconcile(
path = "trio", action = "reconcile", protocol = "-no-reconcile", algorithm = "ecdsa",
)
// Override the WebSocket subprotocol negotiated on connect.
override val refresh = TrioNetworkAction.ECDSA.KeyRefresh(protocol = "my-trio-subprotocol")
// Override the `action` segment of the route.
override val sign = TrioNetworkAction.ECDSA.Signature(action = "signature")
// Pre-signatures: preSign runs over the WebSocket, preSignFinal over HTTP POST.
override val preSign = TrioNetworkAction.ECDSA.PreSignature()
override val preSignFinal = TrioNetworkAction.ECDSA.PreSignatureFinal()
// hardDerivation is ECDSA-only.
override val hardDerivation = TrioNetworkAction.ECDSA.HardDerivation()
// …or just take the stock routing.
override val recover = TrioNetworkAction.ECDSA.Recovery()
override val export = TrioNetworkAction.ECDSA.Export()
override val import = TrioNetworkAction.ECDSA.Import()
}
// Note: `path` + `action` form the /v3/{path}/{action} route (EdDSA inserts an /eddsa
// segment) and also the HTTP POST URL that pre-signatures use; `protocol` is the
// Sec-WebSocket-Protocol header. Change these only to match your own server, and keep
// `path` + `action` valid so requests still reach the cluster.
c. Wire the two halves into trioInitiator
// Pass both halves — your transport and your routing — to the custom-transport
// overload of trioInitiator.
val trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner = messageSigner,
cloudVerifyingKey = cloudPublicKey,
storageClient = storageClient,
networkClient = CustomTransport(),
actionProvider = CustomActions(), // or TrioNetworkActionProvider.Default.ECDSA
)
Most protocol steps run over the open WebSocket; a few request-response steps run over HTTP POST. Trio uses the POST path for pre-signatures — the preSignFinal step — so a fully custom transport that supports pre-signatures must also implement it (PostRequestCapable); the default TrioNetworkClient already does. A transport that never issues pre-signatures can leave it out.
Overview
Protocol traffic between your app and the cloud peers flows through a NetworkClient — a small transport contract the SDK drives to run each MPC step. The SDK ships a WebSocket implementation, TrioNetworkClient, and uses it automatically when you pass a WebsocketConfig to trioInitiator. You only need this guide if you want to customize that transport.
The NetworkClient contract is a fixed rhythm the session repeats for every protocol step (every method is async throws):
| Method | Responsibility |
|---|---|
connect(action:params:) | Open a channel for one step. action carries the routing (which operation, which algorithm); params is optional detail such as a keyId. |
send(bytes:) / send(text:) | Send one framed message to the peer. |
read() | Suspend until the next message arrives, and return its Data. |
disconnect() | Close the channel. The client stays reusable for the next connect. |
TrioNetworkClient implements this contract over URLSessionWebSocketTask. On connect it:
- opens a task at
wss://<host>/v3/{path}/{action}— for example/v3/trio/keygen; - negotiates the step's WebSocket subprotocol (
triofor keygen); - sends
WebsocketConfig.authenticationTokenas the first message, if you set one.
The underlying URLSession is created on connect and released on disconnect, so an idle client holds no transport resources.
Three ways to provide a transport
trioInitiator has three overloads, from zero-config to full control. Pick the one that matches how much of the transport you need to own.
| # | Approach | When to use |
|---|---|---|
| 1 | WebsocketConfig | The default. The SDK builds and owns the WebSocket client. |
| 2 | Subclass TrioNetworkClient | Keep the WebSocket transport, but hook connect / send / read — logging, headers, retry, or route rewriting. |
| 3 | Custom NetworkClient + TrioNetworkActionProvider | Route protocol traffic over a different transport entirely. |
1. Default WebSocket — pass a WebsocketConfig
Give the factory a WebsocketConfig and the SDK builds and owns the default TrioNetworkClient. There is nothing to implement — this is the standard path used in the session setup.
// The default transport. Pass a WebsocketConfig and the SDK builds and owns the
// default TrioNetworkClient (a URLSession WebSocket client) for you — nothing to implement.
let websocketConfig = WebsocketConfig(url: "wss://your-server.example.com")
let trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner: messageSigner,
cloudVerifyingKey: cloudPublicKey,
websocketConfig: websocketConfig,
storageClient: storageClient
)
2. Customize the WebSocket — subclass TrioNetworkClient
Subclass TrioNetworkClient and override connect (or send / read) to add logging, inject headers, add retry/back-off, or rewrite a route — while reusing the default framing, /v3/{path}/{action} routing, subprotocol negotiation, and session lifecycle. Pass your subclass to the trioInitiator overload that accepts a TrioNetworkClient.
TrioNetworkClient and the TrioNetworkAction types are exported from the trio module (import trio).
import trio
// Subclass the default client to hook connect / send / read while reusing its framing
// and /v3/{path}/{action} routing. Here we rewrite the keygen route as an example
// and delegate every other action unchanged; you could just as easily log, add headers,
// or wrap the call in retry/back-off before calling super.
class CustomNetworkClient: TrioNetworkClient {
override func connect(websocketAction: TrioNetworkAction, params: String?) async throws {
let routed: TrioNetworkAction
switch websocketAction {
case is TrioAction.ECDSA.Keygen:
routed = TrioAction.ECDSA.Keygen(path: "custom-path-for-keygen")
default:
routed = websocketAction
}
try await super.connect(websocketAction: routed, params: params)
}
}
// Pass your subclass to the TrioNetworkClient overload of trioInitiator.
let trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner: messageSigner,
cloudVerifyingKey: cloudPublicKey,
networkClient: CustomNetworkClient(websocketConfig: websocketConfig),
storageClient: storageClient
)
3. Bring your own transport — implement NetworkClient
The most control: carry protocol traffic over any channel you like. Because the transport only has to move framed bytes between the parties, this can be a shared app socket, a native bridge, or an already-open host connection — or a peer-to-peer link such as Bluetooth Low Energy (BLE), mapping connect / send / read / disconnect onto GATT characteristic writes and notifications.
A custom transport has two halves, and you supply both to the trioInitiator overload that accepts a NetworkClient + TrioNetworkActionProvider.
a. The transport — implement NetworkClient
How bytes move. The SDK drives your client with connect → send/read → disconnect for each step; the action it hands connect is the step your routing selected.
import trio
// How bytes move. The SDK drives your client with the same rhythm for every step:
// connect → send/read → disconnect. `action` is the concrete step your routing selected.
class CustomTransport: NetworkClient {
func connect(action: NetworkAction, params: String?) async throws {
// `action` is the step the provider chose; `params` may carry a keyId.
// Open a channel for it over your transport (WebSocket, app socket, BLE, …).
}
func send(bytes: Data) async throws { /* send framed bytes to the peer */ }
func send(text: String) async throws { /* send framed text to the peer */ }
func read() async throws -> Data { Data() /* suspend until the next peer message, then return its bytes */ }
func disconnect() async throws { /* close the channel; stay reusable for the next connect */ }
// sendPostRequest has a default that throws .postRequestUnsupported; implement it only
// if your transport supports pre-signatures (trio runs preSignFinal over HTTP POST).
}
b. The routing — implement TrioNetworkActionProvider
How each step is addressed. It exposes one NetworkAction per operation (keygen, reconcile, refresh, hardDerivation, sign, preSign, preSignFinal, recover, export, import) — hardDerivation is ECDSA-only — and the action it supplies is exactly what your client's connect receives. Take the built-in .ecdsa / .eddsa provider for the standard /v3/{path}/{action} routing, or implement it yourself — every TrioNetworkAction lets you override path / action / protocol / algorithm.
import trio
// How each step is addressed. Every TrioAction.ECDSA.* initializer takes
// path / action / protocol / algorithm — override whichever fields you want: pass one,
// spell them all out, or take the defaults, per step.
struct CustomActions: TrioNetworkActionProvider {
// Override a single field (here, the route's `path`).
let keygen: NetworkAction = TrioAction.ECDSA.Keygen(path: "my-keygen-route")
// Spell out every field explicitly.
let reconcile: NetworkAction = TrioAction.ECDSA.Reconcile(
path: "trio", action: "reconcile", protocol: "-no-reconcile", algorithm: "ecdsa"
)
// Override the WebSocket subprotocol negotiated on connect.
let refresh: NetworkAction = TrioAction.ECDSA.KeyRefresh(protocol: "my-trio-subprotocol")
// Override the `action` segment of the route.
let sign: NetworkAction = TrioAction.ECDSA.Signature(action: "signature")
// Pre-signatures: preSign runs over the WebSocket, preSignFinal over HTTP POST.
let preSign: NetworkAction = TrioAction.ECDSA.PreSignature()
let preSignFinal: NetworkAction = TrioAction.ECDSA.PreSignatureFinal()
// hardDerivation is ECDSA-only.
let hardDerivation: NetworkAction = TrioAction.ECDSA.HardDerivation()
// …or just take the stock routing.
let recover: NetworkAction = TrioAction.ECDSA.Recovery()
let export: NetworkAction = TrioAction.ECDSA.Export()
let `import`: NetworkAction = TrioAction.ECDSA.Import()
}
// Note: `path` + `action` form the /v3/{path}/{action} route (EdDSA inserts an /eddsa
// segment) and also the HTTP POST URL that pre-signatures use; `protocol` is the
// Sec-WebSocket-Protocol header. Change these only to match your own server, and keep
// `path` + `action` valid so requests still reach the cluster.
c. Wire the two halves into trioInitiator
// Pass both halves — your transport and your routing — to the custom-transport
// overload of trioInitiator.
let trioSession = SilentShard.ECDSA.trioInitiator(
messageSigner: messageSigner,
cloudVerifyingKey: cloudPublicKey,
storageClient: storageClient,
networkClient: CustomTransport(),
actionProvider: CustomActions() // or .ecdsa
)
Most protocol steps run over the open WebSocket; a few request-response steps run over HTTP POST (sendPostRequest). Trio's pre-signature flow finalizes over that POST path (preSignFinal), so a fully custom trio transport must implement sendPostRequest if it supports pre-signatures — its default reports the step unsupported (postRequestUnsupported). A transport that never issues pre-signatures can leave the default in place.