Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
70 changes: 64 additions & 6 deletions e2e/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ atLeast, time locks, or combinations) without modifying the application logic.

The example consists of the following packages:

- **CLI**: CLI-based application to interact with the contract
- **CLI**: Interactive menu to drive the contract end-to-end
- **Contract**: Smart contract source code and utilities, comprised itself of two modules:
- **Warden** (`generated/Warden.compact`) — access-control library generated
- **Warden** (`packages/contract/src/Warden.compact`) — access-control library generated
from a native-script JSON input. Manages commitments and verifies that the
current set of committed users satisfies the configured policy.
- **TokenSupply** (`e2e/TokenSupply.compact`) — a token supply contract that imports Warden and
- **TokenSupply** (`packages/contract/src/TokenSupply.compact`) — a token supply contract that imports Warden and
calls `Warden_verify()` before allowing `mint` or `burn`.
- **API**: Classes and methods that interface the CLI and the compact contract.
- **Wallet**: Wallet setup and utilities.
Expand Down Expand Up @@ -47,22 +47,80 @@ In the `e2e` directory, the following command compiles the contract and builds t
pnpm build
```

and before running the example, start the services that Midnight requires: a node and an indexer,
to run the undeployed network, and the proof-server necessary for proof generation.
Before running the example, start the local devnet services — a node, an indexer,
and the proof server needed for proof generation:

```bash
docker compose up -d
```

To start only the proof server (e.g. when connecting to a public network):

```bash
docker compose up proof-server
```

After finishing, make sure to shut down the services.

```bash
docker compose down --volumes
```

## Environment configuration

Before running the app, you need to set up an `.env` file at `packages/cli/.env`
with your wallet and contract details, and the network configuration.

Complete it with your wallet's mnemonic phrase, and the secret and randomness used
to create the commitment that was provided to initialize the Warden contract.

```env
WALLET_MNEMONIC=<your mnemonic>
WARDEN_SECRET=<your secret>
WARDEN_RANDOMNESS=<your randomness>
```

When running on the local devnet, you can skip these values — the CLI offers
preset wallet options (1 through 4) with built-in credentials.

### Network

Below are the templates to connect with different networks. For the latest testnet endpoints, see
[Environments and endpoints](https://docs.midnight.network/relnotes/network).

### Local devnet

```env
NETWORK_TYPE=undeployed
NODE_URL=ws://127.0.0.1:9944
INDEXER_URL=http://127.0.0.1:8088/api/v3/graphql
INDEXER_WS_URL=ws://127.0.0.1:8088/api/v3/graphql/ws
PROOF_SERVER_URL=http://127.0.0.1:6300
```

### Preview

```env
NETWORK_TYPE=preview
NODE_URL=wss://rpc.preview.midnight.network/
INDEXER_URL=https://indexer.preview.midnight.network/api/v4/graphql
INDEXER_WS_URL=wss://indexer.preview.midnight.network/api/v4/graphql/ws
PROOF_SERVER_URL=http://127.0.0.1:6300
```

### Preprod

```env
NETWORK_TYPE=preprod
NODE_URL=wss://rpc.preprod.midnight.network/
INDEXER_URL=https://indexer.preprod.midnight.network/api/v4/graphql
INDEXER_WS_URL=wss://indexer.preprod.midnight.network/api/v4/graphql/ws
PROOF_SERVER_URL=http://127.0.0.1:6300
```

## Running the example

In the [`e2e/packages/cli`](/e2e/packages/cli/) directory:
In the [`packages/cli`](packages/cli/) directory:

```bash
pnpm tsx src/index.ts
Expand Down
3 changes: 2 additions & 1 deletion e2e/packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,10 @@
"@e2e/wallet": "workspace:*",
"@midnight-ntwrk/ledger-v8": "8.1.0",
"@midnight-ntwrk/midnight-js": "4.1.1",
"@midnight-ntwrk/wallet-sdk-indexer-client": "1.2.2",
"@midnight-ntwrk/wallet-sdk": "1.2.0",
"@scure/bip39": "^2.2.0",
"commander": "^14.0.3",
"dotenv": "^16.5.0",
"rxjs": "^7.8.2"
}
}
22 changes: 18 additions & 4 deletions e2e/packages/cli/src/config.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import 'dotenv/config';
import { TokenSupplyContractPrivateStateKey } from '@e2e/contract';
import { SecretPair } from '@e2e/api';
import path from 'node:path';

