Skip to content
Open
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
65 changes: 65 additions & 0 deletions packages/create-sei/src/templates.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { describe, it, expect } from '@jest/globals';
import { promises as fs } from 'node:fs';
import path from 'node:path';

const PACKAGE_ROOT = path.resolve(__dirname, '..');
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
const EXTENSIONS_DIR = path.join(PACKAGE_ROOT, 'extensions');

const EXPECTED_TEMPLATES = ['next-template'];
const EXPECTED_EXTENSIONS = ['precompiles'];

describe('Templates', () => {
it('templates directory exists', async () => {
const stat = await fs.stat(TEMPLATES_DIR);
expect(stat.isDirectory()).toBe(true);
});

it.each(EXPECTED_TEMPLATES)('%s template directory exists', async (template) => {
const templatePath = path.join(TEMPLATES_DIR, template);
const stat = await fs.stat(templatePath);
expect(stat.isDirectory()).toBe(true);
});

it.each(EXPECTED_TEMPLATES)('%s template has a valid package.json', async (template) => {
const pkgPath = path.join(TEMPLATES_DIR, template, 'package.json');
const contents = await fs.readFile(pkgPath, 'utf-8');
const parsed = JSON.parse(contents);
expect(typeof parsed.name).toBe('string');
expect(parsed.name.trim().length).toBeGreaterThan(0);
expect(typeof parsed.version).toBe('string');
});

it.each(EXPECTED_TEMPLATES)('%s template has a tsconfig.json', async (template) => {
const tsconfigPath = path.join(TEMPLATES_DIR, template, 'tsconfig.json');
const stat = await fs.stat(tsconfigPath);
expect(stat.isFile()).toBe(true);
});

it.each(EXPECTED_TEMPLATES)('%s template has a src/ directory', async (template) => {
const srcPath = path.join(TEMPLATES_DIR, template, 'src');
const stat = await fs.stat(srcPath);
expect(stat.isDirectory()).toBe(true);
});
});

