diff --git a/Cargo.lock b/Cargo.lock index 57bb4d68..ee5ae816 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -311,18 +311,6 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - [[package]] name = "blake3" version = "1.8.2" @@ -505,6 +493,16 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chainlink_solana" +version = "1.0.0" +source = "git+https://github.com/smartcontractkit/chainlink-solana?branch=solana-2.1#edeaa2d8d1c4c53775ec16f40095184f60647f4a" +dependencies = [ + "borsh 0.10.4", + "borsh-derive 0.10.4", + "solana-program", +] + [[package]] name = "cipher" version = "0.4.4" @@ -643,15 +641,6 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" -[[package]] -name = "fast-math" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2465292146cdfc2011350fe3b1c616ac83cf0faeedb33463ba1c332ed8948d66" -dependencies = [ - "ieee754", -] - [[package]] name = "feature-probe" version = "0.1.1" @@ -685,12 +674,6 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - [[package]] name = "generic-array" version = "0.14.7" @@ -758,12 +741,6 @@ dependencies = [ "digest 0.10.7", ] -[[package]] -name = "ieee754" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9007da9cacbd3e6343da136e98b0d2df013f553d35bdec8b518f07bea768e19c" - [[package]] name = "indexmap" version = "2.9.0" @@ -1118,12 +1095,6 @@ dependencies = [ "proc-macro2", ] -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - [[package]] name = "rand" version = "0.7.3" @@ -1260,6 +1231,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", + "chainlink_solana", "mpl-token-metadata", ] @@ -1269,8 +1241,7 @@ version = "0.1.0" dependencies = [ "anchor-lang", "anchor-spl", - "bitvec", - "solana-merkle-tree", + "chainlink_solana", ] [[package]] @@ -1744,17 +1715,6 @@ dependencies = [ "solana-system-interface", ] -[[package]] -name = "solana-merkle-tree" -version = "2.2.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e804fa0ef3c3c163f34d03c13b425cd4c8b8e150e39fc4ce25e460ea2fa7740" -dependencies = [ - "fast-math", - "solana-hash", - "solana-sha256-hasher", -] - [[package]] name = "solana-message" version = "2.3.0" @@ -2642,12 +2602,6 @@ dependencies = [ "unicode-ident", ] -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - [[package]] name = "thiserror" version = "1.0.69" @@ -2950,15 +2904,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - [[package]] name = "zerocopy" version = "0.7.35" diff --git a/bun.lock b/bun.lock index f2f63ea6..dda4b01a 100644 --- a/bun.lock +++ b/bun.lock @@ -33,25 +33,25 @@ "packages": { "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.0", "", {}, "sha512-/3DDPKHqqIqxUULp8yP4zODUY1i+2xvVWsv8A79xGWdCAG+8sb0hRh0Rk2QyOJUnnbyPUAZYcpBuRe3nS2OIUg=="], - "@babel/runtime": ["@babel/runtime@7.27.6", "", {}, "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q=="], + "@babel/runtime": ["@babel/runtime@7.28.2", "", {}, "sha512-KHp2IflsnGywDjBWDkR9iEqiWSpc8GIi0lgTT3mOElT0PP1tG26P4tmFI2YvAdzgq9RGyoHZQEIEdZy6Ec5xCA=="], - "@biomejs/biome": ["@biomejs/biome@2.1.2", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.2", "@biomejs/cli-darwin-x64": "2.1.2", "@biomejs/cli-linux-arm64": "2.1.2", "@biomejs/cli-linux-arm64-musl": "2.1.2", "@biomejs/cli-linux-x64": "2.1.2", "@biomejs/cli-linux-x64-musl": "2.1.2", "@biomejs/cli-win32-arm64": "2.1.2", "@biomejs/cli-win32-x64": "2.1.2" }, "bin": { "biome": "bin/biome" } }, "sha512-yq8ZZuKuBVDgAS76LWCfFKHSYIAgqkxVB3mGVVpOe2vSkUTs7xG46zXZeNPRNVjiJuw0SZ3+J2rXiYx0RUpfGg=="], + "@biomejs/biome": ["@biomejs/biome@2.1.3", "", { "optionalDependencies": { "@biomejs/cli-darwin-arm64": "2.1.3", "@biomejs/cli-darwin-x64": "2.1.3", "@biomejs/cli-linux-arm64": "2.1.3", "@biomejs/cli-linux-arm64-musl": "2.1.3", "@biomejs/cli-linux-x64": "2.1.3", "@biomejs/cli-linux-x64-musl": "2.1.3", "@biomejs/cli-win32-arm64": "2.1.3", "@biomejs/cli-win32-x64": "2.1.3" }, "bin": { "biome": "bin/biome" } }, "sha512-KE/tegvJIxTkl7gJbGWSgun7G6X/n2M6C35COT6ctYrAy7SiPyNvi6JtoQERVK/VRbttZfgGq96j2bFmhmnH4w=="], - "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-leFAks64PEIjc7MY/cLjE8u5OcfBKkcDB0szxsWUB4aDfemBep1WVKt0qrEyqZBOW8LPHzrFMyDl3FhuuA0E7g=="], + "@biomejs/cli-darwin-arm64": ["@biomejs/cli-darwin-arm64@2.1.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LFLkSWRoSGS1wVUD/BE6Nlt2dSn0ulH3XImzg2O/36BoToJHKXjSxzPEMAqT9QvwVtk7/9AQhZpTneERU9qaXA=="], - "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-Nmmv7wRX5Nj7lGmz0FjnWdflJg4zii8Ivruas6PBKzw5SJX/q+Zh2RfnO+bBnuKLXpj8kiI2x2X12otpH6a32A=="], + "@biomejs/cli-darwin-x64": ["@biomejs/cli-darwin-x64@2.1.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-Q/4OTw8P9No9QeowyxswcWdm0n2MsdCwWcc5NcKQQvzwPjwuPdf8dpPPf4r+x0RWKBtl1FLiAUtJvBlri6DnYw=="], - "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-NWNy2Diocav61HZiv2enTQykbPP/KrA/baS7JsLSojC7Xxh2nl9IczuvE5UID7+ksRy2e7yH7klm/WkA72G1dw=="], + "@biomejs/cli-linux-arm64": ["@biomejs/cli-linux-arm64@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-2hS6LgylRqMFmAZCOFwYrf77QMdUwJp49oe8PX/O8+P2yKZMSpyQTf3Eo5ewnsMFUEmYbPOskafdV1ds1MZMJA=="], - "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-qgHvafhjH7Oca114FdOScmIKf1DlXT1LqbOrrbR30kQDLFPEOpBG0uzx6MhmsrmhGiCFCr2obDamu+czk+X0HQ=="], + "@biomejs/cli-linux-arm64-musl": ["@biomejs/cli-linux-arm64-musl@2.1.3", "", { "os": "linux", "cpu": "arm64" }, "sha512-KXouFSBnoxAWZYDQrnNRzZBbt5s9UJkIm40hdvSL9mBxSSoxRFQJbtg1hP3aa8A2SnXyQHxQfpiVeJlczZt76w=="], - "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Km/UYeVowygTjpX6sGBzlizjakLoMQkxWbruVZSNE6osuSI63i4uCeIL+6q2AJlD3dxoiBJX70dn1enjQnQqwA=="], + "@biomejs/cli-linux-x64": ["@biomejs/cli-linux-x64@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-NxlSCBhLvQtWGagEztfAZ4WcE1AkMTntZV65ZvR+J9jp06+EtOYEBPQndA70ZGhHbEDG57bR6uNvqkd1WrEYVA=="], - "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xlB3mU14ZUa3wzLtXfmk2IMOGL+S0aHFhSix/nssWS/2XlD27q+S6f0dlQ8WOCbYoXcuz8BCM7rCn2lxdTrlQA=="], + "@biomejs/cli-linux-x64-musl": ["@biomejs/cli-linux-x64-musl@2.1.3", "", { "os": "linux", "cpu": "x64" }, "sha512-KaLAxnROouzIWtl6a0Y88r/4hW5oDUJTIqQorOTVQITaKQsKjZX4XCUmHIhdEk8zMnaiLZzRTAwk1yIAl+mIew=="], - "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-G8KWZli5ASOXA3yUQgx+M4pZRv3ND16h77UsdunUL17uYpcL/UC7RkWTdkfvMQvogVsAuz5JUcBDjgZHXxlKoA=="], + "@biomejs/cli-win32-arm64": ["@biomejs/cli-win32-arm64@2.1.3", "", { "os": "win32", "cpu": "arm64" }, "sha512-V9CUZCtWH4u0YwyCYbQ3W5F4ZGPWp2C2TYcsiWFNNyRfmOW1j/TY/jAurl33SaRjgZPO5UUhGyr9m6BN9t84NQ=="], - "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.2", "", { "os": "win32", "cpu": "x64" }, "sha512-9zajnk59PMpjBkty3bK2IrjUsUHvqe9HWwyAWQBjGLE7MIBjbX2vwv1XPEhmO2RRuGoTkVx3WCanHrjAytICLA=="], + "@biomejs/cli-win32-x64": ["@biomejs/cli-win32-x64@2.1.3", "", { "os": "win32", "cpu": "x64" }, "sha512-dxy599q6lgp8ANPpR8sDMscwdp9oOumEsVXuVCVT9N2vAho8uYXlCz53JhxX6LtJOXaE73qzgkGQ7QqvFlMC0g=="], "@coral-xyz/anchor": ["@coral-xyz/anchor@0.31.1", "", { "dependencies": { "@coral-xyz/anchor-errors": "^0.31.1", "@coral-xyz/borsh": "^0.31.1", "@noble/hashes": "^1.3.1", "@solana/web3.js": "^1.69.0", "bn.js": "^5.1.2", "bs58": "^4.0.1", "buffer-layout": "^1.2.2", "camelcase": "^6.3.0", "cross-fetch": "^3.1.5", "eventemitter3": "^4.0.7", "pako": "^2.0.3", "superstruct": "^0.15.4", "toml": "^3.0.0" } }, "sha512-QUqpoEK+gi2S6nlYc2atgT2r41TT3caWr/cPUEL8n8Md9437trZ68STknq897b82p5mW0XrTBNOzRbmIRJtfsA=="], @@ -115,53 +115,53 @@ "@noble/ciphers": ["@noble/ciphers@1.3.0", "", {}, "sha512-2I0gnIVPtfnMw9ee9h1dJG7tp81+8Ob3OJb3Mv37rx5L40/b0i7djjCVvGOVqc9AEIQyvyu1i6ypKdFw8R8gQw=="], - "@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], + "@noble/curves": ["@noble/curves@1.9.6", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-GIKz/j99FRthB8icyJQA51E8Uk5hXmdyThjgQXRKiv9h0zeRlzSCLIzFw6K1LotZ3XuB7yzlf76qk7uBmTdFqA=="], "@noble/hashes": ["@noble/hashes@1.8.0", "", {}, "sha512-jCs9ldd7NwzpgXDIf6P3+NrHh9/sD6CQdxHyjQI+h/6rDNo88ypBxxz45UDuZHz9r3tNz7N/VInSVoVdtXEI4A=="], "@polka/url": ["@polka/url@1.0.0-next.29", "", {}, "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww=="], - "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.45.1", "", { "os": "android", "cpu": "arm" }, "sha512-NEySIFvMY0ZQO+utJkgoMiCAjMrGvnbDLHvcmlA33UXJpYBCvlBEbMMtV837uCkS+plG2umfhn0T5mMAxGrlRA=="], + "@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.46.2", "", { "os": "android", "cpu": "arm" }, "sha512-Zj3Hl6sN34xJtMv7Anwb5Gu01yujyE/cLBDB2gnHTAHaWS1Z38L7kuSG+oAh0giZMqG060f/YBStXtMH6FvPMA=="], - "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.45.1", "", { "os": "android", "cpu": "arm64" }, "sha512-ujQ+sMXJkg4LRJaYreaVx7Z/VMgBBd89wGS4qMrdtfUFZ+TSY5Rs9asgjitLwzeIbhwdEhyj29zhst3L1lKsRQ=="], + "@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.46.2", "", { "os": "android", "cpu": "arm64" }, "sha512-nTeCWY83kN64oQ5MGz3CgtPx8NSOhC5lWtsjTs+8JAJNLcP3QbLCtDDgUKQc/Ro/frpMq4SHUaHN6AMltcEoLQ=="], - "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.45.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-FSncqHvqTm3lC6Y13xncsdOYfxGSLnP+73k815EfNmpewPs+EyM49haPS105Rh4aF5mJKywk9X0ogzLXZzN9lA=="], + "@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.46.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-HV7bW2Fb/F5KPdM/9bApunQh68YVDU8sO8BvcW9OngQVN3HHHkw99wFupuUJfGR9pYLLAjcAOA6iO+evsbBaPQ=="], - "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.45.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-2/vVn/husP5XI7Fsf/RlhDaQJ7x9zjvC81anIVbr4b/f0xtSmXQTFcGIQ/B1cXIYM6h2nAhJkdMHTnD7OtQ9Og=="], + "@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.46.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-SSj8TlYV5nJixSsm/y3QXfhspSiLYP11zpfwp6G/YDXctf3Xkdnk4woJIF5VQe0of2OjzTt8EsxnJDCdHd2xMA=="], - "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.45.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-4g1kaDxQItZsrkVTdYQ0bxu4ZIQ32cotoQbmsAnW1jAE4XCMbcBPDirX5fyUzdhVCKgPcrwWuucI8yrVRBw2+g=="], + "@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.46.2", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-ZyrsG4TIT9xnOlLsSSi9w/X29tCbK1yegE49RYm3tu3wF1L/B6LVMqnEWyDB26d9Ecx9zrmXCiPmIabVuLmNSg=="], - "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.45.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-L/6JsfiL74i3uK1Ti2ZFSNsp5NMiM4/kbbGEcOCps99aZx3g8SJMO1/9Y0n/qKlWZfn6sScf98lEOUe2mBvW9A=="], + "@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.46.2", "", { "os": "freebsd", "cpu": "x64" }, "sha512-pCgHFoOECwVCJ5GFq8+gR8SBKnMO+xe5UEqbemxBpCKYQddRQMgomv1104RnLSg7nNvgKy05sLsY51+OVRyiVw=="], - "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-RkdOTu2jK7brlu+ZwjMIZfdV2sSYHK2qR08FUWcIoqJC2eywHbXr0L8T/pONFwkGukQqERDheaGTeedG+rra6Q=="], + "@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-EtP8aquZ0xQg0ETFcxUbU71MZlHaw9MChwrQzatiE8U/bvi5uv/oChExXC4mWhjiqK7azGJBqU0tt5H123SzVA=="], - "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.45.1", "", { "os": "linux", "cpu": "arm" }, "sha512-3kJ8pgfBt6CIIr1o+HQA7OZ9mp/zDk3ctekGl9qn/pRBgrRgfwiffaUmqioUGN9hv0OHv2gxmvdKOkARCtRb8Q=="], + "@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.46.2", "", { "os": "linux", "cpu": "arm" }, "sha512-qO7F7U3u1nfxYRPM8HqFtLd+raev2K137dsV08q/LRKRLEc7RsiDWihUnrINdsWQxPR9jqZ8DIIZ1zJJAm5PjQ=="], - "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-k3dOKCfIVixWjG7OXTCOmDfJj3vbdhN0QYEqB+OuGArOChek22hn7Uy5A/gTDNAcCy5v2YcXRJ/Qcnm4/ma1xw=="], + "@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-3dRaqLfcOXYsfvw5xMrxAk9Lb1f395gkoBYzSFcc/scgRFptRXL9DOaDpMiehf9CO8ZDRJW2z45b6fpU5nwjng=="], - "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.45.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-PmI1vxQetnM58ZmDFl9/Uk2lpBBby6B6rF4muJc65uZbxCs0EA7hhKCk2PKlmZKuyVSHAyIw3+/SiuMLxKxWog=="], + "@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.46.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-fhHFTutA7SM+IrR6lIfiHskxmpmPTJUXpWIsBXpeEwNgZzZZSg/q4i6FU4J8qOGyJ0TR+wXBwx/L7Ho9z0+uDg=="], - "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-9UmI0VzGmNJ28ibHW2GpE2nF0PBQqsyiS4kcJ5vK+wuwGnV5RlqdczVocDSUfGX/Na7/XINRVoUgJyFIgipoRg=="], + "@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-i7wfGFXu8x4+FRqPymzjD+Hyav8l95UIZ773j7J7zRYc3Xsxy2wIn4x+llpunexXe6laaO72iEjeeGyUFmjKeA=="], - "@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.45.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-7nR2KY8oEOUTD3pBAxIBBbZr0U7U+R9HDTPNy+5nVVHDXI4ikYniH1oxQz9VoB5PbBU1CZuDGHkLJkd3zLMWsg=="], + "@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.46.2", "", { "os": "linux", "cpu": "ppc64" }, "sha512-B/l0dFcHVUnqcGZWKcWBSV2PF01YUt0Rvlurci5P+neqY/yMKchGU8ullZvIv5e8Y1C6wOn+U03mrDylP5q9Yw=="], - "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-nlcl3jgUultKROfZijKjRQLUu9Ma0PeNv/VFHkZiKbXTBQXhpytS8CIj5/NfBeECZtY2FJQubm6ltIxm/ftxpw=="], + "@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-32k4ENb5ygtkMwPMucAb8MtV8olkPT03oiTxJbgkJa7lJ7dZMr0GCFJlyvy+K8iq7F/iuOr41ZdUHaOiqyR3iQ=="], - "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.45.1", "", { "os": "linux", "cpu": "none" }, "sha512-HJV65KLS51rW0VY6rvZkiieiBnurSzpzore1bMKAhunQiECPuxsROvyeaot/tcK3A3aGnI+qTHqisrpSgQrpgA=="], + "@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.46.2", "", { "os": "linux", "cpu": "none" }, "sha512-t5B2loThlFEauloaQkZg9gxV05BYeITLvLkWOkRXogP4qHXLkWSbSHKM9S6H1schf/0YGP/qNKtiISlxvfmmZw=="], - "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.45.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-NITBOCv3Qqc6hhwFt7jLV78VEO/il4YcBzoMGGNxznLgRQf43VQDae0aAzKiBeEPIxnDrACiMgbqjuihx08OOw=="], + "@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.46.2", "", { "os": "linux", "cpu": "s390x" }, "sha512-YKjekwTEKgbB7n17gmODSmJVUIvj8CX7q5442/CK80L8nqOUbMtf8b01QkG3jOqyr1rotrAnW6B/qiHwfcuWQA=="], - "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-+E/lYl6qu1zqgPEnTrs4WysQtvc/Sh4fC2nByfFExqgYrqkKWp1tWIbe+ELhixnenSpBbLXNi6vbEEJ8M7fiHw=="], + "@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-Jj5a9RUoe5ra+MEyERkDKLwTXVu6s3aACP51nkfnK9wJTraCC8IMe3snOfALkrjTYd2G1ViE1hICj0fZ7ALBPA=="], - "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.45.1", "", { "os": "linux", "cpu": "x64" }, "sha512-a6WIAp89p3kpNoYStITT9RbTbTnqarU7D8N8F2CV+4Cl9fwCOZraLVuVFvlpsW0SbIiYtEnhCZBPLoNdRkjQFw=="], + "@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.46.2", "", { "os": "linux", "cpu": "x64" }, "sha512-7kX69DIrBeD7yNp4A5b81izs8BqoZkCIaxQaOpumcJ1S/kmqNFjPhDu1LHeVXv0SexfHQv5cqHsxLOjETuqDuA=="], - "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.45.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-T5Bi/NS3fQiJeYdGvRpTAP5P02kqSOpqiopwhj0uaXB6nzs5JVi2XMJb18JUSKhCOX8+UE1UKQufyD6Or48dJg=="], + "@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.46.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-wiJWMIpeaak/jsbaq2HMh/rzZxHVW1rU6coyeNNpMwk5isiPjSTx0a4YLSlYDwBH/WBvLz+EtsNqQScZTLJy3g=="], - "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.45.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-lxV2Pako3ujjuUe9jiU3/s7KSrDfH6IgTSQOnDWr9aJ92YsFd7EurmClK0ly/t8dzMkDtd04g60WX6yl0sGfdw=="], + "@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.46.2", "", { "os": "win32", "cpu": "ia32" }, "sha512-gBgaUDESVzMgWZhcyjfs9QFK16D8K6QZpwAaVNJxYDLHWayOta4ZMjGm/vsAEy3hvlS2GosVFlBlP9/Wb85DqQ=="], - "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.45.1", "", { "os": "win32", "cpu": "x64" }, "sha512-M/fKi4sasCdM8i0aWJjCSFm2qEnYRR8AMLG2kxp6wD13+tMGA4Z1tVAuHkNRjud5SW2EM3naLuK35w9twvf6aA=="], + "@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.46.2", "", { "os": "win32", "cpu": "x64" }, "sha512-CvUo2ixeIQGtF6WvuB87XWqPQkoFAFqW+HUo/WzHwuHDvIwZCtjdWXoYCcr06iKGydiqTclC4jU/TNObC/xKZg=="], - "@sablier/devkit": ["@sablier/devkit@github:sablier-labs/devkit#60c10da", {}, "sablier-labs-devkit-60c10da"], + "@sablier/devkit": ["@sablier/devkit@github:sablier-labs/devkit#976d4b8", {}, "sablier-labs-devkit-976d4b8"], "@scure/base": ["@scure/base@1.2.6", "", {}, "sha512-g/nm5FgUa//MCj1gV09zTJTaM6KBAHqLN907YVQqf7zC49+DcO4B1so4ZX07Ef10Twr6nuqYEH9GEggFXA4Fmg=="], @@ -175,15 +175,15 @@ "@solana/codecs": ["@solana/codecs@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/codecs-strings": "2.0.0-rc.1", "@solana/options": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-qxoR7VybNJixV51L0G1RD2boZTcxmwUWnKCaJJExQ5qNKwbpSyDdWfFJfM5JhGyKe9DnPVOZB+JHWXnpbZBqrQ=="], - "@solana/codecs-core": ["@solana/codecs-core@2.2.0", "", { "dependencies": { "@solana/errors": "2.2.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-XRrxXy9oz3Q0jAHpeaHcqFDlGTzSabjijdMWjj1v20y6jw5oH0e5QjuafX3s2scJiKo5fBWmcVbod2Qd+niWxQ=="], + "@solana/codecs-core": ["@solana/codecs-core@2.3.0", "", { "dependencies": { "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-oG+VZzN6YhBHIoSKgS5ESM9VIGzhWjEHEGNPSibiDTxFhsFWxNaz8LbMDPjBUE69r9wmdGLkrQ+wVPbnJcZPvw=="], "@solana/codecs-data-structures": ["@solana/codecs-data-structures@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-rinCv0RrAVJ9rE/rmaibWJQxMwC5lSaORSZuwjopSUE6T0nb/MVg6Z1siNCXhh/HFTOg0l8bNvZHgBcN/yvXog=="], - "@solana/codecs-numbers": ["@solana/codecs-numbers@2.2.0", "", { "dependencies": { "@solana/codecs-core": "2.2.0", "@solana/errors": "2.2.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-i6HIXOpEfC3j0yeD1lpHzsvzeZ16NQ//blDxn4q+BpE8JSydm4L+X2rxJpQ05rmzYtDDE8pnKA2Ub0f8oMLU5w=="], + "@solana/codecs-numbers": ["@solana/codecs-numbers@2.3.0", "", { "dependencies": { "@solana/codecs-core": "2.3.0", "@solana/errors": "2.3.0" }, "peerDependencies": { "typescript": ">=5.3.3" } }, "sha512-jFvvwKJKffvG7Iz9dmN51OGB7JBcy2CJ6Xf3NqD/VP90xak66m/Lg48T01u5IQ/hc15mChVHiBm+HHuOFDUrQg=="], "@solana/codecs-strings": ["@solana/codecs-strings@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": ">=5" } }, "sha512-9/wPhw8TbGRTt6mHC4Zz1RqOnuPTqq1Nb4EyuvpZ39GW6O2t2Q7Q0XxiB3+BdoEjwA2XgPw6e2iRfvYgqty44g=="], - "@solana/errors": ["@solana/errors@2.2.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^13.1.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-KItyBPyhRdSeGr8HLeMDSUiVCpwY1pr1evBmqs+IFN6BNWmFDplvROzc/HLqTmeM6D5JauuoYIYqtO81IL65pg=="], + "@solana/errors": ["@solana/errors@2.3.0", "", { "dependencies": { "chalk": "^5.4.1", "commander": "^14.0.0" }, "peerDependencies": { "typescript": ">=5.3.3" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-66RI9MAbwYV0UtP7kGcTBVLxJgUxoZGm8Fbc0ah+lGiAw17Gugco6+9GrJCV83VyF2mDWyYnYM9qdI3yjgpnaQ=="], "@solana/options": ["@solana/options@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/codecs-data-structures": "2.0.0-rc.1", "@solana/codecs-numbers": "2.0.0-rc.1", "@solana/codecs-strings": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-mLUcR9mZ3qfHlmMnREdIFPf9dpMc/Bl66tLSOOWxw4ml5xMT2ohFn7WGqoKcu/UHkT9CrC6+amEdqCNvUqI7AA=="], @@ -193,7 +193,7 @@ "@solana/spl-token-metadata": ["@solana/spl-token-metadata@0.1.6", "", { "dependencies": { "@solana/codecs": "2.0.0-rc.1" }, "peerDependencies": { "@solana/web3.js": "^1.95.3" } }, "sha512-7sMt1rsm/zQOQcUWllQX9mD2O6KhSAtY1hFR2hfFwgqfFWzSY9E9GDvFVNYUI1F0iQKcm6HmePU9QbKRXTEBiA=="], - "@solana/web3.js": ["@solana/web3.js@1.98.2", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-BqVwEG+TaG2yCkBMbD3C4hdpustR4FpuUFRPUmqRZYYlPI9Hg4XMWxHWOWRzHE9Lkc9NDjzXFX7lDXSgzC7R1A=="], + "@solana/web3.js": ["@solana/web3.js@1.98.4", "", { "dependencies": { "@babel/runtime": "^7.25.0", "@noble/curves": "^1.4.2", "@noble/hashes": "^1.4.0", "@solana/buffer-layout": "^4.0.1", "@solana/codecs-numbers": "^2.1.0", "agentkeepalive": "^4.5.0", "bn.js": "^5.2.1", "borsh": "^0.7.0", "bs58": "^4.0.1", "buffer": "6.0.3", "fast-stable-stringify": "^1.0.0", "jayson": "^4.1.1", "node-fetch": "^2.7.0", "rpc-websockets": "^9.0.2", "superstruct": "^2.0.2" } }, "sha512-vv9lfnvjUsRiq//+j5pBdXig0IQdtzA0BRZ3bXEP4KaIyF1CcaydWqgyzQgfZMNIsWNWmG+AUHwPy4AHOD6gpw=="], "@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], @@ -209,7 +209,7 @@ "@types/lodash": ["@types/lodash@4.17.20", "", {}, "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA=="], - "@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + "@types/node": ["@types/node@24.1.0", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ut5FthK5moxFKH2T1CUOC6ctR67rQRvvHdFLCD2Ql6KXmMuCrjsSsRI9UsLCm9M18BMwClv4pn327UvB7eeO1w=="], "@types/uuid": ["@types/uuid@8.3.4", "", {}, "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw=="], @@ -251,7 +251,7 @@ "bigint-buffer": ["bigint-buffer@1.1.5", "", { "dependencies": { "bindings": "^1.3.0" } }, "sha512-trfYco6AoZ+rKhKnxA0hgX0HAbVP/s808/EuDSe2JDzUnCp/xAsli35Orvk67UrTEcwuxZqYZDmfA2RXJgxVvA=="], - "bignumber.js": ["bignumber.js@9.3.0", "", {}, "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA=="], + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], "bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="], @@ -375,7 +375,7 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "loupe": ["loupe@3.1.4", "", {}, "sha512-wJzkKwJrheKtknCOKNEtDK4iqg/MxmZheEMtSTYvnzRdEYaZzmgH976nenp8WdJRdx5Vc1X/9MO0Oszl6ezeXg=="], + "loupe": ["loupe@3.2.0", "", {}, "sha512-2NCfZcT5VGVNX9mSZIxLRkEAegDGBpuQZBy13desuHeVORmBDyAET4TkJr4SjqQy3A8JDofMN6LpkK8Xcm/dlw=="], "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], @@ -401,7 +401,7 @@ "onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="], - "ox": ["ox@0.8.1", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-e+z5epnzV+Zuz91YYujecW8cF01mzmrUtWotJ0oEPym/G82uccs7q0WDHTYL3eiONbTUEvcZrptAKLgTBD3u2A=="], + "ox": ["ox@0.8.6", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "^1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.0.8", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-eiKcgiVVEGDtEpEdFi1EGoVVI48j6icXHce9nFwCNM7CKG3uoCXKdr4TPhS00Iy1TR2aWSF1ltPD0x/YgqIL9w=="], "pako": ["pako@2.1.0", "", {}, "sha512-w+eufiZ1WuJYgPXbV/PO3NCMEc3xqylkKHzp8bxp1uW4qaSNQUkwmLLEc3kKsfz8lpV1F8Ht3U1Cm+9Srog2ug=="], @@ -425,9 +425,9 @@ "rfdc": ["rfdc@1.4.1", "", {}, "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA=="], - "rollup": ["rollup@4.45.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.45.1", "@rollup/rollup-android-arm64": "4.45.1", "@rollup/rollup-darwin-arm64": "4.45.1", "@rollup/rollup-darwin-x64": "4.45.1", "@rollup/rollup-freebsd-arm64": "4.45.1", "@rollup/rollup-freebsd-x64": "4.45.1", "@rollup/rollup-linux-arm-gnueabihf": "4.45.1", "@rollup/rollup-linux-arm-musleabihf": "4.45.1", "@rollup/rollup-linux-arm64-gnu": "4.45.1", "@rollup/rollup-linux-arm64-musl": "4.45.1", "@rollup/rollup-linux-loongarch64-gnu": "4.45.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-gnu": "4.45.1", "@rollup/rollup-linux-riscv64-musl": "4.45.1", "@rollup/rollup-linux-s390x-gnu": "4.45.1", "@rollup/rollup-linux-x64-gnu": "4.45.1", "@rollup/rollup-linux-x64-musl": "4.45.1", "@rollup/rollup-win32-arm64-msvc": "4.45.1", "@rollup/rollup-win32-ia32-msvc": "4.45.1", "@rollup/rollup-win32-x64-msvc": "4.45.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-4iya7Jb76fVpQyLoiVpzUrsjQ12r3dM7fIVz+4NwoYvZOShknRmiv+iu9CClZml5ZLGb0XMcYLutK6w9tgxHDw=="], + "rollup": ["rollup@4.46.2", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.46.2", "@rollup/rollup-android-arm64": "4.46.2", "@rollup/rollup-darwin-arm64": "4.46.2", "@rollup/rollup-darwin-x64": "4.46.2", "@rollup/rollup-freebsd-arm64": "4.46.2", "@rollup/rollup-freebsd-x64": "4.46.2", "@rollup/rollup-linux-arm-gnueabihf": "4.46.2", "@rollup/rollup-linux-arm-musleabihf": "4.46.2", "@rollup/rollup-linux-arm64-gnu": "4.46.2", "@rollup/rollup-linux-arm64-musl": "4.46.2", "@rollup/rollup-linux-loongarch64-gnu": "4.46.2", "@rollup/rollup-linux-ppc64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-gnu": "4.46.2", "@rollup/rollup-linux-riscv64-musl": "4.46.2", "@rollup/rollup-linux-s390x-gnu": "4.46.2", "@rollup/rollup-linux-x64-gnu": "4.46.2", "@rollup/rollup-linux-x64-musl": "4.46.2", "@rollup/rollup-win32-arm64-msvc": "4.46.2", "@rollup/rollup-win32-ia32-msvc": "4.46.2", "@rollup/rollup-win32-x64-msvc": "4.46.2", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-WMmLFI+Boh6xbop+OAGo9cQ3OgX9MIg7xOQjn+pTCwOkk+FNDAeAemXkJ3HzDJrVXleLOFVa1ipuc1AmEx1Dwg=="], - "rpc-websockets": ["rpc-websockets@9.1.1", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-1IXGM/TfPT6nfYMIXkJdzn+L4JEsmb0FL1O2OBjaH03V3yuUDdKFulGLMFG6ErV+8pZ5HVC0limve01RyO+saA=="], + "rpc-websockets": ["rpc-websockets@9.1.3", "", { "dependencies": { "@swc/helpers": "^0.5.11", "@types/uuid": "^8.3.4", "@types/ws": "^8.2.2", "buffer": "^6.0.3", "eventemitter3": "^5.0.1", "uuid": "^8.3.2", "ws": "^8.5.0" }, "optionalDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": "^5.0.2" } }, "sha512-I+kNjW0udB4Fetr3vvtRuYZJS0PcSPyyvBcH5sDdoV8DFs5E4W2pTr7aiMlKfPxANTClP9RlqCPolj9dd5MsEA=="], "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], @@ -509,9 +509,9 @@ "uuid": ["uuid@8.3.2", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg=="], - "viem": ["viem@2.33.0", "", { "dependencies": { "@noble/curves": "1.9.2", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.8.1", "ws": "8.18.2" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-SxBM3CmeU+LWLlBclV9MPdbuFV8mQEl0NeRc9iyYU4a7Xb5sr5oku3s/bRGTPpEP+1hCAHYpM09/ui3/dQ6EsA=="], + "viem": ["viem@2.33.2", "", { "dependencies": { "@noble/curves": "1.9.2", "@noble/hashes": "1.8.0", "@scure/bip32": "1.7.0", "@scure/bip39": "1.6.0", "abitype": "1.0.8", "isows": "1.0.7", "ox": "0.8.6", "ws": "8.18.2" }, "peerDependencies": { "typescript": ">=5.0.4" }, "optionalPeers": ["typescript"] }, "sha512-/720OaM4dHWs8vXwNpyet+PRERhPaW+n/1UVSCzyb9jkmwwVfaiy/R6YfCFb4v+XXbo8s3Fapa3DM5yCRSkulA=="], - "vite": ["vite@7.0.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.2", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-1mncVwJxy2C9ThLwz0+2GKZyEXuC3MyWtAAlNftlZZXZDP3AJt5FmwcMit/IGGaNZ8ZOB2BNO/HFUB+CpN0NQw=="], + "vite": ["vite@7.0.6", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.6", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.40.0", "tinyglobby": "^0.2.14" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-MHFiOENNBd+Bd9uvc8GEsIzdkn1JxMmEeYX35tI3fv0sJBUTfW5tQsoaOwuY4KhBI09A3dUJ/DXf2yxPVPUceg=="], "vite-node": ["vite-node@3.2.4", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="], @@ -545,8 +545,6 @@ "@solana/codecs-strings/@solana/errors": ["@solana/errors@2.0.0-rc.1", "", { "dependencies": { "chalk": "^5.3.0", "commander": "^12.1.0" }, "peerDependencies": { "typescript": ">=5" }, "bin": { "errors": "bin/cli.mjs" } }, "sha512-ejNvQ2oJ7+bcFAYWj225lyRkHnixuAeb7RQCixm+5mH4n1IA4Qya/9Bmfy5RAAHQzxK43clu3kZmL5eF9VGtYQ=="], - "@solana/errors/commander": ["commander@13.1.0", "", {}, "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw=="], - "@solana/options/@solana/codecs-core": ["@solana/codecs-core@2.0.0-rc.1", "", { "dependencies": { "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-bauxqMfSs8EHD0JKESaNmNuNvkvHSuN3bbWAF5RjOfDu2PugxHrvRebmYauvSumZ3cTfQ4HJJX6PG5rN852qyQ=="], "@solana/options/@solana/codecs-numbers": ["@solana/codecs-numbers@2.0.0-rc.1", "", { "dependencies": { "@solana/codecs-core": "2.0.0-rc.1", "@solana/errors": "2.0.0-rc.1" }, "peerDependencies": { "typescript": ">=5" } }, "sha512-J5i5mOkvukXn8E3Z7sGIPxsThRCgSdgTWJDQeZvucQ9PT6Y3HiVXJ0pcWiOWAoQ3RX8e/f4I3IC+wE6pZiJzDQ=="], @@ -573,7 +571,7 @@ "rpc-websockets/eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], - "rpc-websockets/ws": ["ws@8.18.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg=="], + "viem/@noble/curves": ["@noble/curves@1.9.2", "", { "dependencies": { "@noble/hashes": "1.8.0" } }, "sha512-HxngEd2XUcg9xi20JkwlLCtYwfoFw4JGkuZpT+WlsPD4gB/cxkvTD8fSsoAnphGZhFdZYKeQIPCuFlWPm1uE0g=="], "@solana/codecs-data-structures/@solana/errors/commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="], diff --git a/lib/constants.ts b/lib/constants.ts index 6e298a67..b365bdcb 100644 --- a/lib/constants.ts +++ b/lib/constants.ts @@ -16,7 +16,9 @@ export namespace Decimals { } export namespace ProgramId { - export const TOKEN_2022 = TOKEN_2022_PROGRAM_ID; - export const TOKEN_METADATA = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + export const CHAINLINK_PROGRAM = new PublicKey("HEvSKofvBgfaexv23kMabbYqxasxU3mQ4ibBMEmJWHny"); + export const CHAINLINK_SOL_USD_FEED = new PublicKey("99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR"); export const TOKEN = TOKEN_PROGRAM_ID; + export const TOKEN_METADATA = new PublicKey("metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s"); + export const TOKEN_2022 = TOKEN_2022_PROGRAM_ID; } diff --git a/programs/lockup/Cargo.toml b/programs/lockup/Cargo.toml index f9dd531a..a58d4ec2 100644 --- a/programs/lockup/Cargo.toml +++ b/programs/lockup/Cargo.toml @@ -20,3 +20,4 @@ anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } anchor-spl = { version = "0.31.1", features = ["metadata"] } mpl-token-metadata = "5.1.0" + chainlink_solana = { git = "https://github.com/smartcontractkit/chainlink-solana", branch = "solana-2.1" } diff --git a/programs/lockup/src/instructions/create_with_durations.rs b/programs/lockup/src/instructions/create_with_durations.rs index c318983a..49abf0cf 100644 --- a/programs/lockup/src/instructions/create_with_durations.rs +++ b/programs/lockup/src/instructions/create_with_durations.rs @@ -10,8 +10,8 @@ pub fn handler( deposit_amount: u64, cliff_duration: i64, total_duration: i64, - start_unlock: u64, - cliff_unlock: u64, + start_unlock_amount: u64, + cliff_unlock_amount: u64, is_cancelable: bool, ) -> Result<()> { // Declare the start time as the current unix timestamp. @@ -35,8 +35,8 @@ pub fn handler( start_time, cliff_time, end_time, - start_unlock, - cliff_unlock, + start_unlock_amount, + cliff_unlock_amount, is_cancelable, ) } diff --git a/programs/lockup/src/instructions/create_with_timestamps.rs b/programs/lockup/src/instructions/create_with_timestamps.rs index 84961c43..4023c0b9 100644 --- a/programs/lockup/src/instructions/create_with_timestamps.rs +++ b/programs/lockup/src/instructions/create_with_timestamps.rs @@ -205,8 +205,8 @@ pub fn handler( start_time: i64, cliff_time: i64, end_time: i64, - start_unlock: u64, - cliff_unlock: u64, + start_unlock_amount: u64, + cliff_unlock_amount: u64, is_cancelable: bool, ) -> Result<()> { let deposit_token_mint = &ctx.accounts.deposit_token_mint; @@ -214,21 +214,21 @@ pub fn handler( let creator_ata = &ctx.accounts.creator_ata; // Validate parameters - check_create(deposit_amount, start_time, cliff_time, end_time, start_unlock, cliff_unlock)?; + check_create(deposit_amount, start_time, cliff_time, end_time, start_unlock_amount, cliff_unlock_amount)?; // Effect: create the stream data. ctx.accounts.stream_data.create( deposit_token_mint.key(), ctx.bumps.stream_data, cliff_time, - cliff_unlock, + cliff_unlock_amount, deposit_amount, end_time, salt, is_cancelable, ctx.accounts.sender.key(), start_time, - start_unlock, + start_unlock_amount, )?; // Effect: mint the NFT to the recipient. diff --git a/programs/lockup/src/instructions/initialize.rs b/programs/lockup/src/instructions/initialize.rs index 8c7018c4..75d7a917 100644 --- a/programs/lockup/src/instructions/initialize.rs +++ b/programs/lockup/src/instructions/initialize.rs @@ -124,8 +124,13 @@ pub struct Initialize<'info> { } /// See the documentation for [`crate::sablier_lockup::initialize`]. -pub fn handler(ctx: Context, fee_collector: Pubkey) -> Result<()> { - ctx.accounts.treasury.initialize(ctx.bumps.treasury, fee_collector)?; +pub fn handler( + ctx: Context, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, +) -> Result<()> { + ctx.accounts.treasury.initialize(ctx.bumps.treasury, fee_collector, chainlink_program, chainlink_sol_usd_feed)?; ctx.accounts.nft_collection_data.initialize(ctx.bumps.nft_collection_data)?; nft::initialize_collection( diff --git a/programs/lockup/src/instructions/view/mod.rs b/programs/lockup/src/instructions/view/mod.rs index ec6d77dd..f5fc7b26 100644 --- a/programs/lockup/src/instructions/view/mod.rs +++ b/programs/lockup/src/instructions/view/mod.rs @@ -3,6 +3,8 @@ pub mod status_of; pub mod stream_view; pub mod streamed_amount_of; pub mod withdrawable_amount_of; +pub mod withdrawal_fee_in_lamports; pub use status_of::*; pub use stream_view::*; +pub use withdrawal_fee_in_lamports::*; diff --git a/programs/lockup/src/instructions/view/withdrawal_fee_in_lamports.rs b/programs/lockup/src/instructions/view/withdrawal_fee_in_lamports.rs new file mode 100644 index 00000000..a2afff2e --- /dev/null +++ b/programs/lockup/src/instructions/view/withdrawal_fee_in_lamports.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; + +use crate::{ + state::treasury::Treasury, + utils::{ + constants::{seeds::TREASURY, WITHDRAWAL_FEE_USD}, + fee_calculation::convert_usd_fee_to_lamports, + }, +}; + +#[derive(Accounts)] +pub struct WithdrawalFeeInLamports<'info> { + /// Read account: the treasury account that receives the withdrawal fee. + #[account( + seeds = [TREASURY], + bump = treasury.bump + )] + pub treasury: Box>, + + /// Read account: The Chainlink program used to retrieve on-chain price feeds. + /// CHECK: This is the Chainlink program. + #[account(address = treasury.chainlink_program)] + pub chainlink_program: AccountInfo<'info>, + + /// Read account: The account providing the SOL/USD price feed data. + /// CHECK: We're reading data from this Chainlink feed. + #[account(address = treasury.chainlink_sol_usd_feed)] + pub chainlink_sol_usd_feed: AccountInfo<'info>, +} + +pub fn handler(ctx: Context) -> Result { + let fee_in_lamports = convert_usd_fee_to_lamports( + WITHDRAWAL_FEE_USD, + ctx.accounts.chainlink_program.to_account_info(), + ctx.accounts.chainlink_sol_usd_feed.to_account_info(), + ); + + Ok(fee_in_lamports) +} diff --git a/programs/lockup/src/instructions/withdraw.rs b/programs/lockup/src/instructions/withdraw.rs index d86e3090..ffbdf83e 100644 --- a/programs/lockup/src/instructions/withdraw.rs +++ b/programs/lockup/src/instructions/withdraw.rs @@ -10,13 +10,15 @@ use anchor_spl::{ use crate::{ state::{lockup::StreamData, treasury::Treasury}, utils::{ - constants::seeds::*, events::WithdrawFromLockupStream, lockup_math::get_withdrawable_amount, - transfer_helper::transfer_tokens, validations::check_withdraw, + constants::{seeds::*, WITHDRAWAL_FEE_USD}, + events::WithdrawFromLockupStream, + fee_calculation::convert_usd_fee_to_lamports, + lockup_math::get_withdrawable_amount, + transfer_helper::transfer_tokens, + validations::check_withdraw, }, }; -const WITHDRAWAL_FEE: u64 = 10_000_000; // The fee for withdrawing from the stream, in lamports. - #[derive(Accounts)] pub struct Withdraw<'info> { // -------------------------------------------------------------------------- // @@ -108,6 +110,16 @@ pub struct Withdraw<'info> { /// Program account: the Associated Token program. pub associated_token_program: Program<'info, AssociatedToken>, + /// Read account: The Chainlink program used to retrieve on-chain price feeds. + /// CHECK: This is the Chainlink program. + #[account(address = treasury.chainlink_program)] + pub chainlink_program: AccountInfo<'info>, + + /// Read account: The account providing the SOL/USD price feed data. + /// CHECK: We're reading data from this Chainlink feed. + #[account(address = treasury.chainlink_sol_usd_feed)] + pub chainlink_sol_usd_feed: AccountInfo<'info>, + /// Program account: the Token program of the deposited token. pub deposited_token_program: Interface<'info, TokenInterface>, @@ -133,9 +145,13 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { // Effect: update the stream data state. ctx.accounts.stream_data.withdraw(amount)?; - // Interaction: transfer the fee from the signer to the treasury. - let fee_collection_ix = transfer(&ctx.accounts.signer.key(), &ctx.accounts.treasury.key(), WITHDRAWAL_FEE); - invoke(&fee_collection_ix, &[ctx.accounts.signer.to_account_info(), ctx.accounts.treasury.to_account_info()])?; + // Interaction: charge the withdrawal fee. + let fee_in_lamports = charge_withdrawal_fee( + ctx.accounts.chainlink_program.to_account_info(), + ctx.accounts.chainlink_sol_usd_feed.to_account_info(), + ctx.accounts.signer.to_account_info(), + ctx.accounts.treasury.to_account_info(), + )?; // Interaction: transfer the tokens from the stream ATA to the recipient. transfer_tokens( @@ -152,10 +168,29 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { // Log the withdrawal. emit!(WithdrawFromLockupStream { deposited_token_mint: ctx.accounts.deposited_token_mint.key(), + fee_in_lamports, stream_data: ctx.accounts.stream_data.key(), stream_nft_mint: ctx.accounts.stream_nft_mint.key(), - withdrawn_amount: amount + withdrawn_amount: amount, }); Ok(()) } + +/// Charges the withdrawal fee in lamports. +fn charge_withdrawal_fee<'info>( + chainlink_program: AccountInfo<'info>, + chainlink_sol_usd_feed: AccountInfo<'info>, + tx_signer: AccountInfo<'info>, + treasury: AccountInfo<'info>, +) -> Result { + // Calculate the fee in lamports. + let fee_in_lamports: u64 = + convert_usd_fee_to_lamports(WITHDRAWAL_FEE_USD, chainlink_program, chainlink_sol_usd_feed); + + // Interaction: transfer the fee from the signer to the treasury. + let fee_charging_ix = transfer(&tx_signer.key(), &treasury.key(), fee_in_lamports); + invoke(&fee_charging_ix, &[tx_signer, treasury])?; + + Ok(fee_in_lamports) +} diff --git a/programs/lockup/src/lib.rs b/programs/lockup/src/lib.rs index cda55335..45c35215 100644 --- a/programs/lockup/src/lib.rs +++ b/programs/lockup/src/lib.rs @@ -83,14 +83,14 @@ pub mod sablier_lockup { /// /// Refer to the requirements in [`create_with_timestamps`]. #[allow(clippy::too_many_arguments)] - pub fn create_with_durations( + pub fn create_with_durations_ll( ctx: Context, salt: u128, deposit_amount: u64, cliff_duration: i64, total_duration: i64, - start_unlock: u64, - cliff_unlock: u64, + start_unlock_amount: u64, + cliff_unlock_amount: u64, is_cancelable: bool, ) -> Result<()> { instructions::create_with_durations::handler( @@ -99,8 +99,8 @@ pub mod sablier_lockup { deposit_amount, cliff_duration, total_duration, - start_unlock, - cliff_unlock, + start_unlock_amount, + cliff_unlock_amount, is_cancelable, ) } @@ -124,8 +124,8 @@ pub mod sablier_lockup { /// - `start_time` The Unix timestamp indicating the stream's start. /// - `cliff_time` The Unix timestamp indicating the stream's cliff. /// - `end_time` The Unix timestamp indicating the stream's end. - /// - `start_unlock` The amount to be unlocked at the start time. - /// - `cliff_unlock` The amount to be unlocked at the cliff time. + /// - `start_unlock_amount` The amount to be unlocked at the start time. + /// - `cliff_unlock_amount` The amount to be unlocked at the cliff time. /// - `is_cancelable` Indicates if the stream is cancelable. /// /// # Notes @@ -142,18 +142,18 @@ pub mod sablier_lockup { /// - `deposit_amount` must be greater than zero. /// - `start_time` must be greater than zero and less than `end_time`. /// - If set, `cliff_time` must be greater than `start_time` and less than `end_time`. - /// - The sum of `start_unlock` and `cliff_unlock` must be less than or equal to deposit amount. - /// - If `cliff_time` is not set, the `cliff_unlock` amount must be zero. + /// - The sum of `start_unlock_amount` and `cliff_unlock_amount` must be less than or equal to deposit amount. + /// - If `cliff_time` is not set, the `cliff_unlock_amount` amount must be zero. #[allow(clippy::too_many_arguments)] - pub fn create_with_timestamps( + pub fn create_with_timestamps_ll( ctx: Context, salt: u128, deposit_amount: u64, start_time: i64, cliff_time: i64, end_time: i64, - start_unlock: u64, - cliff_unlock: u64, + start_unlock_amount: u64, + cliff_unlock_amount: u64, is_cancelable: bool, ) -> Result<()> { instructions::create_with_timestamps::handler( @@ -163,8 +163,8 @@ pub mod sablier_lockup { start_time, cliff_time, end_time, - start_unlock, - cliff_unlock, + start_unlock_amount, + cliff_unlock_amount, is_cancelable, ) } @@ -176,11 +176,18 @@ pub mod sablier_lockup { /// - `initializer` The transaction signer. /// - `nft_token_program` The Token Program of the NFT collection. /// - /// # Parameters + /// # Parameters: /// - /// - `fee_collector` The address that will have the authority to collect fees. - pub fn initialize(ctx: Context, fee_collector: Pubkey) -> Result<()> { - instructions::initialize::handler(ctx, fee_collector) + /// - `fee_collector`: The address that will have the authority to collect fees. + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. + pub fn initialize( + ctx: Context, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, + ) -> Result<()> { + instructions::initialize::handler(ctx, fee_collector, chainlink_program, chainlink_sol_usd_feed) } /// Removes the right of the stream's sender to cancel the stream. @@ -207,6 +214,8 @@ pub mod sablier_lockup { /// - `withdrawal_recipient` The address of the recipient receiving the withdrawn tokens. /// - `deposited_token_program` The Token Program of the deposited token. /// - `nft_token_program` The Token Program of the NFT. + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. /// /// # Parameters /// @@ -215,7 +224,7 @@ pub mod sablier_lockup { /// # Notes /// /// - If the withdrawal recipient does not have an ATA for the deposited token, one is created. - /// - The signer must pay a fee in the native (SOL) token. + /// - The instruction charges a fee in the native token (SOL), equivalent to $1 USD. /// - Emits [`crate::utils::events::WithdrawFromLockupStream`] event. /// /// # Requirements @@ -224,6 +233,7 @@ pub mod sablier_lockup { /// - `withdrawal_recipient` must be the recipient if the signer is not the stream's recipient. /// - `amount` must be greater than zero and must not exceed the withdrawable amount. /// - The stream must not be Depleted. + /// - `chainlink_program` and `chainlink_sol_usd_feed` must match the ones stored in the treasury. pub fn withdraw(ctx: Context, amount: u64) -> Result<()> { instructions::withdraw::handler(ctx, amount) } @@ -308,4 +318,14 @@ pub mod sablier_lockup { pub fn withdrawable_amount_of(ctx: Context) -> Result { instructions::withdrawable_amount_of::handler(ctx) } + + /// Calculates the withdrawal fee in lamports, which is equivalent to $1 USD. + /// + /// # Accounts Expected: + /// + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. + pub fn withdrawal_fee_in_lamports(ctx: Context) -> Result { + instructions::withdrawal_fee_in_lamports::handler(ctx) + } } diff --git a/programs/lockup/src/state/lockup.rs b/programs/lockup/src/state/lockup.rs index 3b72e74f..de337423 100644 --- a/programs/lockup/src/state/lockup.rs +++ b/programs/lockup/src/state/lockup.rs @@ -56,21 +56,21 @@ impl StreamData { deposited_token_mint: Pubkey, bump: u8, cliff_time: i64, - cliff_unlock: u64, + cliff_unlock_amount: u64, deposit_amount: u64, end_time: i64, salt: u128, is_cancelable: bool, sender: Pubkey, start_time: i64, - start_unlock: u64, + start_unlock_amount: u64, ) -> Result<()> { self.bump = bump; self.amounts = Amounts { - cliff_unlock, + cliff_unlock: cliff_unlock_amount, deposited: deposit_amount, refunded: 0, - start_unlock, + start_unlock: start_unlock_amount, withdrawn: 0, }; self.deposited_token_mint = deposited_token_mint; diff --git a/programs/lockup/src/state/treasury.rs b/programs/lockup/src/state/treasury.rs index f596823e..592a537f 100644 --- a/programs/lockup/src/state/treasury.rs +++ b/programs/lockup/src/state/treasury.rs @@ -5,13 +5,23 @@ use anchor_lang::prelude::*; pub struct Treasury { pub bump: u8, pub fee_collector: Pubkey, + pub chainlink_program: Pubkey, + pub chainlink_sol_usd_feed: Pubkey, } impl Treasury { /// State update for the [`crate::sablier_lockup::initialize`] instruction. - pub fn initialize(&mut self, bump: u8, fee_collector: Pubkey) -> Result<()> { + pub fn initialize( + &mut self, + bump: u8, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, + ) -> Result<()> { self.bump = bump; self.fee_collector = fee_collector; + self.chainlink_program = chainlink_program; + self.chainlink_sol_usd_feed = chainlink_sol_usd_feed; Ok(()) } diff --git a/programs/lockup/src/utils/constants.rs b/programs/lockup/src/utils/constants.rs index 22d6fd10..8a781649 100644 --- a/programs/lockup/src/utils/constants.rs +++ b/programs/lockup/src/utils/constants.rs @@ -1,4 +1,7 @@ pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8; +// The fee amount in USD, denominated in Chainlink's 8-decimal format for USD prices, where 1e8 is $1. +pub const WITHDRAWAL_FEE_USD: u64 = 1e8 as u64; +pub const LAMPORTS_PER_SOL: u64 = 1e9 as u64; // 1 billion lamports in 1 SOL // Account Seeds pub mod seeds { diff --git a/programs/lockup/src/utils/events.rs b/programs/lockup/src/utils/events.rs index 2deafece..f8ed787e 100644 --- a/programs/lockup/src/utils/events.rs +++ b/programs/lockup/src/utils/events.rs @@ -36,6 +36,7 @@ pub struct RenounceLockupStream { #[event] pub struct WithdrawFromLockupStream { pub deposited_token_mint: Pubkey, + pub fee_in_lamports: u64, pub stream_data: Pubkey, pub stream_nft_mint: Pubkey, pub withdrawn_amount: u64, diff --git a/programs/lockup/src/utils/fee_calculation.rs b/programs/lockup/src/utils/fee_calculation.rs new file mode 100644 index 00000000..a3af2e6c --- /dev/null +++ b/programs/lockup/src/utils/fee_calculation.rs @@ -0,0 +1,68 @@ +use crate::utils::constants::LAMPORTS_PER_SOL; +use anchor_lang::prelude::*; +use chainlink_solana as chainlink; + +// TODO: export this into a crate that'd be imported by both the lockup and merkle_instant programs. +/// Converts the fee amount from USD to lamports. +/// The price is considered to be 0 if: +/// 1. The USD fee is 0. +/// 2. The oracle price is ≤ 0. +/// 3. The oracle's update timestamp is in the future. +/// 4. The oracle price hasn't been updated in the last 24 hours. +pub fn convert_usd_fee_to_lamports<'info>( + fee_usd: u64, + chainlink_program: AccountInfo<'info>, + chainlink_sol_usd_feed: AccountInfo<'info>, +) -> u64 { + // If the USD fee is 0, skip the calculations. + if fee_usd == 0 { + return 0; + } + + // Interactions: query the oracle price and the time at which it was updated. + let round = match chainlink::latest_round_data(chainlink_program.clone(), chainlink_sol_usd_feed.clone()) { + Ok(round) => round, + Err(_) => return 0, // If the oracle call fails, skip fee charging. + }; + + // If the price is not greater than 0, skip the calculations. + if round.answer <= 0 { + return 0; + } + + let current_timestamp: u32 = Clock::get().unwrap().unix_timestamp as u32; + + // Due to reorgs and latency issues, the oracle can have a timestamp that is in the future. In + // this case, we ignore the price and skip fee charging. + if current_timestamp < round.timestamp { + return 0; + } + + // If the oracle hasn't been updated in the last 24 hours, we ignore the price and skip fee charging. This is a + // safety check to avoid using outdated prices. + const SECONDS_IN_24_HOURS: u32 = 86400; + if current_timestamp - round.timestamp > SECONDS_IN_24_HOURS { + return 0; + } + + // Interactions: query the oracle decimals. + let oracle_decimals = match chainlink::decimals(chainlink_program.clone(), chainlink_sol_usd_feed.clone()) { + Ok(decimals) => decimals, + Err(_) => return 0, // If the oracle call fails, skip fee charging. + }; + + let price = round.answer as u64; + + let fee_in_lamports: u64 = match oracle_decimals { + 8 => { + // If the oracle decimals are 8, calculate the fee. + fee_usd * LAMPORTS_PER_SOL / price + } + decimals => { + // Otherwise, adjust the calculation to account for the oracle decimals. + fee_usd * 10_u64.pow(1 + decimals as u32) / price + } + }; + + fee_in_lamports +} diff --git a/programs/lockup/src/utils/mod.rs b/programs/lockup/src/utils/mod.rs index dddec081..983c5a88 100644 --- a/programs/lockup/src/utils/mod.rs +++ b/programs/lockup/src/utils/mod.rs @@ -1,6 +1,7 @@ pub mod constants; pub mod errors; pub mod events; +pub mod fee_calculation; pub mod lockup_math; pub mod nft; pub mod transfer_helper; diff --git a/programs/lockup/src/utils/validations.rs b/programs/lockup/src/utils/validations.rs index 56ea6c79..c0c7a485 100644 --- a/programs/lockup/src/utils/validations.rs +++ b/programs/lockup/src/utils/validations.rs @@ -46,8 +46,8 @@ pub fn check_create( start_time: i64, cliff_time: i64, end_time: i64, - start_unlock: u64, - cliff_unlock: u64, + start_unlock_amount: u64, + cliff_unlock_amount: u64, ) -> Result<()> { // Check: the deposit amount is not zero. if deposit_amount == 0 { @@ -77,13 +77,14 @@ pub fn check_create( } } // Check: the cliff unlock amount is zero when the cliff time is zero. - else if cliff_unlock > 0 { + else if cliff_unlock_amount > 0 { return Err(ErrorCode::CliffTimeZeroUnlockAmountNotZero.into()); } // Check: the sum of the start and cliff unlock amounts is not greater than the deposit amount. - let total_unlock = start_unlock.checked_add(cliff_unlock).ok_or(ErrorCode::UnlockAmountsSumTooHigh)?; - if total_unlock > deposit_amount { + let total_unlock_amount = + start_unlock_amount.checked_add(cliff_unlock_amount).ok_or(ErrorCode::UnlockAmountsSumTooHigh)?; + if total_unlock_amount > deposit_amount { return Err(ErrorCode::UnlockAmountsSumTooHigh.into()); } diff --git a/programs/merkle_instant/Cargo.toml b/programs/merkle_instant/Cargo.toml index 3b1126c8..eca92ed6 100644 --- a/programs/merkle_instant/Cargo.toml +++ b/programs/merkle_instant/Cargo.toml @@ -20,6 +20,4 @@ anchor-lang = { version = "0.31.1", features = ["init-if-needed"] } anchor-spl = { version = "0.31.1" } - bitvec = "1.0.1" - # svm-merkle-tree = { git = "https://github.com/deanmlittle/svm-merkle-tree", version = "0.1.1" } - solana-merkle-tree = { version = "2.2.12" } + chainlink_solana = { git = "https://github.com/smartcontractkit/chainlink-solana", branch = "solana-2.1" } diff --git a/programs/merkle_instant/src/instructions/claim.rs b/programs/merkle_instant/src/instructions/claim.rs index 222062f0..972727c8 100644 --- a/programs/merkle_instant/src/instructions/claim.rs +++ b/programs/merkle_instant/src/instructions/claim.rs @@ -10,15 +10,14 @@ use anchor_spl::{ use crate::{ state::{Campaign, ClaimReceipt, Treasury}, utils::{ - constants::{ANCHOR_DISCRIMINATOR_SIZE, CAMPAIGN_SEED, CLAIM_RECEIPT_SEED, TREASURY_SEED}, + constants::{seeds::*, ANCHOR_DISCRIMINATOR_SIZE, CLAIM_FEE_USD}, events, + fee_calculation::convert_usd_fee_to_lamports, transfer_helper::transfer_tokens, validations::check_claim, }, }; -const CLAIM_FEE: u64 = 30_000_000; // The fee for claiming an airdrop, in lamports. - #[derive(Accounts)] #[instruction(index: u32)] pub struct Claim<'info> { @@ -49,7 +48,7 @@ pub struct Claim<'info> { /// Write account: the treasury account that will receive the claim fee. #[account( mut, - seeds = [TREASURY_SEED], + seeds = [TREASURY], bump = treasury.bump )] pub treasury: Box>, @@ -80,7 +79,7 @@ pub struct Claim<'info> { payer = claimer, space = ANCHOR_DISCRIMINATOR_SIZE + ClaimReceipt::INIT_SPACE, seeds = [ - CLAIM_RECEIPT_SEED, + CLAIM_RECEIPT, campaign.key().as_ref(), index.to_le_bytes().as_ref(), ], @@ -97,6 +96,16 @@ pub struct Claim<'info> { /// Program account: the Associated Token program. pub associated_token_program: Program<'info, AssociatedToken>, + /// Read account: The Chainlink program used to retrieve on-chain price feeds. + /// CHECK: This is the Chainlink program. + #[account(address = treasury.chainlink_program)] + pub chainlink_program: AccountInfo<'info>, + + /// Read account: The account providing the SOL/USD price feed data. + /// CHECK: We're reading data from this Chainlink feed. + #[account(address = treasury.chainlink_sol_usd_feed)] + pub chainlink_sol_usd_feed: AccountInfo<'info>, + // -------------------------------------------------------------------------- // // SYSTEM ACCOUNTS // // -------------------------------------------------------------------------- // @@ -110,7 +119,6 @@ pub fn handler(ctx: Context, index: u32, amount: u64, merkle_proof: Vec<[ let airdrop_token_mint = ctx.accounts.airdrop_token_mint.clone(); let claimer = ctx.accounts.claimer.clone(); let recipient = ctx.accounts.recipient.clone(); - let treasury = ctx.accounts.treasury.clone(); // Check: validate the claim. check_claim( @@ -125,9 +133,13 @@ pub fn handler(ctx: Context, index: u32, amount: u64, merkle_proof: Vec<[ ctx.accounts.campaign.claim()?; - // Interaction: transfer the fee from the claimer to the treasury. - let fee_collection_ix = transfer(&claimer.key(), &treasury.key(), CLAIM_FEE); - invoke(&fee_collection_ix, &[claimer.to_account_info(), treasury.to_account_info()])?; + // Interaction: charge the claim fee. + let fee_in_lamports = charge_claim_fee( + ctx.accounts.chainlink_program.to_account_info(), + ctx.accounts.chainlink_sol_usd_feed.to_account_info(), + ctx.accounts.claimer.to_account_info(), + ctx.accounts.treasury.to_account_info(), + )?; // Interaction: transfer tokens from the campaign's ATA to the recipient's ATA. transfer_tokens( @@ -139,7 +151,7 @@ pub fn handler(ctx: Context, index: u32, amount: u64, merkle_proof: Vec<[ amount, airdrop_token_mint.decimals, &[&[ - CAMPAIGN_SEED, + CAMPAIGN, campaign.creator.key().as_ref(), campaign.merkle_root.as_ref(), campaign.campaign_start_time.to_le_bytes().as_ref(), @@ -156,9 +168,27 @@ pub fn handler(ctx: Context, index: u32, amount: u64, merkle_proof: Vec<[ campaign: campaign.key(), claimer: claimer.key(), claim_receipt: ctx.accounts.claim_receipt.key(), + fee_in_lamports, index, recipient: recipient.key(), }); Ok(()) } + +/// Charges the claim fee in lamports. +fn charge_claim_fee<'info>( + chainlink_program: AccountInfo<'info>, + chainlink_sol_usd_feed: AccountInfo<'info>, + tx_signer: AccountInfo<'info>, + treasury: AccountInfo<'info>, +) -> Result { + // Calculate the fee in lamports. + let fee_in_lamports: u64 = convert_usd_fee_to_lamports(CLAIM_FEE_USD, chainlink_program, chainlink_sol_usd_feed); + + // Interaction: transfer the fee from the signer to the treasury. + let fee_charging_ix = transfer(&tx_signer.key(), &treasury.key(), fee_in_lamports); + invoke(&fee_charging_ix, &[tx_signer, treasury])?; + + Ok(fee_in_lamports) +} diff --git a/programs/merkle_instant/src/instructions/clawback.rs b/programs/merkle_instant/src/instructions/clawback.rs index ce1dd858..1b2fb132 100644 --- a/programs/merkle_instant/src/instructions/clawback.rs +++ b/programs/merkle_instant/src/instructions/clawback.rs @@ -6,7 +6,7 @@ use anchor_spl::{ use crate::{ state::Campaign, - utils::{constants::CAMPAIGN_SEED, events, transfer_helper::transfer_tokens, validations::check_clawback}, + utils::{constants::seeds::CAMPAIGN, events, transfer_helper::transfer_tokens, validations::check_clawback}, }; #[derive(Accounts)] @@ -88,7 +88,7 @@ pub fn handler(ctx: Context, amount: u64) -> Result<()> { amount, airdrop_token_mint.decimals, &[&[ - CAMPAIGN_SEED, + CAMPAIGN, campaign.creator.key().as_ref(), campaign.merkle_root.as_ref(), campaign.campaign_start_time.to_le_bytes().as_ref(), diff --git a/programs/merkle_instant/src/instructions/collect_fees.rs b/programs/merkle_instant/src/instructions/collect_fees.rs index b6948c0f..7f25d9ef 100644 --- a/programs/merkle_instant/src/instructions/collect_fees.rs +++ b/programs/merkle_instant/src/instructions/collect_fees.rs @@ -2,7 +2,7 @@ use anchor_lang::prelude::*; use crate::{ state::Treasury, - utils::{constants::*, events, validations::check_collect_fees}, + utils::{constants::seeds::TREASURY, events, validations::check_collect_fees}, }; #[derive(Accounts)] @@ -25,7 +25,7 @@ pub struct CollectFees<'info> { /// Write account: the treasury account that holds the fees. #[account( mut, - seeds = [TREASURY_SEED], + seeds = [TREASURY], bump = treasury.bump, )] pub treasury: Box>, diff --git a/programs/merkle_instant/src/instructions/create_campaign.rs b/programs/merkle_instant/src/instructions/create_campaign.rs index 831becc1..5a6fc945 100644 --- a/programs/merkle_instant/src/instructions/create_campaign.rs +++ b/programs/merkle_instant/src/instructions/create_campaign.rs @@ -7,7 +7,7 @@ use anchor_spl::{ use crate::{ state::Campaign, utils::{ - constants::{ANCHOR_DISCRIMINATOR_SIZE, CAMPAIGN_SEED}, + constants::{seeds::CAMPAIGN, ANCHOR_DISCRIMINATOR_SIZE}, events, }, }; @@ -40,7 +40,7 @@ pub struct CreateCampaign<'info> { payer = creator, space = ANCHOR_DISCRIMINATOR_SIZE + Campaign::INIT_SPACE, seeds = [ - CAMPAIGN_SEED, + CAMPAIGN, creator.key().as_ref(), merkle_root.as_ref(), campaign_start_time.to_le_bytes().as_ref(), diff --git a/programs/merkle_instant/src/instructions/initialize.rs b/programs/merkle_instant/src/instructions/initialize.rs index b95ec636..1f371184 100644 --- a/programs/merkle_instant/src/instructions/initialize.rs +++ b/programs/merkle_instant/src/instructions/initialize.rs @@ -1,6 +1,9 @@ use anchor_lang::prelude::*; -use crate::{state::Treasury, utils::constants::*}; +use crate::{ + state::Treasury, + utils::constants::{seeds::TREASURY, ANCHOR_DISCRIMINATOR_SIZE}, +}; #[derive(Accounts)] pub struct Initialize<'info> { @@ -18,7 +21,7 @@ pub struct Initialize<'info> { #[account( init, payer = initializer, - seeds = [TREASURY_SEED], + seeds = [TREASURY], space = ANCHOR_DISCRIMINATOR_SIZE + Treasury::INIT_SPACE, bump )] @@ -32,8 +35,13 @@ pub struct Initialize<'info> { } /// See the documentation for [`crate::sablier_merkle_instant::initialize`]. -pub fn handler(ctx: Context, fee_collector: Pubkey) -> Result<()> { - ctx.accounts.treasury.initialize(ctx.bumps.treasury, fee_collector)?; +pub fn handler( + ctx: Context, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, +) -> Result<()> { + ctx.accounts.treasury.initialize(ctx.bumps.treasury, fee_collector, chainlink_program, chainlink_sol_usd_feed)?; Ok(()) } diff --git a/programs/merkle_instant/src/instructions/view/claim_fee_in_lamports.rs b/programs/merkle_instant/src/instructions/view/claim_fee_in_lamports.rs new file mode 100644 index 00000000..3bf27321 --- /dev/null +++ b/programs/merkle_instant/src/instructions/view/claim_fee_in_lamports.rs @@ -0,0 +1,39 @@ +use anchor_lang::prelude::*; + +use crate::{ + state::treasury::Treasury, + utils::{ + constants::{seeds::TREASURY, CLAIM_FEE_USD}, + fee_calculation::convert_usd_fee_to_lamports, + }, +}; + +#[derive(Accounts)] +pub struct ClaimFeeInLamports<'info> { + /// Read account: the treasury account that receives the claim fee. + #[account( + seeds = [TREASURY], + bump = treasury.bump + )] + pub treasury: Box>, + + /// Read account: The Chainlink program used to retrieve on-chain price feeds. + /// CHECK: This is the Chainlink program. + #[account(address = treasury.chainlink_program)] + pub chainlink_program: AccountInfo<'info>, + + /// Read account: The account providing the SOL/USD price feed data. + /// CHECK: We're reading data from this Chainlink feed. + #[account(address = treasury.chainlink_sol_usd_feed)] + pub chainlink_sol_usd_feed: AccountInfo<'info>, +} + +pub fn handler(ctx: Context) -> Result { + let fee_in_lamports = convert_usd_fee_to_lamports( + CLAIM_FEE_USD, + ctx.accounts.chainlink_program.to_account_info(), + ctx.accounts.chainlink_sol_usd_feed.to_account_info(), + ); + + Ok(fee_in_lamports) +} diff --git a/programs/merkle_instant/src/instructions/view/has_claimed.rs b/programs/merkle_instant/src/instructions/view/has_claimed.rs index 985075af..d30ded2c 100644 --- a/programs/merkle_instant/src/instructions/view/has_claimed.rs +++ b/programs/merkle_instant/src/instructions/view/has_claimed.rs @@ -1,6 +1,6 @@ use anchor_lang::prelude::*; -use crate::{state::Campaign, utils::constants::*}; +use crate::{state::Campaign, utils::constants::seeds::CLAIM_RECEIPT}; #[derive(Accounts)] #[instruction(_index: u32)] @@ -15,7 +15,7 @@ pub struct HasClaimed<'info> { /// CHECK: If it exists, return true, otherwise false. #[account( seeds = [ - CLAIM_RECEIPT_SEED, + CLAIM_RECEIPT, campaign.key().as_ref(), _index.to_le_bytes().as_ref(), ], diff --git a/programs/merkle_instant/src/instructions/view/mod.rs b/programs/merkle_instant/src/instructions/view/mod.rs index 94f91fdb..44ea347e 100644 --- a/programs/merkle_instant/src/instructions/view/mod.rs +++ b/programs/merkle_instant/src/instructions/view/mod.rs @@ -1,8 +1,10 @@ pub mod campaign_view; +pub mod claim_fee_in_lamports; pub mod has_campaign_started; pub mod has_claimed; pub mod has_expired; pub mod has_grace_period_passed; pub use campaign_view::*; +pub use claim_fee_in_lamports::*; pub use has_claimed::*; diff --git a/programs/merkle_instant/src/lib.rs b/programs/merkle_instant/src/lib.rs index 429ae38c..c913ff3f 100644 --- a/programs/merkle_instant/src/lib.rs +++ b/programs/merkle_instant/src/lib.rs @@ -28,6 +28,8 @@ pub mod sablier_merkle_instant { /// - `recipient` The address of the airdrop recipient. /// - `airdrop_token_mint` The mint of the airdropped token. /// - `airdrop_token_program` The Token Program of the airdropped token. + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. /// /// # Parameters /// @@ -37,6 +39,7 @@ pub mod sablier_merkle_instant { /// /// # Notes /// + /// - The instruction charges a fee in the native token (SOL), equivalent to $2 USD. /// - Emits a [`crate::utils::events::Claim`] event. /// /// # Requirements @@ -46,6 +49,7 @@ pub mod sablier_merkle_instant { /// - The campaign must not have expired. /// - The recipient's airdrop has not been claimed yet. /// - The Merkle proof must be valid. + /// - `chainlink_program` and `chainlink_sol_usd_feed` must match the ones stored in the treasury. pub fn claim(ctx: Context, index: u32, amount: u64, merkle_proof: Vec<[u8; 32]>) -> Result<()> { instructions::claim::handler(ctx, index, amount, merkle_proof) } @@ -152,8 +156,15 @@ pub mod sablier_merkle_instant { /// # Parameters /// /// - `fee_collector` The address that will have the authority to collect fees. - pub fn initialize(ctx: Context, fee_collector: Pubkey) -> Result<()> { - instructions::initialize::handler(ctx, fee_collector) + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. + pub fn initialize( + ctx: Context, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, + ) -> Result<()> { + instructions::initialize::handler(ctx, fee_collector, chainlink_program, chainlink_sol_usd_feed) } // -------------------------------------------------------------------------- // @@ -169,6 +180,16 @@ pub mod sablier_merkle_instant { instructions::campaign_view::handler(ctx) } + /// Calculates the claim fee in lamports, which is equivalent to $2 USD. + /// + /// # Accounts Expected: + /// + /// - `chainlink_program`: The Chainlink program used to retrieve on-chain price feeds. + /// - `chainlink_sol_usd_feed`: The account providing the SOL/USD price feed data. + pub fn claim_fee_in_lamports(ctx: Context) -> Result { + instructions::claim_fee_in_lamports::handler(ctx) + } + /// Returns a flag indicating whether a claim has been made for the given index. /// /// # Accounts Expected diff --git a/programs/merkle_instant/src/state/treasury.rs b/programs/merkle_instant/src/state/treasury.rs index f329e9d5..1dc53c2a 100644 --- a/programs/merkle_instant/src/state/treasury.rs +++ b/programs/merkle_instant/src/state/treasury.rs @@ -5,13 +5,23 @@ use anchor_lang::prelude::*; pub struct Treasury { pub bump: u8, pub fee_collector: Pubkey, + pub chainlink_program: Pubkey, + pub chainlink_sol_usd_feed: Pubkey, } impl Treasury { /// State update for the [`crate::sablier_merkle_instant::initialize`] instruction. - pub fn initialize(&mut self, bump: u8, fee_collector: Pubkey) -> Result<()> { + pub fn initialize( + &mut self, + bump: u8, + fee_collector: Pubkey, + chainlink_program: Pubkey, + chainlink_sol_usd_feed: Pubkey, + ) -> Result<()> { self.bump = bump; self.fee_collector = fee_collector; + self.chainlink_program = chainlink_program; + self.chainlink_sol_usd_feed = chainlink_sol_usd_feed; Ok(()) } diff --git a/programs/merkle_instant/src/utils/constants.rs b/programs/merkle_instant/src/utils/constants.rs index 550a713b..72a09668 100644 --- a/programs/merkle_instant/src/utils/constants.rs +++ b/programs/merkle_instant/src/utils/constants.rs @@ -1,8 +1,13 @@ pub const ANCHOR_DISCRIMINATOR_SIZE: usize = 8; +// The fee amount in USD, denominated in Chainlink's 8-decimal format for USD prices, where 1e8 is $1. +pub const CLAIM_FEE_USD: u64 = 2e8 as u64; +pub const LAMPORTS_PER_SOL: u64 = 1e9 as u64; // 1 billion lamports in 1 SOL pub const CAMPAIGN_IPFS_CID_SIZE: usize = 59; pub const CAMPAIGN_NAME_SIZE: usize = 32; // Account Seeds -pub const CAMPAIGN_SEED: &[u8] = b"campaign"; -pub const CLAIM_RECEIPT_SEED: &[u8] = b"claim_receipt"; -pub const TREASURY_SEED: &[u8] = b"treasury"; +pub mod seeds { + pub const CAMPAIGN: &[u8] = b"campaign"; + pub const CLAIM_RECEIPT: &[u8] = b"claim_receipt"; + pub const TREASURY: &[u8] = b"treasury"; +} diff --git a/programs/merkle_instant/src/utils/events.rs b/programs/merkle_instant/src/utils/events.rs index e384baf6..d9d54f34 100644 --- a/programs/merkle_instant/src/utils/events.rs +++ b/programs/merkle_instant/src/utils/events.rs @@ -21,6 +21,7 @@ pub struct Claim { pub campaign: Pubkey, pub claimer: Pubkey, pub claim_receipt: Pubkey, + pub fee_in_lamports: u64, pub index: u32, pub recipient: Pubkey, } diff --git a/programs/merkle_instant/src/utils/fee_calculation.rs b/programs/merkle_instant/src/utils/fee_calculation.rs new file mode 100644 index 00000000..a3af2e6c --- /dev/null +++ b/programs/merkle_instant/src/utils/fee_calculation.rs @@ -0,0 +1,68 @@ +use crate::utils::constants::LAMPORTS_PER_SOL; +use anchor_lang::prelude::*; +use chainlink_solana as chainlink; + +// TODO: export this into a crate that'd be imported by both the lockup and merkle_instant programs. +/// Converts the fee amount from USD to lamports. +/// The price is considered to be 0 if: +/// 1. The USD fee is 0. +/// 2. The oracle price is ≤ 0. +/// 3. The oracle's update timestamp is in the future. +/// 4. The oracle price hasn't been updated in the last 24 hours. +pub fn convert_usd_fee_to_lamports<'info>( + fee_usd: u64, + chainlink_program: AccountInfo<'info>, + chainlink_sol_usd_feed: AccountInfo<'info>, +) -> u64 { + // If the USD fee is 0, skip the calculations. + if fee_usd == 0 { + return 0; + } + + // Interactions: query the oracle price and the time at which it was updated. + let round = match chainlink::latest_round_data(chainlink_program.clone(), chainlink_sol_usd_feed.clone()) { + Ok(round) => round, + Err(_) => return 0, // If the oracle call fails, skip fee charging. + }; + + // If the price is not greater than 0, skip the calculations. + if round.answer <= 0 { + return 0; + } + + let current_timestamp: u32 = Clock::get().unwrap().unix_timestamp as u32; + + // Due to reorgs and latency issues, the oracle can have a timestamp that is in the future. In + // this case, we ignore the price and skip fee charging. + if current_timestamp < round.timestamp { + return 0; + } + + // If the oracle hasn't been updated in the last 24 hours, we ignore the price and skip fee charging. This is a + // safety check to avoid using outdated prices. + const SECONDS_IN_24_HOURS: u32 = 86400; + if current_timestamp - round.timestamp > SECONDS_IN_24_HOURS { + return 0; + } + + // Interactions: query the oracle decimals. + let oracle_decimals = match chainlink::decimals(chainlink_program.clone(), chainlink_sol_usd_feed.clone()) { + Ok(decimals) => decimals, + Err(_) => return 0, // If the oracle call fails, skip fee charging. + }; + + let price = round.answer as u64; + + let fee_in_lamports: u64 = match oracle_decimals { + 8 => { + // If the oracle decimals are 8, calculate the fee. + fee_usd * LAMPORTS_PER_SOL / price + } + decimals => { + // Otherwise, adjust the calculation to account for the oracle decimals. + fee_usd * 10_u64.pow(1 + decimals as u32) / price + } + }; + + fee_in_lamports +} diff --git a/programs/merkle_instant/src/utils/mod.rs b/programs/merkle_instant/src/utils/mod.rs index c137ffff..823be8a7 100644 --- a/programs/merkle_instant/src/utils/mod.rs +++ b/programs/merkle_instant/src/utils/mod.rs @@ -1,5 +1,6 @@ pub mod constants; pub mod errors; pub mod events; +pub mod fee_calculation; pub mod transfer_helper; pub mod validations; diff --git a/scripts/ts/init-lockup.ts b/scripts/ts/init-lockup.ts index c6abd4d7..fab32565 100644 --- a/scripts/ts/init-lockup.ts +++ b/scripts/ts/init-lockup.ts @@ -3,7 +3,7 @@ import { createMint, getOrCreateAssociatedTokenAccount, mintTo, TOKEN_PROGRAM_ID import { ComputeBudgetProgram, Keypair, type PublicKey } from "@solana/web3.js"; import BN from "bn.js"; import { beforeEach, describe, it } from "vitest"; -import { BN_1, Decimals, ZERO } from "../../lib/constants"; +import { BN_1, Decimals, ProgramId, ZERO } from "../../lib/constants"; import { sol } from "../../lib/convertors"; import { type SablierLockup } from "../../target/types/sablier_lockup"; @@ -87,7 +87,7 @@ async function createStream(params: CreateParams) { }); await lockupProgram.methods - .createWithDurations( + .createWithDurationsLl( salt, depositAmount, cliffDuration, @@ -117,7 +117,7 @@ async function configureTestingEnvironment() { async function initSablierLockup() { await lockupProgram.methods - .initialize(senderKeys.publicKey) + .initialize(senderKeys.publicKey, ProgramId.CHAINLINK_PROGRAM, ProgramId.CHAINLINK_SOL_USD_FEED) .signers([senderKeys]) .accounts({ initializer: senderKeys.publicKey, diff --git a/scripts/ts/init-merkle-instant.ts b/scripts/ts/init-merkle-instant.ts index 5466728d..971b8968 100644 --- a/scripts/ts/init-merkle-instant.ts +++ b/scripts/ts/init-merkle-instant.ts @@ -1,6 +1,7 @@ import * as anchor from "@coral-xyz/anchor"; import type { Keypair } from "@solana/web3.js"; import { beforeEach, describe } from "vitest"; +import { ProgramId } from "../../lib/constants"; import type { SablierMerkleInstant } from "../../target/types/sablier_merkle_instant"; @@ -31,7 +32,7 @@ async function configureTestingEnvironment() { async function initSablierMerkleInstant() { await merkleInstantProgram.methods - .initialize(senderKeys.publicKey) + .initialize(senderKeys.publicKey, ProgramId.CHAINLINK_PROGRAM, ProgramId.CHAINLINK_SOL_USD_FEED) .signers([senderKeys]) .accounts({ initializer: senderKeys.publicKey, diff --git a/tests/common/chainlink-mock.ts b/tests/common/chainlink-mock.ts new file mode 100644 index 00000000..bef49e86 --- /dev/null +++ b/tests/common/chainlink-mock.ts @@ -0,0 +1,54 @@ +import type BN from "bn.js"; +import { type AddedAccount } from "solana-bankrun"; +import { ProgramId } from "../../lib/constants"; +import { toBn } from "../../lib/helpers"; + +/// TODO: add multiple mocks scenarios to match the EVM ones: +/// https://github.com/sablier-labs/evm-utils/blob/9a4139fed83788c5ffb455193f5005abf02ea366/src/mocks/ChainlinkMocks.sol +export class ChainlinkMock { + /// To get the mock data run the CLI: `solana account 99B2bTijsU6f1GCT73HmdR7HCFFjGMBcPZY6jZ96ynrR --url devnet --output json` + /// This data is mocked at "1754142441" Unix timestamp + public MOCK_CHAINLINK_DATA = + "YLNFQoCBSXUCAWQUUYa2ANnUYYrbqZcNlEBTsnm1vMbAD/Shxwnxadi/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA+AAD9IzGmE6swmiVX6E+mpeINgNJ4h4AyDGib6NC7wlNPTCAvIFVTRAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAAAAAr+bMBAQEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD7PsAXAAAAAOkWjmgAAAAAoTdp0wMAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="; + + async accountData(): Promise { + const MOCK_ACCOUNT_DATA = new Uint8Array(Buffer.from(this.MOCK_CHAINLINK_DATA, "base64")); + + return { + address: ProgramId.CHAINLINK_SOL_USD_FEED, + info: { + data: MOCK_ACCOUNT_DATA, + executable: false, + lamports: 2616962, + owner: ProgramId.CHAINLINK_PROGRAM, + }, + }; + } + + public calculateFeeInLamports(feeUSD: BN): BN { + const price = this.getMockPrice(); + const decimals = this.getMockDecimals(); + + return feeUSD.mul(toBn(10).pow(toBn(1 + decimals))).div(toBn(price)); + } + + // TODO: Fix this, I think it's incorrect + public getMockPrice(): number { + const bytes = new Uint8Array(Buffer.from(this.MOCK_CHAINLINK_DATA, "base64")); + // Price at offset 216 (4 bytes, little-endian, unsigned) + const priceRaw = (bytes[216] | (bytes[217] << 8) | (bytes[218] << 16) | (bytes[219] << 24)) >>> 0; + return priceRaw; + } + + public getMockTimestamp(): number { + const bytes = new Uint8Array(Buffer.from(this.MOCK_CHAINLINK_DATA, "base64")); + // Timestamp at offset 208 (4 bytes, little-endian) + return bytes[208] | (bytes[209] << 8) | (bytes[210] << 16) | (bytes[211] << 24); + } + + public getMockDecimals(): number { + const bytes = new Uint8Array(Buffer.from(this.MOCK_CHAINLINK_DATA, "base64")); + // Decimals at offset 138 + return bytes[138]; + } +} diff --git a/tests/common/context.ts b/tests/common/context.ts index c1105c96..5870c7ae 100644 --- a/tests/common/context.ts +++ b/tests/common/context.ts @@ -2,12 +2,20 @@ import * as token from "@solana/spl-token"; import { Keypair, PublicKey } from "@solana/web3.js"; import { BankrunProvider } from "anchor-bankrun"; import type BN from "bn.js"; -import { type AccountInfoBytes, type BanksClient, Clock, type ProgramTestContext, startAnchor } from "solana-bankrun"; -import { Decimals } from "../../lib/constants"; +import { + type AccountInfoBytes, + type AddedProgram, + type BanksClient, + Clock, + type ProgramTestContext, + startAnchor, +} from "solana-bankrun"; +import { Decimals, ProgramId } from "../../lib/constants"; import { dai, sol, usdc } from "../../lib/convertors"; import { toBigInt, toBn } from "../../lib/helpers"; import { type ProgramName } from "../../lib/types"; import { createATAAndFund, createMint } from "./anchor-bankrun"; +import { ChainlinkMock } from "./chainlink-mock"; import { type User } from "./types"; export class TestContext { @@ -22,6 +30,9 @@ export class TestContext { public feeCollector!: User; public recipient!: User; + // Chainlink Mock + public chainlinkMock: ChainlinkMock = new ChainlinkMock(); + // Tokens public dai!: PublicKey; // Token 2022 public randomToken!: PublicKey; // Token standard @@ -30,12 +41,22 @@ export class TestContext { async setUp( programName: ProgramName, programId: PublicKey, - additionalPrograms: { name: string; programId: PublicKey }[] = [], - ) { - const programs = [{ name: programName, programId }, ...additionalPrograms]; - // Start Anchor context with the provided programs - this.context = await startAnchor("", programs, []); + addedPrograms: AddedProgram[] = [], + ) { + const programs = [ + { name: programName, programId }, + { + name: "chainlink_program", + programId: ProgramId.CHAINLINK_PROGRAM, + }, + ...addedPrograms, + ]; + + const addedAccounts = [await this.chainlinkMock.accountData()]; + + // Start Anchor context with the provided programs & accounts + this.context = await startAnchor("", programs, addedAccounts); this.banksClient = this.context.banksClient; this.bankrunProvider = new BankrunProvider(this.context); this.defaultBankrunPayer = this.bankrunProvider.wallet.payer; diff --git a/tests/fixtures/chainlink_program.so b/tests/fixtures/chainlink_program.so new file mode 100644 index 00000000..006efaf3 Binary files /dev/null and b/tests/fixtures/chainlink_program.so differ diff --git a/tests/lockup/context.ts b/tests/lockup/context.ts index a82a9a79..dcf4446b 100644 --- a/tests/lockup/context.ts +++ b/tests/lockup/context.ts @@ -54,8 +54,8 @@ export class LockupTestContext extends TestContext { // Create the default streams this.salts = { - default: await this.createWithTimestamps(), - nonCancelable: await this.createWithTimestamps({ + default: await this.createWithTimestampsLl(), + nonCancelable: await this.createWithTimestampsLl({ isCancelable: false, }), nonExisting: new BN(1729), @@ -107,7 +107,7 @@ export class LockupTestContext extends TestContext { await buildSignAndProcessTx(this.banksClient, collectFeesIx, signer); } - async createWithDurations({ + async createWithDurationsLl({ cliffDuration = Time.CLIFF_DURATION, salt, }: { @@ -117,8 +117,8 @@ export class LockupTestContext extends TestContext { // Use the total supply as the salt for the stream salt = salt ?? (await this.getTotalSupply()); - const createWithDurationsIx = await this.lockup.methods - .createWithDurations( + const createWithDurationsLlIx = await this.lockup.methods + .createWithDurationsLl( salt, Amount.DEPOSIT, cliffDuration, @@ -137,12 +137,12 @@ export class LockupTestContext extends TestContext { }) .instruction(); - await buildSignAndProcessTx(this.banksClient, createWithDurationsIx, this.sender.keys); + await buildSignAndProcessTx(this.banksClient, createWithDurationsLlIx, this.sender.keys); return salt; } - async createWithTimestamps({ + async createWithTimestampsLl({ creator = this.sender.keys, senderPubKey = this.sender.keys.publicKey, recipientPubKey = this.recipient.keys.publicKey, @@ -158,7 +158,7 @@ export class LockupTestContext extends TestContext { salt = salt.isNeg() ? await this.getTotalSupply() : salt; const txIx = await this.lockup.methods - .createWithTimestamps( + .createWithTimestampsLl( salt, depositAmount, timestamps.start, @@ -183,8 +183,8 @@ export class LockupTestContext extends TestContext { return salt; } - async createWithTimestampsToken2022(): Promise { - return await this.createWithTimestamps({ + async createWithTimestampsLlToken2022(): Promise { + return await this.createWithTimestampsLl({ depositTokenMint: this.dai, depositTokenProgram: token.TOKEN_2022_PROGRAM_ID, }); @@ -192,7 +192,7 @@ export class LockupTestContext extends TestContext { async initializeLockup(): Promise { const initializeIx = await this.lockup.methods - .initialize(this.feeCollector.keys.publicKey) + .initialize(this.feeCollector.keys.publicKey, ProgramId.CHAINLINK_PROGRAM, ProgramId.CHAINLINK_SOL_USD_FEED) .accounts({ initializer: this.sender.keys.publicKey, nftTokenProgram: token.TOKEN_PROGRAM_ID, @@ -227,6 +227,8 @@ export class LockupTestContext extends TestContext { const withdrawIx = await this.lockup.methods .withdraw(withdrawAmount) .accounts({ + chainlinkProgram: ProgramId.CHAINLINK_PROGRAM, + chainlinkSolUsdFeed: ProgramId.CHAINLINK_SOL_USD_FEED, depositedTokenMint, depositedTokenProgram, nftTokenProgram: token.TOKEN_PROGRAM_ID, @@ -240,6 +242,17 @@ export class LockupTestContext extends TestContext { await buildSignAndProcessTx(this.banksClient, withdrawIx, signer); } + async withdrawalFeeInLamports(): Promise { + return await this.lockup.methods + .withdrawalFeeInLamports() + .accounts({ + chainlinkProgram: ProgramId.CHAINLINK_PROGRAM, + chainlinkSolUsdFeed: ProgramId.CHAINLINK_SOL_USD_FEED, + }) + .signers([this.defaultBankrunPayer]) + .view(); + } + async withdrawToken2022(salt: BN, signer: Keypair): Promise { await this.withdraw({ depositedTokenMint: this.dai, @@ -261,6 +274,8 @@ export class LockupTestContext extends TestContext { const withdrawMaxIx = await this.lockup.methods .withdrawMax() .accounts({ + chainlinkProgram: ProgramId.CHAINLINK_PROGRAM, + chainlinkSolUsdFeed: ProgramId.CHAINLINK_SOL_USD_FEED, depositedTokenMint, depositedTokenProgram, nftTokenProgram: token.TOKEN_PROGRAM_ID, diff --git a/tests/lockup/unit/cancel.test.ts b/tests/lockup/unit/cancel.test.ts index b4ff4734..aaa38ed2 100644 --- a/tests/lockup/unit/cancel.test.ts +++ b/tests/lockup/unit/cancel.test.ts @@ -110,7 +110,7 @@ describe("cancel", () => { ); // Create a stream with a random token - const salt = await ctx.createWithTimestamps({ + const salt = await ctx.createWithTimestampsLl({ creator: ctx.sender.keys, depositTokenMint: ctx.randomToken, depositTokenProgram: ProgramId.TOKEN, @@ -183,7 +183,7 @@ describe("cancel", () => { describe("given token 2022 standard", () => { it("should cancel the stream", async () => { // Create a stream with a Token2022 mint - const salt = await ctx.createWithTimestampsToken2022(); + const salt = await ctx.createWithTimestampsLlToken2022(); const beforeSenderBalance = await getATABalance(ctx.banksClient, ctx.sender.daiATA); diff --git a/tests/lockup/unit/collectFees.test.ts b/tests/lockup/unit/collectFees.test.ts index 344b580a..bb7103c1 100644 --- a/tests/lockup/unit/collectFees.test.ts +++ b/tests/lockup/unit/collectFees.test.ts @@ -2,13 +2,14 @@ import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, } from "@coral-xyz/anchor-errors"; +import BN from "bn.js"; import { beforeAll, beforeEach, describe, it } from "vitest"; import { REDUNDANCY_BUFFER } from "../../../lib/constants"; import { sleepFor } from "../../../lib/helpers"; -import { assertEqualSOLBalance } from "../../common/assertions"; +import { assertLteBn } from "../../common/assertions"; import { LockupTestContext } from "../context"; import { expectToThrow } from "../utils/assertions"; -import { Amount, Time } from "../utils/defaults"; +import { Time } from "../utils/defaults"; let ctx: LockupTestContext; @@ -31,8 +32,8 @@ describe("collectFees", () => { }); describe("when signer is not the authorized fee collector", () => { - it("should fail", async () => { - await withdrawMultipleTimes(); + it("should revert", async () => { + await withdrawTwice(); await expectToThrow(ctx.collectFees(ctx.eve.keys), CONSTRAINT_ADDRESS); }); }); @@ -46,7 +47,7 @@ describe("collectFees", () => { describe("given accumulated fees", () => { it("should collect the fees", async () => { - await withdrawMultipleTimes(); + await withdrawTwice(); const beforeLamports = { feeRecipient: await getFeeRecipientLamports(), @@ -61,11 +62,23 @@ describe("collectFees", () => { treasury: await ctx.getTreasuryLamports(), }; - // 2 withdrawals worth of fees minus the minimum lamports balance (a buffer on top of the redundancy buffer). - const expectedFeesCollected = Amount.WITHDRAW_FEE.muln(2).sub(REDUNDANCY_BUFFER); + const withdrawalFee = await ctx.withdrawalFeeInLamports(); + // 2 withdrawals worth of fees minus the redundancy buffer. + const expectedFeesCollected = withdrawalFee.muln(2).sub(REDUNDANCY_BUFFER); - assertEqualSOLBalance(afterLamports.treasury, beforeLamports.treasury.sub(expectedFeesCollected)); - assertEqualSOLBalance(afterLamports.feeRecipient, beforeLamports.feeRecipient.add(expectedFeesCollected)); + // Assert that the Treasury has been debited with an amount that is within 5% of the expected amount + const treasuryBalanceDifference = beforeLamports.treasury.sub(afterLamports.treasury).abs(); + assertLteBn( + treasuryBalanceDifference.sub(expectedFeesCollected).abs(), + expectedFeesCollected.mul(new BN(5)).div(new BN(100)), + ); + + // Assert that the fee recipient has been credited with an amount that is within 5% of the expected amount + const feeRecipientBalanceDifference = afterLamports.feeRecipient.sub(beforeLamports.feeRecipient).abs(); + assertLteBn( + feeRecipientBalanceDifference.sub(expectedFeesCollected).abs(), + expectedFeesCollected.mul(new BN(5)).div(new BN(100)), + ); }); }); }); @@ -77,7 +90,7 @@ async function getFeeRecipientLamports() { } /// Helper function to withdraw multiple times so that there are fees collected -async function withdrawMultipleTimes() { +async function withdrawTwice() { await ctx.timeTravelTo(Time.MID_26_PERCENT); await ctx.withdrawMax(); await ctx.timeTravelTo(Time.END); diff --git a/tests/lockup/unit/createWithDurations.test.ts b/tests/lockup/unit/createWithDurations.test.ts index 074d3824..f0b1e999 100644 --- a/tests/lockup/unit/createWithDurations.test.ts +++ b/tests/lockup/unit/createWithDurations.test.ts @@ -7,7 +7,7 @@ import { Time } from "../utils/defaults"; let ctx: LockupTestContext; -describe("createWithDurations", () => { +describe("createWithDurationsLl", () => { describe("when the program is not initialized", () => { beforeAll(async () => { ctx = new LockupTestContext(); @@ -16,7 +16,7 @@ describe("createWithDurations", () => { }); it("should fail", async () => { - await expectToThrow(ctx.createWithDurations({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); + await expectToThrow(ctx.createWithDurationsLl({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); }); }); @@ -29,7 +29,7 @@ describe("createWithDurations", () => { describe("when cliff duration not zero", () => { it("it should create the stream", async () => { - const salt = await ctx.createWithDurations(); + const salt = await ctx.createWithDurationsLl(); const actualStreamData = await ctx.fetchStreamData(salt); const expectedStreamData = ctx.defaultStream({ salt: salt }).data; @@ -39,7 +39,7 @@ describe("createWithDurations", () => { describe("when cliff duration zero", () => { it("it should create the stream", async () => { - const salt = await ctx.createWithDurations({ cliffDuration: ZERO }); + const salt = await ctx.createWithDurationsLl({ cliffDuration: ZERO }); const actualStreamData = await ctx.fetchStreamData(salt); const expectedStreamData = ctx.defaultStream({ salt: salt }).data; diff --git a/tests/lockup/unit/createWithTimestamps.test.ts b/tests/lockup/unit/createWithTimestamps.test.ts index 3e48740e..352f107a 100644 --- a/tests/lockup/unit/createWithTimestamps.test.ts +++ b/tests/lockup/unit/createWithTimestamps.test.ts @@ -11,7 +11,7 @@ import { AMOUNTS, Amount, TIMESTAMPS, Time, UNLOCK_AMOUNTS } from "../utils/defa let ctx: LockupTestContext; -describe("createWithTimestamps", () => { +describe("createWithTimestampsLl", () => { describe("when the program is not initialized", () => { beforeAll(async () => { ctx = new LockupTestContext(); @@ -19,7 +19,7 @@ describe("createWithTimestamps", () => { }); it("should fail", async () => { - await expectToThrow(ctx.createWithTimestamps({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); + await expectToThrow(ctx.createWithTimestampsLl({ salt: ZERO }), ACCOUNT_NOT_INITIALIZED); }); }); @@ -31,7 +31,7 @@ describe("createWithTimestamps", () => { describe("when deposit amount zero", () => { it("should fail", async () => { - await expectToThrow(ctx.createWithTimestamps({ depositAmount: ZERO }), "DepositAmountZero"); + await expectToThrow(ctx.createWithTimestampsLl({ depositAmount: ZERO }), "DepositAmountZero"); }); }); @@ -39,7 +39,7 @@ describe("createWithTimestamps", () => { describe("when start time is zero", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ start: ZERO }), }), "StartTimeNotPositive", @@ -51,7 +51,7 @@ describe("createWithTimestamps", () => { describe("when start time is not positive", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ start: new BN(-1) }), }), "StartTimeNotPositive", @@ -63,7 +63,7 @@ describe("createWithTimestamps", () => { describe("when sender lacks an ATA for deposited token", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ depositTokenMint: ctx.randomToken, }), ACCOUNT_NOT_INITIALIZED, @@ -75,7 +75,7 @@ describe("createWithTimestamps", () => { describe("when sender has an insufficient token balance", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ depositAmount: usdc(1_000_000).addn(1), }), 0x1, @@ -88,7 +88,7 @@ describe("createWithTimestamps", () => { describe("when cliff unlock amount not zero", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ cliff: ZERO }), }), "CliffTimeZeroUnlockAmountNotZero", @@ -99,7 +99,7 @@ describe("createWithTimestamps", () => { describe("when start time not less than end time", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ cliff: ZERO, start: Time.END }), }), "StartTimeNotLessThanEndTime", @@ -111,7 +111,7 @@ describe("createWithTimestamps", () => { it("should create the stream", async () => { const beforeSenderTokenBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); - const salt = await ctx.createWithTimestamps({ + const salt = await ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ cliff: ZERO }), unlockAmounts: UNLOCK_AMOUNTS({ cliff: ZERO, start: ZERO }), }); @@ -131,7 +131,7 @@ describe("createWithTimestamps", () => { describe("when start time not less than cliff time", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ start: Time.CLIFF }), }), "StartTimeNotLessThanCliffTime", @@ -143,7 +143,7 @@ describe("createWithTimestamps", () => { describe("when cliff time not less than end time", () => { it("should fail", async () => { await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ timestamps: TIMESTAMPS({ cliff: Time.END }), }), "CliffTimeNotLessThanEndTime", @@ -156,7 +156,7 @@ describe("createWithTimestamps", () => { it("should fail", async () => { const depositAmount = BN_1000; await expectToThrow( - ctx.createWithTimestamps({ + ctx.createWithTimestampsLl({ depositAmount, unlockAmounts: { cliff: depositAmount, @@ -172,7 +172,7 @@ describe("createWithTimestamps", () => { describe("when token SPL standard", () => { it("should create the stream", async () => { const beforeSenderTokenBalance = await getATABalance(ctx.banksClient, ctx.sender.usdcATA); - const salt = await ctx.createWithTimestamps(); + const salt = await ctx.createWithTimestampsLl(); await assertStreamCreation(salt, beforeSenderTokenBalance); }); @@ -181,7 +181,7 @@ describe("createWithTimestamps", () => { describe("when token 2022 standard", () => { it("should create the stream", async () => { const beforeSenderTokenBalance = await ctx.getSenderTokenBalance(ctx.dai); - const salt = await ctx.createWithTimestampsToken2022(); + const salt = await ctx.createWithTimestampsLlToken2022(); await assertStreamCreation( salt, diff --git a/tests/lockup/unit/withdraw.test.ts b/tests/lockup/unit/withdraw.test.ts index 55e55bd7..7f528b50 100644 --- a/tests/lockup/unit/withdraw.test.ts +++ b/tests/lockup/unit/withdraw.test.ts @@ -8,12 +8,7 @@ import { beforeAll, beforeEach, describe, it } from "vitest"; import { BN_1, ProgramId, ZERO } from "../../../lib/constants"; import type { StreamData } from "../../../target/types/sablier_lockup_structs"; import { createATAAndFund, deriveATAAddress, getATABalance } from "../../common/anchor-bankrun"; -import { - assertAccountExists, - assertAccountNotExists, - assertEqualBn, - assertEqualSOLBalance, -} from "../../common/assertions"; +import { assertAccountExists, assertAccountNotExists, assertEqualBn, assertLteBn } from "../../common/assertions"; import { LockupTestContext } from "../context"; import { assertEqStreamData, expectToThrow } from "../utils/assertions"; import { Amount, Time } from "../utils/defaults"; @@ -115,7 +110,7 @@ describe("withdraw", () => { ); // Create a new stream with a random token - const salt = await ctx.createWithTimestamps({ + const salt = await ctx.createWithTimestampsLl({ depositAmount: Amount.DEPOSIT, depositTokenMint: ctx.randomToken, }); @@ -153,7 +148,10 @@ describe("withdraw", () => { ctx.sender.usdcATA, ); + const txSignerKeys = ctx.recipient.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); await ctx.withdraw({ + signer: txSignerKeys, withdrawalRecipient: ctx.sender.keys.publicKey, }); @@ -162,6 +160,8 @@ describe("withdraw", () => { await postWithdrawAssertions( ctx.salts.default, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.sender.usdcATA, withdrawalRecipientATABalanceBefore, @@ -184,13 +184,17 @@ describe("withdraw", () => { ctx.recipient.usdcATA, ); - await ctx.withdraw(); + const txSignerKeys = ctx.recipient.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); + await ctx.withdraw({ signer: txSignerKeys }); const expectedStreamData = ctx.defaultStream().data; expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; await postWithdrawAssertions( ctx.salts.default, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.recipient.usdcATA, withdrawalRecipientATABalanceBefore, @@ -213,8 +217,10 @@ describe("withdraw", () => { ctx.recipient.usdcATA, ); + const txSignerKeys = ctx.sender.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); await ctx.withdraw({ - signer: ctx.sender.keys, + signer: txSignerKeys, withdrawAmount: Amount.DEPOSIT, }); @@ -225,6 +231,8 @@ describe("withdraw", () => { await postWithdrawAssertions( ctx.salts.default, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.recipient.usdcATA, withdrawalRecipientATABalanceBefore, @@ -247,7 +255,9 @@ describe("withdraw", () => { ctx.recipient.usdcATA, ); - await ctx.withdraw({ signer: ctx.sender.keys }); + const txSignerKeys = ctx.sender.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); + await ctx.withdraw({ signer: txSignerKeys }); const expectedStreamData = ctx.defaultStream({ isCancelable: false, isDepleted: true, @@ -258,6 +268,8 @@ describe("withdraw", () => { await postWithdrawAssertions( ctx.salts.default, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.recipient.usdcATA, withdrawalRecipientATABalanceBefore, @@ -278,11 +290,15 @@ describe("withdraw", () => { ctx.recipient.usdcATA, ); - await ctx.withdraw({ signer: ctx.sender.keys }); + const txSignerKeys = ctx.sender.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); + await ctx.withdraw({ signer: txSignerKeys }); const expectedStreamData = ctx.defaultStream().data; expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; await postWithdrawAssertions( ctx.salts.default, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.recipient.usdcATA, withdrawalRecipientATABalanceBefore, @@ -293,7 +309,7 @@ describe("withdraw", () => { describe("given token 2022 standard", () => { it("should make the withdrawal", async () => { - const salt = await ctx.createWithTimestampsToken2022(); + const salt = await ctx.createWithTimestampsLlToken2022(); // Get the Lamports balance of the Treasury before the withdrawal const treasuryLamportsBefore = await ctx.getTreasuryLamports(); @@ -304,7 +320,9 @@ describe("withdraw", () => { ctx.recipient.daiATA, ); - await ctx.withdrawToken2022(salt, ctx.sender.keys); + const txSignerKeys = ctx.sender.keys; + const txSignerLamportsBefore = await ctx.getLamportsOf(txSignerKeys.publicKey); + await ctx.withdrawToken2022(salt, txSignerKeys); const expectedStreamData = ctx.defaultStreamToken2022({ salt: salt, @@ -312,6 +330,8 @@ describe("withdraw", () => { expectedStreamData.amounts.withdrawn = Amount.WITHDRAW; await postWithdrawAssertions( salt, + txSignerKeys.publicKey, + txSignerLamportsBefore, treasuryLamportsBefore, ctx.recipient.daiATA, withdrawalRecipientATABalanceBefore, @@ -333,6 +353,8 @@ describe("withdraw", () => { async function postWithdrawAssertions( salt: BN, + txSigner: PublicKey, + txSignerLamportsBefore: BN, treasuryLamportsBefore: BN, withdrawalRecipientATA: PublicKey, withdrawalRecipientATABalanceBefore: BN, @@ -342,11 +364,18 @@ async function postWithdrawAssertions( const actualStreamData = await ctx.fetchStreamData(salt); assertEqStreamData(actualStreamData, expectedStreamData); + const expectedFee = await ctx.withdrawalFeeInLamports(); + // Get the Lamports balance of the Treasury after the withdrawal const treasuryLamportsAfter = await ctx.getTreasuryLamports(); - // Assert that the Treasury's balance has been credited with the withdrawal fee - assertEqualSOLBalance(treasuryLamportsAfter, treasuryLamportsBefore.add(Amount.WITHDRAW_FEE)); + // Assert that the tx signer lamports balance has decreased by, at least, the withdrawal fee amount. + // We use `<=` because we don't know the gas cost in advance. + const txSignerLamportsAfter = await ctx.getLamportsOf(txSigner); + assertLteBn(txSignerLamportsAfter, txSignerLamportsBefore.sub(expectedFee)); + + // Assert that the Treasury has been credited with the withdrawal fee. + assertEqualBn(treasuryLamportsAfter, treasuryLamportsBefore.add(expectedFee)); // Get the withdrawal recipient's token balance const withdrawalRecipientTokenBalance = await getATABalance(ctx.banksClient, withdrawalRecipientATA); diff --git a/tests/lockup/utils/defaults.ts b/tests/lockup/utils/defaults.ts index 75f27423..794f59a1 100644 --- a/tests/lockup/utils/defaults.ts +++ b/tests/lockup/utils/defaults.ts @@ -1,7 +1,6 @@ import BN from "bn.js"; -import dayjs from "dayjs"; import { ZERO } from "../../../lib/constants"; -import { sol, usdc } from "../../../lib/convertors"; +import { usdc } from "../../../lib/convertors"; import type { Amounts, Timestamps } from "../../../target/types/sablier_lockup_structs"; import type { UnlockAmounts } from "./types"; @@ -9,8 +8,6 @@ export namespace Amount { export const CLIFF = usdc("2500.000001"); export const DEPOSIT = usdc(10_000); export const START = ZERO; - export const WITHDRAW_FEE = sol("0.01"); - export const WITHDRAW = usdc(2600); export const REFUND = DEPOSIT.sub(WITHDRAW); } @@ -30,7 +27,8 @@ export namespace Seed { */ export namespace Time { export const CLIFF_DURATION = new BN(2500); - export const GENESIS = new BN(dayjs().add(1, "day").unix()); // tomorrow + // We use this fixed timestamp to ensure that the mock Chainlink data is not outdated. + export const GENESIS = new BN(1754142441); // August 2, 2025 1:47:21 PM export const START = GENESIS.add(new BN(1000)); export const TOTAL_DURATION = new BN(10_000); diff --git a/tests/merkle-instant/context.ts b/tests/merkle-instant/context.ts index 82c99f43..ad2e8d88 100644 --- a/tests/merkle-instant/context.ts +++ b/tests/merkle-instant/context.ts @@ -108,6 +108,8 @@ export class MerkleInstantTestContext extends TestContext { airdropTokenMint, airdropTokenProgram, campaign: campaign, + chainlinkProgram: ProgramId.CHAINLINK_PROGRAM, + chainlinkSolUsdFeed: ProgramId.CHAINLINK_SOL_USD_FEED, claimer: claimerKeys.publicKey, recipient: recipientAddress, }) @@ -117,6 +119,17 @@ export class MerkleInstantTestContext extends TestContext { await buildSignAndProcessTx(this.banksClient, txIx, claimerKeys); } + async claimFeeInLamports(): Promise { + return await this.merkleInstant.methods + .claimFeeInLamports() + .accounts({ + chainlinkProgram: ProgramId.CHAINLINK_PROGRAM, + chainlinkSolUsdFeed: ProgramId.CHAINLINK_SOL_USD_FEED, + }) + .signers([this.defaultBankrunPayer]) + .view(); + } + async clawback({ signer = this.campaignCreator.keys, campaign = this.defaultCampaign, @@ -217,7 +230,7 @@ export class MerkleInstantTestContext extends TestContext { async initializeMerkleInstant(): Promise { const initializeIx = await this.merkleInstant.methods - .initialize(this.feeCollector.keys.publicKey) + .initialize(this.feeCollector.keys.publicKey, ProgramId.CHAINLINK_PROGRAM, ProgramId.CHAINLINK_SOL_USD_FEED) .accounts({ initializer: this.campaignCreator.keys.publicKey, }) diff --git a/tests/merkle-instant/unit/claim.test.ts b/tests/merkle-instant/unit/claim.test.ts index 883fd931..ac215372 100644 --- a/tests/merkle-instant/unit/claim.test.ts +++ b/tests/merkle-instant/unit/claim.test.ts @@ -4,7 +4,7 @@ import { assert, beforeAll, beforeEach, describe, it } from "vitest"; import { BN_1, ProgramId, ZERO } from "../../../lib/constants"; import { sleepFor } from "../../../lib/helpers"; import { createATAAndFund, getATABalanceMint } from "../../common/anchor-bankrun"; -import { assertEqualBn, assertEqualSOLBalance, assertLteBn, assertZeroBn } from "../../common/assertions"; +import { assertEqualBn, assertLteBn, assertZeroBn } from "../../common/assertions"; import { MerkleInstantTestContext } from "../context"; import { expectToThrow } from "../utils/assertions"; import { Amount, Campaign, Time } from "../utils/defaults"; @@ -210,16 +210,17 @@ async function testClaim( // Assert that the recipient's ATA balance increased by the claim amount assertEqualBn(recipientAtaBalanceAfter, recipientAtaBalanceBefore.add(Amount.CLAIM)); + const expectedFee = await ctx.claimFeeInLamports(); const claimerLamportsAfter = await ctx.getLamportsOf(claimer.publicKey); - // Assert that the claimer's lamports balance has changed at least by the claim fee amount. - // We use `<=` because we don't know in advance the gas cost. - assertLteBn(claimerLamportsAfter, claimerLamportsBefore.sub(Amount.CLAIM_FEE)); + // Assert that the claimer's lamports balance has decreased by, at least, the claim fee amount. + // We use `<=` because we don't know the gas cost in advance. + assertLteBn(claimerLamportsAfter, claimerLamportsBefore.sub(expectedFee)); const treasuryLamportsAfter = await ctx.getLamportsOf(ctx.treasuryAddress); - // Assert that the treasury's balance has increased by the claim fee amount - assertEqualSOLBalance(treasuryLamportsAfter, treasuryLamportsBefore.add(Amount.CLAIM_FEE)); + // Assert that the Treasury has been credited with the claim fee. + assertEqualBn(treasuryLamportsAfter, treasuryLamportsBefore.add(expectedFee)); } // Implicitly tests the `has_claimed` Ix works. diff --git a/tests/merkle-instant/unit/collectFees.test.ts b/tests/merkle-instant/unit/collectFees.test.ts index 6f88a78c..15ae3e8f 100644 --- a/tests/merkle-instant/unit/collectFees.test.ts +++ b/tests/merkle-instant/unit/collectFees.test.ts @@ -2,12 +2,12 @@ import { ANCHOR_ERROR__ACCOUNT_NOT_INITIALIZED as ACCOUNT_NOT_INITIALIZED, ANCHOR_ERROR__CONSTRAINT_ADDRESS as CONSTRAINT_ADDRESS, } from "@coral-xyz/anchor-errors"; +import BN from "bn.js"; import { beforeAll, beforeEach, describe, it } from "vitest"; import { REDUNDANCY_BUFFER } from "../../../lib/constants"; -import { assertEqualSOLBalance } from "../../common/assertions"; +import { assertLteBn } from "../../common/assertions"; import { MerkleInstantTestContext } from "../context"; import { expectToThrow } from "../utils/assertions"; -import { Amount } from "../utils/defaults"; let ctx: MerkleInstantTestContext; @@ -65,11 +65,22 @@ describe("collectFees", () => { treasury: await ctx.getTreasuryLamports(), }; - // 1 claim worth of fees minus the minimum lamports balance (a buffer on top of the redundancy buffer). - const expectedFeesCollected = Amount.CLAIM_FEE.sub(REDUNDANCY_BUFFER); + const expectedClaimFee = await ctx.claimFeeInLamports(); + const expectedFeesCollected = expectedClaimFee.sub(REDUNDANCY_BUFFER); // 1 claim worth of fees minus the redundancy buffer - assertEqualSOLBalance(afterLamports.treasury, beforeLamports.treasury.sub(expectedFeesCollected)); - assertEqualSOLBalance(afterLamports.feeRecipient, beforeLamports.feeRecipient.add(expectedFeesCollected)); + // Assert that the Treasury has been debited with an amount that is within 5% of the expected amount + const treasuryBalanceDifference = beforeLamports.treasury.sub(afterLamports.treasury).abs(); + assertLteBn( + treasuryBalanceDifference.sub(expectedFeesCollected).abs(), + expectedFeesCollected.mul(new BN(5)).div(new BN(100)), + ); + + // Assert that the fee recipient has been credited with an amount that is within 5% of the expected amount + const feeRecipientBalanceDifference = afterLamports.feeRecipient.sub(beforeLamports.feeRecipient).abs(); + assertLteBn( + feeRecipientBalanceDifference.sub(expectedFeesCollected).abs(), + expectedFeesCollected.mul(new BN(5)).div(new BN(100)), + ); }); }); }); diff --git a/tests/merkle-instant/utils/defaults.ts b/tests/merkle-instant/utils/defaults.ts index 8b248dea..dce086f3 100644 --- a/tests/merkle-instant/utils/defaults.ts +++ b/tests/merkle-instant/utils/defaults.ts @@ -1,17 +1,16 @@ import BN from "bn.js"; import dayjs from "dayjs"; -import { sol, usdc } from "../../../lib/convertors"; +import { usdc } from "../../../lib/convertors"; export namespace Amount { export const AGGREGATE = usdc(10_000); - export const CLAIM_FEE = sol("0.03"); export const CLAIM = usdc(100); export const CLAWBACK = usdc(1000); } export namespace Time { - export const GENESIS_DAY = dayjs().add(1, "day"); - export const GENESIS = new BN(GENESIS_DAY.unix()); // tomorrow + // We use this fixed timestamp to ensure that the mock Chainlink data is not outdated. + export const GENESIS = new BN(1754142441); // August 2, 2025 1:47:21 PM } export namespace Campaign { @@ -19,7 +18,8 @@ export namespace Campaign { export const START_TIME = Time.GENESIS; export const EXPIRATION_TIME = new BN(dayjs().add(10, "days").unix()); export const IPFS_CID = "bafkreiecpwdhvkmw4y6iihfndk7jhwjas3m5htm7nczovt6m37mucwgsrq"; - export const POST_GRACE_PERIOD = new BN(Time.GENESIS_DAY.add(7, "days").add(1, "second").unix()); + const GRACE_PERIOD_SECONDS = new BN(7 * 24 * 60 * 60 + 1); + export const POST_GRACE_PERIOD = Time.GENESIS.add(GRACE_PERIOD_SECONDS); } export namespace Seed {