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

smart-multisig-engine

v0.1.1

Published

A TypeScript utility library for submitting transactions to Safe multisig wallets and retrieving their `safeTxHash` from the Safe Transaction Service.

Readme

smart-multisig-engine

A TypeScript utility library for submitting transactions to Safe multisig wallets and retrieving their safeTxHash from the Safe Transaction Service.

Built on wagmi and viem.

Install

pnpm add smart-multisig-engine

Peer dependencies: @wagmi/core, viem.

Optional peer dependencies for React hooks: react, @tanstack/react-query.

Quick Start

Use submitTx with the generic adapter interface to submit transactions to any supported multisig:

import { submitTx, waitForExecution } from "smart-multisig-engine";
import { config } from "./wagmi-config";

// Submit a transaction
const { txHash } = await submitTx({
  adapter: "safe",
  config,
  walletAddress: "0xYourSafeAddress",
  address: "0xTargetContract",
  abi: contractAbi,
  functionName: "transfer",
  args: [recipientAddress, amount],
  value: 0n,
  chainId: 1n,
});

console.log("Safe TX Hash:", txHash);

// Wait for execution
const { transactionHash } = await waitForExecution({
  adapter: "safe",
  txHash,
  chainId: 1n,
});

console.log("On-chain TX Hash:", transactionHash);

Or use the Safe adapter directly for more control:

import { safe } from "smart-multisig-engine";
import { config } from "./wagmi-config";

const { safeTxHash, txHash } = await safe.submitAndFindSafeTx({
  config,
  safeAddress: "0xYourSafeAddress",
  address: "0xTargetContract",
  abi: contractAbi,
  functionName: "transfer",
  args: [recipientAddress, amount],
  value: 0n,
  chainId: 1,
});

console.log("Safe TX Hash:", safeTxHash);
console.log("On-chain TX Hash:", txHash);

Architecture

The library follows a core/adapter pattern:

  • Core — Pure functions with no I/O. Can be used independently for encoding, matching, or URL resolution.
  • Adapters — I/O implementations that wrap contract calls and external APIs. Currently supports Safe multisig.

Generic Adapter Interface

The library provides adapter-agnostic entry points that route to the appropriate implementation:

import { submitTx, waitForExecution, fetchPendingTxs, simulate, write } from "smart-multisig-engine";

// All functions take an `adapter` parameter to select the implementation
await submitTx({ adapter: "safe", ... });
await waitForExecution({ adapter: "safe", ... });
await fetchPendingTxs({ adapter: "safe", ... });
await simulate({ adapter: "safe", ... });
await write({ adapter: "safe", ... });

Direct Adapter Access

For adapter-specific features, import the adapter namespace directly:

import { safe } from "smart-multisig-engine";

// Access Safe-specific functions
await safe.submitAndFindSafeTx({ ... });
await safe.waitForExecution({ ... });
await safe.fetchPendingTransactions({ ... });
await safe.simulateContractCall(config, { ... });
await safe.writeContractCall(config, request);

API Reference

Generic Adapter Functions

These functions provide a unified interface across all adapters.

submitTx(options): Promise<SubmitTxResult>

Submit a transaction through the specified adapter.

interface SubmitTxOptions {
  adapter: "safe";             // Adapter type
  config: Config;              // wagmi Config instance
  walletAddress: Address;      // The multisig wallet address
  address: Address;            // Target contract address
  abi: Abi;                    // Contract ABI
  functionName: string;        // Function to call
  args?: readonly unknown[];   // Function arguments
  value?: bigint;              // ETH value (default: 0n)
  chainId?: number;            // Chain ID
  txServiceUrl?: string;       // Override service URL
  apiKey?: string;             // API key for the service
  pollingInterval?: number;    // Polling interval in ms
  maxAttempts?: number;        // Max poll attempts
}

interface SubmitTxResult {
  txHash: string;              // The transaction hash (safeTxHash for Safe)
}

waitForExecution(options): Promise<WaitForExecutionResult>

Wait for a transaction to be executed on-chain.

