> ## Documentation Index
> Fetch the complete documentation index at: https://cantonfoundation-feature-integrate-google-analytics.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Local Testing

> Docker-Compose based deployment of a local Canton Network for development and testing

export const networkData = {
  mainnet: {
    name: 'MainNet',
    description: 'Production network for live applications. Data is permanent and never reset. Upgrades follow DevNet and TestNet validation.',
    color: '#22c55e',
    versions: {
      splice: '0.5.18',
      damlSdk: '3.4.12',
      pqs: '3.4.6',
      tokenStandard: '1.0.0',
      walletSdk: '1.1.0',
      dappSdk: '1.1.0',
      walletGateway: '1.2.0'
    },
    advanced: {
      minProtocolVersion: '6',
      migrationId: '4',
      darVersions: [{
        name: 'splice-amulet',
        version: '0.1.14'
      }, {
        name: 'splice-wallet',
        version: '0.1.14'
      }, {
        name: 'splice-dso-governance',
        version: '0.1.20'
      }],
      releaseUrl: 'https://github.com/canton-network/splice/releases/tag/0.5.18'
    },
    endpoint: 'scan.sv-1.global.canton.network.sync.global',
    substitutions: {
      splice_cluster: 'main',
      da_hostname: 'global.canton.network.digitalasset.com',
      gsf_sv_url: 'https://sv.sv-1.global.canton.network.sync.global',
      generic_sv_url: 'https://sv.sv-1.global.canton.network.YOUR_SV_SPONSOR',
      gsf_scan_url: 'https://scan.sv-1.global.canton.network.sync.global',
      generic_scan_url: 'https://scan.sv-1.global.canton.network.YOUR_SV_SPONSOR',
      gsf_sequencer_url: 'https://sequencer-MIGRATION_ID.sv-1.global.canton.network.sync.global',
      version: '0.5.18',
      version_literal: '0.5.18',
      chart_version_literal: '0.5.18',
      chart_version_set: 'export CHART_VERSION=0.5.18',
      image_tag_set: 'export IMAGE_TAG=0.5.18',
      image_tag_set_plain: 'export IMAGE_TAG=0.5.18',
      bundle_download_link: {
        label: 'Download Bundle',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.5.18/0.5.18_splice-node.tar.gz'
      },
      openapi_download_link: {
        label: 'Download OpenAPI specs',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.5.18/0.5.18_openapi.tar.gz'
      },
      helm_repo_prefix: 'oci://ghcr.io/digital-asset/decentralized-canton-sync/helm',
      docker_repo_prefix: 'ghcr.io/digital-asset/decentralized-canton-sync/docker'
    }
  },
  testnet: {
    name: 'TestNet',
    description: 'Pre-production environment for final validation. Requires MainNet approval to join. May be reset periodically.',
    color: '#eab308',
    versions: {
      splice: '0.6.2',
      damlSdk: '3.5.1',
      pqs: '3.5.1-rc6',
      tokenStandard: '1.0.0',
      walletSdk: '1.1.0',
      dappSdk: '1.1.0',
      walletGateway: '1.2.0'
    },
    advanced: {
      minProtocolVersion: '6',
      migrationId: '1',
      darVersions: [{
        name: 'splice-amulet',
        version: '0.1.15'
      }, {
        name: 'splice-wallet',
        version: '0.1.15'
      }, {
        name: 'splice-dso-governance',
        version: '0.1.21'
      }],
      releaseUrl: 'https://github.com/canton-network/splice/releases/tag/0.6.2'
    },
    endpoint: 'scan.sv-1.test.global.canton.network.sync.global',
    substitutions: {
      splice_cluster: 'test',
      da_hostname: 'test.global.canton.network.digitalasset.com',
      gsf_sv_url: 'https://sv.sv-1.test.global.canton.network.sync.global',
      generic_sv_url: 'https://sv.sv-1.test.global.canton.network.YOUR_SV_SPONSOR',
      gsf_scan_url: 'https://scan.sv-1.test.global.canton.network.sync.global',
      generic_scan_url: 'https://scan.sv-1.test.global.canton.network.YOUR_SV_SPONSOR',
      gsf_sequencer_url: 'https://sequencer-MIGRATION_ID.sv-1.test.global.canton.network.sync.global',
      version: '0.6.2',
      version_literal: '0.6.2',
      chart_version_literal: '0.6.2',
      chart_version_set: 'export CHART_VERSION=0.6.2',
      image_tag_set: 'export IMAGE_TAG=0.6.2',
      image_tag_set_plain: 'export IMAGE_TAG=0.6.2',
      bundle_download_link: {
        label: 'Download Bundle',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.6.2/0.6.2_splice-node.tar.gz'
      },
      openapi_download_link: {
        label: 'Download OpenAPI specs',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.6.2/0.6.2_openapi.tar.gz'
      },
      helm_repo_prefix: 'oci://ghcr.io/digital-asset/decentralized-canton-sync/helm',
      docker_repo_prefix: 'ghcr.io/digital-asset/decentralized-canton-sync/docker'
    }
  },
  devnet: {
    name: 'DevNet',
    description: 'Development environment with latest features. Open to any validator (IP allowlist required). Reset every 3 months. Best for testing upgrades.',
    color: '#a78bfa',
    versions: {
      splice: '0.6.4',
      damlSdk: '3.5.1',
      pqs: '3.5.1-rc6',
      tokenStandard: '1.0.0',
      walletSdk: '1.1.0',
      dappSdk: '1.1.0',
      walletGateway: '1.2.0'
    },
    advanced: {
      minProtocolVersion: '6',
      migrationId: '1',
      darVersions: [{
        name: 'splice-amulet',
        version: '0.1.15'
      }, {
        name: 'splice-wallet',
        version: '0.1.15'
      }, {
        name: 'splice-dso-governance',
        version: '0.1.21'
      }],
      releaseUrl: 'https://github.com/canton-network/splice/releases/tag/0.6.4'
    },
    endpoint: 'scan.sv-1.dev.global.canton.network.sync.global',
    substitutions: {
      splice_cluster: 'dev',
      da_hostname: 'dev.global.canton.network.digitalasset.com',
      gsf_sv_url: 'https://sv.sv-1.dev.global.canton.network.sync.global',
      generic_sv_url: 'https://sv.sv-1.dev.global.canton.network.YOUR_SV_SPONSOR',
      gsf_scan_url: 'https://scan.sv-1.dev.global.canton.network.sync.global',
      generic_scan_url: 'https://scan.sv-1.dev.global.canton.network.YOUR_SV_SPONSOR',
      gsf_sequencer_url: 'https://sequencer-MIGRATION_ID.sv-1.dev.global.canton.network.sync.global',
      version: '0.6.4',
      version_literal: '0.6.4',
      chart_version_literal: '0.6.4',
      chart_version_set: 'export CHART_VERSION=0.6.4',
      image_tag_set: 'export IMAGE_TAG=0.6.4',
      image_tag_set_plain: 'export IMAGE_TAG=0.6.4',
      bundle_download_link: {
        label: 'Download Bundle',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.6.4/0.6.4_splice-node.tar.gz'
      },
      openapi_download_link: {
        label: 'Download OpenAPI specs',
        href: 'https://github.com/digital-asset/decentralized-canton-sync/releases/download/v0.6.4/0.6.4_openapi.tar.gz'
      },
      helm_repo_prefix: 'oci://ghcr.io/digital-asset/decentralized-canton-sync/helm',
      docker_repo_prefix: 'ghcr.io/digital-asset/decentralized-canton-sync/docker'
    }
  }
};

