Creates an instance of the Salt SDK
Optionalparams: {The constructor parameters
OptionalauthToken?: stringThe authentication token for the Salt SDK. See Salt.authenticate for the full authentication flow, or Salt#setAuthToken to set a pre-existing token after calling the constructor.
This is the short-lived access token: it expires 20 minutes after the
server issues it, and on its own it cannot be renewed. An instance
constructed with only an authToken therefore stops working at that point,
in one of two ways — API calls reject with a 401 ApiError, while
ceremonies (transaction signing, message signing, account creation) reject
with a ValidationError reading
No refresh token is available. Call authenticate() again.
To get silent auto-refresh instead, pass ConstructorParams.refreshToken alongside it — obtained from Salt.getRefreshToken after Salt.authenticate.
Optionaldomain?: stringThe domain this instance authenticates from, used as the SIWE domain
when Salt.authenticate is called. It is carried inside the signed
SIWE message; the backend resolves the tenant from it and verifies it
against the tenant's registered allowed domains. Required to call
Salt.authenticate (browser or not); clients constructed with a
pre-existing authToken don't need it.
Environment to use. This will be optional in the future, but right now it is required. Use 'STAGING'
OptionalmanualReconnectIntervalMs?: numberPeriod (ms) at which a fallback timer will manually reconnect a
dead socket — recovers from socket.io's "fatal" disconnects (most
notably io server disconnect, fired during relay restarts and
deploys). Recommended for long-running daemons (e.g. 60_000 for
one check per minute). Leave undefined to disable.
OptionalrefreshToken?: stringA refresh token previously obtained from Salt.authenticate (or Salt.getRefreshToken). When provided, the SDK automatically refreshes an expired access token on a 401 and retries the request — no manual re-authentication needed.
InvalidParams thrown if the parameters passed in to the constructor are invalid. Params are optional - the default environment is TESTNET.
InvalidEnvironment Thrown if the provided Environment is invalid.
InvalidAuthToken Thrown if the provided auth token is invalid. The token must be a string. If you need to authenticate, simply omit ConstructorParams.authToken, then use authenticate.
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET' });
// Log in to an existing account
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(),
});
await salt.authenticate(walletClient);
// You are ready to use the SDK. For example...
const orgs = await salt.getOrganisations();
import { Salt } from 'salt-sdk';
const salt = new Salt({ environment: 'MAINNET' });
import { Salt } from 'salt-sdk';
// Connect to local development instance
const salt = new Salt({ environment: {
chainId: 421614,
websocketUrl: 'https://localhost:8545',
saltDomain: 'localhost',
apiUrl: 'https://localhost:8545/api'
}})
The authenticated user's secp256k1 public key, recovered server-side from
the SIWE signature during authenticate. Distinct from the
wallet's EVM address (a hash of this key) — needed wherever a caller must
encrypt something to the user, e.g. the publicKey param of
RoboHost.generateSetupScript and RoboHost.generateCloudFormationUrl.
Undefined until authenticate completes, and cleared again on
logout or an unrecoverable token refresh.
Disconnect the websocket connection. Call this when you are done using a long-lived socket (e.g. after stopping a NudgeListener) to allow the Node.js process to exit cleanly.
Self-contained methods like createAccount and submitTx disconnect automatically — you need this for NudgeListener
Subscribe to the session becoming unauthenticated — an explicit logout, or a refresh that could not recover it (the refresh token is missing, expired, or was revoked). Fires even when nothing in your own code is currently awaiting a rejection (e.g. a background socket reconnect that failed to refresh).
This doesn't force any particular response — it's just the signal that the session is done, so you can chain whatever makes sense for your app: re-authenticate immediately, redirect to a login screen, log and exit, etc.
An unsubscribe function.
Subscribe directly to the relay's nudge event on the underlying
socket. This is the same wire-level event NudgeListener
subscribes to internally; exposing it here lets consumers observe
nudges without going through NudgeListener's validation, dedup,
and nudgeReceived event chain — useful for diagnostics, custom
routing, or applications that don't want the auto-join machinery.
Must be called after authenticate (which initialises the socket).
An unsubscribe function.
Subscribe to the underlying socket's lifecycle: connect, disconnect, reconnect attempts, reconnect-failed terminal state, and auth errors from the relay. Useful for long-running consumers (notably robo daemons) that need to log, alert, or trigger re-auth on relay availability changes.
Must be called after authenticate (which initialises the socket).
The handler does not replay the current connection state on subscribe — it only fires on subsequent transitions. Seed your initial state from a separate check; do not assume the first event reflects the state at subscribe time.
An unsubscribe function.
Subscribe to any application-level event pushed to this user by the
relay (e.g. server-side status updates forwarded via POST /user).
No-op before connect or authenticate has been called.
The event name and payload type T are the caller's responsibility —
salt-sdk does not define or validate application-specific event shapes.
An unsubscribe function.
Creates a new account within an Organisation. Registers the account, nudges co-signers to join the MPC keygen ceremony, and returns before the ceremony completes.
Returns an AccountCeremony. Call
wait() to drive the keygen ceremony and
finalise the account (derives the public key and EVM address). If you skip
wait().
The SDK automatically determines the required robo signers based on the
number of human signers provided: an account is always N humans + (N-1)
robos, so signers must contain exactly 2, 3, or 4 human addresses
(yielding 2+1, 3+2, or 4+3 signer configurations).
Account creation is off-chain for the human and robo signers — no gas is required from any of them. The on-chain shard-registry write is performed and paid for server-side.
The organisation must have a provisioned, online robo host (see createRoboHost). The SDK resolves the robo signers via the API and fails with RoboStatusError if too few robo signers are online.
The account creation parameters
An AccountCeremony with the account ID and a wait() method
InvalidAuthToken if the authentication token is invalid
InvalidSigner if signer is missing or has no attached account
WrongChain if signer is on a chain other than the orchestration chain
ValidationError if the caller is not included in signers, the number of human signers is not 2, 3, or 4, or the organisation has fewer registered robo guardians than the configuration requires
RoboStatusError if the organisation has no online robo host
SocketConnectError if the signer fails to connect to websockets, which are required for setup orchestration
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const signer = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(process.env.RPC_URL),
});
const ceremony = await salt.createAccount({
name: 'Treasury',
organisationId: 'org-id',
signers: [
'0x1111111111111111111111111111111111111111',
'0x2222222222222222222222222222222222222222',
],
signer,
});
const { account: created } = await ceremony.wait();
console.log('Account created:', created.publicKey);
Gets the details of a specific account. The user must have adequate permissions to view the account.
The ID of the account
The account details
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the account does not exist or the API returns an error
Lists an account's signers in signer order, each tagged with its role (human or robo). Robo identity is not encoded in the signer list itself, so this resolves it from the organisation's robo set.
Use this to label a "who's joining" presence list: the
AccountCeremony presence/ready events carry only address and
isOnline, so build a lookup from this method and join by address. Reading
the robo set requires permission only human users hold (a robo guardian
does not), which is why presence stays role-free.
The ID of the account
The account's signers, each with address and isRobo
InvalidAuthToken if the authentication token is invalid.
ApiError if the account does not exist or the API returns an error
const ceremony = await salt.createAccount({ ... });
const signers = await salt.getAccountSigners(ceremony.accountId);
const isRobo = new Map(
signers.map((s) => [s.address.toLowerCase(), s.isRobo])
);
ceremony.on('presence', ({ signers }) => {
signers.forEach((s) =>
console.log(s.address, isRobo.get(s.address.toLowerCase()), s.isOnline)
);
});
Gets the transactions of a specific account. The user must have adequate permissions to view the account's transactions.
The ID of the account
A list of transactions for the account
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the account does not exist or the API returns an error
Join an existing keygen ceremony from a received nudge payload. Use this
when the nudge was received and stored before the signer was available
(e.g. before the wallet was connected), so the join() closure can be
constructed lazily at the point the user consents.
Raw keygen nudge payload as received from the relay.
The co-signer's viem WalletClient.
Optionaloptions: { resolveAccount?: boolean }Optional behaviour flags. resolveAccount fetches the
account record during the ceremony, enabling address-level huddle presence
(presence/ready/onlineSigners) and returning the finalized account
from AccountCeremony.wait. Off by default — the MPC protocol needs
none of it; opt in only when rendering presence or you need the account
metadata. Always forced off for robo clients, which may not be
permitted to read the record.
An AccountCeremony ready for AccountCeremony.wait.
Call this at most once per nudge session. Unlike the NudgeListener
join paths (which dedupe by session ID and refuse a second join), this
manual path does not guard against a double-join: joining the same
session twice starts a duplicate keygen session — a duplicate partyId
broadcasting conflicting round-1 commitments — which corrupts peers with
CBOR-decode / DLog errors. Callers must guard against double-fires (e.g.
a React double-render or a retried click).
InvalidAuthToken if the authentication token is invalid.
InvalidSigner if signer has no attached account.
SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration
Opens a websocket connection to receive account setup nudges and automatically joins the MPC keygen ceremony for each one.
The caller's signer is used to save the keyshare once the ceremony
completes. Listen to the setupComplete event on the returned
NudgeListener to be notified when a new account has been set up.
signer: the viem WalletClient that will participate in keygen ceremonies
OptionalautoJoin?: booleanWhether the listener should automatically join every incoming
keygen ceremony. Defaults to true. Set to false to receive
nudgeReceived events instead and decide per-nudge whether to
call the supplied join callback.
OptionalresolveAccount?: booleanFetch the account record for each joined ceremony, enabling address-level
huddle presence and the account returned from the ceremony's wait().
Off by default — opt in only when rendering presence or you need the
account metadata. Always forced off for robo clients. See
joinAccountCeremony.
NudgeListener for lifecycle events
InvalidAuthToken if the authentication token is invalid
SocketError if the websocket connection fails
InvalidSigner if signer has no attached account
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const account = privateKeyToAccount(process.env.PRIVATE_KEY);
const signer = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const nudgeListener = await salt.listenToAccountNudges({ signer });
nudgeListener.on('setupComplete', ({ account }) => {
console.log('Joined account:', account.id);
});
// When done listening
nudgeListener.disableNudgeListener();
salt.disconnect();
Hosts an MPC threshold signing ceremony over a plain-text message, hashed
per EIP-191 — the personal_sign scheme, where the message is prefixed
with "\x19Ethereum Signed Message:\n<length>" before hashing. Use it for
login challenges, off-chain proofs of account control, and anything else a
wallet's personal_sign would cover. To sign EIP-712 structured data
instead, use signTypedData.
message is always treated as UTF-8 text: a 0x-prefixed string is
hashed as those literal characters, never decoded to bytes. This method
signs no other form — it is not the (deprecated, unprefixed) eth_sign,
and it never accepts a pre-computed digest, so what it signs can always be
reconstructed from the text you passed in.
Mirrors submitTx: it returns the SignMessageHostCeremony rather than the signature, so you can subscribe to progress events before driving it. Call wait() to run the ceremony and obtain the assembled signature.
All account signers must have an active NudgeListener so they can join the ceremony automatically. This method drives the host side; co-signers are nudged and handled transparently.
accountId: the account to sign for; signer: the viem
WalletClient for the calling party (used to load their keyshare);
message: the UTF-8 message to sign
A SignMessageHostCeremony. Await its
wait() for the EvmSignature.
InvalidAuthToken if the authentication token is invalid
InvalidSigner if signer has no attached account
WrongChain if signer is on the wrong orchestration chain
InvalidMessage if message is empty
ValidationError if the caller is not a signer on the account
ApiError if the account does not exist or the API returns an error
SaltCeremonyError if the signing ceremony fails
RoboStatusError if the account's robo quorum is not met (thrown from wait())
SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration
import { verifyMessage } from 'viem';
const ceremony = await salt.signPersonalMessage({
accountId: account.id,
signer,
message: 'hello world',
});
const { signature } = await ceremony.wait();
// The signature verifies against the account's external address —
// `account.evmAddress`, not the address of the signer that hosted.
const valid = await verifyMessage({
address: account.evmAddress!,
message: 'hello world',
signature,
});
const ceremony = await salt.signPersonalMessage({
accountId: account.id,
signer,
message: 'hello world',
});
// Subscribe before awaiting to watch the signing huddle assemble —
// the host plus the account's robo guardians joining to co-sign.
ceremony.on('presence', ({ joined, total, signers }) => {
console.log(`${joined} of ${total} signers present`);
signers.forEach((s) => console.log(s.address, s.isOnline));
});
const { signature } = await ceremony.wait();
const { account } = await (await salt.createAccount({ ... })).wait();
// All signers must have their NudgeListeners running before this call
const ceremony = await salt.signPersonalMessage({
accountId: account.id,
signer,
message: 'challenge',
});
await ceremony.wait();
console.log('Keyshare verified — backup is intact');
Hosts an MPC threshold signing ceremony over EIP-712 typed structured
data — the eth_signTypedData_v4 scheme, where the digest is built from
the domain separator and the hashed struct rather than from free text. Use
it wherever a contract or protocol expects a typed-data signature. For
plain-text messages use signPersonalMessage.
The caller's signer plus the account's robo guardians co-sign, producing a single EVM signature that recovers to the account's public key — the same human + robo path as signPersonalMessage and submitTx, differing only in how the signed digest is derived.
As with signPersonalMessage, no pre-computed digest is ever
accepted: the typed data is distributed to the co-signers, and each derives
the digest itself, so what was signed is always reconstructible from the
typedData you passed in.
accountId: the account to sign for; signer: the viem
WalletClient for the calling party (used to load their keyshare);
typedData: the SaltTypedData to sign
A SignMessageHostCeremony. Await its
wait() for the EvmSignature.
A typed-data signature is an off-chain authorisation and is not subject
to the account's transaction policies — those are evaluated
by submitTx against a transaction. Structures such as ERC-2612
Permit let a third party move funds once signed, so treat what you sign
here with the same care as a transaction.
InvalidAuthToken if the authentication token is invalid
InvalidSigner if signer has no attached account
WrongChain if signer is on the wrong orchestration chain
InvalidTypedData if typedData is malformed, or primaryType names no struct in types
Error (from viem, e.g. InvalidAddressError) if a field value in message does not match the type declared for it
ValidationError if the caller is not a signer on the account
ApiError if the account does not exist or the API returns an error
SaltCeremonyError if the signing ceremony fails
RoboStatusError if the account's robo quorum is not met (thrown from wait())
SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration
import { verifyTypedData } from 'viem';
const typedData = {
domain: {
name: 'Salt Example',
version: '1',
chainId: 421614,
verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC',
},
types: {
Mail: [
{ name: 'from', type: 'address' },
{ name: 'to', type: 'address' },
{ name: 'contents', type: 'string' },
],
},
primaryType: 'Mail',
message: {
from: account.evmAddress!,
to: '0x2222222222222222222222222222222222222222',
contents: 'Hello from Salt',
},
} as const;
const ceremony = await salt.signTypedData({
accountId: account.id,
signer,
typedData,
});
const { signature } = await ceremony.wait();
const valid = await verifyTypedData({
address: account.evmAddress!,
...typedData,
signature,
});
Verifies the integrity of all keyshares for a Salt account by exercising two independent signing flows against a fixed challenge:
Recovery flow — nudges every non-robo (human) signer. Proves that the human keyshares alone can produce a valid threshold signature, validating the account's recovery path.
Regular flow — nudges every robo signer (the calling human is the host). Proves that the standard transaction signing path — one human plus the robo guardians — is operational.
Both ceremonies must complete successfully for the account's keyshares to be considered verified. A failure in either indicates a corrupted or missing keyshare backup.
All account signers (human and robo) must have an active NudgeListener so they can join each ceremony automatically.
accountId: the account to verify; signer: the viem
WalletClient for the calling party (must be a human signer on the account)
A VerifyAccountResult containing the confirmed account address and the signatures produced by each flow.
InvalidAuthToken if the authentication token is invalid
InvalidSigner if signer has no attached account
WrongChain if signer is on the wrong orchestration chain
SaltCeremonyError if either signing ceremony fails
ValidationError if the caller is not a signer on the account
ApiError if the account does not exist or the API returns an error
RoboStatusError if the account's robo quorum is not met for the regular flow
SocketConnectError if the signer fails to connect to websockets, which are required for ceremony orchestration
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const signer = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
// All signers (human and robo) must have their NudgeListeners running
const { accountAddress, signatures } = await salt.verifyAccount({
accountId: 'account-id',
signer,
});
console.log('Keyshares verified for', accountAddress);
console.log('Recovery flow signer:', signatures.recovery.recoveredAddress);
console.log('Regular flow signer:', signatures.regular.recoveredAddress);
Initiate the SIWE authentication flow to get an authentication token from
the Salt API. Signs a SIWE message + nonce with the supplied viem
WalletClient; the server verifies and returns an access token plus a
companion refresh token.
If you already have a token, set it via setAuthToken or in the constructor.
viem WalletClient with an attached account
The short-lived access token, which expires 20 minutes after it is issued. Both it and the refresh token issued alongside it are stored on the client, together with the user's public key (see userPublicKey), so this instance auto-refreshes for as long as it lives. To survive a restart, read the refresh token with getRefreshToken and persist it too: an instance reconstructed from the access token alone cannot refresh. See ConstructorParams.authToken.
InvalidSigner if walletClient has no attached account
ValidationError if no domain was configured on the constructor
InvalidUrl if the API URL or path is invalid
ApiError if the authentication request fails
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(),
});
await salt.authenticate(walletClient);
import { Salt } from 'salt-sdk';
import { createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({
account,
chain: arbitrumSepolia,
transport: http(),
});
const authToken = await salt.authenticate(walletClient);
// Persist both: the access token expires after 20 minutes, and only the
// refresh token can mint a new one. `secureStorage` is a placeholder for
// your app's secure persistence — localStorage is not a safe choice here.
const refreshToken = salt.getRefreshToken();
await secureStorage.set('salt.authToken', authToken);
if (refreshToken) await secureStorage.set('salt.refreshToken', refreshToken);
// Next launch: hand back both, and the SDK refreshes on expiry by itself.
// The refresh token is valid for 30 days, renewed on every use, so a
// session resumes for as long as it is used at least that often.
const resumed = new Salt({
environment: 'TESTNET',
authToken: await secureStorage.get('salt.authToken'),
refreshToken: await secureStorage.get('salt.refreshToken'),
});
Connect the underlying relay socket using the current auth token. Idempotent — safe to call repeatedly; a live connected socket is left untouched. Call this immediately after setAuthToken to ensure the socket is ready before any nudges or server-push events are emitted, without waiting for a ceremony entry point.
SocketError if the connection fails.
Returns the current refresh token, if any. Persist this (e.g. to secure storage) and pass it back via ConstructorParams.refreshToken to resume a session after a restart without re-running the SIWE flow.
Note the refresh token rotates on every use, so always read the latest value (e.g. after each call) rather than caching the first one.
Logs out the current session: revokes the refresh token server-side, clears the in-memory tokens, and disconnects the websocket. After this the instance is unauthenticated — call authenticate to use it again.
Resolves even if the network call fails; local state is always cleared.
Sets the authentication token for the Salt SDK. This is useful if you have previously logged in and stored the token. You can also provide the token to the constructor If you do not have a token already, you can use the authenticate method to obtain one.
The authentication token to use for this connection
This is equivalent to providing the token to the constructor, and carries the same limitation: an access token expires 20 minutes after it is issued and cannot be renewed on its own. This method sets only the access token, so a session restored with it alone cannot auto-refresh — see ConstructorParams.authToken for how that failure surfaces. To restore a refreshable session, pass both tokens to the constructor instead.
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
});
// Retrieve an access token obtained earlier in this process. `secureStorage`
// is a placeholder for your app's secure persistence.
const authToken = await secureStorage.get('salt.authToken');
salt.setAuthToken(authToken);
// You are ready to use the SDK, until the token expires 20 minutes after
// it was issued. To resume a session that outlives that, construct with a
// refresh token as well — see `ConstructorParams.authToken`.
await salt.getOrganisations();
Accepts an Invitation to an Organisation. The list of transactions for the current user can be retrieved with getOrganisationsInvitations.
The ID of the invitation to accept
InvalidAuthToken if the authentication token is invalid
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const invitations = await salt.getOrganisationsInvitations();
for (const invitation of invitations) {
console.log(`Accepting invitation to organisation ${invitation.organisation_id}`);
await salt.acceptOrganisationInvitation(invitation.id);
}
Creates a new Organisation. The authenticated user will be the owner. Collaborators can optionally be invited at creation time — they will receive invitations that can be accepted via acceptOrganisationInvitation.
This operation is off chain, so it does not require any gas.
The organisation creation parameters
The created organisation
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the API returns an error (e.g. duplicate collaborator addresses, invalid name)
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const org = await salt.createOrganisation({
name: 'My Organisation',
owner: {
name: 'Alice',
address: '0x1234567890123456789012345678901234567890',
role: 'CEO',
},
});
console.log('Created organisation:', org.id);
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const org = await salt.createOrganisation({
name: 'My Organisation',
owner: {
name: 'Alice',
address: '0x1234567890123456789012345678901234567890',
role: 'CEO',
},
collaborators: [
{
name: 'Bob',
address: '0x2345678901234567890123456789012345678901',
role: 'CFO',
accessLevel: 2,
},
],
});
Declines a pending Invitation to an Organisation. The counterpart to acceptOrganisationInvitation; pending invitations are listed with getOrganisationsInvitations.
The ID of the invitation to decline
InvalidAuthToken if the authentication token is invalid
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const invitations = await salt.getOrganisationsInvitations();
for (const invitation of invitations) {
await salt.declineOrganisationInvitation(invitation.id);
}
Gets the list of Accounts that belong to a Organisation.
The ID of the Organisation
The list of accounts
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the API returns an error
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token'
});
const accounts = await salt.getAccounts('organisation-id');
accounts.forEach(account => {
console.log(`Account: ${account.name} ID: ${account.id} Public key: ${account.publicKey}`);
});
Gets a single Organisation by its ID. The user must be a collaborator on the organisation.
The ID of the organisation to fetch
An object wrapping the organisation under an organisation key
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the organisation does not exist or the user lacks permission to view it
Gets the list of Organisations that the current user is a collaborator on. Requires authentication.
The list of organisations
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const organisations = await salt.getOrganisations();
organisations.forEach(org => {
console.group('Organisation:', org.name);
org.collaborators.forEach(collaborator => {
console.log('Collaborator:', collaborator.name);
});
console.groupEnd();
});
Returns pending Invitation to an Organisation for the current user. They can be accepted using acceptOrganisationInvitation.
List of invitations that are pending for this user
InvalidAuthToken if the authentication token is invalid
SocketError if the socket connection could not be established
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const invitations = await salt.getOrganisationsInvitations();
if (invitations.length > 0) {
console.log('You have pending invitations');
} else {
console.log('No pending invitations');
}
Invites a new collaborator into an existing Organisation. The invitee is added with status "Invited" and can join by accepting the invitation via acceptOrganisationInvitation. Only owners of the organisation may invite collaborators.
Every existing collaborator is left untouched. To add collaborators at
creation time instead, use the collaborators option of
createOrganisation.
The ID of the organisation to invite into
The invitee's address and role, plus optional name and accessLevel (defaults to 2, Member)
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
InvalidParams if a collaborator with the same address is already in the organisation
ApiError if the organisation does not exist or the user lacks permission to update it
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
await salt.inviteCollaborator('org-id', {
name: 'Bob',
address: '0x2345678901234567890123456789012345678901',
role: 'CFO',
accessLevel: 'member',
});
Leaves an Organisation as the authenticated user. Use this to remove yourself; to remove another collaborator as an owner, use removeCollaborator.
The collaborator is marked Inactive (the API retains collaborators for
audit history) and any pending invitation for you is dropped. An
organisation must always keep at least one active owner, so the last active
owner cannot leave.
The ID of the organisation to leave
The updated organisation
InvalidAuthToken if the authentication token is invalid
ApiError if you are not a collaborator, or leaving would remove the last active owner
Removes a collaborator from an Organisation by revoking their
access. The Salt API retains collaborators for audit history, so the
collaborator is marked Inactive rather than deleted — they will still
appear in getOrganisationById with
status Inactive. Every other collaborator is
left untouched. Only owners of the organisation may remove collaborators.
An organisation must always retain at least one active owner, so removing the last active owner is rejected.
The ID of the organisation the collaborator belongs to
The Ethereum address of the collaborator to remove
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
InvalidParams if the collaborator is not found, or removing them would leave no active owner
ApiError if the organisation does not exist or the user lacks permission to update it
Updates a single collaborator within an Organisation. Use this to change a collaborator's display name, their access level (the permission tier the API enforces), their free-text role label (cosmetic, no permission effect), or their status (to re-activate a removed collaborator or deactivate one). Only owners of the organisation may update collaborators.
Every other collaborator is left untouched. An organisation must always
retain at least one active owner, so demoting the last owner or deactivating
them is rejected. A collaborator's address is immutable and cannot be
changed here.
The ID of the organisation the collaborator belongs to
The Ethereum address of the collaborator to update
The fields to change; provide any of name, role, accessLevel, status
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
InvalidParams if no changes are given, the collaborator is not found, or the change would leave no active owner
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
// Collaborators are unique by EVM address, so a removed collaborator
// cannot be re-invited — reactivate them instead.
await salt.updateCollaborator(
'org-id',
'0x1234567890123456789012345678901234567890',
{ status: 'Active' }
);
ApiError if the organisation does not exist or the user lacks permission to update it
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
await salt.updateCollaborator(
'org-id',
'0x1234567890123456789012345678901234567890',
{ name: 'Alice', accessLevel: 'owner', role: 'CEO' }
);
Creates a new Policy on a Salt account. Policies control what transactions are allowed, denied, or require approval before being signed by the robo guardians.
Only one policy may exist per type + chain on an account; attempting to
create a second of the same type for the same chain throws a
DuplicatePolicyError. To express several constraints of one type, put
them in that policy's params array.
nominated_approvers policies cannot be created: the policy type exists on
the API but approval enforcement is not yet implemented, so creating one
throws a ValidationError. For contract_param_restriction policies
each restriction's solidityType is derived from its functionSignature/
paramIndex (so you need not supply it); this catches a few common mistakes
early — malformed signatures, out-of-range indexes, or illegal operators —
throwing a ValidationError before any request is made. It is not
exhaustive validation.
The policy creation parameters
The created policy
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ValidationError if the policy is a nominated_approvers
policy (not yet implemented), or a contract_param_restriction policy with
an invalid restriction
DuplicatePolicyError if a policy of the same type already exists on the account for the same chain
ApiError if the API returns an error (e.g. invalid policy type or params)
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const policy = await salt.createAccountPolicy({
accountId: 'account-id',
type: 'allowed_recipients',
chain: '11155111', // Sepolia
params: {
recipients: [
{ address: '0x1234567890123456789012345678901234567890', nickname: 'Treasury' },
],
},
});
console.log('Created policy:', policy.id);
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const policy = await salt.createAccountPolicy({
accountId: 'account-id',
type: 'transaction_limit_token_denominated',
chain: '1', // Ethereum Mainnet
params: {
limits: [
{
address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC
amount: '1000',
},
],
},
});
Deletes an existing Policy by its ID.
The ID of the policy to delete
A promise that resolves when the policy has been deleted
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the policy cannot be found or the API returns an error
updateAccountPolicy to change a policy's params instead of removing it
Fetches an existing Policy by its ID.
The ID of the policy to retrieve
The policy
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the policy cannot be found or the API returns an error
listAccountPolicies to fetch all of an account's policies when you don't have a policy ID
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const policy = await salt.getAccountPolicy('policy-id');
console.log(`Policy type: ${policy.type} [chain ${policy.chain}]`);
// PolicyParams is a union, so narrow it with an `in` check
if ('recipients' in policy.params) {
for (const recipient of policy.params.recipients) {
console.log(` ${recipient.address} (${recipient.nickname ?? 'unnamed'})`);
}
}
Lists all policies attached to a Salt account.
Reads the policies embedded in the account record, so this is a convenience wrapper over getAccount — use it when you only need the policy list.
The ID of the account whose policies should be listed.
An array of policies attached to the account.
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the API returns an error.
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const policies = await salt.listAccountPolicies('account-id');
console.log(`Account has ${policies.length} policies`);
for (const policy of policies) {
console.log(` ${policy.type} [${policy.chain}]: ${policy.id}`);
}
Runs a policy check for a Salt account.
Evaluates the account's policies that apply to the proposed transaction and returns which were validated, which were rejected (breach), and whether the account has policies that were not evaluated — see PolicyCheck. Run this before submitTx to surface policy violations early: if a breaching transaction is submitted anyway, the signing ceremony fails with a PolicyBreachError.
The ID of the account whose policies should be evaluated.
The proposed transaction to evaluate policies against (TransactionObjectParams).
A PolicyCheck describing the outcome of each policy.
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the API request fails (e.g. the account does not exist or the caller lacks permission).
const result = await salt.runPoliciesCheck('account-id', {
nonce: 1,
amount: '1000000000000000000', // 1 ETH, in wei
from: '0xYourSaltAccountAddress', // the account sending the transaction, not you
to: '0xRecipientAddress',
network: '1',
data: '0x',
});
if (result.policyBreach) {
console.log('Rejected by:', result.rejectedPolicies.map(p => p.type));
}
Updates an existing Policy by its ID. Replaces the policy's params with the new values provided.
When updating a contract_param_restriction policy, each restriction's
solidityType is derived from its functionSignature/paramIndex (so you
need not supply it). This catches a few common mistakes early — a malformed
signature, an out-of-range paramIndex, or an operator that is illegal for
the parameter type — throwing a ValidationError before any request is
made. It is not exhaustive validation.
The ID of the policy to update
The new policy parameters
The updated policy
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ValidationError if updating a contract_param_restriction policy with an invalid or contradictory restriction
ApiError if the API returns an error (e.g. policy not found or invalid params)
import { Salt } from 'salt-sdk';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token',
});
const updated = await salt.updateAccountPolicy('policy-id', {
recipients: [
{ address: '0x1234567890123456789012345678901234567890', nickname: 'Treasury' },
{ address: '0x2345678901234567890123456789012345678901', nickname: 'Payroll' },
],
});
console.log('Updated policy:', updated.id);
Completes 2FA activation for a robo host, flipping it from inactive to active. This is what makes a robo host usable. getRoboQuorum.
Authorised by the owner, not a privileged session. Like authenticate,
this builds and signs a SIWE message itself — the caller passes the owner's
walletClient and the host's setup OTP (the code shown during robo-guardian
setup — see RoboHost.otp); this method embeds that OTP in the SIWE
statement, has the wallet sign it, and submits it. That proves both control
of the owner wallet and possession of the setup code. On success the server
marks the robo active and consumes the OTP, so activation is single-use.
The robo id and the host's setup OTP
The owner's wallet, used to sign the activation message
The updated robos record (with active: true)
InvalidAuthToken if the authentication token is invalid
ValidationError if called from a robo client, the signer is
invalid, or the constructor was given no domain
ApiError with status 403 if the signer is not the robo
owner, 401 if the OTP is wrong, or 409 if the host is already active
Registers robos for an Organisation and creates their host record. No robo is running after this call — it creates only the server-side record. The robo signers come online once you provision a host with RoboHost.generateSetupScript or RoboHost.generateCloudFormationUrl and its setup completes. Treat the setup OTP as a secret. Only organisation owners can create a robo host.
The name, organisation and owner for the robos
The created robo host, including its setup OTP
This creates only the server-side record — its signers start empty
and no robo is running or online as a result. The guardian signer
addresses are registered separately during the provisioning flow
(RoboHost.generateSetupScript /
RoboHost.generateCloudFormationUrl), and a robo daemon must then
SIWE-authenticate those signers into the /robo websocket namespace
before they count as online.
"Online" is tracked per signer address in the /robo namespace, not
per organisation — so one running daemon's addresses satisfy any
organisation whose robo record lists them. Re-pointing a daemon at
another organisation's record is a configuration change, not a re-keying.
Use getRoboQuorum to verify the organisation's robos are online before attempting createAccount — account creation fails with RoboStatusError if no robo host is reachable.
InvalidAuthToken if the authentication token is invalid
InsufficientPermissions if the caller is not an organisation owner
ValidationError if called from a robo client
ApiError with status 409 if the organisation already has a robo host
ApiError if the API returns any other error
Fetches the existing robo host for an organisation — its public record only: name, signers, setup OTP (present until activation consumes it) and activation state. Everything needed to provision and activate a host is here; readable by any organisation collaborator with robos access, on any tenant.
The host's encrypted seed backup is not included — fetch it separately with getRoboHostSecrets, which is restricted to owners on privileged sessions.
The ID of the organisation
The robo host or null if not found
InvalidAuthToken if the authentication token is invalid
ValidationError if called from a robo client
ApiError if the robo host lookup fails
getRoboHostSecrets for the encrypted seed backup
Fetches a robo host's secret material — currently its encrypted
wallet-seed backup. Restricted to organisation owners operating under
privileged sessions (SIWE domain *.salt.space, or localhost against a
dev backend); unlike getRoboHost, permission failures are not
swallowed.
The robo host id (see RoboHost.id)
The host's secrets — seed is null until provisioning uploads
a backup
InvalidAuthToken if the authentication token is invalid
ValidationError if called from a robo client
InsufficientPermissions if the caller is not an
organisation owner, or the session is not privileged
(unauthorized:not-privileged)
ApiError if the robo host does not exist or the API errors
Reports whether enough robos are online to run a ceremony of the given type (see RoboQuorumParams).
keygen — roster-scoped; the required count comes from humanCount.signing / signMessage — account-scoped; only the account's enrolled
robos count, and the required count is fixed by the account.The ceremony type and its scope
The robo quorum: { requiredCount, onlineCount, met }
InvalidAuthToken if the authentication token is invalid
ValidationError if a keygen humanCount is unsupported
ApiError if the account or organisation does not exist, or the API returns an error
Reports the live presence of robos — an Organisation's whole roster, or the robos enrolled on a single account.
{ organisationId } — every robo signer in the organisation's roster.{ accountId } — only the robos enrolled on that account (the ones holding
its keyshares).Each signer carries an isOnline flag; the result also has an onlineCount
and an isReachable liveness flag (see RoboStatus).
{ organisationId } or { accountId }
The robo signers' presence
InvalidAuthToken if the authentication token is invalid
ApiError if the API returns an error
Gets the nonce for an account on a specific chain — the account's current on-chain transaction count (standard EVM nonce), fetched live from the destination network. submitTx uses the nonce, but if you do not specify one the SDK will automatically use this function to populate it.
The ID of the account
The ID of the chain
The nonce
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
ApiError if the account does not exist or the API returns an error
Gets the current gas price for a specific chain.
The ID of the chain
The current gas price
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
InvalidChain if the specified chain is unknown or unsupported
ApiError if the API request fails
Gets a single transaction by its ID, including its policy check and — once broadcast — its receipt. The user must have adequate permissions to view the transaction's account.
Useful for re-reading a transaction after the fact: a ceremony's wait() already returns the final record, so reach for this when you only kept the ID — for example to inspect a transaction proposed in an earlier session or by another signer.
The ID of the transaction
The transaction record
InvalidAuthToken if the authentication token is invalid. See authenticate for how to authenticate.
InsufficientPermissions if you may not read the account the transaction belongs to
ApiError if the transaction does not exist (404), the ID is malformed (400), or the API returns an error
Initiate, sign and broadcast a generic transaction from a given Salt account. This method provides orchestrated transaction handling with automatic account detail fetching, multi-party MPC signing coordination, and policy enforcement by the account's robo signers.
The Salt account (the destination-chain address derived from its public
key) must hold funds to pay gas on the destination chain. The caller's
walletClient pays orchestration-chain gas only.
For sends, if gas, maxFeePerGas, and maxPriorityFeePerGas are not
provided, they are populated automatically. For contract deployment,
gas is required (the Salt API does not yet estimate gas for deploys).
A viem publicClient for the destination chain is required: the SDK
uses it to broadcast the signed EIP-1559 transaction.
Important: value is denominated in wei (bigint). Use viem's
parseEther('0.5') or parseUnits(amount, decimals) for conversion at
the call site.
End-to-end flow:
publicClientThe transaction parameters. Provide to for a regular send (SendTransactionParams) or omit it for a contract deployment (DeployTransactionParams), which requires data (compiled bytecode) and gas. See TransactionParams for the full parameter definition
A TransactionHostCeremony for orchestrated execution — await its wait() for the final result
InvalidAuthToken if the auth token is missing or invalid
InvalidSigner if walletClient is missing or has no attached account
WrongChain if walletClient is on a chain other than the orchestration chain
ApiError if the API returns an error (i.e. account does not exist, insufficient permissions to view account)
ValidationError if the transaction parameters are invalid
SaltCeremonyError if the underlying SDK returns an unexpected result (e.g. a contract deployment that fails to produce a valid transaction response)
InsufficientFunds if the account cannot cover the transaction's total cost (gas + value) on the destination chain — detected either during gas estimation or at broadcast. Paid by the account itself (its SaltAccount.publicKey), not the signer. Subclass of SaltCeremonyError.
SocketError if the signer fails to connect to websockets, which are required for signing orchestration
import { Salt } from 'salt-sdk';
import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia, mainnet } from 'viem/chains';
const salt = new Salt({
environment: 'TESTNET',
authToken: 'your-auth-token'
});
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });
// Encode the approve function call
const data = encodeFunctionData({
abi: parseAbi(['function approve(address spender, uint256 amount)']),
functionName: 'approve',
args: [
'0xE592427A0AEce92De3Edee1F18E0157C05861564', // Uniswap V3 SwapRouter
1000000000n, // 1000 USDC (6 decimals)
],
});
// Gas and fee data are populated automatically when not provided
await salt.submitTx({
accountId: '0a1b2c3d4e5f',
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC contract
value: 0n,
chainId: 1,
data,
userAddress: account.address,
walletClient,
publicClient,
});
import { Salt } from 'salt-sdk';
import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia, mainnet } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });
const data = encodeFunctionData({
abi: parseAbi(['function approve(address spender, uint256 amount)']),
functionName: 'approve',
args: ['0xE592427A0AEce92De3Edee1F18E0157C05861564', 1000000000n],
});
await salt.submitTx({
accountId: '0a1b2c3d4e5f',
to: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
value: 0n,
chainId: 1,
data,
userAddress: account.address,
walletClient,
publicClient,
gas: '100000',
maxFeePerGas: '30000000000', // 30 gwei
maxPriorityFeePerGas: '1500000000', // 1.5 gwei
});
import { Salt } from 'salt-sdk';
import { createPublicClient, createWalletClient, encodeFunctionData, http, parseAbi, parseEther } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia, mainnet } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const publicClient = createPublicClient({ chain: mainnet, transport: http(process.env.MAINNET_RPC_URL) });
// Encode the submit function call for Lido staking
const data = encodeFunctionData({
abi: parseAbi(['function submit(address _referral) payable returns (uint256)']),
functionName: 'submit',
args: ['0x0000000000000000000000000000000000000000'], // No referral
});
const tx = await salt.submitTx({
accountId: '0a1b2c3d4e5f',
to: '0xae7ab96520DE3A18E5e111B5EaAb095312D7fE84', // Lido stETH contract
value: parseEther('1'), // 1 ETH
chainId: 1,
data,
userAddress: account.address,
walletClient,
publicClient,
});
const result = await tx.wait();
import { Salt } from 'salt-sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia, sepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL) });
// Omit `to` to submit a contract creation transaction.
// `data` should contain the compiled contract bytecode; `gas` is required (API does not yet estimate for deploys).
const tx = await salt.submitTx({
accountId: '0a1b2c3d4e5f',
value: 0n,
chainId: 11155111,
gas: '300000',
data: '0x608060...', // compiled bytecode
userAddress: account.address,
walletClient,
publicClient,
});
const result = await tx.wait();
const ceremony = await salt.submitTx({
accountId: '0a1b2c3d4e5f',
to: '0xRecipient',
value: parseEther('0.1'),
chainId: 11155111,
userAddress: account.address,
walletClient,
publicClient,
});
// Subscribe before awaiting to observe every stage:
// proposing → signing → broadcasting → confirming → success | failure
ceremony.on('stateChanged', ({ stage }) => {
console.log('transaction is now', stage);
});
const { transaction } = await ceremony.wait();
import { Salt, SaltCeremonyError, InsufficientFunds } from 'salt-sdk';
import { createPublicClient, createWalletClient, http } from 'viem';
import { privateKeyToAccount } from 'viem/accounts';
import { arbitrumSepolia, sepolia } from 'viem/chains';
const salt = new Salt({ environment: 'TESTNET', authToken: 'your-auth-token' });
const account = privateKeyToAccount(process.env.PRIVATE_KEY as `0x${string}`);
const walletClient = createWalletClient({ account, chain: arbitrumSepolia, transport: http() });
const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL) });
try {
const tx = await salt.submitTx({
accountId: '0a1b2c3d4e5f',
value: 0n,
chainId: 11155111,
gas: '300000',
data: '0xDEAD', // not valid contract bytecode
userAddress: account.address,
walletClient,
publicClient,
});
await tx.wait();
} catch (error) {
if (error instanceof SaltCeremonyError) {
console.error('Transaction failed:', error.message);
console.error('Raw result:', error.data);
} else if (error instanceof InsufficientFunds) {
console.error(
`Account needs funding on chain ${error.details.chainId}:`,
error.details.accountAddress ?? error.details.accountId
);
} else {
throw error;
}
}
The Salt SDK