From c844d71675ffd565375aadef97e5f9c3d3a99453 Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Sun, 12 Apr 2026 12:12:02 -0300 Subject: [PATCH 1/4] feat(protocol-core): add @quickswap-defi/protocol-core package MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Convert QuickSwap-sdk repo to pnpm workspace monorepo and create protocol-core — a pure, zero-dependency protocol knowledge package that serves as single source of truth for chain configurations, stablecoin registries, fee constants, and protocol detection across all QuickSwap repos. Monorepo conversion: - Move existing SDK to packages/sdk/ (git mv, history preserved) - Add pnpm-workspace.yaml, root package.json, tsconfig.base.json - SDK build and tests unchanged (125 pass, 3 skipped) protocol-core (packages/protocol-core/): - 11 production chains: Polygon, Base, MANTRA, Manta, Soneium, Somnia, IMX, X-Layer, zkEVM, Dogechain, Ethereum - All 44 addresses EIP-55 checksummed with build-time validator - Pure functions + static data, zero runtime dependencies - Deep-frozen registry (Object.freeze recursive) - 167 tests, 12 test files, strict TDD - Dual CJS/ESM build via tsup - Publish: @quickswap-defi/protocol-core on npm --- .gitignore | 4 +- eslint.config.mjs | 2 +- package.json | 82 +--- packages/protocol-core/package.json | 41 ++ .../scripts/validate-addresses.ts | 81 ++++ .../src/__tests__/chains/chain-data.test.ts | 170 +++++++ .../src/__tests__/chains/registry.test.ts | 80 ++++ .../src/__tests__/chains/types.test.ts | 64 +++ .../protocol-core/src/__tests__/index.test.ts | 85 ++++ .../src/__tests__/protocol/fees.test.ts | 22 + .../src/__tests__/protocol/schema.test.ts | 20 + .../src/__tests__/protocol/versions.test.ts | 56 +++ .../src/__tests__/tokens/native.test.ts | 59 +++ .../src/__tests__/tokens/stablecoins.test.ts | 57 +++ .../src/__tests__/utils/constants.test.ts | 16 + .../src/__tests__/utils/math.test.ts | 30 ++ .../src/__tests__/utils/time.test.ts | 65 +++ packages/protocol-core/src/chains/base.ts | 20 + .../protocol-core/src/chains/dogechain.ts | 24 + packages/protocol-core/src/chains/ethereum.ts | 18 + packages/protocol-core/src/chains/imx.ts | 21 + packages/protocol-core/src/chains/manta.ts | 20 + packages/protocol-core/src/chains/mantra.ts | 21 + packages/protocol-core/src/chains/polygon.ts | 22 + packages/protocol-core/src/chains/registry.ts | 46 ++ packages/protocol-core/src/chains/somnia.ts | 19 + packages/protocol-core/src/chains/soneium.ts | 19 + packages/protocol-core/src/chains/types.ts | 35 ++ packages/protocol-core/src/chains/xlayer.ts | 20 + packages/protocol-core/src/chains/zkevm.ts | 21 + packages/protocol-core/src/index.ts | 22 + packages/protocol-core/src/protocol/fees.ts | 21 + packages/protocol-core/src/protocol/schema.ts | 7 + .../protocol-core/src/protocol/versions.ts | 16 + packages/protocol-core/src/tokens/native.ts | 37 ++ .../protocol-core/src/tokens/stablecoins.ts | 25 ++ packages/protocol-core/src/tokens/types.ts | 1 + packages/protocol-core/src/utils/constants.ts | 8 + packages/protocol-core/src/utils/math.ts | 4 + packages/protocol-core/src/utils/time.ts | 27 ++ packages/protocol-core/tsconfig.build.json | 8 + packages/protocol-core/tsconfig.json | 4 + packages/protocol-core/tsup.config.ts | 13 + packages/protocol-core/vitest.config.ts | 20 + packages/sdk/package.json | 74 ++++ {src => packages/sdk/src}/abis/ERC20.json | 0 {src => packages/sdk/src}/constants.ts | 0 {src => packages/sdk/src}/declarations.d.ts | 0 .../sdk/src}/entities/currency.ts | 0 .../src}/entities/fractions/currencyAmount.ts | 0 .../sdk/src}/entities/fractions/fraction.ts | 0 .../sdk/src}/entities/fractions/index.ts | 0 .../sdk/src}/entities/fractions/percent.ts | 0 .../sdk/src}/entities/fractions/price.ts | 0 .../src}/entities/fractions/tokenAmount.ts | 0 {src => packages/sdk/src}/entities/index.ts | 0 {src => packages/sdk/src}/entities/pair.ts | 0 {src => packages/sdk/src}/entities/route.ts | 0 {src => packages/sdk/src}/entities/token.ts | 0 {src => packages/sdk/src}/entities/trade.ts | 0 {src => packages/sdk/src}/errors.ts | 0 {src => packages/sdk/src}/fetcher.ts | 0 {src => packages/sdk/src}/index.ts | 0 {src => packages/sdk/src}/router.ts | 0 {src => packages/sdk/src}/utils.ts | 0 {test => packages/sdk/test}/constants.test.ts | 0 {test => packages/sdk/test}/data.test.ts | 0 {test => packages/sdk/test}/entities.test.ts | 0 {test => packages/sdk/test}/fraction.test.ts | 0 .../sdk/test}/miscellaneous.test.ts | 0 {test => packages/sdk/test}/pair.test.ts | 0 {test => packages/sdk/test}/route.test.ts | 0 {test => packages/sdk/test}/router.test.ts | 0 {test => packages/sdk/test}/token.test.ts | 0 {test => packages/sdk/test}/trade.test.ts | 0 packages/sdk/tsconfig.json | 12 + tsup.config.ts => packages/sdk/tsup.config.ts | 0 .../sdk/vitest.config.ts | 0 pnpm-lock.yaml | 415 +++++++++++++++++- pnpm-workspace.yaml | 2 + tsconfig.json => tsconfig.base.json | 15 +- 81 files changed, 1874 insertions(+), 97 deletions(-) create mode 100644 packages/protocol-core/package.json create mode 100644 packages/protocol-core/scripts/validate-addresses.ts create mode 100644 packages/protocol-core/src/__tests__/chains/chain-data.test.ts create mode 100644 packages/protocol-core/src/__tests__/chains/registry.test.ts create mode 100644 packages/protocol-core/src/__tests__/chains/types.test.ts create mode 100644 packages/protocol-core/src/__tests__/index.test.ts create mode 100644 packages/protocol-core/src/__tests__/protocol/fees.test.ts create mode 100644 packages/protocol-core/src/__tests__/protocol/schema.test.ts create mode 100644 packages/protocol-core/src/__tests__/protocol/versions.test.ts create mode 100644 packages/protocol-core/src/__tests__/tokens/native.test.ts create mode 100644 packages/protocol-core/src/__tests__/tokens/stablecoins.test.ts create mode 100644 packages/protocol-core/src/__tests__/utils/constants.test.ts create mode 100644 packages/protocol-core/src/__tests__/utils/math.test.ts create mode 100644 packages/protocol-core/src/__tests__/utils/time.test.ts create mode 100644 packages/protocol-core/src/chains/base.ts create mode 100644 packages/protocol-core/src/chains/dogechain.ts create mode 100644 packages/protocol-core/src/chains/ethereum.ts create mode 100644 packages/protocol-core/src/chains/imx.ts create mode 100644 packages/protocol-core/src/chains/manta.ts create mode 100644 packages/protocol-core/src/chains/mantra.ts create mode 100644 packages/protocol-core/src/chains/polygon.ts create mode 100644 packages/protocol-core/src/chains/registry.ts create mode 100644 packages/protocol-core/src/chains/somnia.ts create mode 100644 packages/protocol-core/src/chains/soneium.ts create mode 100644 packages/protocol-core/src/chains/types.ts create mode 100644 packages/protocol-core/src/chains/xlayer.ts create mode 100644 packages/protocol-core/src/chains/zkevm.ts create mode 100644 packages/protocol-core/src/index.ts create mode 100644 packages/protocol-core/src/protocol/fees.ts create mode 100644 packages/protocol-core/src/protocol/schema.ts create mode 100644 packages/protocol-core/src/protocol/versions.ts create mode 100644 packages/protocol-core/src/tokens/native.ts create mode 100644 packages/protocol-core/src/tokens/stablecoins.ts create mode 100644 packages/protocol-core/src/tokens/types.ts create mode 100644 packages/protocol-core/src/utils/constants.ts create mode 100644 packages/protocol-core/src/utils/math.ts create mode 100644 packages/protocol-core/src/utils/time.ts create mode 100644 packages/protocol-core/tsconfig.build.json create mode 100644 packages/protocol-core/tsconfig.json create mode 100644 packages/protocol-core/tsup.config.ts create mode 100644 packages/protocol-core/vitest.config.ts create mode 100644 packages/sdk/package.json rename {src => packages/sdk/src}/abis/ERC20.json (100%) rename {src => packages/sdk/src}/constants.ts (100%) rename {src => packages/sdk/src}/declarations.d.ts (100%) rename {src => packages/sdk/src}/entities/currency.ts (100%) rename {src => packages/sdk/src}/entities/fractions/currencyAmount.ts (100%) rename {src => packages/sdk/src}/entities/fractions/fraction.ts (100%) rename {src => packages/sdk/src}/entities/fractions/index.ts (100%) rename {src => packages/sdk/src}/entities/fractions/percent.ts (100%) rename {src => packages/sdk/src}/entities/fractions/price.ts (100%) rename {src => packages/sdk/src}/entities/fractions/tokenAmount.ts (100%) rename {src => packages/sdk/src}/entities/index.ts (100%) rename {src => packages/sdk/src}/entities/pair.ts (100%) rename {src => packages/sdk/src}/entities/route.ts (100%) rename {src => packages/sdk/src}/entities/token.ts (100%) rename {src => packages/sdk/src}/entities/trade.ts (100%) rename {src => packages/sdk/src}/errors.ts (100%) rename {src => packages/sdk/src}/fetcher.ts (100%) rename {src => packages/sdk/src}/index.ts (100%) rename {src => packages/sdk/src}/router.ts (100%) rename {src => packages/sdk/src}/utils.ts (100%) rename {test => packages/sdk/test}/constants.test.ts (100%) rename {test => packages/sdk/test}/data.test.ts (100%) rename {test => packages/sdk/test}/entities.test.ts (100%) rename {test => packages/sdk/test}/fraction.test.ts (100%) rename {test => packages/sdk/test}/miscellaneous.test.ts (100%) rename {test => packages/sdk/test}/pair.test.ts (100%) rename {test => packages/sdk/test}/route.test.ts (100%) rename {test => packages/sdk/test}/router.test.ts (100%) rename {test => packages/sdk/test}/token.test.ts (100%) rename {test => packages/sdk/test}/trade.test.ts (100%) create mode 100755 packages/sdk/tsconfig.json rename tsup.config.ts => packages/sdk/tsup.config.ts (100%) rename vitest.config.ts => packages/sdk/vitest.config.ts (100%) create mode 100644 pnpm-workspace.yaml rename tsconfig.json => tsconfig.base.json (68%) mode change 100755 => 100644 diff --git a/.gitignore b/.gitignore index aa32f7f..d3beeef 100755 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ dist node_modules -/packages \ No newline at end of file +**/dist +**/node_modules +**/coverage \ No newline at end of file diff --git a/eslint.config.mjs b/eslint.config.mjs index 67fad83..cdcd6cb 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -12,5 +12,5 @@ export default tseslint.config( }, }, }, - { ignores: ['dist/', 'node_modules/'] }, + { ignores: ['**/dist/', '**/node_modules/'] }, ) diff --git a/package.json b/package.json index b879c89..d4b8bdd 100755 --- a/package.json +++ b/package.json @@ -1,80 +1,22 @@ { - "name": "@quickswap-defi/sdk", - "license": "MIT", - "version": "1.0.1", - "description": "šŸ›  An SDK for building applications on top of Quickswap.", - "main": "dist/index.js", - "typings": "dist/index.d.ts", - "files": [ - "dist" - ], - "repository": "https://github.com/QuickSwap/QuickSwap-sdk", - "keywords": [ - "quickswap", - "matic", - "ethereum" - ], - "module": "dist/index.mjs", - "exports": { - ".": { - "types": "./dist/index.d.ts", - "import": "./dist/index.mjs", - "require": "./dist/index.js" - } - }, + "name": "quickswap-sdk-monorepo", + "private": true, "scripts": { - "lint": "eslint src test", - "build": "tsup", - "start": "tsup --watch", - "test": "vitest run", - "prepublishOnly": "tsup" - }, - "dependencies": { - "@uniswap/v2-core": "^1.0.0", - "big.js": "^5.2.2", - "decimal.js-light": "^2.5.0", - "jsbi": "^3.1.1", - "tiny-invariant": "^1.1.0", - "tiny-warning": "^1.0.3", - "toformat": "^2.0.0" - }, - "peerDependencies": { - "@ethersproject/address": "^5.0.0-beta", - "@ethersproject/contracts": "^5.0.0-beta", - "@ethersproject/networks": "^5.0.0-beta", - "@ethersproject/providers": "^5.0.0-beta", - "@ethersproject/solidity": "^5.0.0-beta" - }, - "devDependencies": { - "@ethersproject/address": "^5.0.2", - "@ethersproject/contracts": "^5.0.2", - "@ethersproject/networks": "^5.0.2", - "@ethersproject/providers": "^5.0.5", - "@ethersproject/solidity": "^5.0.2", - "@types/big.js": "^4.0.5", - "@eslint/js": "^9.0.0", - "eslint": "^9.0.0", - "tsup": "^8.0.0", - "typescript": "^5.5.0", - "typescript-eslint": "^8.0.0", - "vitest": "^2.0.0" - }, - "engines": { - "node": ">=16" - }, - "publishConfig": { - "access": "public", - "provenance": "true" - }, - "prettier": { - "printWidth": 120, - "semi": false, - "singleQuote": true + "build": "pnpm -r build", + "test": "pnpm -r test", + "lint": "pnpm -r lint", + "build:protocol-core": "pnpm --filter @quickswap-defi/protocol-core build", + "build:sdk": "pnpm --filter @quickswap-defi/sdk build", + "test:protocol-core": "pnpm --filter @quickswap-defi/protocol-core test", + "test:sdk": "pnpm --filter @quickswap-defi/sdk test" }, "pnpm": { "overrides": { "minimatch@<3.1.4": "^3.1.4", "form-data@<2.5.4": "^2.5.4" } + }, + "engines": { + "node": ">=16" } } diff --git a/packages/protocol-core/package.json b/packages/protocol-core/package.json new file mode 100644 index 0000000..49801d7 --- /dev/null +++ b/packages/protocol-core/package.json @@ -0,0 +1,41 @@ +{ + "name": "@quickswap-defi/protocol-core", + "version": "0.1.0", + "license": "MIT", + "description": "Pure protocol knowledge for QuickSwap DEX — chains, stablecoins, fees, schema detection", + "main": "dist/index.js", + "module": "dist/index.mjs", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "files": ["dist"], + "scripts": { + "build": "tsup", + "test": "vitest run", + "test:watch": "vitest", + "test:coverage": "vitest run --coverage", + "lint": "eslint src/", + "validate:addresses": "tsx scripts/validate-addresses.ts", + "prepublishOnly": "pnpm test && pnpm build && pnpm validate:addresses" + }, + "devDependencies": { + "@noble/hashes": "^1.7.0", + "@vitest/coverage-v8": "^2.0.0", + "tsup": "^8.0.0", + "tsx": "^4.0.0", + "typescript": "^5.5.0", + "vitest": "^2.0.0" + }, + "publishConfig": { + "access": "public", + "provenance": true + }, + "engines": { + "node": ">=18" + } +} diff --git a/packages/protocol-core/scripts/validate-addresses.ts b/packages/protocol-core/scripts/validate-addresses.ts new file mode 100644 index 0000000..0746ee7 --- /dev/null +++ b/packages/protocol-core/scripts/validate-addresses.ts @@ -0,0 +1,81 @@ +import { keccak_256 } from '@noble/hashes/sha3' +import { getSupportedChainIds, getChain } from '../src/chains/registry' + +function toChecksumAddress(address: string): string { + const addr = address.toLowerCase().replace('0x', '') + const hashBytes = keccak_256(new TextEncoder().encode(addr)) + const hash = Array.from(hashBytes).map((b) => b.toString(16).padStart(2, '0')).join('') + + let checksummed = '0x' + for (let i = 0; i < addr.length; i++) { + if (parseInt(hash[i], 16) >= 8) { + checksummed += addr[i].toUpperCase() + } else { + checksummed += addr[i] + } + } + return checksummed +} + +function isValidChecksum(address: string): boolean { + if (!address.startsWith('0x') || address.length !== 42) return false + return address === toChecksumAddress(address) +} + +/** Matches any bare EVM address: 0x followed by exactly 40 hex chars */ +const EVM_ADDRESS_RE = /^0x[0-9a-fA-F]{40}$/ + +/** + * Recursively walks an object and collects all string values that look like + * EVM addresses, along with a human-readable path for error messages. + */ +function collectAddresses( + value: unknown, + path: string, + out: Array<{ address: string; path: string }>, +): void { + if (typeof value === 'string') { + if (EVM_ADDRESS_RE.test(value)) { + out.push({ address: value, path }) + } + return + } + if (Array.isArray(value)) { + value.forEach((item, i) => collectAddresses(item, `${path}[${i}]`, out)) + return + } + if (value !== null && typeof value === 'object') { + for (const [key, child] of Object.entries(value as Record)) { + collectAddresses(child, path ? `${path}.${key}` : key, out) + } + } +} + +const chainIds = getSupportedChainIds() +let total = 0 +const errors: string[] = [] + +for (const chainId of chainIds) { + const chain = getChain(chainId)! + const found: Array<{ address: string; path: string }> = [] + collectAddresses(chain, chain.name, found) + + for (const { address, path } of found) { + total++ + if (!isValidChecksum(address)) { + errors.push( + `${chain.name} (${chainId}) ${path}: ${address} → expected ${toChecksumAddress(address)}`, + ) + } + } +} + +if (errors.length > 0) { + console.error(`\nāŒ ${errors.length} invalid EIP-55 checksum(s) found:\n`) + errors.forEach((e) => console.error(` • ${e}`)) + console.error('') + process.exit(1) +} else { + console.log(`āœ“ ${total} addresses validated across ${chainIds.length} chains`) + process.exit(0) +} diff --git a/packages/protocol-core/src/__tests__/chains/chain-data.test.ts b/packages/protocol-core/src/__tests__/chains/chain-data.test.ts new file mode 100644 index 0000000..9cddc45 --- /dev/null +++ b/packages/protocol-core/src/__tests__/chains/chain-data.test.ts @@ -0,0 +1,170 @@ +import { describe, it, expect } from 'vitest' +import { POLYGON } from '../../chains/polygon' +import { BASE } from '../../chains/base' +import { MANTRA } from '../../chains/mantra' +import { MANTA } from '../../chains/manta' +import { SONEIUM } from '../../chains/soneium' +import { SOMNIA } from '../../chains/somnia' +import { IMX } from '../../chains/imx' +import { XLAYER } from '../../chains/xlayer' +import { ZKEVM } from '../../chains/zkevm' +import { DOGECHAIN } from '../../chains/dogechain' +import { ETHEREUM } from '../../chains/ethereum' +import type { ChainConfig } from '../../chains/types' + +const ALL_CHAINS: ChainConfig[] = [ + POLYGON, + BASE, + MANTRA, + MANTA, + SONEIUM, + SOMNIA, + IMX, + XLAYER, + ZKEVM, + DOGECHAIN, + ETHEREUM, +] + +describe('chain data integrity', () => { + describe('basic fields', () => { + it.each(ALL_CHAINS)('$name has positive chainId', (chain) => { + expect(chain.chainId).toBeGreaterThan(0) + }) + + it.each(ALL_CHAINS)('$name has non-empty name', (chain) => { + expect(chain.name).toBeTruthy() + expect(chain.name.length).toBeGreaterThan(0) + }) + + it.each(ALL_CHAINS)('$name has non-empty nativeSymbol', (chain) => { + expect(chain.nativeSymbol).toBeTruthy() + expect(chain.nativeSymbol.length).toBeGreaterThan(0) + }) + }) + + describe('wrappedNative decimals', () => { + it.each(ALL_CHAINS)('$name wrappedNative has 18 decimals', (chain) => { + expect(chain.wrappedNative.decimals).toBe(18) + }) + }) + + describe('stablecoin decimals', () => { + it.each(ALL_CHAINS)('$name stablecoins all have 6 or 18 decimals', (chain) => { + for (const coin of chain.stablecoins) { + expect([6, 18]).toContain(coin.decimals) + } + }) + }) + + describe('stablecoin counts', () => { + it('Polygon has 4 stablecoins', () => { + expect(POLYGON.stablecoins).toHaveLength(4) + }) + + it('Base has 2 stablecoins', () => { + expect(BASE.stablecoins).toHaveLength(2) + }) + + it('MANTRA has 4 stablecoins', () => { + expect(MANTRA.stablecoins).toHaveLength(4) + }) + + it('Manta has 3 stablecoins', () => { + expect(MANTA.stablecoins).toHaveLength(3) + }) + + it('Soneium has 2 stablecoins', () => { + expect(SONEIUM.stablecoins).toHaveLength(2) + }) + + it('Somnia has 2 stablecoins', () => { + expect(SOMNIA.stablecoins).toHaveLength(2) + }) + + it('IMX has 4 stablecoins', () => { + expect(IMX.stablecoins).toHaveLength(4) + }) + + it('X Layer has 3 stablecoins', () => { + expect(XLAYER.stablecoins).toHaveLength(3) + }) + + it('zkEVM has 3 stablecoins', () => { + expect(ZKEVM.stablecoins).toHaveLength(3) + }) + + it('Dogechain has 3 stablecoins', () => { + expect(DOGECHAIN.stablecoins).toHaveLength(3) + }) + + it('Ethereum has 3 stablecoins', () => { + expect(ETHEREUM.stablecoins).toHaveLength(3) + }) + }) + + describe('protocol entries', () => { + it('Ethereum has empty protocols (aggregation-only)', () => { + expect(ETHEREUM.protocols).toHaveLength(0) + }) + + it('Polygon protocols: v3 before v2', () => { + expect(POLYGON.protocols[0].version).toBe('v3') + expect(POLYGON.protocols[1].version).toBe('v2') + }) + + it('Base protocols: v4 before v2', () => { + expect(BASE.protocols[0].version).toBe('v4') + expect(BASE.protocols[1].version).toBe('v2') + }) + + it('Dogechain protocols: v3 before v2', () => { + expect(DOGECHAIN.protocols[0].version).toBe('v3') + expect(DOGECHAIN.protocols[1].version).toBe('v2') + }) + + it('zkEVM protocols: v3 before univ3', () => { + expect(ZKEVM.protocols[0].version).toBe('v3') + expect(ZKEVM.protocols[1].version).toBe('univ3') + }) + + it('MANTRA has v4 with dynamic fee', () => { + expect(MANTRA.protocols).toHaveLength(1) + expect(MANTRA.protocols[0].version).toBe('v4') + expect(MANTRA.protocols[0].hasDynamicFee).toBe(true) + }) + + it('Manta has univ3 without dynamic fee', () => { + expect(MANTA.protocols).toHaveLength(1) + expect(MANTA.protocols[0].version).toBe('univ3') + expect(MANTA.protocols[0].hasDynamicFee).toBe(false) + }) + }) + + describe('Ethereum stablecoins', () => { + it('Ethereum includes USDC', () => { + const symbols = ETHEREUM.stablecoins.map((s) => s.symbol) + expect(symbols).toContain('USDC') + }) + + it('Ethereum includes USDT', () => { + const symbols = ETHEREUM.stablecoins.map((s) => s.symbol) + expect(symbols).toContain('USDT') + }) + }) + + describe('Dogechain DAI isolation', () => { + const ZKEVM_DAI_ADDRESS = '0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4' + + it('Dogechain DAI is NOT the zkEVM DAI address', () => { + const dogeDai = DOGECHAIN.stablecoins.find((s) => s.symbol === 'DAI') + expect(dogeDai).toBeDefined() + expect(dogeDai!.address).not.toBe(ZKEVM_DAI_ADDRESS) + }) + + it('Dogechain DAI address is the correct Dogechain-native DAI', () => { + const dogeDai = DOGECHAIN.stablecoins.find((s) => s.symbol === 'DAI') + expect(dogeDai!.address).toBe('0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C') + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/chains/registry.test.ts b/packages/protocol-core/src/__tests__/chains/registry.test.ts new file mode 100644 index 0000000..d273ede --- /dev/null +++ b/packages/protocol-core/src/__tests__/chains/registry.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from 'vitest' +import { CHAIN_REGISTRY, getChain, getSupportedChainIds } from '../../chains/registry' + +const EXPECTED_CHAIN_IDS = [137, 8453, 5888, 169, 1868, 5031, 13371, 196, 1101, 2000, 1] + +describe('chain registry', () => { + describe('getChain', () => { + it('returns MANTRA config for chainId 5888', () => { + const chain = getChain(5888) + expect(chain).toBeDefined() + expect(chain!.name).toBe('MANTRA') + expect(chain!.chainId).toBe(5888) + }) + + it('returns undefined for unknown chainId', () => { + expect(getChain(99999)).toBeUndefined() + }) + + it('returns Polygon config for chainId 137', () => { + const chain = getChain(137) + expect(chain).toBeDefined() + expect(chain!.nativeSymbol).toBe('POL') + }) + + it('returns Ethereum config for chainId 1', () => { + const chain = getChain(1) + expect(chain).toBeDefined() + expect(chain!.protocols).toHaveLength(0) + }) + }) + + describe('getSupportedChainIds', () => { + it('has length 11', () => { + expect(getSupportedChainIds()).toHaveLength(11) + }) + + it.each(EXPECTED_CHAIN_IDS)('contains chainId %i', (chainId) => { + expect(getSupportedChainIds()).toContain(chainId) + }) + }) + + describe('immutability', () => { + it('CHAIN_REGISTRY is frozen', () => { + expect(Object.isFrozen(CHAIN_REGISTRY)).toBe(true) + }) + + it('mutation of CHAIN_REGISTRY top-level has no effect', () => { + const before = CHAIN_REGISTRY[137] + try { + // @ts-expect-error intentional mutation attempt + CHAIN_REGISTRY[137] = undefined + } catch { + // strict mode throws — that's fine + } + expect(CHAIN_REGISTRY[137]).toBe(before) + }) + + it('nested stablecoins array is frozen (deep freeze)', () => { + const polygon = getChain(137)! + expect(Object.isFrozen(polygon.stablecoins)).toBe(true) + }) + + it('push to frozen stablecoins silently fails or throws', () => { + const polygon = getChain(137)! + const lengthBefore = polygon.stablecoins.length + try { + // @ts-expect-error intentional mutation attempt on readonly + ;(polygon.stablecoins as TokenInfo[]).push({ address: '0x0', symbol: 'FAKE', decimals: 6 }) + } catch { + // strict mode throws — acceptable + } + expect(polygon.stablecoins).toHaveLength(lengthBefore) + }) + + it('wrappedNative object is frozen', () => { + const polygon = getChain(137)! + expect(Object.isFrozen(polygon.wrappedNative)).toBe(true) + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/chains/types.test.ts b/packages/protocol-core/src/__tests__/chains/types.test.ts new file mode 100644 index 0000000..7b20729 --- /dev/null +++ b/packages/protocol-core/src/__tests__/chains/types.test.ts @@ -0,0 +1,64 @@ +import { describe, it, expectTypeOf } from 'vitest' +import { + PROTOCOL_VERSIONS, + SCHEMA_VARIANTS, + type ProtocolVersion, + type SchemaVariant, + type TokenInfo, + type ChainProtocolEntry, + type ChainConfig, +} from '../../chains/types' + +describe('chains/types', () => { + describe('ProtocolVersion', () => { + it('accepts valid values', () => { + const v2: ProtocolVersion = PROTOCOL_VERSIONS.V2 + const v3: ProtocolVersion = PROTOCOL_VERSIONS.V3 + const v4: ProtocolVersion = PROTOCOL_VERSIONS.V4 + const univ3: ProtocolVersion = PROTOCOL_VERSIONS.UNIV3 + expect(v2).toBe('v2') + expect(v3).toBe('v3') + expect(v4).toBe('v4') + expect(univ3).toBe('univ3') + }) + + it('has exactly 4 entries', () => { + expect(Object.keys(PROTOCOL_VERSIONS)).toHaveLength(4) + }) + }) + + describe('SchemaVariant', () => { + it('accepts valid values', () => { + const v2: SchemaVariant = SCHEMA_VARIANTS.V2 + const concentrated: SchemaVariant = SCHEMA_VARIANTS.CONCENTRATED + expect(v2).toBe('v2') + expect(concentrated).toBe('concentrated') + }) + + it('has exactly 2 entries', () => { + expect(Object.keys(SCHEMA_VARIANTS)).toHaveLength(2) + }) + }) + + describe('TokenInfo', () => { + it('is a flat interface with 3 fields', () => { + const token: TokenInfo = { address: '0x123', symbol: 'TEST', decimals: 18 } + expect(Object.keys(token)).toHaveLength(3) + }) + }) + + describe('ChainConfig', () => { + it('has readonly protocols as ReadonlyArray', () => { + const config: ChainConfig = { + chainId: 1, + name: 'Test', + nativeSymbol: 'ETH', + wrappedNative: { address: '0x123', symbol: 'WETH', decimals: 18 }, + protocols: [], + stablecoins: [], + } + expectTypeOf(config.protocols).toEqualTypeOf>() + expectTypeOf(config.stablecoins).toEqualTypeOf>() + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/index.test.ts b/packages/protocol-core/src/__tests__/index.test.ts new file mode 100644 index 0000000..6953f1f --- /dev/null +++ b/packages/protocol-core/src/__tests__/index.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from 'vitest' +import { + PROTOCOL_VERSIONS, + SCHEMA_VARIANTS, + CHAIN_REGISTRY, + getChain, + getSupportedChainIds, + getSchemaVariant, + getSupportedVersions, + getProtocolVersionLabel, + V2_FEE_BPS, + V2_FEE_RATE, + computeV2Fees, + getStablecoins, + getStablecoinAddresses, + isStablecoin, + getNativeToken, + getWrappedNative, + percentChange, + getDayTimestamps, + getDayIds, + getLast7DayTimestamps, + ONE_DAY, + SUBGRAPH_PAGE_SIZE, + MIN_PRICE_USD, + MIN_VOLUME_USD, +} from '../index' + +describe('barrel export (index.ts)', () => { + it('exports all type const objects', () => { + expect(PROTOCOL_VERSIONS.V2).toBe('v2') + expect(SCHEMA_VARIANTS.CONCENTRATED).toBe('concentrated') + }) + + it('exports chain registry functions', () => { + expect(typeof getChain).toBe('function') + expect(typeof getSupportedChainIds).toBe('function') + expect(CHAIN_REGISTRY).toBeDefined() + }) + + it('exports protocol functions', () => { + expect(typeof getSchemaVariant).toBe('function') + expect(typeof getSupportedVersions).toBe('function') + expect(typeof getProtocolVersionLabel).toBe('function') + }) + + it('exports fee constants', () => { + expect(V2_FEE_BPS).toBe(30) + expect(V2_FEE_RATE).toBe(0.003) + expect(typeof computeV2Fees).toBe('function') + }) + + it('exports token functions', () => { + expect(typeof getStablecoins).toBe('function') + expect(typeof getStablecoinAddresses).toBe('function') + expect(typeof isStablecoin).toBe('function') + expect(typeof getNativeToken).toBe('function') + expect(typeof getWrappedNative).toBe('function') + }) + + it('exports utility functions and constants', () => { + expect(typeof percentChange).toBe('function') + expect(typeof getDayTimestamps).toBe('function') + expect(typeof getDayIds).toBe('function') + expect(typeof getLast7DayTimestamps).toBe('function') + expect(ONE_DAY).toBe(86400) + expect(SUBGRAPH_PAGE_SIZE).toBe(1000) + expect(MIN_PRICE_USD).toBe(0.000001) + expect(MIN_VOLUME_USD).toBe(0.0001) + }) + + it('has exactly 24 runtime exports (types excluded)', () => { + // 2 const objects + 3 registry + 3 protocol + 3 fee + 5 token + 8 util = 24 + const allExports = { + PROTOCOL_VERSIONS, SCHEMA_VARIANTS, + CHAIN_REGISTRY, getChain, getSupportedChainIds, + getSchemaVariant, getSupportedVersions, getProtocolVersionLabel, + V2_FEE_BPS, V2_FEE_RATE, computeV2Fees, + getStablecoins, getStablecoinAddresses, isStablecoin, getNativeToken, getWrappedNative, + percentChange, getDayTimestamps, getDayIds, getLast7DayTimestamps, ONE_DAY, + SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD, + } + expect(Object.keys(allExports)).toHaveLength(24) + }) +}) diff --git a/packages/protocol-core/src/__tests__/protocol/fees.test.ts b/packages/protocol-core/src/__tests__/protocol/fees.test.ts new file mode 100644 index 0000000..dde1010 --- /dev/null +++ b/packages/protocol-core/src/__tests__/protocol/fees.test.ts @@ -0,0 +1,22 @@ +import { describe, it, expect } from 'vitest' +import { V2_FEE_BPS, V2_FEE_RATE, computeV2Fees } from '../../protocol/fees' + +describe('protocol/fees', () => { + it('V2_FEE_BPS is 30', () => { + expect(V2_FEE_BPS).toBe(30) + }) + + it('V2_FEE_RATE is 0.003', () => { + expect(V2_FEE_RATE).toBe(0.003) + }) + + describe('computeV2Fees', () => { + it('calculates correct fee', () => { + expect(computeV2Fees(1000)).toBeCloseTo(3, 10) + }) + + it('returns 0 for 0 input', () => { + expect(computeV2Fees(0)).toBe(0) + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/protocol/schema.test.ts b/packages/protocol-core/src/__tests__/protocol/schema.test.ts new file mode 100644 index 0000000..88112d3 --- /dev/null +++ b/packages/protocol-core/src/__tests__/protocol/schema.test.ts @@ -0,0 +1,20 @@ +import { describe, it, expect } from 'vitest' +import { getSchemaVariant } from '../../protocol/schema' + +describe('protocol/schema', () => { + it('maps v3 to concentrated', () => { + expect(getSchemaVariant('v3')).toBe('concentrated') + }) + + it('maps v4 to concentrated', () => { + expect(getSchemaVariant('v4')).toBe('concentrated') + }) + + it('maps univ3 to concentrated', () => { + expect(getSchemaVariant('univ3')).toBe('concentrated') + }) + + it('maps v2 to v2', () => { + expect(getSchemaVariant('v2')).toBe('v2') + }) +}) diff --git a/packages/protocol-core/src/__tests__/protocol/versions.test.ts b/packages/protocol-core/src/__tests__/protocol/versions.test.ts new file mode 100644 index 0000000..16b785e --- /dev/null +++ b/packages/protocol-core/src/__tests__/protocol/versions.test.ts @@ -0,0 +1,56 @@ +import { describe, it, expect } from 'vitest' +import { getSupportedVersions, getProtocolVersionLabel } from '../../protocol/versions' + +describe('protocol/versions', () => { + describe('getSupportedVersions', () => { + it('returns [v3, v2] for Polygon (137)', () => { + expect(getSupportedVersions(137)).toEqual(['v3', 'v2']) + }) + + it('returns [v4, v2] for Base (8453)', () => { + expect(getSupportedVersions(8453)).toEqual(['v4', 'v2']) + }) + + it('returns [v4] for MANTRA (5888)', () => { + expect(getSupportedVersions(5888)).toEqual(['v4']) + }) + + it('returns [univ3] for Manta (169)', () => { + expect(getSupportedVersions(169)).toEqual(['univ3']) + }) + + it('returns [] for Ethereum (1) — aggregation only', () => { + expect(getSupportedVersions(1)).toEqual([]) + }) + + it('returns [] for unknown chain', () => { + expect(getSupportedVersions(99999)).toEqual([]) + }) + }) + + describe('getProtocolVersionLabel', () => { + it('returns v4 for Base CL', () => { + expect(getProtocolVersionLabel(8453, true)).toBe('v4') + }) + + it('returns v3 for Polygon CL', () => { + expect(getProtocolVersionLabel(137, true)).toBe('v3') + }) + + it('returns univ3 for Manta CL', () => { + expect(getProtocolVersionLabel(169, true)).toBe('univ3') + }) + + it('returns v2 when not concentrated liquidity', () => { + expect(getProtocolVersionLabel(137, false)).toBe('v2') + }) + + it('returns v3 as fallback for unknown chain CL', () => { + expect(getProtocolVersionLabel(99999, true)).toBe('v3') + }) + + it('returns v2 for Ethereum (no protocols)', () => { + expect(getProtocolVersionLabel(1, true)).toBe('v3') // fallback — Ethereum has no CL protocol + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/tokens/native.test.ts b/packages/protocol-core/src/__tests__/tokens/native.test.ts new file mode 100644 index 0000000..2d05641 --- /dev/null +++ b/packages/protocol-core/src/__tests__/tokens/native.test.ts @@ -0,0 +1,59 @@ +import { describe, it, expect } from 'vitest' +import { getNativeToken, getWrappedNative } from '../../tokens/native' + +describe('tokens/native', () => { + describe('getNativeToken', () => { + it('returns POL info for Polygon', () => { + const result = getNativeToken(137) + expect(result).toBeDefined() + expect(result!.symbol).toBe('POL') + expect(result!.decimals).toBe(18) + }) + + it('returns ETH info for Base', () => { + const result = getNativeToken(8453) + expect(result).toBeDefined() + expect(result!.symbol).toBe('ETH') + }) + + it('returns SOMI info for Somnia', () => { + const result = getNativeToken(5031) + expect(result).toBeDefined() + expect(result!.symbol).toBe('SOMI') + }) + + it('returns undefined for unknown chain', () => { + expect(getNativeToken(99999)).toBeUndefined() + }) + }) + + describe('getWrappedNative', () => { + it('returns WMATIC for Polygon', () => { + const result = getWrappedNative(137) + expect(result).toBeDefined() + expect(result!.symbol).toBe('WMATIC') + expect(result!.decimals).toBe(18) + }) + + it('returns WETH for Base', () => { + const result = getWrappedNative(8453) + expect(result).toBeDefined() + expect(result!.symbol).toBe('WETH') + }) + + it('returns undefined for unknown chain', () => { + expect(getWrappedNative(99999)).toBeUndefined() + }) + }) + + describe('address equality', () => { + it('getNativeToken and getWrappedNative share the same address', () => { + const chains = [137, 8453, 5888, 169, 1868, 5031, 13371, 196, 1101, 2000, 1] + for (const chainId of chains) { + const native = getNativeToken(chainId) + const wrapped = getWrappedNative(chainId) + expect(native?.address).toBe(wrapped?.address) + } + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/tokens/stablecoins.test.ts b/packages/protocol-core/src/__tests__/tokens/stablecoins.test.ts new file mode 100644 index 0000000..3ddf8b1 --- /dev/null +++ b/packages/protocol-core/src/__tests__/tokens/stablecoins.test.ts @@ -0,0 +1,57 @@ +import { describe, it, expect } from 'vitest' +import { getStablecoins, getStablecoinAddresses, isStablecoin } from '../../tokens/stablecoins' + +describe('tokens/stablecoins', () => { + describe('getStablecoins', () => { + it('returns 4 stablecoins for MANTRA (5888)', () => { + const result = getStablecoins(5888) + expect(result).toHaveLength(4) + expect(result.map((s) => s.symbol)).toContain('mantraUSD') + }) + + it('returns 4 stablecoins for IMX (13371)', () => { + const result = getStablecoins(13371) + expect(result).toHaveLength(4) + expect(result.map((s) => s.symbol)).toContain('axlUSDC') + }) + + it('returns 3 stablecoins for Ethereum (1)', () => { + expect(getStablecoins(1)).toHaveLength(3) + }) + + it('returns [] for unknown chain', () => { + expect(getStablecoins(99999)).toEqual([]) + }) + }) + + describe('getStablecoinAddresses', () => { + it('returns lowercase addresses', () => { + const addresses = getStablecoinAddresses(137) + addresses.forEach((addr) => { + expect(addr).toBe(addr.toLowerCase()) + }) + }) + + it('returns 4 addresses for Polygon', () => { + expect(getStablecoinAddresses(137)).toHaveLength(4) + }) + }) + + describe('isStablecoin', () => { + it('returns true for known stablecoin (lowercase input)', () => { + expect(isStablecoin(137, '0x2791bca1f2de4661ed88a30c99a7a9449aa84174')).toBe(true) + }) + + it('returns true for known stablecoin (checksummed input)', () => { + expect(isStablecoin(137, '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174')).toBe(true) + }) + + it('returns false for non-stablecoin address', () => { + expect(isStablecoin(137, '0x0000000000000000000000000000000000000001')).toBe(false) + }) + + it('returns false for unknown chain', () => { + expect(isStablecoin(99999, '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174')).toBe(false) + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/utils/constants.test.ts b/packages/protocol-core/src/__tests__/utils/constants.test.ts new file mode 100644 index 0000000..2ac765c --- /dev/null +++ b/packages/protocol-core/src/__tests__/utils/constants.test.ts @@ -0,0 +1,16 @@ +import { describe, it, expect } from 'vitest' +import { SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD } from '../../utils/constants' + +describe('utils/constants', () => { + it('SUBGRAPH_PAGE_SIZE is 1000', () => { + expect(SUBGRAPH_PAGE_SIZE).toBe(1000) + }) + + it('MIN_PRICE_USD is 0.000001', () => { + expect(MIN_PRICE_USD).toBe(0.000001) + }) + + it('MIN_VOLUME_USD is 0.0001', () => { + expect(MIN_VOLUME_USD).toBe(0.0001) + }) +}) diff --git a/packages/protocol-core/src/__tests__/utils/math.test.ts b/packages/protocol-core/src/__tests__/utils/math.test.ts new file mode 100644 index 0000000..9cfe8e4 --- /dev/null +++ b/packages/protocol-core/src/__tests__/utils/math.test.ts @@ -0,0 +1,30 @@ +import { describe, it, expect } from 'vitest' +import { percentChange } from '../../utils/math' + +describe('utils/math', () => { + describe('percentChange', () => { + it('calculates positive change', () => { + expect(percentChange(110, 100)).toBe(10) + }) + + it('returns 0 when previous is 0 (divide-by-zero guard)', () => { + expect(percentChange(100, 0)).toBe(0) + }) + + it('calculates negative change', () => { + expect(percentChange(5, 10)).toBe(-50) + }) + + it('returns 0 when values are equal', () => { + expect(percentChange(100, 100)).toBe(0) + }) + + it('handles negative values', () => { + expect(percentChange(-50, -100)).toBe(-50) + }) + + it('handles both zero', () => { + expect(percentChange(0, 0)).toBe(0) + }) + }) +}) diff --git a/packages/protocol-core/src/__tests__/utils/time.test.ts b/packages/protocol-core/src/__tests__/utils/time.test.ts new file mode 100644 index 0000000..2e0dc4b --- /dev/null +++ b/packages/protocol-core/src/__tests__/utils/time.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from 'vitest' +import { ONE_DAY, getDayTimestamps, getDayIds, getLast7DayTimestamps } from '../../utils/time' + +describe('utils/time', () => { + it('ONE_DAY is 86400', () => { + expect(ONE_DAY).toBe(86400) + }) + + describe('getDayTimestamps', () => { + it('returns timestamps for a 3-day range', () => { + const start = 1704067200 // 2024-01-01 00:00:00 UTC + const end = start + 2 * ONE_DAY // 2024-01-03 00:00:00 UTC + const result = getDayTimestamps(start, end) + expect(result).toHaveLength(3) + expect(result[0]).toBe(1704067200) + expect(result[1]).toBe(1704067200 + ONE_DAY) + expect(result[2]).toBe(1704067200 + 2 * ONE_DAY) + }) + + it('returns single element when start equals end', () => { + const ts = 1704067200 + expect(getDayTimestamps(ts, ts)).toHaveLength(1) + }) + + it('snaps to UTC midnight', () => { + const midday = 1704067200 + 43200 // noon + const result = getDayTimestamps(midday, midday) + expect(result[0]).toBe(1704067200) // snapped to midnight + }) + + it('returns empty array when start > end', () => { + expect(getDayTimestamps(1704067200 + ONE_DAY, 1704067200)).toHaveLength(0) + }) + }) + + describe('getDayIds', () => { + it('returns floor(timestamp / ONE_DAY) values', () => { + const start = 1704067200 + const result = getDayIds(start, start + ONE_DAY) + expect(result).toHaveLength(2) + expect(result[0]).toBe(Math.floor(start / ONE_DAY)) + expect(result[1]).toBe(Math.floor(start / ONE_DAY) + 1) + }) + }) + + describe('getLast7DayTimestamps', () => { + it('returns exactly 7 elements', () => { + expect(getLast7DayTimestamps()).toHaveLength(7) + }) + + it('returns descending order (today first)', () => { + const result = getLast7DayTimestamps() + for (let i = 0; i < result.length - 1; i++) { + expect(result[i] - result[i + 1]).toBe(ONE_DAY) + } + }) + + it('first element is today UTC midnight', () => { + const result = getLast7DayTimestamps() + const now = Math.floor(Date.now() / 1000) + const todayMidnight = Math.floor(now / ONE_DAY) * ONE_DAY + expect(result[0]).toBe(todayMidnight) + }) + }) +}) diff --git a/packages/protocol-core/src/chains/base.ts b/packages/protocol-core/src/chains/base.ts new file mode 100644 index 0000000..18315fb --- /dev/null +++ b/packages/protocol-core/src/chains/base.ts @@ -0,0 +1,20 @@ +import type { ChainConfig } from './types' + +export const BASE: ChainConfig = { + chainId: 8453, + name: 'Base', + nativeSymbol: 'ETH', + wrappedNative: { + address: '0x4200000000000000000000000000000000000006', + symbol: 'WETH', + decimals: 18, + }, + protocols: [ + { version: 'v4', hasDynamicFee: true }, + { version: 'v2', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', symbol: 'USDC', decimals: 6 }, + { address: '0xfde4C96c8593536E31F229EA8f37b2ADa2699bb2', symbol: 'USDT', decimals: 6 }, + ], +} diff --git a/packages/protocol-core/src/chains/dogechain.ts b/packages/protocol-core/src/chains/dogechain.ts new file mode 100644 index 0000000..2b4162b --- /dev/null +++ b/packages/protocol-core/src/chains/dogechain.ts @@ -0,0 +1,24 @@ +import type { ChainConfig } from './types' + +// NOTE: DAI address (0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C) is Dogechain's own DAI token. +// Do NOT use zkEVM's DAI (0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4) — interface-v3 has a bug +// referencing DAI[ChainId.ZKEVM] for Dogechain. +export const DOGECHAIN: ChainConfig = { + chainId: 2000, + name: 'Dogechain', + nativeSymbol: 'DOGE', + wrappedNative: { + address: '0xB7ddC6414bf4F5515b52D8BdD69973Ae205ff101', + symbol: 'WWDOGE', + decimals: 18, + }, + protocols: [ + { version: 'v3', hasDynamicFee: false }, + { version: 'v2', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0x765277EebeCA2e31912C9946eAe1021199B39C61', symbol: 'USDC', decimals: 6 }, + { address: '0xE3F5a90F9cb311505cd691a46596599aA1A0AD7D', symbol: 'USDT', decimals: 6 }, + { address: '0x639A647fbe20b6c8ac19E48E2de44ea792c62c5C', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/chains/ethereum.ts b/packages/protocol-core/src/chains/ethereum.ts new file mode 100644 index 0000000..d187cdc --- /dev/null +++ b/packages/protocol-core/src/chains/ethereum.ts @@ -0,0 +1,18 @@ +import type { ChainConfig } from './types' + +export const ETHEREUM: ChainConfig = { + chainId: 1, + name: 'Ethereum', + nativeSymbol: 'ETH', + wrappedNative: { + address: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', + symbol: 'WETH', + decimals: 18, + }, + protocols: [], + stablecoins: [ + { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 }, + { address: '0xdAC17F958D2ee523a2206206994597C13D831ec7', symbol: 'USDT', decimals: 6 }, + { address: '0x6B175474E89094C44Da98b954EedeAC495271d0F', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/chains/imx.ts b/packages/protocol-core/src/chains/imx.ts new file mode 100644 index 0000000..c7c047b --- /dev/null +++ b/packages/protocol-core/src/chains/imx.ts @@ -0,0 +1,21 @@ +import type { ChainConfig } from './types' + +export const IMX: ChainConfig = { + chainId: 13371, + name: 'Immutable zkEVM', + nativeSymbol: 'IMX', + wrappedNative: { + address: '0x3A0C2Ba54D6CBd3121F01b96dFd20e99D1696C9D', + symbol: 'WIMX', + decimals: 18, + }, + protocols: [ + { version: 'univ3', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2', symbol: 'USDC', decimals: 6 }, + { address: '0x68bcc7F1190AF20e7b572BCfb431c3Ac10A936Ab', symbol: 'USDT', decimals: 6 }, + { address: '0x00000000eFE302BEAA2b3e6e1b18d08D69a9012a', symbol: 'AUSD', decimals: 6 }, + { address: '0xEB466342C4d449BC9f53A865D5Cb90586f405215', symbol: 'axlUSDC', decimals: 6 }, + ], +} diff --git a/packages/protocol-core/src/chains/manta.ts b/packages/protocol-core/src/chains/manta.ts new file mode 100644 index 0000000..663da4d --- /dev/null +++ b/packages/protocol-core/src/chains/manta.ts @@ -0,0 +1,20 @@ +import type { ChainConfig } from './types' + +export const MANTA: ChainConfig = { + chainId: 169, + name: 'Manta Pacific', + nativeSymbol: 'ETH', + wrappedNative: { + address: '0x0Dc808adcE2099A9F62AA87D9670745AbA741746', + symbol: 'WETH', + decimals: 18, + }, + protocols: [ + { version: 'univ3', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0xb73603C5d87fA094B7314C74ACE2e64D165016fb', symbol: 'USDC', decimals: 6 }, + { address: '0xf417F5A458eC102B90352F697D6e2Ac3A3d2851f', symbol: 'USDT', decimals: 6 }, + { address: '0x1c466b9371f8aBA0D7c458bE10a62192Fcb8Aa71', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/chains/mantra.ts b/packages/protocol-core/src/chains/mantra.ts new file mode 100644 index 0000000..8acd100 --- /dev/null +++ b/packages/protocol-core/src/chains/mantra.ts @@ -0,0 +1,21 @@ +import type { ChainConfig } from './types' + +export const MANTRA: ChainConfig = { + chainId: 5888, + name: 'MANTRA', + nativeSymbol: 'OM', + wrappedNative: { + address: '0xE3047710EF6cB36Bcf1E58145529778eA7Cb5598', + symbol: 'WMANTRA', + decimals: 18, + }, + protocols: [ + { version: 'v4', hasDynamicFee: true }, + ], + stablecoins: [ + { address: '0xd2b95283011E47257917770D28Bb3EE44c849f6F', symbol: 'mantraUSD', decimals: 18 }, + { address: '0x5E76be0F4e09057D75140216F70fd4cE3365bb29', symbol: 'USDC', decimals: 6 }, + { address: '0x680e8ECB908A2040232ef139A0A52cbE47b9F15B', symbol: 'USDT', decimals: 6 }, + { address: '0x3806640578b710d8480910bF51510bc538d2F51A', symbol: 'USDT_LUCID', decimals: 6 }, + ], +} diff --git a/packages/protocol-core/src/chains/polygon.ts b/packages/protocol-core/src/chains/polygon.ts new file mode 100644 index 0000000..5ae2979 --- /dev/null +++ b/packages/protocol-core/src/chains/polygon.ts @@ -0,0 +1,22 @@ +import type { ChainConfig } from './types' + +export const POLYGON: ChainConfig = { + chainId: 137, + name: 'Polygon PoS', + nativeSymbol: 'POL', + wrappedNative: { + address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', + symbol: 'WMATIC', + decimals: 18, + }, + protocols: [ + { version: 'v3', hasDynamicFee: false }, + { version: 'v2', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', symbol: 'USDC.e', decimals: 6 }, + { address: '0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359', symbol: 'USDC', decimals: 6 }, + { address: '0xc2132D05D31c914a87C6611C10748AEb04B58e8F', symbol: 'USDT', decimals: 6 }, + { address: '0x8f3Cf7ad23Cd3CaDbD9735AFf958023239c6A063', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/chains/registry.ts b/packages/protocol-core/src/chains/registry.ts new file mode 100644 index 0000000..a8cf61d --- /dev/null +++ b/packages/protocol-core/src/chains/registry.ts @@ -0,0 +1,46 @@ +import type { ChainConfig } from './types' +import { POLYGON } from './polygon' +import { BASE } from './base' +import { MANTRA } from './mantra' +import { MANTA } from './manta' +import { SONEIUM } from './soneium' +import { SOMNIA } from './somnia' +import { IMX } from './imx' +import { XLAYER } from './xlayer' +import { ZKEVM } from './zkevm' +import { DOGECHAIN } from './dogechain' +import { ETHEREUM } from './ethereum' + +function deepFreeze(obj: T): Readonly { + Object.freeze(obj) + for (const value of Object.values(obj)) { + if (typeof value === 'object' && value !== null && !Object.isFrozen(value)) { + deepFreeze(value) + } + } + return obj as Readonly +} + +const _registry: Record = { + [POLYGON.chainId]: POLYGON, + [BASE.chainId]: BASE, + [MANTRA.chainId]: MANTRA, + [MANTA.chainId]: MANTA, + [SONEIUM.chainId]: SONEIUM, + [SOMNIA.chainId]: SOMNIA, + [IMX.chainId]: IMX, + [XLAYER.chainId]: XLAYER, + [ZKEVM.chainId]: ZKEVM, + [DOGECHAIN.chainId]: DOGECHAIN, + [ETHEREUM.chainId]: ETHEREUM, +} + +export const CHAIN_REGISTRY: Readonly> = deepFreeze(_registry) + +export function getChain(chainId: number): ChainConfig | undefined { + return CHAIN_REGISTRY[chainId] +} + +export function getSupportedChainIds(): number[] { + return Object.keys(CHAIN_REGISTRY).map(Number) +} diff --git a/packages/protocol-core/src/chains/somnia.ts b/packages/protocol-core/src/chains/somnia.ts new file mode 100644 index 0000000..88a92ad --- /dev/null +++ b/packages/protocol-core/src/chains/somnia.ts @@ -0,0 +1,19 @@ +import type { ChainConfig } from './types' + +export const SOMNIA: ChainConfig = { + chainId: 5031, + name: 'Somnia', + nativeSymbol: 'SOMI', + wrappedNative: { + address: '0x046EDe9564A72571df6F5e44d0405360c0f4dCab', + symbol: 'WSOMI', + decimals: 18, + }, + protocols: [ + { version: 'v4', hasDynamicFee: true }, + ], + stablecoins: [ + { address: '0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00', symbol: 'USDC', decimals: 6 }, + { address: '0x67B302E35Aef5EEE8c32D934F5856869EF428330', symbol: 'USDT', decimals: 6 }, + ], +} diff --git a/packages/protocol-core/src/chains/soneium.ts b/packages/protocol-core/src/chains/soneium.ts new file mode 100644 index 0000000..172f1d1 --- /dev/null +++ b/packages/protocol-core/src/chains/soneium.ts @@ -0,0 +1,19 @@ +import type { ChainConfig } from './types' + +export const SONEIUM: ChainConfig = { + chainId: 1868, + name: 'Soneium', + nativeSymbol: 'ETH', + wrappedNative: { + address: '0x4200000000000000000000000000000000000006', + symbol: 'WETH', + decimals: 18, + }, + protocols: [ + { version: 'v4', hasDynamicFee: true }, + ], + stablecoins: [ + { address: '0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369', symbol: 'USDC', decimals: 6 }, + { address: '0x3A337a6adA9d885b6Ad95ec48F9b75f197b5AE35', symbol: 'USDT', decimals: 6 }, + ], +} diff --git a/packages/protocol-core/src/chains/types.ts b/packages/protocol-core/src/chains/types.ts new file mode 100644 index 0000000..677a6c4 --- /dev/null +++ b/packages/protocol-core/src/chains/types.ts @@ -0,0 +1,35 @@ +export const PROTOCOL_VERSIONS = { + V2: 'v2', + V3: 'v3', + V4: 'v4', + UNIV3: 'univ3', +} as const + +export type ProtocolVersion = (typeof PROTOCOL_VERSIONS)[keyof typeof PROTOCOL_VERSIONS] + +export const SCHEMA_VARIANTS = { + V2: 'v2', + CONCENTRATED: 'concentrated', +} as const + +export type SchemaVariant = (typeof SCHEMA_VARIANTS)[keyof typeof SCHEMA_VARIANTS] + +export interface TokenInfo { + readonly address: string + readonly symbol: string + readonly decimals: number +} + +export interface ChainProtocolEntry { + readonly version: ProtocolVersion + readonly hasDynamicFee: boolean +} + +export interface ChainConfig { + readonly chainId: number + readonly name: string + readonly nativeSymbol: string + readonly wrappedNative: TokenInfo + readonly protocols: ReadonlyArray + readonly stablecoins: ReadonlyArray +} diff --git a/packages/protocol-core/src/chains/xlayer.ts b/packages/protocol-core/src/chains/xlayer.ts new file mode 100644 index 0000000..78cbccf --- /dev/null +++ b/packages/protocol-core/src/chains/xlayer.ts @@ -0,0 +1,20 @@ +import type { ChainConfig } from './types' + +export const XLAYER: ChainConfig = { + chainId: 196, + name: 'X-Layer', + nativeSymbol: 'OKB', + wrappedNative: { + address: '0xe538905cf8410324e03A5A23C1c177a474D59b2b', + symbol: 'WOKB', + decimals: 18, + }, + protocols: [ + { version: 'v3', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0x1E4a5963aBFD975d8c9021ce480b42188849D41d', symbol: 'USDT', decimals: 6 }, + { address: '0x74b7F16337b8972027F6196A17a631aC6dE26d22', symbol: 'USDC', decimals: 6 }, + { address: '0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/chains/zkevm.ts b/packages/protocol-core/src/chains/zkevm.ts new file mode 100644 index 0000000..9cd2e31 --- /dev/null +++ b/packages/protocol-core/src/chains/zkevm.ts @@ -0,0 +1,21 @@ +import type { ChainConfig } from './types' + +export const ZKEVM: ChainConfig = { + chainId: 1101, + name: 'Polygon zkEVM', + nativeSymbol: 'ETH', + wrappedNative: { + address: '0x4F9A0e7FD2Bf6067db6994CF12E4495Df938E6e9', + symbol: 'WETH', + decimals: 18, + }, + protocols: [ + { version: 'v3', hasDynamicFee: false }, + { version: 'univ3', hasDynamicFee: false }, + ], + stablecoins: [ + { address: '0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035', symbol: 'USDC', decimals: 6 }, + { address: '0x1E4a5963aBFD975d8c9021ce480b42188849D41d', symbol: 'USDT', decimals: 6 }, + { address: '0xC5015b9d9161Dca7e18e32f6f25C4aD850731Fd4', symbol: 'DAI', decimals: 18 }, + ], +} diff --git a/packages/protocol-core/src/index.ts b/packages/protocol-core/src/index.ts new file mode 100644 index 0000000..bb4d83a --- /dev/null +++ b/packages/protocol-core/src/index.ts @@ -0,0 +1,22 @@ +// Types +export type { ProtocolVersion, SchemaVariant, TokenInfo, ChainProtocolEntry, ChainConfig } from './chains/types' +export { PROTOCOL_VERSIONS, SCHEMA_VARIANTS } from './chains/types' + +// Chain Registry +export { CHAIN_REGISTRY, getChain, getSupportedChainIds } from './chains/registry' + +// Protocol Functions +export { getSchemaVariant } from './protocol/schema' +export { getSupportedVersions, getProtocolVersionLabel } from './protocol/versions' + +// Fee Constants & Functions +export { V2_FEE_BPS, V2_FEE_RATE, computeV2Fees } from './protocol/fees' + +// Token Functions +export { getStablecoins, getStablecoinAddresses, isStablecoin } from './tokens/stablecoins' +export { getNativeToken, getWrappedNative } from './tokens/native' + +// Utility Functions & Constants +export { percentChange } from './utils/math' +export { getDayTimestamps, getDayIds, getLast7DayTimestamps, ONE_DAY } from './utils/time' +export { SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD } from './utils/constants' diff --git a/packages/protocol-core/src/protocol/fees.ts b/packages/protocol-core/src/protocol/fees.ts new file mode 100644 index 0000000..b59d4ed --- /dev/null +++ b/packages/protocol-core/src/protocol/fees.ts @@ -0,0 +1,21 @@ +/** V2 swap fee in basis points (0.30%) */ +export const V2_FEE_BPS = 30 + +/** V2 swap fee as a decimal rate (30 BPS = 0.003) */ +export const V2_FEE_RATE = 0.003 + +/** + * Compute the V2 fee amount for a given input amount. + * + * @param amountIn - The input token amount (as a plain JS number) + * @returns The fee amount as a JS number + * + * WARNING: This function uses standard IEEE-754 floating-point arithmetic. + * It is intended ONLY for display/analytics approximations (e.g., "~$0.03 fee"). + * Do NOT use the result for on-chain-accurate calculations — for that, use + * integer/BigInt arithmetic with the raw token amounts and apply the 0.30% fee + * in full-precision basis-point math (amountIn * 30 / 10_000). + */ +export function computeV2Fees(amountIn: number): number { + return amountIn * V2_FEE_RATE +} diff --git a/packages/protocol-core/src/protocol/schema.ts b/packages/protocol-core/src/protocol/schema.ts new file mode 100644 index 0000000..d06926a --- /dev/null +++ b/packages/protocol-core/src/protocol/schema.ts @@ -0,0 +1,7 @@ +import type { ProtocolVersion, SchemaVariant } from '../chains/types' + +const CONCENTRATED_VERSIONS = new Set(['v3', 'v4', 'univ3']) + +export function getSchemaVariant(version: ProtocolVersion): SchemaVariant { + return CONCENTRATED_VERSIONS.has(version) ? 'concentrated' : 'v2' +} diff --git a/packages/protocol-core/src/protocol/versions.ts b/packages/protocol-core/src/protocol/versions.ts new file mode 100644 index 0000000..262fd35 --- /dev/null +++ b/packages/protocol-core/src/protocol/versions.ts @@ -0,0 +1,16 @@ +import type { ProtocolVersion } from '../chains/types' +import { getChain } from '../chains/registry' + +export function getSupportedVersions(chainId: number): ProtocolVersion[] { + const chain = getChain(chainId) + if (!chain) return [] + return chain.protocols.map((p) => p.version) +} + +export function getProtocolVersionLabel(chainId: number, isConcentratedLiquidity: boolean): string { + if (!isConcentratedLiquidity) return 'v2' + const chain = getChain(chainId) + if (!chain) return 'v3' + const clProtocol = chain.protocols.find((p) => p.version !== 'v2') + return clProtocol?.version ?? 'v3' +} diff --git a/packages/protocol-core/src/tokens/native.ts b/packages/protocol-core/src/tokens/native.ts new file mode 100644 index 0000000..798198b --- /dev/null +++ b/packages/protocol-core/src/tokens/native.ts @@ -0,0 +1,37 @@ +import type { TokenInfo } from '../chains/types' +import { getChain } from '../chains/registry' + +/** Lazy cache: chainId → frozen TokenInfo for the native gas token */ +const _nativeTokenCache = new Map() + +/** + * Returns the native gas token info for a chain. + * + * The address is the WRAPPED native token address (e.g., WPOL for POL), + * which is the canonical on-chain address used in analytics, subgraph pricing, + * and pool lookups. + * + * NOTE: Some UI contexts expect the zero address (0x000...0) or a sentinel + * for native tokens. This function does NOT return those — + * use the wrapped address for all protocol/analytics operations. + * + * The returned object is frozen and cached — subsequent calls for the same + * chainId return the identical cached reference without re-allocating. + */ +export function getNativeToken(chainId: number): TokenInfo | undefined { + const cached = _nativeTokenCache.get(chainId) + if (cached !== undefined) return cached + const chain = getChain(chainId) + if (!chain) return undefined + const token = Object.freeze({ + address: chain.wrappedNative.address, + symbol: chain.nativeSymbol, + decimals: chain.wrappedNative.decimals, + }) + _nativeTokenCache.set(chainId, token) + return token +} + +export function getWrappedNative(chainId: number): TokenInfo | undefined { + return getChain(chainId)?.wrappedNative +} diff --git a/packages/protocol-core/src/tokens/stablecoins.ts b/packages/protocol-core/src/tokens/stablecoins.ts new file mode 100644 index 0000000..9111c5e --- /dev/null +++ b/packages/protocol-core/src/tokens/stablecoins.ts @@ -0,0 +1,25 @@ +import type { TokenInfo } from '../chains/types' +import { getChain } from '../chains/registry' + +export function getStablecoins(chainId: number): ReadonlyArray { + return getChain(chainId)?.stablecoins ?? [] +} + +export function getStablecoinAddresses(chainId: number): string[] { + return getStablecoins(chainId).map((t) => t.address.toLowerCase()) +} + +/** Lazy cache: chainId → Set of lowercase stablecoin addresses */ +const _stablecoinSetCache = new Map>() + +function getStablecoinSet(chainId: number): Set { + const cached = _stablecoinSetCache.get(chainId) + if (cached !== undefined) return cached + const set = new Set(getStablecoinAddresses(chainId)) + _stablecoinSetCache.set(chainId, set) + return set +} + +export function isStablecoin(chainId: number, address: string): boolean { + return getStablecoinSet(chainId).has(address.toLowerCase()) +} diff --git a/packages/protocol-core/src/tokens/types.ts b/packages/protocol-core/src/tokens/types.ts new file mode 100644 index 0000000..60033df --- /dev/null +++ b/packages/protocol-core/src/tokens/types.ts @@ -0,0 +1 @@ +export type { TokenInfo } from '../chains/types' diff --git a/packages/protocol-core/src/utils/constants.ts b/packages/protocol-core/src/utils/constants.ts new file mode 100644 index 0000000..1281613 --- /dev/null +++ b/packages/protocol-core/src/utils/constants.ts @@ -0,0 +1,8 @@ +/** Maximum items per subgraph page query. Used by leaderboard-server and analytics. */ +export const SUBGRAPH_PAGE_SIZE = 1000 + +/** Minimum USD price threshold. Tokens below this are excluded from analytics. */ +export const MIN_PRICE_USD = 0.000001 + +/** Minimum USD volume threshold. Pairs below this are excluded from analytics. */ +export const MIN_VOLUME_USD = 0.0001 diff --git a/packages/protocol-core/src/utils/math.ts b/packages/protocol-core/src/utils/math.ts new file mode 100644 index 0000000..b408662 --- /dev/null +++ b/packages/protocol-core/src/utils/math.ts @@ -0,0 +1,4 @@ +export function percentChange(current: number, previous: number): number { + if (previous === 0) return 0 + return ((current - previous) / previous) * 100 +} diff --git a/packages/protocol-core/src/utils/time.ts b/packages/protocol-core/src/utils/time.ts new file mode 100644 index 0000000..aad31f3 --- /dev/null +++ b/packages/protocol-core/src/utils/time.ts @@ -0,0 +1,27 @@ +export const ONE_DAY = 86400 + +export function getDayTimestamps(startTime: number, endTime: number): number[] { + const firstDay = Math.floor(startTime / ONE_DAY) * ONE_DAY + const lastDay = Math.floor(endTime / ONE_DAY) * ONE_DAY + const timestamps: number[] = [] + for (let t = firstDay; t <= lastDay; t += ONE_DAY) { + timestamps.push(t) + } + return timestamps +} + +export function getDayIds(startTime: number, endTime: number): number[] { + const firstId = Math.floor(startTime / ONE_DAY) + const lastId = Math.floor(endTime / ONE_DAY) + const ids: number[] = [] + for (let id = firstId; id <= lastId; id++) { + ids.push(id) + } + return ids +} + +export function getLast7DayTimestamps(): number[] { + const now = Math.floor(Date.now() / 1000) + const today = Math.floor(now / ONE_DAY) * ONE_DAY + return Array.from({ length: 7 }, (_, i) => today - i * ONE_DAY) +} diff --git a/packages/protocol-core/tsconfig.build.json b/packages/protocol-core/tsconfig.build.json new file mode 100644 index 0000000..f9066ab --- /dev/null +++ b/packages/protocol-core/tsconfig.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "dist" + }, + "include": ["src/**/*"], + "exclude": ["src/**/*.test.ts"] +} diff --git a/packages/protocol-core/tsconfig.json b/packages/protocol-core/tsconfig.json new file mode 100644 index 0000000..564a599 --- /dev/null +++ b/packages/protocol-core/tsconfig.json @@ -0,0 +1,4 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src"] +} diff --git a/packages/protocol-core/tsup.config.ts b/packages/protocol-core/tsup.config.ts new file mode 100644 index 0000000..0ac0af3 --- /dev/null +++ b/packages/protocol-core/tsup.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'tsup' + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + dts: true, + clean: true, + target: 'es2020', + splitting: false, + sourcemap: true, + minify: false, + treeshake: true, +}) diff --git a/packages/protocol-core/vitest.config.ts b/packages/protocol-core/vitest.config.ts new file mode 100644 index 0000000..6c9fde2 --- /dev/null +++ b/packages/protocol-core/vitest.config.ts @@ -0,0 +1,20 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + coverage: { + provider: 'v8', + include: ['src/**/*.ts'], + exclude: ['src/**/*.test.ts', 'src/index.ts'], + thresholds: { + lines: 95, + functions: 95, + branches: 95, + statements: 95, + }, + }, + }, +}) diff --git a/packages/sdk/package.json b/packages/sdk/package.json new file mode 100644 index 0000000..1b7d9e1 --- /dev/null +++ b/packages/sdk/package.json @@ -0,0 +1,74 @@ +{ + "name": "@quickswap-defi/sdk", + "license": "MIT", + "version": "1.0.1", + "description": "šŸ›  An SDK for building applications on top of Quickswap.", + "main": "dist/index.js", + "typings": "dist/index.d.ts", + "files": [ + "dist" + ], + "repository": "https://github.com/QuickSwap/QuickSwap-sdk", + "keywords": [ + "quickswap", + "matic", + "ethereum" + ], + "module": "dist/index.mjs", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.mjs", + "require": "./dist/index.js" + } + }, + "scripts": { + "lint": "eslint src test", + "build": "tsup", + "start": "tsup --watch", + "test": "vitest run", + "prepublishOnly": "tsup" + }, + "dependencies": { + "@uniswap/v2-core": "^1.0.0", + "big.js": "^5.2.2", + "decimal.js-light": "^2.5.0", + "jsbi": "^3.1.1", + "tiny-invariant": "^1.1.0", + "tiny-warning": "^1.0.3", + "toformat": "^2.0.0" + }, + "peerDependencies": { + "@ethersproject/address": "^5.0.0-beta", + "@ethersproject/contracts": "^5.0.0-beta", + "@ethersproject/networks": "^5.0.0-beta", + "@ethersproject/providers": "^5.0.0-beta", + "@ethersproject/solidity": "^5.0.0-beta" + }, + "devDependencies": { + "@ethersproject/address": "^5.0.2", + "@ethersproject/contracts": "^5.0.2", + "@ethersproject/networks": "^5.0.2", + "@ethersproject/providers": "^5.0.5", + "@ethersproject/solidity": "^5.0.2", + "@types/big.js": "^4.0.5", + "@eslint/js": "^9.0.0", + "eslint": "^9.0.0", + "tsup": "^8.0.0", + "typescript": "^5.5.0", + "typescript-eslint": "^8.0.0", + "vitest": "^2.0.0" + }, + "engines": { + "node": ">=16" + }, + "publishConfig": { + "access": "public", + "provenance": "true" + }, + "prettier": { + "printWidth": 120, + "semi": false, + "singleQuote": true + } +} diff --git a/src/abis/ERC20.json b/packages/sdk/src/abis/ERC20.json similarity index 100% rename from src/abis/ERC20.json rename to packages/sdk/src/abis/ERC20.json diff --git a/src/constants.ts b/packages/sdk/src/constants.ts similarity index 100% rename from src/constants.ts rename to packages/sdk/src/constants.ts diff --git a/src/declarations.d.ts b/packages/sdk/src/declarations.d.ts similarity index 100% rename from src/declarations.d.ts rename to packages/sdk/src/declarations.d.ts diff --git a/src/entities/currency.ts b/packages/sdk/src/entities/currency.ts similarity index 100% rename from src/entities/currency.ts rename to packages/sdk/src/entities/currency.ts diff --git a/src/entities/fractions/currencyAmount.ts b/packages/sdk/src/entities/fractions/currencyAmount.ts similarity index 100% rename from src/entities/fractions/currencyAmount.ts rename to packages/sdk/src/entities/fractions/currencyAmount.ts diff --git a/src/entities/fractions/fraction.ts b/packages/sdk/src/entities/fractions/fraction.ts similarity index 100% rename from src/entities/fractions/fraction.ts rename to packages/sdk/src/entities/fractions/fraction.ts diff --git a/src/entities/fractions/index.ts b/packages/sdk/src/entities/fractions/index.ts similarity index 100% rename from src/entities/fractions/index.ts rename to packages/sdk/src/entities/fractions/index.ts diff --git a/src/entities/fractions/percent.ts b/packages/sdk/src/entities/fractions/percent.ts similarity index 100% rename from src/entities/fractions/percent.ts rename to packages/sdk/src/entities/fractions/percent.ts diff --git a/src/entities/fractions/price.ts b/packages/sdk/src/entities/fractions/price.ts similarity index 100% rename from src/entities/fractions/price.ts rename to packages/sdk/src/entities/fractions/price.ts diff --git a/src/entities/fractions/tokenAmount.ts b/packages/sdk/src/entities/fractions/tokenAmount.ts similarity index 100% rename from src/entities/fractions/tokenAmount.ts rename to packages/sdk/src/entities/fractions/tokenAmount.ts diff --git a/src/entities/index.ts b/packages/sdk/src/entities/index.ts similarity index 100% rename from src/entities/index.ts rename to packages/sdk/src/entities/index.ts diff --git a/src/entities/pair.ts b/packages/sdk/src/entities/pair.ts similarity index 100% rename from src/entities/pair.ts rename to packages/sdk/src/entities/pair.ts diff --git a/src/entities/route.ts b/packages/sdk/src/entities/route.ts similarity index 100% rename from src/entities/route.ts rename to packages/sdk/src/entities/route.ts diff --git a/src/entities/token.ts b/packages/sdk/src/entities/token.ts similarity index 100% rename from src/entities/token.ts rename to packages/sdk/src/entities/token.ts diff --git a/src/entities/trade.ts b/packages/sdk/src/entities/trade.ts similarity index 100% rename from src/entities/trade.ts rename to packages/sdk/src/entities/trade.ts diff --git a/src/errors.ts b/packages/sdk/src/errors.ts similarity index 100% rename from src/errors.ts rename to packages/sdk/src/errors.ts diff --git a/src/fetcher.ts b/packages/sdk/src/fetcher.ts similarity index 100% rename from src/fetcher.ts rename to packages/sdk/src/fetcher.ts diff --git a/src/index.ts b/packages/sdk/src/index.ts similarity index 100% rename from src/index.ts rename to packages/sdk/src/index.ts diff --git a/src/router.ts b/packages/sdk/src/router.ts similarity index 100% rename from src/router.ts rename to packages/sdk/src/router.ts diff --git a/src/utils.ts b/packages/sdk/src/utils.ts similarity index 100% rename from src/utils.ts rename to packages/sdk/src/utils.ts diff --git a/test/constants.test.ts b/packages/sdk/test/constants.test.ts similarity index 100% rename from test/constants.test.ts rename to packages/sdk/test/constants.test.ts diff --git a/test/data.test.ts b/packages/sdk/test/data.test.ts similarity index 100% rename from test/data.test.ts rename to packages/sdk/test/data.test.ts diff --git a/test/entities.test.ts b/packages/sdk/test/entities.test.ts similarity index 100% rename from test/entities.test.ts rename to packages/sdk/test/entities.test.ts diff --git a/test/fraction.test.ts b/packages/sdk/test/fraction.test.ts similarity index 100% rename from test/fraction.test.ts rename to packages/sdk/test/fraction.test.ts diff --git a/test/miscellaneous.test.ts b/packages/sdk/test/miscellaneous.test.ts similarity index 100% rename from test/miscellaneous.test.ts rename to packages/sdk/test/miscellaneous.test.ts diff --git a/test/pair.test.ts b/packages/sdk/test/pair.test.ts similarity index 100% rename from test/pair.test.ts rename to packages/sdk/test/pair.test.ts diff --git a/test/route.test.ts b/packages/sdk/test/route.test.ts similarity index 100% rename from test/route.test.ts rename to packages/sdk/test/route.test.ts diff --git a/test/router.test.ts b/packages/sdk/test/router.test.ts similarity index 100% rename from test/router.test.ts rename to packages/sdk/test/router.test.ts diff --git a/test/token.test.ts b/packages/sdk/test/token.test.ts similarity index 100% rename from test/token.test.ts rename to packages/sdk/test/token.test.ts diff --git a/test/trade.test.ts b/packages/sdk/test/trade.test.ts similarity index 100% rename from test/trade.test.ts rename to packages/sdk/test/trade.test.ts diff --git a/packages/sdk/tsconfig.json b/packages/sdk/tsconfig.json new file mode 100755 index 0000000..cb5ef20 --- /dev/null +++ b/packages/sdk/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.base.json", + "include": ["src", "test"], + "compilerOptions": { + "target": "es2018", + "moduleResolution": "node", + "rootDir": "./", + "sourceMap": true, + "importHelpers": true, + "isolatedModules": false + } +} diff --git a/tsup.config.ts b/packages/sdk/tsup.config.ts similarity index 100% rename from tsup.config.ts rename to packages/sdk/tsup.config.ts diff --git a/vitest.config.ts b/packages/sdk/vitest.config.ts similarity index 100% rename from vitest.config.ts rename to packages/sdk/vitest.config.ts diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 416cb92..1cc5597 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -10,7 +10,30 @@ overrides: importers: - .: + .: {} + + packages/protocol-core: + devDependencies: + '@noble/hashes': + specifier: ^1.7.0 + version: 1.8.0 + '@vitest/coverage-v8': + specifier: ^2.0.0 + version: 2.1.9(vitest@2.1.9(@types/node@24.10.0)(jsdom@11.12.0)) + tsup: + specifier: ^8.0.0 + version: 8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) + tsx: + specifier: ^4.0.0 + version: 4.21.0 + typescript: + specifier: ^5.5.0 + version: 5.9.3 + vitest: + specifier: ^2.0.0 + version: 2.1.9(@types/node@24.10.0)(jsdom@11.12.0) + + packages/sdk: dependencies: '@uniswap/v2-core': specifier: ^1.0.0 @@ -60,7 +83,7 @@ importers: version: 9.39.3 tsup: specifier: ^8.0.0 - version: 8.5.1(postcss@8.5.6)(typescript@5.9.3) + version: 8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3) typescript: specifier: ^5.5.0 version: 5.9.3 @@ -73,6 +96,30 @@ importers: packages: + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} + + '@babel/helper-string-parser@7.27.1': + resolution: {integrity: sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.28.5': + resolution: {integrity: sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.2': + resolution: {integrity: sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/types@7.29.0': + resolution: {integrity: sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==} + engines: {node: '>=6.9.0'} + + '@bcoe/v8-coverage@0.2.3': + resolution: {integrity: sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -493,6 +540,14 @@ packages: resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} engines: {node: '>=18.18'} + '@isaacs/cliui@8.0.2': + resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} + engines: {node: '>=12'} + + '@istanbuljs/schema@0.1.3': + resolution: {integrity: sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==} + engines: {node: '>=8'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -506,6 +561,14 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@noble/hashes@1.8.0': + resolution: {integrity: sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A==} + engines: {node: ^14.21.3 || >=16} + + '@pkgjs/parseargs@0.11.0': + resolution: {integrity: sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==} + engines: {node: '>=14'} + '@rollup/rollup-android-arm-eabi@4.59.0': resolution: {integrity: sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==} cpu: [arm] @@ -719,6 +782,15 @@ packages: resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} engines: {node: '>=10'} + '@vitest/coverage-v8@2.1.9': + resolution: {integrity: sha512-Z2cOr0ksM00MpEfyVE8KXIYPEcBFxdbLSs56L8PO0QQMxt/6bDj45uQfxoc96v05KW3clk7vvgP0qfDit9DmfQ==} + peerDependencies: + '@vitest/browser': 2.1.9 + vitest: 2.1.9 + peerDependenciesMeta: + '@vitest/browser': + optional: true + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -779,16 +851,25 @@ packages: engines: {node: '>=0.4.0'} hasBin: true - ajv@6.12.6: - resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} - ajv@6.14.0: resolution: {integrity: sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==} + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + ansi-styles@4.3.0: resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} engines: {node: '>=8'} + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + any-promise@1.3.0: resolution: {integrity: sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==} @@ -846,6 +927,9 @@ packages: brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} + brace-expansion@2.0.3: + resolution: {integrity: sha512-MCV/fYJEbqx68aE58kv2cA/kiky1G8vux3OR6/jbS+jIMe/6fJWa0DTzJU7dqijOWYwHi1t29FlfYI9uytqlpA==} + brace-expansion@5.0.4: resolution: {integrity: sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==} engines: {node: 18 || 20 || >=22} @@ -969,12 +1053,21 @@ packages: resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} engines: {node: '>= 0.4'} + eastasianwidth@0.2.0: + resolution: {integrity: sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==} + ecc-jsbn@0.1.2: resolution: {integrity: sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==} elliptic@6.6.1: resolution: {integrity: sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==} + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + emoji-regex@9.2.2: + resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} + es-define-property@1.0.1: resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} engines: {node: '>= 0.4'} @@ -1118,6 +1211,10 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + forever-agent@0.6.1: resolution: {integrity: sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==} @@ -1141,6 +1238,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.13.7: + resolution: {integrity: sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==} + getpass@0.1.7: resolution: {integrity: sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==} @@ -1148,6 +1248,11 @@ packages: resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} engines: {node: '>=10.13.0'} + glob@10.5.0: + resolution: {integrity: sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + hasBin: true + globals@14.0.0: resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} engines: {node: '>=18'} @@ -1190,6 +1295,9 @@ packages: html-encoding-sniffer@1.0.2: resolution: {integrity: sha512-71lZziiDnsuabfdYiUeWdCVyKuqwWi23L8YeIgV9jSSZHCtb6wB1BKWooH7L3tn4/FuZJMVWyNaIDr4RGmaSYw==} + html-escaper@2.0.2: + resolution: {integrity: sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==} + http-signature@1.2.0: resolution: {integrity: sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==} engines: {node: '>=0.8', npm: '>=1.3.7'} @@ -1221,6 +1329,10 @@ packages: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + is-glob@4.0.3: resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} engines: {node: '>=0.10.0'} @@ -1234,6 +1346,25 @@ packages: isstream@0.1.2: resolution: {integrity: sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==} + istanbul-lib-coverage@3.2.2: + resolution: {integrity: sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==} + engines: {node: '>=8'} + + istanbul-lib-report@3.0.1: + resolution: {integrity: sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==} + engines: {node: '>=10'} + + istanbul-lib-source-maps@5.0.6: + resolution: {integrity: sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==} + engines: {node: '>=10'} + + istanbul-reports@3.2.0: + resolution: {integrity: sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==} + engines: {node: '>=8'} + + jackspeak@3.4.3: + resolution: {integrity: sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==} + joycon@3.1.1: resolution: {integrity: sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==} engines: {node: '>=10'} @@ -1315,9 +1446,19 @@ packages: loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + magicast@0.3.5: + resolution: {integrity: sha512-L0WhttDl+2BOsybvEOLK7fW3UA0OQ0IQ2d6Zl2x/a6vVRs3bAY0ECOSHHeL5jD+SbOpOCUEi0y1DgHEn9Qn1AQ==} + + make-dir@4.0.0: + resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} + engines: {node: '>=10'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -1343,6 +1484,14 @@ packages: minimatch@3.1.5: resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + mlly@1.8.0: resolution: {integrity: sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g==} @@ -1386,6 +1535,9 @@ packages: resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} engines: {node: '>=10'} + package-json-from-dist@1.0.1: + resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -1401,6 +1553,10 @@ packages: resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} engines: {node: '>=8'} + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} @@ -1502,6 +1658,9 @@ packages: resolution: {integrity: sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==} engines: {node: '>=8'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + rollup@4.59.0: resolution: {integrity: sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1532,6 +1691,10 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1559,6 +1722,22 @@ packages: resolution: {integrity: sha512-ZnWpYnYugiOVEY5GkcuJK1io5V8QmNYChG62gSit9pQVGErXtrKuPC55ITaVSukmMta5qpMU7vqLt2Lnni4f/g==} engines: {node: '>=0.10.0'} + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string-width@5.1.2: + resolution: {integrity: sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==} + engines: {node: '>=12'} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + strip-json-comments@3.1.1: resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} engines: {node: '>=8'} @@ -1575,6 +1754,10 @@ packages: symbol-tree@3.2.4: resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + test-exclude@7.0.2: + resolution: {integrity: sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw==} + engines: {node: '>=18'} + thenify-all@1.6.0: resolution: {integrity: sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==} engines: {node: '>=0.8'} @@ -1652,6 +1835,11 @@ packages: typescript: optional: true + tsx@4.21.0: + resolution: {integrity: sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==} + engines: {node: '>=18.0.0'} + hasBin: true + tunnel-agent@0.6.0: resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} @@ -1791,6 +1979,14 @@ packages: resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} engines: {node: '>=0.10.0'} + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrap-ansi@8.1.0: + resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==} + engines: {node: '>=12'} + ws@5.2.4: resolution: {integrity: sha512-fFCejsuC8f9kOSu9FYaOw8CdO68O3h5v0lg4p74o8JqWpwTf9tniOD+nOB78aWoVSS6WptVUmDrp/KPsMVBWFQ==} peerDependencies: @@ -1823,6 +2019,26 @@ packages: snapshots: + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/helper-string-parser@7.27.1': {} + + '@babel/helper-validator-identifier@7.28.5': {} + + '@babel/parser@7.29.2': + dependencies: + '@babel/types': 7.29.0 + + '@babel/types@7.29.0': + dependencies: + '@babel/helper-string-parser': 7.27.1 + '@babel/helper-validator-identifier': 7.28.5 + + '@bcoe/v8-coverage@0.2.3': {} + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -2214,6 +2430,17 @@ snapshots: '@humanwhocodes/retry@0.4.3': {} + '@isaacs/cliui@8.0.2': + dependencies: + string-width: 5.1.2 + string-width-cjs: string-width@4.2.3 + strip-ansi: 7.2.0 + strip-ansi-cjs: strip-ansi@6.0.1 + wrap-ansi: 8.1.0 + wrap-ansi-cjs: wrap-ansi@7.0.0 + + '@istanbuljs/schema@0.1.3': {} + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -2228,6 +2455,11 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@noble/hashes@1.8.0': {} + + '@pkgjs/parseargs@0.11.0': + optional: true + '@rollup/rollup-android-arm-eabi@4.59.0': optional: true @@ -2407,6 +2639,24 @@ snapshots: '@uniswap/v2-core@1.0.1': {} + '@vitest/coverage-v8@2.1.9(vitest@2.1.9(@types/node@24.10.0)(jsdom@11.12.0))': + dependencies: + '@ampproject/remapping': 2.3.0 + '@bcoe/v8-coverage': 0.2.3 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + istanbul-lib-report: 3.0.1 + istanbul-lib-source-maps: 5.0.6 + istanbul-reports: 3.2.0 + magic-string: 0.30.21 + magicast: 0.3.5 + std-env: 3.10.0 + test-exclude: 7.0.2 + tinyrainbow: 1.2.0 + vitest: 2.1.9(@types/node@24.10.0)(jsdom@11.12.0) + transitivePeerDependencies: + - supports-color + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -2471,13 +2721,6 @@ snapshots: acorn@8.15.0: {} - ajv@6.12.6: - dependencies: - fast-deep-equal: 3.1.3 - fast-json-stable-stringify: 2.1.0 - json-schema-traverse: 0.4.1 - uri-js: 4.4.1 - ajv@6.14.0: dependencies: fast-deep-equal: 3.1.3 @@ -2485,10 +2728,16 @@ snapshots: json-schema-traverse: 0.4.1 uri-js: 4.4.1 + ansi-regex@5.0.1: {} + + ansi-regex@6.2.2: {} + ansi-styles@4.3.0: dependencies: color-convert: 2.0.1 + ansi-styles@6.2.3: {} + any-promise@1.3.0: {} argparse@2.0.1: {} @@ -2540,6 +2789,10 @@ snapshots: balanced-match: 1.0.2 concat-map: 0.0.1 + brace-expansion@2.0.3: + dependencies: + balanced-match: 1.0.2 + brace-expansion@5.0.4: dependencies: balanced-match: 4.0.4 @@ -2659,6 +2912,8 @@ snapshots: gopd: 1.2.0 optional: true + eastasianwidth@0.2.0: {} + ecc-jsbn@0.1.2: dependencies: jsbn: 0.1.1 @@ -2675,6 +2930,10 @@ snapshots: minimalistic-assert: 1.0.1 minimalistic-crypto-utils: 1.0.1 + emoji-regex@8.0.0: {} + + emoji-regex@9.2.2: {} + es-define-property@1.0.1: optional: true @@ -2788,7 +3047,7 @@ snapshots: '@humanwhocodes/module-importer': 1.0.1 '@humanwhocodes/retry': 0.4.3 '@types/estree': 1.0.8 - ajv: 6.12.6 + ajv: 6.14.0 chalk: 4.1.2 cross-spawn: 7.0.6 debug: 4.4.3 @@ -2881,6 +3140,11 @@ snapshots: flatted@3.3.3: {} + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + forever-agent@0.6.1: optional: true @@ -2920,6 +3184,10 @@ snapshots: es-object-atoms: 1.1.1 optional: true + get-tsconfig@4.13.7: + dependencies: + resolve-pkg-maps: 1.0.0 + getpass@0.1.7: dependencies: assert-plus: 1.0.0 @@ -2929,6 +3197,15 @@ snapshots: dependencies: is-glob: 4.0.3 + glob@10.5.0: + dependencies: + foreground-child: 3.3.1 + jackspeak: 3.4.3 + minimatch: 9.0.9 + minipass: 7.1.3 + package-json-from-dist: 1.0.1 + path-scurry: 1.11.1 + globals@14.0.0: {} gopd@1.2.0: @@ -2974,6 +3251,8 @@ snapshots: whatwg-encoding: 1.0.5 optional: true + html-escaper@2.0.2: {} + http-signature@1.2.0: dependencies: assert-plus: 1.0.0 @@ -3001,6 +3280,8 @@ snapshots: is-extglob@2.1.1: {} + is-fullwidth-code-point@3.0.0: {} + is-glob@4.0.3: dependencies: is-extglob: 2.1.1 @@ -3013,6 +3294,33 @@ snapshots: isstream@0.1.2: optional: true + istanbul-lib-coverage@3.2.2: {} + + istanbul-lib-report@3.0.1: + dependencies: + istanbul-lib-coverage: 3.2.2 + make-dir: 4.0.0 + supports-color: 7.2.0 + + istanbul-lib-source-maps@5.0.6: + dependencies: + '@jridgewell/trace-mapping': 0.3.31 + debug: 4.4.3 + istanbul-lib-coverage: 3.2.2 + transitivePeerDependencies: + - supports-color + + istanbul-reports@3.2.0: + dependencies: + html-escaper: 2.0.2 + istanbul-lib-report: 3.0.1 + + jackspeak@3.4.3: + dependencies: + '@isaacs/cliui': 8.0.2 + optionalDependencies: + '@pkgjs/parseargs': 0.11.0 + joycon@3.1.1: {} js-sha3@0.8.0: {} @@ -3117,10 +3425,22 @@ snapshots: loupe@3.2.1: {} + lru-cache@10.4.3: {} + magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + magicast@0.3.5: + dependencies: + '@babel/parser': 7.29.2 + '@babel/types': 7.29.0 + source-map-js: 1.2.1 + + make-dir@4.0.0: + dependencies: + semver: 7.7.3 + math-intrinsics@1.1.0: optional: true @@ -3144,6 +3464,12 @@ snapshots: dependencies: brace-expansion: 1.1.12 + minimatch@9.0.9: + dependencies: + brace-expansion: 2.0.3 + + minipass@7.1.3: {} + mlly@1.8.0: dependencies: acorn: 8.15.0 @@ -3198,6 +3524,8 @@ snapshots: dependencies: p-limit: 3.1.0 + package-json-from-dist@1.0.1: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 @@ -3209,6 +3537,11 @@ snapshots: path-key@3.1.1: {} + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + pathe@1.1.2: {} pathe@2.0.3: {} @@ -3233,11 +3566,12 @@ snapshots: pn@1.1.0: optional: true - postcss-load-config@6.0.1(postcss@8.5.6): + postcss-load-config@6.0.1(postcss@8.5.6)(tsx@4.21.0): dependencies: lilconfig: 3.1.3 optionalDependencies: postcss: 8.5.6 + tsx: 4.21.0 postcss@8.5.6: dependencies: @@ -3304,6 +3638,8 @@ snapshots: resolve-from@5.0.0: {} + resolve-pkg-maps@1.0.0: {} + rollup@4.59.0: dependencies: '@types/estree': 1.0.8 @@ -3354,6 +3690,8 @@ snapshots: siginfo@2.0.0: {} + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} source-map@0.6.1: @@ -3381,6 +3719,26 @@ snapshots: stealthy-require@1.1.1: optional: true + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string-width@5.1.2: + dependencies: + eastasianwidth: 0.2.0 + emoji-regex: 9.2.2 + strip-ansi: 7.2.0 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + strip-json-comments@3.1.1: {} sucrase@3.35.1: @@ -3400,6 +3758,12 @@ snapshots: symbol-tree@3.2.4: optional: true + test-exclude@7.0.2: + dependencies: + '@istanbuljs/schema': 0.1.3 + glob: 10.5.0 + minimatch: 10.2.4 + thenify-all@1.6.0: dependencies: thenify: 3.3.1 @@ -3448,7 +3812,7 @@ snapshots: ts-interface-checker@0.1.13: {} - tsup@8.5.1(postcss@8.5.6)(typescript@5.9.3): + tsup@8.5.1(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3): dependencies: bundle-require: 5.1.0(esbuild@0.27.3) cac: 6.7.14 @@ -3459,7 +3823,7 @@ snapshots: fix-dts-default-cjs-exports: 1.0.1 joycon: 3.1.1 picocolors: 1.1.1 - postcss-load-config: 6.0.1(postcss@8.5.6) + postcss-load-config: 6.0.1(postcss@8.5.6)(tsx@4.21.0) resolve-from: 5.0.0 rollup: 4.59.0 source-map: 0.7.6 @@ -3476,6 +3840,13 @@ snapshots: - tsx - yaml + tsx@4.21.0: + dependencies: + esbuild: 0.27.3 + get-tsconfig: 4.13.7 + optionalDependencies: + fsevents: 2.3.3 + tunnel-agent@0.6.0: dependencies: safe-buffer: 5.2.1 @@ -3629,6 +4000,18 @@ snapshots: word-wrap@1.2.5: {} + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrap-ansi@8.1.0: + dependencies: + ansi-styles: 6.2.3 + string-width: 5.1.2 + strip-ansi: 7.2.0 + ws@5.2.4: dependencies: async-limiter: 1.0.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 0000000..18ec407 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +packages: + - 'packages/*' diff --git a/tsconfig.json b/tsconfig.base.json old mode 100755 new mode 100644 similarity index 68% rename from tsconfig.json rename to tsconfig.base.json index dbd86fd..c4b6b78 --- a/tsconfig.json +++ b/tsconfig.base.json @@ -1,12 +1,8 @@ { - "include": ["src", "test"], "compilerOptions": { - "target": "es2018", - "module": "esnext", - "importHelpers": true, - "declaration": true, - "sourceMap": true, - "rootDir": "./", + "target": "ES2020", + "module": "ESNext", + "moduleResolution": "bundler", "strict": true, "noImplicitAny": true, "strictNullChecks": true, @@ -18,9 +14,10 @@ "noUnusedParameters": true, "noImplicitReturns": true, "noFallthroughCasesInSwitch": true, - "moduleResolution": "node", "esModuleInterop": true, "resolveJsonModule": true, - "skipLibCheck": true + "skipLibCheck": true, + "declaration": true, + "isolatedModules": true } } From cd60b10f55f33797790ff4b45a769174903a21a4 Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Thu, 16 Apr 2026 16:54:55 -0300 Subject: [PATCH 2/4] =?UTF-8?q?refactor(protocol-core):=20consolidate=20Ph?= =?UTF-8?q?ase=201=20=E2=80=94=20remove=20analytics=20leak,=20fix=20data,?= =?UTF-8?q?=20improve=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Remove utils/ module (constants, math, time) that leaked analytics concerns into the protocol knowledge layer. Fix data inconsistencies: WWDOGE→WDOGE typo, WMATIC→WPOL alignment with SDK migration, Dogechain DAI address isolation. Rename hasDynamicFee→exposeDynamicFee for clarity. Add CHAIN_ID const map, getChainOrThrow, and stablecoin cache guard for invalid chainIds. Improve barrel exports and type safety. 154 tests, 100% coverage, tsc --noEmit clean. --- .../src/__tests__/chains/chain-data.test.ts | 8 +-- .../src/__tests__/chains/registry.test.ts | 41 +++++++++++- .../src/__tests__/chains/types.test.ts | 2 +- .../protocol-core/src/__tests__/index.test.ts | 65 +++++++++++-------- .../src/__tests__/protocol/fees.test.ts | 8 +-- .../src/__tests__/protocol/versions.test.ts | 4 +- .../src/__tests__/tokens/native.test.ts | 4 +- .../src/__tests__/utils/constants.test.ts | 16 ----- .../src/__tests__/utils/math.test.ts | 30 --------- .../src/__tests__/utils/time.test.ts | 65 ------------------- packages/protocol-core/src/chains/base.ts | 4 +- .../protocol-core/src/chains/dogechain.ts | 6 +- packages/protocol-core/src/chains/ethereum.ts | 1 + packages/protocol-core/src/chains/imx.ts | 2 +- packages/protocol-core/src/chains/manta.ts | 2 +- packages/protocol-core/src/chains/mantra.ts | 4 +- packages/protocol-core/src/chains/polygon.ts | 6 +- packages/protocol-core/src/chains/registry.ts | 22 +++++++ packages/protocol-core/src/chains/somnia.ts | 2 +- packages/protocol-core/src/chains/soneium.ts | 2 +- packages/protocol-core/src/chains/types.ts | 10 ++- packages/protocol-core/src/chains/xlayer.ts | 2 +- packages/protocol-core/src/chains/zkevm.ts | 4 +- packages/protocol-core/src/index.ts | 22 +++++-- packages/protocol-core/src/protocol/fees.ts | 2 +- .../protocol-core/src/tokens/stablecoins.ts | 5 +- packages/protocol-core/src/tokens/types.ts | 1 - packages/protocol-core/src/utils/constants.ts | 8 --- packages/protocol-core/src/utils/math.ts | 4 -- packages/protocol-core/src/utils/time.ts | 27 -------- packages/sdk/package.json | 2 +- 31 files changed, 158 insertions(+), 223 deletions(-) delete mode 100644 packages/protocol-core/src/__tests__/utils/constants.test.ts delete mode 100644 packages/protocol-core/src/__tests__/utils/math.test.ts delete mode 100644 packages/protocol-core/src/__tests__/utils/time.test.ts delete mode 100644 packages/protocol-core/src/tokens/types.ts delete mode 100644 packages/protocol-core/src/utils/constants.ts delete mode 100644 packages/protocol-core/src/utils/math.ts delete mode 100644 packages/protocol-core/src/utils/time.ts diff --git a/packages/protocol-core/src/__tests__/chains/chain-data.test.ts b/packages/protocol-core/src/__tests__/chains/chain-data.test.ts index 9cddc45..517c9a3 100644 --- a/packages/protocol-core/src/__tests__/chains/chain-data.test.ts +++ b/packages/protocol-core/src/__tests__/chains/chain-data.test.ts @@ -128,16 +128,16 @@ describe('chain data integrity', () => { expect(ZKEVM.protocols[1].version).toBe('univ3') }) - it('MANTRA has v4 with dynamic fee', () => { + it('MANTRA has v4 with exposeDynamicFee', () => { expect(MANTRA.protocols).toHaveLength(1) expect(MANTRA.protocols[0].version).toBe('v4') - expect(MANTRA.protocols[0].hasDynamicFee).toBe(true) + expect(MANTRA.protocols[0].exposeDynamicFee).toBe(true) }) - it('Manta has univ3 without dynamic fee', () => { + it('Manta has univ3 without exposeDynamicFee', () => { expect(MANTA.protocols).toHaveLength(1) expect(MANTA.protocols[0].version).toBe('univ3') - expect(MANTA.protocols[0].hasDynamicFee).toBe(false) + expect(MANTA.protocols[0].exposeDynamicFee).toBe(false) }) }) diff --git a/packages/protocol-core/src/__tests__/chains/registry.test.ts b/packages/protocol-core/src/__tests__/chains/registry.test.ts index d273ede..2f02705 100644 --- a/packages/protocol-core/src/__tests__/chains/registry.test.ts +++ b/packages/protocol-core/src/__tests__/chains/registry.test.ts @@ -1,5 +1,6 @@ import { describe, it, expect } from 'vitest' -import { CHAIN_REGISTRY, getChain, getSupportedChainIds } from '../../chains/registry' +import { CHAIN_REGISTRY, CHAIN_ID, getChain, getChainOrThrow, getSupportedChainIds } from '../../chains/registry' +import type { TokenInfo } from '../../chains/types' const EXPECTED_CHAIN_IDS = [137, 8453, 5888, 169, 1868, 5031, 13371, 196, 1101, 2000, 1] @@ -39,6 +40,43 @@ describe('chain registry', () => { }) }) + describe('getChainOrThrow', () => { + it('returns config for known chain', () => { + const chain = getChainOrThrow(137) + expect(chain.name).toBe('Polygon PoS') + }) + + it('throws for unknown chain', () => { + expect(() => getChainOrThrow(99999)).toThrow('Unsupported chain: 99999') + }) + }) + + describe('CHAIN_ID', () => { + it('maps names to correct chain IDs', () => { + expect(CHAIN_ID.POLYGON).toBe(137) + expect(CHAIN_ID.BASE).toBe(8453) + expect(CHAIN_ID.MANTRA).toBe(5888) + expect(CHAIN_ID.MANTA).toBe(169) + expect(CHAIN_ID.SONEIUM).toBe(1868) + expect(CHAIN_ID.SOMNIA).toBe(5031) + expect(CHAIN_ID.IMX).toBe(13371) + expect(CHAIN_ID.XLAYER).toBe(196) + expect(CHAIN_ID.ZKEVM).toBe(1101) + expect(CHAIN_ID.DOGECHAIN).toBe(2000) + expect(CHAIN_ID.ETHEREUM).toBe(1) + }) + + it('has same keys as registry', () => { + expect(Object.keys(CHAIN_ID)).toHaveLength(getSupportedChainIds().length) + }) + + it('every CHAIN_ID value exists in registry', () => { + for (const id of Object.values(CHAIN_ID)) { + expect(getChain(id)).toBeDefined() + } + }) + }) + describe('immutability', () => { it('CHAIN_REGISTRY is frozen', () => { expect(Object.isFrozen(CHAIN_REGISTRY)).toBe(true) @@ -64,7 +102,6 @@ describe('chain registry', () => { const polygon = getChain(137)! const lengthBefore = polygon.stablecoins.length try { - // @ts-expect-error intentional mutation attempt on readonly ;(polygon.stablecoins as TokenInfo[]).push({ address: '0x0', symbol: 'FAKE', decimals: 6 }) } catch { // strict mode throws — acceptable diff --git a/packages/protocol-core/src/__tests__/chains/types.test.ts b/packages/protocol-core/src/__tests__/chains/types.test.ts index 7b20729..8166779 100644 --- a/packages/protocol-core/src/__tests__/chains/types.test.ts +++ b/packages/protocol-core/src/__tests__/chains/types.test.ts @@ -1,4 +1,4 @@ -import { describe, it, expectTypeOf } from 'vitest' +import { describe, it, expect, expectTypeOf } from 'vitest' import { PROTOCOL_VERSIONS, SCHEMA_VARIANTS, diff --git a/packages/protocol-core/src/__tests__/index.test.ts b/packages/protocol-core/src/__tests__/index.test.ts index 6953f1f..de49106 100644 --- a/packages/protocol-core/src/__tests__/index.test.ts +++ b/packages/protocol-core/src/__tests__/index.test.ts @@ -3,27 +3,32 @@ import { PROTOCOL_VERSIONS, SCHEMA_VARIANTS, CHAIN_REGISTRY, + CHAIN_ID, getChain, + getChainOrThrow, getSupportedChainIds, + POLYGON, + BASE, + MANTRA, + MANTA, + SONEIUM, + SOMNIA, + IMX, + XLAYER, + ZKEVM, + DOGECHAIN, + ETHEREUM, getSchemaVariant, getSupportedVersions, getProtocolVersionLabel, V2_FEE_BPS, V2_FEE_RATE, - computeV2Fees, + computeV2Fee, getStablecoins, getStablecoinAddresses, isStablecoin, getNativeToken, getWrappedNative, - percentChange, - getDayTimestamps, - getDayIds, - getLast7DayTimestamps, - ONE_DAY, - SUBGRAPH_PAGE_SIZE, - MIN_PRICE_USD, - MIN_VOLUME_USD, } from '../index' describe('barrel export (index.ts)', () => { @@ -34,8 +39,24 @@ describe('barrel export (index.ts)', () => { it('exports chain registry functions', () => { expect(typeof getChain).toBe('function') + expect(typeof getChainOrThrow).toBe('function') expect(typeof getSupportedChainIds).toBe('function') expect(CHAIN_REGISTRY).toBeDefined() + expect(CHAIN_ID).toBeDefined() + }) + + it('exports individual chain configs', () => { + expect(POLYGON.chainId).toBe(137) + expect(BASE.chainId).toBe(8453) + expect(MANTRA.chainId).toBe(5888) + expect(MANTA.chainId).toBe(169) + expect(SONEIUM.chainId).toBe(1868) + expect(SOMNIA.chainId).toBe(5031) + expect(IMX.chainId).toBe(13371) + expect(XLAYER.chainId).toBe(196) + expect(ZKEVM.chainId).toBe(1101) + expect(DOGECHAIN.chainId).toBe(2000) + expect(ETHEREUM.chainId).toBe(1) }) it('exports protocol functions', () => { @@ -47,7 +68,7 @@ describe('barrel export (index.ts)', () => { it('exports fee constants', () => { expect(V2_FEE_BPS).toBe(30) expect(V2_FEE_RATE).toBe(0.003) - expect(typeof computeV2Fees).toBe('function') + expect(typeof computeV2Fee).toBe('function') }) it('exports token functions', () => { @@ -58,28 +79,16 @@ describe('barrel export (index.ts)', () => { expect(typeof getWrappedNative).toBe('function') }) - it('exports utility functions and constants', () => { - expect(typeof percentChange).toBe('function') - expect(typeof getDayTimestamps).toBe('function') - expect(typeof getDayIds).toBe('function') - expect(typeof getLast7DayTimestamps).toBe('function') - expect(ONE_DAY).toBe(86400) - expect(SUBGRAPH_PAGE_SIZE).toBe(1000) - expect(MIN_PRICE_USD).toBe(0.000001) - expect(MIN_VOLUME_USD).toBe(0.0001) - }) - - it('has exactly 24 runtime exports (types excluded)', () => { - // 2 const objects + 3 registry + 3 protocol + 3 fee + 5 token + 8 util = 24 + it('has exactly 29 runtime exports (types excluded)', () => { + // 2 const objects + 5 registry + 11 chain configs + 3 protocol + 3 fee + 5 token = 29 const allExports = { PROTOCOL_VERSIONS, SCHEMA_VARIANTS, - CHAIN_REGISTRY, getChain, getSupportedChainIds, + CHAIN_REGISTRY, CHAIN_ID, getChain, getChainOrThrow, getSupportedChainIds, + POLYGON, BASE, MANTRA, MANTA, SONEIUM, SOMNIA, IMX, XLAYER, ZKEVM, DOGECHAIN, ETHEREUM, getSchemaVariant, getSupportedVersions, getProtocolVersionLabel, - V2_FEE_BPS, V2_FEE_RATE, computeV2Fees, + V2_FEE_BPS, V2_FEE_RATE, computeV2Fee, getStablecoins, getStablecoinAddresses, isStablecoin, getNativeToken, getWrappedNative, - percentChange, getDayTimestamps, getDayIds, getLast7DayTimestamps, ONE_DAY, - SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD, } - expect(Object.keys(allExports)).toHaveLength(24) + expect(Object.keys(allExports)).toHaveLength(29) }) }) diff --git a/packages/protocol-core/src/__tests__/protocol/fees.test.ts b/packages/protocol-core/src/__tests__/protocol/fees.test.ts index dde1010..16b3034 100644 --- a/packages/protocol-core/src/__tests__/protocol/fees.test.ts +++ b/packages/protocol-core/src/__tests__/protocol/fees.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { V2_FEE_BPS, V2_FEE_RATE, computeV2Fees } from '../../protocol/fees' +import { V2_FEE_BPS, V2_FEE_RATE, computeV2Fee } from '../../protocol/fees' describe('protocol/fees', () => { it('V2_FEE_BPS is 30', () => { @@ -10,13 +10,13 @@ describe('protocol/fees', () => { expect(V2_FEE_RATE).toBe(0.003) }) - describe('computeV2Fees', () => { + describe('computeV2Fee', () => { it('calculates correct fee', () => { - expect(computeV2Fees(1000)).toBeCloseTo(3, 10) + expect(computeV2Fee(1000)).toBeCloseTo(3, 10) }) it('returns 0 for 0 input', () => { - expect(computeV2Fees(0)).toBe(0) + expect(computeV2Fee(0)).toBe(0) }) }) }) diff --git a/packages/protocol-core/src/__tests__/protocol/versions.test.ts b/packages/protocol-core/src/__tests__/protocol/versions.test.ts index 16b785e..5e74c96 100644 --- a/packages/protocol-core/src/__tests__/protocol/versions.test.ts +++ b/packages/protocol-core/src/__tests__/protocol/versions.test.ts @@ -49,8 +49,8 @@ describe('protocol/versions', () => { expect(getProtocolVersionLabel(99999, true)).toBe('v3') }) - it('returns v2 for Ethereum (no protocols)', () => { - expect(getProtocolVersionLabel(1, true)).toBe('v3') // fallback — Ethereum has no CL protocol + it('returns v3 fallback for Ethereum CL (no protocols)', () => { + expect(getProtocolVersionLabel(1, true)).toBe('v3') }) }) }) diff --git a/packages/protocol-core/src/__tests__/tokens/native.test.ts b/packages/protocol-core/src/__tests__/tokens/native.test.ts index 2d05641..ce72393 100644 --- a/packages/protocol-core/src/__tests__/tokens/native.test.ts +++ b/packages/protocol-core/src/__tests__/tokens/native.test.ts @@ -28,10 +28,10 @@ describe('tokens/native', () => { }) describe('getWrappedNative', () => { - it('returns WMATIC for Polygon', () => { + it('returns WPOL for Polygon', () => { const result = getWrappedNative(137) expect(result).toBeDefined() - expect(result!.symbol).toBe('WMATIC') + expect(result!.symbol).toBe('WPOL') expect(result!.decimals).toBe(18) }) diff --git a/packages/protocol-core/src/__tests__/utils/constants.test.ts b/packages/protocol-core/src/__tests__/utils/constants.test.ts deleted file mode 100644 index 2ac765c..0000000 --- a/packages/protocol-core/src/__tests__/utils/constants.test.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD } from '../../utils/constants' - -describe('utils/constants', () => { - it('SUBGRAPH_PAGE_SIZE is 1000', () => { - expect(SUBGRAPH_PAGE_SIZE).toBe(1000) - }) - - it('MIN_PRICE_USD is 0.000001', () => { - expect(MIN_PRICE_USD).toBe(0.000001) - }) - - it('MIN_VOLUME_USD is 0.0001', () => { - expect(MIN_VOLUME_USD).toBe(0.0001) - }) -}) diff --git a/packages/protocol-core/src/__tests__/utils/math.test.ts b/packages/protocol-core/src/__tests__/utils/math.test.ts deleted file mode 100644 index 9cfe8e4..0000000 --- a/packages/protocol-core/src/__tests__/utils/math.test.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { percentChange } from '../../utils/math' - -describe('utils/math', () => { - describe('percentChange', () => { - it('calculates positive change', () => { - expect(percentChange(110, 100)).toBe(10) - }) - - it('returns 0 when previous is 0 (divide-by-zero guard)', () => { - expect(percentChange(100, 0)).toBe(0) - }) - - it('calculates negative change', () => { - expect(percentChange(5, 10)).toBe(-50) - }) - - it('returns 0 when values are equal', () => { - expect(percentChange(100, 100)).toBe(0) - }) - - it('handles negative values', () => { - expect(percentChange(-50, -100)).toBe(-50) - }) - - it('handles both zero', () => { - expect(percentChange(0, 0)).toBe(0) - }) - }) -}) diff --git a/packages/protocol-core/src/__tests__/utils/time.test.ts b/packages/protocol-core/src/__tests__/utils/time.test.ts deleted file mode 100644 index 2e0dc4b..0000000 --- a/packages/protocol-core/src/__tests__/utils/time.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { describe, it, expect } from 'vitest' -import { ONE_DAY, getDayTimestamps, getDayIds, getLast7DayTimestamps } from '../../utils/time' - -describe('utils/time', () => { - it('ONE_DAY is 86400', () => { - expect(ONE_DAY).toBe(86400) - }) - - describe('getDayTimestamps', () => { - it('returns timestamps for a 3-day range', () => { - const start = 1704067200 // 2024-01-01 00:00:00 UTC - const end = start + 2 * ONE_DAY // 2024-01-03 00:00:00 UTC - const result = getDayTimestamps(start, end) - expect(result).toHaveLength(3) - expect(result[0]).toBe(1704067200) - expect(result[1]).toBe(1704067200 + ONE_DAY) - expect(result[2]).toBe(1704067200 + 2 * ONE_DAY) - }) - - it('returns single element when start equals end', () => { - const ts = 1704067200 - expect(getDayTimestamps(ts, ts)).toHaveLength(1) - }) - - it('snaps to UTC midnight', () => { - const midday = 1704067200 + 43200 // noon - const result = getDayTimestamps(midday, midday) - expect(result[0]).toBe(1704067200) // snapped to midnight - }) - - it('returns empty array when start > end', () => { - expect(getDayTimestamps(1704067200 + ONE_DAY, 1704067200)).toHaveLength(0) - }) - }) - - describe('getDayIds', () => { - it('returns floor(timestamp / ONE_DAY) values', () => { - const start = 1704067200 - const result = getDayIds(start, start + ONE_DAY) - expect(result).toHaveLength(2) - expect(result[0]).toBe(Math.floor(start / ONE_DAY)) - expect(result[1]).toBe(Math.floor(start / ONE_DAY) + 1) - }) - }) - - describe('getLast7DayTimestamps', () => { - it('returns exactly 7 elements', () => { - expect(getLast7DayTimestamps()).toHaveLength(7) - }) - - it('returns descending order (today first)', () => { - const result = getLast7DayTimestamps() - for (let i = 0; i < result.length - 1; i++) { - expect(result[i] - result[i + 1]).toBe(ONE_DAY) - } - }) - - it('first element is today UTC midnight', () => { - const result = getLast7DayTimestamps() - const now = Math.floor(Date.now() / 1000) - const todayMidnight = Math.floor(now / ONE_DAY) * ONE_DAY - expect(result[0]).toBe(todayMidnight) - }) - }) -}) diff --git a/packages/protocol-core/src/chains/base.ts b/packages/protocol-core/src/chains/base.ts index 18315fb..05023fc 100644 --- a/packages/protocol-core/src/chains/base.ts +++ b/packages/protocol-core/src/chains/base.ts @@ -10,8 +10,8 @@ export const BASE: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'v4', hasDynamicFee: true }, - { version: 'v2', hasDynamicFee: false }, + { version: 'v4', exposeDynamicFee: true }, + { version: 'v2', exposeDynamicFee: false }, ], stablecoins: [ { address: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/dogechain.ts b/packages/protocol-core/src/chains/dogechain.ts index 2b4162b..b9abc40 100644 --- a/packages/protocol-core/src/chains/dogechain.ts +++ b/packages/protocol-core/src/chains/dogechain.ts @@ -9,12 +9,12 @@ export const DOGECHAIN: ChainConfig = { nativeSymbol: 'DOGE', wrappedNative: { address: '0xB7ddC6414bf4F5515b52D8BdD69973Ae205ff101', - symbol: 'WWDOGE', + symbol: 'WDOGE', decimals: 18, }, protocols: [ - { version: 'v3', hasDynamicFee: false }, - { version: 'v2', hasDynamicFee: false }, + { version: 'v3', exposeDynamicFee: false }, + { version: 'v2', exposeDynamicFee: false }, ], stablecoins: [ { address: '0x765277EebeCA2e31912C9946eAe1021199B39C61', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/ethereum.ts b/packages/protocol-core/src/chains/ethereum.ts index d187cdc..8dfefcc 100644 --- a/packages/protocol-core/src/chains/ethereum.ts +++ b/packages/protocol-core/src/chains/ethereum.ts @@ -9,6 +9,7 @@ export const ETHEREUM: ChainConfig = { symbol: 'WETH', decimals: 18, }, + // No QuickSwap deployment — reference chain for cross-chain price feeds and stablecoin addresses protocols: [], stablecoins: [ { address: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/imx.ts b/packages/protocol-core/src/chains/imx.ts index c7c047b..cffdd04 100644 --- a/packages/protocol-core/src/chains/imx.ts +++ b/packages/protocol-core/src/chains/imx.ts @@ -10,7 +10,7 @@ export const IMX: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'univ3', hasDynamicFee: false }, + { version: 'univ3', exposeDynamicFee: false }, ], stablecoins: [ { address: '0x6de8aCC0D406837030CE4dd28e7c08C5a96a30d2', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/manta.ts b/packages/protocol-core/src/chains/manta.ts index 663da4d..0b9eec0 100644 --- a/packages/protocol-core/src/chains/manta.ts +++ b/packages/protocol-core/src/chains/manta.ts @@ -10,7 +10,7 @@ export const MANTA: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'univ3', hasDynamicFee: false }, + { version: 'univ3', exposeDynamicFee: false }, ], stablecoins: [ { address: '0xb73603C5d87fA094B7314C74ACE2e64D165016fb', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/mantra.ts b/packages/protocol-core/src/chains/mantra.ts index 8acd100..7e607a7 100644 --- a/packages/protocol-core/src/chains/mantra.ts +++ b/packages/protocol-core/src/chains/mantra.ts @@ -3,14 +3,14 @@ import type { ChainConfig } from './types' export const MANTRA: ChainConfig = { chainId: 5888, name: 'MANTRA', - nativeSymbol: 'OM', + nativeSymbol: 'MANTRA', wrappedNative: { address: '0xE3047710EF6cB36Bcf1E58145529778eA7Cb5598', symbol: 'WMANTRA', decimals: 18, }, protocols: [ - { version: 'v4', hasDynamicFee: true }, + { version: 'v4', exposeDynamicFee: true }, ], stablecoins: [ { address: '0xd2b95283011E47257917770D28Bb3EE44c849f6F', symbol: 'mantraUSD', decimals: 18 }, diff --git a/packages/protocol-core/src/chains/polygon.ts b/packages/protocol-core/src/chains/polygon.ts index 5ae2979..41b6edf 100644 --- a/packages/protocol-core/src/chains/polygon.ts +++ b/packages/protocol-core/src/chains/polygon.ts @@ -6,12 +6,12 @@ export const POLYGON: ChainConfig = { nativeSymbol: 'POL', wrappedNative: { address: '0x0d500B1d8E8eF31E21C99d1Db9A6444d3ADf1270', - symbol: 'WMATIC', + symbol: 'WPOL', decimals: 18, }, protocols: [ - { version: 'v3', hasDynamicFee: false }, - { version: 'v2', hasDynamicFee: false }, + { version: 'v3', exposeDynamicFee: false }, + { version: 'v2', exposeDynamicFee: false }, ], stablecoins: [ { address: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174', symbol: 'USDC.e', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/registry.ts b/packages/protocol-core/src/chains/registry.ts index a8cf61d..161fd80 100644 --- a/packages/protocol-core/src/chains/registry.ts +++ b/packages/protocol-core/src/chains/registry.ts @@ -44,3 +44,25 @@ export function getChain(chainId: number): ChainConfig | undefined { export function getSupportedChainIds(): number[] { return Object.keys(CHAIN_REGISTRY).map(Number) } + +export function getChainOrThrow(chainId: number): ChainConfig { + const chain = CHAIN_REGISTRY[chainId] + if (!chain) { + throw new Error(`Unsupported chain: ${chainId}`) + } + return chain +} + +export const CHAIN_ID = { + POLYGON: POLYGON.chainId, + BASE: BASE.chainId, + MANTRA: MANTRA.chainId, + MANTA: MANTA.chainId, + SONEIUM: SONEIUM.chainId, + SOMNIA: SOMNIA.chainId, + IMX: IMX.chainId, + XLAYER: XLAYER.chainId, + ZKEVM: ZKEVM.chainId, + DOGECHAIN: DOGECHAIN.chainId, + ETHEREUM: ETHEREUM.chainId, +} as const diff --git a/packages/protocol-core/src/chains/somnia.ts b/packages/protocol-core/src/chains/somnia.ts index 88a92ad..4e9fb7c 100644 --- a/packages/protocol-core/src/chains/somnia.ts +++ b/packages/protocol-core/src/chains/somnia.ts @@ -10,7 +10,7 @@ export const SOMNIA: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'v4', hasDynamicFee: true }, + { version: 'v4', exposeDynamicFee: true }, ], stablecoins: [ { address: '0x28BEc7E30E6faee657a03e19Bf1128AaD7632A00', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/soneium.ts b/packages/protocol-core/src/chains/soneium.ts index 172f1d1..3225bdb 100644 --- a/packages/protocol-core/src/chains/soneium.ts +++ b/packages/protocol-core/src/chains/soneium.ts @@ -10,7 +10,7 @@ export const SONEIUM: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'v4', hasDynamicFee: true }, + { version: 'v4', exposeDynamicFee: true }, ], stablecoins: [ { address: '0xbA9986D2381edf1DA03B0B9c1f8b00dc4AacC369', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/types.ts b/packages/protocol-core/src/chains/types.ts index 677a6c4..a9ab6b7 100644 --- a/packages/protocol-core/src/chains/types.ts +++ b/packages/protocol-core/src/chains/types.ts @@ -22,7 +22,15 @@ export interface TokenInfo { export interface ChainProtocolEntry { readonly version: ProtocolVersion - readonly hasDynamicFee: boolean + /** + * Whether the subgraph schema for this protocol version exposes a `dynamicFee` + * field on Pool entities. True for V4 (Algebra Integral), false for V3/V2/UniV3. + * + * This is a subgraph schema capability flag, NOT a protocol-level indicator of + * whether the AMM uses adaptive fees (Algebra V3 pools DO use dynamic fees at + * the protocol level, but the V3 subgraph schema does not expose the field). + */ + readonly exposeDynamicFee: boolean } export interface ChainConfig { diff --git a/packages/protocol-core/src/chains/xlayer.ts b/packages/protocol-core/src/chains/xlayer.ts index 78cbccf..a53a480 100644 --- a/packages/protocol-core/src/chains/xlayer.ts +++ b/packages/protocol-core/src/chains/xlayer.ts @@ -10,7 +10,7 @@ export const XLAYER: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'v3', hasDynamicFee: false }, + { version: 'v3', exposeDynamicFee: false }, ], stablecoins: [ { address: '0x1E4a5963aBFD975d8c9021ce480b42188849D41d', symbol: 'USDT', decimals: 6 }, diff --git a/packages/protocol-core/src/chains/zkevm.ts b/packages/protocol-core/src/chains/zkevm.ts index 9cd2e31..0b06fe3 100644 --- a/packages/protocol-core/src/chains/zkevm.ts +++ b/packages/protocol-core/src/chains/zkevm.ts @@ -10,8 +10,8 @@ export const ZKEVM: ChainConfig = { decimals: 18, }, protocols: [ - { version: 'v3', hasDynamicFee: false }, - { version: 'univ3', hasDynamicFee: false }, + { version: 'v3', exposeDynamicFee: false }, + { version: 'univ3', exposeDynamicFee: false }, ], stablecoins: [ { address: '0xA8CE8aee21bC2A48a5EF670afCc9274C7bbbC035', symbol: 'USDC', decimals: 6 }, diff --git a/packages/protocol-core/src/index.ts b/packages/protocol-core/src/index.ts index bb4d83a..9ca4a97 100644 --- a/packages/protocol-core/src/index.ts +++ b/packages/protocol-core/src/index.ts @@ -3,20 +3,28 @@ export type { ProtocolVersion, SchemaVariant, TokenInfo, ChainProtocolEntry, Cha export { PROTOCOL_VERSIONS, SCHEMA_VARIANTS } from './chains/types' // Chain Registry -export { CHAIN_REGISTRY, getChain, getSupportedChainIds } from './chains/registry' +export { CHAIN_REGISTRY, CHAIN_ID, getChain, getChainOrThrow, getSupportedChainIds } from './chains/registry' + +// Individual Chain Configs +export { POLYGON } from './chains/polygon' +export { BASE } from './chains/base' +export { MANTRA } from './chains/mantra' +export { MANTA } from './chains/manta' +export { SONEIUM } from './chains/soneium' +export { SOMNIA } from './chains/somnia' +export { IMX } from './chains/imx' +export { XLAYER } from './chains/xlayer' +export { ZKEVM } from './chains/zkevm' +export { DOGECHAIN } from './chains/dogechain' +export { ETHEREUM } from './chains/ethereum' // Protocol Functions export { getSchemaVariant } from './protocol/schema' export { getSupportedVersions, getProtocolVersionLabel } from './protocol/versions' // Fee Constants & Functions -export { V2_FEE_BPS, V2_FEE_RATE, computeV2Fees } from './protocol/fees' +export { V2_FEE_BPS, V2_FEE_RATE, computeV2Fee } from './protocol/fees' // Token Functions export { getStablecoins, getStablecoinAddresses, isStablecoin } from './tokens/stablecoins' export { getNativeToken, getWrappedNative } from './tokens/native' - -// Utility Functions & Constants -export { percentChange } from './utils/math' -export { getDayTimestamps, getDayIds, getLast7DayTimestamps, ONE_DAY } from './utils/time' -export { SUBGRAPH_PAGE_SIZE, MIN_PRICE_USD, MIN_VOLUME_USD } from './utils/constants' diff --git a/packages/protocol-core/src/protocol/fees.ts b/packages/protocol-core/src/protocol/fees.ts index b59d4ed..eb53e05 100644 --- a/packages/protocol-core/src/protocol/fees.ts +++ b/packages/protocol-core/src/protocol/fees.ts @@ -16,6 +16,6 @@ export const V2_FEE_RATE = 0.003 * integer/BigInt arithmetic with the raw token amounts and apply the 0.30% fee * in full-precision basis-point math (amountIn * 30 / 10_000). */ -export function computeV2Fees(amountIn: number): number { +export function computeV2Fee(amountIn: number): number { return amountIn * V2_FEE_RATE } diff --git a/packages/protocol-core/src/tokens/stablecoins.ts b/packages/protocol-core/src/tokens/stablecoins.ts index 9111c5e..3fe9ccb 100644 --- a/packages/protocol-core/src/tokens/stablecoins.ts +++ b/packages/protocol-core/src/tokens/stablecoins.ts @@ -12,14 +12,15 @@ export function getStablecoinAddresses(chainId: number): string[] { /** Lazy cache: chainId → Set of lowercase stablecoin addresses */ const _stablecoinSetCache = new Map>() -function getStablecoinSet(chainId: number): Set { +function getStablecoinSet(chainId: number): Set | undefined { const cached = _stablecoinSetCache.get(chainId) if (cached !== undefined) return cached + if (getChain(chainId) === undefined) return undefined const set = new Set(getStablecoinAddresses(chainId)) _stablecoinSetCache.set(chainId, set) return set } export function isStablecoin(chainId: number, address: string): boolean { - return getStablecoinSet(chainId).has(address.toLowerCase()) + return getStablecoinSet(chainId)?.has(address.toLowerCase()) ?? false } diff --git a/packages/protocol-core/src/tokens/types.ts b/packages/protocol-core/src/tokens/types.ts deleted file mode 100644 index 60033df..0000000 --- a/packages/protocol-core/src/tokens/types.ts +++ /dev/null @@ -1 +0,0 @@ -export type { TokenInfo } from '../chains/types' diff --git a/packages/protocol-core/src/utils/constants.ts b/packages/protocol-core/src/utils/constants.ts deleted file mode 100644 index 1281613..0000000 --- a/packages/protocol-core/src/utils/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** Maximum items per subgraph page query. Used by leaderboard-server and analytics. */ -export const SUBGRAPH_PAGE_SIZE = 1000 - -/** Minimum USD price threshold. Tokens below this are excluded from analytics. */ -export const MIN_PRICE_USD = 0.000001 - -/** Minimum USD volume threshold. Pairs below this are excluded from analytics. */ -export const MIN_VOLUME_USD = 0.0001 diff --git a/packages/protocol-core/src/utils/math.ts b/packages/protocol-core/src/utils/math.ts deleted file mode 100644 index b408662..0000000 --- a/packages/protocol-core/src/utils/math.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function percentChange(current: number, previous: number): number { - if (previous === 0) return 0 - return ((current - previous) / previous) * 100 -} diff --git a/packages/protocol-core/src/utils/time.ts b/packages/protocol-core/src/utils/time.ts deleted file mode 100644 index aad31f3..0000000 --- a/packages/protocol-core/src/utils/time.ts +++ /dev/null @@ -1,27 +0,0 @@ -export const ONE_DAY = 86400 - -export function getDayTimestamps(startTime: number, endTime: number): number[] { - const firstDay = Math.floor(startTime / ONE_DAY) * ONE_DAY - const lastDay = Math.floor(endTime / ONE_DAY) * ONE_DAY - const timestamps: number[] = [] - for (let t = firstDay; t <= lastDay; t += ONE_DAY) { - timestamps.push(t) - } - return timestamps -} - -export function getDayIds(startTime: number, endTime: number): number[] { - const firstId = Math.floor(startTime / ONE_DAY) - const lastId = Math.floor(endTime / ONE_DAY) - const ids: number[] = [] - for (let id = firstId; id <= lastId; id++) { - ids.push(id) - } - return ids -} - -export function getLast7DayTimestamps(): number[] { - const now = Math.floor(Date.now() / 1000) - const today = Math.floor(now / ONE_DAY) * ONE_DAY - return Array.from({ length: 7 }, (_, i) => today - i * ONE_DAY) -} diff --git a/packages/sdk/package.json b/packages/sdk/package.json index 1b7d9e1..722dd57 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -64,7 +64,7 @@ }, "publishConfig": { "access": "public", - "provenance": "true" + "provenance": true }, "prettier": { "printWidth": 120, From 7b3224c3431fe8cadfc075368a8429d7c82ce8f7 Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Thu, 16 Apr 2026 19:00:12 -0300 Subject: [PATCH 3/4] ci: replace single-package publish with per-package workflows Split broken publish.yaml into publish-sdk.yaml and publish-protocol-core.yaml. Each workflow independently versions, builds, and publishes its package via OIDC trusted publishers. Namespaced tags: sdk/v*.*.* and protocol-core/v*.*.*. --- .github/workflows/publish-protocol-core.yaml | 318 ++++++++++++++++++ .../{publish.yaml => publish-sdk.yaml} | 76 ++--- 2 files changed, 356 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/publish-protocol-core.yaml rename .github/workflows/{publish.yaml => publish-sdk.yaml} (80%) diff --git a/.github/workflows/publish-protocol-core.yaml b/.github/workflows/publish-protocol-core.yaml new file mode 100644 index 0000000..da0572b --- /dev/null +++ b/.github/workflows/publish-protocol-core.yaml @@ -0,0 +1,318 @@ +name: Publish Protocol Core + +# SECURITY: Uses Trusted Publishers (OIDC) — no long-lived tokens +# Requires NPM trusted publisher config: https://docs.npmjs.com/trusted-publishers +# Package: @quickswap-defi/protocol-core + +on: + workflow_dispatch: + inputs: + bump: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + - prerelease + default: 'patch' + dry_run: + description: 'Dry run (test without publishing)' + required: false + type: boolean + default: false + + push: + tags: + - 'protocol-core/v*.*.*' + +permissions: + contents: write + id-token: write + pull-requests: read + +jobs: + # ============================================ + # JOB 1: Security Audit & Validation + # ============================================ + security-audit: + name: Security Audit + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js 20 LTS + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Verify package integrity + run: | + echo "Verifying packages/protocol-core/package.json integrity..." + node -e "JSON.parse(require('fs').readFileSync('packages/protocol-core/package.json'))" + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run security audit + run: pnpm audit --audit-level=moderate + continue-on-error: true + + - name: Run linter + run: pnpm --filter @quickswap-defi/protocol-core lint + + # ============================================ + # JOB 2: Build & Test + # ============================================ + build-and-test: + name: Build & Test + runs-on: ubuntu-latest + needs: security-audit + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js 20 LTS + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm --filter @quickswap-defi/protocol-core test + + - name: Validate addresses + run: pnpm --filter @quickswap-defi/protocol-core validate:addresses + + - name: Build package + run: pnpm --filter @quickswap-defi/protocol-core build + + - name: Validate build output + run: | + if [ ! -f "packages/protocol-core/dist/index.js" ] && [ ! -f "packages/protocol-core/dist/index.mjs" ]; then + echo "Build failed: packages/protocol-core/dist/ not found or empty" + exit 1 + fi + echo "Build validation passed" + + - name: Upload build artifact + uses: actions/upload-artifact@v4 + with: + name: protocol-core-build + path: packages/protocol-core/dist/ + retention-days: 7 + + # ============================================ + # JOB 3: Version Bump (if manual trigger) + # ============================================ + version-bump: + name: Version Bump + runs-on: ubuntu-latest + needs: build-and-test + if: github.event_name == 'workflow_dispatch' && !inputs.dry_run + timeout-minutes: 5 + + outputs: + new_version: ${{ steps.bump.outputs.new_version }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'pnpm' + + - name: Configure Git + run: | + git config user.name "quickswap-bot" + git config user.email "bot@quickswap.exchange" + + - name: Bump version + id: bump + run: | + cd packages/protocol-core + echo "Current version: $(node -p "require('./package.json').version")" + pnpm version ${{ inputs.bump }} --no-git-tag-version --no-commit-hooks + NEW_VERSION=$(node -p "require('./package.json').version") + echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT + echo "Version bumped to: $NEW_VERSION" + + - name: Commit and push version + run: | + git add packages/protocol-core/package.json + git diff --staged --quiet || git commit -m "chore(release): protocol-core v$(cd packages/protocol-core && node -p "require('./package.json').version") [skip ci]" + git tag "protocol-core/v$(cd packages/protocol-core && node -p "require('./package.json').version")" + git push origin HEAD --follow-tags + + # ============================================ + # JOB 4: Publish to NPM (Secure OIDC) + # ============================================ + publish-npm: + name: Publish to NPM + runs-on: ubuntu-latest + needs: [build-and-test, version-bump] + if: | + always() && + needs.build-and-test.result == 'success' && + ( + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/protocol-core/v')) || + (github.event_name == 'workflow_dispatch' && !inputs.dry_run && needs.version-bump.result == 'success') + ) + timeout-minutes: 10 + + environment: + name: npm-protocol-core + url: https://www.npmjs.com/package/@quickswap-defi/protocol-core + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Fetch latest (after version bump) + if: github.event_name == 'workflow_dispatch' + run: | + git fetch origin --tags --force + git reset --hard origin/${{ github.ref_name }} + + - name: Setup Node.js with NPM registry + uses: actions/setup-node@v4 + with: + node-version: '24' + registry-url: 'https://registry.npmjs.org/' + + - name: Update npm (required for OIDC) + run: | + npm install -g npm@latest + npm --version + + - name: Download build artifact + uses: actions/download-artifact@v4 + with: + name: protocol-core-build + path: packages/protocol-core/dist/ + + - name: Install pnpm + uses: pnpm/action-setup@v4 + with: + version: 9 + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Rebuild (ensure consistency) + run: pnpm --filter @quickswap-defi/protocol-core build + + - name: Pre-publish verification + run: | + cd packages/protocol-core + echo "Package details:" + npm pack --dry-run + echo "" + npm publish --dry-run + + - name: Check if version already exists + id: check_version + run: | + cd packages/protocol-core + PACKAGE_VERSION=$(node -p "require('./package.json').version") + echo "Checking if version $PACKAGE_VERSION already exists..." + if npm view "@quickswap-defi/protocol-core@$PACKAGE_VERSION" version > /dev/null 2>&1; then + echo "exists=true" >> $GITHUB_OUTPUT + echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + else + echo "exists=false" >> $GITHUB_OUTPUT + echo "version=$PACKAGE_VERSION" >> $GITHUB_OUTPUT + fi + + - name: Publish to NPM + if: steps.check_version.outputs.exists == 'false' + run: cd packages/protocol-core && npm publish --access public --provenance + + - name: Skip publish (version exists) + if: steps.check_version.outputs.exists == 'true' + run: | + echo "Skipping: version ${{ steps.check_version.outputs.version }} already exists" + + - name: Get published version + id: version + run: | + VERSION=$(cd packages/protocol-core && node -p "require('./package.json').version") + echo "version=$VERSION" >> $GITHUB_OUTPUT + + - name: Create GitHub Release + uses: softprops/action-gh-release@v2 + with: + tag_name: protocol-core/v${{ steps.version.outputs.version }} + name: "@quickswap-defi/protocol-core v${{ steps.version.outputs.version }}" + body: | + ## NPM Package Published + **Package:** `@quickswap-defi/protocol-core@${{ steps.version.outputs.version }}` + **NPM:** https://www.npmjs.com/package/@quickswap-defi/protocol-core + ### Installation + ```bash + npm install @quickswap-defi/protocol-core@${{ steps.version.outputs.version }} + ``` + draft: false + prerelease: ${{ contains(steps.version.outputs.version, '-') }} + generate_release_notes: true + + # ============================================ + # JOB 5: Post-Publish Verification + # ============================================ + verify-publish: + name: Verify Publication + runs-on: ubuntu-latest + needs: publish-npm + if: needs.publish-npm.result == 'success' + timeout-minutes: 5 + + steps: + - name: Wait for NPM propagation + run: sleep 30 + + - name: Verify package + run: | + npm view @quickswap-defi/protocol-core version + npm view @quickswap-defi/protocol-core --json | jq '.provenance' + + - name: Test installation + run: | + mkdir -p /tmp/test-install && cd /tmp/test-install + npm init -y + npm install @quickswap-defi/protocol-core --ignore-scripts + echo "Package installation successful" diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish-sdk.yaml similarity index 80% rename from .github/workflows/publish.yaml rename to .github/workflows/publish-sdk.yaml index b3c8b3f..9d82938 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish-sdk.yaml @@ -1,11 +1,10 @@ -name: Secure NPM Publish +name: Publish SDK -# āš ļø SECURITY: This workflow uses Trusted Publishers (OIDC) -# No long-lived tokens needed - authentication via GitHub identity -# Requires NPM organization setup: https://docs.npmjs.com/trusted-publishers +# SECURITY: Uses Trusted Publishers (OIDC) — no long-lived tokens +# Requires NPM trusted publisher config: https://docs.npmjs.com/trusted-publishers +# Package: @quickswap-defi/sdk on: - # Manual trigger with version bump options workflow_dispatch: inputs: bump: @@ -24,10 +23,9 @@ on: type: boolean default: false - # Automatic publish on release tags push: tags: - - 'v*.*.*' + - 'sdk/v*.*.*' permissions: contents: write @@ -39,7 +37,7 @@ jobs: # JOB 1: Security Audit & Validation # ============================================ security-audit: - name: šŸ”’ Security Audit + name: Security Audit runs-on: ubuntu-latest timeout-minutes: 10 @@ -62,8 +60,8 @@ jobs: - name: Verify package integrity run: | - echo "šŸ“¦ Verifying package.json integrity..." - node -e "JSON.parse(require('fs').readFileSync('package.json'))" + echo "Verifying packages/sdk/package.json integrity..." + node -e "JSON.parse(require('fs').readFileSync('packages/sdk/package.json'))" - name: Install dependencies run: pnpm install --frozen-lockfile @@ -73,13 +71,13 @@ jobs: continue-on-error: true - name: Run linter - run: pnpm run lint + run: pnpm --filter @quickswap-defi/sdk lint # ============================================ # JOB 2: Build & Test # ============================================ build-and-test: - name: šŸ—ļø Build & Test + name: Build & Test runs-on: ubuntu-latest needs: security-audit timeout-minutes: 10 @@ -103,32 +101,32 @@ jobs: run: pnpm install --frozen-lockfile - name: Run tests - run: pnpm run test + run: pnpm --filter @quickswap-defi/sdk test continue-on-error: true - name: Build package - run: pnpm run build + run: pnpm --filter @quickswap-defi/sdk build - name: Validate build output run: | - if [ ! -f "dist/index.js" ] && [ ! -f "dist/index.mjs" ]; then - echo "āŒ Build failed: dist/ not found or empty" + if [ ! -f "packages/sdk/dist/index.js" ] && [ ! -f "packages/sdk/dist/index.mjs" ]; then + echo "Build failed: packages/sdk/dist/ not found or empty" exit 1 fi - echo "āœ… Build validation passed" + echo "Build validation passed" - name: Upload build artifact uses: actions/upload-artifact@v4 with: name: sdk-build - path: dist/ + path: packages/sdk/dist/ retention-days: 7 # ============================================ # JOB 3: Version Bump (if manual trigger) # ============================================ version-bump: - name: šŸ“ Version Bump + name: Version Bump runs-on: ubuntu-latest needs: build-and-test if: github.event_name == 'workflow_dispatch' && !inputs.dry_run @@ -163,38 +161,38 @@ jobs: - name: Bump version id: bump run: | + cd packages/sdk echo "Current version: $(node -p "require('./package.json').version")" pnpm version ${{ inputs.bump }} --no-git-tag-version --no-commit-hooks NEW_VERSION=$(node -p "require('./package.json').version") echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "āœ… Version bumped to: $NEW_VERSION" + echo "Version bumped to: $NEW_VERSION" - name: Commit and push version run: | - git add package.json pnpm-lock.yaml 2>/dev/null || true - git add package.json - git diff --staged --quiet || git commit -m "chore(release): v$(node -p "require('./package.json').version") [skip ci]" - git tag "v$(node -p "require('./package.json').version")" + git add packages/sdk/package.json + git diff --staged --quiet || git commit -m "chore(release): sdk v$(cd packages/sdk && node -p "require('./package.json').version") [skip ci]" + git tag "sdk/v$(cd packages/sdk && node -p "require('./package.json').version")" git push origin HEAD --follow-tags # ============================================ # JOB 4: Publish to NPM (Secure OIDC) # ============================================ publish-npm: - name: šŸš€ Publish to NPM + name: Publish to NPM runs-on: ubuntu-latest needs: [build-and-test, version-bump] if: | always() && needs.build-and-test.result == 'success' && ( - (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')) || + (github.event_name == 'push' && startsWith(github.ref, 'refs/tags/sdk/v')) || (github.event_name == 'workflow_dispatch' && !inputs.dry_run && needs.version-bump.result == 'success') ) timeout-minutes: 10 environment: - name: npm-production + name: npm-sdk url: https://www.npmjs.com/package/@quickswap-defi/sdk steps: @@ -224,7 +222,7 @@ jobs: uses: actions/download-artifact@v4 with: name: sdk-build - path: dist/ + path: packages/sdk/dist/ - name: Install pnpm uses: pnpm/action-setup@v4 @@ -235,11 +233,12 @@ jobs: run: pnpm install --frozen-lockfile - name: Rebuild (ensure consistency) - run: pnpm run build + run: pnpm --filter @quickswap-defi/sdk build - name: Pre-publish verification run: | - echo "šŸ“‹ Package details:" + cd packages/sdk + echo "Package details:" npm pack --dry-run echo "" npm publish --dry-run @@ -247,6 +246,7 @@ jobs: - name: Check if version already exists id: check_version run: | + cd packages/sdk PACKAGE_VERSION=$(node -p "require('./package.json').version") echo "Checking if version $PACKAGE_VERSION already exists..." if npm view "@quickswap-defi/sdk@$PACKAGE_VERSION" version > /dev/null 2>&1; then @@ -259,26 +259,26 @@ jobs: - name: Publish to NPM if: steps.check_version.outputs.exists == 'false' - run: npm publish --access public --provenance + run: cd packages/sdk && npm publish --access public --provenance - name: Skip publish (version exists) if: steps.check_version.outputs.exists == 'true' run: | - echo "ā­ļø Skipping: version ${{ steps.check_version.outputs.version }} already exists" + echo "Skipping: version ${{ steps.check_version.outputs.version }} already exists" - name: Get published version id: version run: | - VERSION=$(node -p "require('./package.json').version") + VERSION=$(cd packages/sdk && node -p "require('./package.json').version") echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Create GitHub Release uses: softprops/action-gh-release@v2 with: - tag_name: v${{ steps.version.outputs.version }} - name: v${{ steps.version.outputs.version }} + tag_name: sdk/v${{ steps.version.outputs.version }} + name: "@quickswap-defi/sdk v${{ steps.version.outputs.version }}" body: | - ## šŸ“¦ NPM Package Published + ## NPM Package Published **Package:** `@quickswap-defi/sdk@${{ steps.version.outputs.version }}` **NPM:** https://www.npmjs.com/package/@quickswap-defi/sdk ### Installation @@ -293,7 +293,7 @@ jobs: # JOB 5: Post-Publish Verification # ============================================ verify-publish: - name: āœ… Verify Publication + name: Verify Publication runs-on: ubuntu-latest needs: publish-npm if: needs.publish-npm.result == 'success' @@ -313,4 +313,4 @@ jobs: mkdir -p /tmp/test-install && cd /tmp/test-install npm init -y npm install @quickswap-defi/sdk --ignore-scripts - echo "āœ… Package installation successful" + echo "Package installation successful" From 192fc801657ecf96c7bf77a52edf634b70d92f7e Mon Sep 17 00:00:00 2001 From: Henry Palacios Date: Thu, 16 Apr 2026 19:14:09 -0300 Subject: [PATCH 4/4] security(ci): harden publish workflows against supply chain attacks SHA-pin all GitHub Actions to immutable commit hashes, remove continue-on-error from audit and test steps, scope permissions to job level (least privilege), replace third-party softprops/action-gh-release with gh CLI, add branch guards for both tag-push and workflow_dispatch paths, fix TOCTOU in git reset --hard, pin npm@11.5.1, remove redundant artifact download+rebuild. --- .github/workflows/publish-protocol-core.yaml | 105 +++++++++--------- .github/workflows/publish-sdk.yaml | 106 ++++++++++--------- 2 files changed, 114 insertions(+), 97 deletions(-) diff --git a/.github/workflows/publish-protocol-core.yaml b/.github/workflows/publish-protocol-core.yaml index da0572b..71521c8 100644 --- a/.github/workflows/publish-protocol-core.yaml +++ b/.github/workflows/publish-protocol-core.yaml @@ -28,9 +28,7 @@ on: - 'protocol-core/v*.*.*' permissions: - contents: write - id-token: write - pull-requests: read + contents: read jobs: # ============================================ @@ -43,17 +41,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js 20 LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -67,8 +65,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Run security audit - run: pnpm audit --audit-level=moderate - continue-on-error: true + run: pnpm audit --audit-level=high - name: Run linter run: pnpm --filter @quickswap-defi/protocol-core lint @@ -84,15 +81,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js 20 LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -118,7 +115,7 @@ jobs: echo "Build validation passed" - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: protocol-core-build path: packages/protocol-core/dist/ @@ -133,24 +130,33 @@ jobs: needs: build-and-test if: github.event_name == 'workflow_dispatch' && !inputs.dry_run timeout-minutes: 5 + permissions: + contents: write outputs: new_version: ${{ steps.bump.outputs.new_version }} steps: + - name: Verify branch is main + run: | + if [ "${{ github.ref }}" != "refs/heads/main" ]; then + echo "Error: Can only publish from main branch" + exit 1 + fi + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -192,6 +198,9 @@ jobs: (github.event_name == 'workflow_dispatch' && !inputs.dry_run && needs.version-bump.result == 'success') ) timeout-minutes: 10 + permissions: + contents: write + id-token: write environment: name: npm-protocol-core @@ -199,42 +208,43 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - - name: Fetch latest (after version bump) + - name: Fetch version bump if: github.event_name == 'workflow_dispatch' + run: git fetch origin --tags + + - name: Verify tag is on main branch + if: github.event_name == 'push' run: | - git fetch origin --tags --force - git reset --hard origin/${{ github.ref_name }} + git fetch origin main + if ! git merge-base --is-ancestor $GITHUB_SHA origin/main; then + echo "Error: Tag does not point to a commit on main branch" + exit 1 + fi + + - name: Install pnpm + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + with: + version: 9 - name: Setup Node.js with NPM registry - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '24' registry-url: 'https://registry.npmjs.org/' - name: Update npm (required for OIDC) run: | - npm install -g npm@latest + npm install -g npm@11.5.1 npm --version - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: protocol-core-build - path: packages/protocol-core/dist/ - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Rebuild (ensure consistency) + - name: Build package run: pnpm --filter @quickswap-defi/protocol-core build - name: Pre-publish verification @@ -275,21 +285,20 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: protocol-core/v${{ steps.version.outputs.version }} - name: "@quickswap-defi/protocol-core v${{ steps.version.outputs.version }}" - body: | - ## NPM Package Published - **Package:** `@quickswap-defi/protocol-core@${{ steps.version.outputs.version }}` - **NPM:** https://www.npmjs.com/package/@quickswap-defi/protocol-core - ### Installation - ```bash - npm install @quickswap-defi/protocol-core@${{ steps.version.outputs.version }} - ``` - draft: false - prerelease: ${{ contains(steps.version.outputs.version, '-') }} - generate_release_notes: true + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "protocol-core/v${{ steps.version.outputs.version }}" \ + --title "@quickswap-defi/protocol-core v${{ steps.version.outputs.version }}" \ + --notes "## NPM Package Published + **Package:** \`@quickswap-defi/protocol-core@${{ steps.version.outputs.version }}\` + **NPM:** https://www.npmjs.com/package/@quickswap-defi/protocol-core + ### Installation + \`\`\`bash + npm install @quickswap-defi/protocol-core@${{ steps.version.outputs.version }} + \`\`\`" \ + --generate-notes \ + ${{ contains(steps.version.outputs.version, '-') && '--prerelease' || '' }} # ============================================ # JOB 5: Post-Publish Verification diff --git a/.github/workflows/publish-sdk.yaml b/.github/workflows/publish-sdk.yaml index 9d82938..2d0bfce 100644 --- a/.github/workflows/publish-sdk.yaml +++ b/.github/workflows/publish-sdk.yaml @@ -28,9 +28,7 @@ on: - 'sdk/v*.*.*' permissions: - contents: write - id-token: write - pull-requests: read + contents: read jobs: # ============================================ @@ -43,17 +41,17 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js 20 LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -67,8 +65,7 @@ jobs: run: pnpm install --frozen-lockfile - name: Run security audit - run: pnpm audit --audit-level=moderate - continue-on-error: true + run: pnpm audit --audit-level=high - name: Run linter run: pnpm --filter @quickswap-defi/sdk lint @@ -84,15 +81,15 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js 20 LTS - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -102,7 +99,6 @@ jobs: - name: Run tests run: pnpm --filter @quickswap-defi/sdk test - continue-on-error: true - name: Build package run: pnpm --filter @quickswap-defi/sdk build @@ -116,7 +112,7 @@ jobs: echo "Build validation passed" - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: name: sdk-build path: packages/sdk/dist/ @@ -131,24 +127,33 @@ jobs: needs: build-and-test if: github.event_name == 'workflow_dispatch' && !inputs.dry_run timeout-minutes: 5 + permissions: + contents: write outputs: new_version: ${{ steps.bump.outputs.new_version }} steps: + - name: Verify branch is main + run: | + if [ "${{ github.ref }}" != "refs/heads/main" ]; then + echo "Error: Can only publish from main branch" + exit 1 + fi + - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: token: ${{ secrets.GITHUB_TOKEN }} fetch-depth: 0 - name: Install pnpm - uses: pnpm/action-setup@v4 + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 with: version: 9 - name: Setup Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '20' cache: 'pnpm' @@ -190,6 +195,9 @@ jobs: (github.event_name == 'workflow_dispatch' && !inputs.dry_run && needs.version-bump.result == 'success') ) timeout-minutes: 10 + permissions: + contents: write + id-token: write environment: name: npm-sdk @@ -197,42 +205,43 @@ jobs: steps: - name: Checkout repository - uses: actions/checkout@v4 + uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.1.1 with: fetch-depth: 0 - - name: Fetch latest (after version bump) + - name: Fetch version bump if: github.event_name == 'workflow_dispatch' + run: git fetch origin --tags + + - name: Verify tag is on main branch + if: github.event_name == 'push' run: | - git fetch origin --tags --force - git reset --hard origin/${{ github.ref_name }} + git fetch origin main + if ! git merge-base --is-ancestor $GITHUB_SHA origin/main; then + echo "Error: Tag does not point to a commit on main branch" + exit 1 + fi + + - name: Install pnpm + uses: pnpm/action-setup@fe02b34f77f8bc703788d5817da081398fad5dd2 # v4.0.0 + with: + version: 9 - name: Setup Node.js with NPM registry - uses: actions/setup-node@v4 + uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 with: node-version: '24' registry-url: 'https://registry.npmjs.org/' - name: Update npm (required for OIDC) run: | - npm install -g npm@latest + npm install -g npm@11.5.1 npm --version - - name: Download build artifact - uses: actions/download-artifact@v4 - with: - name: sdk-build - path: packages/sdk/dist/ - - - name: Install pnpm - uses: pnpm/action-setup@v4 - with: - version: 9 - - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Rebuild (ensure consistency) + - name: Build package run: pnpm --filter @quickswap-defi/sdk build - name: Pre-publish verification @@ -273,21 +282,20 @@ jobs: echo "version=$VERSION" >> $GITHUB_OUTPUT - name: Create GitHub Release - uses: softprops/action-gh-release@v2 - with: - tag_name: sdk/v${{ steps.version.outputs.version }} - name: "@quickswap-defi/sdk v${{ steps.version.outputs.version }}" - body: | - ## NPM Package Published - **Package:** `@quickswap-defi/sdk@${{ steps.version.outputs.version }}` - **NPM:** https://www.npmjs.com/package/@quickswap-defi/sdk - ### Installation - ```bash - npm install @quickswap-defi/sdk@${{ steps.version.outputs.version }} - ``` - draft: false - prerelease: ${{ contains(steps.version.outputs.version, '-') }} - generate_release_notes: true + env: + GH_TOKEN: ${{ github.token }} + run: | + gh release create "sdk/v${{ steps.version.outputs.version }}" \ + --title "@quickswap-defi/sdk v${{ steps.version.outputs.version }}" \ + --notes "## NPM Package Published + **Package:** \`@quickswap-defi/sdk@${{ steps.version.outputs.version }}\` + **NPM:** https://www.npmjs.com/package/@quickswap-defi/sdk + ### Installation + \`\`\`bash + npm install @quickswap-defi/sdk@${{ steps.version.outputs.version }} + \`\`\`" \ + --generate-notes \ + ${{ contains(steps.version.outputs.version, '-') && '--prerelease' || '' }} # ============================================ # JOB 5: Post-Publish Verification