Branded Types
The nominal branded primitives — Slug, EvmAddress, SvmAddress, ChainId, AssetId, ChainVM, VmToken, NetworkLike and Settling — and constructing them from plain values with an `as` cast.
Several of the library's identifiers are plain strings and numbers at runtime but carry a distinct type — a Slug is not interchangeable with any old string, and a ChainId is not any old number. This is nominal branding: a compile-time tag that stops you from passing, say, a raw address where a slug is expected, even though both are strings once the code runs.
You rarely construct these yourself — they come back already branded from the library (address.publicKey is an EvmAddress, network.chainId is a ChainId). You only need the as cast in the one direction the compiler cannot infer: handing a plain literal into an API that asks for a branded value.
What branding is
A branded type intersects the underlying primitive with a phantom marker field:
type Brand<T, B extends string> = T & { readonly __brand: B };
type Slug = Brand<string, "Slug">;
type ChainId = Brand<number, "ChainId">;The __brand field never exists at runtime — it is erased with all other types. Its only job is to make Slug and string structurally different to the type checker, so an accidental swap is a compile error rather than a silent bug.
The Brand<T, B> helper itself is an internal detail and is not exported from wative-core. The branded aliases below are exported; you compose them with as, you do not import Brand.
The branded primitives
These five are true nominal brands over a single primitive.
| Type | Underlying primitive | Purpose |
|---|---|---|
Slug | string | A validated, URL-safe identifier for a persisted record — an account, network, or asset key. Used as the lookup key in bySlug() and record storage. |
EvmAddress | string | A checksummed 0x EVM account address. What address.publicKey returns on an EVM address. |
SvmAddress | string | A base58 Solana account address. What address.publicKey returns on an SVM address. |
ChainId | number | An EVM-style numeric chain id (for example 1 for Ethereum mainnet). |
AssetId | number | The numeric id of a tracked asset within a workspace. |
The utility types
The remaining four are exported alongside the brands but are plain type aliases — a union or an interface, not a Brand<T, B>.
| Type | Definition | Purpose |
|---|---|---|
ChainVM | "evm" | "svm" | "sui" | The closed set of built-in virtual machines. "sui" is a reserved placeholder; its implementation is not shipped yet. |
VmToken | ChainVM | (string & {}) | The open vm token. The (string & {}) arm keeps evm / svm / sui autocompleting while still accepting any string a third party registers a dialect for. Used at construction and boundary sites. |
NetworkLike | Network | ChainId | string | Anything that can identify a network — resolved internally to a Slug. |
Settling<T> | interface (see below) | The live-async pattern returned where a caller wants a settling object rather than a raw Promise. |
NetworkLike resolution
Wherever an API takes a NetworkLike, the value is resolved to a network slug by this order:
| Input | Resolved by |
|---|---|
a Network | its network.slug |
a ChainId (number) | looked up by chainId — workspace overrides, then built-ins |
a hex string (e.g. "0x1", "0x156") | parsed as a hex chain id, then looked up |
a slug string (e.g. "ethereum", "base") | treated as the slug directly |
If nothing matches, the call throws WativeError("UNSUPPORTED_NETWORK", …).
Settling<T>
interface Settling<T> {
readonly settled: boolean;
readonly value: T | null;
confirm(): Promise<T>;
abort(): void;
}Settling<T> is handed back by live-async methods such as address.refreshBalances(). Read value for the latest resolved value, await confirm() for the settled result, or abort() to stop. TransactionTracker is a richer Settling<TransactionReceipt>.
const settling = evm.refreshBalances(); // Settling<readonly AssetBalance[]>
const balances = await settling.confirm(); // the settled valueConstructing a branded value with as
When you already hold a plain string or number and need to pass it where a branded type is expected, assert the brand with as. This is the idiom used throughout the test suite:
import { Network } from "wative-core";
import type { Slug, ChainId, AssetId } from "wative-core";
// A plain string literal where a Slug is expected.
const base = ws.networks.bySlug("base" as Slug);
// A numeric chain id where a ChainId is expected.
const net = new Network({
slug: "my-testnet",
name: "My Testnet",
chainId: 7_654_321 as ChainId,
rpcUrl: "https://rpc.example",
nativeCurrency: { name: "Test", symbol: "TST", decimals: 18 },
vm: "evm",
});
// A numeric asset id.
await ws.dropAsset(50_000 as AssetId);A small helper reads well when you cast the same brand repeatedly:
const slug = (s: string): Slug => s as Slug;
ws.networks.bySlug(slug("arbitrum-sepolia"));as is an assertion, not a validator — it tells the compiler to trust you, and performs no runtime check. Only brand values you know are well-formed. The library still validates them at the boundary: an out-of-range chainId or a malformed slug is rejected with a WativeError regardless of the cast.
For VmToken, any string is assignable, but a Network whose vm is a token with no registered dialect is refused at construction — register a dialect first.