export const NetworkVariables = ({children, title = null, description, defaultNetwork = 'devnet', networkData = {}}) => {
  const deploymentNetworkOrder = ['devnet', 'testnet', 'mainnet'];
  const tokenPattern = /\|([A-Za-z0-9_]+)\|/g;
  const tokenTextValue = value => {
    if (value && typeof value === 'object' && ('href' in value)) {
      return value.href;
    }
    return value ?? null;
  };
  const replacementForToken = (token, substitutions) => tokenTextValue(substitutions?.[token]);
  const replaceStringProp = (value, substitutions) => {
    if (typeof value !== 'string') {
      return value;
    }
    return value.replace(tokenPattern, (match, token) => replacementForToken(token, substitutions) ?? match);
  };
  const replaceTextNode = (value, substitutions, keyPrefix = 'token') => {
    if (typeof value !== 'string') {
      return value;
    }
    const pieces = [];
    let lastIndex = 0;
    let match;
    tokenPattern.lastIndex = 0;
    while ((match = tokenPattern.exec(value)) !== null) {
      const [rawToken, token] = match;
      const replacement = substitutions?.[token];
      if (!replacement) {
        continue;
      }
      if (match.index > lastIndex) {
        pieces.push(value.slice(lastIndex, match.index));
      }
      if (replacement && typeof replacement === 'object' && replacement.href) {
        const linkLabel = replacement.label ?? replacement.href;
        const networkLabel = activeData?.versions?.splice ? `${activeData.name ?? activeNetwork} ${activeData.versions.splice}` : activeData?.name ?? activeNetwork;
        pieces.push(<a key={`${keyPrefix}-${pieces.length}`} href={replacement.href}>
            {networkLabel ? `${linkLabel} (${networkLabel})` : linkLabel}
          </a>);
      } else {
        pieces.push(String(replacement));
      }
      lastIndex = match.index + rawToken.length;
    }
    if (!pieces.length) {
      return value;
    }
    if (lastIndex < value.length) {
      pieces.push(value.slice(lastIndex));
    }
    return pieces;
  };
  const replaceNode = (node, substitutions, keyPrefix = 'node') => {
    if (typeof node === 'string') {
      return replaceTextNode(node, substitutions, keyPrefix);
    }
    if (Array.isArray(node)) {
      return node.map((child, index) => replaceNode(child, substitutions, `${keyPrefix}-${index}`));
    }
    if (!React.isValidElement(node)) {
      return node;
    }
    const networkOnly = node.props?.['data-network-only'];
    if (typeof networkOnly === 'string') {
      const allowedNetworks = networkOnly.split(',').map(network => network.trim()).filter(Boolean);
      if (!allowedNetworks.includes(activeNetwork)) {
        return null;
      }
    }
    const props = {};
    for (const [name, value] of Object.entries(node.props ?? ({}))) {
      if (name === 'children') {
        continue;
      }
      props[name] = typeof value === 'string' ? replaceStringProp(value, substitutions) : value;
    }
    return React.cloneElement(node, props, React.Children.map(node.props.children, (child, index) => replaceNode(child, substitutions, `${keyPrefix}-${index}`)));
  };
  const options = deploymentNetworkOrder.filter(networkKey => networkData[networkKey]?.substitutions);
  const defaultIndex = Math.max(0, options.indexOf(defaultNetwork));
  const [activeIndex, setActiveIndex] = React.useState(defaultIndex);
  const activeNetwork = options[activeIndex] ?? options[0];
  const activeData = networkData[activeNetwork];
  const panelRef = React.useRef(null);
  React.useEffect(() => {
    if (!activeData?.substitutions || !panelRef.current) {
      return;
    }
    const replaceTokenText = value => value.replace(/\|([A-Za-z0-9_]+)\|/g, (match, token) => replacementForToken(token, activeData.substitutions) ?? match);
    const replaceRenderedCode = () => {
      for (const codeRoot of panelRef.current.querySelectorAll('pre, code')) {
        if (codeRoot.tagName === 'PRE' && codeRoot.querySelector('code')) {
          continue;
        }
        codeRoot.textContent = replaceTokenText(codeRoot.textContent ?? '');
      }
    };
    let animationFrame;
    const scheduleReplaceRenderedCode = () => {
      if (animationFrame) {
        window.cancelAnimationFrame(animationFrame);
      }
      animationFrame = window.requestAnimationFrame(() => {
        animationFrame = null;
        replaceRenderedCode();
      });
    };
    const observer = new MutationObserver(scheduleReplaceRenderedCode);
    const timeouts = [100, 500, 1000].map(delay => window.setTimeout(replaceRenderedCode, delay));
    replaceRenderedCode();
    observer.observe(panelRef.current, {
      childList: true,
      characterData: true,
      subtree: true
    });
    return () => {
      observer.disconnect();
      if (animationFrame) {
        window.cancelAnimationFrame(animationFrame);
      }
      for (const timeout of timeouts) {
        window.clearTimeout(timeout);
      }
    };
  }, [activeNetwork, activeData]);
  if (!activeData) {
    return null;
  }
  const label = networkKey => {
    const network = networkData[networkKey];
    const version = network?.versions?.splice;
    return version ? `${network?.name ?? networkKey} (${version})` : network?.name ?? networkKey;
  };
  return <div data-component="network-variables" className="my-6 overflow-hidden rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-800 dark:bg-gray-950">
      {(title || description) && <div className="border-b border-gray-200 px-4 py-3 dark:border-gray-800">
          {title && <div className="text-sm font-semibold text-gray-950 dark:text-gray-100">{title}</div>}
          {description && <div className="mt-1 text-sm leading-6 text-gray-600 dark:text-gray-400">{description}</div>}
        </div>}
      <div role="tablist" className="flex gap-1 border-b border-gray-200 bg-gray-50 p-1 dark:border-gray-800 dark:bg-gray-900">
        {options.map((networkKey, index) => {
    const isActive = index === activeIndex;
    return <button key={networkKey} type="button" role="tab" aria-selected={isActive} onClick={() => setActiveIndex(index)} className={`min-h-9 px-3 text-sm font-medium transition-colors ${isActive ? 'rounded-md bg-white text-gray-950 shadow-sm dark:bg-gray-800 dark:text-gray-100' : 'rounded-md text-gray-600 hover:bg-white/80 hover:text-gray-950 dark:text-gray-400 dark:hover:bg-gray-800/70 dark:hover:text-gray-100'}`}>
              {label(networkKey)}
            </button>;
  })}
      </div>
      <div key={activeNetwork} ref={panelRef} role="tabpanel" className="space-y-4 px-4 py-4 text-sm leading-6 text-gray-800 dark:text-gray-200">
        {React.Children.map(children, (child, index) => {
    if (typeof child === 'string' && !child.trim()) {
      return null;
    }
    const content = replaceNode(child, activeData.substitutions, `network-${activeNetwork}-${index}`);
    if (content === null || content === false) {
      return null;
    }
    return <div className="min-w-0">
              {content}
            </div>;
  })}
      </div>
    </div>;
};

