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

@crossmint/solana-smart-wallet-sdk

v0.23.0

Published

TypeScript SDK for Solana Smart Wallet program

Readme

@solana-smart-account/sdk

TypeScript SDK for the Solana Smart Account program. Create wallets, manage signers, set spending limits, execute transactions (sync and async), and manage M-of-N multisig signers through a PDA-based smart account.

Install

npm install @solana-smart-account/sdk

Peer dependencies: @coral-xyz/anchor >=0.30.0 and @solana/web3.js >=1.90.0.

Quick Start

The IDL is bundled with the SDK — no need to import it separately.

import {
  SmartAccountClient,
  adminSigner,
  ed25519Key,
  fromTransactionInstruction,
} from "@solana-smart-account/sdk";
import { SystemProgram } from "@solana/web3.js";

const client = SmartAccountClient.create(provider);
const signer = adminSigner(ed25519Key(myKeypair.publicKey));

// Create the smart account on-chain
const { signature, pdas } = await client.createSmartAccountAndSend({
  salt: 42,
  signers: [signer],
});

// Execute a SOL transfer through the wallet
const transferIx = SystemProgram.transfer({
  fromPubkey: pdas.smartAccountPda,
  toPubkey: recipient,
  lamports: 1_000_000,
});

const result = await client.executeTransaction({
  settingsPda: pdas.settingsPda,
  withSigner: ed25519Key(myKeypair.publicKey),
  instructions: [fromTransactionInstruction(transferIx)],
});
await client.send(result.transaction);

Execution Model

All transaction execution follows a unified build → approvals → finalize → send pattern, regardless of signer type.

  1. Build: call .build() on the fluent builder (or executeTransaction()). Returns a BuildResult with { transaction, approvals }.
  2. Approvals: for Ed25519 signers, approvals is empty — the transaction itself carries the signature. For Secp256r1/WebAuthn signers and P256 multisig members, approvals contains the messages to sign externally (WebAuthn, HSM, MPC, etc.).
  3. Finalize: call client.finalizeTransaction(result, signatures) to splice the external signatures into the transaction. Skip this step for Ed25519-only flows.
  4. Send: call client.send(tx) to broadcast.

Fluent builder

const result = await client
  .execute()
  .settingsPda(pdas.settingsPda)
  .withSigner(ed25519Key(myKeypair.publicKey))
  .instruction(fromTransactionInstruction(transferIx))
  .instruction(fromTransactionInstruction(swapIx))
  .build();

await client.send(result.transaction);

.withSigner(key) accepts any SignerKeyed25519Key(...), secp256r1Key(...), or multisigKey(...). The SDK resolves the signer index and sets up replay prevention automatically.

Ed25519 execution

Ed25519 signers produce no approvals — just sign and send:

const result = await client.executeTransaction({
  settingsPda,
  withSigner: ed25519Key(keypair.publicKey),
  instructions: [fromTransactionInstruction(transferIx)],
});
await client.send(result.transaction);

Or with the builder:

const result = await client
  .execute()
  .settingsPda(pda)
  .withSigner(ed25519Key(keypair.publicKey))
  .instruction(fromTransactionInstruction(transferIx))
  .build();

await client.send(result.transaction);

Secp256r1 / WebAuthn execution

P256 signers require on-chain replay prevention. Use .nonce(n) for the built-in nonce or configure a wallet RPP and then call .rpp() for RPP-based per-signer isolation.

const result = await client
  .execute()
  .settingsPda(pda)
  .withSigner(secp256r1Key(passkey))
  .instruction(fromTransactionInstruction(transferIx))
  .nonce(42) // or .rpp() after configuring rppProgramId on the wallet
  .build();

// result.approvals contains the messages to sign externally
const signatures = await Promise.all(
  result.approvals.map(async (a) => ({
    publicKey: a.publicKey,
    signature: await sign(a.message), // WebAuthn, HSM, MPC, etc.
  }))
);

const tx = client.finalizeTransaction(result, signatures);
await client.send(tx);

Multisig execution

Multisig signers may have a mix of Ed25519 and Secp256r1 members. Ed25519 members sign the transaction directly; Secp256r1 members produce approvals.

const result = await client
  .execute()
  .settingsPda(pda)
  .withSigner(
    multisigKey(2, [
      ed25519Member(kp1.publicKey),
      secp256r1Member(pk2),
      secp256r1Member(pk3),
    ])
  )
  .instruction(fromTransactionInstruction(transferIx))
  .build();

// Ed25519 members sign the transaction directly
result.transaction.sign([kp1]);

