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

@miradorlabs/plugins

v1.0.1

Published

Mirador SDK plugins for blockchain transaction and Safe multisig tracing

Readme

@miradorlabs/plugins

Shared plugin system for the Mirador SDKs (@miradorlabs/web-sdk and @miradorlabs/nodejs-sdk). Plugins extend traces with additional methods and contribute data during flush — without any proto dependencies.

Architecture

plugins/                    # This package (proto-free)
├── src/
│   ├── plugin.ts           # Core interfaces: MiradorPlugin, TraceContext, FlushBuilder
│   ├── hints.ts            # HintType constants + HintDataMap type registry
│   ├── types.ts            # Shared types: Chain, ChainInput, TxHashHint, Logger, etc.
│   ├── chains.ts           # toChain(), resolveChainInput() utilities
│   ├── web3-plugin.ts      # Web3Plugin — tx hints, sendTransaction, provider mgmt, Safe hints
│   └── index.ts            # Public exports

web-sdk/src/ingest/
├── hint-serializers.ts     # Proto serializers (protobuf.js class API)
├── trace.ts                # createFlushBuilder() dispatches via HINT_SERIALIZERS
└── ...

nodejs-sdk/src/ingest/
├── hint-serializers.ts     # Proto serializers (ts-proto interface API)
├── trace.ts                # createFlushBuilder() dispatches via HINT_SERIALIZERS
└── ...

Key design: Plugins call builder.addHint(type, data) with plain JS objects. Each SDK's hint-serializers.ts maps hint types to proto-specific serialization. This keeps plugins proto-free while both SDKs serialize correctly.

Using Existing Plugins

Web3Plugin

Adds blockchain transaction tracing methods under trace.web3.evm and Gnosis Safe multisig tracking under trace.web3.safe.

import { Client, Web3Plugin } from '@miradorlabs/web-sdk';
// or: import { Client, Web3Plugin } from '@miradorlabs/nodejs-sdk';

const client = new Client('your-api-key', {
  plugins: [Web3Plugin({ provider: window.ethereum })],
});

const trace = client.trace({ name: 'swap' });

// EVM methods (under web3.evm namespace):
trace.web3.evm.addTxHint('0x123...', 'ethereum');               // Record a tx hash
trace.web3.evm.addTxHint('0x456...', 'polygon', { input: '0x...' }); // With calldata
trace.web3.evm.addTx({ hash: '0x...', chainId: 1 });            // From a tx object
trace.web3.evm.addInputData('0xabcdef...');                      // Raw calldata
trace.web3.evm.resolveChain('ethereum');                         // Resolve chain name

// Provider management:
trace.web3.evm.setProvider(newProvider);                          // Switch provider
trace.web3.evm.getProviderChain();                               // Get detected chain

// Send a transaction (auto-captures tx hash + chain):
const txHash = await trace.web3.evm.sendTransaction(txParams);
// Or with an explicit provider:
const txHash2 = await trace.web3.evm.sendTransaction(txParams, otherProvider);

// Safe methods (under web3.safe namespace):
trace.web3.safe.addMsgHint('0xmsg...', 'ethereum', 'Approval message');
trace.web3.safe.addTxHint('0xsafetx...', 'ethereum', 'Execution tx');

Method Chaining

All void-returning plugin methods support chaining. Chained calls return the root Trace, so you can mix namespaces and core methods freely:

trace
  .web3.evm.addTxHint('0x123...', 'ethereum')
  .web3.safe.addMsgHint('0xabc...', 'ethereum')
  .addAttribute('user', '0xdef...')
  .addTag('swap');

Creating a New Plugin

1. Define Your Plugin

A plugin implements MiradorPlugin<TMethods>:

import type {
  MiradorPlugin,
  PluginSetupResult,
  TraceContext,
  FlushBuilder,
} from '@miradorlabs/plugins';

