A node speaks two RPC protocols, on two separate listeners. wRPC is a WebSocket, and it is what the SDKs on this page use. gRPC is the other one — same method set, different wire format, covered further down.
The wRPC client opens a WebSocket to a node. While it’s connected, every RPC method is callable and notifications stream in. You can point it at a node URL you control, or hand it a Resolver and let it find a public one.
All three SDKs wrap the same wRPC client from rusty-kaspa, so the options below mean the same thing everywhere — only the spelling changes.
Connect via the Resolver
The default for most applications. No URL, no node of your own:
use kaspa_wrpc_client::{
prelude::{NetworkId, NetworkType},
KaspaRpcClient, Resolver, WrpcEncoding,
};
let client = KaspaRpcClient::new(
WrpcEncoding::Borsh,
None, // no url
Some(Resolver::default()),
Some(NetworkId::new(NetworkType::Mainnet)),
None,
)?;
client.connect(None).await?; const { RpcClient, Resolver } = kaspa;
const rpc = new RpcClient({
resolver: new Resolver(),
networkId: 'mainnet'
});
await rpc.connect(); from kaspa import Resolver, RpcClient
client = RpcClient(resolver=Resolver(), network_id="mainnet")
await client.connect()For security-critical applications, connect to your own node instead. A public node can lie to you about balances and chain state. See Run a Node.
Connect to a known node
Pass a URL and skip the resolver. The node must have wRPC enabled — start it with --rpclisten-borsh:
let client = KaspaRpcClient::new(
WrpcEncoding::Borsh,
Some("ws://127.0.0.1:17110"),
None, // no resolver
Some(NetworkId::new(NetworkType::Mainnet)),
None,
)?;
client.connect(None).await?; const rpc = new RpcClient({
url: 'ws://127.0.0.1:17110',
encoding: kaspa.Encoding.Borsh,
networkId: 'mainnet'
});
await rpc.connect(); from kaspa import RpcClient
client = RpcClient(
url="ws://127.0.0.1:17110",
network_id="mainnet",
encoding="borsh",
)
await client.connect()URL schemes are ws:// (plaintext) and wss:// (TLS). Default wRPC ports are 17110 (borsh) and 18110 (json) on mainnet — see Network Parameters.
Connection options
connect() takes a few behavioural overrides:
use std::time::Duration;
use kaspa_wrpc_client::client::{ConnectOptions, ConnectStrategy};
client
.connect(Some(ConnectOptions {
block_async_connect: true,
connect_timeout: Some(Duration::from_millis(5_000)),
strategy: ConnectStrategy::Fallback,
..Default::default()
}))
.await?; await rpc.connect({
blockAsyncConnect: true,
strategy: 'fallback',
timeoutDuration: 5000,
retryInterval: 1000
}); await client.connect(
block_async_connect=True,
strategy="fallback",
timeout_duration=5_000,
retry_interval=1_000,
)block_async_connect—true(the default) makesconnect()await until the socket is open. Set itfalseto return immediately and let the connection complete in the background; poll the connected flag or listen for theconnectevent to know when it’s ready.strategy—retry(the default) loops until a connection succeeds, pausing between attempts;fallbackgives up on the first failure. Applies to both URL-based and resolver-driven clients.connect_timeout/timeoutDuration— a per-attempt ceiling in milliseconds, not an overall wall-clock budget. Underretrythere is no overall ceiling.retry_interval— delay between attempts, in milliseconds.
There’s also a per-attempt URL override, which lets you retarget a long-lived client without rebuilding it.
Inspecting the live client
println!("{}", client.is_connected());
println!("{:?}", client.url()); // resolved or supplied node URL
println!("{:?}", client.encoding());
println!("{:?}", client.node_descriptor()); console.log(rpc.isConnected);
console.log(rpc.url);
console.log(rpc.encoding);
console.log(rpc.nodeId); // resolver-supplied node UID print(client.is_connected)
print(client.url) # resolved or supplied node URL, or None
print(client.encoding) # "borsh" or "json"
print(client.node_id) # resolver-supplied node UID; None for direct URLs
print(client.resolver) # the Resolver instance, or NoneBorsh vs JSON encoding
Borsh — the default — is a compact binary format the node uses natively. Use JSON only to inspect raw frames in a tool that doesn’t speak Borsh, or when targeting a node that hasn’t enabled the Borsh listener.
The two listeners are separate: a node started with only --rpclisten-borsh will refuse a JSON client, and the ports differ.
Reconnects
If the WebSocket drops mid-session, the client reconnects on its own. Calls made during the gap fail; calls made after a successful reconnect work normally.
To stop reconnect attempts, disconnect explicitly — or use the fallback strategy, which gives up after one failed reconnect instead of looping. To observe disruptions, register a listener for the client’s connect and disconnect events — the same event channel node notifications arrive on.
gRPC
The node’s other RPC listener. It carries the same method set as wRPC and it is on by default, bound to loopback on port 16110 — whereas wRPC has to be switched on explicitly. If you are talking to a node you run, on the same machine, gRPC needs no node-side configuration at all.
| wRPC | gRPC | |
|---|---|---|
| Transport | WebSocket | HTTP/2 |
| Default port (mainnet) | 17110 borsh, 18110 json | 16110 |
| On by default | No — --rpclisten-borsh / --rpclisten-json | Yes, on loopback |
| Official clients | Rust, WASM (JS/TS), Python | Rust |
| Works in a browser | Yes | No (needs a grpc-web proxy) |
| Resolver / public nodes | Yes | No — bring your own node |
| Typical users | wallets, dApps, browser clients | miners, stratum bridges, indexers, anything colocated with a node |
Neither is a subset of the other in capability. Pick wRPC if you need a public node or a browser; pick gRPC if you own the node and want the transport every other language can generate a client for.
Rust
kaspa-grpc-client implements the same RpcApi trait the wRPC client does, so everything downstream of the connection is identical — only the constructor changes.
use kaspa_grpc_client::GrpcClient;
use kaspa_rpc_core::api::rpc::RpcApi;
// The grpc:// scheme is required; the client rejects a bare host:port.
let client = GrpcClient::connect("grpc://127.0.0.1:16110".to_string()).await?;
let info = client.get_server_info().await?;
println!("{} · synced: {}", info.server_version, info.is_synced);
client.disconnect().await?; [dependencies]
kaspa-grpc-client = { git = "https://github.com/kaspanet/rusty-kaspa" }
kaspa-rpc-core = { git = "https://github.com/kaspanet/rusty-kaspa" }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] } connect() is the one-argument form. For notifications, a reconnect loop, or a call timeout, use connect_with_args:
use kaspa_grpc_client::GrpcClient;
use kaspa_rpc_core::notify::mode::NotificationMode;
let client = GrpcClient::connect_with_args(
NotificationMode::MultiListeners, // Direct = one channel; MultiListeners = RpcApi listeners
"grpc://127.0.0.1:16110".to_string(),
None, // subscription context
true, // reconnect on drop
None, // connection event sender
false, // override handle stop notify
Some(500_000), // timeout, ms
Default::default(), // counters
)
.await?; Other languages
There is no official gRPC client outside Rust — you generate one from the node’s own protobuf definitions:
rpc.proto— every request and response messagemessages.proto— the service, and the envelopes that wrap the messages
Both are in package protowire. Point your generator at them and you have a client in Go, Python, Java, C#, C++, Node, or anything else protoc supports:
protoc --go_out=. --go-grpc_out=. \
--proto_path=rpc/grpc/core/proto \
rpc/grpc/core/proto/rpc.proto rpc/grpc/core/proto/messages.proto pip install grpcio grpcio-tools
python -m grpc_tools.protoc \
-I rpc/grpc/core/proto \
--python_out=. --grpc_python_out=. \
rpc/grpc/core/proto/rpc.proto rpc/grpc/core/proto/messages.proto npm install @grpc/grpc-js @grpc/proto-loader
# @grpc/proto-loader reads the .proto files at runtime — no codegen step.The service is not what you expect, and this is where most people get stuck. Kaspa’s gRPC surface is a single method:
service RPC {
rpc MessageStream (stream KaspadRequest) returns (stream KaspadResponse) {}
} One bidirectional stream, not one gRPC method per RPC call. So:
- Every call is a
KaspadRequestwhosepayloadis aoneof— you set exactly one field, e.g.getBlockDagInfoRequest, and the field you set is the method name. - Responses come back on the same stream, in whatever order the node finishes them. Set the request’s
idfield and match it against the response’sid— without that you cannot tell two concurrent calls apart. - Notifications arrive on that same stream too, as responses with no matching request. Subscribe by sending a
notify*Request(e.g.notifyBlockAddedRequest), then read notification payloads as they come. - Errors are in the response’s
errorfield, not in the gRPC status. A call can fail while the stream stays perfectly healthy.
If you want a worked reference for the correlation and subscription logic before writing your own, kaspa-grpc-client is the implementation the node’s own tooling uses, and rpc/grpc/examples/simple_client is the smallest end-to-end version of it.
Where to next
- Resolver — how public node discovery actually works.
- RPC Calls — run methods against mainnet directly from the page.
- Transactions — the first thing most people do with a connection.
- Run a Node — enabling the listeners this page connects to.