Skip to content
Closed
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
37 changes: 37 additions & 0 deletions explorer/frontend/.env.nem.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# NEM Explorer Configuration
NEXT_PUBLIC_PLATFORM=nem

# Native Mosaic
NEXT_PUBLIC_NATIVE_MOSAIC_ID=nem.xem
NEXT_PUBLIC_NATIVE_MOSAIC_TICKER=XEM
NEXT_PUBLIC_NATIVE_MOSAIC_DIVISIBILITY=6

# Blockchain
NEXT_PUBLIC_BLOCKCHAIN_UNWIND_LIMIT=360
NEXT_PUBLIC_REQUEST_TIMEOUT=60000

# API
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api/nem
NEXT_PUBLIC_NODELIST_URL=http://localhost:5001/api/nem/nodes

# NEM-specific
NEXT_PUBLIC_NEM_BLOCKCHAIN_UNWIND_LIMIT=360
NEXT_PUBLIC_NEM_SUPERNODE_API_URL=https://nem.io/supernode/api
NEXT_PUBLIC_NEM_NODELIST_URL=http://localhost:5001/api/nem/nodes
NEXT_PUBLIC_NEM_MARKET_DATA_URL=https://min-api.cryptocompare.com/data/price?fsym=XEM&tsyms=USD
NEXT_PUBLIC_NEM_HISTORICAL_PRICE_URL=https://min-api.cryptocompare.com/data/pricehistorical?fsym=XEM&tsyms=USD

# Market Data
NEXT_PUBLIC_MARKET_DATA_URL=https://min-api.cryptocompare.com/data/price?fsym=XEM&tsyms=USD
NEXT_PUBLIC_HISTORICAL_PRICE_URL=https://min-api.cryptocompare.com/data/pricehistorical?fsym=XEM&tsyms=USD

# Social Links
NEXT_PUBLIC_SOCIAL_URL_TWITTER=https://x.com/NEMofficial
NEXT_PUBLIC_SOCIAL_URL_GITHUB=https://github.com/symbol
NEXT_PUBLIC_SOCIAL_URL_DISCORD=

# Footer Links
NEXT_PUBLIC_FOOTER_URL_DOCS=https://docs.nem.io
NEXT_PUBLIC_FOOTER_URL_TECHNICAL_REFERENCE=https://docs.nem.io
NEXT_PUBLIC_FOOTER_URL_FAUCET=
NEXT_PUBLIC_FOOTER_URL_SUPERNODE_PROGRAM=https://nem.io/supernode
40 changes: 40 additions & 0 deletions explorer/frontend/.env.symbol.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Symbol Explorer Configuration
NEXT_PUBLIC_PLATFORM=symbol

# Native Mosaic
NEXT_PUBLIC_NATIVE_MOSAIC_ID=72C0212E67A08BCE
NEXT_PUBLIC_NATIVE_MOSAIC_TICKER=XYM
NEXT_PUBLIC_NATIVE_MOSAIC_DIVISIBILITY=6

# Blockchain
NEXT_PUBLIC_BLOCKCHAIN_UNWIND_LIMIT=360
NEXT_PUBLIC_REQUEST_TIMEOUT=60000

# API
NEXT_PUBLIC_API_BASE_URL=http://localhost:4000/api/symbol
NEXT_PUBLIC_NODELIST_URL=http://localhost:5001/api/symbol/nodes

# Symbol-specific
NEXT_PUBLIC_SYMBOL_STATISTIC_SERVICE_URL=http://localhost:4000/api/symbol/statistics
NEXT_PUBLIC_SYMBOL_NODE_URL=https://401-sai-dual.symboltest.net:3001
NEXT_PUBLIC_SYMBOL_MARKET_DATA_URL=https://min-api.cryptocompare.com/data/price?fsym=XYM&tsyms=USD
NEXT_PUBLIC_SYMBOL_HISTORICAL_PRICE_URL=https://min-api.cryptocompare.com/data/pricehistorical?fsym=XYM&tsyms=USD
# mainnet: 104, testnet: 152
NEXT_PUBLIC_SYMBOL_NETWORK_IDENTIFIER=152
# Epoch adjustment in seconds (from GET /network/properties -> epochAdjustment)
NEXT_PUBLIC_SYMBOL_EPOCH_ADJUSTMENT=1667250467

