npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@somnia-chain/accounts-sdk

v0.3.0

Published

Smart accounts on Somnia: derive an account's address offline, create it, manage its owners, sign through a chain of accounts, and read what controls it. The one gateway to the SomniaAccountFactory and the Safes it deploys.

Readme

@somnia-chain/accounts-sdk

Smart accounts on Somnia. An account is a Safe, deployed by SomniaAccountFactory at an address that is a pure function of (accountId, index) — the same address on every chain, derivable before anything is deployed, and stable when every key around it is replaced.

This package is the one gateway to that: the ABIs, the addresses, the derivation and the encoders. An app never writes a parseAbi([...]), never pastes an address, and never reimplements the salt.

pnpm add @somnia-chain/accounts-sdk viem

viem is a peer dependency (^2) — you bring your own, so the Client you pass in is the same Client type the SDK compiles against.

📚 Full documentation

This page is a tour. The documentation is grouped by what you need:

The shape of it

Nothing here owns a client, a transport or a lifecycle: every function takes the viem Client you already have. So this package produces plans and calls, and whatever is calling it puts them on the wire — which is what makes it safe to depend on from inside another SDK.

planExecution ──> ExecutionPlan ──> prepareExecution ──> Prepared ──> toCall(sig) ──> Call
                  (pure)                                 (pure)                      (data)

Stop at any of those.

The address, before there is an account

import {accountAddress, readAccountAddress} from "@somnia-chain/accounts-sdk";

accountAddress(50312, owner, 0n); // offline, no round trip
await readAccountAddress(client, 50312, owner, 0n); // what the deployed factory says

Both are the same arithmetic; the offline form is pinned against the contracts' own fixtures. Use it for a screen, and ask the chain before publishing a deposit address or moving value — the factory is upgradeable, so the chain is the authority.

→ Account derivation

One signature, every chain

Creating an account and granting it a session key is one transaction authorised by one signature, against an EIP-712 domain that deliberately omits chainId:

import {
  buildAccountAuthorization,
  createAccountsGasLimit,
  prepareAccountCreation,
  signWith,
} from "@somnia-chain/accounts-sdk";

const authorization = buildAccountAuthorization({
  chainId: 50312,
  accountId: owner,
  indices: [0n, 1n, 2n], // a main account and two sub-accounts, one prompt
  additionalOwners: [sessionKey],
  now: BigInt(Math.floor(Date.now() / 1000)),
});

const creation = prepareAccountCreation({authorization, chainId: 50312});
const sig = await signWith(signer, creation);

// One call per chain, returned as data: each needs its own funded sender.
const calls = creation.toCalls(sig, [5031, 50312, 50383]);
const gas = createAccountsGasLimit(authorization.indices.length);
void [calls, gas];

The owner key signs and never sends — execTransaction and the factory both check signatures rather than msg.sender, so it never needs gas.

→ Account authorization · How to create accounts on every chain

Owning, and acting

import {addOwnerCall, ownersOf, readExecutionPlan} from "@somnia-chain/accounts-sdk";

const grant = addOwnerCall(account, sessionKey, 1n); // one execTransaction the owner signs
const current = await ownersOf(client, account); // owners, threshold, and which are accounts
const plan = await readExecutionPlan(client, {chainId: 50312, account, calls});
void [grant, current, plan];

createAccount takes no owner list, and that is a security property rather than an omission: the factory forces the owner from the salt, so a third party cannot inject one — which is why creation is safe to leave permissionless and why anyone can pay for it.

→ How to grant and revoke a session key

Acting with no signature at all

When the key that authorises is also the key that pays, Safe already knows who is asking:

import {prevalidatedExecution, submitCall, EXECUTION_GAS_FLOOR} from "@somnia-chain/accounts-sdk";

const call = prevalidatedExecution(plan, sender.address); // no signature, no wallet round-trip
const {hash, receipt} = await submitCall(client, {chain, sender, call, gas: EXECUTION_GAS_FLOOR});
void [hash, receipt];

The result is the one Call in this package that is not submittable by anyone — the authority is in who broadcasts, not in the bytes.

→ How to act without a signature · The call, and who may send it

Accounts owned by accounts

A sub-account is an ordinary account whose accountId is an account. Only the acting account has a nonce — a parent approves a message, not a transaction — so depth costs one hash and one signature wrapper per level, and nothing else.

import {authorityPath, subAccountAddress} from "@somnia-chain/accounts-sdk";

const desk = subAccountAddress(50312, account, 1n);
const path = await authorityPath(client, desk, sessionKey); // what to pass as `through`
void [desk, path];

→ Act from a sub-account · About accounts owning accounts

Finding accounts, and what controls them

Derivation-based, so no indexer is needed:

import {accountsOf, accountTree, custodyChain, ownersOf} from "@somnia-chain/accounts-sdk";

const scan = await accountsOf(client, 50312, owner); // what exists under a key
const tree = await accountTree(client, 50312, account); // downward
const up = await custodyChain(client, account); // upward, to the key at the top
void [scan, tree, up, ownersOf];