interface WaitForExecutionOptions {
  adapter: "safe";             // Adapter type
  txHash: string;              // Transaction hash to wait for
  chainId: bigint;             // Chain ID
  txServiceUrl?: string;       // Override service URL
  apiKey?: string;             // API key for the service
  pollingInterval?: number;    // Polling interval in ms (default: 5000)
  maxAttempts?: number;        // Max poll attempts (default: 60)
}

interface WaitForExecutionResult {
  transactionHash: string;     // The on-chain transaction hash
}

fetchPendingTxs(options): Promise<unknown[]>

Fetch pending transactions for a wallet.

interface FetchPendingOptions {
  adapter: "safe";             // Adapter type
  walletAddress: string;       // The multisig wallet address
  chainId: bigint;             // Chain ID
  txServiceUrl?: string;       // Override service URL
  apiKey?: string;             // API key for the service
}

simulate(options): Promise<unknown>

Simulate a contract call before submission.

interface SimulateOptions {
  adapter: "safe";             // Adapter type
  config: Config;              // wagmi Config instance
  address: Address;            // Target contract address
  abi: Abi;                    // Contract ABI
  functionName: string;        // Function to call
  args?: readonly unknown[];   // Function arguments
  value?: bigint;              // ETH value
  chainId?: number;            // Chain ID
}

write(options): Promise<string>

Write a contract call (submit the transaction).

interface WriteOptions {
  adapter: "safe";             // Adapter type
  config: Config;              // wagmi Config instance
  request: unknown;            // Request from simulation result
}

Safe Adapter

safe.submitAndFindSafeTx(options): Promise<SubmitAndFindSafeTxResult>

Full flow: simulate → write → poll Safe TX Service → match → return safeTxHash.

interface SubmitAndFindSafeTxOptions {
  config: Config;              // wagmi Config instance
  safeAddress: Address;        // The Safe wallet address to poll
  address: Address;            // Target contract address
  abi: Abi;                    // Contract ABI
  functionName: string;        // Function to call
  args?: readonly unknown[];   // Function arguments
  value?: bigint;              // ETH value (default: 0n)
  chainId?: number;            // Chain ID (auto-detected from config if omitted)
  account?: Address;           // Signer address
  serviceUrl?: string;         // Override Safe TX Service URL
  pollingInterval?: number;    // Polling interval in ms (default: 3000)
  maxAttempts?: number;        // Max poll attempts (default: 20)
}

interface SubmitAndFindSafeTxResult {
  safeTxHash: string;          // The Safe transaction hash
  txHash: `0x${string}`;       // The on-chain transaction hash
}

safe.waitForExecution(options): Promise<WaitForExecutionResult>

Poll the Safe Transaction Service until a transaction is executed.

interface WaitForExecutionOptions {
  safeTxHash: string;          // The Safe transaction hash to monitor
  chainId: bigint;             // Chain ID
  txServiceUrl?: string;       // Override Safe TX Service URL
  apiKey?: string;             // API key for the Safe service
  pollingInterval?: number;    // Polling interval in ms (default: 5000)
  maxAttempts?: number;        // Max poll attempts (default: 60)
}

interface WaitForExecutionResult {
  transactionHash: string;     // The on-chain transaction hash
}

Core Utilities

extractCallData(params): EncodedCallData

Encodes a contract function call into { to, data, value } using viem's encodeFunctionData. Pure function, no network calls.

import { extractCallData } from "smart-multisig-engine";

const callData = extractCallData({
  abi: myAbi,
  functionName: "transfer",
  args: [recipient, amount],
  address: "0xContractAddress",
  value: 0n,
});
// { to: "0x...", data: "0x...", value: 0n }

matchPendingTransaction(pendingTxs, criteria): SafePendingTransaction | undefined

Finds a pending Safe transaction matching { to, value, data }. Comparison is case-insensitive for addresses and converts bigint value to string for matching.

import { matchPendingTransaction } from "smart-multisig-engine";

