diff --git a/.changeset/fresh-tigers-pay.md b/.changeset/fresh-tigers-pay.md new file mode 100644 index 00000000000..a1a0bf05b53 --- /dev/null +++ b/.changeset/fresh-tigers-pay.md @@ -0,0 +1,5 @@ +--- +'@chainlink/solana-functions-adapter': minor +--- + +Add strcUSX junior and senior exchange rate endpoint diff --git a/.changeset/quiet-ducks-compare.md b/.changeset/quiet-ducks-compare.md new file mode 100644 index 00000000000..808d46d05e3 --- /dev/null +++ b/.changeset/quiet-ducks-compare.md @@ -0,0 +1,5 @@ +--- +'@chainlink/solana-functions-adapter': patch +--- + +Add shared Solana exchange-rate utilities. diff --git a/.changeset/sharp-lions-serve.md b/.changeset/sharp-lions-serve.md new file mode 100644 index 00000000000..9c4014c1b39 --- /dev/null +++ b/.changeset/sharp-lions-serve.md @@ -0,0 +1,5 @@ +--- +'@chainlink/solana-functions-adapter': minor +--- + +Add stSLX exchange rate endpoint diff --git a/.pnp.cjs b/.pnp.cjs index 222ee88a714..d2e4d36f2fd 100644 --- a/.pnp.cjs +++ b/.pnp.cjs @@ -7207,7 +7207,7 @@ const RAW_RUNTIME_STATE = ["workspace:packages/sources/solana-functions", {\ "packageLocation": "./packages/sources/solana-functions/",\ "packageDependencies": [\ - ["@chainlink/external-adapter-framework", "npm:2.16.1"],\ + ["@chainlink/external-adapter-framework", "npm:2.17.1"],\ ["@chainlink/solana-functions-adapter", "workspace:packages/sources/solana-functions"],\ ["@coral-xyz/anchor", "npm:0.31.1"],\ ["@solana/addresses", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.3"],\ @@ -7216,6 +7216,7 @@ const RAW_RUNTIME_STATE = ["@solana/rpc", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ ["@solana/spl-stake-pool", "npm:1.1.8"],\ ["@solana/spl-token", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:0.4.14"],\ + ["@solana/sysvars", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ ["@solana/web3.js", "npm:1.98.4"],\ ["@types/bn.js", "npm:5.1.6"],\ ["@types/jest", "npm:29.5.14"],\ @@ -11163,6 +11164,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@solana/accounts", [\ + ["npm:3.0.2", {\ + "packageLocation": "./.yarn/cache/@solana-accounts-npm-3.0.2-db589c23b8-a850f48091.zip/node_modules/@solana/accounts/",\ + "packageDependencies": [\ + ["@solana/accounts", "npm:3.0.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:6.9.0", {\ "packageLocation": "./.yarn/cache/@solana-accounts-npm-6.9.0-873ac954de-6456023113.zip/node_modules/@solana/accounts/",\ "packageDependencies": [\ @@ -11170,6 +11178,25 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2", {\ + "packageLocation": "./.yarn/__virtual__/@solana-accounts-virtual-46fc7dd2d4/0/cache/@solana-accounts-npm-3.0.2-db589c23b8-a850f48091.zip/node_modules/@solana/accounts/",\ + "packageDependencies": [\ + ["@solana/accounts", "virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2"],\ + ["@solana/addresses", "virtual:e3686759fd9553dd07d24afdac656ec8b70018e70ae103cc0ebd771969bdfbea8a234a3ada104a4a42a88252bfabd7b24edf2145757b931840b0b8b31456e29b#npm:3.0.2"],\ + ["@solana/codecs-core", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/codecs-strings", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ + ["@solana/errors", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/rpc-spec", "virtual:c2b196a693e37c0bf4546dafa9ac4c22268bfb0355950ada99d08aa811254ba3266b019d6f58a67972917b5f4dcc63f41674ab14f349c5e90547d4015daba3ea#npm:3.0.2"],\ + ["@solana/rpc-types", "virtual:c2b196a693e37c0bf4546dafa9ac4c22268bfb0355950ada99d08aa811254ba3266b019d6f58a67972917b5f4dcc63f41674ab14f349c5e90547d4015daba3ea#npm:3.0.2"],\ + ["@types/typescript", null],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:c4d94e4399c4de0f7679b59d808e823662e8a1161bc1715a8e1d3b881339aff8f70b4955bfb712902340b57218d77bd40665e6d3ad24370cefa2681b9c215570#npm:6.9.0", {\ "packageLocation": "./.yarn/__virtual__/@solana-accounts-virtual-5bd97f62ff/0/cache/@solana-accounts-npm-6.9.0-873ac954de-6456023113.zip/node_modules/@solana/accounts/",\ "packageDependencies": [\ @@ -11363,6 +11390,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["npm:3.0.2", {\ + "packageLocation": "./.yarn/cache/@solana-codecs-npm-3.0.2-261b7ade89-967acb8199.zip/node_modules/@solana/codecs/",\ + "packageDependencies": [\ + ["@solana/codecs", "npm:3.0.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:6.9.0", {\ "packageLocation": "./.yarn/cache/@solana-codecs-npm-6.9.0-35a2d05344-f17f63e694.zip/node_modules/@solana/codecs/",\ "packageDependencies": [\ @@ -11388,6 +11422,24 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "HARD"\ }],\ + ["virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2", {\ + "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-2d82fb4609/0/cache/@solana-codecs-npm-3.0.2-261b7ade89-967acb8199.zip/node_modules/@solana/codecs/",\ + "packageDependencies": [\ + ["@solana/codecs", "virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2"],\ + ["@solana/codecs-core", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/codecs-data-structures", "virtual:9ffe11166b065cf2d9d01c97d0f98e8abd5fccb267aaee09eb3a3817361ac4de2c0b61b4e081f7bb552a7b822fcea5fb29ee200e32a811785817d5558497ba90#npm:3.0.2"],\ + ["@solana/codecs-numbers", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/codecs-strings", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ + ["@solana/options", "virtual:2d82fb46094c2a274d80db05bc6dea22e72bf844faa2975fc318f9abc34161008004ed73c229a12b4327fc87a976f9b69b231bd424deb03a7d5655a431f90c83#npm:3.0.2"],\ + ["@types/typescript", null],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:c4d94e4399c4de0f7679b59d808e823662e8a1161bc1715a8e1d3b881339aff8f70b4955bfb712902340b57218d77bd40665e6d3ad24370cefa2681b9c215570#npm:6.9.0", {\ "packageLocation": "./.yarn/__virtual__/@solana-codecs-virtual-2de42dd311/0/cache/@solana-codecs-npm-6.9.0-35a2d05344-f17f63e694.zip/node_modules/@solana/codecs/",\ "packageDependencies": [\ @@ -12302,6 +12354,13 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["npm:3.0.2", {\ + "packageLocation": "./.yarn/cache/@solana-options-npm-3.0.2-d90dd3225f-7ff8e1af97.zip/node_modules/@solana/options/",\ + "packageDependencies": [\ + ["@solana/options", "npm:3.0.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:6.9.0", {\ "packageLocation": "./.yarn/cache/@solana-options-npm-6.9.0-3c14ccbfb7-23fef5cec4.zip/node_modules/@solana/options/",\ "packageDependencies": [\ @@ -12309,6 +12368,24 @@ const RAW_RUNTIME_STATE = ],\ "linkType": "SOFT"\ }],\ + ["virtual:2d82fb46094c2a274d80db05bc6dea22e72bf844faa2975fc318f9abc34161008004ed73c229a12b4327fc87a976f9b69b231bd424deb03a7d5655a431f90c83#npm:3.0.2", {\ + "packageLocation": "./.yarn/__virtual__/@solana-options-virtual-fdb32f1617/0/cache/@solana-options-npm-3.0.2-d90dd3225f-7ff8e1af97.zip/node_modules/@solana/options/",\ + "packageDependencies": [\ + ["@solana/codecs-core", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/codecs-data-structures", "virtual:9ffe11166b065cf2d9d01c97d0f98e8abd5fccb267aaee09eb3a3817361ac4de2c0b61b4e081f7bb552a7b822fcea5fb29ee200e32a811785817d5558497ba90#npm:3.0.2"],\ + ["@solana/codecs-numbers", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/codecs-strings", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ + ["@solana/errors", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/options", "virtual:2d82fb46094c2a274d80db05bc6dea22e72bf844faa2975fc318f9abc34161008004ed73c229a12b4327fc87a976f9b69b231bd424deb03a7d5655a431f90c83#npm:3.0.2"],\ + ["@types/typescript", null],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ + }],\ ["virtual:2de42dd3111e77cdd577723ec6be7b78d4a855877fe5a4c1f187e402016a8e14e87aa7506de165ba104542c2d77165793618312b3c6886c6eeabd7de15470da7#npm:6.9.0", {\ "packageLocation": "./.yarn/__virtual__/@solana-options-virtual-d4d5d06140/0/cache/@solana-options-npm-6.9.0-3c14ccbfb7-23fef5cec4.zip/node_modules/@solana/options/",\ "packageDependencies": [\ @@ -13169,6 +13246,13 @@ const RAW_RUNTIME_STATE = }]\ ]],\ ["@solana/sysvars", [\ + ["npm:3.0.2", {\ + "packageLocation": "./.yarn/cache/@solana-sysvars-npm-3.0.2-cad063b41a-423b799039.zip/node_modules/@solana/sysvars/",\ + "packageDependencies": [\ + ["@solana/sysvars", "npm:3.0.2"]\ + ],\ + "linkType": "SOFT"\ + }],\ ["npm:6.9.0", {\ "packageLocation": "./.yarn/cache/@solana-sysvars-npm-6.9.0-dd2a6896ff-4f1c744381.zip/node_modules/@solana/sysvars/",\ "packageDependencies": [\ @@ -13194,6 +13278,23 @@ const RAW_RUNTIME_STATE = "typescript"\ ],\ "linkType": "HARD"\ + }],\ + ["virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2", {\ + "packageLocation": "./.yarn/__virtual__/@solana-sysvars-virtual-68dd0c34ad/0/cache/@solana-sysvars-npm-3.0.2-cad063b41a-423b799039.zip/node_modules/@solana/sysvars/",\ + "packageDependencies": [\ + ["@solana/accounts", "virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2"],\ + ["@solana/codecs", "virtual:68dd0c34ad2f981fcb6940d235350cbbc15a6cb2c2397b6c6453a546bf25494eb5cee0d04ba5c72a17e814b35236504a77c305883792f08a98f8b1fcdfc3de38#npm:3.0.2"],\ + ["@solana/errors", "virtual:c19705464ec0d9a881ece73996d6bb41f7a662050a228e48517a30b9a82c58f20eb78dcf7ae35c506e80aadf54da3443c4ea24c2f1609866f9465441310020d6#npm:3.0.2"],\ + ["@solana/rpc-types", "virtual:c2b196a693e37c0bf4546dafa9ac4c22268bfb0355950ada99d08aa811254ba3266b019d6f58a67972917b5f4dcc63f41674ab14f349c5e90547d4015daba3ea#npm:3.0.2"],\ + ["@solana/sysvars", "virtual:ef003a5149e1a030be208742b73558a4617d3ff34222ae88f1b64560b15c21faffaf169d5b8672dbf9995cc6922dadf923aa2a19841e7237fcbcb97c65f6a492#npm:3.0.2"],\ + ["@types/typescript", null],\ + ["typescript", "patch:typescript@npm%3A5.8.3#optional!builtin::version=5.8.3&hash=5786d5"]\ + ],\ + "packagePeers": [\ + "@types/typescript",\ + "typescript"\ + ],\ + "linkType": "HARD"\ }]\ ]],\ ["@solana/transaction-confirmation", [\ diff --git a/.yarn/cache/@solana-accounts-npm-3.0.2-db589c23b8-a850f48091.zip b/.yarn/cache/@solana-accounts-npm-3.0.2-db589c23b8-a850f48091.zip new file mode 100644 index 00000000000..e94d864b6d5 Binary files /dev/null and b/.yarn/cache/@solana-accounts-npm-3.0.2-db589c23b8-a850f48091.zip differ diff --git a/.yarn/cache/@solana-codecs-npm-3.0.2-261b7ade89-967acb8199.zip b/.yarn/cache/@solana-codecs-npm-3.0.2-261b7ade89-967acb8199.zip new file mode 100644 index 00000000000..1d1ad419846 Binary files /dev/null and b/.yarn/cache/@solana-codecs-npm-3.0.2-261b7ade89-967acb8199.zip differ diff --git a/.yarn/cache/@solana-options-npm-3.0.2-d90dd3225f-7ff8e1af97.zip b/.yarn/cache/@solana-options-npm-3.0.2-d90dd3225f-7ff8e1af97.zip new file mode 100644 index 00000000000..3b4d6a8a919 Binary files /dev/null and b/.yarn/cache/@solana-options-npm-3.0.2-d90dd3225f-7ff8e1af97.zip differ diff --git a/.yarn/cache/@solana-sysvars-npm-3.0.2-cad063b41a-423b799039.zip b/.yarn/cache/@solana-sysvars-npm-3.0.2-cad063b41a-423b799039.zip new file mode 100644 index 00000000000..2ce6374ad94 Binary files /dev/null and b/.yarn/cache/@solana-sysvars-npm-3.0.2-cad063b41a-423b799039.zip differ diff --git a/packages/sources/solana-functions/README.md b/packages/sources/solana-functions/README.md index 3e7d3633ec3..d8d561601d0 100644 --- a/packages/sources/solana-functions/README.md +++ b/packages/sources/solana-functions/README.md @@ -40,9 +40,9 @@ There are no rate limits for this adapter. ## Input Parameters -| Required? | Name | Description | Type | Options | Default | -| :-------: | :------: | :-----------------: | :----: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------: | -| | endpoint | The endpoint to use | string | [anchor-data](#anchor-data-endpoint), [buffer-layout](#buffer-layout-endpoint), [eusx-price](#eusx-price-endpoint), [extension](#extension-endpoint), [pool-token-rate](#pool-token-rate-endpoint), [sanctum-infinity](#sanctum-infinity-endpoint) | `eusx-price` | +| Required? | Name | Description | Type | Options | Default | +| :-------: | :------: | :-----------------: | :----: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------: | :----------: | +| | endpoint | The endpoint to use | string | [anchor-data](#anchor-data-endpoint), [buffer-layout](#buffer-layout-endpoint), [eusx-price](#eusx-price-endpoint), [extension](#extension-endpoint), [pool-token-rate](#pool-token-rate-endpoint), [sanctum-infinity](#sanctum-infinity-endpoint), [stslx-exchange-rate](#stslx-exchange-rate-endpoint), [strcusx-exchange-rate](#strcusx-exchange-rate-endpoint) | `eusx-price` | ## Eusx-price Endpoint @@ -239,4 +239,92 @@ Request: --- +## Stslx-exchange-rate Endpoint + +`stslx-exchange-rate` is the only supported name for this endpoint. + +### Input Params + +| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With | +| :-------: | :------------------------: | :-----: | :--------------------------------------------------------------------------: | :----: | :-----: | :--------------------------------------------: | :--------: | :------------: | +| | slxMintAddress | | SLX mint address | string | | `SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq` | | | +| | stslxMintAddress | | stSLX mint address | string | | `GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq` | | | +| | glamStateAddress | | GLAM state address used to derive the vault PDA | string | | `5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB` | | | +| | glamProtocolProgramAddress | | GLAM protocol program address used to derive the vault PDA | string | | `GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz` | | | +| | minRate | | Minimum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer | string | | | | | +| | maxRate | | Maximum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer | string | | | | | + +### Example + +Request: + +```json +{ + "data": { + "endpoint": "stslx-exchange-rate", + "slxMintAddress": "SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq", + "stslxMintAddress": "GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq", + "glamStateAddress": "5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB", + "glamProtocolProgramAddress": "GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz", + "minRate": "950000000000000000", + "maxRate": "1050000000000000000" + } +} +``` + +--- + +## Strcusx-exchange-rate Endpoint + +`strcusx-exchange-rate` is the only supported name for this endpoint. + +### Input Params + +| Required? | Name | Aliases | Description | Type | Options | Default | Depends On | Not Valid With | +| :-------: | :------------: | :-----: | :-----------------------------------------------------------------------------------: | :----: | :----------------: | :-----: | :--------: | :------------: | +| ✅ | programAddress | | The deployed Solstice yield strategy program address | string | | | | | +| ✅ | strategyName | | Solstice strcUSX strategy/accounting PDA seed from the current deployment/feed config | string | `STRC-USX-1` | | | | +| ✅ | tranche | | The tranche to price: junior or senior | string | `junior`, `senior` | | | | +| | minRate | | Minimum allowed strcUSX exchange rate as an 18-decimal fixed-point integer | string | | | | | +| | maxRate | | Maximum allowed strcUSX exchange rate as an 18-decimal fixed-point integer | string | | | | | + +### Example + +Request: + +```json +{ + "data": { + "endpoint": "strcusx-exchange-rate", + "programAddress": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "strategyName": "STRC-USX-1", + "tranche": "junior", + "minRate": "950000000000000000", + "maxRate": "1050000000000000000" + } +} +``` + +
+Additional Examples + +Request: + +```json +{ + "data": { + "endpoint": "strcusx-exchange-rate", + "programAddress": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "strategyName": "STRC-USX-1", + "tranche": "senior", + "minRate": "950000000000000000", + "maxRate": "1050000000000000000" + } +} +``` + +
+ +--- + MIT License diff --git a/packages/sources/solana-functions/package.json b/packages/sources/solana-functions/package.json index be41c0648aa..79b99b8d23c 100644 --- a/packages/sources/solana-functions/package.json +++ b/packages/sources/solana-functions/package.json @@ -34,7 +34,7 @@ "typescript": "5.8.3" }, "dependencies": { - "@chainlink/external-adapter-framework": "2.16.1", + "@chainlink/external-adapter-framework": "2.17.1", "@coral-xyz/anchor": "^0.31.1", "@solana/addresses": "^3.0.2", "@solana/buffer-layout": "^4.0.1", @@ -42,6 +42,7 @@ "@solana/rpc": "^3.0.2", "@solana/spl-stake-pool": "^1.1.8", "@solana/spl-token": "^0.4.14", + "@solana/sysvars": "3.0.2", "@solana/web3.js": "^1.95.8", "bn.js": "^5.2.0", "tslib": "2.4.1" diff --git a/packages/sources/solana-functions/src/endpoint/index.ts b/packages/sources/solana-functions/src/endpoint/index.ts index afbfd1f5477..665dcb2af21 100644 --- a/packages/sources/solana-functions/src/endpoint/index.ts +++ b/packages/sources/solana-functions/src/endpoint/index.ts @@ -4,3 +4,5 @@ export { endpoint as eusxPrice } from './eusx-price' export { endpoint as extension } from './extension' export { endpoint as poolTokenRate } from './pool-token-rate' export { endpoint as sanctumInfinity } from './sanctum-infinity' +export { endpoint as strcusxExchangeRate } from './strcusx-exchange-rate' +export { endpoint as stslxExchangeRate } from './stslx-exchange-rate' diff --git a/packages/sources/solana-functions/src/endpoint/strcusx-exchange-rate.ts b/packages/sources/solana-functions/src/endpoint/strcusx-exchange-rate.ts new file mode 100644 index 00000000000..eefd888024a --- /dev/null +++ b/packages/sources/solana-functions/src/endpoint/strcusx-exchange-rate.ts @@ -0,0 +1,87 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { type AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { config } from '../config' +import { validateRateBounds } from '../shared/exchange-rate-utils' +import { TRANCHES, type Tranche } from '../transport/strcusx-accounts' +import { strcusxExchangeRateTransport } from '../transport/strcusx-exchange-rate' + +const STRATEGY_NAMES = ['STRC-USX-1'] as const + +export const inputParameters = new InputParameters( + { + programAddress: { + description: 'The deployed Solstice yield strategy program address', + type: 'string', + required: true, + }, + strategyName: { + description: + 'Solstice strcUSX strategy/accounting PDA seed from the current deployment/feed config', + type: 'string', + options: [...STRATEGY_NAMES], + required: true, + }, + tranche: { + description: 'The tranche to price: junior or senior', + type: 'string', + options: [...TRANCHES], + required: true, + }, + minRate: { + description: 'Minimum allowed strcUSX exchange rate as an 18-decimal fixed-point integer', + type: 'string', + required: false, + }, + maxRate: { + description: 'Maximum allowed strcUSX exchange rate as an 18-decimal fixed-point integer', + type: 'string', + required: false, + }, + }, + [ + { + programAddress: '7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe', + strategyName: 'STRC-USX-1', + tranche: 'junior', + minRate: '950000000000000000', + maxRate: '1050000000000000000', + }, + { + programAddress: '7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe', + strategyName: 'STRC-USX-1', + tranche: 'senior', + minRate: '950000000000000000', + maxRate: '1050000000000000000', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: { + Result: string + Data: { + result: string + computedResult: string + tranche: Tranche + decimals: number + boundsApplied: boolean + trancheAssets: string + trancheShares: string + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'strcusx-exchange-rate', + aliases: [], + transport: strcusxExchangeRateTransport, + inputParameters, + customInputValidation: (req): AdapterInputError | undefined => { + validateRateBounds(req.requestContext.data.minRate, req.requestContext.data.maxRate) + + return + }, +}) diff --git a/packages/sources/solana-functions/src/endpoint/stslx-exchange-rate.ts b/packages/sources/solana-functions/src/endpoint/stslx-exchange-rate.ts new file mode 100644 index 00000000000..74f6ace086b --- /dev/null +++ b/packages/sources/solana-functions/src/endpoint/stslx-exchange-rate.ts @@ -0,0 +1,91 @@ +import { AdapterEndpoint } from '@chainlink/external-adapter-framework/adapter' +import { InputParameters } from '@chainlink/external-adapter-framework/validation' +import { type AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { config } from '../config' +import { validateRateBounds } from '../shared/exchange-rate-utils' +import { stslxExchangeRateTransport } from '../transport/stslx-exchange-rate' + +// Defaults come from Solstice's OPDATA-7578 production stSLX feed config and +// Solstice comms. They are request/job-spec fallbacks; if Solstice migrates, +// override these params first and update defaults only when feed defaults change. +export const DEFAULT_SLX_MINT_ADDRESS = 'SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq' +export const DEFAULT_STSLX_MINT_ADDRESS = 'GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq' +export const DEFAULT_GLAM_STATE_ADDRESS = '5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB' +export const DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS = 'GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz' + +export const inputParameters = new InputParameters( + { + slxMintAddress: { + description: 'SLX mint address', + type: 'string', + required: false, + default: DEFAULT_SLX_MINT_ADDRESS, + }, + stslxMintAddress: { + description: 'stSLX mint address', + type: 'string', + required: false, + default: DEFAULT_STSLX_MINT_ADDRESS, + }, + glamStateAddress: { + description: 'GLAM state address used to derive the vault PDA', + type: 'string', + required: false, + default: DEFAULT_GLAM_STATE_ADDRESS, + }, + glamProtocolProgramAddress: { + description: 'GLAM protocol program address used to derive the vault PDA', + type: 'string', + required: false, + default: DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS, + }, + minRate: { + description: 'Minimum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer', + type: 'string', + required: false, + }, + maxRate: { + description: 'Maximum allowed stSLX-SLX exchange rate as an 18-decimal fixed-point integer', + type: 'string', + required: false, + }, + }, + [ + { + slxMintAddress: DEFAULT_SLX_MINT_ADDRESS, + stslxMintAddress: DEFAULT_STSLX_MINT_ADDRESS, + glamStateAddress: DEFAULT_GLAM_STATE_ADDRESS, + glamProtocolProgramAddress: DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS, + minRate: '950000000000000000', + maxRate: '1050000000000000000', + }, + ], +) + +export type BaseEndpointTypes = { + Parameters: typeof inputParameters.definition + Response: { + Result: string + Data: { + result: string + computedResult: string + decimals: number + boundsApplied: boolean + slxBalance: string + stslxSupply: string + } + } + Settings: typeof config.settings +} + +export const endpoint = new AdapterEndpoint({ + name: 'stslx-exchange-rate', + aliases: [], + transport: stslxExchangeRateTransport, + inputParameters, + customInputValidation: (req): AdapterInputError | undefined => { + validateRateBounds(req.requestContext.data.minRate, req.requestContext.data.maxRate) + + return + }, +}) diff --git a/packages/sources/solana-functions/src/idl/strcusx_yield_strategy.json b/packages/sources/solana-functions/src/idl/strcusx_yield_strategy.json new file mode 100644 index 00000000000..5eb173a3aec --- /dev/null +++ b/packages/sources/solana-functions/src/idl/strcusx_yield_strategy.json @@ -0,0 +1,7540 @@ +{ + "address": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "metadata": { + "name": "yield_strategy", + "version": "0.1.0", + "spec": "0.1.0", + "description": "Created with Anchor" + }, + "instructions": [ + { + "name": "accept_controller_authority", + "docs": ["Accept the pending controller authority (step 2 of 2-step rotation)."], + "discriminator": [73, 52, 93, 131, 149, 164, 169, 247], + "accounts": [ + { + "name": "new_authority", + "docs": ["The pending authority taking over."], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller account."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + } + ], + "args": [] + }, + { + "name": "add_yield", + "docs": ["Ops top up the vesting vault with new yield USX."], + "discriminator": [121, 148, 123, 232, 62, 16, 126, 58], + "accounts": [ + { + "name": "signer", + "docs": [ + "Admin signer with `Permission::AddYield`. Also the SPL authority of", + "`admin_asset_account` — this wallet funds the top-up." + ], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `add_yield`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "vesting_vault", + "docs": [ + "Strategy's vesting vault — destination for the top-up.", + "Mutated by the CPI transfer from admin." + ], + "writable": true + }, + { + "name": "admin_asset_account", + "docs": [ + "Admin's USX source token account.", + "Mutated by the CPI transfer; signer signs as authority." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "AddYieldParams" + } + } + } + ] + }, + { + "name": "book_loss", + "docs": ["Ops book loss against the junior tranche."], + "discriminator": [136, 159, 247, 235, 12, 53, 38, 15], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::BookLoss`."], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the asset → loss CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `book_loss`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "asset_vault", + "docs": [ + "Strategy's asset vault — source of the booked loss.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "loss_vault", + "docs": [ + "Strategy's loss vault — destination of the booked loss.", + "Mutated by the CPI transfer." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "BookLossParams" + } + } + } + ] + }, + { + "name": "cancel_pending_controller_authority", + "docs": ["Clear the pending controller authority without rotating."], + "discriminator": [41, 26, 217, 250, 142, 9, 166, 174], + "accounts": [ + { + "name": "authority", + "docs": ["Current authority."], + "signer": true, + "relations": ["controller"] + }, + { + "name": "controller", + "docs": ["Top-level controller account."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + } + ], + "args": [] + }, + { + "name": "cancel_settlement_batch", + "docs": ["Ops cancel an active settlement batch."], + "discriminator": [164, 244, 195, 4, 94, 243, 106, 231], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::CancelSettlementBatch`."], + "signer": true + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch. Mutated by `apply_cancel`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "CancelSettlementBatchParams" + } + } + } + ] + }, + { + "name": "distribute_yield", + "docs": [ + "Ops drip yield from the vesting vault into the asset vault, opening a new", + "vesting epoch." + ], + "discriminator": [233, 92, 186, 157, 235, 238, 212, 114], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::DistributeYield`."], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the vesting → asset CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `distribute_yield`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "vesting_vault", + "docs": [ + "Strategy's vesting vault — source of the drip.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": [ + "Strategy's asset vault — destination of the drip.", + "Mutated by the CPI transfer." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "DistributeYieldParams" + } + } + } + ] + }, + { + "name": "edit_admin", + "docs": ["Authority replaces an existing admin's permission bitmap."], + "discriminator": [162, 22, 210, 186, 8, 127, 32, 19], + "accounts": [ + { + "name": "authority", + "docs": ["Authored call accessible only to the signer matching Controller.authority"], + "signer": true, + "relations": ["controller"] + }, + { + "name": "controller", + "docs": ["Top level Controller account managing the program."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "wallet", + "docs": [ + "The wallet whose admin record is being edited. Used only as the seed", + "source for the admin PDA below; not deserialised.", + "wallet would target a different admin PDA (or fail derivation)." + ] + }, + { + "name": "admin", + "docs": ["Existing admin PDA, selected by the wallet seed. Must already exist."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "wallet" + } + ] + } + } + ], + "args": [ + { + "name": "permissions", + "type": { + "vec": { + "defined": { + "name": "Permission" + } + } + } + } + ] + }, + { + "name": "edit_settlement_batch", + "docs": ["Ops edit a settlement batch's settlement time or junior-share cap."], + "discriminator": [238, 50, 204, 80, 47, 155, 214, 192], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::EditSettlementBatch`."], + "signer": true + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch. Mutated by the handler."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "EditSettlementBatchParams" + } + } + } + ] + }, + { + "name": "edit_strategy", + "docs": ["Admin partial-update of strategy config fields."], + "discriminator": [246, 26, 34, 70, 41, 77, 173, 91], + "accounts": [ + { + "name": "signer", + "docs": ["Program admin wallet with permissions to invoke this instruction."], + "signer": true + }, + { + "name": "admin", + "docs": ["Program admin PDAs that stores the wallet's permissions."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Strategy account to be edited, selected by the name seed. Must already exist."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "EditStrategyParams" + } + } + } + ] + }, + { + "name": "edit_strategy_fee_bps", + "docs": ["Admin updates only the senior instant-unlock fee."], + "discriminator": [120, 114, 175, 56, 35, 255, 216, 20], + "accounts": [ + { + "name": "signer", + "docs": ["Program admin wallet with permissions to invoke this instruction."], + "signer": true + }, + { + "name": "admin", + "docs": ["Program admin PDA that stores the wallet's permissions."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Strategy account to be edited, selected by the name seed. Must already exist."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "EditStrategyFeeBpsParams" + } + } + } + ] + }, + { + "name": "edit_strategy_prices", + "docs": ["Admin updates only the restricted/liquidation price levels."], + "discriminator": [19, 52, 206, 176, 179, 14, 241, 67], + "accounts": [ + { + "name": "signer", + "docs": ["Program admin wallet with permissions to invoke this instruction."], + "signer": true + }, + { + "name": "admin", + "docs": ["Program admin PDA that stores the wallet's permissions."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Strategy account to be edited, selected by the name seed. Must already exist."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "EditStrategyPricesParams" + } + } + } + ] + }, + { + "name": "execute_settlement_batch", + "docs": ["Ops settle a batch: burn shares, escrow net USX, route loss."], + "discriminator": [213, 233, 0, 230, 73, 40, 75, 118], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::ExecuteSettlementBatch`."], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `apply_junior_settlement`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch. Mutated via `apply_settle`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint."] + }, + { + "name": "junior_mint", + "docs": ["Junior tranche mint. Burned against in this instruction."], + "writable": true + }, + { + "name": "asset_vault", + "docs": ["Strategy's USX vault. Source of the net + loss transfers."], + "writable": true + }, + { + "name": "loss_vault", + "docs": ["Strategy's loss vault. Receives `realized_loss`."], + "writable": true + }, + { + "name": "batch_share_vault", + "docs": ["Per-batch junior token escrow. Burned to zero."], + "writable": true + }, + { + "name": "batch_asset_vault", + "docs": ["Per-batch USX escrow. Receives `net_assets`."], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ExecuteSettlementBatchParams" + } + } + } + ] + }, + { + "name": "init_admin", + "docs": ["Authority grants a wallet a new admin record with the given permissions."], + "discriminator": [97, 65, 97, 27, 200, 206, 72, 219], + "accounts": [ + { + "name": "authority", + "docs": ["Authored call accessible only to the signer matching Controller.authority"], + "signer": true, + "relations": ["controller"] + }, + { + "name": "payer", + "docs": ["Payer of the transaction"], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top level Controller account managing the program."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "wallet", + "docs": ["The wallet being granted admin."] + }, + { + "name": "admin", + "docs": ["New per-wallet admin PDA, seeded by `wallet.key()`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "wallet" + } + ] + } + }, + { + "name": "system_program", + "docs": ["System program"], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "permissions", + "type": { + "vec": { + "defined": { + "name": "Permission" + } + } + } + } + ] + }, + { + "name": "init_controller", + "docs": ["Bootstrap the program controller (authority + asset mint)."], + "discriminator": [45, 209, 90, 164, 172, 233, 14, 140], + "accounts": [ + { + "name": "authority", + "docs": ["Becomes `Controller.authority`. Must equal the program's upgrade authority."], + "signer": true + }, + { + "name": "payer", + "docs": ["Payer of the transaction"], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top level Controller account managing the program"], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["USX Mint (USX is SPL token)"] + }, + { + "name": "program_data", + "docs": ["Program data account; binds bootstrap to the upgrade authority."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 99, 191, 184, 59, 172, 115, 40, 123, 45, 178, 103, 99, 186, 38, 20, 234, 233, 212, + 38, 12, 1, 250, 43, 106, 48, 53, 117, 10, 65, 22, 234, 249 + ] + } + ], + "program": { + "kind": "const", + "value": [ + 2, 168, 246, 145, 78, 136, 161, 176, 226, 16, 21, 62, 247, 99, 174, 43, 0, 194, 185, + 61, 22, 193, 36, 210, 192, 83, 122, 16, 4, 128, 0, 0 + ] + } + } + }, + { + "name": "system_program", + "docs": ["System program"], + "address": "11111111111111111111111111111111" + } + ], + "args": [] + }, + { + "name": "init_junior_unlock_share", + "docs": ["User opens a per-(batch, user) junior unlock claim PDA."], + "discriminator": [132, 241, 199, 232, 48, 240, 37, 169], + "accounts": [ + { + "name": "user", + "docs": [ + "Junior depositor; beneficiary of the claim. Decoupled from `payer` so", + "multisig users can have a separate fee/rent payer." + ], + "signer": true + }, + { + "name": "payer", + "docs": ["Rent payer for the new claim PDA."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Status-checked in `validate`."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch the claim is anchored to."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + }, + { + "name": "junior_unlock_share", + "docs": ["New per-`(batch, user)` claim PDA. Created with zero shares."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 74, 85, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 83, 72, 65, 82, 69 + ] + }, + { + "kind": "account", + "path": "settlement_batch" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "docs": ["System program."], + "address": "11111111111111111111111111111111" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitJuniorUnlockShareParams" + } + } + } + ] + }, + { + "name": "init_senior_unlock_cooldown", + "docs": ["User opens a per-user senior cooldown PDA and vault."], + "discriminator": [188, 64, 58, 215, 235, 92, 153, 238], + "accounts": [ + { + "name": "user", + "docs": [ + "Senior holder; beneficiary of the cooldown. Decoupled from `payer` so", + "multisig users can have a separate fee/rent payer." + ], + "signer": true + }, + { + "name": "payer", + "docs": ["Rent payer for the new PDA + vault."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "senior_unlock_cooldown", + "docs": ["New per-`(strategy, user)` cooldown PDA."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 67, 79, 79, 76, 68, 79, + 87, 78 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_cooldown_vault", + "docs": [ + "Per-user senior cooldown vault. Holds USX queued for withdrawal;", + "authority is the cooldown PDA itself, so subsequent unlock/withdraw", + "operations are self-contained against this user's claim." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 67, 79, 79, 76, 68, 79, 87, 78, 95, 86, 65, 85, 76, 84 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "system_program", + "docs": ["System program."], + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitSeniorUnlockCooldownParams" + } + } + } + ] + }, + { + "name": "init_settlement_batch", + "docs": ["Ops open a new junior settlement batch."], + "discriminator": [235, 205, 119, 147, 50, 159, 112, 115], + "accounts": [ + { + "name": "signer", + "docs": ["Program admin wallet with permission to invoke this instruction."], + "signer": true + }, + { + "name": "payer", + "docs": ["Payer of the rent for the new batch + escrow accounts."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; needed to address-check the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin PDA, asserts the signer's permissions."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy, selected by name. Must already exist."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": [ + "New settlement batch account. Seeds bind the batch to its parent", + "strategy + a unique name; `init` rejects duplicates automatically." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "junior_mint", + "docs": ["Junior tranche mint. Address-checked against the parent strategy."] + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "share_vault", + "docs": [ + "Per-batch junior token escrow. Joins deposit here; settle burns;", + "cancel returns." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [66, 65, 84, 67, 72, 95, 83, 72, 65, 82, 69, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "asset_vault", + "docs": [ + "Per-batch USX escrow. Empty pre-settlement; receives net USX on", + "settle and is drained by withdrawals." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [66, 65, 84, 67, 72, 95, 65, 83, 83, 69, 84, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "system_program", + "docs": ["System program."], + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitSettlementBatchParams" + } + } + } + ] + }, + { + "name": "init_strategy", + "docs": ["Authority creates a new strategy (tranche mints, vaults, accounting, config)."], + "discriminator": [154, 74, 215, 216, 229, 204, 141, 241], + "accounts": [ + { + "name": "authority", + "docs": ["Authored call accessible only to the signer matching Controller.authority"], + "signer": true, + "relations": ["controller"] + }, + { + "name": "payer", + "docs": ["Payer of the transaction"], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top level Controller account managing the program"], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["New strategy account to be initialized, must not already exist."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "accounting", + "docs": [ + "Accounting account for the strategy, storing share and asset balances. Initialized alongside the strategy." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": [ + "Mint of the strategy's underlying asset, must match the controller's recorded asset mint." + ] + }, + { + "name": "junior_mint", + "docs": ["Junior share mint, initialized by this instruction."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [74, 85, 78, 73, 79, 82, 95, 77, 73, 78, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "senior_mint", + "docs": ["Senior share mint, initialized by this instruction."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 78, 73, 79, 82, 95, 77, 73, 78, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "asset_vault", + "docs": [ + "Asset vault owned by the strategy, initialized by this instruction.", + "All user funds are ultimately stored here." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 83, 83, 69, 84, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "vesting_vault", + "docs": [ + "Vesting vault owned by the strategy, initialized by this instruction.", + "Used to hold the portion of yield that is still vesting and subject to potential loss." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [86, 69, 83, 84, 73, 78, 71, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "fee_vault", + "docs": ["Fee vault owned by the strategy, initialized by this instruction."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [70, 69, 69, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "loss_vault", + "docs": [ + "Loss vault owned by the strategy, initialized by this instruction.", + "Used to hold assets that are earmarked to cover losses in the event of a strategy incurred loss." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [76, 79, 83, 83, 95, 86, 65, 85, 76, 84] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "junior_dead_share_vault", + "docs": ["Holds `MIN_TRANCHE_SHARES` junior tokens minted at bootstrap; locked."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 74, 85, 78, 73, 79, 82, 95, 68, 69, 65, 68, 95, 83, 72, 65, 82, 69, 95, 86, 65, + 85, 76, 84 + ] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "senior_dead_share_vault", + "docs": ["Holds `MIN_TRANCHE_SHARES` senior tokens minted at bootstrap; locked."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 68, 69, 65, 68, 95, 83, 72, 65, 82, 69, 95, 86, 65, + 85, 76, 84 + ] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "payer_asset_account", + "docs": ["Payer's USX source for the bootstrap seed transfer."], + "writable": true + }, + { + "name": "system_program", + "docs": ["System program"], + "address": "11111111111111111111111111111111" + }, + { + "name": "token_program", + "docs": ["SPL Token program"], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitStrategyParams" + } + } + } + ] + }, + { + "name": "init_tranche_metadata", + "docs": ["Authority attaches Metaplex metadata to both tranche mints."], + "discriminator": [9, 182, 70, 133, 82, 38, 175, 40], + "accounts": [ + { + "name": "authority", + "docs": ["Program authority (the only caller that may set tranche branding)."], + "signer": true, + "relations": ["controller"] + }, + { + "name": "payer", + "docs": ["Funds the two metadata accounts."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Mint authority of both tranche mints; signs the CPIs as a PDA."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "junior_mint" + }, + { + "name": "senior_mint" + }, + { + "name": "junior_metadata", + "docs": [ + "Junior metadata PDA; created by the Metaplex CPI. Unchecked because not initiated" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "account", + "path": "token_metadata_program" + }, + { + "kind": "account", + "path": "junior_mint" + } + ], + "program": { + "kind": "account", + "path": "token_metadata_program" + } + } + }, + { + "name": "senior_metadata", + "docs": [ + "Senior metadata PDA; created by the Metaplex CPI. Unchecked because not initiated" + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "account", + "path": "token_metadata_program" + }, + { + "kind": "account", + "path": "senior_mint" + } + ], + "program": { + "kind": "account", + "path": "token_metadata_program" + } + } + }, + { + "name": "token_metadata_program", + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + }, + { + "name": "system_program", + "address": "11111111111111111111111111111111" + }, + { + "name": "rent", + "address": "SysvarRent111111111111111111111111111111111" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "InitTrancheMetadataParams" + } + } + } + ] + }, + { + "name": "junior_lock", + "docs": ["User deposits USX and mints junior shares."], + "discriminator": [156, 75, 220, 68, 212, 216, 58, 110], + "accounts": [ + { + "name": "user", + "docs": ["User locking USX, signs and receives junior tokens."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy, selected by name."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state for the strategy. Mutated by `apply_junior_mint`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "junior_mint", + "docs": [ + "Junior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI mint to user." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": [ + "Strategy's USX vault — destination for the locked deposit.", + "Mutated by the CPI transfer from user." + ], + "writable": true + }, + { + "name": "user_asset_account", + "docs": [ + "User's USX source token account.", + "Mutated by the CPI transfer; user signs as authority." + ], + "writable": true + }, + { + "name": "user_junior_account", + "docs": ["User's destination junior token account."], + "writable": true + }, + { + "name": "price_update", + "docs": [ + "Pyth price update for the strategy's underlying asset. Anchor", + "validates owner + discriminator + borsh layout via", + "`Account<'_, T>`" + ] + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "JuniorLockParams" + } + } + } + ] + }, + { + "name": "junior_unlock_queued", + "docs": ["User escrows junior shares into a settlement batch."], + "discriminator": [39, 86, 194, 66, 159, 176, 155, 140], + "accounts": [ + { + "name": "user", + "docs": ["Junior depositor; signs the junior token transfer."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch the user is joining. Mutated by `apply_join`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + }, + { + "name": "junior_unlock_share", + "docs": [ + "Per-`(batch, user)` claim record. Must already be initialized via", + "`init_junior_unlock_share`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 74, 85, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 83, 72, 65, 82, 69 + ] + }, + { + "kind": "account", + "path": "settlement_batch" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "junior_mint", + "docs": ["Junior tranche mint. Address-checked against the strategy."] + }, + { + "name": "batch_share_vault", + "docs": [ + "Per-batch junior token escrow. Address-checked against the batch;", + "mutated by the CPI transfer from user." + ], + "writable": true + }, + { + "name": "user_junior_account", + "docs": ["User's source junior token account."], + "writable": true + }, + { + "name": "price_update", + "docs": ["Pyth price update for the strategy's underlying asset."] + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "JuniorUnlockQueuedParams" + } + } + } + ] + }, + { + "name": "junior_withdraw_cancelled", + "docs": ["User reclaims junior shares from a cancelled batch."], + "discriminator": [179, 201, 173, 16, 99, 164, 193, 40], + "accounts": [ + { + "name": "user", + "docs": ["Junior claimant; signs and receives rent on claim PDA close."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Used only to address-check `junior_mint`."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": [ + "Settlement batch the user is reclaiming from. Mutated by", + "`apply_cancelled_withdrawal`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + }, + { + "name": "junior_unlock_share", + "docs": [ + "Per-`(batch, user)` claim record. Closed on success; rent refunded to", + "`user`. Seeds bind it to this batch and signer, so an attacker cannot", + "substitute another user's PDA." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 74, 85, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 83, 72, 65, 82, 69 + ] + }, + { + "kind": "account", + "path": "settlement_batch" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "junior_mint", + "docs": ["Junior tranche mint. Address-checked against the strategy."] + }, + { + "name": "batch_share_vault", + "docs": [ + "Per-batch junior token escrow. Source of the transfer; address-checked", + "against the batch." + ], + "writable": true + }, + { + "name": "user_junior_account", + "docs": ["User's destination junior token account."], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "JuniorWithdrawCancelledParams" + } + } + } + ] + }, + { + "name": "junior_withdraw_settled", + "docs": ["User claims their USX from a settled batch."], + "discriminator": [2, 30, 196, 222, 253, 171, 139, 192], + "accounts": [ + { + "name": "user", + "docs": ["Junior claimant; signs and receives rent on claim PDA close."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": [ + "Settlement batch the user is withdrawing from. Mutated by", + "`apply_settled_withdrawal`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + }, + { + "name": "junior_unlock_share", + "docs": [ + "Per-`(batch, user)` claim record. Closed on success; rent refunded to", + "`user`. Seeds bind it to this batch and signer, so an attacker cannot", + "substitute another user's PDA." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 74, 85, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 83, 72, 65, 82, 69 + ] + }, + { + "kind": "account", + "path": "settlement_batch" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "batch_asset_vault", + "docs": [ + "Per-batch USX escrow. Source of the transfer; address-checked against", + "the batch." + ], + "writable": true + }, + { + "name": "user_asset_account", + "docs": ["User's destination USX token account."], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "JuniorWithdrawSettledParams" + } + } + } + ] + }, + { + "name": "junior_withdraw_winddown", + "docs": ["User redeems junior shares post-liquidation against the residual pool."], + "discriminator": [236, 60, 221, 126, 47, 186, 189, 69], + "accounts": [ + { + "name": "user", + "docs": ["Junior holder; signs and receives USX."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the asset_vault → user CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated by `apply_junior_redeem`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "junior_mint", + "docs": [ + "Junior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI burn." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": ["Strategy's USX vault — source of the redemption transfer."], + "writable": true + }, + { + "name": "user_asset_account", + "docs": ["User's destination USX token account."], + "writable": true + }, + { + "name": "user_junior_account", + "docs": [ + "User's source junior token account; user signs as authority for the", + "CPI burn." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "JuniorWithdrawWinddownParams" + } + } + } + ] + }, + { + "name": "liquidate_strategy", + "docs": [ + "Ops book a terminal liquidation loss; protocol applies the junior-first", + "waterfall internally." + ], + "discriminator": [118, 185, 254, 243, 40, 130, 33, 107], + "accounts": [ + { + "name": "signer", + "docs": [ + "Admin signer with `Permission::Liquidate`. In production this signer", + "is expected to be a multisig." + ], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the asset → loss CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `reset_vesting_schedule` + `book_loss`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "asset_vault", + "docs": [ + "Strategy's asset vault — source of the booked loss.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "loss_vault", + "docs": [ + "Strategy's loss vault — destination of the booked loss.", + "Mutated by the CPI transfer." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "LiquidateStrategyParams" + } + } + } + ] + }, + { + "name": "propose_controller_authority", + "docs": ["Propose a new controller authority (step 1 of 2-step rotation)."], + "discriminator": [20, 230, 43, 53, 249, 142, 185, 64], + "accounts": [ + { + "name": "authority", + "docs": ["Current authority. `has_one` pins this to `Controller.authority`."], + "signer": true, + "relations": ["controller"] + }, + { + "name": "controller", + "docs": ["Top-level controller account."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "ProposeControllerAuthorityParams" + } + } + } + ] + }, + { + "name": "senior_lock", + "docs": ["User deposits USX and mints senior shares."], + "discriminator": [247, 233, 15, 104, 84, 110, 25, 242], + "accounts": [ + { + "name": "user", + "docs": ["User locking USX, signs and receives senior tokens."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy, selected by name."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state for the strategy. Mutated by `apply_senior_mint`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_mint", + "docs": [ + "Senior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI mint to user." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": [ + "Strategy's USX vault — destination for the locked deposit.", + "Mutated by the CPI transfer from user." + ], + "writable": true + }, + { + "name": "user_asset_account", + "docs": [ + "User's USX source token account.", + "Mutated by the CPI transfer; user signs as authority." + ], + "writable": true + }, + { + "name": "user_senior_account", + "docs": ["User's destination senior token account."], + "writable": true + }, + { + "name": "price_update", + "docs": [ + "Pyth price update for the strategy's underlying asset. Anchor", + "validates owner + discriminator + borsh layout via", + "`Account<'_, T>`" + ] + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SeniorLockParams" + } + } + } + ] + }, + { + "name": "senior_unlock_instant", + "docs": ["User burns senior shares and receives USX instantly, minus a fee."], + "discriminator": [142, 212, 245, 29, 232, 1, 177, 55], + "accounts": [ + { + "name": "user", + "docs": ["Senior holder; signs and receives net USX."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated by `apply_senior_instant_unlock`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_mint", + "docs": [ + "Senior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI burn." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": ["Strategy's USX vault — source of both the fee and net transfers."], + "writable": true + }, + { + "name": "fee_vault", + "docs": ["Strategy's fee vault — destination of `fee`."], + "writable": true + }, + { + "name": "user_asset_account", + "docs": ["User's destination USX token account."], + "writable": true + }, + { + "name": "user_senior_account", + "docs": [ + "User's source senior token account; user signs as authority for the", + "CPI burn." + ], + "writable": true + }, + { + "name": "price_update", + "docs": ["Pyth price update for the strategy's underlying asset."] + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SeniorUnlockInstantParams" + } + } + } + ] + }, + { + "name": "senior_unlock_queued", + "docs": ["User burns senior shares and queues USX in their cooldown vault."], + "discriminator": [172, 145, 218, 234, 228, 114, 178, 124], + "accounts": [ + { + "name": "user", + "docs": ["Senior holder; signs the senior burn."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated by `apply_senior_redeem`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "senior_unlock_cooldown", + "docs": ["Per-`(strategy, user)` cooldown PDA. Must already be initialized."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 67, 79, 79, 76, 68, 79, + 87, 78 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_mint", + "docs": [ + "Senior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI burn." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": ["Strategy's USX vault — source of the gross redemption transfer."], + "writable": true + }, + { + "name": "senior_cooldown_vault", + "docs": [ + "Per-user senior cooldown vault — destination of the gross redemption.", + "Address-checked against the cooldown PDA's seeds." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 67, 79, 79, 76, 68, 79, 87, 78, 95, 86, 65, 85, 76, 84 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "user_senior_account", + "docs": [ + "User's source senior token account; user signs as authority for the", + "CPI burn." + ], + "writable": true + }, + { + "name": "price_update", + "docs": ["Pyth price update for the strategy's underlying asset."] + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SeniorUnlockQueuedParams" + } + } + } + ] + }, + { + "name": "senior_withdraw", + "docs": ["User drains their cooldown vault after cooldown elapses."], + "discriminator": [56, 162, 221, 20, 87, 178, 20, 107], + "accounts": [ + { + "name": "user", + "docs": ["Senior holder; signs and receives USX."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "senior_unlock_cooldown", + "docs": [ + "Per-`(strategy, user)` cooldown PDA. Mutated to clear `assets_pending`", + "and `earliest_withdraw_at`." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 67, 79, 79, 76, 68, 79, + 87, 78 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_cooldown_vault", + "docs": [ + "Per-user senior cooldown vault. Source of the transfer; authority is", + "the cooldown PDA which signs the withdrawal." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [ + 83, 69, 78, 73, 79, 82, 95, 67, 79, 79, 76, 68, 79, 87, 78, 95, 86, 65, 85, 76, 84 + ] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "account", + "path": "user" + } + ] + } + }, + { + "name": "user_asset_account", + "docs": ["User's destination USX token account."], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SeniorWithdrawParams" + } + } + } + ] + }, + { + "name": "senior_withdraw_winddown", + "docs": ["User redeems senior shares post-liquidation against the residual pool."], + "discriminator": [148, 239, 118, 155, 116, 244, 65, 106], + "accounts": [ + { + "name": "user", + "docs": ["Senior holder; signs and receives USX."], + "writable": true, + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the asset_vault → user CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated by `apply_senior_redeem`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "senior_mint", + "docs": [ + "Senior tranche mint. Address-checked against the strategy.", + "Mutated by the CPI burn." + ], + "writable": true + }, + { + "name": "asset_vault", + "docs": ["Strategy's USX vault — source of the redemption transfer."], + "writable": true + }, + { + "name": "user_asset_account", + "docs": ["User's destination USX token account."], + "writable": true + }, + { + "name": "user_senior_account", + "docs": [ + "User's source senior token account; user signs as authority for the", + "CPI burn." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SeniorWithdrawWinddownParams" + } + } + } + ] + }, + { + "name": "set_batch_paused", + "docs": ["Admin toggles a settlement batch's pause flag."], + "discriminator": [245, 114, 210, 36, 241, 65, 14, 98], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::EditSettlementBatch`."], + "signer": true + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "settlement_batch", + "docs": ["Settlement batch whose `is_paused` flag this instruction toggles."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72] + }, + { + "kind": "arg", + "path": "params.strategy_name" + }, + { + "kind": "const", + "value": [0] + }, + { + "kind": "arg", + "path": "params.batch_name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetBatchPausedParams" + } + } + } + ] + }, + { + "name": "set_program_paused", + "docs": ["Admin toggles the program-wide kill switch."], + "discriminator": [176, 232, 64, 139, 62, 165, 42, 171], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::PauseProgram`."], + "signer": true + }, + { + "name": "admin", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetProgramPausedParams" + } + } + } + ] + }, + { + "name": "set_strategy_paused", + "docs": ["Admin toggles the per-strategy operational pause."], + "discriminator": [174, 71, 247, 168, 24, 203, 33, 63], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::PauseStrategy`."], + "signer": true + }, + { + "name": "admin", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetStrategyPausedParams" + } + } + } + ] + }, + { + "name": "set_strategy_status", + "docs": ["Admin lifecycle transition: Active → Liquidating → WindedDown."], + "discriminator": [43, 141, 189, 181, 129, 114, 207, 109], + "accounts": [ + { + "name": "signer", + "docs": ["Admin signer with `Permission::SetStrategyStatus`."], + "signer": true + }, + { + "name": "admin", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "controller", + "docs": ["Top-level controller; check for program status."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + }, + { + "name": "accounting", + "docs": [ + "Mutated when the Liquidating → WindedDown transition fires to clear any", + "in-flight vesting schedule." + ], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.name" + } + ] + } + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "SetStrategyStatusParams" + } + } + } + ] + }, + { + "name": "update_tranche_metadata", + "docs": ["Authority updates tranche metadata (name/symbol/uri); stays mutable."], + "discriminator": [89, 114, 32, 94, 96, 60, 54, 48], + "accounts": [ + { + "name": "authority", + "docs": ["Program authority (the only caller that may edit tranche branding)."], + "signer": true, + "relations": ["controller"] + }, + { + "name": "controller", + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "strategy", + "docs": ["Update authority of both metadata accounts; signs the CPIs as a PDA."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "junior_mint" + }, + { + "name": "senior_mint" + }, + { + "name": "junior_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "account", + "path": "token_metadata_program" + }, + { + "kind": "account", + "path": "junior_mint" + } + ], + "program": { + "kind": "account", + "path": "token_metadata_program" + } + } + }, + { + "name": "senior_metadata", + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [109, 101, 116, 97, 100, 97, 116, 97] + }, + { + "kind": "account", + "path": "token_metadata_program" + }, + { + "kind": "account", + "path": "senior_mint" + } + ], + "program": { + "kind": "account", + "path": "token_metadata_program" + } + } + }, + { + "name": "token_metadata_program", + "address": "metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "UpdateTrancheMetadataParams" + } + } + } + ] + }, + { + "name": "withdraw_fee", + "docs": ["Ops drain the fee vault to a multisig-controlled account."], + "discriminator": [14, 122, 231, 218, 31, 238, 223, 150], + "accounts": [ + { + "name": "signer", + "docs": [ + "Admin signer with `Permission::WithdrawFee`. In production this signer", + "is expected to be a multisig. Also the SPL authority of", + "`admin_asset_account`, so the destination is bound to the multisig." + ], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the fee → admin CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `withdraw_fee`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "fee_vault", + "docs": [ + "Strategy's fee vault — source of the withdrawal.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "admin_asset_account", + "docs": [ + "Admin's destination USX token account.", + "Mutated by the CPI transfer; `signer` is checked as the SPL authority", + "so funds can only be routed to an account the multisig controls." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawFeeParams" + } + } + } + ] + }, + { + "name": "withdraw_loss", + "docs": ["Ops drain the loss vault to a multisig-controlled account."], + "discriminator": [18, 225, 126, 73, 88, 202, 25, 119], + "accounts": [ + { + "name": "signer", + "docs": [ + "Admin signer with `Permission::WithdrawLoss`. In production this signer", + "is expected to be a multisig. Also the SPL authority of", + "`admin_asset_account`, so the destination is bound to the multisig." + ], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the loss → admin CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `withdraw_loss`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "loss_vault", + "docs": [ + "Strategy's loss vault — source of the withdrawal.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "admin_asset_account", + "docs": [ + "Admin's destination USX token account.", + "Mutated by the CPI transfer; `signer` is checked as the SPL authority", + "so funds can only be routed to an account the multisig controls." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawLossParams" + } + } + } + ] + }, + { + "name": "withdraw_pending_yield", + "docs": ["Ops drain the vesting vault (inverse of `add_yield`)."], + "discriminator": [133, 119, 150, 133, 204, 147, 124, 233], + "accounts": [ + { + "name": "signer", + "docs": [ + "Admin signer with `Permission::WithdrawPendingYield`. In production this", + "signer is expected to be a multisig. Also the SPL authority of", + "`admin_asset_account`, so the destination is bound to the multisig." + ], + "signer": true + }, + { + "name": "controller", + "docs": ["Top-level controller; address-checks the asset mint."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [67, 79, 78, 84, 82, 79, 76, 76, 69, 82] + } + ] + } + }, + { + "name": "admin", + "docs": ["Per-wallet admin record."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 68, 77, 73, 78] + }, + { + "kind": "account", + "path": "signer" + } + ] + } + }, + { + "name": "strategy", + "docs": ["Parent strategy. Signs the vesting → admin CPI transfer."], + "pda": { + "seeds": [ + { + "kind": "const", + "value": [83, 84, 82, 65, 84, 69, 71, 89] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "accounting", + "docs": ["Accounting state. Mutated via `withdraw_pending_yield`."], + "writable": true, + "pda": { + "seeds": [ + { + "kind": "const", + "value": [65, 67, 67, 79, 85, 78, 84, 73, 78, 71] + }, + { + "kind": "arg", + "path": "params.strategy_name" + } + ] + } + }, + { + "name": "asset_mint", + "docs": ["Underlying asset (USX) mint. Address-checked against the controller."] + }, + { + "name": "vesting_vault", + "docs": [ + "Strategy's vesting vault — source of the withdrawal.", + "Mutated by the CPI transfer; strategy PDA signs as authority." + ], + "writable": true + }, + { + "name": "admin_asset_account", + "docs": [ + "Admin's destination USX token account.", + "Mutated by the CPI transfer; `signer` is checked as the SPL authority", + "so funds can only be routed to an account the multisig controls." + ], + "writable": true + }, + { + "name": "token_program", + "docs": ["SPL Token program."], + "address": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } + ], + "args": [ + { + "name": "params", + "type": { + "defined": { + "name": "WithdrawPendingYieldParams" + } + } + } + ] + } + ], + "accounts": [ + { + "name": "AccountingState", + "discriminator": [9, 238, 56, 53, 228, 92, 217, 40] + }, + { + "name": "Admin", + "discriminator": [244, 158, 220, 65, 8, 73, 4, 65] + }, + { + "name": "Controller", + "discriminator": [184, 79, 171, 0, 183, 43, 113, 110] + }, + { + "name": "JuniorUnlockShare", + "discriminator": [205, 221, 70, 69, 118, 152, 135, 46] + }, + { + "name": "SeniorUnlockCooldown", + "discriminator": [81, 60, 143, 13, 48, 179, 176, 86] + }, + { + "name": "SettlementBatch", + "discriminator": [109, 41, 32, 249, 12, 118, 184, 199] + }, + { + "name": "Strategy", + "discriminator": [174, 110, 39, 119, 82, 106, 169, 102] + } + ], + "events": [ + { + "name": "AddYieldEvent", + "discriminator": [36, 188, 88, 225, 236, 52, 30, 35] + }, + { + "name": "AdminPermissionsEvent", + "discriminator": [255, 171, 188, 5, 6, 127, 200, 71] + }, + { + "name": "BookLossEvent", + "discriminator": [59, 158, 11, 112, 87, 185, 233, 6] + }, + { + "name": "CancelSettlementBatchEvent", + "discriminator": [238, 61, 205, 227, 181, 125, 249, 225] + }, + { + "name": "ControllerAuthorityRotationEvent", + "discriminator": [7, 41, 46, 29, 97, 197, 124, 31] + }, + { + "name": "DistributeYieldEvent", + "discriminator": [57, 131, 22, 46, 43, 84, 189, 122] + }, + { + "name": "EditSettlementBatchEvent", + "discriminator": [38, 252, 23, 174, 126, 148, 66, 164] + }, + { + "name": "EditStrategyEvent", + "discriminator": [79, 19, 118, 209, 131, 199, 33, 115] + }, + { + "name": "ExecuteSettlementBatchEvent", + "discriminator": [4, 220, 35, 125, 23, 200, 118, 121] + }, + { + "name": "InitControllerEvent", + "discriminator": [50, 12, 10, 166, 149, 191, 27, 111] + }, + { + "name": "InitJuniorUnlockShareEvent", + "discriminator": [138, 208, 136, 253, 20, 234, 79, 20] + }, + { + "name": "InitSeniorUnlockCooldownEvent", + "discriminator": [219, 153, 97, 70, 215, 190, 249, 111] + }, + { + "name": "InitSettlementBatchEvent", + "discriminator": [194, 216, 186, 20, 232, 92, 225, 68] + }, + { + "name": "InitStrategyEvent", + "discriminator": [151, 117, 20, 134, 159, 239, 215, 172] + }, + { + "name": "JuniorLockEvent", + "discriminator": [86, 161, 241, 182, 126, 70, 143, 52] + }, + { + "name": "JuniorUnlockQueuedEvent", + "discriminator": [97, 71, 144, 71, 244, 41, 190, 53] + }, + { + "name": "JuniorWithdrawCancelledEvent", + "discriminator": [15, 23, 174, 87, 59, 189, 207, 79] + }, + { + "name": "JuniorWithdrawSettledEvent", + "discriminator": [173, 32, 159, 188, 55, 182, 204, 207] + }, + { + "name": "JuniorWithdrawWinddownEvent", + "discriminator": [67, 230, 60, 9, 134, 239, 60, 104] + }, + { + "name": "LiquidateStrategyEvent", + "discriminator": [137, 218, 106, 221, 121, 85, 179, 141] + }, + { + "name": "SeniorLockEvent", + "discriminator": [195, 217, 1, 181, 167, 131, 223, 238] + }, + { + "name": "SeniorUnlockInstantEvent", + "discriminator": [91, 244, 153, 145, 24, 116, 221, 57] + }, + { + "name": "SeniorUnlockQueuedEvent", + "discriminator": [184, 1, 58, 15, 18, 80, 3, 28] + }, + { + "name": "SeniorWithdrawEvent", + "discriminator": [63, 108, 175, 124, 81, 91, 215, 118] + }, + { + "name": "SeniorWithdrawWinddownEvent", + "discriminator": [26, 96, 247, 106, 154, 161, 198, 10] + }, + { + "name": "SetBatchPausedEvent", + "discriminator": [54, 158, 12, 15, 54, 3, 188, 191] + }, + { + "name": "SetProgramPausedEvent", + "discriminator": [204, 128, 188, 222, 137, 144, 162, 145] + }, + { + "name": "SetStrategyPausedEvent", + "discriminator": [62, 156, 54, 87, 59, 120, 143, 61] + }, + { + "name": "SetStrategyStatusEvent", + "discriminator": [179, 127, 155, 100, 236, 232, 82, 184] + }, + { + "name": "TrancheMetadataEvent", + "discriminator": [45, 131, 113, 141, 136, 29, 186, 146] + }, + { + "name": "WithdrawFeeEvent", + "discriminator": [242, 145, 119, 66, 223, 172, 95, 55] + }, + { + "name": "WithdrawLossEvent", + "discriminator": [211, 161, 159, 250, 203, 248, 120, 132] + }, + { + "name": "WithdrawPendingYieldEvent", + "discriminator": [161, 209, 6, 101, 212, 194, 77, 188] + } + ], + "errors": [ + { + "code": 6000, + "name": "MathError", + "msg": "Math operation failed (overflow, underflow, or cast)" + }, + { + "code": 6001, + "name": "InvalidAuthority", + "msg": "Only the Program authority can access this instruction" + }, + { + "code": 6002, + "name": "NoPendingAuthority", + "msg": "No pending authority rotation is in flight" + }, + { + "code": 6003, + "name": "Unauthorized", + "msg": "Caller lacks the required permission" + }, + { + "code": 6004, + "name": "InvalidAssetMint", + "msg": "Asset mint does not match configuration" + }, + { + "code": 6005, + "name": "InvalidJuniorMint", + "msg": "Junior mint does not match strategy configuration" + }, + { + "code": 6006, + "name": "InvalidSeniorMint", + "msg": "Senior mint does not match strategy configuration" + }, + { + "code": 6007, + "name": "InvalidStrategyStatus", + "msg": "Strategy status is invalid" + }, + { + "code": 6008, + "name": "PriceFeedNotFullyVerified", + "msg": "Price feed update is not fully verified" + }, + { + "code": 6009, + "name": "MismatchedFeedId", + "msg": "Price feed id does not match strategy configuration" + }, + { + "code": 6010, + "name": "PriceTooOld", + "msg": "Price feed update is older than the configured maximum age" + }, + { + "code": 6011, + "name": "PriceUnreliable", + "msg": "Price feed update is unreliable: non-positive price, or confidence exceeds configured maximum" + }, + { + "code": 6012, + "name": "InsufficientShares", + "msg": "Not enough shares outstanding to burn" + }, + { + "code": 6013, + "name": "InsufficientAssets", + "msg": "Not enough assets in tranche to withdraw" + }, + { + "code": 6014, + "name": "LiquidationPriceReached", + "msg": "STRC price is at or below the configured liquidation level" + }, + { + "code": 6015, + "name": "SlippageExceeded", + "msg": "Quoted shares fall short of the requested minimum" + }, + { + "code": 6016, + "name": "ZeroSharesMint", + "msg": "Deposit is too small to mint any shares at the current price" + }, + { + "code": 6017, + "name": "ZeroAssetsDeposit", + "msg": "Zero assets deposited" + }, + { + "code": 6018, + "name": "ZeroSharesUnlock", + "msg": "Shares to unlock is zero" + }, + { + "code": 6019, + "name": "ShareCapExceeded", + "msg": "Mint would exceed the tranche's supply cap" + }, + { + "code": 6020, + "name": "RestrictedPriceReached", + "msg": "STRC price is at or below the configured restricted level" + }, + { + "code": 6021, + "name": "JuniorQueueCutoffPassed", + "msg": "Batch settlement is too close; queue in a later batch" + }, + { + "code": 6022, + "name": "BelowMinimumUnlock", + "msg": "Shares to unlock fall below the configured minimum" + }, + { + "code": 6023, + "name": "SettlementBeforeDate", + "msg": "Settlement is not yet allowed before the batch's settlement date" + }, + { + "code": 6024, + "name": "LossExceedsRedemption", + "msg": "Realized loss exceeds the gross junior redemption" + }, + { + "code": 6025, + "name": "LossExceedsConfiguredCap", + "msg": "Realized loss exceeds the configured per-batch cap" + }, + { + "code": 6026, + "name": "FeeExceedsRedemption", + "msg": "Instant-unlock fee exceeds the gross senior redemption" + }, + { + "code": 6027, + "name": "ZeroAssetsUnlock", + "msg": "Net assets payable to the user is zero" + }, + { + "code": 6028, + "name": "CooldownNotElapsed", + "msg": "Senior cooldown has not elapsed yet" + }, + { + "code": 6029, + "name": "ZeroYield", + "msg": "Yield to distribute is zero" + }, + { + "code": 6030, + "name": "SeniorYieldExceedsTotal", + "msg": "Senior yield exceeds total yield" + }, + { + "code": 6031, + "name": "ZeroLoss", + "msg": "Loss to book is zero" + }, + { + "code": 6032, + "name": "SeniorLossExceedsTotal", + "msg": "Senior loss exceeds total loss" + }, + { + "code": 6033, + "name": "ZeroAssetsWithdraw", + "msg": "Assets to withdraw is zero" + }, + { + "code": 6034, + "name": "PauseStateUnchanged", + "msg": "Pause state is already at the requested value" + }, + { + "code": 6035, + "name": "StrategyPaused", + "msg": "Strategy is paused" + }, + { + "code": 6036, + "name": "ProgramPaused", + "msg": "Program is paused" + }, + { + "code": 6037, + "name": "InvalidStrategyName", + "msg": "Strategy name must be 1..=32 bytes" + }, + { + "code": 6038, + "name": "InvalidSettlementBatchName", + "msg": "Settlement batch name must be 1..=32 bytes" + }, + { + "code": 6039, + "name": "InvalidSettlementTime", + "msg": "Settlement time must be in the future" + }, + { + "code": 6040, + "name": "InvalidStrategyConfig", + "msg": "Strategy config is invalid" + }, + { + "code": 6041, + "name": "InsufficientBalance", + "msg": "Not enough balance to perform the operation" + }, + { + "code": 6042, + "name": "VestingOverlap", + "msg": "Vesting schedule overlaps with existing schedule" + }, + { + "code": 6043, + "name": "LossMoreThanVestedAssets", + "msg": "Loss exceeds vested assets" + }, + { + "code": 6044, + "name": "InvalidOperation", + "msg": "Invalid operation" + }, + { + "code": 6045, + "name": "InvalidStrategyStatusTransition", + "msg": "Strategy status transition is not allowed" + }, + { + "code": 6046, + "name": "InvalidSettlementBatchStatus", + "msg": "Settlement batch status is invalid" + }, + { + "code": 6047, + "name": "InvalidSettlementBatchStatusForOperation", + "msg": "Settlement batch is in the wrong status for this operation" + }, + { + "code": 6048, + "name": "SettlementBatchPaused", + "msg": "Settlement batch is paused" + }, + { + "code": 6049, + "name": "OrphanedTrancheAssets", + "msg": "Redemption would leave non-zero tranche assets backing zero or below-minimum shares" + }, + { + "code": 6050, + "name": "WouldDrainJuniorClaim", + "msg": "Book loss would drain junior claim to zero; use liquidate_strategy instead" + }, + { + "code": 6051, + "name": "BatchJuniorCapExceeded", + "msg": "Junior unlock would exceed the batch's max_junior_shares cap" + }, + { + "code": 6052, + "name": "InvalidBatchJuniorCap", + "msg": "Batch junior cap must be 0 or at or above current total_shares" + } + ], + "types": [ + { + "name": "AccountingState", + "docs": [ + "Tracks share supply, asset balances, and linear vesting for the senior", + "and junior tranches. Junior amounts are derived as `total - senior`." + ], + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "docs": ["Human-readable identifier matching the owning [`crate::state::Strategy`]."], + "type": { + "array": ["u8", 32] + } + }, + { + "name": "bump", + "docs": ["PDA bump for this accounting account."], + "type": "u8" + }, + { + "name": "senior_shares", + "docs": ["Outstanding senior tranche shares."], + "type": "u128" + }, + { + "name": "junior_shares", + "docs": ["Outstanding junior tranche shares."], + "type": "u128" + }, + { + "name": "total_assets", + "docs": ["Total assets backing both tranches (includes unvested portion)."], + "type": "u128" + }, + { + "name": "senior_assets", + "docs": ["Portion of `total_assets` allocated to the senior tranche."], + "type": "u128" + }, + { + "name": "total_vesting_assets", + "docs": ["Total assets currently subject to the active vesting schedule."], + "type": "u64" + }, + { + "name": "senior_vesting_assets", + "docs": ["Senior portion of `total_vesting_assets`."], + "type": "u64" + }, + { + "name": "vesting_start_time", + "docs": [ + "Unix timestamp when vesting begins; before this, all vesting assets are unvested." + ], + "type": "u64" + }, + { + "name": "vesting_end_time", + "docs": [ + "Unix timestamp when vesting completes; at/after this, all vesting assets are vested." + ], + "type": "u64" + }, + { + "name": "vesting_vault_balance", + "docs": ["Total assets (USX) in the vesting vault (pending yields)"], + "type": "u128" + }, + { + "name": "loss_vault_balance", + "docs": ["Total assets (USX) in the loss vault"], + "type": "u128" + }, + { + "name": "fee_vault_balance", + "docs": ["Total assets (USX) in the fee vault"], + "type": "u128" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 256] + } + } + ] + } + }, + { + "name": "AddYieldEvent", + "docs": [ + "Emitted by `add_yield`. Records the admin top-up of USX into the", + "strategy's vesting vault — yield is held there until `distribute_yield`", + "rolls a new vesting epoch." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "assets_to_top_up", + "type": "u64" + } + ] + } + }, + { + "name": "AddYieldParams", + "docs": ["Args for topping up the strategy's vesting vault with new yield."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "assets_to_top_up", + "docs": ["Amount of USX (raw token units) to top up to the vesting vault."], + "type": "u64" + } + ] + } + }, + { + "name": "Admin", + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": ["PDA bump for this account."], + "type": "u8" + }, + { + "name": "wallet", + "docs": ["The admin wallet — also the second seed component."], + "type": "pubkey" + }, + { + "name": "permissions", + "docs": ["Bitmap of granted capabilities."], + "type": "u64" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 128] + } + } + ] + } + }, + { + "name": "AdminPermissionsEvent", + "docs": [ + "Emitted by both `init_admin` and `edit_admin`. `is_init` distinguishes", + "the first-time grant from a subsequent edit; `permissions` is the full", + "post-write bitmap." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "wallet", + "type": "pubkey" + }, + { + "name": "permissions", + "type": "u64" + }, + { + "name": "is_init", + "type": "bool" + } + ] + } + }, + { + "name": "BookLossEvent", + "docs": [ + "Emitted by `book_loss`. Records the USX moved from `asset_vault` to", + "`loss_vault`. Junior tranche absorbs the entire amount in BAU/Restricted", + "operation; the liquidation instruction emits its own event with", + "a `senior_loss` field." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "total_loss", + "type": "u64" + } + ] + } + }, + { + "name": "BookLossParams", + "docs": [ + "Args for booking a realised trading loss against the junior tranche.", + "", + "`senior_loss` is intentionally omitted — `book_loss` shields senior assets", + "in BAU/Restricted operation." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "total_loss", + "docs": [ + "Total USX (raw token units) to move from `asset_vault` to `loss_vault`.", + "Junior tranche absorbs the entire amount via `total_assets - senior_assets`." + ], + "type": "u64" + } + ] + } + }, + { + "name": "CancelSettlementBatchEvent", + "docs": [ + "Emitted by `cancel_settlement_batch`. Records the cancel and the", + "`total_shares` snapshot — i.e. how many junior tokens are now waiting", + "to be reclaimed by their owners via `junior_withdraw_cancelled`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "total_shares", + "type": "u64" + } + ] + } + }, + { + "name": "CancelSettlementBatchParams", + "docs": ["Args for cancelling a junior settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "Controller", + "docs": [ + "Program-level singleton account. Holds settings shared across every", + "strategy under this program (authority, the deposit asset mint, global", + "pause). Per-strategy state lives in [`crate::state::Strategy`]." + ], + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": ["PDA bump for this controller account."], + "type": "u8" + }, + { + "name": "authority", + "docs": ["Admin authority allowed to invoke privileged instructions."], + "type": "pubkey" + }, + { + "name": "pending_authority", + "docs": [ + "Two-step rotation slot. `Pubkey::default()` when no rotation is in", + "flight; otherwise the address that `accept_controller_authority` will", + "promote into `authority`." + ], + "type": "pubkey" + }, + { + "name": "asset_mint", + "docs": ["Deposit asset mint (USX) shared by all strategies under this program."], + "type": "pubkey" + }, + { + "name": "is_paused", + "docs": [ + "Program-wide pause toggle (1 = paused). Halts all strategies regardless", + "of their individual pause/lifecycle state." + ], + "type": "u8" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 256] + } + } + ] + } + }, + { + "name": "ControllerAuthorityRotationEvent", + "docs": [ + "Emitted by `propose_controller_authority`, `accept_controller_authority`,", + "and `cancel_pending_controller_authority`. Carries a pre/post snapshot of", + "both `authority` and `pending_authority` so an indexer can reconstruct", + "the rotation lifecycle from a single event stream.", + "", + "Per-kind semantics:", + "- `Proposed`: `authority_before == authority_after`;", + "`pending_authority_after` is the proposed pubkey (non-default).", + "- `Accepted`: `authority_after == pending_authority_before`;", + "`pending_authority_after == Pubkey::default()`.", + "- `Cancelled`: `authority_before == authority_after`;", + "`pending_authority_after == Pubkey::default()`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "kind", + "type": { + "defined": { + "name": "ControllerAuthorityRotationKind" + } + } + }, + { + "name": "signer", + "docs": ["Wallet that invoked the instruction"], + "type": "pubkey" + }, + { + "name": "authority_before", + "type": "pubkey" + }, + { + "name": "authority_after", + "type": "pubkey" + }, + { + "name": "pending_authority_before", + "type": "pubkey" + }, + { + "name": "pending_authority_after", + "type": "pubkey" + } + ] + } + }, + { + "name": "ControllerAuthorityRotationKind", + "docs": [ + "Discriminates the three controller-authority-rotation instructions", + "emitting [`ControllerAuthorityRotationEvent`]. Variant indices are stable;", + "only append new variants at the end." + ], + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Proposed" + }, + { + "name": "Accepted" + }, + { + "name": "Cancelled" + } + ] + } + }, + { + "name": "DistributeYieldEvent", + "docs": [ + "Emitted by `distribute_yield`. Records the dripped amount, senior split,", + "and the new vesting window so indexers can track the schedule without", + "re-reading state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "total_yield", + "type": "u64" + }, + { + "name": "senior_yield", + "type": "u64" + }, + { + "name": "vesting_start_time", + "type": "u64" + }, + { + "name": "vesting_end_time", + "type": "u64" + } + ] + } + }, + { + "name": "DistributeYieldParams", + "docs": ["Args for dripping vested yield from the vesting vault into the asset vault."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "total_yield", + "docs": [ + "Total USX (raw token units) to drip from the vesting vault into the", + "asset vault." + ], + "type": "u64" + }, + { + "name": "senior_yield", + "docs": [ + "Senior tranche's portion of `total_yield`. Junior gets the remainder", + "implicitly." + ], + "type": "u64" + } + ] + } + }, + { + "name": "EditSettlementBatchEvent", + "docs": [ + "Emitted by `edit_settlement_batch`. Carries the post-edit snapshot of", + "the editable fields so indexers can reflect the change without diffing", + "against prior state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "settlement_time", + "type": "u64" + }, + { + "name": "max_junior_shares", + "type": "u64" + } + ] + } + }, + { + "name": "EditSettlementBatchParams", + "docs": [ + "Args for editing a junior settlement batch. Each `Option` field is", + "optional — `None` leaves the on-chain value unchanged." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + }, + { + "name": "settlement_time", + "docs": [ + "New earliest unix time at which settlement may run. Must be at or", + "after the current chain time when set." + ], + "type": { + "option": "u64" + } + }, + { + "name": "max_junior_shares", + "docs": [ + "junior-share cap. `0` disables the check. Lowering below the", + "current `total_shares` is rejected (would put the batch past its", + "own cap)." + ], + "type": { + "option": "u64" + } + } + ] + } + }, + { + "name": "EditStrategyEvent", + "docs": [ + "Emitted by any strategy-config edit instruction. Carries the full", + "post-edit config snapshot — indexers can answer \"what is the config now?\"", + "without comparing against prior state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "pyth_price_feed_id", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price_max_age", + "type": "u64" + }, + { + "name": "price_max_conf_bps", + "type": "u32" + }, + { + "name": "restricted_price", + "type": "u32" + }, + { + "name": "liquidation_price", + "type": "u32" + }, + { + "name": "yield_vesting_period_seconds", + "type": "u32" + }, + { + "name": "junior_queue_time_seconds", + "type": "u32" + }, + { + "name": "senior_queue_time_seconds", + "type": "u32" + }, + { + "name": "junior_share_cap", + "type": "u128" + }, + { + "name": "senior_share_cap", + "type": "u128" + }, + { + "name": "junior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_instant_unlock_fee_bps", + "type": "u32" + }, + { + "name": "max_settlement_loss_bps", + "type": "u32" + } + ] + } + }, + { + "name": "EditStrategyFeeBpsParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "senior_instant_unlock_fee_bps", + "type": "u32" + } + ] + } + }, + { + "name": "EditStrategyParams", + "docs": [ + "Partial update bundle. Each `Some` overwrites the corresponding `Strategy`", + "field; `None` leaves it as-is. `name` selects the strategy by PDA seed and", + "is itself immutable — there is no rename path." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "pyth_price_feed_id", + "type": { + "option": { + "array": ["u8", 32] + } + } + }, + { + "name": "price_max_age", + "type": { + "option": "u64" + } + }, + { + "name": "price_max_conf_bps", + "type": { + "option": "u32" + } + }, + { + "name": "restricted_price", + "type": { + "option": "u32" + } + }, + { + "name": "liquidation_price", + "type": { + "option": "u32" + } + }, + { + "name": "yield_vesting_period_seconds", + "type": { + "option": "u32" + } + }, + { + "name": "junior_queue_time_seconds", + "type": { + "option": "u32" + } + }, + { + "name": "senior_queue_time_seconds", + "type": { + "option": "u32" + } + }, + { + "name": "junior_share_cap", + "type": { + "option": "u128" + } + }, + { + "name": "senior_share_cap", + "type": { + "option": "u128" + } + }, + { + "name": "junior_minimum_unlock_shares", + "type": { + "option": "u64" + } + }, + { + "name": "senior_minimum_unlock_shares", + "type": { + "option": "u64" + } + }, + { + "name": "senior_instant_unlock_fee_bps", + "type": { + "option": "u32" + } + }, + { + "name": "max_settlement_loss_bps", + "type": { + "option": "u32" + } + } + ] + } + }, + { + "name": "EditStrategyPricesParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "restricted_price", + "type": "u32" + }, + { + "name": "liquidation_price", + "type": "u32" + } + ] + } + }, + { + "name": "ExecuteSettlementBatchEvent", + "docs": [ + "Emitted by `execute_settlement_batch`. Records what was burned and how the", + "gross redemption was split between the batch escrow (net) and the loss", + "vault (realized_loss)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "total_shares_burned", + "type": "u64" + }, + { + "name": "gross_assets", + "type": "u64" + }, + { + "name": "net_assets", + "type": "u64" + }, + { + "name": "realized_loss", + "type": "u64" + } + ] + } + }, + { + "name": "ExecuteSettlementBatchParams", + "docs": ["Args for settling a junior settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + }, + { + "name": "realized_loss", + "docs": [ + "USX loss admin is declaring for this batch (taken from the gross", + "junior redemption and routed to the loss vault). Must satisfy", + "`loss <= gross` and the strategy's `max_settlement_loss_bps` cap." + ], + "type": "u64" + } + ] + } + }, + { + "name": "InitControllerEvent", + "docs": ["Emitted by `init_controller`. One per program deployment."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "asset_mint", + "type": "pubkey" + } + ] + } + }, + { + "name": "InitJuniorUnlockShareEvent", + "docs": [ + "Emitted by `init_junior_unlock_share`. Records a user opening their", + "per-`(strategy, batch)` claim PDA." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + } + ] + } + }, + { + "name": "InitJuniorUnlockShareParams", + "docs": ["Args for opening a per-`(strategy, batch, user)` junior unlock claim PDA."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed for the settlement batch lookup."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "InitSeniorUnlockCooldownEvent", + "docs": [ + "Emitted by `init_senior_unlock_cooldown`. Records a user opening their", + "per-`(strategy, user)` cooldown PDA + vault." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + } + ] + } + }, + { + "name": "InitSeniorUnlockCooldownParams", + "docs": ["Args for opening a per-`(strategy, user)` senior cooldown PDA + vault."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "InitSettlementBatchEvent", + "docs": [ + "Emitted by `init_settlement_batch`. Carries the batch identity and the", + "initial settlement window so an indexer can register the new batch", + "without re-reading the account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "settlement_time", + "type": "u64" + }, + { + "name": "max_junior_shares", + "docs": ["`0` if the batch has no junior-share cap."], + "type": "u64" + } + ] + } + }, + { + "name": "InitSettlementBatchParams", + "docs": ["Args for opening a new junior settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. Used as a PDA seed."], + "type": "string" + }, + { + "name": "name", + "docs": [ + "Human-readable batch identifier (e.g. \"MAY-25-01\"). Used as a PDA", + "seed; uniqueness is enforced by the `init` constraint on the batch", + "account." + ], + "type": "string" + }, + { + "name": "settlement_time", + "docs": ["Earliest unix time at which settlement may be executed."], + "type": "u64" + }, + { + "name": "max_junior_shares", + "docs": [ + "Optional cap on the cumulative junior shares this batch will accept.", + "First-come-first-serve: joins past the cap reject. 0 disables." + ], + "type": "u64" + } + ] + } + }, + { + "name": "InitStrategyEvent", + "docs": [ + "Emitted by `init_strategy`. Carries the full launch config so an indexer", + "can reconstruct the strategy without separately reading the account." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "pyth_price_feed_id", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price_max_age", + "type": "u64" + }, + { + "name": "price_max_conf_bps", + "type": "u32" + }, + { + "name": "restricted_price", + "type": "u32" + }, + { + "name": "liquidation_price", + "type": "u32" + }, + { + "name": "yield_vesting_period_seconds", + "type": "u32" + }, + { + "name": "junior_queue_time_seconds", + "type": "u32" + }, + { + "name": "senior_queue_time_seconds", + "type": "u32" + }, + { + "name": "junior_share_cap", + "type": "u128" + }, + { + "name": "senior_share_cap", + "type": "u128" + }, + { + "name": "junior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_instant_unlock_fee_bps", + "type": "u32" + }, + { + "name": "max_settlement_loss_bps", + "type": "u32" + } + ] + } + }, + { + "name": "InitStrategyParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "pyth_price_feed_id", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price_max_age", + "type": "u64" + }, + { + "name": "price_max_conf_bps", + "type": "u32" + }, + { + "name": "restricted_price", + "type": "u32" + }, + { + "name": "liquidation_price", + "type": "u32" + }, + { + "name": "yield_vesting_period_seconds", + "type": "u32" + }, + { + "name": "junior_queue_time_seconds", + "type": "u32" + }, + { + "name": "senior_queue_time_seconds", + "type": "u32" + }, + { + "name": "junior_share_cap", + "type": "u128" + }, + { + "name": "senior_share_cap", + "type": "u128" + }, + { + "name": "junior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_minimum_unlock_shares", + "type": "u64" + }, + { + "name": "senior_instant_unlock_fee_bps", + "type": "u32" + }, + { + "name": "max_settlement_loss_bps", + "type": "u32" + } + ] + } + }, + { + "name": "InitTrancheMetadataParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": "string" + }, + { + "name": "junior", + "type": { + "defined": { + "name": "MetadataInput" + } + } + }, + { + "name": "senior", + "type": { + "defined": { + "name": "MetadataInput" + } + } + } + ] + } + }, + { + "name": "JuniorLockEvent", + "docs": [ + "Emitted by `junior_lock`. Captures the deposited USX and minted junior", + "shares so an indexer can track tranche TVL without re-reading state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "assets_to_deposit", + "type": "u64" + }, + { + "name": "shares_minted", + "type": "u64" + } + ] + } + }, + { + "name": "JuniorLockParams", + "docs": ["Args for a junior tranche mint."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. Used as a PDA seed."], + "type": "string" + }, + { + "name": "assets_to_deposit", + "docs": ["Amount of USX (raw token units) the user is locking."], + "type": "u64" + }, + { + "name": "min_shares_out", + "docs": ["Slippage protection: minimum shares to receive."], + "type": "u64" + } + ] + } + }, + { + "name": "JuniorUnlockQueuedEvent", + "docs": [ + "Emitted by `junior_unlock_queued`. `shares_to_unlock` is the delta from", + "this call; `user_total_shares` is the running total in the user's", + "`JuniorUnlockShare` PDA after the update." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_to_unlock", + "type": "u64" + }, + { + "name": "user_total_shares", + "type": "u64" + } + ] + } + }, + { + "name": "JuniorUnlockQueuedParams", + "docs": ["Args for joining (or topping up) a junior settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + }, + { + "name": "shares_to_unlock", + "docs": [ + "Junior tokens to escrow into the batch. Per-call minimum is enforced", + "against `Strategy.junior_minimum_unlock_shares` when configured." + ], + "type": "u64" + } + ] + } + }, + { + "name": "JuniorUnlockShare", + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": ["PDA bump for this account."], + "type": "u8" + }, + { + "name": "batch", + "docs": ["Settlement batch this claim belongs to. Also a PDA seed component."], + "type": "pubkey" + }, + { + "name": "user", + "docs": [ + "Owner of this claim. Also a PDA seed component; only this wallet can", + "withdraw from this position." + ], + "type": "pubkey" + }, + { + "name": "shares", + "docs": [ + "Junior shares the user has contributed to the batch. Monotonically", + "increases via top-ups while the batch is Active; consumed in full when", + "the user withdraws (the account is then closed)." + ], + "type": "u64" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 64] + } + } + ] + } + }, + { + "name": "JuniorWithdrawCancelledEvent", + "docs": [ + "Emitted by `junior_withdraw_cancelled`. Records a junior reclaiming their", + "junior tokens from a cancelled batch and consuming their claim PDA." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_returned", + "type": "u64" + } + ] + } + }, + { + "name": "JuniorWithdrawCancelledParams", + "docs": ["Args for reclaiming junior tokens from a cancelled settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "JuniorWithdrawSettledEvent", + "docs": [ + "Emitted by `junior_withdraw_settled`. Records a junior consuming their", + "claim against a settled batch — `shares_burned` is the share count", + "retired from the batch pool and `assets_withdrawn` is the USX moved out", + "of the batch escrow to the user." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_burned", + "type": "u64" + }, + { + "name": "assets_withdrawn", + "type": "u64" + } + ] + } + }, + { + "name": "JuniorWithdrawSettledParams", + "docs": ["Args for claiming USX from a settled junior settlement batch."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "JuniorWithdrawWinddownEvent", + "docs": [ + "Emitted by `junior_withdraw_winddown`. Records a junior redeeming junior", + "tokens against the post-liquidation pool — `shares_burned` is destroyed", + "and `assets_withdrawn` is moved out of `asset_vault` to the user." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_burned", + "type": "u64" + }, + { + "name": "assets_withdrawn", + "type": "u64" + } + ] + } + }, + { + "name": "JuniorWithdrawWinddownParams", + "docs": ["Args for redeeming junior tokens after the strategy has winded down."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "shares_to_unlock", + "docs": ["Junior shares to burn."], + "type": "u64" + } + ] + } + }, + { + "name": "LiquidateStrategyEvent", + "docs": [ + "Emitted by `liquidate_strategy`. Terminal loss-booking event of the", + "strategy lifecycle; carries a comprehensive pre/post snapshot so an", + "indexer or auditor can reconstruct the full impact without re-reading", + "state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "timestamp", + "type": "u64" + }, + { + "name": "total_loss", + "type": "u64" + }, + { + "name": "junior_loss", + "type": "u64" + }, + { + "name": "senior_loss", + "type": "u64" + }, + { + "name": "total_assets_before", + "type": "u128" + }, + { + "name": "senior_assets_before", + "type": "u128" + }, + { + "name": "loss_vault_balance_before", + "type": "u128" + }, + { + "name": "vesting_vault_balance_before", + "type": "u128" + }, + { + "name": "fee_vault_balance_before", + "type": "u128" + }, + { + "name": "asset_vault_balance_before", + "type": "u64" + }, + { + "name": "junior_shares", + "type": "u128" + }, + { + "name": "senior_shares", + "type": "u128" + }, + { + "name": "total_vesting_assets_before", + "type": "u64" + }, + { + "name": "senior_vesting_assets_before", + "type": "u64" + }, + { + "name": "total_assets_after", + "type": "u128" + }, + { + "name": "senior_assets_after", + "type": "u128" + }, + { + "name": "loss_vault_balance_after", + "type": "u128" + } + ] + } + }, + { + "name": "LiquidateStrategyParams", + "docs": ["Args for booking the terminal liquidation loss."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "total_loss", + "docs": [ + "Total USX (raw token units) moved from `asset_vault` to `loss_vault`.", + "The protocol applies the waterfall internally: junior absorbs first,", + "senior absorbs only the residual once junior is fully wiped." + ], + "type": "u64" + } + ] + } + }, + { + "name": "MetadataInput", + "docs": ["Name/symbol/uri for one tranche mint"], + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "symbol", + "type": "string" + }, + { + "name": "uri", + "type": "string" + } + ] + } + }, + { + "name": "Permission", + "docs": [ + "Single capability. Variant index == bit position in the persisted bitmap,", + "so the enum is the single source of truth for which bit is which.", + "", + "Adding a new permission: append the next variant. NEVER reorder or remove", + "— existing on-chain `Admin.permissions` bitmaps depend on the position." + ], + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "EditStrategy" + }, + { + "name": "SetStrategyStatus" + }, + { + "name": "AddYield" + }, + { + "name": "DistributeYield" + }, + { + "name": "BookLoss" + }, + { + "name": "WithdrawFee" + }, + { + "name": "WithdrawLoss" + }, + { + "name": "PauseStrategy" + }, + { + "name": "PauseProgram" + }, + { + "name": "Liquidate" + }, + { + "name": "InitiateSettlementBatch" + }, + { + "name": "EditSettlementBatch" + }, + { + "name": "ExecuteSettlementBatch" + }, + { + "name": "CancelSettlementBatch" + }, + { + "name": "EditStrategyPrices" + }, + { + "name": "EditStrategyFeeBps" + }, + { + "name": "WithdrawPendingYield" + }, + { + "name": "PauseSettlementBatch" + } + ] + } + }, + { + "name": "PriceFeedMessage", + "type": { + "kind": "struct", + "fields": [ + { + "name": "feed_id", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price", + "type": "i64" + }, + { + "name": "conf", + "type": "u64" + }, + { + "name": "exponent", + "type": "i32" + }, + { + "name": "publish_time", + "type": "i64" + }, + { + "name": "prev_publish_time", + "type": "i64" + }, + { + "name": "ema_price", + "type": "i64" + }, + { + "name": "ema_conf", + "type": "u64" + } + ] + } + }, + { + "name": "PriceUpdateV2", + "type": { + "kind": "struct", + "fields": [ + { + "name": "write_authority", + "type": "pubkey" + }, + { + "name": "verification_level", + "type": { + "defined": { + "name": "VerificationLevel" + } + } + }, + { + "name": "price_message", + "type": { + "defined": { + "name": "PriceFeedMessage" + } + } + }, + { + "name": "posted_slot", + "type": "u64" + } + ] + } + }, + { + "name": "ProposeControllerAuthorityParams", + "docs": [ + "Args for proposing a new controller authority. Two-step rotation:", + "the address set here must call `accept_controller_authority` to take over." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "new_authority", + "docs": ["Address the current authority is proposing to take over."], + "type": "pubkey" + } + ] + } + }, + { + "name": "SeniorLockEvent", + "docs": [ + "Emitted by `senior_lock`. Captures the deposited USX and minted senior", + "shares so an indexer can track tranche TVL without re-reading state." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "assets_to_deposit", + "type": "u64" + }, + { + "name": "shares_minted", + "type": "u64" + } + ] + } + }, + { + "name": "SeniorLockParams", + "docs": ["Args for a senior tranche mint."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. Used as a PDA seed."], + "type": "string" + }, + { + "name": "assets_to_deposit", + "docs": ["Amount of USX (raw token units) the user is locking."], + "type": "u64" + }, + { + "name": "min_shares_out", + "docs": ["Slippage protection: minimum shares to receive."], + "type": "u64" + } + ] + } + }, + { + "name": "SeniorUnlockCooldown", + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "bump", + "docs": ["PDA bump for this account."], + "type": "u8" + }, + { + "name": "vault_bump", + "docs": ["PDA bump for the per-user `senior_cooldown_vault`."], + "type": "u8" + }, + { + "name": "user", + "docs": [ + "Owner of this cooldown. Also a PDA seed component; only this wallet", + "can queue into / withdraw from this cooldown." + ], + "type": "pubkey" + }, + { + "name": "assets_pending", + "docs": [ + "USX the program currently owes this user, accumulated across queue", + "calls. Drains to zero on a successful `senior_withdraw`." + ], + "type": "u64" + }, + { + "name": "earliest_withdraw_at", + "docs": [ + "Earliest time `senior_withdraw` may run. Extended (never advanced) on", + "each new queue: `max(self, now + senior_queue_time_seconds)`." + ], + "type": "u64" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 64] + } + } + ] + } + }, + { + "name": "SeniorUnlockInstantEvent", + "docs": [ + "Emitted by `senior_unlock_instant`. Captures the burned senior shares,", + "the gross USX redemption, the fee routed to the fee vault, and the net", + "USX paid to the user." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_burned", + "type": "u64" + }, + { + "name": "gross_assets", + "type": "u64" + }, + { + "name": "fee_assets", + "type": "u64" + }, + { + "name": "net_assets", + "type": "u64" + } + ] + } + }, + { + "name": "SeniorUnlockInstantParams", + "docs": ["Args for an instant senior unlock."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. Used as a PDA seed."], + "type": "string" + }, + { + "name": "shares_to_unlock", + "docs": ["Senior shares to burn."], + "type": "u64" + }, + { + "name": "min_assets_out", + "docs": ["Slippage protection: minimum net USX (post-fee) the user accepts."], + "type": "u64" + } + ] + } + }, + { + "name": "SeniorUnlockQueuedEvent", + "docs": [ + "Emitted by `senior_unlock_queued`. `assets_added` is the gross USX from", + "this call; `total_assets_pending` is the accumulated balance on the", + "user's cooldown PDA after the update; `earliest_withdraw_at` is the", + "post-update deadline (`max(prior_value, now + senior_queue_time_seconds)`)." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_burned", + "type": "u64" + }, + { + "name": "assets_added", + "type": "u64" + }, + { + "name": "total_assets_pending", + "type": "u64" + }, + { + "name": "earliest_withdraw_at", + "type": "u64" + } + ] + } + }, + { + "name": "SeniorUnlockQueuedParams", + "docs": ["Args for queueing a senior unlock with a cooldown."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. Used as a PDA seed."], + "type": "string" + }, + { + "name": "shares_to_unlock", + "docs": ["Senior shares to burn."], + "type": "u64" + }, + { + "name": "min_assets_out", + "docs": ["Slippage protection: minimum USX the user accepts for this queue."], + "type": "u64" + } + ] + } + }, + { + "name": "SeniorWithdrawEvent", + "docs": [ + "Emitted by `senior_withdraw`. Records the cooldown PDA draining to the", + "user; `assets_pending` resets to 0 on the PDA but the PDA itself stays", + "alive for reuse on subsequent queues." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "assets_withdrawn", + "type": "u64" + } + ] + } + }, + { + "name": "SeniorWithdrawParams", + "docs": [ + "Args for withdrawing senior USX from a cooldown PDA after the cooldown", + "elapsed." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + } + ] + } + }, + { + "name": "SeniorWithdrawWinddownEvent", + "docs": [ + "Emitted by `senior_withdraw_winddown`. Records a senior redeeming senior", + "tokens against the post-liquidation pool — `shares_burned` is destroyed", + "and `assets_withdrawn` is moved out of `asset_vault` to the user." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "user", + "type": "pubkey" + }, + { + "name": "shares_burned", + "type": "u64" + }, + { + "name": "assets_withdrawn", + "type": "u64" + } + ] + } + }, + { + "name": "SeniorWithdrawWinddownParams", + "docs": ["Args for redeeming senior tokens after the strategy has winded down."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "shares_to_unlock", + "docs": ["Senior shares to burn."], + "type": "u64" + } + ] + } + }, + { + "name": "SetBatchPausedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "batch_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "new_is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "SetBatchPausedParams", + "docs": ["Args for toggling the per-batch operational pause flag."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed for the settlement batch lookup."], + "type": "string" + }, + { + "name": "batch_name", + "docs": ["Target settlement batch name. PDA seed."], + "type": "string" + }, + { + "name": "new_is_paused", + "docs": ["Requested pause state. Must differ from the current value."], + "type": "bool" + } + ] + } + }, + { + "name": "SetProgramPausedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "new_is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "SetProgramPausedParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "new_is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "SetStrategyPausedEvent", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "new_is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "SetStrategyPausedParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "new_is_paused", + "type": "bool" + } + ] + } + }, + { + "name": "SetStrategyStatusEvent", + "docs": [ + "Emitted by `set_strategy_status`. Statuses are sent as raw `u8` to match", + "on-chain storage; consumers map them back via `StrategyStatus`." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "previous_status", + "type": "u8" + }, + { + "name": "new_status", + "type": "u8" + } + ] + } + }, + { + "name": "SetStrategyStatusParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "type": "string" + }, + { + "name": "new_status", + "type": { + "defined": { + "name": "StrategyStatus" + } + } + } + ] + } + }, + { + "name": "SettlementBatch", + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Name of the parent strategy. Also a PDA seed component."], + "type": { + "array": ["u8", 32] + } + }, + { + "name": "name", + "docs": [ + "Human-readable batch identifier (e.g. \"MAY-25-01\"), UTF-8 right-padded", + "with zeros. Also a PDA seed component." + ], + "type": { + "array": ["u8", 32] + } + }, + { + "name": "bump", + "docs": ["PDA bump for this batch account."], + "type": "u8" + }, + { + "name": "share_vault_bump", + "docs": ["PDA bump for `share_vault`."], + "type": "u8" + }, + { + "name": "asset_vault_bump", + "docs": ["PDA bump for `asset_vault`."], + "type": "u8" + }, + { + "name": "share_vault", + "docs": [ + "Junior token escrow. Holds shares deposited by joining users until", + "settlement (then burned) or cancellation (then returned)." + ], + "type": "pubkey" + }, + { + "name": "asset_vault", + "docs": ["USX escrow. Empty pre-settlement; receives net redemption USX on settle."], + "type": "pubkey" + }, + { + "name": "status", + "docs": ["Lifecycle phase as a `SettlementBatchStatus`."], + "type": "u8" + }, + { + "name": "is_paused", + "docs": ["Operational pause toggle (1 = paused)."], + "type": "u8" + }, + { + "name": "settlement_time", + "docs": [ + "Earliest time at which settlement is permitted. Editable by ops", + "(postpone). Used to enforce per-user `junior_queue_time` on join." + ], + "type": "u64" + }, + { + "name": "settled_at", + "docs": ["Time at which settlement was applied. Zero until Settled."], + "type": "u64" + }, + { + "name": "total_shares", + "docs": ["Outstanding junior claim against this batch."], + "type": "u64" + }, + { + "name": "total_assets", + "docs": ["USX held in `asset_vault` and claimable by `total_shares`."], + "type": "u64" + }, + { + "name": "realized_loss", + "docs": [ + "Loss booked against this batch at settlement, in USX. Zero pre-settle.", + "Stored for transparency / audit; not used in withdrawal math." + ], + "type": "u64" + }, + { + "name": "max_junior_shares", + "docs": [ + "Cap on `total_shares` for this batch. Joins are rejected once the", + "cumulative junior shares would exceed this; first-come-first-serve.", + "0 disables the check." + ], + "type": "u64" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 120] + } + } + ] + } + }, + { + "name": "Strategy", + "docs": [ + "Per-strategy account. Owns the tranche mints and program vaults, and stores", + "all configuration that gates user instructions. One instance per isolated", + "strategy; multiple strategies coexist under the same program." + ], + "serialization": "bytemuck", + "repr": { + "kind": "c", + "packed": true + }, + "type": { + "kind": "struct", + "fields": [ + { + "name": "name", + "docs": ["Human-readable identifier, UTF-8 bytes right-padded with zeros."], + "type": { + "array": ["u8", 32] + } + }, + { + "name": "bump", + "docs": ["PDA bump for this strategy account."], + "type": "u8" + }, + { + "name": "junior_mint_bump", + "docs": ["PDA bump for `junior_mint`."], + "type": "u8" + }, + { + "name": "senior_mint_bump", + "docs": ["PDA bump for `senior_mint`."], + "type": "u8" + }, + { + "name": "asset_vault_bump", + "docs": ["PDA bump for `asset_vault`."], + "type": "u8" + }, + { + "name": "vesting_vault_bump", + "docs": ["PDA bump for `vesting_vault`."], + "type": "u8" + }, + { + "name": "fee_vault_bump", + "docs": ["PDA bump for `fee_vault`."], + "type": "u8" + }, + { + "name": "loss_vault_bump", + "docs": ["PDA bump for `loss_vault`."], + "type": "u8" + }, + { + "name": "junior_mint", + "docs": ["Junior tranche token mint (first-loss capital)."], + "type": "pubkey" + }, + { + "name": "senior_mint", + "docs": ["Senior tranche token mint (protected capital, paid first)."], + "type": "pubkey" + }, + { + "name": "asset_vault", + "docs": ["Holds the strategy's USX book value backing both tranches."], + "type": "pubkey" + }, + { + "name": "vesting_vault", + "docs": ["Temporarily holds yield USX after `add_yield` for distribution."], + "type": "pubkey" + }, + { + "name": "fee_vault", + "docs": ["Collects fees."], + "type": "pubkey" + }, + { + "name": "loss_vault", + "docs": ["Holds USX moved out of `asset_vault` to account for booked losses."], + "type": "pubkey" + }, + { + "name": "status", + "docs": ["Lifecycle phase as a `StrategyStatus`."], + "type": "u8" + }, + { + "name": "is_paused", + "docs": ["Operational pause toggle (1 = paused)."], + "type": "u8" + }, + { + "name": "pyth_price_feed_id", + "docs": ["Pyth price feed ID for the underlying asset."], + "type": { + "array": ["u8", 32] + } + }, + { + "name": "price_max_age", + "docs": [ + "Maximum age (in seconds) of a Pyth price update before it's considered", + "stale and rejected." + ], + "type": "u64" + }, + { + "name": "price_max_conf_bps", + "docs": [ + "Reject Pyth price updates whose `conf` (confidence interval) exceeds", + "this fraction of `price`, in basis points (1 bps = 0.01%). 0 disables", + "the check." + ], + "type": "u32" + }, + { + "name": "restricted_price", + "docs": [ + "Underlying asset price level at which queued unlocks become restricted,", + "but the strategy is not yet eligible for liquidation. Scaled by", + "`10^[crate::STRATEGY_PRICE_EXPONENT]` (6 decimals; e.g. `1_000_000` = $1.00)." + ], + "type": "u32" + }, + { + "name": "liquidation_price", + "docs": [ + "Underlying asset price level that gates mints and withdrawals and", + "unlocks the liquidation path. Scaled by", + "`10^[crate::STRATEGY_PRICE_EXPONENT]` (6 decimals; e.g. `500_000` = $0.50)." + ], + "type": "u32" + }, + { + "name": "yield_vesting_period_seconds", + "docs": [ + "Duration over which newly distributed yield vests linearly into the", + "tranches." + ], + "type": "u32" + }, + { + "name": "junior_queue_time_seconds", + "docs": [ + "Minimum time a junior unlock request must sit in a settlement batch", + "before the batch can settle." + ], + "type": "u32" + }, + { + "name": "senior_queue_time_seconds", + "docs": [ + "Cooldown duration for a queued senior unlock before withdrawal is", + "permitted." + ], + "type": "u32" + }, + { + "name": "junior_share_cap", + "docs": [ + "Maximum total junior tranche shares outstanding. Mints that would", + "exceed this are rejected." + ], + "type": "u128" + }, + { + "name": "senior_share_cap", + "docs": [ + "Maximum total senior tranche shares outstanding. Mints that would", + "exceed this are rejected." + ], + "type": "u128" + }, + { + "name": "junior_minimum_unlock_shares", + "docs": [ + "Minimum share amount for a single junior unlock; 0 disables the check.", + "Reduces operational overhead from dust-sized unlocks." + ], + "type": "u64" + }, + { + "name": "senior_minimum_unlock_shares", + "docs": ["Minimum share amount for a single senior unlock; 0 disables the check."], + "type": "u64" + }, + { + "name": "senior_instant_unlock_fee_bps", + "docs": [ + "Fee charged on senior instant unlock, in basis points (1 bps = 0.01%).", + "Routed to `fee_vault`. Capped at", + "[`crate::MAX_SENIOR_INSTANT_UNLOCK_FEE_BPS`] (< 100%) so the instant", + "unlock always leaves a non-zero net for non-dust redemptions." + ], + "type": "u32" + }, + { + "name": "max_settlement_loss_bps", + "docs": [ + "Operational sanity check against admin decimal-point errors at", + "settlement, not a user-facing slippage cap. Bps of the settlement's", + "gross junior redemption (1 bps = 0.01%). `0` means uncapped (up to", + "100% loss permitted), NOT \"0% loss permitted\"." + ], + "type": "u32" + }, + { + "name": "_reserved", + "docs": ["Reserved space for future fields."], + "type": { + "array": ["u8", 256] + } + } + ] + } + }, + { + "name": "StrategyStatus", + "docs": [ + "Lifecycle phase of the strategy. Independent of `is_paused`, which is an", + "operational toggle that can apply within any lifecycle phase." + ], + "repr": { + "kind": "rust" + }, + "type": { + "kind": "enum", + "variants": [ + { + "name": "Active" + }, + { + "name": "Liquidating" + }, + { + "name": "WindedDown" + } + ] + } + }, + { + "name": "TrancheMetadataEvent", + "docs": [ + "Emitted by `init_tranche_metadata` (both tranches) and", + "`update_tranche_metadata` (the `Some` tranches). Values live in the Metaplex", + "accounts and the instruction data." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "is_init", + "type": "bool" + }, + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "authority", + "type": "pubkey" + }, + { + "name": "junior_updated", + "type": "bool" + }, + { + "name": "senior_updated", + "type": "bool" + } + ] + } + }, + { + "name": "UpdateTrancheMetadataParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": "string" + }, + { + "name": "junior", + "type": { + "option": { + "defined": { + "name": "MetadataInput" + } + } + } + }, + { + "name": "senior", + "type": { + "option": { + "defined": { + "name": "MetadataInput" + } + } + } + } + ] + } + }, + { + "name": "VerificationLevel", + "docs": [ + "* This enum represents how many guardian signatures were checked for a Pythnet price update\n * If full, guardian quorum has been attained\n * If partial, at least config.minimum signatures have been verified, but in the case config.minimum_signatures changes in the future we also include the number of signatures that were checked" + ], + "type": { + "kind": "enum", + "variants": [ + { + "name": "Partial", + "fields": [ + { + "name": "num_signatures", + "type": "u8" + } + ] + }, + { + "name": "Full" + } + ] + } + }, + { + "name": "WithdrawFeeEvent", + "docs": [ + "Emitted by `withdraw_fee`. Records USX moved out of `fee_vault` to the", + "admin's destination account. Routed via the strategy PDA, which is the", + "SPL authority of the source vault." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "assets_to_withdraw", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawFeeParams", + "docs": ["Args for moving accumulated fees out of the strategy's `fee_vault`."], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "assets_to_withdraw", + "docs": [ + "Amount of USX (raw token units) to move from `fee_vault` to the", + "admin's destination account." + ], + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawLossEvent", + "docs": [ + "Emitted by `withdraw_loss`. Records USX moved out of `loss_vault` to the", + "admin's destination account. Routed via the strategy PDA, which is the", + "SPL authority of the source vault." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "assets_to_withdraw", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawLossParams", + "docs": [ + "Args for moving booked losses out of the strategy's `loss_vault`. Drains", + "flow to the multisig so the underlying USX can be redeemed against the", + "realized STRC loss off-chain." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "assets_to_withdraw", + "docs": [ + "Amount of USX (raw token units) to move from `loss_vault` to the", + "admin's destination account." + ], + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawPendingYieldEvent", + "docs": [ + "Emitted by `withdraw_pending_yield`. Records USX moved out of `vesting_vault`", + "(pre-distribution operator top-up) back to the admin's destination account.", + "Inverse of `add_yield`. Routed via the strategy PDA, which is the SPL", + "authority of the source vault." + ], + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "type": { + "array": ["u8", 32] + } + }, + { + "name": "signer", + "type": "pubkey" + }, + { + "name": "assets_to_withdraw", + "type": "u64" + } + ] + } + }, + { + "name": "WithdrawPendingYieldParams", + "type": { + "kind": "struct", + "fields": [ + { + "name": "strategy_name", + "docs": ["Parent strategy name. PDA seed."], + "type": "string" + }, + { + "name": "assets_to_withdraw", + "docs": [ + "Amount of USX (raw token units) to move from `vesting_vault` to the", + "admin's destination account." + ], + "type": "u64" + } + ] + } + } + ], + "constants": [ + { + "name": "ACCOUNTING_NAMESPACE", + "type": "bytes", + "value": "[65, 67, 67, 79, 85, 78, 84, 73, 78, 71]" + }, + { + "name": "ADMIN_NAMESPACE", + "type": "bytes", + "value": "[65, 68, 77, 73, 78]" + }, + { + "name": "ASSET_MINT_DECIMALS", + "type": "u8", + "value": "6" + }, + { + "name": "ASSET_VAULT_NAMESPACE", + "type": "bytes", + "value": "[65, 83, 83, 69, 84, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "BATCH_ASSET_VAULT_NAMESPACE", + "type": "bytes", + "value": "[66, 65, 84, 67, 72, 95, 65, 83, 83, 69, 84, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "BATCH_NAME_SEPARATOR", + "docs": [ + "Separator seed inserted between `strategy_name` and `batch_name` in", + "batch-family PDAs (settlement_batch, batch_share_vault, batch_asset_vault).", + "Prevents seed-concatenation collisions between distinct (strategy_name,", + "batch_name) pairs. Uses NULL — already excluded by the `is_ascii_graphic`", + "validation on stored names — so the demarcation is unambiguous." + ], + "type": "bytes", + "value": "[0]" + }, + { + "name": "BATCH_SHARE_VAULT_NAMESPACE", + "type": "bytes", + "value": "[66, 65, 84, 67, 72, 95, 83, 72, 65, 82, 69, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "CONTROLLER_NAMESPACE", + "type": "bytes", + "value": "[67, 79, 78, 84, 82, 79, 76, 76, 69, 82]" + }, + { + "name": "FEE_VAULT_NAMESPACE", + "type": "bytes", + "value": "[70, 69, 69, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "JUNIOR_DEAD_SHARE_VAULT_NAMESPACE", + "type": "bytes", + "value": "[74, 85, 78, 73, 79, 82, 95, 68, 69, 65, 68, 95, 83, 72, 65, 82, 69, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "JUNIOR_MINT_NAMESPACE", + "type": "bytes", + "value": "[74, 85, 78, 73, 79, 82, 95, 77, 73, 78, 84]" + }, + { + "name": "JUNIOR_UNLOCK_SHARE_NAMESPACE", + "type": "bytes", + "value": "[74, 85, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 83, 72, 65, 82, 69]" + }, + { + "name": "LOSS_VAULT_NAMESPACE", + "type": "bytes", + "value": "[76, 79, 83, 83, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "MAX_COOLDOWN_SECONDS", + "docs": [ + "Ceiling on user-facing queue/cooldown windows (`senior_queue_time_seconds`,", + "`junior_queue_time_seconds`). Caps the worst-case admin decimal-point error." + ], + "type": "u32", + "value": "7776000" + }, + { + "name": "MAX_PRICE_MAX_AGE", + "docs": [ + "Ceiling on `Strategy.price_max_age`. Catches admin decimal-point errors;", + "production max-age values are seconds-to-minutes." + ], + "type": "u64", + "value": "86400" + }, + { + "name": "MAX_SENIOR_INSTANT_UNLOCK_FEE_BPS", + "docs": [ + "Ceiling on `Strategy.senior_instant_unlock_fee_bps`. Kept well below 100%", + "(10_000 bps): at exactly 100% the fee equals the gross redemption for every", + "amount, so `net == 0` always trips the `ZeroAssetsUnlock` guard in", + "`senior_unlock_instant` — the path looks validly configured but is silently", + "disabled. 20% sits above any realistic exit penalty while keeping the path", + "live for all non-dust redemptions." + ], + "type": "u32", + "value": "2000" + }, + { + "name": "MAX_VESTING_OVERLAP_SECONDS", + "docs": [ + "Tolerance for back-to-back `distribute_yield` calls: a new vesting epoch", + "may start up to this many seconds before the previous one ends. Keeps", + "scheduled ops resilient to network/queue jitter without letting an admin", + "instantly graduate the previous unvested tail.", + "", + "`Strategy.yield_vesting_period_seconds` must exceed this so the", + "\"no-overlap\" guard inside `distribute_yield` cannot be sidestepped via", + "`saturating_sub`." + ], + "type": "u64", + "value": "300" + }, + { + "name": "MAX_VESTING_PERIOD_SECONDS", + "docs": [ + "Ceiling on `Strategy.yield_vesting_period_seconds`. Mirrors the cooldown", + "cap; longer than any realistic yield cadence." + ], + "type": "u32", + "value": "7776000" + }, + { + "name": "MIN_TRANCHE_SHARES", + "docs": [ + "Floor on tranche share supply enforced on every redemption. Reaching", + "zero is rejected — the operational pattern is to seed each tranche", + "with at least this many shares at bootstrap." + ], + "type": "u128", + "value": "1000" + }, + { + "name": "SENIOR_COOLDOWN_VAULT_NAMESPACE", + "type": "bytes", + "value": "[83, 69, 78, 73, 79, 82, 95, 67, 79, 79, 76, 68, 79, 87, 78, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "SENIOR_DEAD_SHARE_VAULT_NAMESPACE", + "type": "bytes", + "value": "[83, 69, 78, 73, 79, 82, 95, 68, 69, 65, 68, 95, 83, 72, 65, 82, 69, 95, 86, 65, 85, 76, 84]" + }, + { + "name": "SENIOR_MINT_NAMESPACE", + "type": "bytes", + "value": "[83, 69, 78, 73, 79, 82, 95, 77, 73, 78, 84]" + }, + { + "name": "SENIOR_UNLOCK_COOLDOWN_NAMESPACE", + "type": "bytes", + "value": "[83, 69, 78, 73, 79, 82, 95, 85, 78, 76, 79, 67, 75, 95, 67, 79, 79, 76, 68, 79, 87, 78]" + }, + { + "name": "SETTLEMENT_BATCH_NAMESPACE", + "type": "bytes", + "value": "[83, 69, 84, 84, 76, 69, 77, 69, 78, 84, 95, 66, 65, 84, 67, 72]" + }, + { + "name": "SHARE_MINT_DECIMALS", + "type": "u8", + "value": "6" + }, + { + "name": "STRATEGY_NAMESPACE", + "type": "bytes", + "value": "[83, 84, 82, 65, 84, 69, 71, 89]" + }, + { + "name": "STRATEGY_PRICE_EXPONENT", + "docs": [ + "log10 exponent for `u32` price fields on `Strategy` (`restricted_price`,", + "`liquidation_price`). A stored value of `1_234_567` represents", + "`1_234_567 × 10^STRATEGY_PRICE_EXPONENT = 1.234567` USD, i.e. 6 decimals." + ], + "type": "i32", + "value": "-6" + }, + { + "name": "STRATEGY_PRICE_MIN", + "docs": [ + "Minimum permitted value for `restricted_price` / `liquidation_price` on", + "`Strategy`. Catches decimal-place mistakes — e.g. submitting `50` thinking", + "\"$50\" when the scale would actually represent `$0.00005`. At exponent = `-6`,", + "this floor is `$0.001`." + ], + "type": "u32", + "value": "1000" + }, + { + "name": "VESTING_VAULT_NAMESPACE", + "type": "bytes", + "value": "[86, 69, 83, 84, 73, 78, 71, 95, 86, 65, 85, 76, 84]" + } + ] +} diff --git a/packages/sources/solana-functions/src/index.ts b/packages/sources/solana-functions/src/index.ts index 13a693a8d5e..2aa353987e6 100644 --- a/packages/sources/solana-functions/src/index.ts +++ b/packages/sources/solana-functions/src/index.ts @@ -8,13 +8,24 @@ import { extension, poolTokenRate, sanctumInfinity, + strcusxExchangeRate, + stslxExchangeRate, } from './endpoint' export const adapter = new Adapter({ defaultEndpoint: eusxPrice.name, name: 'SOLANA_FUNCTIONS', config, - endpoints: [eusxPrice, anchorData, sanctumInfinity, bufferLayout, extension, poolTokenRate], + endpoints: [ + eusxPrice, + anchorData, + sanctumInfinity, + bufferLayout, + extension, + poolTokenRate, + stslxExchangeRate, + strcusxExchangeRate, + ], }) export const server = (): Promise => expose(adapter) diff --git a/packages/sources/solana-functions/src/shared/account-reader.ts b/packages/sources/solana-functions/src/shared/account-reader.ts index b04702901be..05d07a57267 100644 --- a/packages/sources/solana-functions/src/shared/account-reader.ts +++ b/packages/sources/solana-functions/src/shared/account-reader.ts @@ -1,22 +1,29 @@ -import { BorshCoder, Idl } from '@coral-xyz/anchor' -import { getProgramDerivedAddress, type Address } from '@solana/addresses' +import { BorshCoder, type BorshAccountsCoder, type Idl } from '@coral-xyz/anchor' +import { type Address } from '@solana/addresses' import { type Rpc, type SolanaRpcApi } from '@solana/rpc' +import { derivePda, type PdaSeed } from './solana-account-utils' + +export type Stringable = { toString(): string } + +export const toBigint = (value: Stringable) => BigInt(value.toString()) + +export const decodeAnchorAccount = ( + coder: BorshAccountsCoder, + accountName: string, + data: Buffer, +) => coder.decode(accountName, data) as T export class SolanaAccountReader { // Fetch account information by deriving an address given a program address and a list of seeds // accountName must match the IDL exactly - // seeds typed as any due to type not being exported by @solana/addresses async fetchAccountInformationByAddressAndSeeds( rpc: Rpc, programAddress: Address, - seeds: any[], + seeds: PdaSeed[], accountName: string, idl: Idl, ): Promise { - const [pda] = await getProgramDerivedAddress({ - programAddress, - seeds, - }) + const pda = await derivePda(programAddress, seeds) return this.fetchAccountInformation(rpc, pda, accountName, idl) } @@ -36,7 +43,7 @@ export class SolanaAccountReader { } const dataEncoded = value.data[0] as string const data = Buffer.from(dataEncoded, encoding) - const coder = new BorshCoder(idl as unknown as Idl) - return coder.accounts.decode(accountName, data) as unknown as T + const coder = new BorshCoder(idl) + return decodeAnchorAccount(coder.accounts, accountName, data) } } diff --git a/packages/sources/solana-functions/src/shared/buffer-layout-accounts.ts b/packages/sources/solana-functions/src/shared/buffer-layout-accounts.ts index e6bc8393539..79dc86a1259 100644 --- a/packages/sources/solana-functions/src/shared/buffer-layout-accounts.ts +++ b/packages/sources/solana-functions/src/shared/buffer-layout-accounts.ts @@ -3,7 +3,39 @@ import { type Address } from '@solana/addresses' import * as BufferLayout from '@solana/buffer-layout' import { type Rpc, type SolanaRpcApi } from '@solana/rpc' import { StakePoolLayout } from '@solana/spl-stake-pool' -import { AccountLayout, MintLayout } from '@solana/spl-token' +import { + AccountLayout, + MintLayout, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { assertDataLength, assertOwnerProgram, type AccountInfo } from './solana-account-utils' + +export const LEGACY_TOKEN_PROGRAM_ADDRESS = TOKEN_PROGRAM_ID.toBase58() +export const TOKEN_2022_PROGRAM_ADDRESS = TOKEN_2022_PROGRAM_ID.toBase58() +export const TOKEN_PROGRAM_ADDRESSES = [LEGACY_TOKEN_PROGRAM_ADDRESS, TOKEN_2022_PROGRAM_ADDRESS] + +export type MintInfo = { + supply: bigint + decimals: number +} + +export type TokenAccountInfo = { + mintAddress: string + ownerAddress: string + amount: bigint +} + +type DecodedMint = { + supply: bigint + decimals: number +} + +type DecodedTokenAccount = { + mint: { toString(): string } + owner: { toString(): string } + amount: bigint +} interface SanctumPoolState { total_sol_value: bigint @@ -35,7 +67,7 @@ const SanctumPoolStateLayout = BufferLayout.struct([ BufferLayout.blob(32, 'lp_token_mint'), ]) -const solanaTokenProgramAddress = 'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA' +const solanaTokenProgramAddress = LEGACY_TOKEN_PROGRAM_ADDRESS const solanaStakePoolProgramAddress = 'SPoo1Ku8WFXoNDMHPsrGSTSG1Y47rzgn41SLUNakuHy' const sanctumControllerProgramAddress = '5ocnV1qiCgaQR8Jb8xWnVbApfaygJ8tNoZfgPwsgx9kx' @@ -45,6 +77,33 @@ const programToBufferLayoutMap: Record[]> = [sanctumControllerProgramAddress]: [SanctumPoolStateLayout], } +export const assertTokenProgramOwner = ( + accountInfo: AccountInfo | null | undefined, + description: string, +) => + assertOwnerProgram(accountInfo, description, TOKEN_PROGRAM_ADDRESSES, 'a supported token program') + +export const decodeMintInfo = (data: Buffer, description: string): MintInfo => { + assertDataLength(data, description, MintLayout.span) + const decoded = MintLayout.decode(data) as DecodedMint + + return { + supply: decoded.supply, + decimals: decoded.decimals, + } +} + +export const decodeTokenAccountInfo = (data: Buffer, description: string): TokenAccountInfo => { + assertDataLength(data, description, AccountLayout.span) + const decoded = AccountLayout.decode(data) as DecodedTokenAccount + + return { + mintAddress: decoded.mint.toString(), + ownerAddress: decoded.owner.toString(), + amount: decoded.amount, + } +} + const getLayout = (programAddress: string, dataLength: number): BufferLayout.Layout => { const layoutCandidates = programToBufferLayoutMap[programAddress] if (!layoutCandidates) { diff --git a/packages/sources/solana-functions/src/shared/exchange-rate-utils.ts b/packages/sources/solana-functions/src/shared/exchange-rate-utils.ts new file mode 100644 index 00000000000..ca1c42b2737 --- /dev/null +++ b/packages/sources/solana-functions/src/shared/exchange-rate-utils.ts @@ -0,0 +1,82 @@ +import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' + +export const RESULT_DECIMALS = 18 +const POSITIVE_INTEGER_PATTERN = /^[1-9]\d*$/ + +export const validateRateBound = (value: string, name: string) => { + if (!POSITIVE_INTEGER_PATTERN.test(value)) { + throw new AdapterInputError({ + message: `${name} must be a positive base-10 integer string`, + statusCode: 400, + }) + } +} + +export const toRateBounds = (minRateValue?: string, maxRateValue?: string) => ({ + minRate: minRateValue === undefined ? undefined : BigInt(minRateValue), + maxRate: maxRateValue === undefined ? undefined : BigInt(maxRateValue), +}) + +export const validateRateBounds = (minRateValue?: string, maxRateValue?: string) => { + if (minRateValue !== undefined) { + validateRateBound(minRateValue, 'minRate') + } + if (maxRateValue !== undefined) { + validateRateBound(maxRateValue, 'maxRate') + } + + const { minRate, maxRate } = toRateBounds(minRateValue, maxRateValue) + if (minRate !== undefined && maxRate !== undefined && minRate > maxRate) { + throw new AdapterInputError({ + message: 'minRate must be less than or equal to maxRate', + statusCode: 400, + }) + } +} + +export const applyRateBounds = (computedRate: bigint, minRate?: bigint, maxRate?: bigint) => { + let rate = computedRate + if (minRate !== undefined && rate < minRate) { + rate = minRate + } + if (maxRate !== undefined && rate > maxRate) { + rate = maxRate + } + + return { + rate, + boundsApplied: rate !== computedRate, + } +} + +export const calculateNormalizedRate = ( + assets: bigint, + shares: bigint, + assetDecimals: number, + shareDecimals: number, +) => { + if (shares === 0n) { + return null + } + + return ( + (assets * 10n ** BigInt(RESULT_DECIMALS + shareDecimals)) / + (shares * 10n ** BigInt(assetDecimals)) + ) +} + +export const calculateUnvestedAssets = ( + assets: bigint, + unixTimestamp: bigint, + vestingStartTime: bigint, + vestingEndTime: bigint, +) => { + if (assets === 0n || vestingEndTime <= vestingStartTime || unixTimestamp >= vestingEndTime) { + return 0n + } + if (unixTimestamp <= vestingStartTime) { + return assets + } + + return (assets * (vestingEndTime - unixTimestamp)) / (vestingEndTime - vestingStartTime) +} diff --git a/packages/sources/solana-functions/src/shared/solana-account-utils.ts b/packages/sources/solana-functions/src/shared/solana-account-utils.ts new file mode 100644 index 00000000000..1fce43c02cd --- /dev/null +++ b/packages/sources/solana-functions/src/shared/solana-account-utils.ts @@ -0,0 +1,114 @@ +import { + AdapterDataProviderError, + AdapterInputError, +} from '@chainlink/external-adapter-framework/validation/error' +import { address, getProgramDerivedAddress } from '@solana/addresses' +import { type Rpc, type SolanaRpcApi } from '@solana/rpc' +import { getSysvarClockDecoder, SYSVAR_CLOCK_ADDRESS } from '@solana/sysvars' + +export const CLOCK_SYSVAR_ADDRESS = SYSVAR_CLOCK_ADDRESS +const clockDecoder = getSysvarClockDecoder() + +type MultipleAccountsResponse = Awaited< + ReturnType['getMultipleAccounts']>['send']> +> + +export type AccountInfo = NonNullable[number] + +export type PdaSeed = Parameters[0]['seeds'][number] + +export const providerError = (message: string) => + new AdapterDataProviderError( + { + message, + statusCode: 502, + }, + { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + ) + +export const parseSolanaAddress = (value: string, name: string) => { + try { + return address(value) + } catch { + throw new AdapterInputError({ + message: `${name} must be a valid Solana address`, + statusCode: 400, + }) + } +} + +export const derivePda = async (programAddress: string, seeds: PdaSeed[]) => { + const [pda] = await getProgramDerivedAddress({ + programAddress: parseSolanaAddress(programAddress, 'programAddress'), + seeds, + }) + + return pda +} + +export const getAccountDataBuffer = ( + accountInfo: AccountInfo | null | undefined, + description: string, +) => { + const encodedData = accountInfo?.data?.[0] + if (typeof encodedData !== 'string' || encodedData.length === 0) { + throw providerError(`No account data found for ${description}`) + } + + return Buffer.from(encodedData, 'base64') +} + +export const assertOwnerProgram = ( + accountInfo: AccountInfo | null | undefined, + description: string, + expectedOwners: string[], + ownerDescription: string, +) => { + const owner = accountInfo?.owner?.toString() + if (!owner || !expectedOwners.includes(owner)) { + throw providerError( + `Expected ${description} to be owned by ${ownerDescription} [${expectedOwners.join( + ', ', + )}], found '${owner}'`, + ) + } +} + +export const assertDataLength = (data: Buffer, description: string, minLength: number) => { + if (data.length < minLength) { + throw providerError( + `Expected ${description} account data to be at least ${minLength} bytes, found ${data.length}`, + ) + } +} + +export const decodeClockUnixTimestamp = (accountInfo: AccountInfo | null | undefined) => { + const data = getAccountDataBuffer(accountInfo, `Clock sysvar '${CLOCK_SYSVAR_ADDRESS}'`) + + return clockDecoder.decode(data).unixTimestamp +} + +export const fetchMultipleAccounts = async (rpc: Rpc, addresses: string[]) => { + const encoding = 'base64' as const + const validatedAddresses = addresses.map((accountAddress) => + parseSolanaAddress(accountAddress, 'address'), + ) + let resp: MultipleAccountsResponse + try { + resp = await rpc.getMultipleAccounts(validatedAddresses, { encoding }).send() + } catch (e: unknown) { + throw providerError(e instanceof Error ? e.message : 'Failed to fetch Solana accounts') + } + + if (!resp.value || resp.value.length !== addresses.length) { + throw providerError( + `Expected ${addresses.length} account responses, received ${resp.value?.length ?? 0}`, + ) + } + + return resp.value +} diff --git a/packages/sources/solana-functions/src/transport/strcusx-accounts.ts b/packages/sources/solana-functions/src/transport/strcusx-accounts.ts new file mode 100644 index 00000000000..31e85c56a90 --- /dev/null +++ b/packages/sources/solana-functions/src/transport/strcusx-accounts.ts @@ -0,0 +1,136 @@ +import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { BorshAccountsCoder, type Idl } from '@coral-xyz/anchor' +import * as StrcusxYieldStrategyIDL from '../idl/strcusx_yield_strategy.json' +import { decodeAnchorAccount, toBigint, type Stringable } from '../shared/account-reader' +import { calculateUnvestedAssets } from '../shared/exchange-rate-utils' +import { derivePda, providerError, type PdaSeed } from '../shared/solana-account-utils' + +const STRATEGY_NAME_LENGTH = 32 +export const TRANCHES = ['junior', 'senior'] as const +export type Tranche = (typeof TRANCHES)[number] + +export const PDA_SEEDS = { + CONTROLLER: 'CONTROLLER', + STRATEGY: 'STRATEGY', + ACCOUNTING: 'ACCOUNTING', +} as const + +const strcusxAccountsCoder = new BorshAccountsCoder(StrcusxYieldStrategyIDL as Idl) + +type DecodedAccountingState = { + senior_shares: Stringable + junior_shares: Stringable + total_assets: Stringable + senior_assets: Stringable + total_vesting_assets: Stringable + senior_vesting_assets: Stringable + vesting_start_time: Stringable + vesting_end_time: Stringable +} + +type AccountingState = { + seniorShares: bigint + juniorShares: bigint + totalAssets: bigint + seniorAssets: bigint + totalVestingAssets: bigint + seniorVestingAssets: bigint + vestingStartTime: bigint + vestingEndTime: bigint +} + +export const parseStrategyName = (value: string) => { + const byteLength = Buffer.byteLength(value) + if (byteLength === 0 || byteLength > STRATEGY_NAME_LENGTH) { + throw new AdapterInputError({ + message: `strategyName must be 1-${STRATEGY_NAME_LENGTH} UTF-8 bytes`, + statusCode: 400, + }) + } + + return value +} + +export const deriveAccountAddress = (programAddress: string, seeds: PdaSeed[]) => + derivePda(programAddress, seeds).then((pda) => pda.toString()) + +export const decodeControllerState = (data: Buffer) => { + const decoded = decodeAnchorAccount<{ asset_mint: Stringable }>( + strcusxAccountsCoder, + 'Controller', + data, + ) + + return { + assetMintAddress: decoded.asset_mint.toString(), + } +} + +export const decodeStrategyState = (data: Buffer) => { + const decoded = decodeAnchorAccount<{ junior_mint: Stringable; senior_mint: Stringable }>( + strcusxAccountsCoder, + 'Strategy', + data, + ) + + return { + juniorMintAddress: decoded.junior_mint.toString(), + seniorMintAddress: decoded.senior_mint.toString(), + } +} + +export const decodeAccountingState = (data: Buffer): AccountingState => { + const decoded = decodeAnchorAccount( + strcusxAccountsCoder, + 'AccountingState', + data, + ) + + return { + seniorShares: toBigint(decoded.senior_shares), + juniorShares: toBigint(decoded.junior_shares), + totalAssets: toBigint(decoded.total_assets), + seniorAssets: toBigint(decoded.senior_assets), + totalVestingAssets: toBigint(decoded.total_vesting_assets), + seniorVestingAssets: toBigint(decoded.senior_vesting_assets), + vestingStartTime: toBigint(decoded.vesting_start_time), + vestingEndTime: toBigint(decoded.vesting_end_time), + } +} + +export const calculateBookValueAssets = (accounting: AccountingState, unixTimestamp: bigint) => { + if (accounting.seniorVestingAssets > accounting.totalVestingAssets) { + throw providerError( + 'AccountingState seniorVestingAssets must be less than or equal to totalVestingAssets', + ) + } + + const unvestedTotalVestingAssets = calculateUnvestedAssets( + accounting.totalVestingAssets, + unixTimestamp, + accounting.vestingStartTime, + accounting.vestingEndTime, + ) + const unvestedSeniorVestingAssets = calculateUnvestedAssets( + accounting.seniorVestingAssets, + unixTimestamp, + accounting.vestingStartTime, + accounting.vestingEndTime, + ) + + if (accounting.totalAssets < unvestedTotalVestingAssets) { + throw providerError( + 'AccountingState totalAssets must be greater than or equal to unvested totalVestingAssets', + ) + } + if (accounting.seniorAssets < unvestedSeniorVestingAssets) { + throw providerError( + 'AccountingState seniorAssets must be greater than or equal to unvested seniorVestingAssets', + ) + } + + return { + totalAssets: accounting.totalAssets - unvestedTotalVestingAssets, + seniorAssets: accounting.seniorAssets - unvestedSeniorVestingAssets, + } +} diff --git a/packages/sources/solana-functions/src/transport/strcusx-exchange-rate.ts b/packages/sources/solana-functions/src/transport/strcusx-exchange-rate.ts new file mode 100644 index 00000000000..74181cef6c1 --- /dev/null +++ b/packages/sources/solana-functions/src/transport/strcusx-exchange-rate.ts @@ -0,0 +1,198 @@ +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { type Rpc, type SolanaRpcApi } from '@solana/rpc' +import { BaseEndpointTypes, inputParameters } from '../endpoint/strcusx-exchange-rate' +import { decodeMintInfo } from '../shared/buffer-layout-accounts' +import { + applyRateBounds, + calculateNormalizedRate, + RESULT_DECIMALS, + toRateBounds, +} from '../shared/exchange-rate-utils' +import { + CLOCK_SYSVAR_ADDRESS, + decodeClockUnixTimestamp, + fetchMultipleAccounts, + getAccountDataBuffer, + parseSolanaAddress, + providerError, +} from '../shared/solana-account-utils' +import { SolanaRpcFactory } from '../shared/solana-rpc-factory' +import { + calculateBookValueAssets, + decodeAccountingState, + decodeControllerState, + decodeStrategyState, + deriveAccountAddress, + parseStrategyName, + PDA_SEEDS, + TRANCHES, +} from './strcusx-accounts' + +const logger = makeLogger('StrcusxExchangeRateTransport') +const JUNIOR_TRANCHE = TRANCHES[0] + +type RequestParams = typeof inputParameters.validated + +export class StrcusxExchangeRateTransport extends SubscriptionTransport { + rpc!: Rpc + + async initialize( + dependencies: TransportDependencies, + adapterSettings: BaseEndpointTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.rpc = new SolanaRpcFactory().create(adapterSettings.RPC_URL) + } + + async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { + await Promise.all(entries.map(async (param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(param: RequestParams) { + let response: AdapterResponse + try { + response = await this._handleRequest(param) + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + response = { + statusCode: (e as AdapterInputError)?.statusCode || 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest( + params: RequestParams, + ): Promise> { + const providerDataRequestedUnixMs = Date.now() + const programAddress = parseSolanaAddress(params.programAddress, 'programAddress').toString() + const strategyName = parseStrategyName(params.strategyName) + const tranche = params.tranche + const { minRate, maxRate } = toRateBounds(params.minRate, params.maxRate) + + const [controllerAddress, strategyAddress, accountingAddress] = await Promise.all([ + deriveAccountAddress(programAddress, [PDA_SEEDS.CONTROLLER]), + deriveAccountAddress(programAddress, [PDA_SEEDS.STRATEGY, strategyName]), + deriveAccountAddress(programAddress, [PDA_SEEDS.ACCOUNTING, strategyName]), + ]) + + const [controllerAccount, strategyAccount, accountingAccount, clockAccount] = + await fetchMultipleAccounts(this.rpc, [ + controllerAddress, + strategyAddress, + accountingAddress, + CLOCK_SYSVAR_ADDRESS, + ]) + + const controller = decodeControllerState( + getAccountDataBuffer(controllerAccount, `Controller account '${controllerAddress}'`), + ) + const strategy = decodeStrategyState( + getAccountDataBuffer(strategyAccount, `Strategy account '${strategyAddress}'`), + ) + const accounting = decodeAccountingState( + getAccountDataBuffer(accountingAccount, `Accounting account '${accountingAddress}'`), + ) + + const trancheMintAddress = + tranche === JUNIOR_TRANCHE ? strategy.juniorMintAddress : strategy.seniorMintAddress + const [assetMintAccount, trancheMintAccount] = await fetchMultipleAccounts(this.rpc, [ + controller.assetMintAddress, + trancheMintAddress, + ]) + + const assetMint = decodeMintInfo( + getAccountDataBuffer(assetMintAccount, `asset mint '${controller.assetMintAddress}'`), + `asset mint '${controller.assetMintAddress}'`, + ) + const trancheMint = decodeMintInfo( + getAccountDataBuffer(trancheMintAccount, `${tranche} mint '${trancheMintAddress}'`), + `${tranche} mint '${trancheMintAddress}'`, + ) + + const clockUnixTimestamp = decodeClockUnixTimestamp(clockAccount) + + // Accounting totals include vesting assets; remove unvested amounts using Solana Clock time. + const bookValueAssets = calculateBookValueAssets(accounting, clockUnixTimestamp) + + if (bookValueAssets.totalAssets < bookValueAssets.seniorAssets) { + throw providerError( + `AccountingState vested totalAssets must be greater than or equal to vested seniorAssets`, + ) + } + + const juniorAssets = bookValueAssets.totalAssets - bookValueAssets.seniorAssets + + // Junior gets residual vested assets; senior gets vested senior assets. + // Rate = assets / shares, normalized by mint decimals. + const trancheAssets = tranche === JUNIOR_TRANCHE ? juniorAssets : bookValueAssets.seniorAssets + const trancheShares = + tranche === JUNIOR_TRANCHE ? accounting.juniorShares : accounting.seniorShares + const selectedComputedRate = calculateNormalizedRate( + trancheAssets, + trancheShares, + assetMint.decimals, + trancheMint.decimals, + ) + + if (selectedComputedRate === null) { + throw providerError(`${tranche} tranche shares are zero`) + } + + const { rate, boundsApplied } = applyRateBounds(selectedComputedRate, minRate, maxRate) + const result = rate.toString() + const computedResult = selectedComputedRate.toString() + if (boundsApplied) { + logger.warn( + { + tranche, + computedResult, + result, + minRate: minRate?.toString(), + maxRate: maxRate?.toString(), + }, + 'strcUSX exchange rate bounds applied', + ) + } + + return { + data: { + result, + computedResult, + tranche, + decimals: RESULT_DECIMALS, + boundsApplied, + trancheAssets: trancheAssets.toString(), + trancheShares: trancheShares.toString(), + }, + statusCode: 200, + result, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: Number(clockUnixTimestamp * 1000n), + }, + } + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const strcusxExchangeRateTransport = new StrcusxExchangeRateTransport() diff --git a/packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts b/packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts new file mode 100644 index 00000000000..e59ca23a5f6 --- /dev/null +++ b/packages/sources/solana-functions/src/transport/stslx-exchange-rate.ts @@ -0,0 +1,178 @@ +import { EndpointContext } from '@chainlink/external-adapter-framework/adapter' +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { SubscriptionTransport } from '@chainlink/external-adapter-framework/transports/abstract/subscription' +import { AdapterResponse, makeLogger, sleep } from '@chainlink/external-adapter-framework/util' +import { AdapterInputError } from '@chainlink/external-adapter-framework/validation/error' +import { address, getAddressEncoder, type Address } from '@solana/addresses' +import { type Rpc, type SolanaRpcApi } from '@solana/rpc' +import { ASSOCIATED_TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { BaseEndpointTypes, inputParameters } from '../endpoint/stslx-exchange-rate' +import { + assertTokenProgramOwner, + decodeMintInfo, + decodeTokenAccountInfo, + LEGACY_TOKEN_PROGRAM_ADDRESS, +} from '../shared/buffer-layout-accounts' +import { + applyRateBounds, + calculateNormalizedRate, + RESULT_DECIMALS, + toRateBounds, +} from '../shared/exchange-rate-utils' +import { + derivePda, + fetchMultipleAccounts, + getAccountDataBuffer, + parseSolanaAddress, + providerError, +} from '../shared/solana-account-utils' +import { SolanaRpcFactory } from '../shared/solana-rpc-factory' + +const logger = makeLogger('StslxExchangeRateTransport') + +const ASSOCIATED_TOKEN_PROGRAM_ADDRESS = ASSOCIATED_TOKEN_PROGRAM_ID.toBase58() +const GLAM_VAULT_SEED = 'vault' +const addressEncoder = getAddressEncoder() + +type RequestParams = typeof inputParameters.validated + +// Solstice provided the GLAM program/state config. GLAM derives the vault PDA +// from its program, the configured state address, and the "vault" seed. +const deriveVaultAddress = (glamStateAddress: Address, glamProtocolProgramAddress: Address) => + derivePda(glamProtocolProgramAddress, [GLAM_VAULT_SEED, addressEncoder.encode(glamStateAddress)]) + +// The derived SLX ATA assumes SLX is a legacy SPL mint; stSLX is read directly +// and can be owned by either the legacy SPL Token program or Token-2022. +const deriveSlxTokenAccountAddress = (vaultAddress: Address, slxMintAddress: Address) => + derivePda(ASSOCIATED_TOKEN_PROGRAM_ADDRESS, [ + addressEncoder.encode(vaultAddress), + addressEncoder.encode(address(LEGACY_TOKEN_PROGRAM_ADDRESS)), + addressEncoder.encode(slxMintAddress), + ]) + +export class StslxExchangeRateTransport extends SubscriptionTransport { + rpc!: Rpc + + async initialize( + dependencies: TransportDependencies, + adapterSettings: BaseEndpointTypes['Settings'], + endpointName: string, + transportName: string, + ): Promise { + await super.initialize(dependencies, adapterSettings, endpointName, transportName) + this.rpc = new SolanaRpcFactory().create(adapterSettings.RPC_URL) + } + + async backgroundHandler(context: EndpointContext, entries: RequestParams[]) { + await Promise.all(entries.map(async (param) => this.handleRequest(param))) + await sleep(context.adapterSettings.BACKGROUND_EXECUTE_MS) + } + + async handleRequest(param: RequestParams) { + let response: AdapterResponse + try { + response = await this._handleRequest(param) + } catch (e: unknown) { + const errorMessage = e instanceof Error ? e.message : 'Unknown error occurred' + logger.error(e, errorMessage) + response = { + statusCode: (e as AdapterInputError)?.statusCode || 502, + errorMessage, + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + await this.responseCache.write(this.name, [{ params: param, response }]) + } + + async _handleRequest( + params: RequestParams, + ): Promise> { + const providerDataRequestedUnixMs = Date.now() + const slxMintAddress = parseSolanaAddress(params.slxMintAddress, 'slxMintAddress') + const stslxMintAddress = parseSolanaAddress(params.stslxMintAddress, 'stslxMintAddress') + const glamStateAddress = parseSolanaAddress(params.glamStateAddress, 'glamStateAddress') + const glamProtocolProgramAddress = parseSolanaAddress( + params.glamProtocolProgramAddress, + 'glamProtocolProgramAddress', + ) + const { minRate, maxRate } = toRateBounds(params.minRate, params.maxRate) + const vaultAddress = await deriveVaultAddress(glamStateAddress, glamProtocolProgramAddress) + const slxTokenAccountAddress = await deriveSlxTokenAccountAddress(vaultAddress, slxMintAddress) + + // The stSLX feed reads GLAM vault's canonical SLX ATA as its SLX balance source. + const [slxMintAccount, stslxMintAccount, slxTokenAccount] = await fetchMultipleAccounts( + this.rpc, + [slxMintAddress, stslxMintAddress, slxTokenAccountAddress], + ) + + assertTokenProgramOwner(slxMintAccount, `SLX mint '${slxMintAddress}'`) + assertTokenProgramOwner(stslxMintAccount, `stSLX mint '${stslxMintAddress}'`) + + const slxMint = decodeMintInfo( + getAccountDataBuffer(slxMintAccount, `SLX mint '${slxMintAddress}'`), + `SLX mint '${slxMintAddress}'`, + ) + const stslxMint = decodeMintInfo( + getAccountDataBuffer(stslxMintAccount, `stSLX mint '${stslxMintAddress}'`), + `stSLX mint '${stslxMintAddress}'`, + ) + const slxToken = decodeTokenAccountInfo( + getAccountDataBuffer(slxTokenAccount, `SLX token account '${slxTokenAccountAddress}'`), + `SLX token account '${slxTokenAccountAddress}'`, + ) + + const computedRate = calculateNormalizedRate( + slxToken.amount, + stslxMint.supply, + slxMint.decimals, + stslxMint.decimals, + ) + if (computedRate === null) { + throw providerError(`stSLX mint '${stslxMintAddress}' has zero supply`) + } + + const { rate, boundsApplied } = applyRateBounds(computedRate, minRate, maxRate) + const result = rate.toString() + const computedResult = computedRate.toString() + if (boundsApplied) { + logger.warn( + { + computedResult, + result, + minRate: minRate?.toString(), + maxRate: maxRate?.toString(), + }, + 'stSLX exchange rate bounds applied', + ) + } + + return { + data: { + result, + computedResult, + decimals: RESULT_DECIMALS, + boundsApplied, + slxBalance: slxToken.amount.toString(), + stslxSupply: stslxMint.supply.toString(), + }, + statusCode: 200, + result, + timestamps: { + providerDataRequestedUnixMs, + providerDataReceivedUnixMs: Date.now(), + providerIndicatedTimeUnixMs: undefined, + }, + } + } + + getSubscriptionTtlFromConfig(adapterSettings: BaseEndpointTypes['Settings']): number { + return adapterSettings.WARMUP_SUBSCRIPTION_TTL + } +} + +export const stslxExchangeRateTransport = new StslxExchangeRateTransport() diff --git a/packages/sources/solana-functions/test/fixtures/strcusx-account-data-2026-06-25.json b/packages/sources/solana-functions/test/fixtures/strcusx-account-data-2026-06-25.json new file mode 100644 index 00000000000..4250839d12e --- /dev/null +++ b/packages/sources/solana-functions/test/fixtures/strcusx-account-data-2026-06-25.json @@ -0,0 +1,45 @@ +{ + "jsonrpc": "2.0", + "result": { + "context": { + "apiVersion": "4.1.0-rc.1", + "slot": 471830950 + }, + "value": [ + { + "data": [ + "uE+rALcrcW7/542Jpr7kht2QLlcZDxM4wrOudalo25BE5Z1HN2V2uq8AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADoViUD4wufyvLXgV0Vb24E5kY5omoEzfKSj3aSa6xpnAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base64" + ], + "executable": false, + "lamports": 3410400, + "owner": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "rentEpoch": 18446744073709552000, + "space": 362 + }, + { + "data": [ + "rm4nd1JqqWZTVFJDLVVTWC0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAPz+///+/P0GC/RNgn6TbRccEvQQZ37LTNwraJkh/lfQ0K0sKK7RyjfY9u+mWNEe6caUzdeVzsP3NfF2Gru6P5u89e6/XAIvqR4KktoQlXrrIHpYsjQUziie8ej41VMwGz1k3w9a7dcyHp8RLQNd9/wSSoR4tgqv4lGhZr6gkag64e5hIy5LoKdzxxvgXSMlBkRks9Enu/PGlntfafzcMkeNcSAtaoyM/bruZOAx8oRFggY02zX6m+9VuBgP0coKtXSI0Dst/VsAACuJudyP3580cJpbEGtHLw85u2ypzgSw/X8ulxaI4uU7WAIAAAAAAAAAAAAAoLsNACChBwCAcAAALAEAAFoAAAAAgMakfo0DAAAAAAAAAAAAAIDGpH6NAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", + "base64" + ], + "executable": false, + "lamports": 5185200, + "owner": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "rentEpoch": 18446744073709552000, + "space": 617 + }, + { + "data": [ + "Ce44NeRc2ShTVFJDLVVTWC0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAP8AwusLAAAAAAAAAAAAAAAAgHTSGgAAAAAAAAAAAAAAAIE2viYAAAAAAAAAAAAAAAAAwusLAAAAAAAAAAAAAAAAAQAAAAAAAAAAAAAAAAAAANzvH2oAAAAAXGAgagAAAAABAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAJDQAwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "base64" + ], + "executable": false, + "lamports": 3960240, + "owner": "7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe", + "rentEpoch": 18446744073709552000, + "space": 441 + } + ] + }, + "id": 1 +} diff --git a/packages/sources/solana-functions/test/integration/strcusx-exchange-rate.test.ts b/packages/sources/solana-functions/test/integration/strcusx-exchange-rate.test.ts new file mode 100644 index 00000000000..9613eb12c3a --- /dev/null +++ b/packages/sources/solana-functions/test/integration/strcusx-exchange-rate.test.ts @@ -0,0 +1,274 @@ +import { + TestAdapter, + makeStub, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import { MintLayout, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +const programAddress = '7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe' +const strategyName = 'STRC-USX-1' +const controllerAddress = 'DChEFFUoGXeZgh4Mivq7gR8mW5DQi7yMaQ1naqmnxB3q' +const strategyAddress = 'AT57KkNUMM3UeVwQmvTL8undUkFKYYRigtsCToxfpP1o' +const accountingAddress = '31vVMMVrketFGG9s25PxtQzm8HsAkqoSEoYuj4bXWcVn' +const assetMintAddress = '4ujhCkYxvGwdQnKRRzCjuVreThRAzY3k4n78iypNSQce' +const juniorMintAddress = 'Qc25hHS8uv2CEZUd9vC1sKBwsHgMdosF6KG6MsBavSd' +const seniorMintAddress = '4m1JrzTPgaKg1DwG19BotH4ZAUyrMzjmSDkGUr38YAai' +const assetVaultAddress = 'CPAUEk6XiZf4mvnWhEZZn1ojA3PyhTzDkovZX9sK6bgJ' +const vestingVaultAddress = '4NeU4YUyTX2fN9XTTRDpqddL94AvvWrcvVf4FGKaXBsd' +const feeVaultAddress = 'CGfUqdJoGKSEQMjdiRebxTxqB2PtfsJcphorC5Nnpxgs' +const lossVaultAddress = 'J5TUHd2nzueopWNatEMW514uAYwyyLxioYsPQA6UuGt2' +const clockSysvarAddress = 'SysvarC1ock11111111111111111111111111111111' +const tokenProgramAddress = TOKEN_PROGRAM_ID.toBase58() +const minRate = '950000000000000000' +const maxRate = '1050000000000000000' +const clockUnixTimestamp = 1_781_704_234n + +const accountingDiscriminator = Buffer.from([9, 238, 56, 53, 228, 92, 217, 40]) +const controllerDiscriminator = Buffer.from([184, 79, 171, 0, 183, 43, 113, 110]) +const strategyDiscriminator = Buffer.from([174, 110, 39, 119, 82, 106, 169, 102]) +const controllerAccountLength = 362 +const strategyAccountLength = 617 +const accountingAccountLength = 441 + +const seniorShares = 200_000_000n +const juniorShares = 450_000_000n +const totalAssets = 650_000_001n +const seniorAssets = 200_000_000n +const mintDecimals = 6 +const expectedJuniorRate = '1000000002222222222' +const expectedSeniorRate = '1000000000000000000' +const providerDataTimestampUnixMs = 1_781_704_234_111 +const providerIndicatedTimeUnixMs = Number(clockUnixTimestamp * 1000n) + +const writeU128LE = (buffer: Buffer, value: bigint, offset: number) => { + buffer.writeBigUInt64LE(value & ((1n << 64n) - 1n), offset) + buffer.writeBigUInt64LE(value >> 64n, offset + 8) +} + +const writePublicKey = (buffer: Buffer, address: string, offset: number) => { + new PublicKey(address).toBuffer().copy(buffer, offset) +} + +const writeName = (buffer: Buffer, value: string, offset: number) => { + Buffer.from(value).copy(buffer, offset) +} + +const encodeMint = (supply: bigint, decimals: number) => { + const buffer = Buffer.alloc(MintLayout.span) + MintLayout.encode( + { + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const encodeClock = (unixTimestamp: bigint) => { + const buffer = Buffer.alloc(40) + buffer.writeBigUInt64LE(0n, 0) + buffer.writeBigInt64LE(0n, 8) + buffer.writeBigUInt64LE(0n, 16) + buffer.writeBigUInt64LE(0n, 24) + buffer.writeBigInt64LE(unixTimestamp, 32) + return buffer.toString('base64') +} + +const encodeController = () => { + const buffer = Buffer.alloc(controllerAccountLength) + controllerDiscriminator.copy(buffer, 0) + writePublicKey(buffer, assetMintAddress, 73) + buffer[105] = 0 + return buffer.toString('base64') +} + +const encodeStrategy = () => { + const buffer = Buffer.alloc(strategyAccountLength) + strategyDiscriminator.copy(buffer, 0) + writeName(buffer, strategyName, 8) + writePublicKey(buffer, juniorMintAddress, 47) + writePublicKey(buffer, seniorMintAddress, 79) + writePublicKey(buffer, assetVaultAddress, 111) + writePublicKey(buffer, vestingVaultAddress, 143) + writePublicKey(buffer, feeVaultAddress, 175) + writePublicKey(buffer, lossVaultAddress, 207) + writeU128LE(buffer, 1_000_000_000_000_000n, 305) + writeU128LE(buffer, 1_000_000_000_000_000n, 321) + return buffer.toString('base64') +} + +const encodeAccounting = () => { + const buffer = Buffer.alloc(accountingAccountLength) + accountingDiscriminator.copy(buffer, 0) + writeName(buffer, strategyName, 8) + writeU128LE(buffer, seniorShares, 41) + writeU128LE(buffer, juniorShares, 57) + writeU128LE(buffer, totalAssets, 73) + writeU128LE(buffer, seniorAssets, 89) + return buffer.toString('base64') +} + +const makeAccountInfoResponse = (data: string, owner = tokenProgramAddress) => ({ + data: [data, 'base64'], + owner, +}) + +const solanaRpc = makeStub('solanaRpc', { + getMultipleAccounts: (addresses: string[]) => ({ + async send() { + const accountsByAddress: Record> = { + [controllerAddress]: makeAccountInfoResponse(encodeController(), programAddress), + [strategyAddress]: makeAccountInfoResponse(encodeStrategy(), programAddress), + [accountingAddress]: makeAccountInfoResponse(encodeAccounting(), programAddress), + [assetMintAddress]: makeAccountInfoResponse( + encodeMint(1_000_000_000_000_000_000n, mintDecimals), + ), + [juniorMintAddress]: makeAccountInfoResponse(encodeMint(juniorShares, mintDecimals)), + [seniorMintAddress]: makeAccountInfoResponse(encodeMint(seniorShares, mintDecimals)), + [clockSysvarAddress]: makeAccountInfoResponse(encodeClock(clockUnixTimestamp)), + } + + return { + value: addresses.map((address) => accountsByAddress[address] ?? null), + } + }, + }), +}) + +const createSolanaRpc = () => solanaRpc + +jest.mock('@solana/rpc', () => ({ + createSolanaRpc() { + return createSolanaRpc() + }, +})) + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.RPC_URL = 'solana.rpc.url' + process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0' + const mockDate = new Date('2026-06-17T13:50:34.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + spy.mockRestore() + }) + + describe('strcusx-exchange-rate', () => { + it('should return junior success', async () => { + const response = await testAdapter.request({ + endpoint: 'strcusx-exchange-rate', + programAddress, + strategyName, + tranche: 'junior', + minRate, + maxRate, + }) + expect(response.statusCode).toBe(200) + expect(response.json()).toEqual({ + data: { + boundsApplied: false, + computedResult: expectedJuniorRate, + decimals: 18, + result: expectedJuniorRate, + trancheAssets: (totalAssets - seniorAssets).toString(), + trancheShares: juniorShares.toString(), + tranche: 'junior', + }, + result: expectedJuniorRate, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: providerDataTimestampUnixMs, + providerDataRequestedUnixMs: providerDataTimestampUnixMs, + providerIndicatedTimeUnixMs, + }, + }) + }) + + it('should return senior success', async () => { + const response = await testAdapter.request({ + endpoint: 'strcusx-exchange-rate', + programAddress, + strategyName, + tranche: 'senior', + minRate, + maxRate, + }) + expect(response.statusCode).toBe(200) + expect(response.json()).toEqual({ + data: { + boundsApplied: false, + computedResult: expectedSeniorRate, + decimals: 18, + result: expectedSeniorRate, + trancheAssets: seniorAssets.toString(), + trancheShares: seniorShares.toString(), + tranche: 'senior', + }, + result: expectedSeniorRate, + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: providerDataTimestampUnixMs, + providerDataRequestedUnixMs: providerDataTimestampUnixMs, + providerIndicatedTimeUnixMs, + }, + }) + }) + + it('should return success when bounds are omitted', async () => { + const response = await testAdapter.request({ + endpoint: 'strcusx-exchange-rate', + programAddress, + strategyName, + tranche: 'junior', + }) + + expect(response.statusCode).toBe(200) + expect(response.json().data.boundsApplied).toBe(false) + expect(response.json().result).toBe(expectedJuniorRate) + }) + + it('should reject inverted bounds', async () => { + const response = await testAdapter.request({ + endpoint: 'strcusx-exchange-rate', + programAddress, + strategyName, + tranche: 'junior', + minRate: maxRate, + maxRate: minRate, + }) + + expect(response.statusCode).toBe(400) + expect(response.json()).toEqual({ + error: { + message: 'minRate must be less than or equal to maxRate', + name: 'AdapterError', + }, + status: 'errored', + statusCode: 400, + }) + }) + }) +}) diff --git a/packages/sources/solana-functions/test/integration/stslx-exchange-rate.test.ts b/packages/sources/solana-functions/test/integration/stslx-exchange-rate.test.ts new file mode 100644 index 00000000000..3beeb908d9d --- /dev/null +++ b/packages/sources/solana-functions/test/integration/stslx-exchange-rate.test.ts @@ -0,0 +1,177 @@ +import { + TestAdapter, + makeStub, + setEnvVariables, +} from '@chainlink/external-adapter-framework/util/testing-utils' +import { + AccountLayout, + MintLayout, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' + +const slxMintAddress = 'SLXdx4BUt2v9uJQNzWqSfzTJ9UKLUDsvxHFMEEdrfgq' +const stslxMintAddress = 'GxHksENo754dKj6kv5d2z7ey9KwE7YSRYgRCtoFYd2yq' +const vaultAddress = 'GMwdh2jTdTrrhA7dMR7Cc2zC6gV38UePzAXeoFHrXnfH' +const slxTokenAccountAddress = '7CssRFNePpnDiCzjRC5kPRDpEJn87JMeDG7s6Gww9CTf' +const minRate = '1000000000000000000' +const maxRate = '2000000000000000000' +const slxBalance = 1_500_000_000n +const stslxSupply = 1_000_000n +const tokenProgramAddress = TOKEN_PROGRAM_ID.toBase58() +const token2022ProgramAddress = TOKEN_2022_PROGRAM_ID.toBase58() + +const encodeMint = (supply: bigint, decimals: number) => { + const buffer = Buffer.alloc(MintLayout.span) + MintLayout.encode( + { + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const encodeTokenAccount = (amount: bigint) => { + const buffer = Buffer.alloc(AccountLayout.span) + AccountLayout.encode( + { + mint: new PublicKey(slxMintAddress), + owner: new PublicKey(vaultAddress), + amount, + delegateOption: 0, + delegate: PublicKey.default, + state: 1, + isNativeOption: 0, + isNative: 0n, + delegatedAmount: 0n, + closeAuthorityOption: 0, + closeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const makeAccountInfoResponse = (data: string, owner = tokenProgramAddress) => ({ + data: [data, 'base64'], + owner, +}) + +const solanaRpc = makeStub('solanaRpc', { + getMultipleAccounts: (addresses: string[]) => ({ + async send() { + const accountsByAddress: Record> = { + [slxMintAddress]: makeAccountInfoResponse(encodeMint(100_000_000_000n, 9)), + [stslxMintAddress]: makeAccountInfoResponse( + encodeMint(stslxSupply, 6), + token2022ProgramAddress, + ), + [slxTokenAccountAddress]: makeAccountInfoResponse(encodeTokenAccount(slxBalance)), + } + + return { + value: addresses.map((address) => accountsByAddress[address] ?? null), + } + }, + }), +}) + +const createSolanaRpc = () => solanaRpc + +jest.mock('@solana/rpc', () => ({ + createSolanaRpc() { + return createSolanaRpc() + }, +})) + +describe('execute', () => { + let spy: jest.SpyInstance + let testAdapter: TestAdapter + let oldEnv: NodeJS.ProcessEnv + + beforeAll(async () => { + oldEnv = JSON.parse(JSON.stringify(process.env)) + process.env.RPC_URL = 'solana.rpc.url' + process.env.BACKGROUND_EXECUTE_MS = process.env.BACKGROUND_EXECUTE_MS ?? '0' + const mockDate = new Date('2001-01-01T11:11:11.111Z') + spy = jest.spyOn(Date, 'now').mockReturnValue(mockDate.getTime()) + + const adapter = (await import('./../../src')).adapter + adapter.rateLimiting = undefined + testAdapter = await TestAdapter.startWithMockedCache(adapter, { + testAdapter: {} as TestAdapter, + }) + }) + + afterAll(async () => { + setEnvVariables(oldEnv) + await testAdapter.api.close() + spy.mockRestore() + }) + + describe('stslx-exchange-rate', () => { + it('should return success', async () => { + const data = { + endpoint: 'stslx-exchange-rate', + minRate, + maxRate, + } + const response = await testAdapter.request(data) + expect(response.statusCode).toBe(200) + expect(response.json()).toEqual({ + data: { + boundsApplied: false, + computedResult: '1500000000000000000', + decimals: 18, + result: '1500000000000000000', + slxBalance: slxBalance.toString(), + stslxSupply: stslxSupply.toString(), + }, + result: '1500000000000000000', + statusCode: 200, + timestamps: { + providerDataReceivedUnixMs: 978347471111, + providerDataRequestedUnixMs: 978347471111, + }, + }) + }) + + it('should return success when bounds are omitted', async () => { + const response = await testAdapter.request({ + endpoint: 'stslx-exchange-rate', + }) + + expect(response.statusCode).toBe(200) + expect(response.json().data.boundsApplied).toBe(false) + expect(response.json().result).toBe('1500000000000000000') + }) + + it('should reject inverted bounds', async () => { + const response = await testAdapter.request({ + endpoint: 'stslx-exchange-rate', + minRate: maxRate, + maxRate: minRate, + }) + + expect(response.statusCode).toBe(400) + expect(response.json()).toEqual({ + error: { + message: 'minRate must be less than or equal to maxRate', + name: 'AdapterError', + }, + status: 'errored', + statusCode: 400, + }) + }) + }) +}) diff --git a/packages/sources/solana-functions/test/unit/account-reader.test.ts b/packages/sources/solana-functions/test/unit/account-reader.test.ts index abd3f48d3ec..56e6e022f65 100644 --- a/packages/sources/solana-functions/test/unit/account-reader.test.ts +++ b/packages/sources/solana-functions/test/unit/account-reader.test.ts @@ -13,6 +13,7 @@ jest.mock('@coral-xyz/anchor', () => { }) jest.mock('@solana/addresses', () => ({ + address: jest.fn((value: string) => value), getProgramDerivedAddress: jest.fn(), })) diff --git a/packages/sources/solana-functions/test/unit/buffer-layout-accounts.test.ts b/packages/sources/solana-functions/test/unit/buffer-layout-accounts.test.ts index 6a5cef86934..90a75f67d14 100644 --- a/packages/sources/solana-functions/test/unit/buffer-layout-accounts.test.ts +++ b/packages/sources/solana-functions/test/unit/buffer-layout-accounts.test.ts @@ -1,6 +1,13 @@ import { makeStub } from '@chainlink/external-adapter-framework/util/testing-utils' import { type Rpc, type SolanaRpcApi } from '@solana/rpc' -import { fetchFieldFromBufferLayoutStateAccount } from '../../src/shared/buffer-layout-accounts' +import { + TOKEN_2022_PROGRAM_ADDRESS, + assertTokenProgramOwner, + decodeMintInfo, + decodeTokenAccountInfo, + fetchFieldFromBufferLayoutStateAccount, +} from '../../src/shared/buffer-layout-accounts' +import { type AccountInfo } from '../../src/shared/solana-account-utils' import * as sanctumInfinityPoolAccountData from '../fixtures/sanctum-infinity-pool-account-data-2025-10-07.json' import * as sanctumInfinityTokenAccountData from '../fixtures/sanctum-infinity-token-account-data-2025-10-07.json' import * as tokenAccountData from '../fixtures/token-account-data-2025-12-01.json' @@ -17,6 +24,8 @@ describe('buffer-layout-accounts', () => { getAccountInfoMock.mockReturnValue({ send: sendMock }) }) + const makeAccountInfo = (owner: string) => ({ owner } as unknown as AccountInfo) + describe('fetchFieldFromBufferLayoutStateAccount', () => { it('should fetch and decode field from mint account', async () => { const response = makeStub('response', sanctumInfinityTokenAccountData.result) @@ -125,6 +134,25 @@ describe('buffer-layout-accounts', () => { expect(getAccountInfoMock).toHaveBeenCalledTimes(1) }) + it('should not use the exact-span layout map for Token-2022 accounts', async () => { + const response = makeStub('response', { + value: { + data: [Buffer.alloc(200).toString('base64'), 'base64'], + owner: TOKEN_2022_PROGRAM_ADDRESS, + }, + }) + + sendMock.mockResolvedValue(response) + + await expect(() => + fetchFieldFromBufferLayoutStateAccount({ + stateAccountAddress: '5oVNBeEEQvYi1cX3ir8Dx5n1P7pdxydbGF2X4TxVusJm', + field: 'amount', + rpc, + }), + ).rejects.toThrow(`No layout known for program address '${TOKEN_2022_PROGRAM_ADDRESS}'`) + }) + it('should throw for unknown program', async () => { const response = makeStub('response', { value: { @@ -173,4 +201,44 @@ describe('buffer-layout-accounts', () => { expect(getAccountInfoMock).toHaveBeenCalledTimes(1) }) }) + + describe('decodeMintInfo', () => { + it('should decode SPL mint supply and decimals', () => { + const data = Buffer.from( + sanctumInfinityTokenAccountData.result.value.data[0] as string, + 'base64', + ) + + expect(decodeMintInfo(data, 'test mint')).toEqual({ + supply: 1_116_792_619_507_830n, + decimals: 9, + }) + }) + }) + + describe('assertTokenProgramOwner', () => { + it('should accept supported SPL token programs', () => { + expect(() => + assertTokenProgramOwner(makeAccountInfo(TOKEN_2022_PROGRAM_ADDRESS), 'test mint'), + ).not.toThrow() + }) + + it('should reject unsupported owners', () => { + expect(() => assertTokenProgramOwner(makeAccountInfo('other-owner'), 'test mint')).toThrow( + 'Expected test mint to be owned by a supported token program', + ) + }) + }) + + describe('decodeTokenAccountInfo', () => { + it('should decode SPL token account mint, owner, and amount', () => { + const data = Buffer.from(tokenAccountData.result.value.data[0] as string, 'base64') + + expect(decodeTokenAccountInfo(data, 'test token account')).toEqual({ + mintAddress: '8fr7WGTVFszfyNWRMXj6fRjZZAnDwmXwEpCrtzmUkdih', + ownerAddress: 'DT7z9w9fGJ6sH7vmGbPCa5JLi2xp6XPrL61z2gctzmHb', + amount: 34_228_590_128n, + }) + }) + }) }) diff --git a/packages/sources/solana-functions/test/unit/exchange-rate-utils.test.ts b/packages/sources/solana-functions/test/unit/exchange-rate-utils.test.ts new file mode 100644 index 00000000000..85c6517b357 --- /dev/null +++ b/packages/sources/solana-functions/test/unit/exchange-rate-utils.test.ts @@ -0,0 +1,89 @@ +import { + applyRateBounds, + calculateNormalizedRate, + calculateUnvestedAssets, + toRateBounds, + validateRateBound, + validateRateBounds, +} from '../../src/shared/exchange-rate-utils' + +describe('exchange-rate-utils', () => { + describe('validateRateBound', () => { + it('should accept positive base-10 integer strings', () => { + expect(() => validateRateBound('1000000000000000000', 'minRate')).not.toThrow() + }) + + it('should reject non-positive or non-canonical values', () => { + for (const value of ['0', '-1', '01', 'not-a-rate']) { + expect(() => validateRateBound(value, 'minRate')).toThrow( + 'minRate must be a positive base-10 integer string', + ) + } + }) + }) + + describe('toRateBounds', () => { + it('should convert valid bounds', () => { + expect(toRateBounds('1', '2')).toEqual({ minRate: 1n, maxRate: 2n }) + }) + + it('should allow omitted bounds', () => { + expect(toRateBounds(undefined, '2')).toEqual({ minRate: undefined, maxRate: 2n }) + expect(toRateBounds('1', undefined)).toEqual({ minRate: 1n, maxRate: undefined }) + expect(toRateBounds(undefined, undefined)).toEqual({ + minRate: undefined, + maxRate: undefined, + }) + }) + }) + + describe('validateRateBounds', () => { + it('should reject inverted bounds', () => { + expect(() => validateRateBounds('2', '1')).toThrow( + 'minRate must be less than or equal to maxRate', + ) + }) + }) + + describe('applyRateBounds', () => { + it('should leave in-range rates unchanged', () => { + expect(applyRateBounds(10n, 1n, 20n)).toEqual({ rate: 10n, boundsApplied: false }) + expect(applyRateBounds(10n, undefined, undefined)).toEqual({ + rate: 10n, + boundsApplied: false, + }) + }) + + it('should clamp below-minimum and above-maximum rates', () => { + expect(applyRateBounds(10n, 11n, 20n)).toEqual({ rate: 11n, boundsApplied: true }) + expect(applyRateBounds(21n, 1n, 20n)).toEqual({ rate: 20n, boundsApplied: true }) + expect(applyRateBounds(10n, 11n, undefined)).toEqual({ rate: 11n, boundsApplied: true }) + expect(applyRateBounds(21n, undefined, 20n)).toEqual({ rate: 20n, boundsApplied: true }) + }) + }) + + describe('calculateNormalizedRate', () => { + it('should calculate an 18-decimal normalized rate', () => { + expect(calculateNormalizedRate(1_500_000_000n, 1_000_000n, 9, 6)).toBe( + 1_500_000_000_000_000_000n, + ) + }) + + it('should return null when shares are zero', () => { + expect(calculateNormalizedRate(1n, 0n, 6, 6)).toBeNull() + }) + }) + + describe('calculateUnvestedAssets', () => { + it('should use direct unvested formula with floor rounding', () => { + expect(calculateUnvestedAssets(10n, 1n, 0n, 3n)).toBe(6n) + }) + + it('should handle inactive, pending, complete, and empty vesting schedules', () => { + expect(calculateUnvestedAssets(10n, 1n, 3n, 1n)).toBe(0n) + expect(calculateUnvestedAssets(10n, 1n, 2n, 3n)).toBe(10n) + expect(calculateUnvestedAssets(10n, 3n, 1n, 3n)).toBe(0n) + expect(calculateUnvestedAssets(0n, 1n, 0n, 3n)).toBe(0n) + }) + }) +}) diff --git a/packages/sources/solana-functions/test/unit/solana-account-utils.test.ts b/packages/sources/solana-functions/test/unit/solana-account-utils.test.ts new file mode 100644 index 00000000000..7d189c1e393 --- /dev/null +++ b/packages/sources/solana-functions/test/unit/solana-account-utils.test.ts @@ -0,0 +1,144 @@ +import { address, getAddressEncoder } from '@solana/addresses' +import { type Rpc, type SolanaRpcApi } from '@solana/rpc' +import { + CLOCK_SYSVAR_ADDRESS, + assertDataLength, + assertOwnerProgram, + decodeClockUnixTimestamp, + derivePda, + fetchMultipleAccounts, + getAccountDataBuffer, + parseSolanaAddress, + type AccountInfo, +} from '../../src/shared/solana-account-utils' + +describe('solana-account-utils', () => { + const systemProgramAddress = '11111111111111111111111111111111' + + const makeAccountInfo = (data: string, owner = systemProgramAddress) => + ({ data: [data, 'base64'], owner } as unknown as AccountInfo) + + describe('parseSolanaAddress', () => { + it('should validate Solana addresses', () => { + expect(parseSolanaAddress(systemProgramAddress, 'programAddress')).toBe(systemProgramAddress) + expect(() => parseSolanaAddress('not-an-address', 'programAddress')).toThrow( + 'programAddress must be a valid Solana address', + ) + }) + }) + + describe('derivePda', () => { + it('should derive PDAs with @solana/addresses', async () => { + const addressEncoder = getAddressEncoder() + const glamProtocolProgramAddress = 'GLAMpaME8wdTEzxtiYEAa5yD8fZbxZiz2hNtV58RZiEz' + const glamStateAddress = '5E2scHi8LyZAqZeVHnXLeFhwoePxD2CTdSruWmjgVEoB' + + await expect( + derivePda(glamProtocolProgramAddress, [ + 'vault', + addressEncoder.encode(address(glamStateAddress)), + ]), + ).resolves.toBe('GMwdh2jTdTrrhA7dMR7Cc2zC6gV38UePzAXeoFHrXnfH') + }) + }) + + describe('getAccountDataBuffer', () => { + it('should decode base64 account data', () => { + const data = Buffer.from('hello').toString('base64') + + expect(getAccountDataBuffer(makeAccountInfo(data), 'test account').toString()).toBe('hello') + }) + + it('should throw when account data is missing', () => { + expect(() => getAccountDataBuffer(null, 'test account')).toThrow( + 'No account data found for test account', + ) + }) + + it('should throw when account data is not a base64 string', () => { + const accountInfo = { data: [123, 'base64'], owner: systemProgramAddress } + + expect(() => + getAccountDataBuffer(accountInfo as unknown as AccountInfo, 'test account'), + ).toThrow('No account data found for test account') + }) + }) + + describe('assertOwnerProgram', () => { + it('should accept an expected owner', () => { + expect(() => + assertOwnerProgram( + makeAccountInfo('', 'owner-1'), + 'test account', + ['owner-1'], + 'test program', + ), + ).not.toThrow() + }) + + it('should throw for an unexpected owner', () => { + expect(() => + assertOwnerProgram( + makeAccountInfo('', 'owner-2'), + 'test account', + ['owner-1'], + 'test program', + ), + ).toThrow("Expected test account to be owned by test program [owner-1], found 'owner-2'") + }) + }) + + describe('assertDataLength', () => { + it('should assert minimum data length', () => { + expect(() => assertDataLength(Buffer.alloc(2), 'test account', 3)).toThrow( + 'Expected test account account data to be at least 3 bytes, found 2', + ) + }) + }) + + describe('decodeClockUnixTimestamp', () => { + it('should decode the Solana Clock sysvar timestamp', () => { + const data = Buffer.alloc(40) + data.writeBigInt64LE(1_781_704_234n, 32) + + expect(decodeClockUnixTimestamp(makeAccountInfo(data.toString('base64')))).toBe( + 1_781_704_234n, + ) + }) + + it('should reject missing Clock sysvar data', () => { + expect(() => decodeClockUnixTimestamp(null)).toThrow( + `No account data found for Clock sysvar '${CLOCK_SYSVAR_ADDRESS}'`, + ) + }) + }) + + describe('fetchMultipleAccounts', () => { + const sendMock = jest.fn() + const getMultipleAccountsMock = jest.fn() + const rpc = { getMultipleAccounts: getMultipleAccountsMock } as unknown as Rpc + + beforeEach(() => { + jest.resetAllMocks() + getMultipleAccountsMock.mockReturnValue({ send: sendMock }) + }) + + it('should fetch base64 accounts in one request', async () => { + const accounts = [makeAccountInfo('AA==')] + sendMock.mockResolvedValue({ value: accounts }) + + await expect(fetchMultipleAccounts(rpc, [systemProgramAddress])).resolves.toBe(accounts) + expect(getMultipleAccountsMock).toHaveBeenCalledWith([systemProgramAddress], { + encoding: 'base64', + }) + }) + + it('should throw when response count does not match request count', async () => { + sendMock.mockResolvedValue({ value: [] }) + + await expect(fetchMultipleAccounts(rpc, [systemProgramAddress])).rejects.toThrow( + 'Expected 1 account responses, received 0', + ) + }) + }) +}) diff --git a/packages/sources/solana-functions/test/unit/strcusx-exchange-rate.test.ts b/packages/sources/solana-functions/test/unit/strcusx-exchange-rate.test.ts new file mode 100644 index 00000000000..fd1c178edff --- /dev/null +++ b/packages/sources/solana-functions/test/unit/strcusx-exchange-rate.test.ts @@ -0,0 +1,565 @@ +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { LoggerFactoryProvider } from '@chainlink/external-adapter-framework/util' +import { makeStub } from '@chainlink/external-adapter-framework/util/testing-utils' +import { MintLayout, TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' +import { BaseEndpointTypes } from '../../src/endpoint/strcusx-exchange-rate' +import { deriveAccountAddress, PDA_SEEDS } from '../../src/transport/strcusx-accounts' +import { StrcusxExchangeRateTransport } from '../../src/transport/strcusx-exchange-rate' +import strcusxAccountFixture from '../fixtures/strcusx-account-data-2026-06-25.json' + +const programAddress = '7iNvMc3x5VvwNmYomAAg86CpWeEw7QfDF2z5GgtDzHXe' +const strategyName = 'STRC-USX-1' +const controllerAddress = 'DChEFFUoGXeZgh4Mivq7gR8mW5DQi7yMaQ1naqmnxB3q' +const strategyAddress = 'AT57KkNUMM3UeVwQmvTL8undUkFKYYRigtsCToxfpP1o' +const accountingAddress = '31vVMMVrketFGG9s25PxtQzm8HsAkqoSEoYuj4bXWcVn' +const assetMintAddress = '4ujhCkYxvGwdQnKRRzCjuVreThRAzY3k4n78iypNSQce' +const juniorMintAddress = 'Qc25hHS8uv2CEZUd9vC1sKBwsHgMdosF6KG6MsBavSd' +const seniorMintAddress = '4m1JrzTPgaKg1DwG19BotH4ZAUyrMzjmSDkGUr38YAai' +const assetVaultAddress = 'CPAUEk6XiZf4mvnWhEZZn1ojA3PyhTzDkovZX9sK6bgJ' +const vestingVaultAddress = '4NeU4YUyTX2fN9XTTRDpqddL94AvvWrcvVf4FGKaXBsd' +const feeVaultAddress = 'CGfUqdJoGKSEQMjdiRebxTxqB2PtfsJcphorC5Nnpxgs' +const lossVaultAddress = 'J5TUHd2nzueopWNatEMW514uAYwyyLxioYsPQA6UuGt2' +const clockSysvarAddress = 'SysvarC1ock11111111111111111111111111111111' +const tokenProgramAddress = TOKEN_PROGRAM_ID.toBase58() +const token2022ProgramAddress = TOKEN_2022_PROGRAM_ID.toBase58() +const minRate = '950000000000000000' +const maxRate = '1050000000000000000' +const clockUnixTimestamp = 1_781_704_234n +const providerIndicatedTimeUnixMs = Number(clockUnixTimestamp * 1000n) + +const accountingDiscriminator = Buffer.from([9, 238, 56, 53, 228, 92, 217, 40]) +const controllerDiscriminator = Buffer.from([184, 79, 171, 0, 183, 43, 113, 110]) +const strategyDiscriminator = Buffer.from([174, 110, 39, 119, 82, 106, 169, 102]) +const controllerAccountLength = 362 +const strategyAccountLength = 617 +const accountingAccountLength = 441 + +const seniorShares = 200_000_000n +const juniorShares = 450_000_000n +const totalAssets = 650_000_001n +const seniorAssets = 200_000_000n +const juniorAssets = totalAssets - seniorAssets +const mintDecimals = 6 +const expectedSeniorRate = '1000000000000000000' +const expectedJuniorRate = '1000000002222222222' +const expectedHalfVestedSeniorRate = '1020000000000000000' +const expectedHalfVestedJuniorRate = '1013333333333333333' + +const writeU128LE = (buffer: Buffer, value: bigint, offset: number) => { + buffer.writeBigUInt64LE(value & ((1n << 64n) - 1n), offset) + buffer.writeBigUInt64LE(value >> 64n, offset + 8) +} + +const writeU64LE = (buffer: Buffer, value: bigint, offset: number) => { + buffer.writeBigUInt64LE(value, offset) +} + +const writePublicKey = (buffer: Buffer, address: string, offset: number) => { + new PublicKey(address).toBuffer().copy(buffer, offset) +} + +const writeName = (buffer: Buffer, value: string, offset: number) => { + Buffer.from(value).copy(buffer, offset) +} + +const encodeMint = (supply: bigint, decimals: number) => { + const buffer = Buffer.alloc(MintLayout.span) + MintLayout.encode( + { + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const encodeMintWithExtensions = (supply: bigint, decimals: number) => + Buffer.concat([ + Buffer.from(encodeMint(supply, decimals), 'base64'), + Buffer.alloc(64, 1), + ]).toString('base64') + +const encodeClock = (unixTimestamp: bigint) => { + const buffer = Buffer.alloc(40) + buffer.writeBigUInt64LE(0n, 0) + buffer.writeBigInt64LE(0n, 8) + buffer.writeBigUInt64LE(0n, 16) + buffer.writeBigUInt64LE(0n, 24) + buffer.writeBigInt64LE(unixTimestamp, 32) + return buffer.toString('base64') +} + +const encodeController = () => { + const buffer = Buffer.alloc(controllerAccountLength) + controllerDiscriminator.copy(buffer, 0) + writePublicKey(buffer, assetMintAddress, 73) + buffer[105] = 0 + return buffer.toString('base64') +} + +const encodeStrategy = () => { + const buffer = Buffer.alloc(strategyAccountLength) + strategyDiscriminator.copy(buffer, 0) + writeName(buffer, strategyName, 8) + buffer[40] = 255 + buffer[41] = 254 + buffer[42] = 253 + buffer[43] = 252 + buffer[44] = 251 + buffer[45] = 250 + buffer[46] = 249 + writePublicKey(buffer, juniorMintAddress, 47) + writePublicKey(buffer, seniorMintAddress, 79) + writePublicKey(buffer, assetVaultAddress, 111) + writePublicKey(buffer, vestingVaultAddress, 143) + writePublicKey(buffer, feeVaultAddress, 175) + writePublicKey(buffer, lossVaultAddress, 207) + buffer[239] = 0 + buffer[240] = 0 + writeU128LE(buffer, 1_000_000_000_000_000n, 305) + writeU128LE(buffer, 1_000_000_000_000_000n, 321) + return buffer.toString('base64') +} + +const encodeAccounting = ({ + name = strategyName, + seniorSharesValue = seniorShares, + juniorSharesValue = juniorShares, + totalAssetsValue = totalAssets, + seniorAssetsValue = seniorAssets, + totalVestingAssetsValue = 0n, + seniorVestingAssetsValue = 0n, + vestingStartTimeValue = 0n, + vestingEndTimeValue = 0n, +} = {}) => { + const buffer = Buffer.alloc(accountingAccountLength) + accountingDiscriminator.copy(buffer, 0) + writeName(buffer, name, 8) + buffer[40] = 255 + writeU128LE(buffer, seniorSharesValue, 41) + writeU128LE(buffer, juniorSharesValue, 57) + writeU128LE(buffer, totalAssetsValue, 73) + writeU128LE(buffer, seniorAssetsValue, 89) + writeU64LE(buffer, totalVestingAssetsValue, 105) + writeU64LE(buffer, seniorVestingAssetsValue, 113) + writeU64LE(buffer, vestingStartTimeValue, 121) + writeU64LE(buffer, vestingEndTimeValue, 129) + return buffer.toString('base64') +} + +const makeAccountInfoResponse = (data: string, owner = tokenProgramAddress) => ({ + data: [data, 'base64'], + owner, +}) + +const getMultipleAccountsSendMock = jest.fn() +const getMultipleAccountsRequestMock = jest.fn() + +const mockRpcRequests = () => { + getMultipleAccountsRequestMock.mockImplementation( + (addresses: string[], config: { encoding: string }) => ({ + send() { + return getMultipleAccountsSendMock(addresses, config) + }, + }), + ) +} + +const solanaRpc = makeStub('solanaRpc', { + getMultipleAccounts: getMultipleAccountsRequestMock, +}) + +const createSolanaRpc = () => solanaRpc + +jest.mock('@solana/rpc', () => ({ + createSolanaRpc() { + return createSolanaRpc() + }, +})) + +const log = jest.fn() +const logger = { + fatal: log, + error: log, + warn: log, + info: log, + debug: log, + trace: log, + msgPrefix: 'mock-logger', +} + +const loggerFactory = { child: () => logger } + +LoggerFactoryProvider.set(loggerFactory) + +describe('StrcusxExchangeRateTransport', () => { + const transportName = 'default_single_transport' + const endpointName = 'strcusx-exchange-rate' + const RPC_URL = 'https://solana.rpc.url' + + const adapterSettings = makeStub('adapterSettings', { + RPC_URL, + SOLANA_COMMITMENT: 'finalized', + WARMUP_SUBSCRIPTION_TTL: 10_000, + BACKGROUND_EXECUTE_MS: 1500, + MAX_COMMON_KEY_SIZE: 300, + } as unknown as BaseEndpointTypes['Settings']) + + const dependencies = makeStub('dependencies', { + responseCache: { write: jest.fn() }, + subscriptionSetFactory: { + buildSet: jest.fn(), + }, + } as unknown as TransportDependencies) + + const juniorParam = makeStub('juniorParam', { + endpoint: 'strcusx-exchange-rate', + programAddress, + strategyName, + tranche: 'junior' as const, + minRate, + maxRate, + } as const) + + const seniorParam = makeStub('seniorParam', { + ...juniorParam, + tranche: 'senior' as const, + }) + + let transport: StrcusxExchangeRateTransport + + type AccountInfo = ReturnType | null + + const getFixtureAccountInfo = (index: number): Exclude => { + const account = strcusxAccountFixture.result.value[index] + if (!account) { + throw new Error(`Missing strcUSX fixture account at index ${index}`) + } + + return { + data: account.data as [string, string], + owner: account.owner, + } + } + + const mockAccountData = ({ + accountingData = encodeAccounting(), + unixTimestamp = clockUnixTimestamp, + overrides = {}, + }: { + accountingData?: string + unixTimestamp?: bigint + overrides?: Record + } = {}) => { + const accountsByAddress: Record = { + [controllerAddress]: makeAccountInfoResponse(encodeController(), programAddress), + [strategyAddress]: makeAccountInfoResponse(encodeStrategy(), programAddress), + [accountingAddress]: makeAccountInfoResponse(accountingData, programAddress), + [assetMintAddress]: makeAccountInfoResponse( + encodeMint(1_000_000_000_000_000_000n, mintDecimals), + ), + [juniorMintAddress]: makeAccountInfoResponse(encodeMint(juniorShares, mintDecimals)), + [seniorMintAddress]: makeAccountInfoResponse(encodeMint(seniorShares, mintDecimals)), + [clockSysvarAddress]: makeAccountInfoResponse(encodeClock(unixTimestamp)), + ...overrides, + } + + getMultipleAccountsSendMock.mockImplementation((addresses: string[]) => ({ + value: addresses.map((address) => accountsByAddress[address] ?? null), + })) + } + + beforeEach(async () => { + jest.resetAllMocks() + mockRpcRequests() + + transport = new StrcusxExchangeRateTransport() + + await transport.initialize(dependencies, adapterSettings, endpointName, transportName) + }) + + describe('PDA derivation', () => { + it('should derive expected devnet addresses', async () => { + expect(await deriveAccountAddress(programAddress, [PDA_SEEDS.CONTROLLER])).toBe( + controllerAddress, + ) + expect(await deriveAccountAddress(programAddress, [PDA_SEEDS.STRATEGY, strategyName])).toBe( + strategyAddress, + ) + expect(await deriveAccountAddress(programAddress, [PDA_SEEDS.ACCOUNTING, strategyName])).toBe( + accountingAddress, + ) + }) + }) + + describe('handleRequest', () => { + it('should cache a 502 response when Solana account fetch rejects', async () => { + getMultipleAccountsSendMock.mockRejectedValueOnce(new Error('RPC unavailable')) + + await transport.handleRequest(juniorParam) + + expect(dependencies.responseCache.write).toBeCalledWith(transportName, [ + { + params: juniorParam, + response: { + statusCode: 502, + errorMessage: 'RPC unavailable', + timestamps: { + providerDataRequestedUnixMs: 0, + providerDataReceivedUnixMs: 0, + providerIndicatedTimeUnixMs: undefined, + }, + }, + }, + ]) + expect(dependencies.responseCache.write).toBeCalledTimes(1) + }) + }) + + describe('_handleRequest', () => { + it('should decode recorded real strcUSX account fixtures', async () => { + expect(strcusxAccountFixture.result.value.map((account) => account.space)).toEqual([ + controllerAccountLength, + strategyAccountLength, + accountingAccountLength, + ]) + expect( + strcusxAccountFixture.result.value.map( + (account) => Buffer.from(account.data[0]!, 'base64').length, + ), + ).toEqual([controllerAccountLength, strategyAccountLength, accountingAccountLength]) + + mockAccountData({ + unixTimestamp: 1n, + overrides: { + [controllerAddress]: getFixtureAccountInfo(0), + [strategyAddress]: getFixtureAccountInfo(1), + [accountingAddress]: getFixtureAccountInfo(2), + }, + }) + + const response = await transport._handleRequest(juniorParam) + + expect(response.data).toMatchObject({ + result: '1000000000000000000', + computedResult: '1000000000000000000', + trancheAssets: '450000000', + trancheShares: juniorShares.toString(), + }) + }) + + it('should return the junior tranche exchange rate', async () => { + mockAccountData() + + const response = await transport._handleRequest(juniorParam) + + expect(response).toEqual({ + statusCode: 200, + result: expectedJuniorRate, + data: { + result: expectedJuniorRate, + computedResult: expectedJuniorRate, + tranche: 'junior', + decimals: 18, + boundsApplied: false, + trancheAssets: juniorAssets.toString(), + trancheShares: juniorShares.toString(), + }, + timestamps: { + providerDataRequestedUnixMs: expect.any(Number), + providerDataReceivedUnixMs: expect.any(Number), + providerIndicatedTimeUnixMs, + }, + }) + expect(getMultipleAccountsRequestMock).toBeCalledWith( + [controllerAddress, strategyAddress, accountingAddress, clockSysvarAddress], + { encoding: 'base64' }, + ) + expect(getMultipleAccountsRequestMock).toBeCalledWith([assetMintAddress, juniorMintAddress], { + encoding: 'base64', + }) + }) + + it('should return the senior tranche exchange rate', async () => { + mockAccountData() + + const response = await transport._handleRequest(seniorParam) + + expect(response.result).toBe(expectedSeniorRate) + expect(response.data?.result).toBe(expectedSeniorRate) + expect(response.data?.computedResult).toBe(expectedSeniorRate) + expect(response.data?.tranche).toBe('senior') + expect(response.data?.trancheAssets).toBe(seniorAssets.toString()) + expect(response.data?.trancheShares).toBe(seniorShares.toString()) + expect(getMultipleAccountsRequestMock).toBeCalledWith([assetMintAddress, seniorMintAddress], { + encoding: 'base64', + }) + }) + + it('should decode Token-2022 mint accounts with extension bytes', async () => { + mockAccountData({ + overrides: { + [assetMintAddress]: makeAccountInfoResponse( + encodeMintWithExtensions(1_000_000_000_000_000_000n, mintDecimals), + token2022ProgramAddress, + ), + [juniorMintAddress]: makeAccountInfoResponse( + encodeMintWithExtensions(juniorShares, mintDecimals), + token2022ProgramAddress, + ), + }, + }) + + const response = await transport._handleRequest(juniorParam) + + expect(response.result).toBe(expectedJuniorRate) + expect(response.data?.trancheAssets).toBe(juniorAssets.toString()) + expect(response.data?.trancheShares).toBe(juniorShares.toString()) + }) + + it('should exclude unvested total and senior yield from exchange rates', async () => { + mockAccountData({ + accountingData: encodeAccounting({ + totalAssetsValue: 670_000_000n, + seniorAssetsValue: 208_000_000n, + totalVestingAssetsValue: 20_000_000n, + seniorVestingAssetsValue: 8_000_000n, + vestingStartTimeValue: 1_000n, + vestingEndTimeValue: 3_000n, + }), + unixTimestamp: 2_000n, + }) + + const juniorResponse = await transport._handleRequest(juniorParam) + const seniorResponse = await transport._handleRequest(seniorParam) + + expect(juniorResponse.result).toBe(expectedHalfVestedJuniorRate) + expect(juniorResponse.data?.computedResult).toBe(expectedHalfVestedJuniorRate) + expect(seniorResponse.result).toBe(expectedHalfVestedSeniorRate) + expect(seniorResponse.data?.computedResult).toBe(expectedHalfVestedSeniorRate) + expect(juniorResponse.data?.trancheAssets).toBe('456000000') + expect(juniorResponse.data?.trancheShares).toBe(juniorShares.toString()) + expect(seniorResponse.data?.trancheAssets).toBe('204000000') + expect(seniorResponse.data?.trancheShares).toBe(seniorShares.toString()) + expect(getMultipleAccountsRequestMock).toBeCalledWith( + expect.arrayContaining([clockSysvarAddress]), + { encoding: 'base64' }, + ) + }) + + it('should floor the direct unvested asset calculation during active vesting', async () => { + mockAccountData({ + accountingData: encodeAccounting({ + totalVestingAssetsValue: 10n, + seniorVestingAssetsValue: 4n, + vestingStartTimeValue: 0n, + vestingEndTimeValue: 3n, + }), + unixTimestamp: 1n, + }) + + const response = await transport._handleRequest(seniorParam) + + expect(response.result).toBe('999999990000000000') + expect(response.data?.computedResult).toBe('999999990000000000') + expect(response.data?.trancheAssets).toBe('199999998') + expect(response.data?.trancheShares).toBe(seniorShares.toString()) + }) + + it('should treat non-active vesting schedules as fully vested', async () => { + mockAccountData({ + accountingData: encodeAccounting({ + totalVestingAssetsValue: 20_000_000n, + seniorVestingAssetsValue: 8_000_000n, + vestingStartTimeValue: 3_000n, + vestingEndTimeValue: 1_000n, + }), + unixTimestamp: 2_000n, + }) + + const juniorResponse = await transport._handleRequest(juniorParam) + const seniorResponse = await transport._handleRequest(seniorParam) + + expect(juniorResponse.result).toBe(expectedJuniorRate) + expect(seniorResponse.result).toBe(expectedSeniorRate) + }) + + it('should clamp the exchange rate to minRate', async () => { + mockAccountData() + const minClampedRate = (BigInt(expectedJuniorRate) + 1n).toString() + + const response = await transport._handleRequest({ + ...juniorParam, + minRate: minClampedRate, + }) + + expect(response.result).toBe(minClampedRate) + expect(response.data?.result).toBe(minClampedRate) + expect(response.data?.computedResult).toBe(expectedJuniorRate) + expect(response.data?.boundsApplied).toBe(true) + expect(log).toHaveBeenCalledWith( + { + tranche: 'junior', + computedResult: expectedJuniorRate, + result: minClampedRate, + minRate: minClampedRate, + maxRate, + }, + 'strcUSX exchange rate bounds applied', + ) + }) + + it('should clamp the exchange rate to maxRate', async () => { + mockAccountData() + const maxClampedRate = (BigInt(expectedJuniorRate) - 1n).toString() + + const response = await transport._handleRequest({ + ...juniorParam, + maxRate: maxClampedRate, + }) + + expect(response.result).toBe(maxClampedRate) + expect(response.data?.result).toBe(maxClampedRate) + expect(response.data?.computedResult).toBe(expectedJuniorRate) + expect(response.data?.boundsApplied).toBe(true) + }) + + it('should error when account state is inconsistent', async () => { + mockAccountData({ + overrides: { + [accountingAddress]: makeAccountInfoResponse( + encodeAccounting({ totalAssetsValue: seniorAssets - 1n }), + programAddress, + ), + }, + }) + + await expect(transport._handleRequest(juniorParam)).rejects.toThrow( + 'vested totalAssets must be greater than or equal to vested seniorAssets', + ) + }) + + it('should error when selected tranche shares are zero', async () => { + mockAccountData({ + overrides: { + [accountingAddress]: makeAccountInfoResponse( + encodeAccounting({ juniorSharesValue: 0n }), + programAddress, + ), + }, + }) + + await expect(transport._handleRequest(juniorParam)).rejects.toThrow( + 'junior tranche shares are zero', + ) + }) + }) +}) diff --git a/packages/sources/solana-functions/test/unit/stslx-exchange-rate.test.ts b/packages/sources/solana-functions/test/unit/stslx-exchange-rate.test.ts new file mode 100644 index 00000000000..f6acac6c9fd --- /dev/null +++ b/packages/sources/solana-functions/test/unit/stslx-exchange-rate.test.ts @@ -0,0 +1,271 @@ +import { TransportDependencies } from '@chainlink/external-adapter-framework/transports' +import { LoggerFactoryProvider } from '@chainlink/external-adapter-framework/util' +import { makeStub } from '@chainlink/external-adapter-framework/util/testing-utils' +import { + AccountLayout, + MintLayout, + TOKEN_2022_PROGRAM_ID, + TOKEN_PROGRAM_ID, +} from '@solana/spl-token' +import { PublicKey } from '@solana/web3.js' +import { + BaseEndpointTypes, + DEFAULT_GLAM_PROTOCOL_PROGRAM_ADDRESS as GLAM_PROTOCOL_PROGRAM_ADDRESS, + DEFAULT_GLAM_STATE_ADDRESS as GLAM_STATE_ADDRESS, + DEFAULT_SLX_MINT_ADDRESS as SLX_MINT_ADDRESS, + DEFAULT_STSLX_MINT_ADDRESS as STSLX_MINT_ADDRESS, +} from '../../src/endpoint/stslx-exchange-rate' +import { StslxExchangeRateTransport } from '../../src/transport/stslx-exchange-rate' + +const tokenProgramAddress = TOKEN_PROGRAM_ID.toBase58() +const token2022ProgramAddress = TOKEN_2022_PROGRAM_ID.toBase58() +const GLAM_VAULT_ADDRESS = 'GMwdh2jTdTrrhA7dMR7Cc2zC6gV38UePzAXeoFHrXnfH' +const SLX_TOKEN_ACCOUNT_ADDRESS = '7CssRFNePpnDiCzjRC5kPRDpEJn87JMeDG7s6Gww9CTf' + +const encodeMint = (supply: bigint, decimals: number) => { + const buffer = Buffer.alloc(MintLayout.span) + MintLayout.encode( + { + mintAuthorityOption: 0, + mintAuthority: PublicKey.default, + supply, + decimals, + isInitialized: true, + freezeAuthorityOption: 0, + freezeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const encodeTokenAccount = ( + amount: bigint, + mintAddress = SLX_MINT_ADDRESS, + ownerAddress = GLAM_VAULT_ADDRESS, +) => { + const buffer = Buffer.alloc(AccountLayout.span) + AccountLayout.encode( + { + mint: new PublicKey(mintAddress), + owner: new PublicKey(ownerAddress), + amount, + delegateOption: 0, + delegate: PublicKey.default, + state: 1, + isNativeOption: 0, + isNative: 0n, + delegatedAmount: 0n, + closeAuthorityOption: 0, + closeAuthority: PublicKey.default, + }, + buffer, + ) + + return buffer.toString('base64') +} + +const makeAccountInfo = (data: string, owner = tokenProgramAddress) => ({ + data: [data, 'base64'], + owner, +}) + +const getMultipleAccountsSendMock = jest.fn() +const getMultipleAccountsRequestMock = jest.fn() + +const mockRpcRequests = () => { + getMultipleAccountsRequestMock.mockImplementation( + (addresses: string[], config: { encoding: string }) => ({ + send() { + return getMultipleAccountsSendMock(addresses, config) + }, + }), + ) +} + +const solanaRpc = makeStub('solanaRpc', { + getMultipleAccounts: getMultipleAccountsRequestMock, +}) + +const createSolanaRpc = () => solanaRpc + +jest.mock('@solana/rpc', () => ({ + createSolanaRpc() { + return createSolanaRpc() + }, +})) + +const log = jest.fn() +const logger = { + fatal: log, + error: log, + warn: log, + info: log, + debug: log, + trace: log, + msgPrefix: 'mock-logger', +} + +const loggerFactory = { child: () => logger } + +LoggerFactoryProvider.set(loggerFactory) + +describe('StslxExchangeRateTransport', () => { + const transportName = 'default_single_transport' + const endpointName = 'stslx-exchange-rate' + const RPC_URL = 'https://solana.rpc.url' + const slxBalance = 1_500_000_000n + const stslxSupply = 1_000_000n + const slxMintDecimals = 9 + const stslxMintDecimals = 6 + const minRate = '1000000000000000000' + const maxRate = '2000000000000000000' + const expectedRate = '1500000000000000000' + + const adapterSettings = makeStub('adapterSettings', { + RPC_URL, + SOLANA_COMMITMENT: 'finalized', + WARMUP_SUBSCRIPTION_TTL: 10_000, + BACKGROUND_EXECUTE_MS: 1500, + MAX_COMMON_KEY_SIZE: 300, + } as unknown as BaseEndpointTypes['Settings']) + + const dependencies = makeStub('dependencies', { + responseCache: { write: jest.fn() }, + subscriptionSetFactory: { + buildSet: jest.fn(), + }, + } as unknown as TransportDependencies) + + const param = makeStub('param', { + endpoint: 'stslx-exchange-rate', + slxMintAddress: SLX_MINT_ADDRESS, + stslxMintAddress: STSLX_MINT_ADDRESS, + glamStateAddress: GLAM_STATE_ADDRESS, + glamProtocolProgramAddress: GLAM_PROTOCOL_PROGRAM_ADDRESS, + minRate, + maxRate, + }) + + let transport: StslxExchangeRateTransport + + type AccountInfo = ReturnType | null + + const mockAccountData = (overrides: Record = {}) => { + const accountsByAddress: Record = { + [SLX_MINT_ADDRESS]: makeAccountInfo(encodeMint(100_000_000_000n, slxMintDecimals)), + [STSLX_MINT_ADDRESS]: makeAccountInfo( + encodeMint(stslxSupply, stslxMintDecimals), + token2022ProgramAddress, + ), + [SLX_TOKEN_ACCOUNT_ADDRESS]: makeAccountInfo(encodeTokenAccount(slxBalance)), + ...overrides, + } + + getMultipleAccountsSendMock.mockImplementation((addresses: string[]) => ({ + value: addresses.map((address) => accountsByAddress[address] ?? null), + })) + } + + beforeEach(async () => { + jest.resetAllMocks() + mockRpcRequests() + + transport = new StslxExchangeRateTransport() + + await transport.initialize(dependencies, adapterSettings, endpointName, transportName) + }) + + describe('_handleRequest', () => { + it('should read all accounts atomically and return the normalized exchange rate', async () => { + mockAccountData() + + const response = await transport._handleRequest(param) + + expect(response).toEqual({ + statusCode: 200, + result: expectedRate, + data: { + result: expectedRate, + computedResult: expectedRate, + decimals: 18, + boundsApplied: false, + slxBalance: slxBalance.toString(), + stslxSupply: stslxSupply.toString(), + }, + timestamps: { + providerDataRequestedUnixMs: expect.any(Number), + providerDataReceivedUnixMs: expect.any(Number), + providerIndicatedTimeUnixMs: undefined, + }, + }) + expect(getMultipleAccountsRequestMock).toBeCalledWith( + [SLX_MINT_ADDRESS, STSLX_MINT_ADDRESS, SLX_TOKEN_ACCOUNT_ADDRESS], + { encoding: 'base64' }, + ) + }) + + it('should clamp the exchange rate to minRate', async () => { + mockAccountData() + const minClampedRate = (BigInt(expectedRate) + 1n).toString() + + const response = await transport._handleRequest({ + ...param, + minRate: minClampedRate, + }) + + expect(response.result).toBe(minClampedRate) + expect(response.data?.result).toBe(minClampedRate) + expect(response.data?.computedResult).toBe(expectedRate) + expect(response.data).not.toHaveProperty('minRate') + expect(response.data?.boundsApplied).toBe(true) + expect(log).toHaveBeenCalledWith( + { + computedResult: expectedRate, + result: minClampedRate, + minRate: minClampedRate, + maxRate, + }, + 'stSLX exchange rate bounds applied', + ) + }) + + it('should clamp the exchange rate to maxRate', async () => { + mockAccountData() + const maxClampedRate = (BigInt(expectedRate) - 1n).toString() + + const response = await transport._handleRequest({ + ...param, + maxRate: maxClampedRate, + }) + + expect(response.result).toBe(maxClampedRate) + expect(response.data?.result).toBe(maxClampedRate) + expect(response.data?.computedResult).toBe(expectedRate) + expect(response.data).not.toHaveProperty('maxRate') + expect(response.data?.boundsApplied).toBe(true) + }) + + it('should error when the stSLX mint has zero supply', async () => { + mockAccountData({ + [STSLX_MINT_ADDRESS]: makeAccountInfo( + encodeMint(0n, stslxMintDecimals), + token2022ProgramAddress, + ), + }) + + await expect(transport._handleRequest(param)).rejects.toThrow('has zero supply') + }) + + it('should error when the derived SLX base-asset ATA is missing', async () => { + mockAccountData({ + [SLX_TOKEN_ACCOUNT_ADDRESS]: null, + }) + + await expect(transport._handleRequest(param)).rejects.toThrow( + `No account data found for SLX token account '${SLX_TOKEN_ACCOUNT_ADDRESS}'`, + ) + }) + }) +}) diff --git a/yarn.lock b/yarn.lock index 9a14e6695d9..5dce8dd6b67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4591,7 +4591,7 @@ __metadata: version: 0.0.0-use.local resolution: "@chainlink/solana-functions-adapter@workspace:packages/sources/solana-functions" dependencies: - "@chainlink/external-adapter-framework": "npm:2.16.1" + "@chainlink/external-adapter-framework": "npm:2.17.1" "@coral-xyz/anchor": "npm:^0.31.1" "@solana/addresses": "npm:^3.0.2" "@solana/buffer-layout": "npm:^4.0.1" @@ -4599,6 +4599,7 @@ __metadata: "@solana/rpc": "npm:^3.0.2" "@solana/spl-stake-pool": "npm:^1.1.8" "@solana/spl-token": "npm:^0.4.14" + "@solana/sysvars": "npm:3.0.2" "@solana/web3.js": "npm:^1.95.8" "@types/bn.js": "npm:^5.1.0" "@types/jest": "npm:^29.5.14" @@ -7962,6 +7963,22 @@ __metadata: languageName: node linkType: hard +"@solana/accounts@npm:3.0.2": + version: 3.0.2 + resolution: "@solana/accounts@npm:3.0.2" + dependencies: + "@solana/addresses": "npm:3.0.2" + "@solana/codecs-core": "npm:3.0.2" + "@solana/codecs-strings": "npm:3.0.2" + "@solana/errors": "npm:3.0.2" + "@solana/rpc-spec": "npm:3.0.2" + "@solana/rpc-types": "npm:3.0.2" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/a850f48091a3ae5b7698025ae91193332f72ebaf9dbc90b04557b35e5550d2a68ced399490f400e711eca59cdf0f5feefb2f858120bdd5a720b89cce2358c6bf + languageName: node + linkType: hard + "@solana/accounts@npm:6.9.0": version: 6.9.0 resolution: "@solana/accounts@npm:6.9.0" @@ -8325,6 +8342,21 @@ __metadata: languageName: node linkType: hard +"@solana/codecs@npm:3.0.2": + version: 3.0.2 + resolution: "@solana/codecs@npm:3.0.2" + dependencies: + "@solana/codecs-core": "npm:3.0.2" + "@solana/codecs-data-structures": "npm:3.0.2" + "@solana/codecs-numbers": "npm:3.0.2" + "@solana/codecs-strings": "npm:3.0.2" + "@solana/options": "npm:3.0.2" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/967acb8199bfdeb2c8f16d3f9450332e2c2de9229c4b77e05d79c6cbfa44b614bc390068a5dd6cb685b008ce15bc27db099346465c11a95b4f46099c6500b429 + languageName: node + linkType: hard + "@solana/codecs@npm:6.9.0": version: 6.9.0 resolution: "@solana/codecs@npm:6.9.0" @@ -8658,6 +8690,21 @@ __metadata: languageName: node linkType: hard +"@solana/options@npm:3.0.2": + version: 3.0.2 + resolution: "@solana/options@npm:3.0.2" + dependencies: + "@solana/codecs-core": "npm:3.0.2" + "@solana/codecs-data-structures": "npm:3.0.2" + "@solana/codecs-numbers": "npm:3.0.2" + "@solana/codecs-strings": "npm:3.0.2" + "@solana/errors": "npm:3.0.2" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/7ff8e1af979f28bf498698ad1d7b11d7e632307101764c675bb80779978bc98be290260b82fc6697aa1578812b1f22fb08a1b5b5447c2c18813a352fe1d68609 + languageName: node + linkType: hard + "@solana/options@npm:6.9.0": version: 6.9.0 resolution: "@solana/options@npm:6.9.0" @@ -9194,6 +9241,20 @@ __metadata: languageName: node linkType: hard +"@solana/sysvars@npm:3.0.2": + version: 3.0.2 + resolution: "@solana/sysvars@npm:3.0.2" + dependencies: + "@solana/accounts": "npm:3.0.2" + "@solana/codecs": "npm:3.0.2" + "@solana/errors": "npm:3.0.2" + "@solana/rpc-types": "npm:3.0.2" + peerDependencies: + typescript: ">=5.3.3" + checksum: 10/423b799039dfaf319e74a3abe170c2358043ffa460e6bd1e776fea91b049663bb824dbe5bcc0c160c454e4aa644690e311884e2118064a22e6b2fdc3a11cf095 + languageName: node + linkType: hard + "@solana/sysvars@npm:6.9.0": version: 6.9.0 resolution: "@solana/sysvars@npm:6.9.0"