Every one of these distinguishes absent from unread. deployed: undefined is not deployed: false, and rendering them alike shows a funded account as empty.

→ How to find an account

When something goes wrong

import {SomniaAccountsError, TransactionRevertedError} from "@somnia-chain/accounts-sdk";

try {
  await Promise.resolve();
} catch (e) {
  if (e instanceof TransactionRevertedError) console.error(e.reason ?? "reverted");
  else if (e instanceof SomniaAccountsError) console.error("ours:", e.message);
  else throw e;
}

Every failure extends SomniaAccountsError, so one catch tells you whether it was us, and the leaf class tells you what to do about it. Nothing raw escapes: a wallet that answers badly is a WalletResponseError, a chain that does not answer is an RpcError, an argument refusable offline is an InvalidInputError.

→ Errors · How to handle errors and reverts

Gas, in one paragraph

Safe's setup and execTransaction run nested, so EIP-150's 63/64 rule hands the inner frame a fraction of what remains rather than everything left in the transaction. These calls need a floor of gas remaining; eth_estimateGas measures gas spent; no multiplier on the latter reliably clears the former. Sending 3,000,000 reverts after burning 2,908,286 while 20,000,000 succeeds on 2,774,511. Use CREATE_ACCOUNT_GAS_LIMIT, createAccountsGasLimit, EXECUTION_GAS_FLOOR and measureGas — and assert receipt.status, because an out-of-gas revert produces a real hash and no account.

→ Gas

When something goes wrong

import {SomniaAccountsError, TransactionRevertedError} from "@somnia-chain/accounts-sdk";

Every failure this SDK raises extends SomniaAccountsError, so one catch tells you whether it was us, and the leaf class tells you what to do about it. The classes carry the values you branch on as readonly fields — WrongSignerError names both addresses, TransactionRevertedError carries the hash and, where the replay could decode one, the reason — and chain whatever caused them in cause. Nothing raw escapes: a wallet that answers badly is a WalletResponseError, a chain that does not answer at all is an RpcError, and an argument that could be refused offline is an InvalidInputError rather than a RangeError.

Using it as an execution layer

This package is designed to be driven by another SDK. Nothing here owns a client, a transport or a lifecycle: every function takes the viem Client you already have, so the calling SDK keeps its own chain, its own RPC and its own cleanup, and there is never a second transport.

That makes an adapter small. The shape below is the one Somnia Markets' trader expects — an object that says which chain and which actor it sends as, turns a list of calls into a receipt, and lets the SDK decode the result out of receipt.logs without fetching it again:

import {
  readExecutionPlan,
  prepareExecution,
  signAndSubmit,
  type Call,
} from "@somnia-chain/accounts-sdk";

const executor = {
  chainId: chain.id,
  actor: account, // what the chain sees as msg.sender
  async execute(calls: readonly Call[]) {
    const plan = await readExecutionPlan(client, {chainId: chain.id, account, calls, through});
    const {hash, receipt} = await signAndSubmit(client, {
      prepared: prepareExecution(plan, signer.address),
      signer,
      sender, // a relayer, or the signer itself when it can pay
      chain,
    });
    return {hash, receipt};
  },
};

through is the part a generic Safe integration does not have. It is the chain of accounts between the signer and the account being acted on, so an account owned by an account signs correctly at any depth — where a one-level integration requires the signer to be a direct owner on a threshold-1 Safe. Pass [] for the direct case.

If you take prepared.toCall(signature) and broadcast the bytes yourself rather than going through signAndSubmit, use measureGas for the limit — it is the same policy signAndSubmit applies, and the reason it is exported is that a copy of it drifts:

import {measureGas} from "@somnia-chain/accounts-sdk";

const call = prepared.toCall(signature);
const gas = await measureGas(client, {call, payer: sender.address, floor: prepared.gasFloor});

A fifth over the estimate, never below prepared.gasFloor. Omit payer for a relayer. Estimating from an address that cannot afford the call fails with an insufficient-funds error that reads as though the call itself is broken, which is why the payer is a parameter rather than an afterthought.

If sender is a relayer, implement send(call, chainId, hints) and use hints.gas. Safe's setup and execTransaction run nested, so EIP-150's 63/64 rule hands the inner frame a fraction of what remains rather than everything left in the transaction: these calls need a floor of gas remaining, eth_estimateGas measures gas spent, and no multiplier on the latter reliably clears the former. Account creation carries a measured 20,000,000 for that reason, and a relayer that hardcodes something reasonable-looking instead fails every creation with an out-of-gas receipt that does not say why. The third argument is optional, so a two-argument send still compiles — it just guesses.

Two things worth knowing before you wire this up. A Safe can swallow an inner revert: the outer transaction mines with status: "success" while a failure event says the inner call did not run. Which path a Safe takes depends on the gas fields, and execTransactionCall sets safeTxGas and gasPrice to zero, so a failure reverts the outer transaction and arrives as TransactionRevertedError. And signAndSubmit needs a sender that can pay; give it a signer that cannot and it raises SenderRequiredError rather than failing at broadcast against the wrong address.

Licence

MIT.