Skip to content
Merged
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, expect, it } from 'bun:test';
import { promises as fs } from 'node:fs';
import path from 'node:path';

const PACKAGE_ROOT = path.resolve(import.meta.dir, '..');
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates');
const EXTENSIONS_DIR = path.join(PACKAGE_ROOT, 'extensions');

const EXPECTED_TEMPLATES = ['next-template'];
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
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);
});
});
77 changes: 77 additions & 0 deletions packages/precompiles/src/precompiles/__tests__/abis.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import {
ADDRESS_PRECOMPILE_ABI,
BANK_PRECOMPILE_ABI,
DISTRIBUTION_PRECOMPILE_ABI,
GOVERNANCE_PRECOMPILE_ABI,
JSON_PRECOMPILE_ABI,
P256_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[]][] = [
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
['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', 'submitProposal', 'voteWeighted']],
['JSON', JSON_PRECOMPILE_ABI, ['extractAsBytes', 'extractAsBytesList', 'extractAsUint256', 'extractAsBytesFromArray']],
['P256', P256_PRECOMPILE_ABI, ['verify']],
['POINTER', POINTER_PRECOMPILE_ABI, ['addCW20Pointer', 'addCW721Pointer', 'addCW1155Pointer', 'addNativePointer']],
['POINTERVIEW', POINTERVIEW_PRECOMPILE_ABI, ['getCW20Pointer', 'getCW721Pointer', 'getCW1155Pointer', 'getNativePointer']],
['SOLO', SOLO_PRECOMPILE_ABI, ['claim', 'claimSpecific']],
['STAKING', STAKING_PRECOMPILE_ABI, ['delegate', 'undelegate', 'redelegate', 'delegation']],
['WASM', WASM_PRECOMPILE_ABI, ['execute', 'execute_batch']]
];

describe('Precompile ABIs — function names', () => {
it.each(PRECOMPILE_ABIS)('%s ABI contains all expected function names', (_name, abi, expectedFunctions) => {
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
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][] = PRECOMPILE_ABIS.map(([name, abi]) => [name, 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);
}
});
});
52 changes: 52 additions & 0 deletions packages/precompiles/src/precompiles/__tests__/addresses.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import {
ADDRESS_PRECOMPILE_ADDRESS,
BANK_PRECOMPILE_ADDRESS,
DISTRIBUTION_PRECOMPILE_ADDRESS,
GOVERNANCE_PRECOMPILE_ADDRESS,
JSON_PRECOMPILE_ADDRESS,
P256_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],
['JSON', JSON_PRECOMPILE_ADDRESS],
['P256', P256_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. */
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
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', () => {
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
// Sei precompiles live in the reserved 0x1000–0x10FF range
for (const [, address] of PRECOMPILE_ADDRESSES) {
expect(address.toLowerCase()).toMatch(/^0x0{36}/);
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
}
});
});
16 changes: 16 additions & 0 deletions packages/registry/src/networks/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,4 +40,20 @@ describe('Networks configuration', () => {
expect(testnet.evm_ws?.some(({ provider, url }) => provider === 'dRPC' && url === 'wss://sei-testnet.drpc.org')).toBeTrue();
expect(testnet.explorers?.some(({ name }) => name === 'Seiscan')).toBeTrue();
});

it('should have RPC URLs starting with https:// or wss://', () => {
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
for (const networkConfig of Object.values(NETWORKS)) {
for (const endpoint of networkConfig.rpc) {
expect(endpoint.url.startsWith('https://') || endpoint.url.startsWith('wss://')).toBe(true);
}
}
});

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);
}
}
});
});
28 changes: 28 additions & 0 deletions packages/registry/src/tokens/__tests__/index.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,31 @@ 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 assets of Object.values(TOKEN_LIST)) {
for (const asset of assets) {
if (asset.images?.png) {
expect(asset.images.png).toMatch(/^https:\/\//);
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
}
if (asset.images?.svg) {
expect(asset.images.svg).toMatch(/^https:\/\//);
}
}
}
});

it('all token image URLs are non-empty when present', () => {
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
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);
}
}
}
});
Comment thread
cursor[bot] marked this conversation as resolved.
Outdated
});
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\./);
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
});

it('walletIcon is a non-empty string', () => {
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
expect(typeof config.walletIcon).toBe('string');
expect((config.walletIcon as string).trim().length).toBeGreaterThan(0);
Comment thread
alexander-sei marked this conversation as resolved.
Outdated
});

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