describe('Extensions', () => {
it('extensions directory exists', async () => {
const stat = await fs.stat(EXTENSIONS_DIR);
expect(stat.isDirectory()).toBe(true);
});

it.each(EXPECTED_EXTENSIONS)('%s extension directory exists', async (extension) => {
const extensionPath = path.join(EXTENSIONS_DIR, extension);
const stat = await fs.stat(extensionPath);
expect(stat.isDirectory()).toBe(true);
});

it.each(EXPECTED_EXTENSIONS)('%s extension has a valid package.json', async (extension) => {
const pkgPath = path.join(EXTENSIONS_DIR, extension, 'package.json');
const contents = await fs.readFile(pkgPath, 'utf-8');
const parsed = JSON.parse(contents);
expect(typeof parsed.name).toBe('string');
expect(parsed.name.trim().length).toBeGreaterThan(0);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ const mockR = new Uint8Array([0, 1, 2]); // should be stripped
const mockS = new Uint8Array([0, 3, 4]); // should be stripped
const path = "m/44'/118'/0'/0/0";

/** BIP44 derivation path pattern: m/purpose'/coin_type'/account'/change/index */
const BIP44_PATTERN = /^m\/44'\/\d+'\/\d+'\/\d+\/\d+$/;

const mockSeiApp = {
getCosmosAddress: jest.fn().mockResolvedValue({
address: mockAddress,
Expand All @@ -20,6 +23,28 @@ const mockSeiApp = {
})
};

describe('BIP44 derivation paths', () => {
it('standard Cosmos derivation path matches BIP44 pattern', () => {
expect("m/44'/118'/0'/0/0").toMatch(BIP44_PATTERN);
});

it('EVM-compatible derivation path matches BIP44 pattern', () => {
expect("m/44'/60'/0'/0/0").toMatch(BIP44_PATTERN);
});

it('non-zero account indices match BIP44 pattern', () => {
expect("m/44'/118'/1'/0/0").toMatch(BIP44_PATTERN);
expect("m/44'/118'/0'/0/5").toMatch(BIP44_PATTERN);
});

it('path is passed through to getCosmosAddress unchanged', async () => {
const customPath = "m/44'/118'/0'/0/3";
const signer = new SeiLedgerOfflineAminoSigner(mockSeiApp as never, customPath);
await signer.getAccounts();
expect(mockSeiApp.getCosmosAddress).toHaveBeenCalledWith(customPath);
});
});

describe('SeiLedgerOfflineAminoSigner', () => {
let signer: SeiLedgerOfflineAminoSigner;

Expand Down
91 changes: 91 additions & 0 deletions packages/precompiles/src/precompiles/__tests__/abis.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import {
ADDRESS_PRECOMPILE_ABI,
BANK_PRECOMPILE_ABI,
DISTRIBUTION_PRECOMPILE_ABI,
GOVERNANCE_PRECOMPILE_ABI,
IBC_PRECOMPILE_ABI,
JSON_PRECOMPILE_ABI,
ORACLE_PRECOMPILE_ABI,
POINTER_PRECOMPILE_ABI,
POINTERVIEW_PRECOMPILE_ABI,
SOLO_PRECOMPILE_ABI,
STAKING_PRECOMPILE_ABI,
WASM_PRECOMPILE_ABI
} from '../index';

type AbiEntry = { type: string; name?: string; inputs?: readonly unknown[]; outputs?: readonly unknown[]; stateMutability?: string };
type Abi = readonly AbiEntry[];

function getFunctionNames(abi: Abi): string[] {
return abi.filter((entry) => entry.type === 'function').map((entry) => entry.name!);
}

function getFunctions(abi: Abi): AbiEntry[] {
return abi.filter((entry) => entry.type === 'function');
}

const PRECOMPILE_ABIS: [string, Abi, string[]][] = [
['ADDRESS', ADDRESS_PRECOMPILE_ABI, ['getSeiAddr', 'getEvmAddr', 'associate', 'associatePubKey']],
['BANK', BANK_PRECOMPILE_ABI, ['send', 'sendNative', 'balance', 'all_balances', 'supply', 'decimals', 'name', 'symbol']],
['DISTRIBUTION', DISTRIBUTION_PRECOMPILE_ABI, ['setWithdrawAddress', 'withdrawDelegationRewards', 'withdrawMultipleDelegationRewards', 'rewards']],
['GOVERNANCE', GOVERNANCE_PRECOMPILE_ABI, ['vote', 'deposit']],
['IBC', IBC_PRECOMPILE_ABI, ['transfer']],
['JSON', JSON_PRECOMPILE_ABI, ['extractAsBytes', 'extractAsBytesList']],
['ORACLE', ORACLE_PRECOMPILE_ABI, ['getExchangeRates', 'getOracleTwaps']],
['POINTER', POINTER_PRECOMPILE_ABI, ['addCW20Pointer', 'addCW721Pointer', 'addNativePointer']],
['POINTERVIEW', POINTERVIEW_PRECOMPILE_ABI, ['getCW20Pointer', 'getCW721Pointer']],
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

POINTERVIEW test omits getNativePointer from expected functions

Low Severity

The POINTERVIEW entry in PRECOMPILE_ABIS only lists ['getCW20Pointer', 'getCW721Pointer'] as expected functions, but POINTERVIEW_PRECOMPILE_ABI actually contains a third function, getNativePointer. The test only verifies that listed functions are present (not that all functions are listed), so this silently skips validation of getNativePointer.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02d84ca. Configure here.

['STAKING', STAKING_PRECOMPILE_ABI, ['delegate', 'undelegate', 'redelegate', 'delegation']],
['WASM', WASM_PRECOMPILE_ABI, ['execute', 'execute_batch']]
];
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SOLO precompile omitted from function name tests

Low Severity

SOLO_PRECOMPILE_ABI is included in ALL_ABIS for top-level structure tests but is missing from the PRECOMPILE_ABIS array used for function name validation. The SOLO ABI has two functions (claim and claimSpecific) whose names are never checked, unlike every other precompile. This appears to be an accidental omission since SOLO is consistently included in every other test list in the PR.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02d84ca. Configure here.


describe('Precompile ABIs — function names', () => {
it.each(PRECOMPILE_ABIS)('%s ABI contains all expected function names', (_name, abi, expectedFunctions) => {
const actualFunctions = getFunctionNames(abi as Abi);
for (const fn of expectedFunctions) {
expect(actualFunctions).toContain(fn);
}
});
});

describe('Precompile ABIs — function entry structure', () => {
it.each(PRECOMPILE_ABIS)('%s ABI functions each have inputs, outputs, and stateMutability', (_name, abi) => {
const functions = getFunctions(abi as Abi);
expect(functions.length).toBeGreaterThan(0);

for (const fn of functions) {
expect(Array.isArray(fn.inputs)).toBe(true);
expect(Array.isArray(fn.outputs)).toBe(true);
expect(typeof fn.stateMutability).toBe('string');
expect(['view', 'nonpayable', 'payable', 'pure']).toContain(fn.stateMutability);
}
});
});

describe('Precompile ABIs — top-level structure', () => {
const ALL_ABIS: [string, Abi][] = [
['ADDRESS', ADDRESS_PRECOMPILE_ABI],
['BANK', BANK_PRECOMPILE_ABI],
['DISTRIBUTION', DISTRIBUTION_PRECOMPILE_ABI],
['GOVERNANCE', GOVERNANCE_PRECOMPILE_ABI],
['IBC', IBC_PRECOMPILE_ABI],
['JSON', JSON_PRECOMPILE_ABI],
['ORACLE', ORACLE_PRECOMPILE_ABI],
['POINTER', POINTER_PRECOMPILE_ABI],
['POINTERVIEW', POINTERVIEW_PRECOMPILE_ABI],
['SOLO', SOLO_PRECOMPILE_ABI],
['STAKING', STAKING_PRECOMPILE_ABI],
['WASM', WASM_PRECOMPILE_ABI]
];

it.each(ALL_ABIS)('%s ABI is a non-empty array', (_name, abi) => {
expect(Array.isArray(abi)).toBe(true);
expect((abi as Abi).length).toBeGreaterThan(0);
});

it.each(ALL_ABIS)('%s ABI entries each have a type field', (_name, abi) => {
for (const entry of abi as Abi) {
expect(typeof entry.type).toBe('string');
expect(entry.type.length).toBeGreaterThan(0);
}
});
});
54 changes: 54 additions & 0 deletions packages/precompiles/src/precompiles/__tests__/addresses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import {
ADDRESS_PRECOMPILE_ADDRESS,
BANK_PRECOMPILE_ADDRESS,
DISTRIBUTION_PRECOMPILE_ADDRESS,
GOVERNANCE_PRECOMPILE_ADDRESS,
IBC_PRECOMPILE_ADDRESS,
JSON_PRECOMPILE_ADDRESS,
ORACLE_PRECOMPILE_ADDRESS,
POINTER_PRECOMPILE_ADDRESS,
POINTERVIEW_PRECOMPILE_ADDRESS,
SOLO_PRECOMPILE_ADDRESS,
STAKING_PRECOMPILE_ADDRESS,
WASM_PRECOMPILE_ADDRESS
} from '../index';

const PRECOMPILE_ADDRESSES: [string, string][] = [
['ADDRESS', ADDRESS_PRECOMPILE_ADDRESS],
['BANK', BANK_PRECOMPILE_ADDRESS],
['DISTRIBUTION', DISTRIBUTION_PRECOMPILE_ADDRESS],
['GOVERNANCE', GOVERNANCE_PRECOMPILE_ADDRESS],
['IBC', IBC_PRECOMPILE_ADDRESS],
['JSON', JSON_PRECOMPILE_ADDRESS],
['ORACLE', ORACLE_PRECOMPILE_ADDRESS],
['POINTER', POINTER_PRECOMPILE_ADDRESS],
['POINTERVIEW', POINTERVIEW_PRECOMPILE_ADDRESS],
['SOLO', SOLO_PRECOMPILE_ADDRESS],
['STAKING', STAKING_PRECOMPILE_ADDRESS],
['WASM', WASM_PRECOMPILE_ADDRESS]
];

/** Validates an ERC-55 checksummed Ethereum address: 0x + exactly 40 hex characters. */
function isValidEthAddress(address: string): boolean {
return /^0x[0-9a-fA-F]{40}$/.test(address);
}

describe('Precompile addresses', () => {
it.each(PRECOMPILE_ADDRESSES)('%s address is a valid 42-character Ethereum address', (_name, address) => {
expect(typeof address).toBe('string');
expect(isValidEthAddress(address)).toBe(true);
});

it('all precompile addresses are unique', () => {
const addresses = PRECOMPILE_ADDRESSES.map(([, addr]) => addr.toLowerCase());
const unique = new Set(addresses);
expect(unique.size).toBe(addresses.length);
});

it('all precompile addresses start with 0x000000000000000000000000000000000000', () => {
// Sei precompiles live in the reserved 0x1000–0x10FF range
for (const [, address] of PRECOMPILE_ADDRESSES) {
expect(address.toLowerCase()).toMatch(/^0x0{36}/);
}
});
});
15 changes: 15 additions & 0 deletions packages/registry/src/gas/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,19 @@ describe('GasInfo Tests', () => {
expect(pacific1.min_gas_price).toBeGreaterThanOrEqual(0.01);
expect(pacific1.module_adjustments.dex.sudo_gas_price).toBeLessThanOrEqual(0.02);
});

it('all networks have a positive min_gas_price', () => {
for (const [network, info] of Object.entries(GAS_INFO)) {
expect(info.min_gas_price).toBeGreaterThan(0);
if (info.min_gas_price <= 0) {
throw new Error(`Network ${network} has non-positive min_gas_price: ${info.min_gas_price}`);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unreachable throw statements after Jest expect assertions

Low Severity

Several tests follow an expect() assertion with an if check and throw new Error(...) meant to provide a better error message. This code is unreachable: if the expect passes, the if condition is false; if expect fails, Jest throws before reaching the if. The same pattern appears in gas, ibc, networks, and tokens test files.

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 02d84ca. Configure here.

}
});

it('all networks use usei as the fee denom', () => {
for (const info of Object.values(GAS_INFO)) {
expect(info.denom).toBe('usei');
}
});
});
24 changes: 24 additions & 0 deletions packages/registry/src/ibc/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,28 @@ describe('IBCInfo Tests', () => {
expect(firstChannel.counterparty_chain_name).not.toBe('');
expect(firstChannel.dst_channel.startsWith('channel-')).toBeTruthy();
});

it('all channel IDs follow the channel-N format across all networks', () => {
const channelPattern = /^channel-\d+$/;
for (const [network, channels] of Object.entries(IBC_INFO)) {
for (const channel of channels) {
expect(channel.src_channel).toMatch(channelPattern);
expect(channel.dst_channel).toMatch(channelPattern);
if (!channelPattern.test(channel.src_channel)) {
throw new Error(`Network ${network} has invalid src_channel: ${channel.src_channel}`);
}
if (!channelPattern.test(channel.dst_channel)) {
throw new Error(`Network ${network} has invalid dst_channel: ${channel.dst_channel}`);
}
}
}
});

it('all counterparty chain names are non-empty strings', () => {
for (const channels of Object.values(IBC_INFO)) {
for (const channel of channels) {
expect(channel.counterparty_chain_name.trim().length).toBeGreaterThan(0);
}
}
});
});
21 changes: 21 additions & 0 deletions packages/registry/src/networks/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,25 @@ describe('Networks configuration', () => {
}
}
});

it('should have RPC URLs starting with https:// or wss://', () => {
for (const [networkId, networkConfig] of Object.entries(NETWORKS)) {
for (const endpoint of networkConfig.rpc) {
const url = endpoint.url;
const isValid = url.startsWith('https://') || url.startsWith('wss://');
expect(isValid).toBe(true);
if (!isValid) {
throw new Error(`Network ${networkId} has invalid RPC URL: ${url}`);
}
}
}
});

it('should have a non-empty provider name for each RPC endpoint', () => {
for (const networkConfig of Object.values(NETWORKS)) {
for (const endpoint of networkConfig.rpc) {
expect(endpoint.provider.trim().length).toBeGreaterThan(0);
}
}
});
});
34 changes: 34 additions & 0 deletions packages/registry/src/tokens/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,37 @@ it('should contain the "sei" asset with correct properties in each network', ()
}
}
});