// Secp256r1 members sign via approvals
const sigs = await Promise.all(
  result.approvals.map(async (a) => ({
    publicKey: a.publicKey,
    signature: await signP256(a.message),
  }))
);

const tx = client.finalizeTransaction(result, sigs);
await client.send(tx);

Dry run / simulate

const dryRun = await client
  .execute()
  .settingsPda(pda)
  .withSigner(ed25519Key(keypair.publicKey))
  .instruction(transferIx)
  .simulate();

console.log(dryRun.success, dryRun.unitsConsumed);

Address Lookup Tables

When transactions reference many accounts they may exceed Solana's 1232-byte transaction size limit. Use Address Lookup Tables (ALTs) to compress account keys:

const altAccount = await connection.getAddressLookupTable(altAddress);

const result = await client
  .execute()
  .settingsPda(pdas.settingsPda)
  .withSigner(secp256r1Key(passkey))
  .instruction(complexSwapIx)
  .addressLookupTables([altAccount.value!])
  .build();

If a transaction exceeds 1232 bytes, the SDK throws a TransactionTooLarge error with the actual size and a suggestion to use .addressLookupTables().

Builder Pattern

The SDK is designed around instruction builders — methods that return TransactionInstruction objects you can include in any transaction. The *AndSend convenience methods are thin wrappers around these builders.

Instruction builders (primary API)

Every operation has a builder method that returns a TransactionInstruction:

// Build the instruction — do not send yet
const { instruction, pdas } = await client.createSmartAccount({
  salt,
  signers,
});

// Combine with other instructions in your own transaction
const tx = new Transaction().add(computeBudgetIx, instruction);
await provider.sendAndConfirm(tx, [payer]);

Convenience *AndSend methods

Each builder method has a *AndSend counterpart that builds and sends in one call. These are useful for quick scripts or simple flows, but use the builder methods when you need to:

  • Compose instructions into a larger transaction
  • Add compute budget instructions
  • Sign with multiple keypairs
  • Control transaction building precisely

Counterfactual Addresses

The wallet address is a PDA derived from sha256(salt || borsh(signers)). You can compute it offline and send funds to it before ever calling createSmartAccount. The address is deterministic and collision-resistant: same config = same address, different config = different address.

import {
  computeConfigHash,
  deriveSmartAccountAddress,
  adminSigner,
  ed25519Key,
} from "@solana-smart-account/sdk";

const signers = [adminSigner(ed25519Key(userPubkey))];
const configHash = computeConfigHash(salt, signers);
const { smartAccountPda } = deriveSmartAccountAddress(configHash);

// smartAccountPda can receive SOL/tokens now, deploy later

The salt parameter lets you create multiple wallets with the same signer set.

Spending Limits

Standard signers can be restricted to per-mint spend caps over a time window. Admins set limits via updateSignerPolicies; the program enforces them at execution time.

Setting limits

import {
  standardSigner,
  ed25519Key,
  PublicKey,
} from "@solana-smart-account/sdk";

const USDC_MINT = new PublicKey("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v");

// Allow a Standard signer to spend up to 100 USDC per day
await client.updateSignerPoliciesAndSend({
  settingsPda: pdas.settingsPda,
  signerKey: ed25519Key(standardSignerPubkey),
  limits: [
    {
      kind: "Token",
      mint: USDC_MINT,
      limitAmount: 100_000_000n, // 100 USDC in base units (6 decimals)
      windowSeconds: 86_400n, // 1 day in seconds
      currentSpent: 0n, // managed by program
      windowStartTimestamp: 0n, // managed by program
      allowedRecipients: [],
    },
  ],
});

To remove all limits and make a signer unrestricted, pass limits: [].

Native SOL limits

Use the Sol variant (kind: "Sol") to limit native SOL outflows:

{
  kind: "Sol",
  limitAmount: 1_000_000_000n,   // 1 SOL in lamports
  windowSeconds: 0n,             // lifetime cap — never resets
  currentSpent: 0n,
  windowStartTimestamp: 0n,
  allowedRecipients: [],
}

How enforcement works

Spending limits are a per-signer property — each Standard signer has its own independent set of limits. The program enforces them outcome-based: it snapshots balances before CPI execution, runs the instructions, then checks the delta against each limit.

The token accounts referenced by your CPI instructions (e.g., the source ATA in a token transfer) are automatically included in remaining_accounts by the SDK. The program discovers them there and snapshots their balances — no extra configuration needed.

Enforcement rules:

  • Any asset (SOL or token) that decreases must have a matching spending limit on the signer
  • Unlisted asset decreases are rejected (OperationNotPermitted)
  • Token account state changes beyond the balance field (Approve, Freeze, SetAuthority) are blocked
  • Windows auto-reset: once current_timestamp >= window_start_timestamp + window_seconds, the window resets on the next transaction
  • windowSeconds = 0 creates a lifetime cap that never resets

