-
Notifications
You must be signed in to change notification settings - Fork 50
fix(mcp-server): resolve NFT owners and cover EVM resources (PLT-347) #335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
alexander-sei
merged 9 commits into
main
from
alexanderh/plt-347-update-sei-js-tests-to-cover-minimum-required-coverage-for
Aug 20, 2026
Merged
Changes from 2 commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
182e9bd
test: re-land SDK unit coverage on remaining packages (PLT-347)
alexander-sei dcd5ce6
test: satisfy ABI stateMutability typecheck (PLT-347)
alexander-sei a558505
test(mcp-server): cover registerEVMResources handlers (PLT-347)
alexander-sei 5a22e67
test: address coverage review feedback (PLT-347)
alexander-sei 890a121
fix(mcp-server): return current NFT resource owner (PLT-347)
alexander-sei a1902e5
test(mcp-server): fail loudly on missing resource mocks (PLT-347)
alexander-sei 170558b
fix(mcp-server): preserve NFT ownership validation (PLT-347)
alexander-sei dc657b9
fix(mcp-server): expose NFT owner lookup failures (PLT-347)
alexander-sei e488c02
test(mcp-server): cover non-numeric NFT token IDs (PLT-347)
alexander-sei File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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, '..'); | ||
| const TEMPLATES_DIR = path.join(PACKAGE_ROOT, 'templates'); | ||
| const EXTENSIONS_DIR = path.join(PACKAGE_ROOT, 'extensions'); | ||
|
|
||
| const EXPECTED_TEMPLATES = ['next-template']; | ||
|
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
77
packages/precompiles/src/precompiles/__tests__/abis.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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[]][] = [ | ||
|
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) => { | ||
|
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
52
packages/precompiles/src/precompiles/__tests__/addresses.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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. */ | ||
|
alexander-sei marked this conversation as resolved.
Outdated
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', () => { | ||
|
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}/); | ||
|
alexander-sei marked this conversation as resolved.
Outdated
|
||
| } | ||
| }); | ||
| }); | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
32 changes: 32 additions & 0 deletions
32
packages/sei-global-wallet/src/lib/__tests__/config.spec.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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\./); | ||
|
alexander-sei marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| it('walletIcon is a non-empty string', () => { | ||
|
alexander-sei marked this conversation as resolved.
Outdated
|
||
| expect(typeof config.walletIcon).toBe('string'); | ||
| expect((config.walletIcon as string).trim().length).toBeGreaterThan(0); | ||
|
alexander-sei marked this conversation as resolved.
Outdated
|
||
| }); | ||
|
|
||
| it('walletIcon is a valid data URI', () => { | ||
| expect(config.walletIcon as string).toMatch(/^data:/); | ||
| }); | ||
| }); | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.