# Market Data
NEXT_PUBLIC_MARKET_DATA_URL=https://min-api.cryptocompare.com/data/price?fsym=XYM&tsyms=USD
NEXT_PUBLIC_HISTORICAL_PRICE_URL=https://min-api.cryptocompare.com/data/pricehistorical?fsym=XYM&tsyms=USD

# Social Links
NEXT_PUBLIC_SOCIAL_URL_TWITTER=https://x.com/thesymbolchain
NEXT_PUBLIC_SOCIAL_URL_GITHUB=https://github.com/symbol
NEXT_PUBLIC_SOCIAL_URL_DISCORD=

# Footer Links
NEXT_PUBLIC_FOOTER_URL_DOCS=https://docs.symboltest.net/
NEXT_PUBLIC_FOOTER_URL_TECHNICAL_REFERENCE=https://docs.symboltest.net/
NEXT_PUBLIC_FOOTER_URL_FAUCET=https://testnet.symbol.tools/
NEXT_PUBLIC_FOOTER_URL_SUPERNODE_PROGRAM=
15 changes: 9 additions & 6 deletions explorer/frontend/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ frontend/
├── pages/ # Website pages .
├── public/ # Images, fonts and other static assets
├── styles/ # Global SCSS files
├── variants/ # Chain-specific frontend adapters and theme configuration
└── utils/ # Utility functions and helpers
```

Expand All @@ -24,17 +25,19 @@ All environment variables listed below are required for both development and pro

These variables are exposed to the browser, meaning they can be accessed both on the server and in the client-side code

- **`NATIVE_MOSAIC_ID`**: Native mosaic ID. Example: `nem.xem`.
- **`PLATFORM`** / **`NEXT_PUBLIC_PLATFORM`**: Explorer variant. Supported values: `nem`, `symbol`. Defaults to `nem`.

- **`NATIVE_MOSAIC_TICKER`**: Native mosaic ticker. Example: `XEM`.
- **`NATIVE_MOSAIC_ID`** / **`NEXT_PUBLIC_NATIVE_MOSAIC_ID`**: Native mosaic ID. Example: `nem.xem`.

- **`NATIVE_MOSAIC_DIVISIBILITY`**: Native mosaic divisibility. Example: `6`.
- **`NATIVE_MOSAIC_TICKER`** / **`NEXT_PUBLIC_NATIVE_MOSAIC_TICKER`**: Native mosaic ticker. Example: `XEM`.

- **`BLOCKCHAIN_UNWIND_LIMIT`**: Blockchain unwind limit. Example: `360`.
- **`NATIVE_MOSAIC_DIVISIBILITY`** / **`NEXT_PUBLIC_NATIVE_MOSAIC_DIVISIBILITY`**: Native mosaic divisibility. Example: `6`.

- **`REQUEST_TIMEOUT`**: The timeout duration (in milliseconds) for network requests made by the application. Example: `15000` (15 seconds).
- **`BLOCKCHAIN_UNWIND_LIMIT`** / **`NEXT_PUBLIC_BLOCKCHAIN_UNWIND_LIMIT`**: Blockchain unwind limit. Example: `360`.

- **`API_BASE_URL`**: Explorer REST API endpoint. Example: `http://explorer-backend.com:4000/api/nem`.
- **`REQUEST_TIMEOUT`** / **`NEXT_PUBLIC_REQUEST_TIMEOUT`**: The timeout duration (in milliseconds) for network requests made by the application. Example: `15000` (15 seconds).

- **`API_BASE_URL`** / **`NEXT_PUBLIC_API_BASE_URL`**: Explorer REST API endpoint. Example: `http://explorer-backend.com:4000/api/nem`.

- **`SUPERNODE_API_URL`**: Supernodes API endpoint. Example: `https://nem.io/supernode/api`.

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import IconTransactionType from '@/components/IconTransactionType';
import { TRANSACTION_TYPE } from '@/constants';
import { render, screen } from '@testing-library/react';

jest.mock('@/components/CustomImage', () => {
const CustomImageMock = props => <div data-testid="transaction-type-icon" data-src={props.src} />;

return CustomImageMock;
});