const match = matchPendingTransaction(pendingTransactions, {
  to: "0xContractAddress",
  value: 0n,
  data: "0xEncodedCalldata",
});

getSafeServiceUrl(chainId): string

Resolves the Safe Transaction Service base URL for a chain ID. Throws if the chain is not in the known list.

import { getSafeServiceUrl } from "smart-multisig-engine";

getSafeServiceUrl(1);        // "https://safe-transaction-mainnet.safe.global"
getSafeServiceUrl(11155111); // "https://safe-transaction-sepolia.safe.global"

SAFE_TX_SERVICE_URLS

The Record<number, string> map of supported chain IDs:

| Chain ID | Network | |----------|---------| | 1 | Ethereum Mainnet | | 10 | Optimism | | 56 | BSC | | 100 | Gnosis Chain | | 137 | Polygon | | 8453 | Base | | 42161 | Arbitrum | | 11155111 | Sepolia | | 84532 | Base Sepolia |


Safe Low-Level Utilities

safe.simulateContractCall(config, params)

Wraps wagmi's simulateContract. Validates the transaction will succeed before submission.

import { safe } from "smart-multisig-engine";

const simulation = await safe.simulateContractCall(config, {
  address: "0xContract",
  abi: myAbi,
  functionName: "transfer",
  args: [recipient, amount],
});

safe.writeContractCall(config, request): Promise<Hex>

Wraps wagmi's writeContract. Accepts the request from a prior simulation result.

import { safe } from "smart-multisig-engine";

const txHash = await safe.writeContractCall(config, simulation.request);

safe.fetchPendingTransactions(options): Promise<SafePendingTransaction[]>

Fetches pending (unexecuted) multisig transactions from the Safe Transaction Service REST API.

import { safe } from "smart-multisig-engine";

const pending = await safe.fetchPendingTransactions({
  safeAddress: "0xSafeAddress",
  chainId: 1n,
  apiKey: "optional-api-key",
});

Using Individual Bricks

The high-level functions are convenience wrappers. You can compose the bricks yourself for custom flows:

import {
  extractCallData,
  matchPendingTransaction,
  safe,
} from "smart-multisig-engine";

// 1. Encode the call data
const callData = extractCallData({ abi, functionName, args, address, value });

// 2. Simulate
const simulation = await safe.simulateContractCall(config, { abi, functionName, args, address, value });

// 3. Submit on-chain
const txHash = await safe.writeContractCall(config, simulation.request);

// 4. Poll for the pending Safe transaction
const pending = await safe.fetchPendingTransactions({
  safeAddress,
  chainId: 1n,
});

// 5. Match
const match = matchPendingTransaction(pending, callData);
console.log(match?.safeTxHash);

// 6. Wait for execution
const result = await safe.waitForExecution({
  safeTxHash: match.safeTxHash,
  chainId: 1n,
});
console.log("Executed:", result.transactionHash);

React Hooks

The library provides React Query hooks via a separate entry point. These hooks wrap the adapter functions and provide a familiar React Query interface.

# Requires these peer dependencies
pnpm add react @tanstack/react-query
import {
  useSubmitTx,
  useWaitForExecution,
  useWaitForExecutionReceipt,
  useFetchPendingTxs,
  useSimulate,
  useWrite,
} from "smart-multisig-engine/react";

Hooks Overview

| Hook | Type | Description | |------|------|-------------| | useSubmitTx | Mutation | Submit a transaction through an adapter | | useWaitForExecution | Query | Wait for a transaction to be executed | | useWaitForExecutionReceipt | Query | Wait for execution + fetch receipt with logs | | useFetchPendingTxs | Query | Fetch pending transactions for a wallet | | useSimulate | Query | Simulate a contract call | | useWrite | Mutation | Write a contract call |

useSubmitTx

Mutation hook for submitting transactions.

const { mutate, mutateAsync, isPending, isSuccess, data } = useSubmitTx();

// Submit a transaction
mutate({
  adapter: "safe",
  config,
  walletAddress: "0xSafe...",
  address: "0xContract...",
  abi,
  functionName: "transfer",
  args: [recipient, amount],
  chainId: 11155111n,
});