export interface Config {
Expand All @@ -9,10 +11,20 @@ export interface Config {
readonly indexerWS: string;
readonly node: string;
readonly proofServer: string;
readonly networkId: string;
}

export const currentDir = path.resolve(new URL(import.meta.url).pathname, '..');

export const getWalletMnemonic = (): string | undefined => process.env.WALLET_MNEMONIC;

export const getWardenSecretPair = (): SecretPair | undefined => {
const secret = process.env.WARDEN_SECRET;
const randomness = process.env.WARDEN_RANDOMNESS;
if (!secret || !randomness) return undefined;
return { secret, randomness };
};

export class StandaloneConfig implements Config {
privateStateStoreName = TokenSupplyContractPrivateStateKey;
logDir = path.resolve(currentDir, '..', 'logs', 'standalone', `${new Date().toISOString()}.log`);
Expand All @@ -26,8 +38,10 @@ export class StandaloneConfig implements Config {
'managed',
'sentinel'
);
indexer = 'http://127.0.0.1:8088/api/v3/graphql';
indexerWS = 'ws://127.0.0.1:8088/api/v3/graphql/ws';
node = 'http://127.0.0.1:9944';
proofServer = 'http://127.0.0.1:6300';
networkId = process.env.NETWORK_TYPE ?? 'undeployed';
indexer = process.env.INDEXER_URL ?? 'http://127.0.0.1:8088/api/v3/graphql';
indexerWS = process.env.INDEXER_WS_URL ?? 'ws://127.0.0.1:8088/api/v3/graphql/ws';
node = process.env.NODE_URL ?? 'ws://127.0.0.1:9944';
proofServer = process.env.PROOF_SERVER_URL ?? 'http://127.0.0.1:6300';
syncTimeoutMs = Number(process.env.SYNC_TIMEOUT_MS) || 300_000;
}
50 changes: 40 additions & 10 deletions e2e/packages/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,62 @@
import { Command } from 'commander';
import { stdin as input, stdout as output } from 'node:process';
import { buildWalletAndWaitForFunds } from '@e2e/wallet';
import { StandaloneConfig } from './config.js';
import { seeds } from './utils/constants.js';
import { StandaloneConfig, getWalletMnemonic, getWardenSecretPair } from './config.js';
import { seeds, SeedAndSecretPair } from './utils/constants.js';
import { runCli } from './cli.js';
import { createInterface } from 'readline/promises';
import { mnemonicToSeedSync } from '@scure/bip39';

const config = new StandaloneConfig();
const networkId = config.networkId;

const program = new Command()
.name('compact-e2e')
.description('Interactive CLI for token supply contract')
.version('0.0.1');

program
.argument('[id]', 'Wallet ID (1-4)', '1')
.argument('[id]', 'Wallet ID (1-4, undeployed only)')
.option('--max-supply <value>', 'Maximum token supply', '1000000000000')
.action(async (id, options) => {
const n = Number(id);
if (!Number.isInteger(n) || n < 1 || n > 4) {
console.error('Error: wallet id must be 1, 2, 3, or 4');
process.exit(1);
const isTestnet = networkId !== 'undeployed';
let walletDetails: SeedAndSecretPair;

if (id !== undefined) {
if (isTestnet) {
console.error('Error: wallet id argument is not supported on testnet');
process.exit(1);
}
const n = Number(id);
if (!Number.isInteger(n) || n < 1 || n > 4) {
console.error('Error: wallet id must be 1, 2, 3, or 4');
process.exit(1);
}
walletDetails = seeds[n - 1];
console.info('Building wallet %d...', n);
} else {
const mnemonic = getWalletMnemonic();
const pair = getWardenSecretPair();

if (mnemonic && pair) {
const seed = Buffer.from(mnemonicToSeedSync(mnemonic)).toString('hex');
walletDetails = { seed, pair };
console.info('Building wallet from .env configuration...');
} else if (!isTestnet) {
walletDetails = seeds[0];
console.info('Building wallet 1 (no .env mnemonic found, using default)...');
} else {
console.error(
'Error: WALLET_MNEMONIC, WARDEN_SECRET, and WARDEN_RANDOMNESS are required on testnet'
);
process.exit(1);
}
}

const maxSupply = BigInt(options.maxSupply);
console.info('Building wallet %d...', n);
const ctx = await buildWalletAndWaitForFunds(config, seeds[n - 1].seed);
const ctx = await buildWalletAndWaitForFunds(config, walletDetails.seed);
const rli = createInterface({ input, output, terminal: true });
await runCli(config, ctx, seeds[n - 1], n - 1, maxSupply, rli).finally(
await runCli(config, ctx, walletDetails, 0, maxSupply, rli).finally(
ctx.wallet.stop.bind(ctx.wallet)
);
rli.close();
Expand Down
4 changes: 2 additions & 2 deletions e2e/packages/cli/src/mint-after.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ import { buildWalletAndWaitForFunds } from '@e2e/wallet';
import { StandaloneConfig } from './config.js';
import { seeds } from './utils/constants.js';
import { showBalances, sleep } from './utils/index.js';
import { BlockHash } from '@midnight-ntwrk/wallet-sdk-indexer-client';
import { QueryRunner } from '@midnight-ntwrk/wallet-sdk-indexer-client/effect';
import { BlockHash } from '@midnight-ntwrk/wallet-sdk/indexer-client';
import { QueryRunner } from '@midnight-ntwrk/wallet-sdk/indexer-client/effect';

const config = new StandaloneConfig();

Expand Down
7 changes: 1 addition & 6 deletions e2e/packages/wallet/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,7 @@
"dependencies": {
"@midnight-ntwrk/ledger-v8": "8.1.0",
"@midnight-ntwrk/midnight-js": "4.1.1",
"@midnight-ntwrk/wallet-sdk-address-format": "3.1.2",
"@midnight-ntwrk/wallet-sdk-dust-wallet": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-facade": "^3.0.0",
"@midnight-ntwrk/wallet-sdk-hd": "^3.0.2",
"@midnight-ntwrk/wallet-sdk-shielded": "2.1.0",
"@midnight-ntwrk/wallet-sdk-unshielded-wallet": "2.1.0",
"@midnight-ntwrk/wallet-sdk": "1.2.0",
"rxjs": "^7.8.2"
}
}
27 changes: 13 additions & 14 deletions e2e/packages/wallet/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,20 @@
import * as ledger from '@midnight-ntwrk/ledger-v8';
import { getNetworkId, setNetworkId } from '@midnight-ntwrk/midnight-js/network-id';
import { type MidnightProvider, type WalletProvider } from '@midnight-ntwrk/midnight-js/types';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk-dust-wallet';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
import { Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk-shielded';
import {
createKeystore,
PublicKey,
UnshieldedWallet,
} from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DustWallet } from '@midnight-ntwrk/wallet-sdk/dust';
import { WalletFacade } from '@midnight-ntwrk/wallet-sdk/facade';
import { Roles } from '@midnight-ntwrk/wallet-sdk/hd';
import { ShieldedWallet } from '@midnight-ntwrk/wallet-sdk/shielded';
import { createKeystore, PublicKey, UnshieldedWallet } from '@midnight-ntwrk/wallet-sdk/unshielded';
import * as Rx from 'rxjs';
import {
deriveKeysFromSeed,
formatBalance,
isWalletSynced,
registerForDustGeneration,
signTransactionIntents,
syncWallet,
waitForFunds,
waitForSync,
withStatus,
} from './utils/index.js';
import { Config, WalletContext } from './utils/types.js';
Expand All @@ -28,7 +25,7 @@ export const buildWalletAndWaitForFunds = async (
seed: string,
wait: boolean = true
): Promise<WalletContext> => {
setNetworkId('undeployed');
setNetworkId(config.networkId);

// Derive HD keys and initialize the three sub-wallets
const { wallet, shieldedSecretKeys, dustSecretKey, unshieldedKeystore } = await withStatus(
Expand All @@ -40,7 +37,7 @@ export const buildWalletAndWaitForFunds = async (
const unshieldedKeystore = createKeystore(keys[Roles.NightExternal], getNetworkId());

const wallet = await WalletFacade.init({
configuration: createConfiguration(),
configuration: createConfiguration(config),
shielded: (cfg) => ShieldedWallet(cfg).startWithSecretKeys(shieldedSecretKeys),
unshielded: (cfg) =>
UnshieldedWallet(cfg).startWithPublicKey(PublicKey.fromKeyStore(unshieldedKeystore)),
Expand All @@ -57,7 +54,9 @@ export const buildWalletAndWaitForFunds = async (
);

// Wait for the wallet to sync with the network
const syncedState = await withStatus('Syncing with network', () => waitForSync(wallet));
const syncedState = await withStatus('Syncing with network', () =>
syncWallet(wallet, config.syncTimeoutMs)
);

// Check if wallet has funds; if not, wait for incoming tokens
const balance = syncedState.unshielded.balances[ledger.unshieldedToken().raw] ?? 0n;
Expand All @@ -83,7 +82,7 @@ export const buildWalletAndWaitForFunds = async (
export const createWalletAndMidnightProvider = async (
ctx: WalletContext
): Promise<WalletProvider & MidnightProvider> => {
const state = await Rx.firstValueFrom(ctx.wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const state = await Rx.firstValueFrom(ctx.wallet.state().pipe(Rx.filter(isWalletSynced)));

return {
getCoinPublicKey() {
Expand Down
12 changes: 6 additions & 6 deletions e2e/packages/wallet/src/utils/balance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@ import {
ShieldedAddress,
ShieldedCoinPublicKey,
ShieldedEncryptionPublicKey,
} from '@midnight-ntwrk/wallet-sdk-address-format';
import type { FacadeState, WalletFacade } from '@midnight-ntwrk/wallet-sdk-facade';
import { Roles } from '@midnight-ntwrk/wallet-sdk-hd';
import { createKeystore } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
} from '@midnight-ntwrk/wallet-sdk/address-format';
import type { FacadeState, WalletFacade } from '@midnight-ntwrk/wallet-sdk/facade';
import { Roles } from '@midnight-ntwrk/wallet-sdk/hd';
import { createKeystore } from '@midnight-ntwrk/wallet-sdk/unshielded';
import * as Rx from 'rxjs';
import { deriveKeysFromSeed } from './index.js';
import { deriveKeysFromSeed, isWalletSynced } from './index.js';

export interface Balances {
dust: bigint;
Expand Down Expand Up @@ -64,7 +64,7 @@ export async function getBalancesAndAddresses(
wallet: WalletFacade,
seed: string
): Promise<{ balances: Balances; addresses: Addresses }> {
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter((s) => s.isSynced)));
const state = await Rx.firstValueFrom(wallet.state().pipe(Rx.filter(isWalletSynced)));
return {
balances: getBalances(state),
addresses: getAddresses(seed, state),
Expand Down
23 changes: 14 additions & 9 deletions e2e/packages/wallet/src/utils/config.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk-unshielded-wallet';
import { DefaultConfiguration } from '@midnight-ntwrk/wallet-sdk-facade';
import { InMemoryTransactionHistoryStorage } from '@midnight-ntwrk/wallet-sdk';
import {
DefaultConfiguration,
mergeWalletEntries,
WalletEntrySchema,
} from '@midnight-ntwrk/wallet-sdk/facade';
import { Config } from './types.js';

export const createConfiguration = (): DefaultConfiguration => ({
networkId: 'undeployed',
export const createConfiguration = (config: Config): DefaultConfiguration => ({
networkId: config.networkId,
indexerClientConnection: {
indexerHttpUrl: 'http://localhost:8088/api/v3/graphql',
indexerWsUrl: 'ws://localhost:8088/api/v3/graphql/ws',
indexerHttpUrl: config.indexer,
indexerWsUrl: config.indexerWS,
},
provingServerUrl: new URL('http://localhost:6300'),
relayURL: new URL('ws://localhost:9944'),
provingServerUrl: new URL(config.proofServer),
relayURL: new URL(config.node),
costParameters: {
additionalFeeOverhead: 300_000_000_000_000n,
feeBlocksMargin: 5,
},
txHistoryStorage: new InMemoryTransactionHistoryStorage(),
txHistoryStorage: new InMemoryTransactionHistoryStorage(WalletEntrySchema, mergeWalletEntries),
});
Loading
Loading