describe('IconTransactionType', () => {
it('uses namespace icon for address alias transactions', () => {
// Act:
render(<IconTransactionType value={TRANSACTION_TYPE.ADDRESS_ALIAS} />);

// Assert:
expect(screen.getByTestId('transaction-type-icon')).toHaveAttribute('data-src', '/images/transaction/namespace.svg');
});

it('uses account multisig icon for multisig account modification transactions', () => {
// Act:
render(<IconTransactionType value={TRANSACTION_TYPE.MULTISIG_ACCOUNT_MODIFICATION} />);

// Assert:
expect(screen.getByTestId('transaction-type-icon')).toHaveAttribute('data-src', '/images/transaction/account-multisig.svg');
});
});
101 changes: 101 additions & 0 deletions explorer/frontend/__tests__/config/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
describe('config', () => {
const originalEnv = process.env;

afterEach(() => {
jest.resetModules();
process.env = originalEnv;
});

const loadPublicConfig = env => {
jest.resetModules();
process.env = {
...originalEnv,
...env
};
Object.keys(process.env)
.filter(key => key.startsWith('NEXT_PUBLIC_') && !(key in env))
.forEach(key => delete process.env[key]);

return require('@/config').createPublicAppConfig();
};

it('uses legacy environment variable names', () => {
// Act:
const config = loadPublicConfig({
PLATFORM: 'nem',
NATIVE_MOSAIC_ID: 'nem.xem',
NATIVE_MOSAIC_TICKER: 'XEM',
NATIVE_MOSAIC_DIVISIBILITY: '6',
API_BASE_URL: 'https://legacy.backend'
});

// Assert:
expect(config.PLATFORM).toBe('nem');
expect(config.NATIVE_MOSAIC_ID).toBe('nem.xem');
expect(config.NATIVE_MOSAIC_TICKER).toBe('XEM');
expect(config.NATIVE_MOSAIC_DIVISIBILITY).toBe(6);
expect(config.API_BASE_URL).toBe('https://legacy.backend');
});

it('prefers NEXT_PUBLIC environment variable names', () => {
// Act:
const config = loadPublicConfig({
PLATFORM: 'symbol',
NATIVE_MOSAIC_ID: 'symbol.xym',
NEXT_PUBLIC_PLATFORM: 'symbol',
NEXT_PUBLIC_NATIVE_MOSAIC_ID: '72C0212E67A08BCE',
NEXT_PUBLIC_NATIVE_MOSAIC_TICKER: 'XYM',
NEXT_PUBLIC_NATIVE_MOSAIC_DIVISIBILITY: '6',
NEXT_PUBLIC_API_BASE_URL: 'https://symbol.backend',
NEXT_PUBLIC_SYMBOL_NODE_URL: 'https://symbol.node'
});

// Assert:
expect(config.PLATFORM).toBe('symbol');
expect(config.NATIVE_MOSAIC_ID).toBe('72C0212E67A08BCE');
expect(config.NATIVE_MOSAIC_TICKER).toBe('XYM');
expect(config.NATIVE_MOSAIC_DIVISIBILITY).toBe(6);
expect(config.API_BASE_URL).toBe('https://symbol.backend');
expect(config.SYMBOL_NODE_URL).toBe('https://symbol.node');
});

it('uses Next app config on client before the inline appConfig script executes', () => {
// Arrange:
const originalAppConfig = window.appConfig;
const originalNextData = window['__NEXT_DATA__'];
delete window.appConfig;
window['__NEXT_DATA__'] = {
props: {
appConfig: {
PLATFORM: 'nem',
SOCIAL_URL_GITHUB: 'https://github.com/symbol'
}
}
};
jest.resetModules();

// Act:
const config = require('@/config').default;

// Assert:
expect(config.SOCIAL_URL_GITHUB).toBe('https://github.com/symbol');

// Cleanup:
window.appConfig = originalAppConfig;
window['__NEXT_DATA__'] = originalNextData;
});

it('escapes app config before injecting it into the inline bootstrap script', () => {
// Arrange:
const { serializeAppConfig } = require('@/config');

// Act:
const serializedConfig = serializeAppConfig({
SOCIAL_URL_GITHUB: '</script><script>alert(1)</script>'
});

// Assert:
expect(serializedConfig).toBe('{"SOCIAL_URL_GITHUB":"\\u003c/script>\\u003cscript>alert(1)\\u003c/script>"}');
expect(serializedConfig).not.toContain('</script>');
});
});
14 changes: 14 additions & 0 deletions explorer/frontend/__tests__/constants/index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BACKEND_HEALTH_ERROR, TRANSACTION_TYPE } from '@/constants';

