Mini Apps

Local simulator

Install simulated Nimiq and Ethereum providers for local development and tests.

Use @onmax/nimiq-mini-app-kit/dev when local code needs Nimiq Pay-like provider behavior without the real host app.

Do not ship simulator providers in production bundles. Production apps should read providers published by Nimiq Pay.

Choose the simulator mode

Use installSimulatedNimiqProvider() or installSimulatedEthereumProvider() when a browser app expects window.nimiq or window.ethereum.

Use createSimulatedNimiqProvider() or createSimulatedEthereumProvider() in unit tests where you can pass a provider directly.

Use createSimulatedHostRuntime() when you are building a local host shell with approval UI and wallet state.

Use blocking providers when you want quick browser confirm() prompts without a custom host shell.

Install providers in development only

Install simulated providers behind a development guard before app code reads them.

app/dev-mini-app-provider.ts
import {
  installSimulatedEthereumProvider,
  installSimulatedNimiqProvider,
} from '@onmax/nimiq-mini-app-kit/dev'

const NIMIQ_PRIVATE_KEY = '4f4e9c6d93fab8a1716dd2136bfdcf8bc6bfcb7e5fcb868f1f8f1eb8bbf4d2f0'
const ETHEREUM_ACCOUNT = '0xf5d7f7c63cb8F6d9Db7D2674bfaB3F5D2aED2fcb'

export async function installLocalMiniAppProviders() {
  if (!import.meta.env.DEV)
    return

  await installSimulatedNimiqProvider({
    privateKeyHex: NIMIQ_PRIVATE_KEY,
    blockNumber: 1_000_000,
    consensusEstablished: true,
  })

  await installSimulatedEthereumProvider({
    account: ETHEREUM_ACCOUNT,
    chainId: '0x89',
    connected: true,
    tokenContracts: {
      '0xc2132d05d31c914a87c6611c10748aeb04b58e8f': {
        decimals: 6,
        symbol: 'USDT',
        balancesByAddress: {
          [ETHEREUM_ACCOUNT.toLowerCase()]: '1000000',
        },
      },
    },
  })
}

Expected behavior:

  • window.nimiq is available after the first installer resolves.
  • window.ethereum is available after the second installer resolves.
  • The Ethereum provider announces itself as Nimiq Pay Simulator through EIP-6963.

Test delayed injection and missing providers

Use injectionDelayMs and missingProvider to prove unavailable-provider UI.

test/provider-injection.test.ts
import { expect, it } from 'vitest'
import {
  installSimulatedNimiqProvider,
  waitForInjectedNimiqProvider,
} from '@onmax/nimiq-mini-app-kit/dev'

it('waits for delayed window.nimiq injection', async () => {
  const source = { window: {} as Window }

  const installed = installSimulatedNimiqProvider({
    source,
    injectionDelayMs: 20,
  })

  expect(source.window.nimiq).toBeUndefined()
  await installed

  await expect(waitForInjectedNimiqProvider({
    source: () => source.window.nimiq,
    timeoutMs: 100,
    intervalMs: 5,
  })).resolves.toBe(source.window.nimiq)
})

Expected behavior:

  • Before the installer resolves, no provider exists.
  • After the delay, waitForInjectedNimiqProvider() resolves to the injected provider.
  • With missingProvider: true, the installer removes window.nimiq.

Drive error states from a controller

Use a controller when a test needs to mutate provider behavior between assertions.

test/sign-error.test.ts
import { expect, it } from 'vitest'
import { createSimulatedNimiqProviderController } from '@onmax/nimiq-mini-app-kit/dev'

it('shows a rejected signature state', async () => {
  const controller = createSimulatedNimiqProviderController()

  controller.setConfig({ signError: 'Rejected in host shell' })

  await expect(controller.provider.sign('hello')).resolves.toEqual({
    error: {
      type: 'SIGN_ERROR',
      message: 'Rejected in host shell',
    },
  })
})

Simulate Ethereum reads and writes

Use createSimulatedEthereumProvider() for deterministic EIP-1193 behavior in unit tests.

test/ethereum-transfer.test.ts
import { expect, it } from 'vitest'
import { createSimulatedEthereumProvider } from '@onmax/nimiq-mini-app-kit/dev'

it('updates native balances after a simulated transfer', async () => {
  const account = '0xf5d7f7c63cb8F6d9Db7D2674bfaB3F5D2aED2fcb'
  const recipient = '0xabcdefabcdefabcdefabcdefabcdefabcdefabcd'
  const provider = createSimulatedEthereumProvider({
    account,
    connected: true,
    chainId: '0x89',
    nativeBalances: {
      [account.toLowerCase()]: '0x64',
    },
  })

  await provider.request({
    method: 'eth_sendTransaction',
    params: [{ from: account, to: recipient, value: '0xa' }],
  })

  await expect(provider.request({
    method: 'eth_getBalance',
    params: [account, 'latest'],
  })).resolves.toBe('0x5a')

  await expect(provider.request({
    method: 'eth_getBalance',
    params: [recipient, 'latest'],
  })).resolves.toBe('0xa')
})

Use blocking prompts for a fast manual loop

Use blocking providers when you want direct browser confirmation dialogs and do not need a simulator sidecar.

app/blocking-dev-provider.ts
import {
  createSimulatedBlockingEthereumProvider,
  createSimulatedBlockingNimiqProvider,
} from '@onmax/nimiq-mini-app-kit/dev'

export function installBlockingProviders() {
  if (!import.meta.env.DEV)
    return

  window.nimiq = createSimulatedBlockingNimiqProvider({
    source: window,
    appName: 'Dino',
    appOrigin: window.location.origin,
  })

  window.ethereum = createSimulatedBlockingEthereumProvider({
    source: window,
    appName: 'Dino',
    appOrigin: window.location.origin,
    input: {
      account: '0xf5d7f7c63cb8F6d9Db7D2674bfaB3F5D2aED2fcb',
    },
  })
}

Expected behavior:

  • listAccounts() asks to connect the Nimiq account.
  • eth_requestAccounts asks to connect the Ethereum account.
  • Sign, chain, and transaction requests use browser confirmation dialogs.

Simulator coverage

  • Nimiq account reads, signatures, transaction hashes, consensus state, and block height.
  • Nimiq transaction previews for basic, data, and staking methods.
  • Ethereum account, sign, chain, read-only RPC, native transfer, and ERC-20 transfer scenarios.
  • EIP-6963 provider announcement.
  • Host bridge messages for custom simulator shells.
Copyright © 2026