// Define the methods your plugin adds to Trace
export interface MyMethods {
  trackAction(name: string, data: Record<string, unknown>): void;
  getActionCount(): number;
}

export function MyPlugin(): MiradorPlugin<MyMethods> {
  return {
    name: 'my-plugin', // Unique name

    setup(ctx: TraceContext): PluginSetupResult<MyMethods> {
      // Plugin-local state (closure-scoped, per-trace)
      const pendingActions: Array<{ name: string; data: Record<string, unknown>; timestamp: Date }> = [];

      // Method implementations
      function trackAction(name: string, data: Record<string, unknown>): void {
        if (ctx.isClosed()) {
          ctx.logger.warn('[MyPlugin] Trace is closed, ignoring trackAction');
          return;
        }
        pendingActions.push({ name, data, timestamp: new Date() });
        ctx.addEvent(`action:${name}`, data);   // Use core trace primitives
        ctx.addAttribute('last_action', name);
        ctx.scheduleFlush();                     // Trigger batched flush
      }

      function getActionCount(): number {
        return pendingActions.length;
      }

      // Lifecycle hooks
      function onFlush(builder: FlushBuilder): void {
        // Contribute data to the flush payload
        for (const action of pendingActions) {
          builder.addEvent({
            name: `plugin:${action.name}`,
            details: JSON.stringify(action.data),
            timestamp: action.timestamp,
          });
        }
        pendingActions.length = 0; // Clear after flush
      }

      function hasPendingData(): boolean {
        return pendingActions.length > 0;
      }

      function onClose(): void {
        pendingActions.length = 0; // Cleanup
      }

      return {
        methods: { trackAction, getActionCount },
        noopMethods: { getActionCount: () => 0 }, // For sampled-out traces
        onFlush,
        onClose,
        hasPendingData,
      };
    },
  };
}

2. Custom Namespaces

Plugins can nest their methods under namespaces by using nested objects in the TMethods type. The type system and runtime both handle arbitrary nesting automatically.

// Define methods under a namespace
export interface AnalyticsMethods {
  analytics: {
    track(event: string, data?: Record<string, unknown>): void;
    identify(userId: string): void;
    getSessionId(): string;
  };
}

export function AnalyticsPlugin(): MiradorPlugin<AnalyticsMethods> {
  return {
    name: 'analytics',
    setup(ctx: TraceContext): PluginSetupResult<AnalyticsMethods> {
      const sessionId = crypto.randomUUID();

      return {
        methods: {
          analytics: {
            track(event, data) {
              ctx.addEvent(`analytics:${event}`, data);
              ctx.scheduleFlush();
            },
            identify(userId) {
              ctx.addAttribute('analytics.userId', userId);
            },
            getSessionId() {
              return sessionId;
            },
          },
        },
        noopMethods: {
          analytics: { getSessionId: () => '' },
        },
      };
    },
  };
}

Usage:

const client = new Client('key', {
  plugins: [Web3Plugin(), AnalyticsPlugin()],
});

const trace = client.trace({ name: 'swap' });

// Namespaced access
trace.analytics.track('page_view', { page: '/swap' });
trace.analytics.identify('user123');
trace.analytics.getSessionId(); // Returns the session ID

// Chaining across namespaces — void methods return the root Trace
trace.analytics.track('click')
     .web3.evm.addTxHint('0x...', 'ethereum')
     .analytics.identify('user123')
     .addAttribute('key', 'value');

Multiple plugins can share a top-level namespace. TypeScript's intersection merges them automatically:

// Plugin A: { myNs: { foo(): void } }
// Plugin B: { myNs: { bar(): void } }
// Result:   trace.myNs.foo() and trace.myNs.bar() both work

You can also nest arbitrarily deep: { a: { b: { c: { doThing(): void } } } } works.

3. Use Your Plugin

const client = new Client('key', {
  plugins: [Web3Plugin(), MyPlugin()],
});