describe('constants', () => {
it('exports Symbol alias transaction type names', () => {
// Assert:
expect(TRANSACTION_TYPE.MOSAIC_ALIAS).toBe('MOSAIC_ALIAS');
expect(TRANSACTION_TYPE.MOSAIC_DEFINITION).toBe('MOSAIC_DEFINITION');
});

it('exports backend health error names', () => {
// Assert:
expect(BACKEND_HEALTH_ERROR.SYNCHRONIZATION).toBe('synchronization');
});
});
125 changes: 125 additions & 0 deletions explorer/frontend/__tests__/next-config.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
const { spawnSync } = require('child_process');
const path = require('path');

jest.mock('@next/env', () => ({
loadEnvConfig: jest.fn()
}));

describe('next.config', () => {
const originalEnv = process.env;

afterEach(() => {
jest.resetModules();
process.env = originalEnv;
});

const loadNextConfig = env => {
jest.resetModules();
process.env = {
...originalEnv,
...env
};

return require('../next.config');
};

it('aliases browser-safe stubs for client webpack builds', () => {
// Arrange:
const nextConfig = loadNextConfig({ NEXT_PUBLIC_PLATFORM: 'symbol' });
const webpackConfig = {
resolve: {
alias: {}
}
};

// Act:
const result = nextConfig.webpack(webpackConfig, { isServer: false });

// Assert:
expect(result).toBe(webpackConfig);
expect(result.resolve.alias['symbol-crypto-wasm-node']).toContain('utils/symbol-wasm-stub.js');
expect(result.resolve.alias['bitcore-mnemonic']).toContain('utils/bitcore-mnemonic-stub.js');
});

it('aliases only the shared mnemonic stub for server webpack builds', () => {
// Arrange:
const nextConfig = loadNextConfig({ NEXT_PUBLIC_PLATFORM: 'symbol' });
const webpackConfig = {
resolve: {
alias: {}
}
};

// Act:
const result = nextConfig.webpack(webpackConfig, { isServer: true });

// Assert:
expect(result.resolve.alias['symbol-crypto-wasm-node']).toBeUndefined();
expect(result.resolve.alias['bitcore-mnemonic']).toContain('utils/bitcore-mnemonic-stub.js');
});

it('injects the selected variant variables into shared SCSS files', () => {
// Arrange:
const nextConfig = loadNextConfig({ NEXT_PUBLIC_PLATFORM: 'symbol' });

// Act:
const result = nextConfig.sassOptions.additionalData('$color: red;', {
resourcePath: '/workspace/styles/globals.scss'
});

// Assert:
expect(result).toBe('@import "variants/symbol/styles/variables.scss";\n$color: red;');
});

it('does not inject variant variables into variable or variant SCSS files', () => {
// Arrange:
const nextConfig = loadNextConfig({ NEXT_PUBLIC_PLATFORM: 'symbol' });

// Act + Assert:
expect(nextConfig.sassOptions.additionalData('$color: red;', {
resourcePath: '/workspace/styles/variables.scss'
})).toBe('$color: red;');
expect(nextConfig.sassOptions.additionalData('$color: red;', {
resourcePath: 'C:\\workspace\\variants\\symbol\\styles\\variables.scss'
})).toBe('$color: red;');
});

it('executes Next config callbacks in a native Node process', () => {
// Arrange:
const script = `
const nextConfig = require('./next.config');
const clientConfig = { resolve: { alias: {} } };
const serverConfig = { resolve: { alias: {} } };
nextConfig.webpack(clientConfig, { isServer: false });
nextConfig.webpack(serverConfig, { isServer: true });
const injected = nextConfig.sassOptions.additionalData('$color: red;', {
resourcePath: '/workspace/styles/globals.scss'
});
const skipped = nextConfig.sassOptions.additionalData('$color: red;', {
resourcePath: 'C:\\\\workspace\\\\variants\\\\symbol\\\\styles\\\\variables.scss'
});
if (!clientConfig.resolve.alias['symbol-crypto-wasm-node'])
throw new Error('client alias was not configured');
if (serverConfig.resolve.alias['symbol-crypto-wasm-node'])
throw new Error('server alias was configured');
if (!injected.includes('variants/symbol/styles/variables.scss'))
throw new Error('variant variables were not injected');
if ('$color: red;' !== skipped)
throw new Error('variant variables were injected into a variant file');
`;

// Act:
const result = spawnSync(process.execPath, ['-e', script], {
cwd: path.resolve(__dirname, '..'),
encoding: 'utf8',
env: {
...process.env,
NEXT_PUBLIC_PLATFORM: 'symbol'
}
});

// Assert:
expect(result.stderr).toBe('');
expect(result.status).toBe(0);
});
});
Loading