describe('Token image URL validation', () => {
it('all token image URLs use https:// scheme', () => {
for (const [network, assets] of Object.entries(TOKEN_LIST)) {
for (const asset of assets) {
if (asset.images?.png) {
expect(asset.images.png).toMatch(/^https:\/\//);
if (!asset.images.png.startsWith('https://')) {
throw new Error(`Network ${network} token "${asset.symbol}" has non-https PNG image URL: ${asset.images.png}`);
}
}
if (asset.images?.svg) {
expect(asset.images.svg).toMatch(/^https:\/\//);
if (!asset.images.svg.startsWith('https://')) {
throw new Error(`Network ${network} token "${asset.symbol}" has non-https SVG image URL: ${asset.images.svg}`);
}
}
}
}
});

it('all token image URLs are non-empty when present', () => {
for (const assets of Object.values(TOKEN_LIST)) {
for (const asset of assets) {
if (asset.images?.png) {
expect(asset.images.png.trim().length).toBeGreaterThan(0);
}
if (asset.images?.svg) {
expect(asset.images.svg.trim().length).toBeGreaterThan(0);
}
}
}
});
});
32 changes: 32 additions & 0 deletions packages/sei-global-wallet/src/lib/__tests__/config.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { config } from '../config';

describe('sei-global-wallet config', () => {
it('walletName is a non-empty string', () => {
expect(typeof config.walletName).toBe('string');
expect(config.walletName.trim().length).toBeGreaterThan(0);
});

it('walletUrl starts with https://', () => {
expect(typeof config.walletUrl).toBe('string');
expect(config.walletUrl).toMatch(/^https:\/\//);
});

it('environmentId is a non-empty string', () => {
expect(typeof config.environmentId).toBe('string');
expect(config.environmentId.trim().length).toBeGreaterThan(0);
});

it('eip6963.rdns matches the io.sei.* pattern', () => {
expect(typeof config.eip6963.rdns).toBe('string');
expect(config.eip6963.rdns).toMatch(/^io\.sei\./);
});

it('walletIcon is a non-empty string', () => {
expect(typeof config.walletIcon).toBe('string');
expect((config.walletIcon as string).trim().length).toBeGreaterThan(0);
});

it('walletIcon is a valid data URI', () => {
expect(config.walletIcon as string).toMatch(/^data:/);
});
});
Loading