Multisig Signers

The program supports M-of-N multisig signer slots where each member can be either Ed25519 or Secp256r1. All member proofs must be submitted in a single on-chain transaction — there is no proposal state or async voting.

Creating a multisig signer

import {
  multisigKey,
  ed25519Member,
  adminSigner,
} from "@solana-smart-account/sdk";

// 2-of-3 multisig: any two of these three keypairs can authorize
const key = multisigKey(2, [
  ed25519Member(keypair1.publicKey),
  ed25519Member(keypair2.publicKey),
  ed25519Member(keypair3.publicKey),
]);

const { pdas } = await client.createSmartAccountAndSend({
  salt: 42,
  signers: [adminSigner(key)],
});

Executing with a multisig

Use the unified fluent builder with .withSigner(multisigKey(...)). See the Multisig execution example above.

Multisig helpers

| Helper | Description | | -------------------- | ---------------------------------------------------------------- | | multisigKey(t,m) | Create a multisig SignerKey with threshold t and members m | | ed25519Member(k) | Create an Ed25519 member from a PublicKey or bytes | | secp256r1Member(k) | Create a Secp256r1 member from compressed/uncompressed key |

API Reference

SmartAccountClient

Create via SmartAccountClient.create(provider) (recommended) or new SmartAccountClient({ provider, program }) for full control.

Account Creation

| Method | Returns | Description | | ----------------------------------- | ----------------------- | ------------------------------ | | createSmartAccount(params) | { instruction, pdas } | Build the creation instruction | | createSmartAccountAndSend(params) | { signature, pdas } | Create and send in one call |

Params: { salt: number, signers: SmartAccountSigner[], payer?: PublicKey }

Signer Management

| Method | Returns | Description | | ----------------------------- | ------------------------ | ------------------------------- | | addSigner(params) | TransactionInstruction | Build add-signer instruction | | addSignerAndSend(params) | string (signature) | Add signer and send | | removeSigner(params) | TransactionInstruction | Build remove-signer instruction | | removeSignerAndSend(params) | string (signature) | Remove signer and send |

Caller must be a registered Admin. Cannot remove the last Admin.

Spending Limit Policies

| Method | Returns | Description | | ------------------------------------- | ------------------------ | --------------------------------- | | updateSignerPolicies(params) | TransactionInstruction | Build update-policies instruction | | updateSignerPoliciesAndSend(params) | string (signature) | Update policies and send |

Params: { settingsPda, signerKey, limits: SpendingLimit[], payer? }

  • Caller must be a registered Admin.
  • Target signer must be Standard (policies cannot be set on Admin signers).
  • Replaces all existing limits atomically. Pass limits: [] to remove all restrictions.
  • currentSpent and windowStartTimestamp in the policy objects you pass are ignored — the program resets them.
  • Maximum policies per signer is capped (see MAX_POLICIES_PER_SIGNER in constants.rs).

To inspect current spend state, fetch the settings account with getSmartAccountInfo() and read signer.role.standard.limits.

Transaction Execution

| Method | Returns | Description | | ---------------------------- | --------------------------- | --------------------------------------------------------------------------------- | | execute() | ExecuteTransactionBuilder | Fluent builder — chain .settingsPda(), .withSigner(), .instruction(), .build() | | executeTransaction(params) | Promise<BuildResult> | Build execute result directly | | finalizeTransaction(result, signatures) | Transaction | Splice external Secp256r1 signatures into a built transaction | | send(tx) | Promise<string> | Broadcast a transaction and return the signature |

executeTransaction and the fluent execute() builder both return a BuildResult:

type BuildResult = {
  transaction: Transaction;
  approvals: Approval[];
};

type Approval = {
  publicKey: Uint8Array; // compressed Secp256r1 public key
  message: Uint8Array;   // message to sign (pass to WebAuthn / HSM / MPC)
};

For Ed25519 signers, approvals is always empty — call client.send(result.transaction) directly.

For Secp256r1 signers, sign each approval externally, then call finalizeTransaction(result, signatures) before sending.