// Or with async/await
const { txHash } = await mutateAsync({ ... });

useWaitForExecution

Query hook that waits for a Safe transaction to be executed. Maintains a stable loading state throughout polling (similar to wagmi's useWaitForTransactionReceipt).

interface UseWaitForExecutionOptions {
  adapter: "safe";
  txHash: string;
  chainId: bigint;
  txServiceUrl?: string;
  apiKey?: string;
  enabled?: boolean;           // Default: true
  pollingInterval?: number;    // Default: 5000ms
  timeout?: number;            // Default: 120000ms (2 min)
}
const { data, isLoading, isSuccess, isError } = useWaitForExecution({
  adapter: "safe",
  txHash: safeTxHash,
  chainId: 11155111n,
  timeout: 120000,  // Fail after 2 minutes
  enabled: !!safeTxHash,
});

// States are stable throughout polling:
// - isLoading = true while waiting
// - isSuccess = true once executed
// - isError = true if timeout reached

if (isSuccess) {
  console.log("Executed:", data.transactionHash);
}

useWaitForExecutionReceipt

Query hook that waits for execution AND fetches the full transaction receipt with logs.

interface UseWaitForExecutionReceiptOptions {
  adapter: "safe";
  txHash: string;
  chainId: bigint;
  config: Config;              // wagmi config for fetching receipt
  txServiceUrl?: string;
  apiKey?: string;
  enabled?: boolean;           // Default: true
  pollingInterval?: number;    // Default: 5000ms
  timeout?: number;            // Default: 120000ms
  confirmations?: number;      // Default: 1
}
const { data, isLoading, isSuccess } = useWaitForExecutionReceipt({
  adapter: "safe",
  txHash: safeTxHash,
  chainId: 11155111n,
  config: wagmiConfig,
  confirmations: 1,
});

if (isSuccess) {
  console.log("Transaction hash:", data.transactionHash);
  console.log("Logs:", data.receipt.logs);
  console.log("Gas used:", data.receipt.gasUsed);
}

useFetchPendingTxs

Query hook for fetching pending transactions.

const { data: pending, isLoading } = useFetchPendingTxs({
  adapter: "safe",
  walletAddress: "0xSafe...",
  chainId: 11155111n,
  refetchInterval: 10000,  // Optional: auto-refresh every 10s
});

Full Example

import { useSubmitTx, useWaitForExecutionReceipt } from "smart-multisig-engine/react";

function SafeTransaction() {
  const submitTx = useSubmitTx();

  const { data, isLoading, isSuccess } = useWaitForExecutionReceipt({
    adapter: "safe",
    txHash: submitTx.data?.txHash,
    chainId: 11155111n,
    config: wagmiConfig,
    enabled: !!submitTx.data?.txHash,
  });

  const handleSubmit = () => {
    submitTx.mutate({
      adapter: "safe",
      config: wagmiConfig,
      walletAddress: safeAddress,
      address: contractAddress,
      abi: contractAbi,
      functionName: "transfer",
      args: [recipient, amount],
      chainId: 11155111n,
    });
  };

  return (
    <div>
      <button onClick={handleSubmit} disabled={submitTx.isPending}>
        {submitTx.isPending ? "Submitting..." : "Submit Transaction"}
      </button>

      {submitTx.isSuccess && !isSuccess && (
        <p>Waiting for execution... {isLoading && "(polling)"}</p>
      )}

      {isSuccess && (
        <div>
          <p>Executed: {data.transactionHash}</p>
          <p>Gas used: {data.receipt.gasUsed.toString()}</p>
        </div>
      )}
    </div>
  );
}

Development

pnpm install
pnpm build        # Bundle with tsup (ESM + CJS + .d.ts)
pnpm dev          # Watch mode
pnpm test         # Run vitest (watch mode)
pnpm test --run   # Run tests once
pnpm typecheck    # tsc --noEmit
pnpm clean        # Remove dist/

License

ISC