LocalNet provides a straightforward topology comprising three participants, three validators, a PostgreSQL database, and several web applications (wallet, sv, scan) behind an NGINX gateway. Each validator plays a distinct role within the Splice ecosystem:

* **app-provider**: for the user operating their application
* **app-user**: for a user wanting to use the app from the App Provider
* **sv**: for providing the Global Synchronizer and handling AMT

Designed primarily for development and testing, LocalNet is not intended for production use.

## Setup

<NetworkVariables networkData={networkData}>
  1. Download the release artifacts from the |bundle\_download\_link| link, and extract the bundle:

     > ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
     > tar xzvf |version|_splice-node.tar.gz
     > ```

     The extracted docker compose files defining LocalNet are located in `splice-node/docker-compose/localnet`.

  2. Export these two environment variables used in the later commands:

     * **LOCALNET\_DIR**: Specifies the path to the LocalNet directory.
     * **IMAGE\_TAG**: Specifies the version of Splice to be used in LocalNet.

     For the bundle that you downloaded use:

     > ```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
     > export LOCALNET_DIR=$PWD/splice-node/docker-compose/localnet
     > |image_tag_set_plain|
     > ```

  3. See `use-localnet` for the commands to start, stop, inspect, and administrate the LocalNet nodes.

  Optional: use the Docker Compose profiles (e.g., `--profile app-provider`) alongside the corresponding environment variables (e.g., `APP_PROVIDER_PROFILE=on/off`) to disable specific validator nodes; for example, to reduce the resource needs of LocalNet. By default, all three validators are active.

  Optional: use the following additional environment variables to configure:

  * **LOCALNET\_DIR/compose.env**: Contains Docker Compose configuration variables.
  * **LOCALNET\_ENV\_DIR**: Overrides the default environment file directory. The default is `$LOCALNET_DIR/env`.
  * **LOCALNET\_ENV\_DIR/common.env**: Shared environment variables across Docker Compose and container configurations. It sets default ports, DB credentials, and Splice UI configurations.

  Resource constraints for containers can be configured via: - **LOCALNET\_DIR/resource-constraints.yaml**
</NetworkVariables>

## Exposed Ports

The following section details the ports used by various services. The default database port is **DB\_PORT=5432**.

Other ports are generated using specific patterns based on the validator:

* For the Super Validator (sv), the port is specified as `4${PORT_SUFFIX}`.
* For the App Provider, the port is specified as `3${PORT_SUFFIX}`.
* For the App User, the port is specified as `2${PORT_SUFFIX}`.

These patterns apply to the following ports suffixes:

* **PARTICIPANT\_LEDGER\_API\_PORT\_SUFFIX**: 901
* **PARTICIPANT\_ADMIN\_API\_PORT\_SUFFIX**: 902
* **PARTICIPANT\_JSON\_API\_PORT\_SUFFIX**: 975
* **VALIDATOR\_ADMIN\_API\_PORT\_SUFFIX**: 903
* **CANTON\_HTTP\_HEALTHCHECK\_PORT\_SUFFIX**: 900
* **CANTON\_GRPC\_HEALTHCHECK\_PORT\_SUFFIX**: 961

UI Ports are defined as follows:

* **APP\_USER\_UI\_PORT**: 2000
* **APP\_PROVIDER\_UI\_PORT**: 3000
* **SV\_UI\_PORT**: 4000

## Database

LocalNet uses a single PostgreSQL database for all components. Database configurations are sourced from `LOCALNET_ENV_DIR/postgres.env`.

## Application UIs

* **App User Wallet UI**

  > * **URL**: [http://wallet.localhost:2000](http://wallet.localhost:2000)
  > * **Description**: Interface for managing user wallets.

* **App Provider Wallet UI**

  > * **URL**: [http://wallet.localhost:3000](http://wallet.localhost:3000)
  > * **Description**: Interface for managing user wallets.

* **Super Validator Web UI**

  > * **URL**: [http://sv.localhost:4000](http://sv.localhost:4000)
  > * **Description**: Interface for super validator functionalities.

* **Scan Web UI**

  > * **URL**: [http://scan.localhost:4000](http://scan.localhost:4000)
  > * **Description**: Interface to monitor transactions.

<Note>
  `LocalNet` rounds may take up to 6 rounds (equivalent to one hour) to display in the scan UI.
</Note>

In most scenarios, the `*.localhost` domains (e.g., `http://scan.localhost`) will resolve to your local host IP `127.0.0.1`. There are some situations where the resolution does not occur and the solution is to add entries to your `/etc/hosts` file. For example, to resolve `http://scan.localhost` and `http://wallet.localhost` add these entry to the file:

