diff --git a/limit-order-api-demo/README.md b/limit-order-api-demo/README.md new file mode 100644 index 0000000..39b7672 --- /dev/null +++ b/limit-order-api-demo/README.md @@ -0,0 +1,27 @@ +# Pendle Limit Order API Demo + +This repository is designed for developers seeking to interact with Pendle Limit Order using TypeScript. For simplicity, the examples are implemented purely in Node.js so that users can focus on the backend logic required to achieve the following Limit Order operations: + +* Maker APIs: + - Generate limit order data (generateLimitOrderData()) + - Sign limit order (signLimitOrderData()) + - Post limit order to backend system (postLimitOrder()) + +* Taker APIs: + - Fetch limit orders to fill + - Fill a limit order + +## Getting Started +To run examples: +- Change directory to the demo project folder: `cd limit-order-api-demo` +- Install required dependencies with `npm install` +- Configure the ethers.js signer in `/src/libs/signer.ts` +- Execute the index.ts file using `npm run start` + +## Api specifications +* [Maker](https://api-v2.pendle.finance/limit-order/docs) +* [Taker](https://api-v2.pendle.finance/limit-order/docs) + +## Additional Notes + +Please be aware that the code examples provided in this repository are for educational purposes and are not intended for production use. They serve as a starting point for integrating Pendle Limit Order functionality into your decentralized application (dApp). diff --git a/limit-order-api-demo/package-lock.json b/limit-order-api-demo/package-lock.json new file mode 100644 index 0000000..ccb5b0f --- /dev/null +++ b/limit-order-api-demo/package-lock.json @@ -0,0 +1,374 @@ +{ + "name": "limit-order-api-demo", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "limit-order-api-demo", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "@types/node": "^20.12.7", + "axios": "^1.6.8", + "ethers": "^6.12.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } + }, + "node_modules/@adraffy/ens-normalize": { + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz", + "integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==" + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@noble/curves": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz", + "integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==", + "dependencies": { + "@noble/hashes": "1.3.2" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@noble/hashes": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz", + "integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==", + "engines": { + "node": ">= 16" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", + "integrity": "sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==" + }, + "node_modules/@types/node": { + "version": "20.12.7", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.12.7.tgz", + "integrity": "sha512-wq0cICSkRLVaf3UGLMGItu/PtdY7oaXaI/RVU+xliKVOtRna3PRY57ZDfztpDL0n11vfymMUnXv8QwYCO7L1wg==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/acorn": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.11.3.tgz", + "integrity": "sha512-Y9rRfJG5jcKOE0CLisYbojUjIrIEE7AGMzA/Sm4BslANhbS+cDMpgBdcPT91oJ7OuJ9hYJBx59RjbhxVnrF8Xg==", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.2.tgz", + "integrity": "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/aes-js": { + "version": "4.0.0-beta.5", + "resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz", + "integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==" + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, + "node_modules/axios": { + "version": "1.6.8", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.6.8.tgz", + "integrity": "sha512-v/ZHtJDU39mDpyBoFVkETcd/uNdxrWRrg3bKpOKzXFA6Bvqopts6ALSMU3y6ijYxbw2B+wPrIv46egTzJXCLGQ==", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/ethers": { + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/ethers/-/ethers-6.12.0.tgz", + "integrity": "sha512-zL5NlOTjML239gIvtVJuaSk0N9GQLi1Hom3ZWUszE5lDTQE/IVB62mrPkQ2W1bGcZwVGSLaetQbWNQSvI4rGDQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/ethers-io/" + }, + { + "type": "individual", + "url": "https://www.buymeacoffee.com/ricmoo" + } + ], + "dependencies": { + "@adraffy/ens-normalize": "1.10.1", + "@noble/curves": "1.2.0", + "@noble/hashes": "1.3.2", + "@types/node": "18.15.13", + "aes-js": "4.0.0-beta.5", + "tslib": "2.4.0", + "ws": "8.5.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ethers/node_modules/@types/node": { + "version": "18.15.13", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.15.13.tgz", + "integrity": "sha512-N+0kuo9KgrUQ1Sn/ifDXsvg0TTleP7rIy4zOBGECxAljqvqfqpTfzx0Q1NUedOixRMBfe2Whhb056a42cWs26Q==" + }, + "node_modules/follow-redirects": { + "version": "1.15.6", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", + "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==" + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.0.tgz", + "integrity": "sha512-d6xOpEDfsi2CZVlPQzGeux8XMwLT9hssAsaPYExaQMuYskwb+x1x7J371tWlbBdWHroy99KnVB6qIkUbs5X3UQ==" + }, + "node_modules/typescript": { + "version": "5.4.5", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.4.5.tgz", + "integrity": "sha512-vcI4UpRgg81oIRUFwR0WSIHKt11nJ7SAVlYNIu+QpqeyXP+gpQJy/Z4+F0aGxSE4MqwjyXvW/TzgkLAx2AGHwQ==", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==" + }, + "node_modules/ws": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.5.0.tgz", + "integrity": "sha512-BWX0SWVgLPzYwF8lTzEy1egjhS4S4OEAHfsO8o65WOVsrnSRGaSiUaa9e0ggGlkMTtBlmOpEXiie9RUcBO86qg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/limit-order-api-demo/package.json b/limit-order-api-demo/package.json new file mode 100644 index 0000000..3e75d2b --- /dev/null +++ b/limit-order-api-demo/package.json @@ -0,0 +1,19 @@ +{ + "name": "limit-order-api-demo", + "version": "1.0.0", + "description": "Pendle Limit Order API Demo", + "main": "index.js", + "scripts": { + "test": "echo \"Error: no test specified\" && exit 1", + "start": "npx ts-node ./src/index.ts" + }, + "author": "Dac Do Dinh", + "license": "ISC", + "dependencies": { + "@types/node": "^20.12.7", + "axios": "^1.6.8", + "ethers": "^6.12.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.5" + } +} diff --git a/limit-order-api-demo/src/abis/PendleLimitRouter.json b/limit-order-api-demo/src/abis/PendleLimitRouter.json new file mode 100644 index 0000000..884f866 --- /dev/null +++ b/limit-order-api-demo/src/abis/PendleLimitRouter.json @@ -0,0 +1,918 @@ +[ + { + "inputs": [ + { + "internalType": "address", + "name": "_WNATIVE", + "type": "address" + } + ], + "stateMutability": "nonpayable", + "type": "constructor" + }, + { + "inputs": [ + { + "internalType": "bool", + "name": "success", + "type": "bool" + }, + { + "internalType": "bytes", + "name": "res", + "type": "bytes" + } + ], + "name": "SimulationResults", + "type": "error" + }, + { + "anonymous": false, + "inputs": [], + "name": "EIP712DomainChanged", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": false, + "internalType": "uint8", + "name": "version", + "type": "uint8" + } + ], + "name": "Initialized", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "oldNonce", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "newNonce", + "type": "uint256" + } + ], + "name": "NonceIncreased", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + } + ], + "name": "OrderCanceled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "netInputFromMaker", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "netOutputToMaker", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "notionalVolume", + "type": "uint256" + } + ], + "name": "OrderFilled", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "bytes32", + "name": "orderHash", + "type": "bytes32" + }, + { + "indexed": true, + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "indexed": true, + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "netInputFromMaker", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "netOutputToMaker", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "feeAmount", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "uint256", + "name": "notionalVolume", + "type": "uint256" + }, + { + "indexed": false, + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "indexed": false, + "internalType": "address", + "name": "taker", + "type": "address" + } + ], + "name": "OrderFilledV2", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ + { + "indexed": true, + "internalType": "address", + "name": "previousOwner", + "type": "address" + }, + { + "indexed": true, + "internalType": "address", + "name": "newOwner", + "type": "address" + } + ], + "name": "OwnershipTransferred", + "type": "event" + }, + { + "inputs": [], + "name": "DOMAIN_SEPARATOR", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "uint8", + "name": "amount", + "type": "uint8" + } + ], + "name": "advanceNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lnImpliedRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "failSafeRate", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "internalType": "struct Order[]", + "name": "orders", + "type": "tuple[]" + } + ], + "name": "cancelBatch", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lnImpliedRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "failSafeRate", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "internalType": "struct Order", + "name": "order", + "type": "tuple" + } + ], + "name": "cancelSingle", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "claimOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [], + "name": "eip712Domain", + "outputs": [ + { + "internalType": "bytes1", + "name": "fields", + "type": "bytes1" + }, + { + "internalType": "string", + "name": "name", + "type": "string" + }, + { + "internalType": "string", + "name": "version", + "type": "string" + }, + { + "internalType": "uint256", + "name": "chainId", + "type": "uint256" + }, + { + "internalType": "address", + "name": "verifyingContract", + "type": "address" + }, + { + "internalType": "bytes32", + "name": "salt", + "type": "bytes32" + }, + { + "internalType": "uint256[]", + "name": "extensions", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "feeRecipient", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lnImpliedRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "failSafeRate", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "internalType": "struct Order", + "name": "order", + "type": "tuple" + }, + { + "internalType": "bytes", + "name": "signature", + "type": "bytes" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + } + ], + "internalType": "struct FillOrderParams[]", + "name": "params", + "type": "tuple[]" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "maxTaking", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "", + "type": "bytes" + }, + { + "internalType": "bytes", + "name": "callback", + "type": "bytes" + } + ], + "name": "fill", + "outputs": [ + { + "internalType": "uint256", + "name": "actualMaking", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "actualTaking", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "totalFee", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "callbackReturn", + "type": "bytes" + } + ], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "YT", + "type": "address" + } + ], + "name": "getLnFeeRateRoot", + "outputs": [ + { + "internalType": "uint256", + "name": "res", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "components": [ + { + "internalType": "uint256", + "name": "salt", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "expiry", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "nonce", + "type": "uint256" + }, + { + "internalType": "enum IPLimitOrderType.OrderType", + "name": "orderType", + "type": "uint8" + }, + { + "internalType": "address", + "name": "token", + "type": "address" + }, + { + "internalType": "address", + "name": "YT", + "type": "address" + }, + { + "internalType": "address", + "name": "maker", + "type": "address" + }, + { + "internalType": "address", + "name": "receiver", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makingAmount", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "lnImpliedRate", + "type": "uint256" + }, + { + "internalType": "uint256", + "name": "failSafeRate", + "type": "uint256" + }, + { + "internalType": "bytes", + "name": "permit", + "type": "bytes" + } + ], + "internalType": "struct Order", + "name": "order", + "type": "tuple" + } + ], + "name": "hashOrder", + "outputs": [ + { + "internalType": "bytes32", + "name": "", + "type": "bytes32" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "increaseNonce", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeRecipient", + "type": "address" + } + ], + "name": "initialize", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "name": "nonce", + "outputs": [ + { + "internalType": "uint256", + "name": "", + "type": "uint256" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "makerAddress", + "type": "address" + }, + { + "internalType": "uint256", + "name": "makerNonce", + "type": "uint256" + } + ], + "name": "nonceEquals", + "outputs": [ + { + "internalType": "bool", + "name": "", + "type": "bool" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "orderHashes", + "type": "bytes32[]" + } + ], + "name": "orderStatuses", + "outputs": [ + { + "internalType": "uint256[]", + "name": "remainings", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "filledAmounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "bytes32[]", + "name": "orderHashes", + "type": "bytes32[]" + } + ], + "name": "orderStatusesRaw", + "outputs": [ + { + "internalType": "uint256[]", + "name": "remainingsRaw", + "type": "uint256[]" + }, + { + "internalType": "uint256[]", + "name": "filledAmounts", + "type": "uint256[]" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "owner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [], + "name": "pendingOwner", + "outputs": [ + { + "internalType": "address", + "name": "", + "type": "address" + } + ], + "stateMutability": "view", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "_feeRecipient", + "type": "address" + } + ], + "name": "setFeeRecipient", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address[]", + "name": "YTs", + "type": "address[]" + }, + { + "internalType": "uint256[]", + "name": "lnFeeRateRoots", + "type": "uint256[]" + } + ], + "name": "setLnFeeRateRoots", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "target", + "type": "address" + }, + { + "internalType": "bytes", + "name": "data", + "type": "bytes" + } + ], + "name": "simulate", + "outputs": [], + "stateMutability": "payable", + "type": "function" + }, + { + "inputs": [ + { + "internalType": "address", + "name": "newOwner", + "type": "address" + }, + { + "internalType": "bool", + "name": "direct", + "type": "bool" + }, + { + "internalType": "bool", + "name": "renounce", + "type": "bool" + } + ], + "name": "transferOwnership", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function" + }, + { + "stateMutability": "payable", + "type": "receive" + } +] diff --git a/limit-order-api-demo/src/index.ts b/limit-order-api-demo/src/index.ts new file mode 100644 index 0000000..619e167 --- /dev/null +++ b/limit-order-api-demo/src/index.ts @@ -0,0 +1,62 @@ +import { cancelSingleOrder } from './makers/cancel-orders'; +import { generateLimitOrderData } from './makers/generate-limit-order-data'; +import { getMakerActiveLimitOrders } from './makers/get-active-orders'; +import { increaseNonce } from './makers/increase-nonce'; +import { postLimitOrder } from './makers/post-limit-order'; +import { signLimitOrderData } from './makers/sign-limit-order'; +import { fromBackendSignedLimitOrderToOrderStruct } from './share/types'; +import { fillLimitOrders } from './takers/fill-limit-orders'; +import { getLimitOrders } from './takers/get-limit-orders'; + +/** + * MAKER + */ +async function makeLimitOrderExample() { + // Step 1: Generate limit order data + const limitOrderData = await generateLimitOrderData(); + + // Step 2: Sign signature + const signature = await signLimitOrderData(limitOrderData); + + // Step 3: Post limit order to limit order system + await postLimitOrder(limitOrderData, signature); +} +// void makeLimitOrderExample(); + +async function cancelOrderExample() { + // Step 1: Get active orders from limit order's backend + const limitOrders = await getMakerActiveLimitOrders() + if (limitOrders.length == 0) { + throw new Error('Maker does not have any active orders'); + } + + console.log(limitOrders) + + // Step 2: Cancel the order + // In this example, we will cancel the first order fetched from backend + // await cancelSingleOrder(fromBackendSignedLimitOrderToOrderStruct(limitOrders[0])) +} +// void cancelOrderExample() + +async function increaseNonceExample() { + // Order need to have nonce >= maker's nonce to be able to be filled. So increase nonce will help you to + // deactivate all your current orders (suppose that you always make orders with your current nonce) + await increaseNonce() +} +void increaseNonceExample() + +/** + * TAKER + */ +async function fetchAndFillOrderExample() { + // Step 1: Fetch the limit orders + const limitOrdersData = await getLimitOrders(); + console.debug(limitOrdersData); + // Step 2: Fill the first fetched order + if (limitOrdersData.length == 0) { + throw new Error('Can not find any order to fill'); + } + const orderToFill = limitOrdersData[0]; + await fillLimitOrders([orderToFill]); +} +// void fetchAndFillOrderExample(); diff --git a/limit-order-api-demo/src/makers/cancel-orders.ts b/limit-order-api-demo/src/makers/cancel-orders.ts new file mode 100644 index 0000000..1d4d2dd --- /dev/null +++ b/limit-order-api-demo/src/makers/cancel-orders.ts @@ -0,0 +1,30 @@ +import { getSigner } from "../share/signer"; +import { OrderStruct } from "../share/types"; +import { Contract } from "ethers"; +import { LIMIT_ORDER_CONTRACTS } from "../share/constants"; +import abi from '../abis/PendleLimitRouter.json'; + + +export async function cancelSingleOrder(order: OrderStruct) { + const signer = getSigner(); + const contract = new Contract(LIMIT_ORDER_CONTRACTS.ARBITRUM, abi, signer); + + const tx = await contract.cancelSingle(order); + await tx.wait(); + + console.debug(tx); + + console.log('Finished cancelling the order') +} + +export async function cancelBatchOrders(orders: OrderStruct[]) { + const signer = getSigner(); + const contract = new Contract(LIMIT_ORDER_CONTRACTS.ARBITRUM, abi, signer); + + const tx = await contract.cancelBatch(orders); + await tx.wait(); + + console.debug(tx); + + console.log('Finished cancelling the orders') +} diff --git a/limit-order-api-demo/src/makers/generate-limit-order-data.ts b/limit-order-api-demo/src/makers/generate-limit-order-data.ts new file mode 100644 index 0000000..844a562 --- /dev/null +++ b/limit-order-api-demo/src/makers/generate-limit-order-data.ts @@ -0,0 +1,54 @@ +import { ChainId, LIMIT_ORDER_DOMAIN, aUSDC_MARKET } from '../share/constants'; +import { LimitOrderType } from '../share/types'; +import { getSigner } from '../share/signer'; +import axios from 'axios'; + +export interface GenerateLimitOrderDataRequest { + chainId: number; + YT: string; + orderType: LimitOrderType; + token: string; + maker: string; + makingAmount: string; + impliedApy: number; + expiry: string; +} + +export interface GeneratedLimitOrderData extends GenerateLimitOrderDataRequest { + salt: string; + nonce: string; + failSafeRate: string; + receiver: string; + lnImpliedRate: string; + permit: string; +} + +export async function generateLimitOrderData() { + const targetPath = '/v1/makers/generate-limit-order-data'; + + const signerAddress = await getSigner().getAddress(); + + const requestBody: GenerateLimitOrderDataRequest = { + chainId: ChainId.ARBITRUM, + YT: aUSDC_MARKET.yt, + maker: signerAddress, + orderType: LimitOrderType.TOKEN_FOR_PT, + token: aUSDC_MARKET.tokenIn.usdc, // Use USDC as token in to swap to PT + makingAmount: '10000000', // 10 USDC + impliedApy: 0.1, // 10% implied APY + expiry: String(Math.floor(Date.now() / 1000) + 20 * 60), // order will be expired in 20 minutes + }; + + console.debug(requestBody); + + try { + console.log(`\nGetting the limit order data...`); + const { data } = await axios.post(LIMIT_ORDER_DOMAIN + targetPath, requestBody); + + console.debug(data); + + return data as GeneratedLimitOrderData; + } catch (error) { + throw error; + } +} diff --git a/limit-order-api-demo/src/makers/get-active-orders.ts b/limit-order-api-demo/src/makers/get-active-orders.ts new file mode 100644 index 0000000..ae4749a --- /dev/null +++ b/limit-order-api-demo/src/makers/get-active-orders.ts @@ -0,0 +1,35 @@ +import axios from 'axios'; +import { getSigner } from "../share/signer"; +import { ChainId, LIMIT_ORDER_DOMAIN } from "../share/constants"; +import { BackendSignedLimitOrder, LimitOrderType } from "../share/types"; + +export interface LimitOrderMakerQuery { + skip: number; + limit: number; + chainId: number; + maker: string; + yt?: string; + type?: LimitOrderType; + isActive?: boolean; +} + +export async function getMakerActiveLimitOrders(): Promise { + const targetPath = '/v1/makers/limit-orders' + console.log('Fetching limit order...') + + const requestQuery: LimitOrderMakerQuery = { + skip: 0, + limit: 10, + chainId: ChainId.ARBITRUM, + maker: await getSigner().getAddress(), + isActive: true + } + + console.debug(requestQuery) + + const {data} = await axios.get(LIMIT_ORDER_DOMAIN + targetPath, {params: requestQuery}) + + console.log(`\nFinished fetching limit orders`); + + return data.results as BackendSignedLimitOrder[] +} diff --git a/limit-order-api-demo/src/makers/increase-nonce.ts b/limit-order-api-demo/src/makers/increase-nonce.ts new file mode 100644 index 0000000..931ca88 --- /dev/null +++ b/limit-order-api-demo/src/makers/increase-nonce.ts @@ -0,0 +1,18 @@ +import { getSigner } from "../share/signer"; +import { OrderStruct } from "../share/types"; +import { Contract } from "ethers"; +import { LIMIT_ORDER_CONTRACTS } from "../share/constants"; +import abi from '../abis/PendleLimitRouter.json'; + + +export async function increaseNonce() { + const signer = getSigner(); + const contract = new Contract(LIMIT_ORDER_CONTRACTS.ARBITRUM, abi, signer); + + const tx = await contract.increaseNonces(); + await tx.wait(); + + console.debug(tx); + + console.log('Finished cancelling the order') +} diff --git a/limit-order-api-demo/src/makers/post-limit-order.ts b/limit-order-api-demo/src/makers/post-limit-order.ts new file mode 100644 index 0000000..58ace33 --- /dev/null +++ b/limit-order-api-demo/src/makers/post-limit-order.ts @@ -0,0 +1,38 @@ +import { LimitOrderType } from 'src/share/types'; +import { GeneratedLimitOrderData } from './generate-limit-order-data'; +import axios from 'axios'; +import { LIMIT_ORDER_DOMAIN } from '../share/constants'; + +// The difference between CreateLimitOrderRequest and GeneratedLimitOrderData is the name of fields YT and orderType +// and CreateLimitOrderRequest has signature +export interface CreateLimitOrderRequest extends Omit { + yt: string; + type: LimitOrderType; + signature: string; +} + +export async function postLimitOrder(generatedLimitOrderData: GeneratedLimitOrderData, signature: string) { + const targetPath = '/v1/makers/limit-orders'; + + console.log(`\nPreparing the create limit order request...`); + + const requestBody: CreateLimitOrderRequest = { + ...generatedLimitOrderData, + yt: generatedLimitOrderData.YT, + type: generatedLimitOrderData.orderType, + signature, + }; + + console.debug(requestBody); + + try { + console.log(`\nPosting the limit order...`); + const { data } = await axios.post(LIMIT_ORDER_DOMAIN + targetPath, requestBody); + + console.log(data); + + console.log(`\nPosted the limit order successfully`); + } catch (error) { + throw error; + } +} diff --git a/limit-order-api-demo/src/makers/sign-limit-order.ts b/limit-order-api-demo/src/makers/sign-limit-order.ts new file mode 100644 index 0000000..ded230e --- /dev/null +++ b/limit-order-api-demo/src/makers/sign-limit-order.ts @@ -0,0 +1,22 @@ +import { GeneratedLimitOrderData, generateLimitOrderData } from './generate-limit-order-data'; +import { limitOrderDomainArbitrum, typesLimitOrder } from '../share/constants'; +import { getSigner } from '../share/signer'; + +export async function signLimitOrderData(data: GeneratedLimitOrderData): Promise { + try { + const signer = getSigner(); + + console.log(`\nStart signing the limit order data...`); + + console.log(data); + console.log(typesLimitOrder); + + const signature = await signer.signTypedData(limitOrderDomainArbitrum, typesLimitOrder, data); + + console.log(`\nSigned the limit order data successfully`); + + return signature; + } catch (error) { + throw error; + } +} diff --git a/limit-order-api-demo/src/share/constants.ts b/limit-order-api-demo/src/share/constants.ts new file mode 100644 index 0000000..1d7986b --- /dev/null +++ b/limit-order-api-demo/src/share/constants.ts @@ -0,0 +1,42 @@ +import { TypedDataField } from 'ethers'; + +export const LIMIT_ORDER_DOMAIN = `https://api-v2.pendle.finance/limit-order`; + +export enum ChainId { + ARBITRUM = 42161, +} + +export const LIMIT_ORDER_CONTRACTS = { + ARBITRUM: '0x000000000000c9b3e2c3ec88b1b4c0cd853f4321', +}; + +export const aUSDC_MARKET = { + yt: '0xa1c32ef8d3c4c30cb596bab8647e11daf0fa5c94', + tokenIn: { + usdc: '0xaf88d065e77c8cc2239327c5edb3a432268e5831', + }, +}; + +export const typesLimitOrder: Record = { + Order: [ + { name: 'salt', type: 'uint256' }, + { name: 'expiry', type: 'uint256' }, + { name: 'nonce', type: 'uint256' }, + { name: 'orderType', type: 'uint8' }, + { name: 'token', type: 'address' }, + { name: 'YT', type: 'address' }, + { name: 'maker', type: 'address' }, + { name: 'receiver', type: 'address' }, + { name: 'makingAmount', type: 'uint256' }, + { name: 'lnImpliedRate', type: 'uint256' }, + { name: 'failSafeRate', type: 'uint256' }, + { name: 'permit', type: 'bytes' }, + ], +}; + +export const limitOrderDomainArbitrum = { + name: 'Pendle Limit Order Protocol', + version: '1', + chainId: 42161, + verifyingContract: LIMIT_ORDER_CONTRACTS.ARBITRUM, +}; diff --git a/limit-order-api-demo/src/share/provider.ts b/limit-order-api-demo/src/share/provider.ts new file mode 100644 index 0000000..aa4a909 --- /dev/null +++ b/limit-order-api-demo/src/share/provider.ts @@ -0,0 +1,13 @@ +import { ethers } from 'ethers'; +import { ChainId } from './constants'; + +export function getProvider(): ethers.Provider { + // Replace this with a RPC of your choice + const providerUrl = 'https://arbitrum.llamarpc.com'; + const providerOptions = { + // Testing on Polygon POS + chainId: ChainId.ARBITRUM, + name: 'Arbitrum', + }; + return new ethers.JsonRpcProvider(providerUrl, providerOptions); +} diff --git a/limit-order-api-demo/src/share/signer.ts b/limit-order-api-demo/src/share/signer.ts new file mode 100644 index 0000000..e218990 --- /dev/null +++ b/limit-order-api-demo/src/share/signer.ts @@ -0,0 +1,13 @@ +import { ethers } from 'ethers'; +import { getProvider } from './provider'; + +/** + * To be able to run this example, you need to implement this function + * For example, you can implement this by + * return new ethers.Wallet('YOUR_PRIVATE_KEY', getProvider()); + * + * CAUTION: Never expose your private keys (i.e. commit to a public repo) + */ +export function getSigner(): ethers.Signer { + throw new Error(""); +} diff --git a/limit-order-api-demo/src/share/types.ts b/limit-order-api-demo/src/share/types.ts new file mode 100644 index 0000000..a7388cc --- /dev/null +++ b/limit-order-api-demo/src/share/types.ts @@ -0,0 +1,55 @@ +export enum LimitOrderType { + TOKEN_FOR_PT, + PT_FOR_TOKEN, + TOKEN_FOR_YT, + YT_FOR_TOKEN, +} + +// OrderStruct has the same schema with struct Order on contract +export interface OrderStruct { + salt: string; + expiry: string; + nonce: string; + orderType: LimitOrderType; + token: string; + YT: string; + maker: string; + receiver: string; + makingAmount: string; + lnImpliedRate: string; + failSafeRate: string; + permit: string; +} + +export type BackendSignedLimitOrder = Omit & { + id: string; + signature: string; + type: LimitOrderType; + yt: string; +} + +// SignedLimitOrder with some supported fields +export interface SignedLimitOrderInfo { + order: BackendSignedLimitOrder; + makingAmount: string; + netFromTaker: string; + netToTaker: string; +} + + +export function fromBackendSignedLimitOrderToOrderStruct(order: BackendSignedLimitOrder) { + return { + salt: order.salt, + expiry: order.expiry, + nonce: order.nonce, + orderType: order.type, + token: order.token, + YT: order.yt, + maker: order.maker, + receiver: order.receiver, + makingAmount: order.makingAmount, + lnImpliedRate: order.lnImpliedRate, + failSafeRate: order.failSafeRate, + permit: order.permit, + } satisfies OrderStruct; +} diff --git a/limit-order-api-demo/src/takers/fill-limit-orders.ts b/limit-order-api-demo/src/takers/fill-limit-orders.ts new file mode 100644 index 0000000..7780407 --- /dev/null +++ b/limit-order-api-demo/src/takers/fill-limit-orders.ts @@ -0,0 +1,42 @@ +import { Contract } from 'ethers'; +import { LIMIT_ORDER_CONTRACTS } from '../share/constants'; +import abi from '../abis/PendleLimitRouter.json'; +import { getSigner } from '../share/signer'; +import { SignedLimitOrderInfo, fromBackendSignedLimitOrderToOrderStruct } from '../share/types'; + +export async function fillLimitOrders(limitOrdersInfo: SignedLimitOrderInfo[]) { + const signer = getSigner(); + const contract = new Contract(LIMIT_ORDER_CONTRACTS.ARBITRUM, abi, signer); + + const fillParams = limitOrdersInfo.map((limitOrderInfo) => { + const { order, makingAmount } = limitOrderInfo; + return { + order: fromBackendSignedLimitOrderToOrderStruct(order), + signature: order.signature, + makingAmount: makingAmount, // How much you want to fill the order, the same unit with makingAmount of the order + }; + }); + + const sumNetFromTaker = limitOrdersInfo.reduce((acc, limitOrderInfo) => { + return acc + BigInt(limitOrderInfo.netFromTaker); + }, 0n); + + // Maximum amount to be used to fill the order + // We recommend buffer the returned value from BE by 1% because + // the netFromTaker amount will change by time + const maxTaking = (sumNetFromTaker * 101n) / 100n; + + const tx = await contract.fill( + fillParams, // limit of order to fill + signer.getAddress(), + maxTaking, + '0x', + '0x' + ); + + await tx.wait(); + + console.debug(tx); + + console.log('Finished fill the order'); +} diff --git a/limit-order-api-demo/src/takers/get-limit-orders.ts b/limit-order-api-demo/src/takers/get-limit-orders.ts new file mode 100644 index 0000000..8415a23 --- /dev/null +++ b/limit-order-api-demo/src/takers/get-limit-orders.ts @@ -0,0 +1,38 @@ +import axios from 'axios'; +import { ChainId, LIMIT_ORDER_DOMAIN, aUSDC_MARKET } from '../share/constants'; +import { LimitOrderType, SignedLimitOrderInfo } from '../share/types'; + +export interface LimitOrderTakerQuery { + skip: number; + limit: number; + chainId: number; + yt: string; + type: LimitOrderType; + sortBy: 'Implied Rate'; + sortOrder: 'asc' | 'desc'; +} + + +export async function getLimitOrders(): Promise { + const targetPath = '/v1/takers/limit-orders'; + + console.log(`Fetching limit orders...`); + + const requestQuery: LimitOrderTakerQuery = { + skip: 0, + limit: 10, // Use skip and limit to fetch the orders, you can fetch upto 100 orders at a request + chainId: ChainId.ARBITRUM, + yt: aUSDC_MARKET.yt, + type: LimitOrderType.TOKEN_FOR_PT, + sortBy: 'Implied Rate', + sortOrder: 'asc', + }; + + console.debug(requestQuery); + + const { data } = await axios.get(LIMIT_ORDER_DOMAIN + targetPath, { params: requestQuery }); + + console.log(`\nFinished fetching limit orders`); + + return data.results as SignedLimitOrderInfo[]; +} diff --git a/limit-order-api-demo/tsconfig.json b/limit-order-api-demo/tsconfig.json new file mode 100644 index 0000000..a7d6849 --- /dev/null +++ b/limit-order-api-demo/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + /* Basic Options */ + "incremental": true, /* Enable incremental compilation */ + "target": "es2020", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019' or 'ESNEXT'. */ + "module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */ + // "lib": [], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ + "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + "outDir": "./dist", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + /* Module Resolution Options */ + // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + "resolveJsonModule": true, + }, + "exclude": [ + "node_modules", + "dist" + ] +}