const trace = client.trace({ name: 'test' });
trace.trackAction('click', { button: 'submit' }); // Your method
trace.web3.evm.addTxHint('0x...', 'ethereum');     // Web3Plugin still works
trace.getActionCount();                            // Returns 1

4. Plugin Lifecycle

client.trace({ name: 'test' })
  │
  ├── plugin.setup(ctx) called for each plugin
  │     └── Returns { methods, onFlush, onClose, hasPendingData }
  │     └── methods are recursively merged onto the Trace instance (supports nested namespaces)
  │
  ├── trace.trackAction(...)        ← Your plugin method
  │     └── Buffers data, calls ctx.scheduleFlush()
  │
  ├── [microtask] flush triggered
  │     ├── SDK builds FlushTraceData (events, attributes, tags)
  │     ├── SDK creates FlushBuilder wrapping FlushTraceData
  │     └── plugin.onFlush(builder) called for each plugin
  │           └── Plugin dumps buffered data via builder
  │
  └── trace.close()
        └── plugin.onClose() called for each plugin

5. TraceContext API

The ctx object provides these methods for plugins:

| Method | Description | |--------|-------------| | ctx.addEvent(name, details?, options?) | Record an event (options: captureStackTrace, severity) | | ctx.addAttribute(key, value) | Set a trace attribute | | ctx.addAttributes(attrs) | Set multiple attributes | | ctx.addTag(tag) | Add a tag | | ctx.addTags(tags) | Add multiple tags | | ctx.getTraceId() | Get the trace ID | | ctx.isClosed() | Check if trace is closed | | ctx.scheduleFlush() | Trigger a batched flush | | ctx.logger | Logger instance (debug, warn, error) |

6. FlushBuilder API

The builder object in onFlush provides:

| Method | Description | |--------|-------------| | builder.addHint(type, data) | Add a typed hint (see hint types below) | | builder.addEvent(event) | Add an event ({ name, details?, timestamp, severity? }) | | builder.addAttribute(key, value) | Add an attribute | | builder.addTag(tag) | Add a tag |

Adding a Custom Hint Type

If your plugin needs to contribute structured data beyond events/attributes (e.g., a new proto field on the backend), you need to register a hint type.

1. Add the hint type constant

In plugins/src/hints.ts:

export const HintType = {
  TX_HASH: 'tx_hash',
  SAFE_MSG: 'safe_msg',
  SAFE_TX: 'safe_tx',
  MY_HINT: 'my_hint',  // Add your type
} as const;

2. Add the data shape to HintDataMap

In plugins/src/hints.ts:

export interface HintDataMap {
  // ... existing entries
  [HintType.MY_HINT]: {
    field1: string;
    field2: number;
    chain: Chain;
    timestamp: Date;
  };
}

3. Add serializers in each SDK

In web-sdk/src/ingest/hint-serializers.ts and nodejs-sdk/src/ingest/hint-serializers.ts, add an entry to HINT_SERIALIZERS that maps your hint type to proto serialization.

4. Use in your plugin's onFlush

function onFlush(builder: FlushBuilder): void {
  for (const item of pendingItems) {
    builder.addHint(HintType.MY_HINT, item); // Type-safe!
  }
  pendingItems.length = 0;
}

Best Practices

  • Check ctx.isClosed() before buffering data in plugin methods
  • Call ctx.scheduleFlush() after adding data — this batches flushes via microtask
  • Clear buffers in onFlush — set .length = 0 after iterating
  • Clear buffers in onClose — prevent memory leaks
  • Implement hasPendingData() — the SDK uses this to decide whether to flush
  • Provide noopMethods for methods that return values — these are used for sampled-out traces
  • Use unique plugin names — duplicate names will log a warning

Building

npm run build   # Outputs to dist/ (ESM + CJS + type declarations)

Both SDKs depend on this package via "@miradorlabs/plugins": "file:../plugins" and inline it into their bundles via Rollup's node-resolve plugin. Consumers of the SDKs don't need to install this package separately.