diff --git a/package.json b/package.json index 84349e9..eba5751 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "uniswap-dev-kit", - "version": "1.0.7", + "version": "1.0.8", "description": "A modern TypeScript library for integrating Uniswap into your dapp.", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -58,10 +58,10 @@ "@biomejs/biome": "1.9.4", "@semantic-release/changelog": "^6.0.3", "@semantic-release/commit-analyzer": "^13.0.1", + "@semantic-release/git": "^10.0.1", "@semantic-release/github": "^11.0.2", "@semantic-release/npm": "^12.0.1", "@semantic-release/release-notes-generator": "^14.0.3", - "@semantic-release/git": "^10.0.1", "@testing-library/jest-dom": "^6.6.3", "@testing-library/react": "^14.2.1", "@types/node": "^22.15.17", @@ -82,6 +82,7 @@ "@uniswap/permit2-sdk": "^1.3.1", "@uniswap/router-sdk": "^2.0.3", "@uniswap/sdk-core": "^7.7.2", + "@uniswap/universal-router-sdk": "^4.19.5", "@uniswap/v3-sdk": "^3.25.2", "@uniswap/v4-sdk": "^1.21.4", "ethers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 70a51a0..6825aeb 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -20,6 +20,9 @@ importers: '@uniswap/sdk-core': specifier: ^7.7.2 version: 7.7.2 + '@uniswap/universal-router-sdk': + specifier: ^4.19.5 + version: 4.19.5(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.8.3)(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.8.3)(utf-8-validate@5.0.10)) @@ -847,6 +850,12 @@ 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' @@ -1216,6 +1225,14 @@ packages: resolution: {integrity: sha512-mh/YNbwKb7Mut96VuEtL+Z5bRe0xVIbjjiryn+iMMrK2sFKhR4duk/86mEz0UO5gSx4pQIw9G5276P5heY/7Rg==} engines: {node: '>=10'} + '@uniswap/universal-router-sdk@4.19.5': + resolution: {integrity: sha512-FpfvTrJnvxhLdfX1N+GqOfAbHEsbFHP7J3JbpSAFWl3QDW63BB1/rqgW3E17xQha+WnPdP7uMLHwe7oi6mr39Q==} + 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'} @@ -1538,6 +1555,9 @@ 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'} @@ -5374,6 +5394,10 @@ 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': {} @@ -6014,6 +6038,31 @@ snapshots: transitivePeerDependencies: - hardhat + '@uniswap/universal-router-sdk@4.19.5(bufferutil@4.0.9)(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.8.3)(utf-8-validate@5.0.10))(utf-8-validate@5.0.10)': + dependencies: + '@openzeppelin/contracts': 4.7.0 + '@uniswap/permit2-sdk': 1.3.1(bufferutil@4.0.9)(utf-8-validate@5.0.10) + '@uniswap/router-sdk': 2.0.3(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.8.3)(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.8.3)(utf-8-validate@5.0.10)) + '@uniswap/v4-sdk': 1.21.4(hardhat@2.24.0(bufferutil@4.0.9)(typescript@5.8.3)(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': @@ -6810,6 +6859,8 @@ snapshots: big.js@6.2.2: {} + bignumber.js@9.3.0: {} + binary-extensions@2.3.0: {} bn.js@4.12.2: {} diff --git a/src/constants/abis/Permit2.ts b/src/constants/abis/Permit2.ts new file mode 100644 index 0000000..53b42e7 --- /dev/null +++ b/src/constants/abis/Permit2.ts @@ -0,0 +1,518 @@ +export default [ + { + inputs: [{ internalType: "uint256", name: "deadline", type: "uint256" }], + name: "AllowanceExpired", + type: "error", + }, + { inputs: [], name: "ExcessiveInvalidation", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + name: "InsufficientAllowance", + type: "error", + }, + { + inputs: [{ internalType: "uint256", name: "maxAmount", type: "uint256" }], + name: "InvalidAmount", + type: "error", + }, + { inputs: [], name: "InvalidContractSignature", type: "error" }, + { inputs: [], name: "InvalidNonce", type: "error" }, + { inputs: [], name: "InvalidSignature", type: "error" }, + { inputs: [], name: "InvalidSignatureLength", type: "error" }, + { inputs: [], name: "InvalidSigner", type: "error" }, + { inputs: [], name: "LengthMismatch", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "signatureDeadline", type: "uint256" }, + ], + name: "SignatureExpired", + type: "error", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint160", + name: "amount", + type: "uint160", + }, + { + indexed: false, + internalType: "uint48", + name: "expiration", + type: "uint48", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: false, + internalType: "address", + name: "spender", + type: "address", + }, + ], + name: "Lockdown", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint48", + name: "newNonce", + type: "uint48", + }, + { + indexed: false, + internalType: "uint48", + name: "oldNonce", + type: "uint48", + }, + ], + name: "NonceInvalidation", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "token", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { + indexed: false, + internalType: "uint160", + name: "amount", + type: "uint160", + }, + { + indexed: false, + internalType: "uint48", + name: "expiration", + type: "uint48", + }, + { indexed: false, internalType: "uint48", name: "nonce", type: "uint48" }, + ], + name: "Permit", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "word", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "mask", + type: "uint256", + }, + ], + name: "UnorderedNonceInvalidation", + type: "event", + }, + { + inputs: [], + name: "DOMAIN_SEPARATOR", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + { internalType: "address", name: "", type: "address" }, + ], + name: "allowance", + outputs: [ + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + ], + name: "approve", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint48", name: "newNonce", type: "uint48" }, + ], + name: "invalidateNonces", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "uint256", name: "wordPos", type: "uint256" }, + { internalType: "uint256", name: "mask", type: "uint256" }, + ], + name: "invalidateUnorderedNonces", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + ], + internalType: "struct IAllowanceTransfer.TokenSpenderPair[]", + name: "approvals", + type: "tuple[]", + }, + ], + name: "lockdown", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "", type: "address" }, + { internalType: "uint256", name: "", type: "uint256" }, + ], + name: "nonceBitmap", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + internalType: "struct IAllowanceTransfer.PermitDetails[]", + name: "details", + type: "tuple[]", + }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "sigDeadline", type: "uint256" }, + ], + internalType: "struct IAllowanceTransfer.PermitBatch", + name: "permitBatch", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "uint48", name: "expiration", type: "uint48" }, + { internalType: "uint48", name: "nonce", type: "uint48" }, + ], + internalType: "struct IAllowanceTransfer.PermitDetails", + name: "details", + type: "tuple", + }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "sigDeadline", type: "uint256" }, + ], + internalType: "struct IAllowanceTransfer.PermitSingle", + name: "permitSingle", + type: "tuple", + }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permit", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions[]", + name: "permitted", + type: "tuple[]", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitBatchTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails[]", + name: "transferDetails", + type: "tuple[]", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions", + name: "permitted", + type: "tuple", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails", + name: "transferDetails", + type: "tuple", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes32", name: "witness", type: "bytes32" }, + { internalType: "string", name: "witnessTypeString", type: "string" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitWitnessTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { + components: [ + { internalType: "address", name: "token", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.TokenPermissions[]", + name: "permitted", + type: "tuple[]", + }, + { internalType: "uint256", name: "nonce", type: "uint256" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.PermitBatchTransferFrom", + name: "permit", + type: "tuple", + }, + { + components: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "requestedAmount", type: "uint256" }, + ], + internalType: "struct ISignatureTransfer.SignatureTransferDetails[]", + name: "transferDetails", + type: "tuple[]", + }, + { internalType: "address", name: "owner", type: "address" }, + { internalType: "bytes32", name: "witness", type: "bytes32" }, + { internalType: "string", name: "witnessTypeString", type: "string" }, + { internalType: "bytes", name: "signature", type: "bytes" }, + ], + name: "permitWitnessTransferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "address", name: "token", type: "address" }, + ], + internalType: "struct IAllowanceTransfer.AllowanceTransferDetails[]", + name: "transferDetails", + type: "tuple[]", + }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint160", name: "amount", type: "uint160" }, + { internalType: "address", name: "token", type: "address" }, + ], + name: "transferFrom", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/constants/abis/V4PoolManager.ts b/src/constants/abis/V4PoolManager.ts new file mode 100644 index 0000000..462e72d --- /dev/null +++ b/src/constants/abis/V4PoolManager.ts @@ -0,0 +1,693 @@ +export default [ + { + inputs: [ + { internalType: "address", name: "initialOwner", type: "address" }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "AlreadyUnlocked", type: "error" }, + { + inputs: [ + { internalType: "address", name: "currency0", type: "address" }, + { internalType: "address", name: "currency1", type: "address" }, + ], + name: "CurrenciesOutOfOrderOrEqual", + type: "error", + }, + { inputs: [], name: "CurrencyNotSettled", type: "error" }, + { inputs: [], name: "DelegateCallNotAllowed", type: "error" }, + { inputs: [], name: "InvalidCaller", type: "error" }, + { inputs: [], name: "ManagerLocked", type: "error" }, + { inputs: [], name: "MustClearExactPositiveDelta", type: "error" }, + { inputs: [], name: "NonzeroNativeValue", type: "error" }, + { inputs: [], name: "PoolNotInitialized", type: "error" }, + { inputs: [], name: "ProtocolFeeCurrencySynced", type: "error" }, + { + inputs: [{ internalType: "uint24", name: "fee", type: "uint24" }], + name: "ProtocolFeeTooLarge", + type: "error", + }, + { inputs: [], name: "SwapAmountCannotBeZero", type: "error" }, + { + inputs: [{ internalType: "int24", name: "tickSpacing", type: "int24" }], + name: "TickSpacingTooLarge", + type: "error", + }, + { + inputs: [{ internalType: "int24", name: "tickSpacing", type: "int24" }], + name: "TickSpacingTooSmall", + type: "error", + }, + { inputs: [], name: "UnauthorizedDynamicLPFeeUpdate", type: "error" }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "spender", + type: "address", + }, + { indexed: true, internalType: "uint256", name: "id", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Approval", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "PoolId", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "uint256", + name: "amount0", + type: "uint256", + }, + { + indexed: false, + internalType: "uint256", + name: "amount1", + type: "uint256", + }, + ], + name: "Donate", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "PoolId", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "Currency", + name: "currency0", + type: "address", + }, + { + indexed: true, + internalType: "Currency", + name: "currency1", + type: "address", + }, + { indexed: false, internalType: "uint24", name: "fee", type: "uint24" }, + { + indexed: false, + internalType: "int24", + name: "tickSpacing", + type: "int24", + }, + { + indexed: false, + internalType: "contract IHooks", + name: "hooks", + type: "address", + }, + { + indexed: false, + internalType: "uint160", + name: "sqrtPriceX96", + type: "uint160", + }, + { indexed: false, internalType: "int24", name: "tick", type: "int24" }, + ], + name: "Initialize", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "PoolId", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "int24", + name: "tickLower", + type: "int24", + }, + { + indexed: false, + internalType: "int24", + name: "tickUpper", + type: "int24", + }, + { + indexed: false, + internalType: "int256", + name: "liquidityDelta", + type: "int256", + }, + { + indexed: false, + internalType: "bytes32", + name: "salt", + type: "bytes32", + }, + ], + name: "ModifyLiquidity", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "owner", + type: "address", + }, + { + indexed: true, + internalType: "address", + name: "operator", + type: "address", + }, + { indexed: false, internalType: "bool", name: "approved", type: "bool" }, + ], + name: "OperatorSet", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "address", name: "user", type: "address" }, + { + indexed: true, + internalType: "address", + name: "newOwner", + type: "address", + }, + ], + name: "OwnershipTransferred", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: true, + internalType: "address", + name: "protocolFeeController", + type: "address", + }, + ], + name: "ProtocolFeeControllerUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "PoolId", name: "id", type: "bytes32" }, + { + indexed: false, + internalType: "uint24", + name: "protocolFee", + type: "uint24", + }, + ], + name: "ProtocolFeeUpdated", + type: "event", + }, + { + anonymous: false, + inputs: [ + { indexed: true, internalType: "PoolId", name: "id", type: "bytes32" }, + { + indexed: true, + internalType: "address", + name: "sender", + type: "address", + }, + { + indexed: false, + internalType: "int128", + name: "amount0", + type: "int128", + }, + { + indexed: false, + internalType: "int128", + name: "amount1", + type: "int128", + }, + { + indexed: false, + internalType: "uint160", + name: "sqrtPriceX96", + type: "uint160", + }, + { + indexed: false, + internalType: "uint128", + name: "liquidity", + type: "uint128", + }, + { indexed: false, internalType: "int24", name: "tick", type: "int24" }, + { indexed: false, internalType: "uint24", name: "fee", type: "uint24" }, + ], + name: "Swap", + type: "event", + }, + { + anonymous: false, + inputs: [ + { + indexed: false, + internalType: "address", + name: "caller", + type: "address", + }, + { indexed: true, internalType: "address", name: "from", type: "address" }, + { indexed: true, internalType: "address", name: "to", type: "address" }, + { indexed: true, internalType: "uint256", name: "id", type: "uint256" }, + { + indexed: false, + internalType: "uint256", + name: "amount", + type: "uint256", + }, + ], + name: "Transfer", + type: "event", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + ], + name: "allowance", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "spender", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "approve", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + ], + name: "balanceOf", + outputs: [{ internalType: "uint256", name: "balance", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "from", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "burn", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "Currency", name: "currency", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "clear", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "recipient", type: "address" }, + { internalType: "Currency", name: "currency", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "collectProtocolFees", + outputs: [ + { internalType: "uint256", name: "amountCollected", type: "uint256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { internalType: "uint256", name: "amount0", type: "uint256" }, + { internalType: "uint256", name: "amount1", type: "uint256" }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "donate", + outputs: [{ internalType: "BalanceDelta", name: "delta", type: "int256" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "slot", type: "bytes32" }], + name: "extsload", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes32", name: "startSlot", type: "bytes32" }, + { internalType: "uint256", name: "nSlots", type: "uint256" }, + ], + name: "extsload", + outputs: [{ internalType: "bytes32[]", name: "", type: "bytes32[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32[]", name: "slots", type: "bytes32[]" }], + name: "extsload", + outputs: [{ internalType: "bytes32[]", name: "", type: "bytes32[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32[]", name: "slots", type: "bytes32[]" }], + name: "exttload", + outputs: [{ internalType: "bytes32[]", name: "", type: "bytes32[]" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "bytes32", name: "slot", type: "bytes32" }], + name: "exttload", + outputs: [{ internalType: "bytes32", name: "", type: "bytes32" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { internalType: "uint160", name: "sqrtPriceX96", type: "uint160" }, + ], + name: "initialize", + outputs: [{ internalType: "int24", name: "tick", type: "int24" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "owner", type: "address" }, + { internalType: "address", name: "operator", type: "address" }, + ], + name: "isOperator", + outputs: [{ internalType: "bool", name: "isOperator", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "mint", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { + components: [ + { internalType: "int24", name: "tickLower", type: "int24" }, + { internalType: "int24", name: "tickUpper", type: "int24" }, + { internalType: "int256", name: "liquidityDelta", type: "int256" }, + { internalType: "bytes32", name: "salt", type: "bytes32" }, + ], + internalType: "struct IPoolManager.ModifyLiquidityParams", + name: "params", + type: "tuple", + }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "modifyLiquidity", + outputs: [ + { internalType: "BalanceDelta", name: "callerDelta", type: "int256" }, + { internalType: "BalanceDelta", name: "feesAccrued", type: "int256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "owner", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "protocolFeeController", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [{ internalType: "Currency", name: "currency", type: "address" }], + name: "protocolFeesAccrued", + outputs: [{ internalType: "uint256", name: "amount", type: "uint256" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "operator", type: "address" }, + { internalType: "bool", name: "approved", type: "bool" }, + ], + name: "setOperator", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { internalType: "uint24", name: "newProtocolFee", type: "uint24" }, + ], + name: "setProtocolFee", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "controller", type: "address" }], + name: "setProtocolFeeController", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [], + name: "settle", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "recipient", type: "address" }], + name: "settleFor", + outputs: [{ internalType: "uint256", name: "", type: "uint256" }], + stateMutability: "payable", + type: "function", + }, + { + inputs: [{ internalType: "bytes4", name: "interfaceId", type: "bytes4" }], + name: "supportsInterface", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { + components: [ + { internalType: "bool", name: "zeroForOne", type: "bool" }, + { internalType: "int256", name: "amountSpecified", type: "int256" }, + { + internalType: "uint160", + name: "sqrtPriceLimitX96", + type: "uint160", + }, + ], + internalType: "struct IPoolManager.SwapParams", + name: "params", + type: "tuple", + }, + { internalType: "bytes", name: "hookData", type: "bytes" }, + ], + name: "swap", + outputs: [ + { internalType: "BalanceDelta", name: "swapDelta", type: "int256" }, + ], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "Currency", name: "currency", type: "address" }], + name: "sync", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "Currency", name: "currency", type: "address" }, + { internalType: "address", name: "to", type: "address" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "take", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transfer", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { internalType: "address", name: "sender", type: "address" }, + { internalType: "address", name: "receiver", type: "address" }, + { internalType: "uint256", name: "id", type: "uint256" }, + { internalType: "uint256", name: "amount", type: "uint256" }, + ], + name: "transferFrom", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "address", name: "newOwner", type: "address" }], + name: "transferOwnership", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes", name: "data", type: "bytes" }], + name: "unlock", + outputs: [{ internalType: "bytes", name: "result", type: "bytes" }], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [ + { + components: [ + { internalType: "Currency", name: "currency0", type: "address" }, + { internalType: "Currency", name: "currency1", type: "address" }, + { internalType: "uint24", name: "fee", type: "uint24" }, + { internalType: "int24", name: "tickSpacing", type: "int24" }, + { internalType: "contract IHooks", name: "hooks", type: "address" }, + ], + internalType: "struct PoolKey", + name: "key", + type: "tuple", + }, + { internalType: "uint24", name: "newDynamicLPFee", type: "uint24" }, + ], + name: "updateDynamicLPFee", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, +] as const; diff --git a/src/constants/abis/V4PositionMananger.ts b/src/constants/abis/V4PositionMananger.ts index 157c395..afdba41 100644 --- a/src/constants/abis/V4PositionMananger.ts +++ b/src/constants/abis/V4PositionMananger.ts @@ -1,4 +1,4 @@ -export const V4PositionManagerAbi = [ +export default [ { inputs: [ { diff --git a/src/constants/abis/V4Quoter.ts b/src/constants/abis/V4Quoter.ts index 980a4c2..5b0f3ef 100644 --- a/src/constants/abis/V4Quoter.ts +++ b/src/constants/abis/V4Quoter.ts @@ -1,4 +1,4 @@ -export const V4QuoterAbi = [ +export default [ { inputs: [ { diff --git a/src/constants/abis/V4StateView.ts b/src/constants/abis/V4StateView.ts index 6184543..6fbb1c2 100644 --- a/src/constants/abis/V4StateView.ts +++ b/src/constants/abis/V4StateView.ts @@ -1,4 +1,4 @@ -export const V4StateViewAbi = [ +export default [ { inputs: [ { diff --git a/src/constants/abis/V4UniversalRouter.ts b/src/constants/abis/V4UniversalRouter.ts new file mode 100644 index 0000000..27c4bfd --- /dev/null +++ b/src/constants/abis/V4UniversalRouter.ts @@ -0,0 +1,206 @@ +export default [ + { + inputs: [ + { + components: [ + { internalType: "address", name: "permit2", type: "address" }, + { internalType: "address", name: "weth9", type: "address" }, + { internalType: "address", name: "v2Factory", type: "address" }, + { internalType: "address", name: "v3Factory", type: "address" }, + { + internalType: "bytes32", + name: "pairInitCodeHash", + type: "bytes32", + }, + { + internalType: "bytes32", + name: "poolInitCodeHash", + type: "bytes32", + }, + { internalType: "address", name: "v4PoolManager", type: "address" }, + { + internalType: "address", + name: "v3NFTPositionManager", + type: "address", + }, + { + internalType: "address", + name: "v4PositionManager", + type: "address", + }, + ], + internalType: "struct RouterParameters", + name: "params", + type: "tuple", + }, + ], + stateMutability: "nonpayable", + type: "constructor", + }, + { inputs: [], name: "BalanceTooLow", type: "error" }, + { inputs: [], name: "ContractLocked", type: "error" }, + { + inputs: [{ internalType: "Currency", name: "currency", type: "address" }], + name: "DeltaNotNegative", + type: "error", + }, + { + inputs: [{ internalType: "Currency", name: "currency", type: "address" }], + name: "DeltaNotPositive", + type: "error", + }, + { inputs: [], name: "ETHNotAccepted", type: "error" }, + { + inputs: [ + { internalType: "uint256", name: "commandIndex", type: "uint256" }, + { internalType: "bytes", name: "message", type: "bytes" }, + ], + name: "ExecutionFailed", + type: "error", + }, + { inputs: [], name: "FromAddressIsNotOwner", type: "error" }, + { inputs: [], name: "InputLengthMismatch", type: "error" }, + { inputs: [], name: "InsufficientBalance", type: "error" }, + { inputs: [], name: "InsufficientETH", type: "error" }, + { inputs: [], name: "InsufficientToken", type: "error" }, + { + inputs: [{ internalType: "bytes4", name: "action", type: "bytes4" }], + name: "InvalidAction", + type: "error", + }, + { inputs: [], name: "InvalidBips", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "commandType", type: "uint256" }], + name: "InvalidCommandType", + type: "error", + }, + { inputs: [], name: "InvalidEthSender", type: "error" }, + { inputs: [], name: "InvalidPath", type: "error" }, + { inputs: [], name: "InvalidReserves", type: "error" }, + { inputs: [], name: "LengthMismatch", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "tokenId", type: "uint256" }], + name: "NotAuthorizedForToken", + type: "error", + }, + { inputs: [], name: "NotPoolManager", type: "error" }, + { inputs: [], name: "OnlyMintAllowed", type: "error" }, + { inputs: [], name: "SliceOutOfBounds", type: "error" }, + { inputs: [], name: "TransactionDeadlinePassed", type: "error" }, + { inputs: [], name: "UnsafeCast", type: "error" }, + { + inputs: [{ internalType: "uint256", name: "action", type: "uint256" }], + name: "UnsupportedAction", + type: "error", + }, + { inputs: [], name: "V2InvalidPath", type: "error" }, + { inputs: [], name: "V2TooLittleReceived", type: "error" }, + { inputs: [], name: "V2TooMuchRequested", type: "error" }, + { inputs: [], name: "V3InvalidAmountOut", type: "error" }, + { inputs: [], name: "V3InvalidCaller", type: "error" }, + { inputs: [], name: "V3InvalidSwap", type: "error" }, + { inputs: [], name: "V3TooLittleReceived", type: "error" }, + { inputs: [], name: "V3TooMuchRequested", type: "error" }, + { + inputs: [ + { + internalType: "uint256", + name: "minAmountOutReceived", + type: "uint256", + }, + { internalType: "uint256", name: "amountReceived", type: "uint256" }, + ], + name: "V4TooLittleReceived", + type: "error", + }, + { + inputs: [ + { + internalType: "uint256", + name: "maxAmountInRequested", + type: "uint256", + }, + { internalType: "uint256", name: "amountRequested", type: "uint256" }, + ], + name: "V4TooMuchRequested", + type: "error", + }, + { + inputs: [], + name: "V3_POSITION_MANAGER", + outputs: [ + { + internalType: "contract INonfungiblePositionManager", + name: "", + type: "address", + }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "V4_POSITION_MANAGER", + outputs: [ + { internalType: "contract IPositionManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "commands", type: "bytes" }, + { internalType: "bytes[]", name: "inputs", type: "bytes[]" }, + ], + name: "execute", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [ + { internalType: "bytes", name: "commands", type: "bytes" }, + { internalType: "bytes[]", name: "inputs", type: "bytes[]" }, + { internalType: "uint256", name: "deadline", type: "uint256" }, + ], + name: "execute", + outputs: [], + stateMutability: "payable", + type: "function", + }, + { + inputs: [], + name: "msgSender", + outputs: [{ internalType: "address", name: "", type: "address" }], + stateMutability: "view", + type: "function", + }, + { + inputs: [], + name: "poolManager", + outputs: [ + { internalType: "contract IPoolManager", name: "", type: "address" }, + ], + stateMutability: "view", + type: "function", + }, + { + inputs: [ + { internalType: "int256", name: "amount0Delta", type: "int256" }, + { internalType: "int256", name: "amount1Delta", type: "int256" }, + { internalType: "bytes", name: "data", type: "bytes" }, + ], + name: "uniswapV3SwapCallback", + outputs: [], + stateMutability: "nonpayable", + type: "function", + }, + { + inputs: [{ internalType: "bytes", name: "data", type: "bytes" }], + name: "unlockCallback", + outputs: [{ internalType: "bytes", name: "", type: "bytes" }], + stateMutability: "nonpayable", + type: "function", + }, + { stateMutability: "payable", type: "receive" }, +] as const; diff --git a/src/core/uniDevKitV4.ts b/src/core/uniDevKitV4.ts index daaf8f4..20be7d7 100644 --- a/src/core/uniDevKitV4.ts +++ b/src/core/uniDevKitV4.ts @@ -1,4 +1,5 @@ import { getChainById } from "@/constants/chains"; +import type { BuildSwapCallDataParams } from "@/types"; import type { UniDevKitV4Config, UniDevKitV4Instance } from "@/types/core"; import type { PoolParams } from "@/types/utils/getPool"; import type { GetPoolKeyFromPoolIdParams } from "@/types/utils/getPoolKeyFromPoolId"; @@ -8,6 +9,7 @@ import type { } from "@/types/utils/getPosition"; import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; import type { GetTokensParams } from "@/types/utils/getTokens"; +import { buildSwapCallData } from "@/utils/buildSwapCallData"; import { getPool } from "@/utils/getPool"; import { getPoolKeyFromPoolId } from "@/utils/getPoolKeyFromPoolId"; import { getPosition } from "@/utils/getPosition"; @@ -15,12 +17,8 @@ import { getQuote } from "@/utils/getQuote"; import { getTokens } from "@/utils/getTokens"; import type { Token } from "@uniswap/sdk-core"; import type { Pool, PoolKey } from "@uniswap/v4-sdk"; -import { - http, - type Address, - type PublicClient, - createPublicClient, -} from "viem"; +import type { Abi, Address, Hex, PublicClient } from "viem"; +import { http, createPublicClient } from "viem"; /** * Main class for interacting with Uniswap V4 contracts. @@ -87,6 +85,62 @@ export class UniDevKitV4 { return address; } + /** + * Loads the ABI for a specific contract using dynamic imports. + * This method is used internally to lazy load ABIs only when needed. + * @param name - The name of the contract to load the ABI for + * @returns Promise resolving to the contract's ABI + * @throws Will throw an error if the contract ABI is not found + * @private + */ + private async loadAbi( + name: keyof UniDevKitV4Config["contracts"], + ): Promise { + const abiMap: Record< + keyof UniDevKitV4Config["contracts"], + () => Promise | null + > = { + permit2: () => import("@/constants/abis/Permit2").then((m) => m.default), + 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; + } + + /** + * Retrieves the ABI for a specific contract. + * This method uses dynamic imports to load ABIs on demand, reducing the initial bundle size. + * @param name - The name of the contract (e.g., "poolManager", "quoter") + * @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'); + * ``` + */ + async getContractAbi( + name: keyof UniDevKitV4Config["contracts"], + ): Promise { + return this.loadAbi(name); + } + /** * Retrieves a Uniswap V4 pool instance for a given token pair. * @param params Pool parameters including tokens, fee tier, tick spacing, and hooks configuration @@ -132,4 +186,8 @@ export class UniDevKitV4 { ): Promise { return getPoolKeyFromPoolId(params, this.instance); } + + async buildSwapCallData(params: BuildSwapCallDataParams): Promise { + return buildSwapCallData(params, this.instance); + } } diff --git a/src/helpers/swap.ts b/src/helpers/swap.ts new file mode 100644 index 0000000..33fc330 --- /dev/null +++ b/src/helpers/swap.ts @@ -0,0 +1,23 @@ +import { Percent } from "@uniswap/sdk-core"; + +/** + * Utility function to calculate minimum output amount based on slippage + * Uses Uniswap SDK's Percent class for accurate calculations + * + * @param expectedOutput - Expected output amount + * @param slippageTolerance - Slippage tolerance in basis points + * @returns Minimum output amount accounting for slippage + */ +export function calculateMinimumOutput( + expectedOutput: bigint, + slippageTolerance: number, +): bigint { + // Use SDK's Percent class for precise slippage calculations + const slippagePercent = new Percent(slippageTolerance, 10_000); + const slippageDecimal = Number.parseFloat(slippagePercent.toFixed(18)); + const slippageAmount = BigInt( + Math.floor(Number(expectedOutput) * slippageDecimal), + ); + + return expectedOutput - slippageAmount; +} diff --git a/src/test/utils/buildSwapCallData.test.ts b/src/test/utils/buildSwapCallData.test.ts new file mode 100644 index 0000000..377b049 --- /dev/null +++ b/src/test/utils/buildSwapCallData.test.ts @@ -0,0 +1,105 @@ +import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; +import { buildSwapCallData } from "@/utils/buildSwapCallData"; +import * as getQuoteModule from "@/utils/getQuote"; +import { Token } from "@uniswap/sdk-core"; +import { Pool } from "@uniswap/v4-sdk"; +import { type Address, zeroAddress } from "viem"; +import { describe, expect, it, vi } from "vitest"; + +const sdkInstance = createMockSdkInstance(); + +// Mock getQuote to return a fixed amount +vi.spyOn(getQuoteModule, "getQuote").mockImplementation(async () => ({ + amountOut: BigInt(1000000000000000000), // 1 WETH + estimatedGasUsed: BigInt(100000), + timestamp: Date.now(), +})); + +describe("buildSwapCallData", () => { + // USDC and WETH on Mainnet + const mockTokens: [Address, Address] = [ + "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + "0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2", // WETH + ]; + + const mockTokenInstances = [ + new Token(1, mockTokens[0], 6, "USDC", "USD Coin"), + new Token(1, mockTokens[1], 18, "WETH", "Wrapped Ether"), + ]; + + const mockPool = new Pool( + mockTokenInstances[0], + mockTokenInstances[1], + 3000, + 60, + zeroAddress, + "79228162514264337593543950336", + "1000000", + 0, + ); + + it("should build swap calldata for token0 to token1", async () => { + const params = { + tokenIn: mockTokens[0], + amountIn: BigInt(1000000), // 1 USDC + slippageTolerance: 50, + pool: mockPool, + }; + + const calldata = await buildSwapCallData(params, sdkInstance); + expect(calldata).toBeDefined(); + expect(calldata).toMatch(/^0x/); // Should be a hex string + }); + + it("should build swap calldata for token1 to token0", async () => { + const params = { + tokenIn: mockTokens[1], + amountIn: BigInt(1000000000000000000), // 1 WETH + slippageTolerance: 50, + pool: mockPool, + }; + + const calldata = await buildSwapCallData(params, sdkInstance); + expect(calldata).toBeDefined(); + expect(calldata).toMatch(/^0x/); + }); + + it("should include minimum output amount in calldata", async () => { + const params = { + tokenIn: mockTokens[0], + amountIn: BigInt(1000000), + slippageTolerance: 50, + pool: mockPool, + }; + + const calldata = await buildSwapCallData(params, sdkInstance); + expect(calldata).toBeDefined(); + expect(calldata).toMatch(/^0x/); + }); + + it("should handle zero minimum output amount", async () => { + const params = { + tokenIn: mockTokens[0], + amountIn: BigInt(1000000), + slippageTolerance: 50, + pool: mockPool, + }; + + const calldata = await buildSwapCallData(params, sdkInstance); + expect(calldata).toBeDefined(); + expect(calldata).toMatch(/^0x/); + }); + + it("should include deadline in calldata", async () => { + const params = { + tokenIn: mockTokens[0], + amountIn: BigInt(1000000), + slippageTolerance: 50, + pool: mockPool, + }; + + const calldata = await buildSwapCallData(params, sdkInstance); + expect(calldata).toBeDefined(); + expect(calldata).toMatch(/^0x/); + }); +}); diff --git a/src/test/utils/getQuote.test.ts b/src/test/utils/getQuote.test.ts index f0ffcad..558b8fd 100644 --- a/src/test/utils/getQuote.test.ts +++ b/src/test/utils/getQuote.test.ts @@ -1,9 +1,20 @@ import { createMockSdkInstance } from "@/test/helpers/sdkInstance"; import { getQuote } from "@/utils/getQuote"; +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"; +const mockPool: Pool = { + poolKey: { + currency0: "0x123", + currency1: "0x456", + fee: 3000, + tickSpacing: 10, + hooks: "0x", + }, +} as Pool; + describe("getQuote", () => { it("should throw error if SDK instance not found", async () => { const mockDeps = createMockSdkInstance(); @@ -14,10 +25,9 @@ describe("getQuote", () => { await expect( getQuote( { - tokens: ["0x123", "0x456"], - zeroForOne: true, + pool: mockPool, amountIn: BigInt(1000000), - feeTier: 3000, + zeroForOne: true, }, mockDeps, ), @@ -32,10 +42,9 @@ describe("getQuote", () => { const result = await getQuote( { - tokens: ["0x123", "0x456"], - zeroForOne: true, + pool: mockPool, amountIn: BigInt(1000000), - feeTier: 3000, + zeroForOne: true, }, mockDeps, ); diff --git a/src/types/utils/buildPermit2CallData.ts b/src/types/utils/buildPermit2CallData.ts new file mode 100644 index 0000000..000e621 --- /dev/null +++ b/src/types/utils/buildPermit2CallData.ts @@ -0,0 +1,84 @@ +import type { Address } from "viem"; + +/** + * Token permissions structure for Permit2 SignatureTransfer + * @public + */ +export interface TokenPermissions { + /** The token contract address */ + token: Address; + /** The amount to be transferred */ + amount: bigint; +} + +/** + * Permit data structure for SignatureTransfer operations + * @public + */ +export interface PermitTransferFrom { + /** Token and amount permissions */ + permitted: TokenPermissions; + /** Unique nonce for replay protection */ + nonce: bigint; + /** Unix timestamp when the permit expires */ + deadline: bigint; +} + +/** + * EIP-712 typed data structure for PermitTransferFrom signatures + * @public + */ +export interface PermitTransferFromTypedData { + /** EIP-712 domain separator */ + domain: { + /** Domain name */ + name: string; + /** Domain version */ + version: string; + /** Chain ID */ + chainId: number; + /** Verifying contract address */ + verifyingContract: Address; + }; + /** EIP-712 type definitions */ + types: { + /** PermitTransferFrom type definition */ + PermitTransferFrom: readonly [ + { readonly name: "permitted"; readonly type: "TokenPermissions" }, + { readonly name: "spender"; readonly type: "address" }, + { readonly name: "nonce"; readonly type: "uint256" }, + { readonly name: "deadline"; readonly type: "uint256" }, + ]; + /** TokenPermissions type definition */ + TokenPermissions: readonly [ + { readonly name: "token"; readonly type: "address" }, + { readonly name: "amount"; readonly type: "uint256" }, + ]; + }; + /** Primary type for signing */ + primaryType: "PermitTransferFrom"; + /** Message data to be signed */ + message: PermitTransferFrom & { + /** Address authorized to execute the transfer */ + spender: Address; + }; +} + +/** + * Permit parameters for building typed data + * @public + */ +export interface PermitParams { + /** Token contract address */ + token: Address; + /** Amount to be permitted for transfer */ + amount: bigint; + /** Token owner address */ + owner: Address; + /** Address authorized to execute the transfer */ + spender: Address; + /** Optional unique nonce for replay protection (auto-fetched if not provided) */ + nonce?: bigint; + /** Optional deadline in seconds from now (defaults to 30 minutes) */ + deadlineSeconds?: number; +} diff --git a/src/types/utils/buildSwapCallData.ts b/src/types/utils/buildSwapCallData.ts new file mode 100644 index 0000000..9f85762 --- /dev/null +++ b/src/types/utils/buildSwapCallData.ts @@ -0,0 +1,27 @@ +import type { Pool } from "@uniswap/v4-sdk"; +import type { Address } from "viem"; + +/** + * Command codes for Universal Router operations + * @see https://docs.uniswap.org/contracts/universal-router/technical-reference + */ +export const COMMANDS = { + SWAP_EXACT_IN_SINGLE: 0x06, + SETTLE_ALL: 0x0c, + TAKE_ALL: 0x0f, + V4_ROUTER_EXECUTE: 0x10, +} as const; + +/** + * 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) */ + amountIn: bigint; + /** Pool */ + pool: Pool; + /** Slippage tolerance in basis points (e.g., 50 = 0.5%). Defaults to 50 (0.5%) */ + slippageTolerance?: number; +}; diff --git a/src/types/utils/getPoolKeyFromPoolId.ts b/src/types/utils/getPoolKeyFromPoolId.ts index db67ccd..36134d4 100644 --- a/src/types/utils/getPoolKeyFromPoolId.ts +++ b/src/types/utils/getPoolKeyFromPoolId.ts @@ -5,19 +5,3 @@ export interface GetPoolKeyFromPoolIdParams { /** The 32-byte pool ID in hex format (0x...) */ poolId: `0x${string}`; } - -/** - * Pool key information returned from the contract - */ -export interface PoolKey { - /** First token address in the pool */ - currency0: `0x${string}`; - /** Second token address in the pool */ - currency1: `0x${string}`; - /** Fee tier of the pool */ - fee: number; - /** Tick spacing of the pool */ - tickSpacing: number; - /** Hooks contract address */ - hooks: `0x${string}`; -} diff --git a/src/types/utils/getQuote.ts b/src/types/utils/getQuote.ts index b96ac2e..71a7cb0 100644 --- a/src/types/utils/getQuote.ts +++ b/src/types/utils/getQuote.ts @@ -1,24 +1,14 @@ -import type { FeeTier } from "@/types/utils/getPool"; -import type { Address, Hex } from "viem"; +import type { Pool } from "@uniswap/v4-sdk"; +import type { Hex } from "viem"; /** * Parameters required for fetching a quote using the V4 Quoter contract. */ export interface QuoteParams { /** - * Array of two token addresses representing the pair. The order will be handled internally. + * The pool instance to quote from */ - tokens: [Address, Address]; - - /** - * The fee tier of the pool (e.g., FeeTier.MEDIUM). - */ - feeTier: FeeTier; - - /** - * The tick spacing for the pool. If not provided, it will be derived from the fee tier. - */ - tickSpacing?: number; + pool: Pool; /** * The amount of tokens being swapped, expressed as a bigint. @@ -30,11 +20,6 @@ export interface QuoteParams { */ zeroForOne: boolean; - /** - * Address of the hooks contract, if any. Defaults to zero address if not provided. - */ - hooks?: Address; - /** * Optional additional data for the hooks, if any. */ diff --git a/src/types/utils/index.ts b/src/types/utils/index.ts index 772a1ce..0899b8f 100644 --- a/src/types/utils/index.ts +++ b/src/types/utils/index.ts @@ -1,3 +1,4 @@ +export * from "@/types/utils/buildSwapCallData"; export * from "@/types/utils/getPool"; export * from "@/types/utils/getPoolKeyFromPoolId"; export * from "@/types/utils/getPosition"; diff --git a/src/utils/buildSwapCallData.ts b/src/utils/buildSwapCallData.ts new file mode 100644 index 0000000..a13930d --- /dev/null +++ b/src/utils/buildSwapCallData.ts @@ -0,0 +1,132 @@ +import { calculateMinimumOutput } from "@/helpers/swap"; +import type { BuildSwapCallDataParams } from "@/types"; +import { COMMANDS } from "@/types"; +import type { UniDevKitV4Instance } from "@/types/core"; +import { getQuote } from "@/utils/getQuote"; +import { ethers } from "ethers"; +import type { Hex } from "viem"; + +/** + * 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. + * + * @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 + * const swapParams = { + * tokenIn: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC + * amountIn: parseUnits("100", 6), // 100 USDC + * pool: pool, + * slippageTolerance: 50, // 0.5% + * }; + * + * const calldata = await buildSwapCallData(swapParams); + * + * // Send transaction + * const tx = await sendTransaction({ + * to: universalRouterAddress, + * data: calldata, + * value: 0, + * }); + * ``` + */ +export async function buildSwapCallData( + params: BuildSwapCallDataParams, + instance: UniDevKitV4Instance, +): Promise { + // Extract and set default parameters + const { tokenIn, amountIn, pool, slippageTolerance = 50 } = 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 amountOutMin = calculateMinimumOutput( + quote.amountOut, + slippageTolerance, + ); + + // Encode Universal Router commands + const commands = ethers.utils.solidityPack( + ["uint8"], + [COMMANDS.V4_ROUTER_EXECUTE], + ); + + // Encode swap actions sequence + const actions = ethers.utils.solidityPack( + ["uint8", "uint8", "uint8"], + [COMMANDS.SWAP_EXACT_IN_SINGLE, COMMANDS.SETTLE_ALL, COMMANDS.TAKE_ALL], + ); + + // Encode swap parameters + const exactInputSingleParams = ethers.utils.defaultAbiCoder.encode( + [ + "tuple(tuple(address currency0, address currency1, uint24 fee, int24 tickSpacing, address hooks) poolKey, bool zeroForOne, uint128 amountIn, uint128 amountOutMinimum, bytes hookData)", + ], + [ + { + poolKey: pool.poolKey, + zeroForOne, + amountIn: ethers.BigNumber.from(amountIn.toString()), + amountOutMinimum: amountOutMin, + hookData: "0x", + }, + ], + ); + + // Encode token amounts for settlement + const swapParams = [ + exactInputSingleParams, + ethers.utils.defaultAbiCoder.encode( + ["address", "uint128"], + zeroForOne + ? [pool.poolKey.currency0, amountIn] + : [pool.poolKey.currency1, amountIn], + ), + ethers.utils.defaultAbiCoder.encode( + ["address", "uint128"], + zeroForOne ? [pool.poolKey.currency1, 0] : [pool.poolKey.currency0, 0], + ), + ]; + + // Encode final inputs + const inputs = [ + ethers.utils.defaultAbiCoder.encode( + ["bytes", "bytes[]"], + [actions, swapParams], + ), + ]; + + // Set 5-minute deadline + const deadline = BigInt(Math.floor(Date.now() / 1000) + 60 * 5); + + // Create Universal Router interface + const universalRouterInterface = new ethers.utils.Interface([ + "function execute(bytes commands, bytes[] inputs, uint256 deadline)", + ]); + + // Encode final calldata + return universalRouterInterface.encodeFunctionData("execute", [ + commands, + inputs, + deadline, + ]) as Hex; +} diff --git a/src/utils/getPool.ts b/src/utils/getPool.ts index 2800158..fae7672 100644 --- a/src/utils/getPool.ts +++ b/src/utils/getPool.ts @@ -1,5 +1,5 @@ -import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; -import { V4StateViewAbi } from "@/constants/abis/V4StateView"; +import V4PositionManagerAbi from "@/constants/abis/V4PositionMananger"; +import V4StateViewAbi from "@/constants/abis/V4StateView"; import { getTickSpacingForFee } from "@/helpers/fees"; import { sortTokens } from "@/helpers/tokens"; import type { UniDevKitV4Instance } from "@/types/core"; @@ -8,7 +8,7 @@ import { getTokens } from "@/utils/getTokens"; import { Pool } from "@uniswap/v4-sdk"; import { slice, zeroAddress } from "viem"; -const DEFAULT_HOOKS = zeroAddress; +export const DEFAULT_HOOKS = zeroAddress; /** * Retrieves a Uniswap V4 pool instance for a given token pair, fee tier, tick spacing, and hooks configuration. diff --git a/src/utils/getPoolKeyFromPoolId.ts b/src/utils/getPoolKeyFromPoolId.ts index f1123d5..2906043 100644 --- a/src/utils/getPoolKeyFromPoolId.ts +++ b/src/utils/getPoolKeyFromPoolId.ts @@ -1,9 +1,7 @@ -import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; +import V4PositionManagerAbi from "@/constants/abis/V4PositionMananger"; import type { UniDevKitV4Instance } from "@/types/core"; -import type { - GetPoolKeyFromPoolIdParams, - PoolKey, -} from "@/types/utils/getPoolKeyFromPoolId"; +import type { GetPoolKeyFromPoolIdParams } from "@/types/utils/getPoolKeyFromPoolId"; +import type { PoolKey } from "@uniswap/v4-sdk"; /** * Retrieves the pool key information for a given pool ID. diff --git a/src/utils/getPosition.ts b/src/utils/getPosition.ts index dc3554d..69151c5 100644 --- a/src/utils/getPosition.ts +++ b/src/utils/getPosition.ts @@ -1,5 +1,5 @@ -import { V4PositionManagerAbi } from "@/constants/abis/V4PositionMananger"; -import { V4StateViewAbi } from "@/constants/abis/V4StateView"; +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 { diff --git a/src/utils/getQuote.ts b/src/utils/getQuote.ts index e4a3dac..9b1aad2 100644 --- a/src/utils/getQuote.ts +++ b/src/utils/getQuote.ts @@ -1,20 +1,15 @@ -import { V4QuoterAbi } from "@/constants/abis/V4Quoter"; -import { sortTokens } from "@/helpers/tokens"; +import V4QuoterAbi from "@/constants/abis/V4Quoter"; import type { UniDevKitV4Instance } from "@/types/core"; -import { FeeTier, TICK_SPACING_BY_FEE } from "@/types/utils/getPool"; import type { QuoteParams, QuoteResponse } from "@/types/utils/getQuote"; -import { zeroAddress } from "viem"; /** * Fetches a quote for a token swap using the V4 Quoter contract. - * This function constructs the pool key from the given tokens and parameters, - * and then simulates the quote to estimate the output amount. + * This function uses the provided pool instance to simulate the quote. * - * @param params - The parameters required for the quote, including tokens, fee tier, tick spacing, and amount. - * @param chainId - (Optional) The chain ID to use. If only one instance is registered, this is not required. + * @param params - The parameters required for the quote, including pool and amount. + * @param instance - UniDevKitV4 instance for contract interaction * @returns A Promise that resolves to the quote result, including the amount out and gas estimate. * @throws Will throw an error if: - * - SDK instance is not found * - Simulation fails (e.g., insufficient liquidity, invalid parameters) * - Contract call reverts */ @@ -24,30 +19,20 @@ export async function getQuote( ): Promise { const { client, contracts } = instance; const { quoter } = contracts; + const { + pool: { poolKey }, + } = params; try { - // Sort tokens to ensure consistent pool key ordering - const [currency0, currency1] = sortTokens( - params.tokens[0], - params.tokens[1], - ); - - // Use provided tick spacing or derive from fee tier - const fee = (params.feeTier ?? FeeTier.MEDIUM) as FeeTier; - const tickSpacing = params.tickSpacing ?? TICK_SPACING_BY_FEE[fee]; - - // Construct the poolKey - const poolKey = { - currency0, - currency1, - fee, - tickSpacing, - hooks: params.hooks || zeroAddress, - }; - // Build the parameters for quoteExactInputSingle const quoteParams = { - poolKey, + 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}`, + }, zeroForOne: params.zeroForOne, exactAmount: params.amountIn, hookData: params.hookData || "0x",