```
127.0.0.1   scan.localhost
127.0.0.1   wallet.localhost
```

## Default Wallet Users

* **App User**: app-user
* **App Provider**: app-provider
* **SV**: sv

## Swagger UI

When the `swagger-ui` profile is enabled, the Swagger UI for the `JSON Ledger API HTTP Endpoints` across all running participants is available at [http://localhost:9090](http://localhost:9090). Note: Some endpoints require a JWT token when using the **Try it out** feature. One method to obtain this token is via the Canton Console. Start the Canton Console `make canton-console` and execute the following command:

```none theme={"theme":{"light":"github-light","dark":"github-dark"}}
`app-provider`.adminToken
```

For proper functionality, Swagger UI relies on a localhost nginx proxy for `canton.localhost` configured for each participant. For example, the `JSON Ledger API HTTP Endpoints` for the app-provider can be accessed at the nginx proxy URL `http://canton.localhost:${APP_PROVIDER_UI_PORT}` via Swagger UI, which corresponds to accessing `localhost:3${PARTICIPANT_JSON_API_PORT}` directly. The nginx proxy only adds additional headers to resolve CORS issues within Swagger UI.

## Use LocalNet

### Start LocalNet nodes

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file $LOCALNET_DIR/compose.env \
               --env-file $LOCALNET_DIR/env/common.env \
               -f $LOCALNET_DIR/compose.yaml \
               -f $LOCALNET_DIR/resource-constraints.yaml \
               --profile sv \
               --profile app-provider \
               --profile app-user up -d
```

### Stop LocalNet nodes

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file $LOCALNET_DIR/compose.env \
               --env-file $LOCALNET_DIR/env/common.env \
               -f $LOCALNET_DIR/compose.yaml \
               -f $LOCALNET_DIR/resource-constraints.yaml \
               --profile sv \
               --profile app-provider \
               --profile app-user down -v
```

### Start nodes including a swagger-ui

See `swagger-ui` for more information.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file $LOCALNET_DIR/compose.env \
               --env-file $LOCALNET_DIR/env/common.env \
               -f $LOCALNET_DIR/compose.yaml \
               -f $LOCALNET_DIR/resource-constraints.yaml \
               --profile sv \
               --profile app-provider \
               --profile app-user \
               --profile swagger-ui up -d
```

### Stop nodes including a swagger-ui

See `swagger-ui` for more information.

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file $LOCALNET_DIR/compose.env \
               --env-file $LOCALNET_DIR/env/common.env \
               -f $LOCALNET_DIR/compose.yaml \
               -f $LOCALNET_DIR/resource-constraints.yaml \
               --profile sv \
               --profile app-provider \
               --profile app-user \
               --profile swagger-ui down -v
```

### Access the Canton Admin Console

Use the Canton Admin Console to inspect and modify the run configuration of the Canton sequencer, mediator, and participant nodes in your LocalNet deployment.

* [Canton Console How-To](/global-synchronizer/canton-console/console-overview)
* [Canton Console commands](/global-synchronizer/reference/canton-console-commands)

```bash theme={"theme":{"light":"github-light","dark":"github-dark"}}
docker compose --env-file $LOCALNET_DIR/compose.env \
               --env-file $LOCALNET_DIR/env/common.env \
               -f $LOCALNET_DIR/compose.yaml \
               -f $LOCALNET_DIR/resource-constraints.yaml \
               run --rm console
```

## Multiple Synchronizers

LocalNet supports running multiple synchronizers side by side.

By default, a single synchronizer controlled by the Super Validator (sv) is active. This synchronizer simulates the **Global Synchronizer**.

To enable a second synchronizer called `app-synchronizer`, start LocalNet with the `multi-sync` Docker Compose profile (`--profile multi-sync`). The additional synchronizer has the following characteristics:

* It is managed by the `app-sequencer` and `app-mediator` nodes.
* It simulates a **private synchronizer**.
* Both the `app-provider` and `app-user` participants are cross-connected to the Global Synchronizer and the `app-synchronizer`.

## Using Non-Default Protocol Versions

The protocol version used in the LocalNet synchronizer and participants can be configured by setting the `CANTON_PROTOCOL_VERSION` environment variable to the required version prior to launching LocalNet. Non-stable protocol versions can be used for early testing, but require explicit opt-in. To enable that, export also a `ALPHA_PROTOCOL_VERSION_ENV=$LOCALNET_DIR/env/alpha-protocol-version.env` environment variable.

<Warning>
  Non-stable protocol versions are unreleased versions that are under development, and are subject to announced breaking changes. One implication of this is that this environment usually cannot be upgraded, and will therefore require a full reset for every change. Use only for early testing and development purposes.
</Warning>
