diff --git a/package.json b/package.json index 2ddcba8..c4dd661 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniswap-dev-kit", - "version": "1.0.16", + "version": "1.1.0", "description": "A modern TypeScript library for integrating Uniswap into your dapp.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -49,9 +49,9 @@ "registry": "https://registry.npmjs.org/" }, "peerDependencies": { - "@tanstack/react-query": "^5.87.4", - "react": "^19.1.1", - "react-dom": "^19.1.1" + "@tanstack/react-query": "^5", + "react": "^18 || ^19", + "react-dom": "^18 || ^19" }, "devDependencies": { "@biomejs/biome": "2.2.4", @@ -80,9 +80,7 @@ }, "dependencies": { "@uniswap/permit2-sdk": "^1.4.0", - "@uniswap/router-sdk": "^2.0.4", "@uniswap/sdk-core": "^7.7.2", - "@uniswap/universal-router-sdk": "^4.19.7", "@uniswap/v3-sdk": "^3.25.2", "@uniswap/v4-sdk": "^1.21.4", "ethers": "^5.7.2", @@ -96,7 +94,9 @@ "jsbi": "3.2.5" }, "peerDependencyRules": { - "ignoreMissing": ["@testing-library/dom"] + "ignoreMissing": [ + "@testing-library/dom" + ] } } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index aa20458..da50ab9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -13,20 +13,14 @@ importers: .: dependencies: '@tanstack/react-query': - specifier: ^5.87.4 + specifier: ^5 version: 5.87.4(react@19.1.1) '@uniswap/permit2-sdk': specifier: ^1.4.0 version: 1.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': - specifier: ^2.0.4 - version: 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) '@uniswap/sdk-core': specifier: ^7.7.2 version: 7.7.2 - '@uniswap/universal-router-sdk': - specifier: ^4.19.7 - version: 4.19.7(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10) '@uniswap/v3-sdk': specifier: ^3.25.2 version: 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) @@ -40,10 +34,10 @@ importers: specifier: 3.2.5 version: 3.2.5 react: - specifier: ^19.1.1 + specifier: ^18 || ^19 version: 19.1.1 react-dom: - specifier: ^19.1.1 + specifier: ^18 || ^19 version: 19.1.1(react@19.1.1) viem: specifier: ^2.37.5 @@ -759,36 +753,36 @@ packages: resolution: {integrity: sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==} engines: {node: '>= 8'} - '@nomicfoundation/edr-darwin-arm64@0.11.0': - resolution: {integrity: sha512-aYTVdcSs27XG7ayTzvZ4Yn9z/ABSaUwicrtrYK2NR8IH0ik4N4bWzo/qH8rax6rewVLbHUkGyGYnsy5ZN4iiMw==} + '@nomicfoundation/edr-darwin-arm64@0.11.3': + resolution: {integrity: sha512-w0tksbdtSxz9nuzHKsfx4c2mwaD0+l5qKL2R290QdnN9gi9AV62p9DHkOgfBdyg6/a6ZlnQqnISi7C9avk/6VA==} engines: {node: '>= 18'} - '@nomicfoundation/edr-darwin-x64@0.11.0': - resolution: {integrity: sha512-RxX7UYgvJrfcyT/uHUn44Nsy1XaoW+Q1khKMdHKxeW7BrgIi+Lz+siz3bX5vhSoAnKilDPhIVLrnC8zxQhjR2A==} + '@nomicfoundation/edr-darwin-x64@0.11.3': + resolution: {integrity: sha512-QR4jAFrPbOcrO7O2z2ESg+eUeIZPe2bPIlQYgiJ04ltbSGW27FblOzdd5+S3RoOD/dsZGKAvvy6dadBEl0NgoA==} engines: {node: '>= 18'} - '@nomicfoundation/edr-linux-arm64-gnu@0.11.0': - resolution: {integrity: sha512-J0j+rs0s11FuSipt/ymqrFmpJ7c0FSz1/+FohCIlUXDxFv//+1R/8lkGPjEYFmy8DPpk/iO8mcpqHTGckREbqA==} + '@nomicfoundation/edr-linux-arm64-gnu@0.11.3': + resolution: {integrity: sha512-Ktjv89RZZiUmOFPspuSBVJ61mBZQ2+HuLmV67InNlh9TSUec/iDjGIwAn59dx0bF/LOSrM7qg5od3KKac4LJDQ==} engines: {node: '>= 18'} - '@nomicfoundation/edr-linux-arm64-musl@0.11.0': - resolution: {integrity: sha512-4r32zkGMN7WT/CMEuW0VjbuEdIeCskHNDMW4SSgQSJOE/N9L1KSLJCSsAbPD3aYE+e4WRDTyOwmuLjeUTcLZKQ==} + '@nomicfoundation/edr-linux-arm64-musl@0.11.3': + resolution: {integrity: sha512-B3sLJx1rL2E9pfdD4mApiwOZSrX0a/KQSBWdlq1uAhFKqkl00yZaY4LejgZndsJAa4iKGQJlGnw4HCGeVt0+jA==} engines: {node: '>= 18'} - '@nomicfoundation/edr-linux-x64-gnu@0.11.0': - resolution: {integrity: sha512-SmdncQHLYtVNWLIMyGaY6LpAfamzTDe3fxjkirmJv3CWR5tcEyC6LMui/GsIVnJzXeNJBXAzwl8hTUAxHTM6kQ==} + '@nomicfoundation/edr-linux-x64-gnu@0.11.3': + resolution: {integrity: sha512-D/4cFKDXH6UYyKPu6J3Y8TzW11UzeQI0+wS9QcJzjlrrfKj0ENW7g9VihD1O2FvXkdkTjcCZYb6ai8MMTCsaVw==} engines: {node: '>= 18'} - '@nomicfoundation/edr-linux-x64-musl@0.11.0': - resolution: {integrity: sha512-w6hUqpn/trwiH6SRuRGysj37LsQVCX5XDCA3Xi81sbOaLhbHrNvK9TXWyZmcuzbdTKQQW6VNywcSxDdOiChcJg==} + '@nomicfoundation/edr-linux-x64-musl@0.11.3': + resolution: {integrity: sha512-ergXuIb4nIvmf+TqyiDX5tsE49311DrBky6+jNLgsGDTBaN1GS3OFwFS8I6Ri/GGn6xOaT8sKu3q7/m+WdlFzg==} engines: {node: '>= 18'} - '@nomicfoundation/edr-win32-x64-msvc@0.11.0': - resolution: {integrity: sha512-BLmULjRKoH9BsX+c4Na2ypV7NGeJ+M6Zpqj/faPOwleVscDdSr/IhriyPaXCe8dyfwbge7lWsbekiADtPSnB2Q==} + '@nomicfoundation/edr-win32-x64-msvc@0.11.3': + resolution: {integrity: sha512-snvEf+WB3OV0wj2A7kQ+ZQqBquMcrozSLXcdnMdEl7Tmn+KDCbmFKBt3Tk0X3qOU4RKQpLPnTxdM07TJNVtung==} engines: {node: '>= 18'} - '@nomicfoundation/edr@0.11.0': - resolution: {integrity: sha512-36WERf8ldvyHR6UAbcYsa+vpbW7tCrJGBwF4gXSsb8+STj1n66Hz85Y/O7B9+8AauX3PhglvV5dKl91tk43mWw==} + '@nomicfoundation/edr@0.11.3': + resolution: {integrity: sha512-kqILRkAd455Sd6v8mfP3C1/0tCOynJWY+Ir+k/9Boocu2kObCrsFgG+ZWB7fSBVdd9cPVSNrnhWS+V+PEo637g==} engines: {node: '>= 18'} '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': @@ -883,12 +877,6 @@ packages: '@openzeppelin/contracts@3.4.2-solc-0.7': resolution: {integrity: sha512-W6QmqgkADuFcTLzHL8vVoNBtkwjvQRpYIAom7KiUNoLKghyx3FgH0GBjt8NRvigV1ZmMOBllvE1By1C+bi8WpA==} - '@openzeppelin/contracts@4.7.0': - resolution: {integrity: sha512-52Qb+A1DdOss8QvJrijYYPSf32GUg2pGaG/yCxtaA3cu4jduouTdg4XZSMLW9op54m1jH7J8hoajhHKOPsoJFw==} - - '@openzeppelin/contracts@5.0.2': - resolution: {integrity: sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==} - '@paulmillr/qr@0.2.1': resolution: {integrity: sha512-IHnV6A+zxU7XwmKFinmYjUcwlyK9+xkG3/s9KcQhI9BjQKycrJ1JRO+FbNYPwZiPKW3je/DR0k7w8/gLa5eaxQ==} deprecated: 'The package is now available as "qr": npm install qr' @@ -1053,6 +1041,9 @@ packages: '@scure/base@1.2.5': resolution: {integrity: sha512-9rE6EOVeIQzt5TSu4v+K523F8u6DhBsoZWPGKlnCshhlDhy0kJzUX4V+tr2dWmzF1GdekvThABoEQBGBQI7xZw==} + '@scure/base@1.2.6': + resolution: {integrity: sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg==} + '@scure/bip32@1.1.5': resolution: {integrity: sha512-XyNh1rB0SkEqd3tXcXMi+Xe1fvg+kUIcoRIEujP1Jgv7DqW2r9lg3Ah0NkFaCs9sTkQAQA8kw7xiRXzENi9Rtw==} @@ -1212,8 +1203,8 @@ packages: '@types/aria-query@5.0.4': resolution: {integrity: sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==} - '@types/bn.js@5.1.6': - resolution: {integrity: sha512-Xh8vSwUeMKeYYrj3cX4lGQgFSF/N03r+tv4AiLl1SucqV+uTQpxRcnM8AkXKHwYP9ZPXOYXRr2KPXpVlIvqh9w==} + '@types/bn.js@5.2.0': + resolution: {integrity: sha512-DLbJ1BPqxvQhIGbeu8VbUC1DiAiahHtAYvA0ZEAa4P31F7IaArc8z3C3BRQdWX4mtLQuABG4yzp76ZrS02Ui1Q==} '@types/chai@5.2.2': resolution: {integrity: sha512-8kB30R7Hwqf40JPiKhVzodJs2Qc1ZJ5zuT3uzw5Hq/dhNCl3G3l83jfpdI1e20BP348+fV7VIL/+FxaXkqBmWg==} @@ -1242,6 +1233,9 @@ packages: '@types/node@24.3.1': resolution: {integrity: sha512-3vXmQDXy+woz+gnrTvuvNrPzekOi+Ds0ReMxw0LzBiK3a+1k0kQn9f2NWk+lgD4rJehFUmYy2gMhJ2ZI+7YP9g==} + '@types/node@24.6.2': + resolution: {integrity: sha512-d2L25Y4j+W3ZlNAeMKcy7yDsK425ibcAOO2t7aPTz6gNMH0z2GThtwENCDc0d/Pw9wgyRqE5Px1wkV7naz8ang==} + '@types/normalize-package-data@2.4.4': resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==} @@ -1266,9 +1260,6 @@ packages: '@uniswap/permit2-sdk@1.4.0': resolution: {integrity: sha512-l/aGhfhB93M76vXs4eB8QNwhELE6bs66kh7F1cyobaPtINaVpMmlJv+j3KmHeHwAZIsh7QXyYzhDxs07u0Pe4Q==} - '@uniswap/router-sdk@2.0.4': - resolution: {integrity: sha512-MxCtD+g+2pzzd9rZ6HKTdv1ZK2mLjREoDRNAp9+F961zCCVhgJr9L1/6Hour27/xxCyljwmG83Zn1cSS054giw==} - '@uniswap/sdk-core@7.7.2': resolution: {integrity: sha512-0KqXw+y0opBo6eoPAEoLHEkNpOu0NG9gEk7GAYIGok+SHX89WlykWsRYeJKTg9tOwhLpcG9oHg8xZgQ390iOrA==} engines: {node: '>=10'} @@ -1277,22 +1268,10 @@ packages: resolution: {integrity: sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg==} engines: {node: '>=10'} - '@uniswap/universal-router-sdk@4.19.7': - resolution: {integrity: sha512-fh7YflU4Crl5WTlaDnyW3heMIOEZdGnYkM/bJ1L7gcWY5n0Y6BgRSnskFIiZw3LDKDBIqkxMPcKytEf9ijaPWQ==} - engines: {node: '>=14'} - - '@uniswap/universal-router@2.0.0-beta.2': - resolution: {integrity: sha512-/USVkWZrOCjLeZluR7Yk8SpfWDUKG/MLcOyuxuwnqM1xCJj5ekguSYhct+Yfo/3t9fsZcnL8vSYgz0MKqAomGg==} - engines: {node: '>=14'} - '@uniswap/v2-core@1.0.1': resolution: {integrity: sha512-MtybtkUPSyysqLY2U210NBDeCHX+ltHt3oADGdjqoThZaFRDKwM6k1Nb3F0A3hk5hwuQvytFWhrWHOEq6nVJ8Q==} engines: {node: '>=10'} - '@uniswap/v2-sdk@4.15.2': - resolution: {integrity: sha512-EtROgWTdhHzw4EUj7SdK9wjppOG7psJ16c656cRuv69nWbD9QyDL2shVcQccEiY7ak9WlJ+bIv/VldybXYBDuw==} - engines: {node: '>=10'} - '@uniswap/v3-core@1.0.0': resolution: {integrity: sha512-kSC4djMGKMHj7sLMYVnn61k9nu+lHjMIxgg9CDQT+s2QYLoA56GbSK9Oxr+qJXzzygbkrmuY6cwgP6cW2JXPFA==} engines: {node: '>=10'} @@ -1611,9 +1590,6 @@ packages: big.js@6.2.2: resolution: {integrity: sha512-y/ie+Faknx7sZA5MfGA2xKlu0GDv8RWrXGsmlteyJQ2lvoKv9GBK/fpRMc2qlSoBAgNxrixICFCBefIq8WCQpQ==} - bignumber.js@9.3.0: - resolution: {integrity: sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==} - binary-extensions@2.3.0: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} @@ -1637,6 +1613,9 @@ packages: brace-expansion@2.0.1: resolution: {integrity: sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==} + brace-expansion@2.0.2: + resolution: {integrity: sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==} + braces@3.0.3: resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} engines: {node: '>=8'} @@ -1910,6 +1889,15 @@ packages: supports-color: optional: true + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + decamelize@1.2.0: resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==} engines: {node: '>=0.10.0'} @@ -2252,8 +2240,8 @@ packages: flatted@3.3.3: resolution: {integrity: sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==} - follow-redirects@1.15.9: - resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==} + follow-redirects@1.15.11: + resolution: {integrity: sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==} engines: {node: '>=4.0'} peerDependencies: debug: '*' @@ -2623,6 +2611,9 @@ packages: isarray@1.0.0: resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + isarray@2.0.5: + resolution: {integrity: sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==} + isexe@2.0.0: resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} @@ -3300,8 +3291,8 @@ packages: preact@10.24.2: resolution: {integrity: sha512-1cSoF0aCC8uaARATfrlz4VCBqE8LwZwRfLgkxJOQwAlQt6ayTmi0D9OF7nXid1POI5SZidFuG9CnlXbDfLqY/Q==} - preact@10.26.6: - resolution: {integrity: sha512-5SRRBinwpwkaD+OqlBDeITlRgvd8I8QlxHJw9AxSdMNV6O+LodN9nUyYGpSF7sadHjs6RzeFShMexC6DbtWr9g==} + preact@10.27.2: + resolution: {integrity: sha512-5SYSgFKSyhCbk6SrXyMpqjb5+MQBgfvEKE/OC+PujcY34sOpqtr+0AZQtPYx5IA6VxynQ7rUPCtKzyovpj9Bpg==} pretty-format@27.5.1: resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} @@ -3508,6 +3499,11 @@ packages: engines: {node: '>=10'} hasBin: true + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + serialize-javascript@6.0.2: resolution: {integrity: sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==} @@ -3521,8 +3517,9 @@ packages: setprototypeof@1.2.0: resolution: {integrity: sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==} - sha.js@2.4.11: - resolution: {integrity: sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==} + sha.js@2.4.12: + resolution: {integrity: sha512-8LzC5+bvI45BjpfXU8V5fdU2mfeKiQe1D1gIMn7XUlF3OTUrpdJpPPH4EMAnF0DsHHdSZqCdSss5qCmJKuiO3w==} + engines: {node: '>= 0.10'} hasBin: true shebang-command@2.0.0: @@ -3784,6 +3781,10 @@ packages: resolution: {integrity: sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==} engines: {node: '>=0.6.0'} + to-buffer@1.2.2: + resolution: {integrity: sha512-db0E3UJjcFhpDhAF4tLo03oli3pwl3dbnzXOUIlRKrp+ldk/VUxzpWYZENsw2SZiuBjHAk7DfB0VU7NKdpb6sw==} + engines: {node: '>= 0.4'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3856,6 +3857,10 @@ packages: resolution: {integrity: sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==} engines: {node: '>=16'} + typed-array-buffer@1.0.3: + resolution: {integrity: sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==} + engines: {node: '>= 0.4'} + typedoc@0.28.12: resolution: {integrity: sha512-H5ODu4f7N+myG4MfuSp2Vh6wV+WLoZaEYxKPt2y8hmmqNEMVrH69DAjjdmYivF4tP/C2jrIZCZhPalZlTU/ipA==} engines: {node: '>= 18', pnpm: '>= 10'} @@ -3888,6 +3893,9 @@ packages: undici-types@7.10.0: resolution: {integrity: sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==} + undici-types@7.13.0: + resolution: {integrity: sha512-Ov2Rr9Sx+fRgagJ5AX0qvItZG/JKKoBRAVITs1zk7IqZGTJUwgUr7qoYBpWwakpWilTZFM98rG/AFRocu10iIQ==} + undici@5.29.0: resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==} engines: {node: '>=14.0'} @@ -3986,8 +3994,8 @@ packages: resolution: {integrity: sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} - use-sync-external-store@1.4.0: - resolution: {integrity: sha512-9WXSPC5fMv61vaupRkCKCxsPxBocVnwakBEkMIHHpkTTg6icbJtg6jzgtLDm4bl3cSHAca52rYWih0k4K3PfHw==} + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 @@ -4393,7 +4401,7 @@ snapshots: '@babel/runtime@7.27.1': {} - '@base-org/account@1.1.1(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4)': + '@base-org/account@1.1.1(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -4402,7 +4410,7 @@ snapshots: ox: 0.6.9(typescript@5.9.2)(zod@3.22.4) preact: 10.24.2 viem: 2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) - zustand: 5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) + zustand: 5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -4457,12 +4465,12 @@ snapshots: eth-json-rpc-filters: 6.0.1 eventemitter3: 5.0.1 keccak: 3.0.4 - preact: 10.26.6 - sha.js: 2.4.11 + preact: 10.27.2 + sha.js: 2.4.12 transitivePeerDependencies: - supports-color - '@coinbase/wallet-sdk@4.3.6(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4)': + '@coinbase/wallet-sdk@4.3.6(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4)': dependencies: '@noble/hashes': 1.4.0 clsx: 1.2.1 @@ -4471,7 +4479,7 @@ snapshots: ox: 0.6.9(typescript@5.9.2)(zod@3.22.4) preact: 10.24.2 viem: 2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) - zustand: 5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) + zustand: 5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)) transitivePeerDependencies: - '@types/react' - bufferutil @@ -5238,8 +5246,8 @@ snapshots: dependencies: '@ethereumjs/tx': 4.2.0 '@types/debug': 4.1.12 - debug: 4.4.1(supports-color@8.1.1) - semver: 7.7.1 + debug: 4.4.3(supports-color@8.1.1) + semver: 7.7.2 superstruct: 1.0.4 transitivePeerDependencies: - supports-color @@ -5326,29 +5334,29 @@ snapshots: '@nodelib/fs.scandir': 2.1.5 fastq: 1.19.1 - '@nomicfoundation/edr-darwin-arm64@0.11.0': {} + '@nomicfoundation/edr-darwin-arm64@0.11.3': {} - '@nomicfoundation/edr-darwin-x64@0.11.0': {} + '@nomicfoundation/edr-darwin-x64@0.11.3': {} - '@nomicfoundation/edr-linux-arm64-gnu@0.11.0': {} + '@nomicfoundation/edr-linux-arm64-gnu@0.11.3': {} - '@nomicfoundation/edr-linux-arm64-musl@0.11.0': {} + '@nomicfoundation/edr-linux-arm64-musl@0.11.3': {} - '@nomicfoundation/edr-linux-x64-gnu@0.11.0': {} + '@nomicfoundation/edr-linux-x64-gnu@0.11.3': {} - '@nomicfoundation/edr-linux-x64-musl@0.11.0': {} + '@nomicfoundation/edr-linux-x64-musl@0.11.3': {} - '@nomicfoundation/edr-win32-x64-msvc@0.11.0': {} + '@nomicfoundation/edr-win32-x64-msvc@0.11.3': {} - '@nomicfoundation/edr@0.11.0': + '@nomicfoundation/edr@0.11.3': dependencies: - '@nomicfoundation/edr-darwin-arm64': 0.11.0 - '@nomicfoundation/edr-darwin-x64': 0.11.0 - '@nomicfoundation/edr-linux-arm64-gnu': 0.11.0 - '@nomicfoundation/edr-linux-arm64-musl': 0.11.0 - '@nomicfoundation/edr-linux-x64-gnu': 0.11.0 - '@nomicfoundation/edr-linux-x64-musl': 0.11.0 - '@nomicfoundation/edr-win32-x64-msvc': 0.11.0 + '@nomicfoundation/edr-darwin-arm64': 0.11.3 + '@nomicfoundation/edr-darwin-x64': 0.11.3 + '@nomicfoundation/edr-linux-arm64-gnu': 0.11.3 + '@nomicfoundation/edr-linux-arm64-musl': 0.11.3 + '@nomicfoundation/edr-linux-x64-gnu': 0.11.3 + '@nomicfoundation/edr-linux-x64-musl': 0.11.3 + '@nomicfoundation/edr-win32-x64-msvc': 0.11.3 '@nomicfoundation/solidity-analyzer-darwin-arm64@0.1.2': optional: true @@ -5450,10 +5458,6 @@ snapshots: '@openzeppelin/contracts@3.4.2-solc-0.7': {} - '@openzeppelin/contracts@4.7.0': {} - - '@openzeppelin/contracts@5.0.2': {} - '@paulmillr/qr@0.2.1': {} '@pnpm/config.env-replace@1.1.0': {} @@ -5800,6 +5804,8 @@ snapshots: '@scure/base@1.2.5': {} + '@scure/base@1.2.6': {} + '@scure/bip32@1.1.5': dependencies: '@noble/hashes': 1.2.0 @@ -6055,9 +6061,9 @@ snapshots: '@types/aria-query@5.0.4': {} - '@types/bn.js@5.1.6': + '@types/bn.js@5.2.0': dependencies: - '@types/node': 24.3.1 + '@types/node': 24.6.2 '@types/chai@5.2.2': dependencies: @@ -6085,6 +6091,10 @@ snapshots: dependencies: undici-types: 7.10.0 + '@types/node@24.6.2': + dependencies: + undici-types: 7.13.0 + '@types/normalize-package-data@2.4.4': {} '@types/react-dom@19.1.9(@types/react@19.1.13)': @@ -6109,17 +6119,6 @@ snapshots: - bufferutil - utf-8-validate - '@uniswap/router-sdk@2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))': - dependencies: - '@ethersproject/abi': 5.8.0 - '@uniswap/sdk-core': 7.7.2 - '@uniswap/swap-router-contracts': 1.3.1(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@uniswap/v2-sdk': 4.15.2 - '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - transitivePeerDependencies: - - hardhat - '@uniswap/sdk-core@7.7.2': dependencies: '@ethersproject/address': 5.8.0 @@ -6143,41 +6142,8 @@ snapshots: transitivePeerDependencies: - hardhat - '@uniswap/universal-router-sdk@4.19.7(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': - dependencies: - '@openzeppelin/contracts': 4.7.0 - '@uniswap/permit2-sdk': 1.4.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) - '@uniswap/router-sdk': 2.0.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@uniswap/sdk-core': 7.7.2 - '@uniswap/universal-router': 2.0.0-beta.2 - '@uniswap/v2-core': 1.0.1 - '@uniswap/v2-sdk': 4.15.2 - '@uniswap/v3-core': 1.0.0 - '@uniswap/v3-sdk': 3.25.2(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)) - bignumber.js: 9.3.0 - ethers: 5.7.2(bufferutil@4.0.9)(utf-8-validate@5.0.10) - transitivePeerDependencies: - - bufferutil - - hardhat - - utf-8-validate - - '@uniswap/universal-router@2.0.0-beta.2': - dependencies: - '@openzeppelin/contracts': 5.0.2 - '@uniswap/v2-core': 1.0.1 - '@uniswap/v3-core': 1.0.0 - '@uniswap/v2-core@1.0.1': {} - '@uniswap/v2-sdk@4.15.2': - dependencies: - '@ethersproject/address': 5.8.0 - '@ethersproject/solidity': 5.8.0 - '@uniswap/sdk-core': 7.7.2 - tiny-invariant: 1.3.3 - tiny-warning: 1.0.3 - '@uniswap/v3-core@1.0.0': {} '@uniswap/v3-core@1.0.1': {} @@ -6272,15 +6238,15 @@ snapshots: loupe: 3.2.1 tinyrainbow: 2.0.0 - '@wagmi/connectors@5.9.9(@types/react@19.1.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)': + '@wagmi/connectors@5.9.9(@types/react@19.1.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4)': dependencies: - '@base-org/account': 1.1.1(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4) - '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4) + '@base-org/account': 1.1.1(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4) + '@coinbase/wallet-sdk': 4.3.6(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(zod@3.22.4) '@gemini-wallet/core': 0.2.0(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)) '@metamask/sdk': 0.32.0(bufferutil@4.0.9)(utf-8-validate@5.0.10) '@safe-global/safe-apps-provider': 0.18.6(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) '@safe-global/safe-apps-sdk': 9.1.0(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) - '@wagmi/core': 2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)) '@walletconnect/ethereum-provider': 2.21.1(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) cbw-sdk: '@coinbase/wallet-sdk@3.9.3' viem: 2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) @@ -6315,12 +6281,12 @@ snapshots: - utf-8-validate - zod - '@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))': + '@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))': dependencies: eventemitter3: 5.0.1 mipd: 0.0.7(typescript@5.9.2) viem: 2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) - zustand: 5.0.0(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)) + zustand: 5.0.0(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)) optionalDependencies: '@tanstack/query-core': 5.87.4 typescript: 5.9.2 @@ -6871,7 +6837,7 @@ snapshots: agent-base@6.0.2: dependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -6966,8 +6932,6 @@ snapshots: big.js@6.2.2: {} - bignumber.js@9.3.0: {} - binary-extensions@2.3.0: {} bn.js@4.12.2: {} @@ -6993,6 +6957,10 @@ snapshots: dependencies: balanced-match: 1.0.2 + brace-expansion@2.0.2: + dependencies: + balanced-match: 1.0.2 + braces@3.0.3: dependencies: fill-range: 7.1.1 @@ -7251,7 +7219,11 @@ snapshots: dependencies: ms: 2.1.3 - debug@4.4.1(supports-color@8.1.1): + debug@4.4.1: + dependencies: + ms: 2.1.3 + + debug@4.4.3(supports-color@8.1.1): dependencies: ms: 2.1.3 optionalDependencies: @@ -7669,9 +7641,9 @@ snapshots: flatted@3.3.3: {} - follow-redirects@1.15.9(debug@4.4.1): + follow-redirects@1.15.11(debug@4.4.3): optionalDependencies: - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) for-each@0.3.5: dependencies: @@ -7830,10 +7802,10 @@ snapshots: dependencies: '@ethereumjs/util': 9.1.0 '@ethersproject/abi': 5.8.0 - '@nomicfoundation/edr': 0.11.0 + '@nomicfoundation/edr': 0.11.3 '@nomicfoundation/solidity-analyzer': 0.1.2 '@sentry/node': 5.30.0 - '@types/bn.js': 5.1.6 + '@types/bn.js': 5.2.0 '@types/lru-cache': 5.1.1 adm-zip: 0.4.16 aggregate-error: 3.1.0 @@ -7841,7 +7813,7 @@ snapshots: boxen: 5.1.2 chokidar: 4.0.3 ci-info: 2.0.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) enquirer: 2.4.1 env-paths: 2.2.1 ethereum-cryptography: 1.2.0 @@ -7861,7 +7833,7 @@ snapshots: raw-body: 2.5.2 resolve: 1.17.0 semver: 6.3.1 - solc: 0.8.26(debug@4.4.1) + solc: 0.8.26(debug@4.4.3) source-map-support: 0.5.21 stacktrace-parser: 0.1.11 tinyglobby: 0.2.15 @@ -7941,7 +7913,7 @@ snapshots: https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) transitivePeerDependencies: - supports-color @@ -8079,6 +8051,8 @@ snapshots: isarray@1.0.0: {} + isarray@2.0.5: {} + isexe@2.0.0: {} isows@1.0.6(ws@8.18.0(bufferutil@4.0.9)(utf-8-validate@5.0.10)): @@ -8300,7 +8274,7 @@ snapshots: micro-packed@0.7.3: dependencies: - '@scure/base': 1.2.5 + '@scure/base': 1.2.6 micromatch@4.0.8: dependencies: @@ -8325,7 +8299,7 @@ snapshots: minimatch@5.1.6: dependencies: - brace-expansion: 2.0.1 + brace-expansion: 2.0.2 minimatch@9.0.5: dependencies: @@ -8348,7 +8322,7 @@ snapshots: ansi-colors: 4.1.3 browser-stdout: 1.3.1 chokidar: 3.6.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.3(supports-color@8.1.1) diff: 5.2.0 escape-string-regexp: 4.0.0 find-up: 5.0.0 @@ -8672,7 +8646,7 @@ snapshots: preact@10.24.2: {} - preact@10.26.6: {} + preact@10.27.2: {} pretty-format@27.5.1: dependencies: @@ -8919,6 +8893,8 @@ snapshots: semver@7.7.1: {} + semver@7.7.2: {} + serialize-javascript@6.0.2: dependencies: randombytes: 2.1.0 @@ -8936,10 +8912,11 @@ snapshots: setprototypeof@1.2.0: {} - sha.js@2.4.11: + sha.js@2.4.12: dependencies: inherits: 2.0.4 safe-buffer: 5.2.1 + to-buffer: 1.2.2 shebang-command@2.0.0: dependencies: @@ -8989,11 +8966,11 @@ snapshots: transitivePeerDependencies: - supports-color - solc@0.8.26(debug@4.4.1): + solc@0.8.26(debug@4.4.3): dependencies: command-exists: 1.2.9 commander: 8.3.0 - follow-redirects: 1.15.9(debug@4.4.1) + follow-redirects: 1.15.11(debug@4.4.3) js-sha3: 0.8.0 memorystream: 0.3.1 semver: 5.7.2 @@ -9194,6 +9171,12 @@ snapshots: dependencies: os-tmpdir: 1.0.2 + to-buffer@1.2.2: + dependencies: + isarray: 2.0.5 + safe-buffer: 5.2.1 + typed-array-buffer: 1.0.3 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -9248,6 +9231,12 @@ snapshots: type-fest@4.41.0: {} + typed-array-buffer@1.0.3: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + is-typed-array: 1.1.15 + typedoc@0.28.12(typescript@5.9.2): dependencies: '@gerrit0/mini-shiki': 3.12.2 @@ -9274,6 +9263,8 @@ snapshots: undici-types@7.10.0: {} + undici-types@7.13.0: {} + undici@5.29.0: dependencies: '@fastify/busboy': 2.1.1 @@ -9311,7 +9302,7 @@ snapshots: url-join@5.0.0: {} - use-sync-external-store@1.4.0(react@19.1.1): + use-sync-external-store@1.6.0(react@19.1.1): dependencies: react: 19.1.1 @@ -9342,7 +9333,7 @@ snapshots: dependencies: derive-valtio: 0.1.0(valtio@1.13.2(@types/react@19.1.13)(react@19.1.1)) proxy-compare: 2.6.0 - use-sync-external-store: 1.4.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.1.1) optionalDependencies: '@types/react': 19.1.13 react: 19.1.1 @@ -9384,7 +9375,7 @@ snapshots: vite-node@3.2.4(@types/node@24.3.1)(yaml@2.8.1): dependencies: cac: 6.7.14 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 vite: 6.3.5(@types/node@24.3.1)(yaml@2.8.1) @@ -9426,7 +9417,7 @@ snapshots: '@vitest/spy': 3.2.4 '@vitest/utils': 3.2.4 chai: 5.2.0 - debug: 4.4.1(supports-color@8.1.1) + debug: 4.4.1 expect-type: 1.2.1 magic-string: 0.30.17 pathe: 2.0.3 @@ -9466,10 +9457,10 @@ snapshots: wagmi@2.16.9(@tanstack/query-core@5.87.4)(@tanstack/react-query@5.87.4(react@19.1.1))(@types/react@19.1.13)(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(utf-8-validate@5.0.10)(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4): dependencies: '@tanstack/react-query': 5.87.4(react@19.1.1) - '@wagmi/connectors': 5.9.9(@types/react@19.1.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(utf-8-validate@5.0.10)(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) - '@wagmi/core': 2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.4.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)) + '@wagmi/connectors': 5.9.9(@types/react@19.1.13)(@wagmi/core@2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)))(bufferutil@4.0.9)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(utf-8-validate@5.0.10)(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4))(zod@3.22.4) + '@wagmi/core': 2.20.3(@tanstack/query-core@5.87.4)(@types/react@19.1.13)(react@19.1.1)(typescript@5.9.2)(use-sync-external-store@1.6.0(react@19.1.1))(viem@2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4)) react: 19.1.1 - use-sync-external-store: 1.4.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.1.1) viem: 2.37.5(bufferutil@4.0.9)(typescript@5.9.2)(utf-8-validate@5.0.10)(zod@3.22.4) optionalDependencies: typescript: 5.9.2 @@ -9672,14 +9663,14 @@ snapshots: zod@3.22.4: {} - zustand@5.0.0(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)): + zustand@5.0.0(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)): optionalDependencies: '@types/react': 19.1.13 react: 19.1.1 - use-sync-external-store: 1.4.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.1.1) - zustand@5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.4.0(react@19.1.1)): + zustand@5.0.3(@types/react@19.1.13)(react@19.1.1)(use-sync-external-store@1.6.0(react@19.1.1)): optionalDependencies: '@types/react': 19.1.13 react: 19.1.1 - use-sync-external-store: 1.4.0(react@19.1.1) + use-sync-external-store: 1.6.0(react@19.1.1) diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index 9de71d7..f6f02c5 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -1,38 +1,36 @@ -import type { Currency } from '@uniswap/sdk-core' -import type { Pool, PoolKey } from '@uniswap/v4-sdk' -import type { Abi, Address, PublicClient } from 'viem' -import { createPublicClient, http } from 'viem' import { getChainById } from '@/constants/chains' -import type { BuildSwapCallDataParams } from '@/types' -import type { UniDevKitV4Config, UniDevKitV4Instance } from '@/types/core' -import type { - BuildAddLiquidityCallDataResult, - BuildAddLiquidityParams, -} from '@/types/utils/buildAddLiquidityCallData' -import type { BuildCollectFeesCallDataParams } from '@/types/utils/buildCollectFeesCallData' -import type { BuildRemoveLiquidityCallDataParams } from '@/types/utils/buildRemoveLiquidityCallData' -import type { PoolParams } from '@/types/utils/getPool' -import type { GetPoolKeyFromPoolIdParams } from '@/types/utils/getPoolKeyFromPoolId' -import type { GetPositionParams, GetPositionResponse } from '@/types/utils/getPosition' -import type { QuoteParams, QuoteResponse } from '@/types/utils/getQuote' -import type { GetTokensParams } from '@/types/utils/getTokens' -import type { - PreparePermit2BatchDataParams, - PreparePermit2BatchDataResult, - PreparePermit2DataParams, - PreparePermit2DataResult, -} from '@/types/utils/permit2' +import { FeeTier } from '@/types/utils/getPool' import { buildAddLiquidityCallData } from '@/utils/buildAddLiquidityCallData' import { buildCollectFeesCallData } from '@/utils/buildCollectFeesCallData' import { buildRemoveLiquidityCallData } from '@/utils/buildRemoveLiquidityCallData' import { buildSwapCallData } from '@/utils/buildSwapCallData' import { getPool } from '@/utils/getPool' -import { getPoolKeyFromPoolId } from '@/utils/getPoolKeyFromPoolId' -import { getPosition } from '@/utils/getPosition' +import { getPositionDetails } from '@/utils/getPosition' import { getQuote } from '@/utils/getQuote' import { getTokens } from '@/utils/getTokens' import { preparePermit2BatchData } from '@/utils/preparePermit2BatchData' import { preparePermit2Data } from '@/utils/preparePermit2Data' +import type { BuildSwapCallDataArgs } from '@/types' +import type { UniDevKitV4Config, UniDevKitV4Instance } from '@/types/core' +import type { + BuildAddLiquidityArgs, + BuildAddLiquidityCallDataResult, +} from '@/types/utils/buildAddLiquidityCallData' +import type { GetPositionDetailsResponse } from '@/types/utils/getPosition' +import type { QuoteResponse, SwapExactInSingle } from '@/types/utils/getQuote' +import type { GetTokensArgs } from '@/types/utils/getTokens' +import type { + PreparePermit2BatchDataArgs, + PreparePermit2BatchDataResult, + PreparePermit2DataArgs, + PreparePermit2DataResult, +} from '@/types/utils/permit2' +import type { BuildCollectFeesCallDataArgs } from '@/types/utils/buildCollectFeesCallData' +import type { BuildRemoveLiquidityCallDataArgs } from '@/types/utils/buildRemoveLiquidityCallData' +import type { PoolArgs } from '@/types/utils/getPool' +import type { Currency } from '@uniswap/sdk-core' +import type { Pool } from '@uniswap/v4-sdk' +import { type Address, createPublicClient, http, type PublicClient } from 'viem' /** * Main class for interacting with Uniswap V4 contracts. @@ -62,27 +60,11 @@ export class UniDevKitV4 { } /** - * Returns the current PublicClient instance. - * @returns The current PublicClient. - */ - getClient(): UniDevKitV4Instance['client'] { - return this.instance.client - } - - /** - * Returns the current chain ID. - * @returns The chain ID currently configured. - */ - getChainId(): number { - return this.instance.chain.id - } - - /** - * Returns the current set of contract addresses. - * @returns An object containing the configured contract addresses. + * Returns the FeeTier enum for accessing standard fee tiers. + * @returns The FeeTier enum containing LOWEST, LOW, MEDIUM, and HIGH fee tiers */ - getContracts(): UniDevKitV4Config['contracts'] { - return this.instance.contracts + public getFeeTier(): typeof FeeTier { + return FeeTier } /** @@ -91,7 +73,7 @@ export class UniDevKitV4 { * @returns The address of the specified contract. * @throws Will throw an error if the contract address is not found. */ - getContractAddress(name: keyof UniDevKitV4Config['contracts']): Address { + public getContractAddress(name: keyof UniDevKitV4Config['contracts']): Address { const address = this.instance.contracts[name] if (!address) { throw new Error(`Contract address for ${name} not found.`) @@ -100,162 +82,162 @@ export class UniDevKitV4 { } /** - * Loads the ABI for a specific contract using dynamic imports. - * This method is used internally to lazy load ABIs only when needed. - * @param name @type {keyof UniDevKitV4Config["contracts"]} - * @returns Promise resolving to the contract's ABI - * @throws Will throw an error if the contract ABI is not found - * @private + * Creates a Uniswap V4 Pool instance by fetching real-time pool state from the blockchain. + * + * This method uses multicall to efficiently fetch pool data from the V4StateView contract, + * calling getSlot0() and getLiquidity() in a single transaction. It then uses the Uniswap V4 SDK's + * Pool constructor with the live data to create a fully initialized pool instance. + * + * @param args @type {PoolArgs} - Pool configuration including currencies, fee tier, tick spacing, and hooks + * @returns Promise - A fully initialized Pool instance with current market state + * @throws Error if pool data cannot be fetched or pool doesn't exist */ - private async loadAbi(name: keyof UniDevKitV4Config['contracts']): Promise { - const abiMap: Record Promise | null> = { - poolManager: () => import('@/constants/abis/V4PoolManager').then((m) => m.default), - positionManager: () => import('@/constants/abis/V4PositionMananger').then((m) => m.default), - positionDescriptor: () => null, // TODO: add position descriptor abi - quoter: () => import('@/constants/abis/V4Quoter').then((m) => m.default), - stateView: () => import('@/constants/abis/V4StateView').then((m) => m.default), - universalRouter: () => import('@/constants/abis/V4UniversalRouter').then((m) => m.default), - } - - const loader = abiMap[name] - if (!loader) { - throw new Error(`Contract abi for ${name} not found.`) - } - const abi = await loader() - if (abi === null) { - throw new Error(`Contract abi for ${name} not found.`) - } - return abi + public async getPool(args: PoolArgs): Promise { + return getPool(args, this.instance) } /** - * Retrieves the ABI for a specific contract. - * This method uses dynamic imports to load ABIs on demand, reducing the initial bundle size. - * @param name @type {keyof UniDevKitV4Config["contracts"]} - * @returns Promise resolving to the contract's ABI - * @throws Will throw an error if the contract ABI is not found - * @example - * ```typescript - * const poolManagerAbi = await uniDevKit.getContractAbi('poolManager'); - * ``` + * Fetches ERC20 token metadata and creates Currency instances using Uniswap SDK-Core. + * + * This method uses multicall to efficiently fetch symbol(), name(), and decimals() from multiple + * ERC20 tokens in a single transaction. For native currency (ETH), it creates an Ether instance + * using the chain ID. For ERC20 tokens, it creates Token instances with the fetched metadata. + * + * @param args @type {GetTokensArgs} - Array of token addresses to fetch + * @returns Promise - Array of Currency instances (Token or Ether) + * @throws Error if token data cannot be fetched from the blockchain */ - async getContractAbi(name: keyof UniDevKitV4Config['contracts']): Promise { - return this.loadAbi(name) + public async getTokens(args: GetTokensArgs): Promise { + return getTokens(args, this.instance) } /** - * Retrieves a Uniswap V4 pool instance for a given token pair. - * @param params @type {PoolParams} - * @returns Promise resolving to pool data - * @throws Error if pool data cannot be fetched + * Simulates a token swap using the V4 Quoter contract to get exact output amounts and gas estimates. + * + * This method uses client.simulateContract() to call V4Quoter.quoteExactInputSingle() and simulate + * the swap without executing it. It provides accurate pricing information and gas estimates for + * the transaction without requiring multicall since it's a single contract simulation. + * + * @param args @type {SwapExactInSingle} - Swap parameters including pool key, amount in, and direction + * @returns Promise - Quote data with amount out, gas estimate, and timestamp + * @throws Error if simulation fails or contract call reverts */ - async getPool(params: PoolParams): Promise { - return getPool(params, this.instance) + public async getQuote(args: SwapExactInSingle): Promise { + return getQuote(args, this.instance) } /** - * Retrieves token information for a given array of token addresses. - * @param params @type {GetTokensParams} - * @returns Promise resolving to Token instances for each token address. - * @throws Error if token data cannot be fetched + * Fetches detailed position information from the V4 PositionManager contract. + * + * This method uses multicall to efficiently call V4PositionManager.getPoolAndPositionInfo() and + * getPositionLiquidity() in a single transaction. It retrieves the position's tick range, liquidity, + * and associated pool key, then decodes the raw position data to provide structured information. + * + * @param tokenId - The NFT token ID of the position + * @returns Promise - Position details including tick range, liquidity, and pool key + * @throws Error if position data cannot be fetched or position doesn't exist */ - async getTokens(params: GetTokensParams): Promise { - return getTokens(params, this.instance) + public async getPositionDetails(tokenId: string): Promise { + return getPositionDetails(tokenId, this.instance) } /** - * Retrieves a Uniswap V4 position information for a given token ID. - * @param params @type {GetPositionParams} - * @returns Promise resolving to position data including pool, token0, token1, poolId, and tokenId - * @throws Error if SDK instance is not found or if position data is invalid + * Generates Universal Router calldata for executing token swaps using Uniswap V4. + * + * This method uses the V4Planner from the Uniswap V4 SDK to build swap actions and parameters. + * It creates SWAP_EXACT_IN_SINGLE actions with settle and take operations, and optionally + * includes Permit2 signatures for token approvals. No blockchain calls are made - this is + * purely a calldata generation method that returns Universal Router calldata. + * + * @param args @type {BuildSwapCallDataArgs} - Swap configuration including pool, amounts, and recipient + * @returns Hex - Encoded Universal Router calldata ready for transaction execution + * @throws Error if swap parameters are invalid or calldata generation fails */ - async getPosition(params: GetPositionParams): Promise { - return getPosition(params, this.instance) + public buildSwapCallData(args: BuildSwapCallDataArgs) { + return buildSwapCallData(args) } /** - * Retrieves a Uniswap V4 quote for a given token pair and amount in. - * @param params @type {QuoteParams} - * @returns Promise resolving to quote data including amount out, estimated gas used, and timestamp - * @throws Error if SDK instance is not found or if quote data is invalid + * Creates Position instances and generates V4PositionManager calldata for adding liquidity. + * + * This method uses Uniswap V3 SDK's Position.fromAmounts/fromAmount0/fromAmount1 to create positions, + * and V4PositionManager.addCallParameters to generate the mint calldata. It handles both existing + * pools and new pool creation, with support for Permit2 batch approvals and native currency handling. + * No blockchain calls are made - this is purely a calldata generation method. + * + * @param args @type {BuildAddLiquidityArgs} - Liquidity parameters including amounts, tick range, and slippage + * @returns Promise - Calldata and value for the mint transaction + * @throws Error if position creation fails or invalid parameters are provided */ - async getQuote(params: QuoteParams): Promise { - return getQuote(params, this.instance) - } - - /** - * Retrieves a Uniswap V4 pool key from a given pool ID. - * @param params @type {GetPoolKeyFromPoolIdParams} - * @returns Promise resolving to pool key data including pool address, token0, token1, and fee - * @throws Error if SDK instance is not found or if pool key data is invalid - */ - async getPoolKeyFromPoolId(params: GetPoolKeyFromPoolIdParams): Promise { - return getPoolKeyFromPoolId(params, this.instance) - } - - /** - * Builds a swap call data for a given swap parameters. - * @param params @type {BuildSwapCallDataParams} - * @returns Promise resolving to swap call data including calldata and value - * @throws Error if SDK instance is not found or if swap call data is invalid - */ - async buildSwapCallData(params: BuildSwapCallDataParams) { - return buildSwapCallData(params, this.instance) - } - - /** - * Builds a add liquidity call data for a given add liquidity parameters. - * @param params @type {BuildAddLiquidityParams} - * @returns Promise resolving to add liquidity call data including calldata and value - * @throws Error if SDK instance is not found or if add liquidity call data is invalid - */ - async buildAddLiquidityCallData( - params: BuildAddLiquidityParams, + public async buildAddLiquidityCallData( + args: BuildAddLiquidityArgs, ): Promise { - return buildAddLiquidityCallData(params, this.instance) + return buildAddLiquidityCallData(args, this.instance) } /** - * Prepares the permit2 batch data for multiple tokens. (Used to add liquidity) - * Use toSign.values to sign the permit2 batch data. - * @param params @type {PreparePermit2BatchDataParams} - * @returns Promise resolving to permit2 batch data - * @throws Error if SDK instance is not found or if permit2 batch data is invalid + * Generates V4PositionManager calldata for removing liquidity from existing positions. + * + * This method uses V4PositionManager.removeCallParameters to create burn calldata for + * reducing or completely removing liquidity from a position. It calculates the appropriate + * amounts based on the liquidity percentage to remove. No blockchain calls are made - + * this is purely a calldata generation method. + * + * @param args @type {BuildRemoveLiquidityCallDataArgs} - Parameters for liquidity removal + * @returns Promise - Calldata and value for the burn transaction + * @throws Error if position data is invalid or removal parameters are incorrect */ - async preparePermit2BatchData( - params: PreparePermit2BatchDataParams, - ): Promise { - return preparePermit2BatchData(params, this.instance) + public async buildRemoveLiquidityCallData(args: BuildRemoveLiquidityCallDataArgs) { + return buildRemoveLiquidityCallData(args, this.instance) } /** - * Prepares the permit2 simple data for a single token. (Used to swap) - * Use toSign.values to sign the permit2 simple data. - * @param params @type {PreparePermit2DataParams} - * @returns Promise resolving to permit2 simple data - * @throws Error if SDK instance is not found or if permit2 simple data is invalid + * Generates V4PositionManager calldata for collecting accumulated fees from positions. + * + * This method uses V4PositionManager.collectCallParameters to create calldata for + * collecting fees earned by a liquidity position. It handles both token0 and token1 + * fee collection with proper recipient addressing. No blockchain calls are made - + * this is purely a calldata generation method. + * + * @param args @type {BuildCollectFeesCallDataArgs} - Fee collection parameters + * @returns Promise - Calldata and value for the collect transaction + * @throws Error if position data is invalid or collection parameters are incorrect */ - async preparePermit2Data(params: PreparePermit2DataParams): Promise { - return preparePermit2Data(params, this.instance) + public async buildCollectFeesCallData(args: BuildCollectFeesCallDataArgs) { + return buildCollectFeesCallData(args, this.instance) } /** - * Builds a remove liquidity call data for a given remove liquidity parameters. - * @param params @type {BuildRemoveLiquidityCallDataParams} - * @returns Promise resolving to remove liquidity call data including calldata and value - * @throws Error if SDK instance is not found or if remove liquidity call data is invalid + * Prepares Permit2 batch approval data for multiple tokens using the Permit2 SDK. + * + * This method uses multicall to efficiently fetch allowance() data from the Permit2 contract + * for multiple tokens in a single transaction. It creates batch permit structures that allow + * the Universal Router to spend multiple tokens. Typically used for adding liquidity where + * multiple token approvals are needed. Use the returned toSign.values for signing. + * + * @param args @type {PreparePermit2BatchDataArgs} - Batch permit parameters for multiple tokens + * @returns Promise - Structured permit data ready for signing + * @throws Error if permit data generation fails or parameters are invalid */ - async buildRemoveLiquidityCallData(params: BuildRemoveLiquidityCallDataParams) { - return buildRemoveLiquidityCallData(params, this.instance) + public async preparePermit2BatchData( + args: PreparePermit2BatchDataArgs, + ): Promise { + return preparePermit2BatchData(args, this.instance) } /** - * Builds a collect fees call data for a given collect fees parameters. - * @param params @type {BuildCollectFeesCallDataParams} - * @returns Promise resolving to collect fees call data including calldata and value - * @throws Error if SDK instance is not found or if collect fees call data is invalid + * Prepares Permit2 single token approval data using the Permit2 SDK. + * + * This method creates a single permit structure that allows the Universal Router to spend + * one token. It's typically used for swaps where only one token approval is needed. + * No blockchain calls are made - this is purely a permit data generation method. + * Use the returned toSign.values for signing the permit data. + * + * @param args @type {PreparePermit2DataArgs} - Single permit parameters for one token + * @returns Promise - Structured permit data ready for signing + * @throws Error if permit data generation fails or parameters are invalid */ - async buildCollectFeesCallData(params: BuildCollectFeesCallDataParams) { - return buildCollectFeesCallData(params, this.instance) + public async preparePermit2Data(args: PreparePermit2DataArgs): Promise { + return preparePermit2Data(args, this.instance) } } diff --git a/src/test/core/uniDevKitV4.test.ts b/src/test/core/uniDevKitV4.test.ts index 0b0d22e..01a54ad 100644 --- a/src/test/core/uniDevKitV4.test.ts +++ b/src/test/core/uniDevKitV4.test.ts @@ -22,11 +22,6 @@ describe('UniDevKitV4', () => { sdk = new UniDevKitV4(config) }) - it('should initialize with correct config', () => { - expect(sdk.getChainId()).toBe(config.chainId) - expect(sdk.getContracts()).toEqual(config.contracts) - }) - it('should get contract address', () => { expect(sdk.getContractAddress('quoter')).toBe(config.contracts.quoter) }) @@ -43,14 +38,5 @@ describe('UniDevKitV4', () => { rpcUrl: 'https://base-rpc.com', } sdk = new UniDevKitV4(newConfig) - expect(sdk.getChainId()).toBe(newConfig.chainId) - }) - - it('should create client with custom native currency', () => { - const customConfig: UniDevKitV4Config = { - ...config, - } - const customSdk = new UniDevKitV4(customConfig) - expect(customSdk.getClient()).toBeDefined() }) }) diff --git a/src/test/utils/buildAddLiquidityCallData.test.ts b/src/test/utils/buildAddLiquidityCallData.test.ts index 6ef7d34..509b8cd 100644 --- a/src/test/utils/buildAddLiquidityCallData.test.ts +++ b/src/test/utils/buildAddLiquidityCallData.test.ts @@ -1,69 +1,394 @@ -import { parseUnits } from 'viem' -import { describe, expect, it } from 'vitest' import { createMockSdkInstance } from '@/test/helpers/sdkInstance' import { createTestPool, TEST_ADDRESSES } from '@/test/helpers/testFactories' import { buildAddLiquidityCallData } from '@/utils/buildAddLiquidityCallData' +import { V4PositionManager, Position } from '@uniswap/v4-sdk' +import { parseUnits } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { getDefaultDeadline } from '@/utils/getDefaultDeadline' +import { percentFromBips } from '@/helpers/percent' +import { nearestUsableTick, encodeSqrtRatioX96, TickMath } from '@uniswap/v3-sdk' +import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common' + +// Test constants +const MOCK_DEADLINE = BigInt(1234567890) +const MOCK_DEADLINE_STRING = '1234567890' +const MOCK_SLIPPAGE_PERCENT = { numerator: 50n, denominator: 10000n } +const MOCK_SQRT_PRICE_X96 = '79228162514264337593543950336' +const MOCK_CALLDATA = '0x1234567890abcdef' +const MOCK_VALUE = '0x0' +const MOCK_POSITION_LIQUIDITY = '1000000' +const MOCK_POSITION_TICK_LOWER = -60 +const MOCK_POSITION_TICK_UPPER = 60 +const CUSTOM_SLIPPAGE_BIPS = 500 // 5% +const CUSTOM_DEADLINE = '1234567890' +const CUSTOM_TICK_LOWER = -120 +const CUSTOM_TICK_UPPER = 120 +const MOCK_SIGNATURE = '0x1234567890abcdef' + +// Mock the V4PositionManager.addCallParameters method +vi.mock('@uniswap/v4-sdk', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + V4PositionManager: { + ...actual.V4PositionManager, + addCallParameters: vi.fn(), + }, + Position: { + ...actual.Position, + fromAmounts: vi.fn(), + fromAmount0: vi.fn(), + fromAmount1: vi.fn(), + }, + } +}) + +// Mock getDefaultDeadline +vi.mock('@/utils/getDefaultDeadline', () => ({ + getDefaultDeadline: vi.fn(), +})) + +// Mock percentFromBips +vi.mock('@/helpers/percent', () => ({ + percentFromBips: vi.fn(), +})) + +// Mock nearestUsableTick +vi.mock('@uniswap/v3-sdk', () => ({ + nearestUsableTick: vi.fn(), + TickMath: { + MIN_TICK: -887272, + MAX_TICK: 887272, + }, + encodeSqrtRatioX96: vi.fn(), +})) + +// Type for the options passed to V4PositionManager.addCallParameters +// This includes custom properties that our implementation adds +type AddCallParametersOptions = { + recipient: string + deadline: string + slippageTolerance: unknown + createPool?: boolean + sqrtPriceX96?: string + useNative?: unknown + batchPermit?: unknown +} describe('buildAddLiquidityCallData', () => { const instance = createMockSdkInstance() const pool = createTestPool() - it('should build add liquidity calldata with both amounts', async () => { + // Get mocked functions at module level + const mockAddCallParameters = vi.mocked(V4PositionManager.addCallParameters) + const mockPositionFromAmounts = vi.mocked(Position.fromAmounts) + const mockPositionFromAmount0 = vi.mocked(Position.fromAmount0) + const mockPositionFromAmount1 = vi.mocked(Position.fromAmount1) + const mockGetDefaultDeadline = vi.mocked(getDefaultDeadline) + const mockPercentFromBips = vi.mocked(percentFromBips) + const mockNearestUsableTick = vi.mocked(nearestUsableTick) + const mockEncodeSqrtRatioX96 = vi.mocked(encodeSqrtRatioX96) + + // Create mock position instances + const mockPosition = { + pool, + tickLower: MOCK_POSITION_TICK_LOWER, + tickUpper: MOCK_POSITION_TICK_UPPER, + liquidity: MOCK_POSITION_LIQUIDITY, + } as any + + beforeEach(() => { + vi.clearAllMocks() + + // Default mock implementations + mockAddCallParameters.mockReturnValue({ + calldata: MOCK_CALLDATA, + value: MOCK_VALUE, + }) + + mockPositionFromAmounts.mockReturnValue(mockPosition) + mockPositionFromAmount0.mockReturnValue(mockPosition) + mockPositionFromAmount1.mockReturnValue(mockPosition) + + mockGetDefaultDeadline.mockResolvedValue(MOCK_DEADLINE) + mockPercentFromBips.mockReturnValue(MOCK_SLIPPAGE_PERCENT as any) + mockNearestUsableTick.mockImplementation((tick: number) => tick) + mockEncodeSqrtRatioX96.mockReturnValue({ + toString: () => MOCK_SQRT_PRICE_X96, + } as any) + }) + + it('should call V4PositionManager.addCallParameters with correct parameters when both amounts are provided', async () => { + const amount0 = parseUnits('100', 6).toString() + const amount1 = parseUnits('0.04', 18).toString() + const params = { + pool, + amount0, + amount1, + recipient: TEST_ADDRESSES.recipient, + } + + await buildAddLiquidityCallData(params, instance) + + // Verify getDefaultDeadline was called + expect(mockGetDefaultDeadline).toHaveBeenCalledWith(instance) + + // Verify percentFromBips was called with default slippage (50 bips) + expect(mockPercentFromBips).toHaveBeenCalledWith(DEFAULT_SLIPPAGE_TOLERANCE) + + // Verify nearestUsableTick was called with correct parameters for default ticks + expect(mockNearestUsableTick).toHaveBeenCalledWith(TickMath.MIN_TICK, pool.tickSpacing) + expect(mockNearestUsableTick).toHaveBeenCalledWith(TickMath.MAX_TICK, pool.tickSpacing) + + // Verify Position.fromAmounts was called with exact parameters + expect(mockPositionFromAmounts).toHaveBeenCalledWith({ + pool, + tickLower: TickMath.MIN_TICK, // nearestUsableTick result + tickUpper: TickMath.MAX_TICK, // nearestUsableTick result + amount0, + amount1, + useFullPrecision: true, + }) + + // Verify V4PositionManager.addCallParameters was called with exact parameters + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [position, options] = mockAddCallParameters.mock.calls[0] + + expect(position).toBe(mockPosition) + expect(options).toEqual({ + recipient: TEST_ADDRESSES.recipient, + deadline: MOCK_DEADLINE_STRING, // getDefaultDeadline result + slippageTolerance: MOCK_SLIPPAGE_PERCENT, // percentFromBips result + createPool: false, // Pool has liquidity + sqrtPriceX96: pool.sqrtRatioX96.toString(), // Pool's current sqrtPriceX96 + useNative: undefined, + batchPermit: undefined, + }) + }) + + it('should call V4PositionManager.addCallParameters with correct parameters when only amount0 is provided', async () => { + const amount0 = parseUnits('100', 6).toString() + const params = { + pool, + amount0, + recipient: TEST_ADDRESSES.recipient, + } + + await buildAddLiquidityCallData(params, instance) + + // Verify Position.fromAmount0 was called with exact parameters + expect(mockPositionFromAmount0).toHaveBeenCalledWith({ + pool, + tickLower: TickMath.MIN_TICK, // nearestUsableTick result + tickUpper: TickMath.MAX_TICK, // nearestUsableTick result + amount0, + useFullPrecision: true, + }) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [position, options] = mockAddCallParameters.mock.calls[0] + + expect(position).toBe(mockPosition) + expect((options as AddCallParametersOptions).createPool).toBe(false) + }) + + it('should call V4PositionManager.addCallParameters with correct parameters when only amount1 is provided', async () => { + const amount1 = parseUnits('0.04', 18).toString() + const params = { + pool, + amount1, + recipient: TEST_ADDRESSES.recipient, + } + + await buildAddLiquidityCallData(params, instance) + + // Verify Position.fromAmount1 was called with exact parameters + expect(mockPositionFromAmount1).toHaveBeenCalledWith({ + pool, + tickLower: TickMath.MIN_TICK, // nearestUsableTick result + tickUpper: TickMath.MAX_TICK, // nearestUsableTick result + amount1, + }) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [position, options] = mockAddCallParameters.mock.calls[0] + + expect(position).toBe(mockPosition) + expect((options as AddCallParametersOptions).createPool).toBe(false) + }) + + it('should call V4PositionManager.addCallParameters with createPool=true when pool has no liquidity', async () => { + const emptyPool = createTestPool() + // Mock pool with no liquidity + Object.defineProperty(emptyPool, 'liquidity', { + value: { toString: () => '0' }, + writable: true, + }) + + const amount0 = parseUnits('100', 6).toString() + const amount1 = parseUnits('0.04', 18).toString() + const params = { + pool: emptyPool, + amount0, + amount1, + recipient: TEST_ADDRESSES.recipient, + } + + await buildAddLiquidityCallData(params, instance) + + // Verify encodeSqrtRatioX96 was called for new pool + expect(mockEncodeSqrtRatioX96).toHaveBeenCalledWith(amount1, amount0) + + // Verify Position.fromAmounts was called with the empty pool + expect(mockPositionFromAmounts).toHaveBeenCalledWith({ + pool: emptyPool, + tickLower: TickMath.MIN_TICK, // nearestUsableTick result + tickUpper: TickMath.MAX_TICK, // nearestUsableTick result + amount0, + amount1, + useFullPrecision: true, + }) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockAddCallParameters.mock.calls[0] + + expect((options as AddCallParametersOptions).createPool).toBe(true) + expect((options as AddCallParametersOptions).sqrtPriceX96).toBe(MOCK_SQRT_PRICE_X96) // encodeSqrtRatioX96 result + }) + + it('should call V4PositionManager.addCallParameters with custom tick bounds when provided', async () => { + const amount0 = parseUnits('100', 6).toString() + // Use ticks that are valid for the pool's tickSpacing (60) + const tickLower = CUSTOM_TICK_LOWER // -120 is a multiple of 60 + const tickUpper = CUSTOM_TICK_UPPER // 120 is a multiple of 60 const params = { pool, - amount0: parseUnits('100', 6).toString(), - amount1: parseUnits('0.04', 18).toString(), + amount0, recipient: TEST_ADDRESSES.recipient, + tickLower, + tickUpper, } - const result = await buildAddLiquidityCallData(params, instance) + await buildAddLiquidityCallData(params, instance) - expect(result.calldata).toMatch(/^0x[a-fA-F0-9]+$/) - expect(result.value).toMatch(/^0x[a-fA-F0-9]+$/) - expect(result.calldata.length).toBeGreaterThan(10) + // Verify nearestUsableTick was NOT called since custom ticks were provided + expect(mockNearestUsableTick).not.toHaveBeenCalled() + + // Verify Position.fromAmount0 was called with exact custom tick parameters + expect(mockPositionFromAmount0).toHaveBeenCalledWith({ + pool, + tickLower, + tickUpper, + amount0, + useFullPrecision: true, + }) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [position] = mockAddCallParameters.mock.calls[0] + + expect(position).toBe(mockPosition) }) - it('should build add liquidity calldata with only amount0', async () => { + it('should call V4PositionManager.addCallParameters with custom slippage tolerance when provided', async () => { + const amount0 = parseUnits('100', 6).toString() + const customSlippage = CUSTOM_SLIPPAGE_BIPS // 5% const params = { pool, - amount0: parseUnits('100', 6).toString(), + amount0, recipient: TEST_ADDRESSES.recipient, + slippageTolerance: customSlippage, } - const result = await buildAddLiquidityCallData(params, instance) + await buildAddLiquidityCallData(params, instance) - expect(result.calldata).toMatch(/^0x[a-fA-F0-9]+$/) - expect(result.value).toMatch(/^0x[a-fA-F0-9]+$/) + // Verify percentFromBips was called with custom slippage + expect(mockPercentFromBips).toHaveBeenCalledWith(customSlippage) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockAddCallParameters.mock.calls[0] + + expect(options.slippageTolerance).toEqual(MOCK_SLIPPAGE_PERCENT) }) - it('should build add liquidity calldata with permit2 batch signature', async () => { + it('should call V4PositionManager.addCallParameters with custom deadline when provided', async () => { + const amount0 = parseUnits('100', 6).toString() + const customDeadline = CUSTOM_DEADLINE const params = { pool, - amount0: parseUnits('100', 6).toString(), + amount0, recipient: TEST_ADDRESSES.recipient, - permit2BatchSignature: { - owner: TEST_ADDRESSES.user, - permitBatch: { - details: [ - { - token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - amount: '100000000', - expiration: Math.floor(Date.now() / 1000) + 1800, - nonce: 0, - }, - ], - spender: TEST_ADDRESSES.recipient, - sigDeadline: Math.floor(Date.now() / 1000) + 1800, - }, - signature: - '0x1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890', + deadline: customDeadline, + } + + await buildAddLiquidityCallData(params, instance) + + // Verify getDefaultDeadline was NOT called since custom deadline was provided + expect(mockGetDefaultDeadline).not.toHaveBeenCalled() + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockAddCallParameters.mock.calls[0] + + expect(options.deadline).toBe(customDeadline) + }) + + it('should call V4PositionManager.addCallParameters with permit2 batch signature when provided', async () => { + const amount0 = parseUnits('100', 6).toString() + const permit2BatchSignature = { + owner: TEST_ADDRESSES.user, + permitBatch: { + details: [ + { + token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + amount: '100000000', + expiration: Math.floor(Date.now() / 1000) + 1800, + nonce: 0, + }, + ], + spender: TEST_ADDRESSES.recipient, + sigDeadline: Math.floor(Date.now() / 1000) + 1800, }, + signature: MOCK_SIGNATURE, + } + const params = { + pool, + amount0, + recipient: TEST_ADDRESSES.recipient, + permit2BatchSignature, } - const result = await buildAddLiquidityCallData(params, instance) + await buildAddLiquidityCallData(params, instance) - expect(result.calldata).toMatch(/^0x[a-fA-F0-9]+$/) - expect(result.value).toMatch(/^0x[a-fA-F0-9]+$/) + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockAddCallParameters.mock.calls[0] + + expect(options.batchPermit).toEqual({ + owner: permit2BatchSignature.owner, + permitBatch: permit2BatchSignature.permitBatch, + signature: permit2BatchSignature.signature, + }) + }) + + it('should call V4PositionManager.addCallParameters with native currency when pool has native token', async () => { + // Create a pool with native token (WETH as native) + const nativePool = createTestPool() + Object.defineProperty(nativePool.token0, 'isNative', { + value: true, + writable: true, + }) + + const amount0 = parseUnits('100', 6).toString() + const params = { + pool: nativePool, + amount0, + recipient: TEST_ADDRESSES.recipient, + } + + await buildAddLiquidityCallData(params, instance) + + expect(mockAddCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockAddCallParameters.mock.calls[0] + + expect(options.useNative).toBe(nativePool.token0) }) it('should throw error when neither amount0 nor amount1 is provided', async () => { @@ -73,7 +398,30 @@ describe('buildAddLiquidityCallData', () => { } await expect(buildAddLiquidityCallData(params, instance)).rejects.toThrow( - 'At least one of amount0 or amount1 must be provided.', + 'Invalid input: at least one of amount0 or amount1 must be defined.', + ) + + expect(mockAddCallParameters).not.toHaveBeenCalled() + }) + + it('should throw error when creating pool with only one amount', async () => { + const emptyPool = createTestPool() + Object.defineProperty(emptyPool, 'liquidity', { + value: { toString: () => '0' }, + writable: true, + }) + + const amount0 = parseUnits('100', 6).toString() + const params = { + pool: emptyPool, + amount0, + recipient: TEST_ADDRESSES.recipient, + } + + await expect(buildAddLiquidityCallData(params, instance)).rejects.toThrow( + 'Both amount0 and amount1 are required when creating a new pool.', ) + + expect(mockAddCallParameters).not.toHaveBeenCalled() }) }) diff --git a/src/test/utils/buildRemoveLiquidityCallData.test.ts b/src/test/utils/buildRemoveLiquidityCallData.test.ts index 9596be0..2b35a2b 100644 --- a/src/test/utils/buildRemoveLiquidityCallData.test.ts +++ b/src/test/utils/buildRemoveLiquidityCallData.test.ts @@ -1,82 +1,177 @@ import { V4PositionManager } from '@uniswap/v4-sdk' +import { Percent } from '@uniswap/sdk-core' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockSdkInstance } from '@/test/helpers/sdkInstance' -import { createMockPositionData } from '@/test/helpers/testFactories' +import { createMockPositionData, createTestPool } from '@/test/helpers/testFactories' import { getPosition } from '@/utils/getPosition' +import { getDefaultDeadline } from '@/utils/getDefaultDeadline' +import { percentFromBips } from '@/helpers/percent' +import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common' +import type { GetPositionResponse } from '@/types/utils/getPosition' +import { buildRemoveLiquidityCallData } from '@/utils/buildRemoveLiquidityCallData' -const instance = createMockSdkInstance() -const mockPosition = createMockPositionData() +// Test constants +const MOCK_DEADLINE = BigInt(1234567890) +const MOCK_DEADLINE_STRING = '1234567890' +const MOCK_SLIPPAGE_PERCENT = new Percent(50, 10000) // 0.5% +const MOCK_CALLDATA = '0x1234567890abcdef' +const MOCK_VALUE = '0x0' +const CUSTOM_SLIPPAGE_BIPS = 500 // 5% +const CUSTOM_DEADLINE = '1234567890' +const CUSTOM_LIQUIDITY_PERCENTAGE = 7500 // 75% +const MOCK_TOKEN_ID = '123' +// Mock the V4PositionManager.removeCallParameters method +vi.mock('@uniswap/v4-sdk', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + V4PositionManager: { + ...actual.V4PositionManager, + removeCallParameters: vi.fn(), + }, + } +}) + +// Mock getPosition vi.mock('@/utils/getPosition', () => ({ getPosition: vi.fn(), })) +// Mock getDefaultDeadline vi.mock('@/utils/getDefaultDeadline', () => ({ - getDefaultDeadline: vi.fn().mockResolvedValue('1234567890'), + getDefaultDeadline: vi.fn(), })) +// Mock percentFromBips +vi.mock('@/helpers/percent', () => ({ + percentFromBips: vi.fn(), +})) + +// Type for the options passed to V4PositionManager.removeCallParameters +type RemoveCallParametersOptions = { + slippageTolerance: unknown + deadline: string + liquidityPercentage: unknown + tokenId: string +} + describe('buildRemoveLiquidityCallData', () => { + const instance = createMockSdkInstance() + const pool = createTestPool() + const mockPositionData = createMockPositionData(pool) + + // Get mocked functions at module level + const mockRemoveCallParameters = vi.mocked(V4PositionManager.removeCallParameters) + const mockGetPosition = vi.mocked(getPosition) + const mockGetDefaultDeadline = vi.mocked(getDefaultDeadline) + const mockPercentFromBips = vi.mocked(percentFromBips) + beforeEach(() => { - vi.resetAllMocks() - }) + vi.clearAllMocks() - it('should build calldata for removing liquidity', async () => { - vi.mocked(getPosition).mockResolvedValueOnce(mockPosition) - vi.spyOn(V4PositionManager, 'removeCallParameters').mockReturnValueOnce({ - calldata: '0x123', - value: '0', + // Default mock implementations + mockRemoveCallParameters.mockReturnValue({ + calldata: MOCK_CALLDATA, + value: MOCK_VALUE, }) - const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData') - const result = await buildRemoveLiquidityCallData( - { liquidityPercentage: 10_000, tokenId: '1', deadline: '123' }, - instance, - ) - - expect(result.calldata).toBe('0x123') - expect(result.value).toBe('0') + mockGetPosition.mockResolvedValue(mockPositionData) + mockGetDefaultDeadline.mockResolvedValue(MOCK_DEADLINE) + mockPercentFromBips.mockReturnValue(MOCK_SLIPPAGE_PERCENT) }) - it('should use custom slippageTolerance', async () => { - vi.mocked(getPosition).mockResolvedValueOnce(mockPosition) - const spy = vi.spyOn(V4PositionManager, 'removeCallParameters').mockReturnValueOnce({ - calldata: '0xabc', - value: '1', + it('should call V4PositionManager.removeCallParameters with correct parameters when all required params are provided', async () => { + const params = { + liquidityPercentage: 10_000, // 100% + tokenId: MOCK_TOKEN_ID, + deadline: CUSTOM_DEADLINE, + slippageTolerance: CUSTOM_SLIPPAGE_BIPS, + } + + const result = await buildRemoveLiquidityCallData(params, instance) + + // Verify getPosition was called with correct tokenId + expect(mockGetPosition).toHaveBeenCalledWith({ tokenId: MOCK_TOKEN_ID }, instance) + + // Verify getDefaultDeadline was NOT called since custom deadline was provided + expect(mockGetDefaultDeadline).not.toHaveBeenCalled() + + // Verify percentFromBips was called with custom slippage + expect(mockPercentFromBips).toHaveBeenCalledWith(CUSTOM_SLIPPAGE_BIPS) + expect(mockPercentFromBips).toHaveBeenCalledWith(10_000) // liquidityPercentage + + // Verify V4PositionManager.removeCallParameters was called with exact parameters + expect(mockRemoveCallParameters).toHaveBeenCalledTimes(1) + const [position, options] = mockRemoveCallParameters.mock.calls[0] + + expect(position).toBe(mockPositionData.position) + expect(options).toEqual({ + slippageTolerance: MOCK_SLIPPAGE_PERCENT, + deadline: CUSTOM_DEADLINE, + liquidityPercentage: MOCK_SLIPPAGE_PERCENT, // percentFromBips result for liquidityPercentage + tokenId: MOCK_TOKEN_ID, }) - const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData') - await buildRemoveLiquidityCallData( - { liquidityPercentage: 5000, tokenId: '1', slippageTolerance: 123, deadline: '123' }, - instance, + // Verify return value + expect(result).toEqual({ + calldata: MOCK_CALLDATA, + value: MOCK_VALUE, + }) + }) + + it('should call V4PositionManager.removeCallParameters with default deadline when not provided', async () => { + const params = { + liquidityPercentage: CUSTOM_LIQUIDITY_PERCENTAGE, + tokenId: MOCK_TOKEN_ID, + } + + await buildRemoveLiquidityCallData(params, instance) + + // Verify getDefaultDeadline was called + expect(mockGetDefaultDeadline).toHaveBeenCalledWith(instance) + + // Verify percentFromBips was called with default slippage + expect(mockPercentFromBips).toHaveBeenCalledWith(DEFAULT_SLIPPAGE_TOLERANCE) + expect(mockPercentFromBips).toHaveBeenCalledWith(CUSTOM_LIQUIDITY_PERCENTAGE) + + expect(mockRemoveCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockRemoveCallParameters.mock.calls[0] + + expect((options as RemoveCallParametersOptions).deadline).toBe(MOCK_DEADLINE_STRING) + expect((options as RemoveCallParametersOptions).slippageTolerance).toEqual( + MOCK_SLIPPAGE_PERCENT, ) + }) - expect(spy).toHaveBeenCalledWith( - mockPosition.position, - expect.objectContaining({ slippageTolerance: expect.any(Object) }), + it('should call V4PositionManager.removeCallParameters with default slippage when not provided', async () => { + const params = { + liquidityPercentage: 5000, // 50% + tokenId: MOCK_TOKEN_ID, + deadline: CUSTOM_DEADLINE, + } + + await buildRemoveLiquidityCallData(params, instance) + + // Verify percentFromBips was called with default slippage + expect(mockPercentFromBips).toHaveBeenCalledWith(DEFAULT_SLIPPAGE_TOLERANCE) + expect(mockPercentFromBips).toHaveBeenCalledWith(5000) + + expect(mockRemoveCallParameters).toHaveBeenCalledTimes(1) + const [, options] = mockRemoveCallParameters.mock.calls[0] + + expect((options as RemoveCallParametersOptions).slippageTolerance).toEqual( + MOCK_SLIPPAGE_PERCENT, ) }) - it('should throw if position not found', async () => { - vi.mocked(getPosition).mockResolvedValueOnce(undefined as any) + it('should throw error when position is not found', async () => { + mockGetPosition.mockResolvedValueOnce(undefined as unknown as GetPositionResponse) - const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData') await expect( buildRemoveLiquidityCallData({ liquidityPercentage: 10_000, tokenId: '404' }, instance), ).rejects.toThrow('Position not found') - }) - - it('should throw if V4PositionManager throws', async () => { - vi.mocked(getPosition).mockResolvedValueOnce(mockPosition) - vi.spyOn(V4PositionManager, 'removeCallParameters').mockImplementationOnce(() => { - throw new Error('fail') - }) - const { buildRemoveLiquidityCallData } = await import('@/utils/buildRemoveLiquidityCallData') - await expect( - buildRemoveLiquidityCallData( - { liquidityPercentage: 10_000, tokenId: '1', deadline: '123' }, - instance, - ), - ).rejects.toThrow('fail') + expect(mockRemoveCallParameters).not.toHaveBeenCalled() }) }) diff --git a/src/test/utils/buildSwapCallData.test.ts b/src/test/utils/buildSwapCallData.test.ts index 64d3dc8..f6baba1 100644 --- a/src/test/utils/buildSwapCallData.test.ts +++ b/src/test/utils/buildSwapCallData.test.ts @@ -1,50 +1,316 @@ -import { zeroAddress } from 'viem' -import { describe, expect, it, vi } from 'vitest' -import { createMockSdkInstance } from '@/test/helpers/sdkInstance' -import { createTestPool, USDC, WETH } from '@/test/helpers/testFactories' +// Mock V4Planner +vi.mock('@uniswap/v4-sdk', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + V4Planner: vi.fn().mockImplementation(() => ({ + addAction: vi.fn(), + addSettle: vi.fn(), + addTake: vi.fn(), + actions: '0x1234567890abcdef', + params: ['0xabcdef1234567890'], + })), + } +}) + +// Mock ethers +vi.mock('ethers', () => ({ + ethers: { + utils: { + defaultAbiCoder: { + encode: vi.fn().mockReturnValue('0xencoded'), + }, + solidityPack: vi.fn().mockReturnValue('0xpacked'), + Interface: vi.fn().mockImplementation(() => ({ + encodeFunctionData: vi.fn().mockReturnValue('0x1234567890abcdef'), + })), + }, + }, +})) + +import { Actions, V4Planner } from '@uniswap/v4-sdk' +import { beforeEach, describe, expect, it, vi } from 'vitest' +import { createTestPool, TEST_ADDRESSES } from '@/test/helpers/testFactories' import { buildSwapCallData } from '@/utils/buildSwapCallData' -import * as getQuoteModule from '@/utils/getQuote' +import { COMMANDS } from '@/types/utils/buildSwapCallData' +import { ethers } from 'ethers' -const sdkInstance = createMockSdkInstance() +// Test constants +const MOCK_AMOUNT_IN = BigInt(1000000) // 1 USDC +const MOCK_AMOUNT_OUT_MINIMUM = BigInt(950000000000000000) // 0.95 WETH +const MOCK_PERMIT_SIGNATURE = '0x1234567890abcdef' as const +const MOCK_PERMIT = { + details: { + token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48' as const, + amount: BigInt(1000000), + expiration: Math.floor(Date.now() / 1000) + 1800, + nonce: 0, + }, + spender: TEST_ADDRESSES.recipient, + sigDeadline: Math.floor(Date.now() / 1000) + 1800, +} +const MOCK_CUSTOM_ACTION = { + action: Actions.SWAP_EXACT_IN_SINGLE, + parameters: [{ poolKey: '0x123', amountIn: '1000000' }], +} -// Mock getQuote to return a fixed amount -vi.spyOn(getQuoteModule, 'getQuote').mockImplementation(async () => ({ - amountOut: BigInt(1000000000000000000), // 1 WETH - estimatedGasUsed: BigInt(100000), - timestamp: Date.now(), -})) +// Mock planner type for testing +type MockPlanner = { + addAction: ReturnType + addSettle: ReturnType + addTake: ReturnType + actions: string + params: string[] +} describe('buildSwapCallData', () => { - const mockTokens = [USDC.address as `0x${string}`, WETH.address as `0x${string}`] as const const mockPool = createTestPool() + let mockPlanner: MockPlanner + + beforeEach(async () => { + vi.clearAllMocks() - it.each([ - { tokenIn: mockTokens[0], amountIn: BigInt(1000000), description: 'USDC to WETH' }, - { tokenIn: mockTokens[1], amountIn: BigInt(1000000000000000000), description: 'WETH to USDC' }, - ])('should build swap calldata for $description', async ({ tokenIn, amountIn }) => { - const params = { - tokenIn, - amountIn, - slippageTolerance: 50, - pool: mockPool, - recipient: zeroAddress, + // Reset V4Planner mock + mockPlanner = { + addAction: vi.fn(), + addSettle: vi.fn(), + addTake: vi.fn(), + actions: '0x1234567890abcdef', + params: ['0xabcdef1234567890'], } - const calldata = await buildSwapCallData(params, sdkInstance) - expect(calldata).toMatch(/^0x[a-fA-F0-9]+$/) - expect(calldata.length).toBeGreaterThan(10) // Basic validation it's not empty + vi.mocked(V4Planner).mockImplementation(() => mockPlanner as unknown as V4Planner) }) - it('should handle different slippage tolerances', async () => { - const params = { - tokenIn: mockTokens[0], - amountIn: BigInt(1000000), - slippageTolerance: 100, // 1% - pool: mockPool, - recipient: zeroAddress, - } + it('should build calldata with default actions', () => { + const testCases = [{ zeroForOne: true }, { zeroForOne: false }] + + testCases.forEach(({ zeroForOne }) => { + vi.clearAllMocks() + mockPlanner = { + addAction: vi.fn(), + addSettle: vi.fn(), + addTake: vi.fn(), + actions: '0x1234567890abcdef', + params: ['0xabcdef1234567890'], + } + vi.mocked(V4Planner).mockImplementation(() => mockPlanner as unknown as V4Planner) + + const params = { + amountIn: MOCK_AMOUNT_IN, + amountOutMinimum: MOCK_AMOUNT_OUT_MINIMUM, + pool: mockPool, + zeroForOne, + recipient: TEST_ADDRESSES.recipient, + } + + buildSwapCallData(params) + + // Verify V4Planner was instantiated + expect(V4Planner).toHaveBeenCalledTimes(1) + + // Verify default actions were added + expect(mockPlanner.addAction).toHaveBeenCalledWith(Actions.SWAP_EXACT_IN_SINGLE, [ + { + poolKey: mockPool.poolKey, + zeroForOne, + amountIn: MOCK_AMOUNT_IN.toString(), + amountOutMinimum: MOCK_AMOUNT_OUT_MINIMUM.toString(), + hookData: '0x', + }, + ]) + + // Verify settle and take actions based on zeroForOne + const expectedSettleCurrency = zeroForOne ? mockPool.currency0 : mockPool.currency1 + const expectedTakeCurrency = zeroForOne ? mockPool.currency1 : mockPool.currency0 + expect(mockPlanner.addSettle).toHaveBeenCalledWith(expectedSettleCurrency, true) + expect(mockPlanner.addTake).toHaveBeenCalledWith( + expectedTakeCurrency, + TEST_ADDRESSES.recipient, + ) + + // Verify ethers.utils.Interface was called with correct ABI + const mockInterface = vi.mocked(ethers.utils.Interface) + const mockInterfaceInstance = mockInterface.mock.results[0].value + expect(mockInterface).toHaveBeenCalledWith([ + 'function execute(bytes commands, bytes[] inputs, uint256 deadline)', + ]) + + // Verify encodeFunctionData was called with correct parameters + expect(mockInterfaceInstance.encodeFunctionData).toHaveBeenCalledWith('execute', [ + '0xpacked', + ['0xencoded'], + expect.any(BigInt), + ]) + }) + }) + + it('should build calldata with custom actions', () => { + const testCases = [ + { + customActions: [MOCK_CUSTOM_ACTION], + }, + { + customActions: [ + MOCK_CUSTOM_ACTION, + { + action: Actions.SWAP_EXACT_IN_SINGLE, + parameters: [{ poolKey: '0x456', amountIn: '2000000' }], + }, + ], + }, + ] + + testCases.forEach(({ customActions }) => { + vi.clearAllMocks() + mockPlanner = { + addAction: vi.fn(), + addSettle: vi.fn(), + addTake: vi.fn(), + actions: '0x1234567890abcdef', + params: ['0xabcdef1234567890'], + } + vi.mocked(V4Planner).mockImplementation(() => mockPlanner as unknown as V4Planner) + + const params = { + amountIn: MOCK_AMOUNT_IN, + amountOutMinimum: MOCK_AMOUNT_OUT_MINIMUM, + pool: mockPool, + zeroForOne: true, + recipient: TEST_ADDRESSES.recipient, + customActions, + } + + buildSwapCallData(params) + + // Verify custom actions were added + expect(mockPlanner.addAction).toHaveBeenCalledTimes(customActions.length) + expect(mockPlanner.addAction).toHaveBeenNthCalledWith( + 1, + MOCK_CUSTOM_ACTION.action, + MOCK_CUSTOM_ACTION.parameters, + ) + + // Verify default actions were NOT called + expect(mockPlanner.addSettle).not.toHaveBeenCalled() + expect(mockPlanner.addTake).not.toHaveBeenCalled() + + // Verify ethers.utils.Interface was called with correct ABI + const mockInterface = vi.mocked(ethers.utils.Interface) + const mockInterfaceInstance = mockInterface.mock.results[0].value + expect(mockInterface).toHaveBeenCalledWith([ + 'function execute(bytes commands, bytes[] inputs, uint256 deadline)', + ]) + + // Verify encodeFunctionData was called with correct parameters + expect(mockInterfaceInstance.encodeFunctionData).toHaveBeenCalledWith('execute', [ + '0xpacked', + ['0xencoded'], + expect.any(BigInt), + ]) + }) + }) + + it('should handle permit2 signature correctly', () => { + const testCases = [ + { + permit2Signature: undefined, + expectedCommands: [COMMANDS.V4_SWAP], + }, + { + permit2Signature: { + signature: MOCK_PERMIT_SIGNATURE, + owner: TEST_ADDRESSES.user, + permit: MOCK_PERMIT, + }, + expectedCommands: [COMMANDS.PERMIT2_PERMIT, COMMANDS.V4_SWAP], + }, + ] + + testCases.forEach(({ permit2Signature }) => { + vi.clearAllMocks() + mockPlanner = { + addAction: vi.fn(), + addSettle: vi.fn(), + addTake: vi.fn(), + actions: '0x1234567890abcdef', + params: ['0xabcdef1234567890'], + } + vi.mocked(V4Planner).mockImplementation(() => mockPlanner as unknown as V4Planner) + + const params = { + amountIn: MOCK_AMOUNT_IN, + amountOutMinimum: MOCK_AMOUNT_OUT_MINIMUM, + pool: mockPool, + zeroForOne: true, + recipient: TEST_ADDRESSES.recipient, + ...(permit2Signature && { permit2Signature }), + } + + buildSwapCallData(params) + + // Verify solidityPack was called with correct parameters based on permit2Signature + if (permit2Signature) { + expect(ethers.utils.solidityPack).toHaveBeenCalledWith( + ['uint8', 'uint8'], + [COMMANDS.PERMIT2_PERMIT, COMMANDS.V4_SWAP], + ) + } else { + expect(ethers.utils.solidityPack).toHaveBeenCalledWith(['uint8'], [COMMANDS.V4_SWAP]) + } + + // Verify defaultAbiCoder.encode was called with correct parameters + if (permit2Signature) { + // Should be called 3 times: once for initial planner, once for permit2 struct, once for final planner + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenCalledTimes(3) + // First call: initial planner actions and params + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenNthCalledWith( + 1, + ['bytes', 'bytes[]'], + expect.any(Array), + ) + // Second call: permit2 struct input (inside buildPermit2StructInput) + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenNthCalledWith( + 2, + [ + 'tuple(' + + 'tuple(address token,uint160 amount,uint48 expiration,uint48 nonce) details,' + + 'address spender,' + + 'uint256 sigDeadline' + + ')', + 'bytes', + ], + [permit2Signature.permit, permit2Signature.signature], + ) + // Third call: final planner actions and params + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenNthCalledWith( + 3, + ['bytes', 'bytes[]'], + expect.any(Array), + ) + } else { + // Should be called once: only for planner + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenCalledTimes(1) + expect(ethers.utils.defaultAbiCoder.encode).toHaveBeenCalledWith( + ['bytes', 'bytes[]'], + expect.any(Array), + ) + } + + // Verify ethers.utils.Interface was called with correct ABI + const mockInterface = vi.mocked(ethers.utils.Interface) + const mockInterfaceInstance = mockInterface.mock.results[0].value + expect(mockInterface).toHaveBeenCalledWith([ + 'function execute(bytes commands, bytes[] inputs, uint256 deadline)', + ]) - const calldata = await buildSwapCallData(params, sdkInstance) - expect(calldata).toMatch(/^0x[a-fA-F0-9]+$/) + // Verify encodeFunctionData was called with correct parameters + const expectedInputs = permit2Signature ? ['0xencoded', '0xencoded'] : ['0xencoded'] + expect(mockInterfaceInstance.encodeFunctionData).toHaveBeenCalledWith('execute', [ + '0xpacked', + expectedInputs, + expect.any(BigInt), + ]) + }) }) }) diff --git a/src/test/utils/getPool.test.ts b/src/test/utils/getPool.test.ts index b85cd57..79e1268 100644 --- a/src/test/utils/getPool.test.ts +++ b/src/test/utils/getPool.test.ts @@ -53,7 +53,8 @@ describe('getPool', () => { await expect( getPool( { - tokens: mockTokens, + currencyA: mockTokens[0], + currencyB: mockTokens[1], fee: FeeTier.MEDIUM, }, mockDeps, @@ -63,24 +64,23 @@ describe('getPool', () => { it('should return pool when it exists', async () => { const mockTokenInstances = [ - new Token(1, mockTokens[0], 18, 'TOKEN0', 'Token 0'), - new Token(1, mockTokens[1], 18, 'TOKEN1', 'Token 1'), + new Token(1, mockTokens[0], 6, 'USDC', 'USD Coin'), + new Token(1, mockTokens[1], 18, 'WETH', 'Wrapped Ether'), ] - const mockPoolData = [ - [mockTokens[0], mockTokens[1], FeeTier.MEDIUM, 60, zeroAddress], - // slot0: [sqrtPriceX96, tick, observationIndex, observationCardinality, observationCardinalityNext, feeProtocol] - ['79228162514264337593543950336', 0, 0, 0, 0, 0], - // liquidity - '1000000000000000000', - ] + // Mock the multicall response with the correct structure + const mockSlot0Data = ['79228162514264337593543950336', 0, 0, 0] + const mockLiquidityData = '1000000' + + const mockPoolData = [mockSlot0Data, mockLiquidityData] mockGetTokens.mockResolvedValueOnce(mockTokenInstances) vi.mocked(mockDeps.client.multicall).mockResolvedValueOnce(mockPoolData) const result = await getPool( { - tokens: mockTokens, + currencyA: mockTokens[0], + currencyB: mockTokens[1], fee: FeeTier.MEDIUM, }, mockDeps, @@ -108,7 +108,8 @@ describe('getPool', () => { await expect( getPool( { - tokens: mockTokens, + currencyA: mockTokens[0], + currencyB: mockTokens[1], fee: FeeTier.MEDIUM, }, mockDeps, diff --git a/src/test/utils/getPoolKeyFromPoolId.test.ts b/src/test/utils/getPoolKeyFromPoolId.test.ts index 9f815b8..889b258 100644 --- a/src/test/utils/getPoolKeyFromPoolId.test.ts +++ b/src/test/utils/getPoolKeyFromPoolId.test.ts @@ -7,9 +7,7 @@ describe('getPoolKeyFromPoolId', () => { const mockDeps = createMockSdkInstance() mockDeps.client.readContract = vi.fn().mockRejectedValueOnce(new Error('SDK not initialized')) - await expect(getPoolKeyFromPoolId({ poolId: '0x123' }, mockDeps)).rejects.toThrow( - 'SDK not initialized', - ) + await expect(getPoolKeyFromPoolId('0x123', mockDeps)).rejects.toThrow('SDK not initialized') }) it('should return pool key when SDK instance exists', async () => { @@ -24,7 +22,7 @@ describe('getPoolKeyFromPoolId', () => { const mockDeps = createMockSdkInstance() mockDeps.client.readContract = vi.fn().mockResolvedValueOnce(mockPoolKey) - const result = await getPoolKeyFromPoolId({ poolId: '0x123' }, mockDeps) + const result = await getPoolKeyFromPoolId('0x123', mockDeps) expect(result).toEqual({ currency0: '0x123', @@ -45,8 +43,6 @@ describe('getPoolKeyFromPoolId', () => { const mockDeps = createMockSdkInstance() mockDeps.client.readContract = vi.fn().mockRejectedValueOnce(new Error('Contract read failed')) - await expect(getPoolKeyFromPoolId({ poolId: '0x123' }, mockDeps)).rejects.toThrow( - 'Contract read failed', - ) + await expect(getPoolKeyFromPoolId('0x123', mockDeps)).rejects.toThrow('Contract read failed') }) }) diff --git a/src/test/utils/getQuote.test.ts b/src/test/utils/getQuote.test.ts index 62fcb59..89192c0 100644 --- a/src/test/utils/getQuote.test.ts +++ b/src/test/utils/getQuote.test.ts @@ -1,56 +1,99 @@ -import type { Pool } from '@uniswap/v4-sdk' -import type { Abi } from 'viem' -import type { SimulateContractReturnType } from 'viem/actions' -import { describe, expect, it, vi } from 'vitest' -import { createMockSdkInstance } from '@/test/helpers/sdkInstance' -import { getQuote } from '@/utils/getQuote' - -const mockPool: Pool = { +import type { Abi } from "viem"; +import type { SimulateContractReturnType } from "viem/actions"; +import { describe, expect, it, vi } from "vitest"; +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; +import { getQuote } from "@/utils/getQuote"; +import type { SwapExactInSingle } from "@/types/utils/getQuote"; + +const mockSwapParams: SwapExactInSingle = { poolKey: { - currency0: '0x123', - currency1: '0x456', + currency0: "0x123", + currency1: "0x456", fee: 3000, tickSpacing: 10, - hooks: '0x', + hooks: "0x", }, -} as Pool - -describe('getQuote', () => { - it('should throw error if SDK instance not found', async () => { - const mockDeps = createMockSdkInstance() - mockDeps.client.simulateContract = vi.fn().mockRejectedValueOnce(new Error('SDK not found')) - - await expect( - getQuote( - { - pool: mockPool, - amountIn: BigInt(1000000), - zeroForOne: true, - }, - mockDeps, - ), - ).rejects.toThrow('SDK not found') - }) - - it('should handle quote simulation', async () => { - const mockDeps = createMockSdkInstance() + zeroForOne: true, + amountIn: "1000000", +}; + +describe("getQuote", () => { + it("should throw error if SDK instance not found", async () => { + const mockDeps = createMockSdkInstance(); + mockDeps.client.simulateContract = vi + .fn() + .mockRejectedValueOnce(new Error("SDK not found")); + + await expect(getQuote(mockSwapParams, mockDeps)).rejects.toThrow( + "SDK not found" + ); + }); + + it("should handle quote simulation", async () => { + const mockDeps = createMockSdkInstance(); mockDeps.client.simulateContract = vi.fn().mockResolvedValueOnce({ result: [BigInt(1000000), BigInt(21000)], - } as SimulateContractReturnType) + } as SimulateContractReturnType); - const result = await getQuote( - { - pool: mockPool, - amountIn: BigInt(1000000), - zeroForOne: true, - }, - mockDeps, - ) + const result = await getQuote(mockSwapParams, mockDeps); expect(result).toEqual({ amountOut: BigInt(1000000), estimatedGasUsed: BigInt(21000), timestamp: expect.any(Number), - }) - }) -}) + }); + }); + + it("should handle quote simulation with optional parameters", async () => { + const mockDeps = createMockSdkInstance(); + mockDeps.client.simulateContract = vi.fn().mockResolvedValueOnce({ + result: [BigInt(950000), BigInt(25000)], + } as SimulateContractReturnType); + + const swapParamsWithOptional: SwapExactInSingle = { + ...mockSwapParams, + amountOutMinimum: "950000", + hookData: "0x1234", + }; + + const result = await getQuote(swapParamsWithOptional, mockDeps); + + expect(result).toEqual({ + amountOut: BigInt(950000), + estimatedGasUsed: BigInt(25000), + timestamp: expect.any(Number), + }); + }); + + it("should handle string amountIn conversion to bigint", async () => { + const mockDeps = createMockSdkInstance(); + mockDeps.client.simulateContract = vi.fn().mockResolvedValueOnce({ + result: [BigInt(2000000), BigInt(30000)], + } as SimulateContractReturnType); + + const swapParamsWithStringAmount: SwapExactInSingle = { + ...mockSwapParams, + amountIn: "2000000", // String amount + }; + + const result = await getQuote(swapParamsWithStringAmount, mockDeps); + + expect(result).toEqual({ + amountOut: BigInt(2000000), + estimatedGasUsed: BigInt(30000), + timestamp: expect.any(Number), + }); + + // Verify that the string was converted to bigint in the contract call + expect(mockDeps.client.simulateContract).toHaveBeenCalledWith({ + address: expect.any(String), + abi: expect.any(Array), + functionName: "quoteExactInputSingle", + args: [ + expect.objectContaining({ + exactAmount: BigInt(2000000), // Should be converted to bigint + }), + ], + }); + }); +}); diff --git a/src/test/utils/preparePermit2BatchData.test.ts b/src/test/utils/preparePermit2BatchData.test.ts index 6bc8c6f..782cc6d 100644 --- a/src/test/utils/preparePermit2BatchData.test.ts +++ b/src/test/utils/preparePermit2BatchData.test.ts @@ -1,91 +1,184 @@ -import { describe, expect, it, vi } from 'vitest' +import { PERMIT2_ADDRESS, AllowanceTransfer } from '@uniswap/permit2-sdk' +import { type Block, zeroAddress } from 'viem' +import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockSdkInstance } from '@/test/helpers/sdkInstance' import { preparePermit2BatchData } from '@/utils/preparePermit2BatchData' -describe('preparePermit2BatchData', () => { - const instance = createMockSdkInstance() +// Mock AllowanceTransfer.getPermitData +vi.mock('@uniswap/permit2-sdk', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + AllowanceTransfer: { + ...actual.AllowanceTransfer, + getPermitData: vi.fn(), + }, + } +}) - // Mock multicall response - vi.spyOn(instance.client, 'multicall').mockImplementation(async () => [ +describe('preparePermit2BatchData', () => { + const mockInstance = createMockSdkInstance() + const mockBlockTimestamp = 1234567890n + const mockMulticallResponse = [ { amount: 0n, - expiration: 0n, - nonce: 0n, + expiration: 1234567890n, + nonce: 42n, }, { amount: 0n, - expiration: 0n, - nonce: 0n, + expiration: 1234567890n, + nonce: 43n, }, - ]) + ] - it('should prepare permit2 batch data correctly', async () => { - const params = { - tokens: [ - '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - ], - spender: '0x1234567890123456789012345678901234567890', - owner: '0x0987654321098765432109876543210987654321', - } + const mockParams = { + tokens: [ + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', // WETH + ], + spender: '0x1234567890123456789012345678901234567890', + owner: '0x0987654321098765432109876543210987654321', + } - const result = await preparePermit2BatchData(params, instance) + const mockGetPermitData = vi.mocked(AllowanceTransfer.getPermitData) - expect(result).toBeDefined() - expect(result.owner).toBe(params.owner) - expect(result.permitBatch.spender).toBe(params.spender) - expect(result.permitBatch.details).toHaveLength(2) - expect(result.toSign.domain).toBeDefined() - expect(result.toSign.types).toBeDefined() - expect(result.toSign.values).toBeDefined() + beforeEach(() => { + vi.clearAllMocks() + + // Mock client methods + vi.spyOn(mockInstance.client, 'multicall').mockImplementation(async () => mockMulticallResponse) + vi.spyOn(mockInstance.client, 'getBlock').mockResolvedValue({ + timestamp: mockBlockTimestamp, + } as Block) + + // Mock getPermitData + mockGetPermitData.mockReturnValue({ + domain: { name: 'Permit2', version: '1', chainId: 1, verifyingContract: PERMIT2_ADDRESS }, + types: { PermitBatch: [] }, + values: mockParams, + } as unknown as ReturnType) }) - it('should handle native token (zero address) correctly', async () => { - const params = { - tokens: [ - '0x0000000000000000000000000000000000000000', - '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2', - ], - spender: '0x1234567890123456789012345678901234567890', - owner: '0x0987654321098765432109876543210987654321', - } + it('should call client.multicall with correct parameters', async () => { + await preparePermit2BatchData(mockParams, mockInstance) + + expect(mockInstance.client.multicall).toHaveBeenCalledWith({ + allowFailure: false, + contracts: expect.arrayContaining([ + expect.objectContaining({ + address: PERMIT2_ADDRESS, + abi: expect.any(Array), + functionName: 'allowance', + args: [mockParams.owner, mockParams.tokens[0], mockParams.spender], + }), + expect.objectContaining({ + address: PERMIT2_ADDRESS, + abi: expect.any(Array), + functionName: 'allowance', + args: [mockParams.owner, mockParams.tokens[1], mockParams.spender], + }), + ]), + }) + }) - const result = await preparePermit2BatchData(params, instance) + it('should call AllowanceTransfer.getPermitData with correct parameters', async () => { + await preparePermit2BatchData(mockParams, mockInstance) - expect(result.permitBatch.details).toHaveLength(1) - expect(result.permitBatch.details[0].token).toBe('0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2') + expect(mockGetPermitData).toHaveBeenCalledWith( + expect.objectContaining({ + details: expect.arrayContaining([ + expect.objectContaining({ + token: mockParams.tokens[0], + amount: expect.any(String), + expiration: Number(mockMulticallResponse[0].expiration), + nonce: Number(mockMulticallResponse[0].nonce), + }), + expect.objectContaining({ + token: mockParams.tokens[1], + amount: expect.any(String), + expiration: Number(mockMulticallResponse[1].expiration), + nonce: Number(mockMulticallResponse[1].nonce), + }), + ]), + spender: mockParams.spender, + sigDeadline: expect.any(Number), + }), + PERMIT2_ADDRESS, + mockInstance.chain.id, + ) }) - it('should build permit2 batch data with signature correctly', async () => { - const params = { - tokens: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'], - spender: '0x1234567890123456789012345678901234567890', - owner: '0x0987654321098765432109876543210987654321', - } + it('should call client.getBlock and use block timestamp for sigDeadline when not provided', async () => { + const result = await preparePermit2BatchData(mockParams, mockInstance) - const result = await preparePermit2BatchData(params, instance) - const signature = - '0x1234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890' + expect(mockInstance.client.getBlock).toHaveBeenCalledWith() + expect(result.permitBatch.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600) + }) - const permitWithSignature = result.buildPermit2BatchDataWithSignature(signature) + it('should not call client.getBlock when sigDeadline is provided', async () => { + const customDeadline = 1234567890 + const result = await preparePermit2BatchData( + { + ...mockParams, + sigDeadline: customDeadline, + }, + mockInstance, + ) - expect(permitWithSignature).toBeDefined() - expect(permitWithSignature.owner).toBe(params.owner) - expect(permitWithSignature.permitBatch).toBe(result.permitBatch) - expect(permitWithSignature.signature).toBe(signature) + expect(mockInstance.client.getBlock).not.toHaveBeenCalled() + expect(result.permitBatch.sigDeadline).toBe(customDeadline) }) - it('should use provided sigDeadline if available', async () => { - const sigDeadline = Math.floor(Date.now() / 1000) + 3600 - const params = { - tokens: ['0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'], - spender: '0x1234567890123456789012345678901234567890', - owner: '0x0987654321098765432109876543210987654321', - sigDeadline, + it('should filter out native tokens from multicall', async () => { + const paramsWithNative = { + tokens: [ + zeroAddress, // Native token + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC + ], + spender: mockParams.spender, + owner: mockParams.owner, } - const result = await preparePermit2BatchData(params, instance) + await preparePermit2BatchData(paramsWithNative, mockInstance) + + // Should only call multicall for non-native tokens + expect(mockInstance.client.multicall).toHaveBeenCalledWith({ + allowFailure: false, + contracts: expect.arrayContaining([ + expect.objectContaining({ + args: [ + mockParams.owner, + '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', + mockParams.spender, + ], + }), + ]), + }) + }) + + it('should return correct result structure', async () => { + const result = await preparePermit2BatchData(mockParams, mockInstance) + + expect(result.owner).toBe(mockParams.owner) + expect(result.permitBatch.spender).toBe(mockParams.spender) + expect(result.permitBatch.details).toHaveLength(2) + expect(result.permitBatch.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600) + expect(result.toSign.domain).toBeDefined() + expect(result.toSign.types).toBeDefined() + expect(result.toSign.values).toBeDefined() + expect(result.buildPermit2BatchDataWithSignature).toBeDefined() + }) + + it('should build permit2 batch data with signature correctly', async () => { + const result = await preparePermit2BatchData(mockParams, mockInstance) + const signature = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' + const permitWithSignature = result.buildPermit2BatchDataWithSignature(signature) - expect(result.permitBatch.sigDeadline).toBe(sigDeadline) + expect(permitWithSignature).toEqual({ + owner: mockParams.owner, + permitBatch: result.permitBatch, + signature, + }) }) }) diff --git a/src/test/utils/preparePermit2Data.test.ts b/src/test/utils/preparePermit2Data.test.ts index 3edef05..9075a26 100644 --- a/src/test/utils/preparePermit2Data.test.ts +++ b/src/test/utils/preparePermit2Data.test.ts @@ -1,12 +1,29 @@ -import { PERMIT2_ADDRESS } from '@uniswap/permit2-sdk' +import { PERMIT2_ADDRESS, AllowanceTransfer } from '@uniswap/permit2-sdk' import { type Block, zeroAddress } from 'viem' import { beforeEach, describe, expect, it, vi } from 'vitest' import { createMockSdkInstance } from '@/test/helpers/sdkInstance' import { preparePermit2Data } from '../../utils/preparePermit2Data' +// Mock AllowanceTransfer.getPermitData +vi.mock('@uniswap/permit2-sdk', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + AllowanceTransfer: { + ...actual.AllowanceTransfer, + getPermitData: vi.fn(), + }, + } +}) + describe('preparePermit2Data', () => { const mockInstance = createMockSdkInstance() const mockBlockTimestamp = 1234567890n + const mockAllowance = { + amount: '1000000', + expiration: '1234567890', + nonce: '42', + } const mockParams = { token: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48', // USDC @@ -14,15 +31,23 @@ describe('preparePermit2Data', () => { owner: '0x0987654321098765432109876543210987654321', } + const mockGetPermitData = vi.mocked(AllowanceTransfer.getPermitData) + beforeEach(() => { - vi.spyOn(mockInstance.client, 'readContract').mockImplementation(async () => ({ - amount: '0', - expiration: '0', - nonce: '0', - })) + vi.clearAllMocks() + + // Mock client methods + vi.spyOn(mockInstance.client, 'readContract').mockImplementation(async () => mockAllowance) vi.spyOn(mockInstance.client, 'getBlock').mockResolvedValue({ timestamp: mockBlockTimestamp, } as Block) + + // Mock getPermitData + mockGetPermitData.mockReturnValue({ + domain: { name: 'Permit2', version: '1', chainId: 1, verifyingContract: PERMIT2_ADDRESS }, + types: { PermitSingle: [] }, + values: mockParams, + } as unknown as ReturnType) }) it('should throw error for native token', async () => { @@ -37,21 +62,45 @@ describe('preparePermit2Data', () => { ).rejects.toThrow('Native tokens are not supported for permit2') }) - it('should prepare permit2 data with default sigDeadline', async () => { + it('should call client.readContract with correct parameters', async () => { + await preparePermit2Data(mockParams, mockInstance) + + expect(mockInstance.client.readContract).toHaveBeenCalledWith({ + address: PERMIT2_ADDRESS, + abi: expect.any(Array), + functionName: 'allowance', + args: [mockParams.owner, mockParams.token, mockParams.spender], + }) + }) + + it('should call AllowanceTransfer.getPermitData with correct parameters', async () => { + await preparePermit2Data(mockParams, mockInstance) + + expect(mockGetPermitData).toHaveBeenCalledWith( + expect.objectContaining({ + details: expect.objectContaining({ + token: mockParams.token, + amount: expect.any(String), + expiration: mockAllowance.expiration, + nonce: mockAllowance.nonce, + }), + spender: mockParams.spender, + sigDeadline: expect.any(Number), + }), + PERMIT2_ADDRESS, + mockInstance.chain.id, + ) + }) + + it('should call client.getBlock and use block timestamp for sigDeadline when not provided', async () => { const result = await preparePermit2Data(mockParams, mockInstance) - expect(result.owner).toBe(mockParams.owner) - expect(result.permit.details.token).toBe(mockParams.token) - expect(result.permit.spender).toBe(mockParams.spender) - expect(result.permit.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600) // block timestamp + 1 hour - expect(result.toSign.domain).toBeDefined() - expect(result.toSign.types).toBeDefined() - expect(result.toSign.values).toBeDefined() - expect(result.buildPermit2DataWithSignature).toBeDefined() + expect(mockInstance.client.getBlock).toHaveBeenCalledWith() + expect(result.permit.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600) }) - it('should prepare permit2 data with custom sigDeadline', async () => { - const customDeadline = Number(mockBlockTimestamp) + 7200 // 2 hours from now + it('should not call client.getBlock when sigDeadline is provided', async () => { + const customDeadline = 1234567890 const result = await preparePermit2Data( { ...mockParams, @@ -60,10 +109,24 @@ describe('preparePermit2Data', () => { mockInstance, ) + expect(mockInstance.client.getBlock).not.toHaveBeenCalled() expect(result.permit.sigDeadline).toBe(customDeadline) }) - it('should build permit2 data with signature', async () => { + it('should return correct result structure', async () => { + const result = await preparePermit2Data(mockParams, mockInstance) + + expect(result.owner).toBe(mockParams.owner) + expect(result.permit.details.token).toBe(mockParams.token) + expect(result.permit.spender).toBe(mockParams.spender) + expect(result.permit.sigDeadline).toBe(Number(mockBlockTimestamp) + 3600) + expect(result.toSign.domain).toBeDefined() + expect(result.toSign.types).toBeDefined() + expect(result.toSign.values).toBeDefined() + expect(result.buildPermit2DataWithSignature).toBeDefined() + }) + + it('should build permit2 data with signature correctly', async () => { const result = await preparePermit2Data(mockParams, mockInstance) const signature = '0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef' const permitWithSignature = result.buildPermit2DataWithSignature(signature) @@ -74,26 +137,4 @@ describe('preparePermit2Data', () => { signature, }) }) - - it('should fetch current allowance details', async () => { - const mockAllowance = { - amount: '1000000', - expiration: '1234567890', - nonce: '42', - } - - vi.spyOn(mockInstance.client, 'readContract').mockImplementationOnce(async () => mockAllowance) - - const result = await preparePermit2Data(mockParams, mockInstance) - - expect(mockInstance.client.readContract).toHaveBeenCalledWith({ - address: PERMIT2_ADDRESS, - abi: expect.any(Array), - functionName: 'allowance', - args: [mockParams.owner, mockParams.token, mockParams.spender], - }) - - expect(result.permit.details.expiration).toBe(mockAllowance.expiration) - expect(result.permit.details.nonce).toBe(mockAllowance.nonce) - }) }) diff --git a/src/types/utils/buildAddLiquidityCallData.ts b/src/types/utils/buildAddLiquidityCallData.ts index c88f3a0..5261b92 100644 --- a/src/types/utils/buildAddLiquidityCallData.ts +++ b/src/types/utils/buildAddLiquidityCallData.ts @@ -3,7 +3,7 @@ import type { BatchPermitOptions, Pool } from '@uniswap/v4-sdk' /** * Common base parameters for building add liquidity call data. */ -type BaseAddLiquidityParams = { +type BaseAddLiquidityArgs = { /** * The Uniswap V4 pool to add liquidity to. */ @@ -55,7 +55,7 @@ type BaseAddLiquidityParams = { permit2BatchSignature?: BatchPermitOptions } -export type BuildAddLiquidityParams = BaseAddLiquidityParams +export type BuildAddLiquidityArgs = BaseAddLiquidityArgs /** * Result of building add liquidity call data. diff --git a/src/types/utils/buildCollectFeesCallData.ts b/src/types/utils/buildCollectFeesCallData.ts index 2e9b30a..5d906af 100644 --- a/src/types/utils/buildCollectFeesCallData.ts +++ b/src/types/utils/buildCollectFeesCallData.ts @@ -1,7 +1,7 @@ /** * Parameters required to build the calldata for collecting fees from a Uniswap v4 position. */ -export interface BuildCollectFeesCallDataParams { +export interface BuildCollectFeesCallDataArgs { /** * The tokenId of the position to collect fees from. */ @@ -16,9 +16,4 @@ export interface BuildCollectFeesCallDataParams { * Optional deadline for the transaction (default: 5 minutes from now). */ deadline?: string - - /** - * The slippage tolerance for the transaction. - */ - slippageTolerance?: number } diff --git a/src/types/utils/buildRemoveLiquidityCallData.ts b/src/types/utils/buildRemoveLiquidityCallData.ts index f8137d9..3882f9e 100644 --- a/src/types/utils/buildRemoveLiquidityCallData.ts +++ b/src/types/utils/buildRemoveLiquidityCallData.ts @@ -1,7 +1,7 @@ /** * Parameters required to build the calldata for removing liquidity from a Uniswap v4 position. */ -export interface BuildRemoveLiquidityCallDataParams { +export interface BuildRemoveLiquidityCallDataArgs { /** * The percentage of liquidity to remove from the position. */ diff --git a/src/types/utils/buildSwapCallData.ts b/src/types/utils/buildSwapCallData.ts index 3c7fce3..95d38f6 100644 --- a/src/types/utils/buildSwapCallData.ts +++ b/src/types/utils/buildSwapCallData.ts @@ -1,6 +1,7 @@ import type { PermitSingle } from '@uniswap/permit2-sdk' import type { Pool } from '@uniswap/v4-sdk' import type { Address, Hex } from 'viem' +import type { Actions } from '@uniswap/v4-sdk' /** * Command codes for Universal Router operations @@ -17,21 +18,22 @@ export const COMMANDS = { /** * Parameters for building a V4 swap */ -export type BuildSwapCallDataParams = { - /** Input token address */ - tokenIn: Address - /** Amount of input tokens to swap (in token's smallest unit) */ +export type BuildSwapCallDataArgs = { amountIn: bigint - /** Pool */ + amountOutMinimum: bigint pool: Pool - /** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */ - slippageTolerance?: number - /** Recipient address */ + /** The direction of the swap, true for currency0 to currency1, false for currency1 to currency0 */ + zeroForOne: boolean + //slippageTolerance?: number recipient: Address - /** Permit2 signature */ permit2Signature?: { signature: Hex owner: Address permit: PermitSingle } + /** Custom actions to override default swap behavior. If not provided, uses default SWAP_EXACT_IN_SINGLE */ + customActions?: { + action: Actions + parameters: unknown[] + }[] } diff --git a/src/types/utils/getPool.ts b/src/types/utils/getPool.ts index f6b05cc..5d4194e 100644 --- a/src/types/utils/getPool.ts +++ b/src/types/utils/getPool.ts @@ -23,14 +23,17 @@ export const TICK_SPACING_BY_FEE: Record = { /** * Parameters for retrieving a Uniswap V4 pool instance. + * Aligned with Uniswap V4 SDK Pool constructor parameters. */ -export interface PoolParams { - /** Array of two token addresses representing the pair */ - tokens: [Address, Address] - /** Optional fee tier of the pool (default: FeeTier.MEDIUM) */ - fee?: FeeTier - /** Optional tick spacing for the pool (default: derived from fee tier) */ +export interface PoolArgs { + /** First currency in the pool pair */ + currencyA: Address + /** Second currency in the pool pair */ + currencyB: Address + /** Fee tier of the pool (default: FeeTier.MEDIUM) */ + fee: FeeTier + /** Tick spacing for the pool (default: derived from fee tier) */ tickSpacing?: number - /** Optional hooks contract address (default: DEFAULT_HOOKS) */ + /** Hooks contract address (default: DEFAULT_HOOKS) */ hooks?: `0x${string}` } diff --git a/src/types/utils/getPosition.ts b/src/types/utils/getPosition.ts index 9a56e24..24f7c7d 100644 --- a/src/types/utils/getPosition.ts +++ b/src/types/utils/getPosition.ts @@ -1,5 +1,5 @@ import type { Currency } from '@uniswap/sdk-core' -import type { Pool, Position } from '@uniswap/v4-sdk' +import type { Pool, PoolKey, Position } from '@uniswap/v4-sdk' /** * Parameters required for retrieving a Uniswap V4 position instance. @@ -9,6 +9,17 @@ export interface GetPositionParams { tokenId: string } +/** + * Response structure for retrieving a Uniswap V4 position instance. + */ +export interface GetPositionDetailsResponse { + tokenId: string + tickLower: number + tickUpper: number + liquidity: bigint + poolKey: PoolKey +} + /** * Response structure for retrieving a Uniswap V4 position instance. */ diff --git a/src/types/utils/getQuote.ts b/src/types/utils/getQuote.ts index a6c3c8e..9e44610 100644 --- a/src/types/utils/getQuote.ts +++ b/src/types/utils/getQuote.ts @@ -1,47 +1,86 @@ -import type { Pool } from '@uniswap/v4-sdk' -import type { Hex } from 'viem' +import type { SwapExactInSingle as UniswapSwapExactInSingle } from '@uniswap/v4-sdk' /** - * Parameters required for fetching a quote using the V4 Quoter contract. + * Extended SwapExactInSingle type that ensures alignment with Uniswap V4 SDK + * while providing additional flexibility for our use case. + * + * + * @example + * ```typescript + * const swapParams: SwapExactInSingle = { + * poolKey: { + * currency0: "0x...", + * currency1: "0x...", + * fee: 500, + * tickSpacing: 10, + * hooks: "0x0000000000000000000000000000000000000000" + * }, + * zeroForOne: true, + * amountIn: "1000000" + * }; + * ``` */ -export interface QuoteParams { +export interface SwapExactInSingle extends Partial { /** - * The pool instance to quote from + * Pool key with currency addresses, fee, tick spacing, and hooks. + * @required Must match Uniswap V4 structure exactly */ - pool: Pool + poolKey: UniswapSwapExactInSingle['poolKey'] /** - * The amount of tokens being swapped, expressed as a bigint. + * Direction of the swap. True if swapping from currency0 to currency1. + * @required Must match Uniswap V4 structure exactly */ - amountIn: bigint + zeroForOne: UniswapSwapExactInSingle['zeroForOne'] /** - * Direction of the swap. True if swapping from the lower token to the higher token, false otherwise. + * The amount of tokens being swapped, as string (numberish). + * Accepts bigint.toString(), number, etc. + * @required Must match Uniswap V4 structure exactly */ - zeroForOne: boolean + amountIn: UniswapSwapExactInSingle['amountIn'] /** - * Optional additional data for the hooks, if any. + * Optional minimum amount out for slippage protection. + * @optional Made optional for flexibility, defaults to "0" if not provided */ - hookData?: Hex + amountOutMinimum?: UniswapSwapExactInSingle['amountOutMinimum'] + + /** + * Optional additional data for the hooks. + * @optional Made optional for flexibility, defaults to "0x" if not provided + */ + hookData?: UniswapSwapExactInSingle['hookData'] } /** * Response structure for a successful quote simulation. + * + * @example + * ```typescript + * const response: QuoteResponse = { + * amountOut: 950000n, + * estimatedGasUsed: 150000n, + * timestamp: 1703123456789 + * }; + * ``` */ export interface QuoteResponse { /** * The estimated amount of tokens out for the given input amount. + * @returns The output amount as a bigint */ amountOut: bigint /** * The estimated gas used for the transaction. + * @returns Gas estimate as a bigint */ estimatedGasUsed: bigint /** * The timestamp when the quote was fetched. + * @returns Unix timestamp in milliseconds */ timestamp: number } diff --git a/src/types/utils/getTokens.ts b/src/types/utils/getTokens.ts index 4e4bd9d..57bce04 100644 --- a/src/types/utils/getTokens.ts +++ b/src/types/utils/getTokens.ts @@ -1,9 +1,9 @@ import type { Address } from 'viem' /** - * Parameters for getTokens function + * Arguments for getTokens function */ -export interface GetTokensParams { +export interface GetTokensArgs { /** Array of token contract addresses (at least one) */ addresses: [Address, ...Address[]] } diff --git a/src/types/utils/permit2.ts b/src/types/utils/permit2.ts index 6fb339e..3de6c0a 100644 --- a/src/types/utils/permit2.ts +++ b/src/types/utils/permit2.ts @@ -16,17 +16,17 @@ interface BasePermit2Data { } /** - * Interface for the parameters required to generate a Permit2 batch signature + * Interface for the arguments required to generate a Permit2 batch signature */ -export interface PreparePermit2BatchDataParams extends BasePermit2Data { +export interface PreparePermit2BatchDataArgs extends BasePermit2Data { /** Array of token addresses to permit */ tokens: (Address | string)[] } /** - * Interface for the parameters required to generate a single Permit2 signature + * Interface for the arguments required to generate a single Permit2 signature */ -export interface PreparePermit2DataParams extends BasePermit2Data { +export interface PreparePermit2DataArgs extends BasePermit2Data { /** Token address to permit */ token: Address | string } diff --git a/src/utils/buildAddLiquidityCallData.ts b/src/utils/buildAddLiquidityCallData.ts index cca27a7..92d6bc5 100644 --- a/src/utils/buildAddLiquidityCallData.ts +++ b/src/utils/buildAddLiquidityCallData.ts @@ -2,12 +2,12 @@ import { encodeSqrtRatioX96, nearestUsableTick, TickMath } from '@uniswap/v3-sdk import { Position, V4PositionManager } from '@uniswap/v4-sdk' import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common' import { percentFromBips } from '@/helpers/percent' +import { getDefaultDeadline } from '@/utils/getDefaultDeadline' import type { UniDevKitV4Instance } from '@/types' import type { BuildAddLiquidityCallDataResult, - BuildAddLiquidityParams, + BuildAddLiquidityArgs, } from '@/types/utils/buildAddLiquidityCallData' -import { getDefaultDeadline } from '@/utils/getDefaultDeadline' /** * Builds the calldata and native value required to add liquidity to a Uniswap V4 pool. @@ -64,7 +64,7 @@ import { getDefaultDeadline } from '@/utils/getDefaultDeadline' */ export async function buildAddLiquidityCallData( - params: BuildAddLiquidityParams, + params: BuildAddLiquidityArgs, instance: UniDevKitV4Instance, ): Promise { const { @@ -88,11 +88,6 @@ export async function buildAddLiquidityCallData( const tickLower = tickLowerParam ?? nearestUsableTick(TickMath.MIN_TICK, pool.tickSpacing) const tickUpper = tickUpperParam ?? nearestUsableTick(TickMath.MAX_TICK, pool.tickSpacing) - // Validate input - if (!amount0 && !amount1) { - throw new Error('At least one of amount0 or amount1 must be provided.') - } - let sqrtPriceX96: string if (createPool) { if (!amount0 || !amount1) { diff --git a/src/utils/buildCollectFeesCallData.ts b/src/utils/buildCollectFeesCallData.ts index 5be245d..6760f8b 100644 --- a/src/utils/buildCollectFeesCallData.ts +++ b/src/utils/buildCollectFeesCallData.ts @@ -1,8 +1,7 @@ import { V4PositionManager } from '@uniswap/v4-sdk' -import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common' import { percentFromBips } from '@/helpers/percent' import type { UniDevKitV4Instance } from '@/types' -import type { BuildCollectFeesCallDataParams } from '@/types/utils/buildCollectFeesCallData' +import type { BuildCollectFeesCallDataArgs } from '@/types/utils/buildCollectFeesCallData' import { getDefaultDeadline } from '@/utils/getDefaultDeadline' import { getPosition } from '@/utils/getPosition' @@ -30,12 +29,7 @@ import { getPosition } from '@/utils/getPosition' * ``` */ export async function buildCollectFeesCallData( - { - tokenId, - recipient, - deadline: deadlineParam, - slippageTolerance, - }: BuildCollectFeesCallDataParams, + { tokenId, recipient, deadline: deadlineParam }: BuildCollectFeesCallDataArgs, instance: UniDevKitV4Instance, ) { const positionData = await getPosition({ tokenId }, instance) @@ -47,10 +41,11 @@ export async function buildCollectFeesCallData( try { const { calldata, value } = V4PositionManager.collectCallParameters(positionData.position, { + tokenId, recipient, + slippageTolerance: percentFromBips(0), deadline, - tokenId, - slippageTolerance: percentFromBips(slippageTolerance ?? DEFAULT_SLIPPAGE_TOLERANCE), + hookData: positionData.pool.hooks, }) return { diff --git a/src/utils/buildRemoveLiquidityCallData.ts b/src/utils/buildRemoveLiquidityCallData.ts index e2d9058..81b32e5 100644 --- a/src/utils/buildRemoveLiquidityCallData.ts +++ b/src/utils/buildRemoveLiquidityCallData.ts @@ -2,7 +2,7 @@ import { V4PositionManager } from '@uniswap/v4-sdk' import { DEFAULT_SLIPPAGE_TOLERANCE } from '@/constants/common' import { percentFromBips } from '@/helpers/percent' import type { UniDevKitV4Instance } from '@/types' -import type { BuildRemoveLiquidityCallDataParams } from '@/types/utils/buildRemoveLiquidityCallData' +import type { BuildRemoveLiquidityCallDataArgs } from '@/types/utils/buildRemoveLiquidityCallData' import { getDefaultDeadline } from '@/utils/getDefaultDeadline' import { getPosition } from '@/utils/getPosition' @@ -32,7 +32,7 @@ export async function buildRemoveLiquidityCallData( deadline: deadlineParam, slippageTolerance, tokenId, - }: BuildRemoveLiquidityCallDataParams, + }: BuildRemoveLiquidityCallDataArgs, instance: UniDevKitV4Instance, ) { // Get position data @@ -43,21 +43,15 @@ export async function buildRemoveLiquidityCallData( const deadline = deadlineParam ?? (await getDefaultDeadline(instance)).toString() - // Build remove liquidity call data - try { - const { calldata, value } = V4PositionManager.removeCallParameters(positionData.position, { - slippageTolerance: percentFromBips(slippageTolerance ?? DEFAULT_SLIPPAGE_TOLERANCE), - deadline: deadline, - liquidityPercentage: percentFromBips(liquidityPercentage), - tokenId: tokenId, - }) + const { calldata, value } = V4PositionManager.removeCallParameters(positionData.position, { + slippageTolerance: percentFromBips(slippageTolerance ?? DEFAULT_SLIPPAGE_TOLERANCE), + deadline: deadline, + liquidityPercentage: percentFromBips(liquidityPercentage), + tokenId: tokenId, + }) - return { - calldata: calldata, - value: value, - } - } catch (error) { - console.error(error) - throw error + return { + calldata: calldata, + value: value, } } diff --git a/src/utils/buildSwapCallData.ts b/src/utils/buildSwapCallData.ts index af4091e..c5b9aa7 100644 --- a/src/utils/buildSwapCallData.ts +++ b/src/utils/buildSwapCallData.ts @@ -2,10 +2,7 @@ import type { PermitSingle } from '@uniswap/permit2-sdk' import { Actions, V4Planner } from '@uniswap/v4-sdk' import { ethers } from 'ethers' import type { Hex } from 'viem' -import { calculateMinimumOutput } from '@/helpers/swap' -import { type BuildSwapCallDataParams, COMMANDS } from '@/types' -import type { UniDevKitV4Instance } from '@/types/core' -import { getQuote } from '@/utils/getQuote' +import { type BuildSwapCallDataArgs, COMMANDS } from '@/types' const buildPermit2StructInput = (permit: PermitSingle, signature: Hex) => { return ethers.utils.defaultAbiCoder.encode( @@ -25,94 +22,48 @@ const buildPermit2StructInput = (permit: PermitSingle, signature: Hex) => { * Builds calldata for a Uniswap V4 swap * * This function creates the necessary calldata to execute a token swap through - * Uniswap V4's Universal Router. It handles pool discovery, parameter encoding, - * and deadline management. + * Uniswap V4's Universal Router. * * @param params - Swap configuration parameters - * @param instance - UniDevKitV4 instance for pool operations - * @returns Promise resolving to encoded calldata - * - * @throws Error if pool doesn't exist - * - * @example - * ```typescript - * // Basic swap - * const swapParams = { - * tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC - * amountIn: parseUnits("100", 6), // 100 USDC - * pool: pool, - * slippageTolerance: 50, // 0.5% - * }; - * - * const calldata = await buildSwapCallData(swapParams, instance); - * - * // Swap with permit2 - * const permitData = await preparePermit2Data({ - * token: tokenIn, - * spender: universalRouterAddress, - * owner: userAddress - * }, instance); - * - * const signature = await signer._signTypedData(permitData.toSign); - * const permitWithSignature = permitData.buildPermit2DataWithSignature(signature); - * - * const swapParamsWithPermit = { - * ...swapParams, - * permit2Signature: permitWithSignature - * }; - * - * const calldataWithPermit = await buildSwapCallData(swapParamsWithPermit, instance); - * - * // Send transaction - * const tx = await sendTransaction({ - * to: universalRouterAddress, - * data: calldata, - * value: 0, - * }); - * ``` + * @returns encoded calldata */ -export async function buildSwapCallData( - params: BuildSwapCallDataParams, - instance: UniDevKitV4Instance, -): Promise { - // Extract and set default parameters - const { tokenIn, amountIn, pool, slippageTolerance = 50, permit2Signature, recipient } = params - - const zeroForOne = tokenIn.toLowerCase() === pool.poolKey.currency0.toLowerCase() - - // Get quote and calculate minimum output amount - const quote = await getQuote( - { - pool, - amountIn, - zeroForOne, - }, - instance, - ) - - // Calculate minimum output amount based on slippage - const amountOutMinimum = calculateMinimumOutput(quote.amountOut, slippageTolerance) +export function buildSwapCallData(params: BuildSwapCallDataArgs): Hex { + const { + amountIn, + pool, + zeroForOne, + permit2Signature, + recipient, + amountOutMinimum, + customActions, + } = params const planner = new V4Planner() - planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [ - { - poolKey: pool.poolKey, - zeroForOne, - amountIn: amountIn.toString(), - amountOutMinimum: amountOutMinimum.toString(), - hookData: '0x', - }, - ]) - - const currencyIn = zeroForOne ? pool.currency0 : pool.currency1 - const currencyOut = zeroForOne ? pool.currency1 : pool.currency0 - - // Agrega la acción de settle - planner.addSettle(currencyIn, true) - - // Agrega la acción de take - planner.addTake(currencyOut, recipient) + // Use custom actions if provided, otherwise use default SWAP_EXACT_IN_SINGLE + if (customActions && customActions.length > 0) { + // Add custom actions to the planner + for (const customAction of customActions) { + planner.addAction(customAction.action, customAction.parameters) + } + } else { + planner.addAction(Actions.SWAP_EXACT_IN_SINGLE, [ + { + poolKey: pool.poolKey, + zeroForOne, + amountIn: amountIn.toString(), + amountOutMinimum: amountOutMinimum.toString(), + hookData: '0x', + }, + ]) + + const currencyIn = zeroForOne ? pool.currency0 : pool.currency1 + const currencyOut = zeroForOne ? pool.currency1 : pool.currency0 + + // Add settle and take actions for default behavior + planner.addSettle(currencyIn, true) + planner.addTake(currencyOut, recipient) + } let commands = ethers.utils.solidityPack(['uint8'], [COMMANDS.V4_SWAP]) @@ -125,7 +76,6 @@ export async function buildSwapCallData( // Combine actions and params into a single bytes array to match with V4_SWAP command input let inputs = [ - // V4_SWAP input ethers.utils.defaultAbiCoder.encode(['bytes', 'bytes[]'], [planner.actions, planner.params]), ] diff --git a/src/utils/getPool.ts b/src/utils/getPool.ts index 19fb7fc..423eecb 100644 --- a/src/utils/getPool.ts +++ b/src/utils/getPool.ts @@ -1,58 +1,49 @@ import { Pool } from '@uniswap/v4-sdk' -import { slice, zeroAddress } from 'viem' -import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' +import { zeroAddress } from 'viem' import V4StateViewAbi from '@/constants/abis/V4StateView' import { getTickSpacingForFee } from '@/helpers/fees' +import { getTokens } from '@/utils/getTokens' import { sortTokens } from '@/helpers/tokens' import type { UniDevKitV4Instance } from '@/types/core' -import { FeeTier, type PoolParams } from '@/types/utils/getPool' -import { getTokens } from '@/utils/getTokens' +import type { PoolArgs } from '@/types/utils/getPool' export const DEFAULT_HOOKS = zeroAddress /** - * Retrieves a Uniswap V4 pool instance for a given token pair, fee tier, tick spacing, and hooks configuration. - * @param params Pool parameters including tokens, fee tier, tick spacing, and hooks configuration + * Retrieves a Uniswap V4 pool instance for a given currency pair, fee tier, tick spacing, and hooks configuration. + * @param args Pool arguments including currencyA, currencyB, fee tier, tick spacing, and hooks configuration * @param instance UniDevKitV4Instance * @returns Promise resolving to pool data * @throws Error if SDK instance or token instances are not found or if pool data is not found */ -export async function getPool(params: PoolParams, instance: UniDevKitV4Instance): Promise { - const { client, contracts } = instance - const { positionManager, stateView } = contracts - - const { tokens, fee = FeeTier.MEDIUM, tickSpacing, hooks = DEFAULT_HOOKS } = params - - // Use provided tick spacing or derive from fee tier - const finalTickSpacing = tickSpacing ?? getTickSpacingForFee(fee) +export async function getPool(args: PoolArgs, instance: UniDevKitV4Instance): Promise { + const { currencyA, currencyB, fee, tickSpacing, hooks = DEFAULT_HOOKS } = args - const [token0, token1] = sortTokens(tokens[0], tokens[1]) + const [_currencyA, _currencyB] = sortTokens(currencyA, currencyB) const tokenInstances = await getTokens( { - addresses: [token0, token1], + addresses: [_currencyA, _currencyB], }, instance, ) + // Use provided tick spacing or derive from fee tier + const _tickSpacing = tickSpacing ?? getTickSpacingForFee(fee) + const poolId32Bytes = Pool.getPoolId( tokenInstances[0], tokenInstances[1], fee, - finalTickSpacing, + _tickSpacing, hooks, ) as `0x${string}` - const poolId25Bytes = slice(poolId32Bytes, 0, 25) as `0x${string}` + const { client, contracts } = instance + const { stateView } = contracts const poolData = await client.multicall({ allowFailure: false, contracts: [ - { - address: positionManager, - abi: V4PositionManagerAbi, - functionName: 'poolKeys', - args: [poolId25Bytes], - }, { address: stateView, abi: V4StateViewAbi, @@ -72,8 +63,8 @@ export async function getPool(params: PoolParams, instance: UniDevKitV4Instance) throw new Error('Failed to fetch pool data') } - const [poolKeysData, slot0Data, liquidityData] = poolData - const poolExists = poolKeysData && Number(poolKeysData[3]) > 0 && slot0Data && liquidityData + const [slot0Data, liquidityData] = poolData + const poolExists = slot0Data && liquidityData if (!poolExists) { throw new Error('Pool does not exist') @@ -84,11 +75,11 @@ export async function getPool(params: PoolParams, instance: UniDevKitV4Instance) tokenInstances[0], tokenInstances[1], fee, - finalTickSpacing, + _tickSpacing, hooks, slot0Data[0].toString(), liquidityData.toString(), - Number(slot0Data[1]), + slot0Data[1], ) return pool diff --git a/src/utils/getPoolKeyFromPoolId.ts b/src/utils/getPoolKeyFromPoolId.ts index 67b8d52..e89c555 100644 --- a/src/utils/getPoolKeyFromPoolId.ts +++ b/src/utils/getPoolKeyFromPoolId.ts @@ -1,7 +1,6 @@ import type { PoolKey } from '@uniswap/v4-sdk' import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' import type { UniDevKitV4Instance } from '@/types/core' -import type { GetPoolKeyFromPoolIdParams } from '@/types/utils/getPoolKeyFromPoolId' /** * Retrieves the pool key information for a given pool ID. @@ -10,13 +9,13 @@ import type { GetPoolKeyFromPoolIdParams } from '@/types/utils/getPoolKeyFromPoo * @throws Error if SDK instance is not found */ export async function getPoolKeyFromPoolId( - params: GetPoolKeyFromPoolIdParams, + poolId: string, instance: UniDevKitV4Instance, ): Promise { const { client, contracts } = instance const { positionManager } = contracts - const poolId25Bytes = `0x${params.poolId.slice(2, 52)}` as `0x${string}` + const poolId25Bytes = `0x${poolId.slice(2, 52)}` as `0x${string}` const [currency0, currency1, fee, tickSpacing, hooks] = await client.readContract({ address: positionManager, diff --git a/src/utils/getPosition.ts b/src/utils/getPosition.ts index 3661175..bc494b5 100644 --- a/src/utils/getPosition.ts +++ b/src/utils/getPosition.ts @@ -3,7 +3,11 @@ import V4PositionManagerAbi from '@/constants/abis/V4PositionMananger' import V4StateViewAbi from '@/constants/abis/V4StateView' import { decodePositionInfo } from '@/helpers/positions' import type { UniDevKitV4Instance } from '@/types/core' -import type { GetPositionParams, GetPositionResponse } from '@/types/utils/getPosition' +import type { + GetPositionDetailsResponse, + GetPositionParams, + GetPositionResponse, +} from '@/types/utils/getPosition' import { getTokens } from '@/utils/getTokens' /** @@ -19,28 +23,12 @@ export async function getPosition( ): Promise { const { client, contracts } = instance - const { positionManager, stateView } = contracts + const { stateView } = contracts - // Fetch poolKey and raw position info - const [poolAndPositionInfo, liquidity] = await client.multicall({ - allowFailure: false, - contracts: [ - { - address: positionManager, - abi: V4PositionManagerAbi, - functionName: 'getPoolAndPositionInfo', - args: [BigInt(params.tokenId)], - }, - { - address: positionManager, - abi: V4PositionManagerAbi, - functionName: 'getPositionLiquidity', - args: [BigInt(params.tokenId)], - }, - ], - }) + // Get position details using the dedicated function + const positionDetails = await getPositionDetails(params.tokenId, instance) - const [poolKey, rawInfo] = poolAndPositionInfo + const { poolKey, liquidity, tickLower, tickUpper } = positionDetails const { currency0, currency1, fee, tickSpacing, hooks } = poolKey if (liquidity === 0n) { @@ -49,7 +37,7 @@ export async function getPosition( const tokens = await getTokens( { - addresses: [currency0, currency1], + addresses: [currency0 as `0x${string}`, currency1 as `0x${string}`], }, instance, ) @@ -93,8 +81,6 @@ export async function getPosition( tick, ) - const { tickLower, tickUpper } = decodePositionInfo(rawInfo) - const position = new V4Position({ pool, liquidity: liquidity.toString(), @@ -111,3 +97,47 @@ export async function getPosition( tokenId: params.tokenId, } } + +/** + * Retrieves a Uniswap V4 position instance for a given token ID. + * @param params Position parameters including token ID + * @param instance UniDevKitV4Instance + * @returns Promise + */ +export async function getPositionDetails( + tokenId: string, + instance: UniDevKitV4Instance, +): Promise { + const { client, contracts } = instance + + const { positionManager } = contracts + + // Fetch poolKey and raw position info + const [poolAndPositionInfo, liquidity] = await client.multicall({ + allowFailure: false, + contracts: [ + { + address: positionManager, + abi: V4PositionManagerAbi, + functionName: 'getPoolAndPositionInfo', + args: [BigInt(tokenId)], + }, + { + address: positionManager, + abi: V4PositionManagerAbi, + functionName: 'getPositionLiquidity', + args: [BigInt(tokenId)], + }, + ], + }) + + const positionInfo = decodePositionInfo(poolAndPositionInfo[1]) + + return { + tokenId, + tickLower: positionInfo.tickLower, + tickUpper: positionInfo.tickUpper, + liquidity, + poolKey: poolAndPositionInfo[0], + } +} diff --git a/src/utils/getQuote.ts b/src/utils/getQuote.ts index c0d96a0..6d235d1 100644 --- a/src/utils/getQuote.ts +++ b/src/utils/getQuote.ts @@ -1,6 +1,6 @@ import V4QuoterAbi from '@/constants/abis/V4Quoter' import type { UniDevKitV4Instance } from '@/types/core' -import type { QuoteParams, QuoteResponse } from '@/types/utils/getQuote' +import type { SwapExactInSingle, QuoteResponse } from '@/types/utils/getQuote' /** * Fetches a quote for a token swap using the V4 Quoter contract. @@ -14,28 +14,26 @@ import type { QuoteParams, QuoteResponse } from '@/types/utils/getQuote' * - Contract call reverts */ export async function getQuote( - params: QuoteParams, + params: SwapExactInSingle, instance: UniDevKitV4Instance, ): Promise { const { client, contracts } = instance const { quoter } = contracts - const { - pool: { poolKey }, - } = params try { // Build the parameters for quoteExactInputSingle + // Using SwapExactInSingle structure directly from Uniswap V4 SDK const quoteParams = { poolKey: { - currency0: poolKey.currency0 as `0x${string}`, - currency1: poolKey.currency1 as `0x${string}`, - fee: poolKey.fee, - tickSpacing: poolKey.tickSpacing, - hooks: poolKey.hooks as `0x${string}`, + currency0: params.poolKey.currency0 as `0x${string}`, + currency1: params.poolKey.currency1 as `0x${string}`, + fee: params.poolKey.fee, + tickSpacing: params.poolKey.tickSpacing, + hooks: params.poolKey.hooks as `0x${string}`, }, zeroForOne: params.zeroForOne, - exactAmount: params.amountIn, - hookData: params.hookData || '0x', + exactAmount: BigInt(params.amountIn), + hookData: (params.hookData || '0x') as `0x${string}`, } // Simulate the quote to estimate the amount out @@ -47,7 +45,7 @@ export async function getQuote( }) // Extract the results - const [amountOut, gasEstimate] = simulation.result as [bigint, bigint] + const [amountOut, gasEstimate] = simulation.result return { amountOut, diff --git a/src/utils/getTokens.ts b/src/utils/getTokens.ts index ede8352..d198cec 100644 --- a/src/utils/getTokens.ts +++ b/src/utils/getTokens.ts @@ -1,7 +1,7 @@ import { type Currency, Ether, Token } from '@uniswap/sdk-core' import { erc20Abi, zeroAddress } from 'viem' import type { UniDevKitV4Instance } from '@/types/core' -import type { GetTokensParams } from '@/types/utils/getTokens' +import type { GetTokensArgs } from '@/types/utils/getTokens' /** * Retrieves Token instances for a list of token addresses on a specific chain. @@ -11,7 +11,7 @@ import type { GetTokensParams } from '@/types/utils/getTokens' * @throws Error if token data cannot be fetched */ export async function getTokens( - params: GetTokensParams, + params: GetTokensArgs, instance: UniDevKitV4Instance, ): Promise { const { addresses } = params diff --git a/src/utils/preparePermit2BatchData.ts b/src/utils/preparePermit2BatchData.ts index 49d7ec9..2f54a45 100644 --- a/src/utils/preparePermit2BatchData.ts +++ b/src/utils/preparePermit2BatchData.ts @@ -56,11 +56,11 @@ import { zeroAddress } from 'viem' /** */ import type { UniDevKitV4Instance } from '@/types' import type { - PreparePermit2BatchDataParams, + PreparePermit2BatchDataArgs, PreparePermit2BatchDataResult, } from '@/types/utils/permit2' export async function preparePermit2BatchData( - params: PreparePermit2BatchDataParams, + params: PreparePermit2BatchDataArgs, instance: UniDevKitV4Instance, ): Promise { const { tokens, spender, owner, sigDeadline: sigDeadlineParam } = params diff --git a/src/utils/preparePermit2Data.ts b/src/utils/preparePermit2Data.ts index f8bd7fe..4f0b1e3 100644 --- a/src/utils/preparePermit2Data.ts +++ b/src/utils/preparePermit2Data.ts @@ -8,7 +8,31 @@ import type { TypedDataField } from 'ethers' import type { Address, Hex } from 'viem' import { zeroAddress } from 'viem' import type { UniDevKitV4Instance } from '@/types' -import type { PreparePermit2DataParams, PreparePermit2DataResult } from '@/types/utils/permit2' +import type { PreparePermit2DataArgs, PreparePermit2DataResult } from '@/types/utils/permit2' + +export const allowanceAbi = [ + { + name: 'allowance', + type: 'function', + stateMutability: 'view', + inputs: [ + { name: 'owner', type: 'address' }, + { name: 'token', type: 'address' }, + { name: 'spender', type: 'address' }, + ], + outputs: [ + { + components: [ + { name: 'amount', type: 'uint160' }, + { name: 'expiration', type: 'uint48' }, + { name: 'nonce', type: 'uint48' }, + ], + name: 'details', + type: 'tuple', + }, + ], + }, +] as const /** * Prepares the permit2 data for a single token @@ -57,51 +81,27 @@ import type { PreparePermit2DataParams, PreparePermit2DataResult } from '@/types * @throws Error if any required dependencies are missing */ export async function preparePermit2Data( - params: PreparePermit2DataParams, + params: PreparePermit2DataArgs, instance: UniDevKitV4Instance, ): Promise { const { token, spender, owner, sigDeadline: sigDeadlineParam } = params - const chainId = instance.chain.id + if (token.toLowerCase() === zeroAddress.toLowerCase()) { + throw new Error('Native tokens are not supported for permit2') + } + // calculate sigDeadline if not provided let sigDeadline = sigDeadlineParam if (!sigDeadline) { const blockTimestamp = await instance.client.getBlock().then((block) => block.timestamp) - sigDeadline = Number(blockTimestamp + 60n * 60n) // 30 minutes from current block timestamp } - if (token.toLowerCase() === zeroAddress.toLowerCase()) { - throw new Error('Native tokens are not supported for permit2') - } - // Fetch allowance details for each token const details = await instance.client.readContract({ address: PERMIT2_ADDRESS as `0x${string}`, - abi: [ - { - name: 'allowance', - type: 'function', - stateMutability: 'view', - inputs: [ - { name: 'owner', type: 'address' }, - { name: 'token', type: 'address' }, - { name: 'spender', type: 'address' }, - ], - outputs: [ - { - components: [ - { name: 'amount', type: 'uint160' }, - { name: 'expiration', type: 'uint48' }, - { name: 'nonce', type: 'uint48' }, - ], - name: 'details', - type: 'tuple', - }, - ], - }, - ] as const, + abi: allowanceAbi, functionName: 'allowance', args: [owner as `0x${string}`, token as `0x${string}`, spender as `0x${string}`], })