Replay prevention: Ed25519 signers typically omit replay prevention (Solana's native transaction dedup is sufficient). Secp256r1 signers require explicit replay prevention; use .nonce(n) for the built-in nonce or .rpp(RPP_PROGRAM_ID) for per-signer RPP isolation.

remainingAccounts: If you omit remainingAccounts, the SDK collects them automatically from your instruction list. Pass them explicitly if you need fine-grained control.

addressLookupTableAccounts (optional): Pass AddressLookupTableAccount[] to compress account keys via ALTs. Exposed in the fluent builder via .addressLookupTables(alts).

Queries

| Method | Returns | Description | | ---------------------------------- | ---------------------- | ------------------------------------------------- | | getSmartAccountInfo(settingsPda) | SmartAccountSettings | Fetch account state (signers, nonce, spend state) | | deriveAddress(salt, signers) | SmartAccountPdas | Offline PDA derivation |

The SmartAccountSettings returned by getSmartAccountInfo includes signers[].role.standard.limits, which contains live currentSpent and windowStartTimestamp values.

Type Helpers

// Build signer keys
ed25519Key(publicKey); // from PublicKey or Uint8Array
secp256r1Key(uncompressedXY); // 33-byte compressed or 64-byte uncompressed passkey key
multisigKey(threshold, members); // M-of-N multisig

// Build multisig members
ed25519Member(publicKey); // from PublicKey or Uint8Array
secp256r1Member(pubkey); // compressed or uncompressed

// Build signers
adminSigner(key);
standardSigner(key); // unrestricted Standard signer (no limits)
standardSigner(key, [limit1, limit2]); // Standard signer with spending limits (outcome-based enforcement)

// Build spending limits
solLimit({ limitAmount, windowSeconds }); // native SOL limit
tokenLimit({ mint, limitAmount, windowSeconds }); // SPL token limit

// Inspect
isAdmin(role);
isStandard(role);
keyBytes(signerKey);
findSignerIndex(signers, key); // index of key in settings.signers[]

// Convert web3.js instructions for execute_transaction
fromTransactionInstruction(ix);

Key types

| Type | Description | | --------------- | -------------------------------------------------------- | | BuildResult | { transaction, approvals } — returned by all build paths | | Approval | { publicKey, message } — one entry per Secp256r1 signer | | SignatureEntry| { publicKey, signature } — passed to finalizeTransaction |

Error Handling

The SDK provides typed errors with actionable messages:

import { withParsedErrors } from "@solana-smart-account/sdk";

try {
  await withParsedErrors(async () => {
    const result = await client.executeTransaction({ ... });
    await client.send(result.transaction);
  });
} catch (e) {
  if (e instanceof SmartAccountError) {
    console.log(e.code);    // "SpendingLimitExceeded"
    console.log(e.message); // includes fix suggestion
  }
}

Or parse errors manually with parseSmartAccountError(err).

The SDK also validates transaction size before sending. If a transaction exceeds the Solana limit (1232 bytes), a SmartAccountError with code "TransactionTooLarge" is thrown before any RPC call is made. Use assertTransactionSize(serializedBytes) for custom validation.

Settings Compression

Dormant smart accounts can compress their Settings PDA into a 33-byte hash receipt, reclaiming rent. The Smart Account PDA and its funds are unaffected. Restoration is permissionless — anyone who has the original settings data can restore it.

Compress (admin sets authority first)

// Step 1 — designate a compress authority (Admin-only, one-time per wallet)
await client.setCompressAuthorityAndSend({ settingsPda, compressAuthority });

// Step 2 — compress (called by the compress authority)
await client.compressSettingsAndSend({ settingsPda, configHash });

Restore — inline path (small wallets)

Pass the raw settings bytes directly. Works as long as the serialized settings data fits within the Solana transaction size limit.

await client.restoreSettingsAndSend({
  settingsPda,
  configHash,
  accountBump,
  settingsData,
});

Restore — buffer path (large wallets)

For wallets whose settings data exceeds the available transaction space, write the data into a temporary buffer PDA first, then call restoreSettings pointing at the buffer.

// Step 1 — write settings data into a buffer PDA (chunked if needed)
await client.writeRestoreBufferAndSend({
  configHash,
  settingsData,
  bufferId: 0,
});

// Step 2 — restore from buffer (pass empty settingsData + buffer PDA as remaining account)
await client.restoreSettingsAndSend({
  settingsPda,
  configHash,
  accountBump,
  settingsData: Buffer.alloc(0), // empty signals "read from buffer"
  remainingAccounts: [{ pubkey: bufferPda, isSigner: false, isWritable: true }],
});

The bufferId (u8) is caller-chosen. If you wrote incorrect data into a buffer, pass a fresh bufferId to start over — the old buffer can be cleaned up separately.

Clean up a stale buffer

close_restore_buffer is permissionless — anyone can reclaim rent from an abandoned buffer.

await client.closeRestoreBufferAndSend({ configHash, bufferId: 0 });

Security Notes

  • Admin vs Standard: Admin signers can add/remove signers and update spending limit policies. Standard signers can only execute transactions, subject to their policies.
  • Spending limit enforcement is on-chain and unforgeable: The program snapshots balances before any CPI, so even a malicious instruction that briefly inflates a balance cannot bypass the check. All enforcement runs on-chain — SDK-side logic is for convenience only (ATA resolution, nonce fetching).
  • Replay prevention: The replay_prevention argument on execute_transaction selects between the built-in nonce and an external RPP. Ed25519 signers typically pass null (Solana's native transaction dedup is sufficient). Secp256r1 signers need explicit replay prevention. When using RPP, the wallet's configured rppProgramId is enforced on-chain.
  • CPI reentrancy: The program rejects any CPI that targets itself or the configured RPP, preventing mid-execution state manipulation.
  • Settings protection: During CPI execution, the settings account is forced read-only regardless of what the caller specifies.

Replay Prevention (RPP)

The program uses a built-in non-decreasing nonce by default. The submitted nonce must be >= current on-chain value; after success the stored nonce becomes submitted + 1. Callers can skip ahead (e.g., 0 → 100), so a dropped transaction never blocks future ones. For Secp256r1 signers with concurrent transactions, you can use an external Replay Prevention Program for per-signer nonce isolation. The wallet stores the chosen rppProgramId in Settings at creation time or via setRppProgramAndSend, and transactions using .rpp() pass only the RPP params plus the required CPI accounts.

RppClient

The SDK ships a separate RppClient for managing the RPP program itself — authorizing wallets, managing the treasury, and querying state. It is separate from SmartAccountClient because the RPP has its own authority model.

import { RppClient, RPP_PROGRAM_ID } from "@solana-smart-account/sdk";

const rppClient = new RppClient({ provider });

// Authorize a wallet (authority-only)
await rppClient.authorizeWalletAndSend({ wallet: smartAccountPda });

// Check whether a wallet is currently authorized
const state = await rppClient.getWalletRppState(smartAccountPda);
console.log(state?.authorized); // true

// Deauthorize a wallet (permanent in V1 — no re-auth path)
await rppClient.deauthorizeWalletAndSend({ wallet: smartAccountPda });

// Fund the shared treasury (permissionless)
await rppClient.fundTreasuryAndSend({ amount: 10_000_000 }); // 0.01 SOL

// Withdraw from treasury (authority-only)
await rppClient.withdrawTreasuryAndSend({
  amount: 5_000_000,
  recipient: myPubkey,
});

// Rotate the RPP authority
await rppClient.updateAuthorityAndSend({ newAuthority: newAuthorityPubkey });

Setup (deployer only)

// Called once after deployment — creates RppConfig and the treasury PDA
await rppClient.createRppAndSend({ depositLamports: 100_000_000 }); // 0.1 SOL

Builder pattern

Every operation has a builder method returning a TransactionInstruction and a *AndSend convenience wrapper:

const ix = rppClient.authorizeWallet({ wallet: smartAccountPda });
const tx = new Transaction().add(ix);
await provider.sendAndConfirm(tx, [authorityKeypair]);

Queries

| Method | Returns | Description | | --------------------------- | ------------------- | -------------------------------------------------------- | | getRppConfig() | RppConfig \| null | Global config: authority, treasury PDA, treasury balance | | getWalletRppState(wallet) | RppState \| null | Per-wallet state: authorized flag, active nonce entries | | getTreasuryBalance() | number | Current treasury lamport balance |

getRppConfig() returns null if create_rpp has not been called yet. getWalletRppState() returns null if the wallet has never been authorized.

Maintenance

// Permissionless — prunes expired entries, returns freed rent to the treasury
await rppClient.pruneStaleEntriesAndSend({ wallet: smartAccountPda });

See the RPP program docs for the full authority model, trust assumptions, and V1 limitations.

Signer Types

| Type | Key Size | Auth Mechanism | | ---------------- | -------- | ---------------------------------------------------- | | Ed25519 | 32 bytes | Standard Solana transaction signing | | Secp256r1 (P256) | 33 bytes | Precompile introspection (passkeys, WebAuthn) | | Multisig | Variable | M-of-N: Ed25519 + Secp256r1 member proofs in one tx |

Secp256r1 signers use the Secp256r1SigVerify precompile. The precompile instruction must be included in the same transaction — the program verifies it via instruction sysvar introspection. The SDK handles this automatically via finalizeTransaction.

Multisig signers embed a MultisigKey { threshold, members } directly in the signer slot. All member proofs (Ed25519 tx signatures + Secp256r1 precompile instructions) must appear in the same transaction. There is no on-chain proposal state — this mirrors the Stellar smart account multisig model.