diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 7f11afb385..c601038141 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -2,17 +2,18 @@ ## Definition of Done -1. [ ] Acceptance criteria are met. -2. [ ] PR is manually tested before the merge by developer(s). +1. [ ] If required, the desciption of your change is added to the [QA changelog](https://www.notion.so/octantapp/Changelog-for-the-QA-d96fa3b411cf488bb1d8d9a598d88281) +2. [ ] Acceptance criteria are met. +3. [ ] PR is manually tested before the merge by developer(s). - [ ] Happy path is manually checked. -3. [ ] PR is manually tested by QA when their assistance is required (1). +4. [ ] PR is manually tested by QA when their assistance is required (1). - [ ] Octant Areas & Test Cases are checked for impact and updated if required (2). -4. [ ] Unit tests are added unless there is a reason to omit them. -5. [ ] Automated tests are added when required. -6. [ ] The code is merged. -7. [ ] Tech documentation is added / updated, reviewed and approved (including mandatory approval by a code owner, should such exist for changed files). +5. [ ] Unit tests are added unless there is a reason to omit them. +6. [ ] Automated tests are added when required. +7. [ ] The code is merged. +8. [ ] Tech documentation is added / updated, reviewed and approved (including mandatory approval by a code owner, should such exist for changed files). - [ ] BE: Swagger documentation is updated. -8. [ ] When required by QA: +9. [ ] When required by QA: - [ ] Deployed to the relevant environment. - [ ] Passed system tests. diff --git a/.github/workflows/deploy-pr.yml b/.github/workflows/deploy-pr.yml index a8703199ff..3c27a7d756 100644 --- a/.github/workflows/deploy-pr.yml +++ b/.github/workflows/deploy-pr.yml @@ -41,6 +41,7 @@ jobs: anvil-block-time: 5 decision-window: 1800 epoch-duration: 3600 + ipfs-gateways: 'https://ipfs.octant.wildland.dev/ipfs/' secrets: inherit run: diff --git a/.github/workflows/e2e-run.yml b/.github/workflows/e2e-run.yml index e0b1321f7b..ac979b838d 100644 --- a/.github/workflows/e2e-run.yml +++ b/.github/workflows/e2e-run.yml @@ -72,6 +72,7 @@ jobs: glm-claim-enabled: true vault-confirm-withdrawals-enabled: true anvil-block-time: 5 + ipfs-gateways: 'https://ipfs.octant.wildland.dev/ipfs/' secrets: inherit run-e2e-tests: diff --git a/.github/workflows/tpl-deploy-app.yml b/.github/workflows/tpl-deploy-app.yml index b009f240db..157ef536bf 100644 --- a/.github/workflows/tpl-deploy-app.yml +++ b/.github/workflows/tpl-deploy-app.yml @@ -118,6 +118,10 @@ on: required: false default: true type: boolean + ipfs-gateways: + required: false + default: 'https://turquoise-accused-gayal-88.mypinata.cloud/ipfs/,https://octant.infura-ipfs.io/ipfs/' + type: string env: ENV_TYPE: ${{ inputs.env-type }} @@ -140,7 +144,7 @@ env: BACKEND_SERVER_REPLICAS: ${{ inputs.backend-server-replicas }} OCTANT_ENV: ${{ inputs.octant-env }} GLM_SENDER_NONCE: ${{ vars.GLM_SENDER_NONCE }} - MAINNET_PROPOSAL_CIDS: "QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK,Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe,QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V" + MAINNET_PROPOSAL_CIDS: "QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK,Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe,QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V,QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf" # ---------------------------------------------------------------------------- # CONTRACTS / ANVIL / SUBGRAPH / SYNPRESS NETWORK: ${{ inputs.chain-name }} @@ -152,14 +156,15 @@ env: SKIP_LOCAL_SUBGRAPH_UPDATE: ${{ inputs.skip-local-subgraph-update }} ANVIL_BLOCK_TIME: ${{ inputs.anvil-block-time }} SECRET_WORDS: test test test test test test test test test test test junk - PROPOSALS_ADDRESSES: 0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95,0x0c9dc7622aE5f56491aB4cCe060d6002450B79D2,0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29,0x1c01595f9534E33d411035AE99a4317faeC4f6Fe,0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953,0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9,0x4C6fd545fc18C6538eC304Ae549717CA58f0D6eb,0x7Dd488f03E0A043b550E82D3C2685aA83B96407C,0x9cce47E9cF12C6147c9844adBB81fE85880c4df4,0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f,0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05,0x242ba6d68FfEb4a098B591B32d370F973FF882B7,0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20,0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714,0x3250c2CEE20FA34D1c4F68eAA87E53512e95A62a,0x5597cD8d55D2Db56b10FF4F8fe69C8922BF6C537,0x7380A42137D16a0E7684578d8b3d32e1fbD021B5,0x9438b8B447179740cD97869997a2FCc9b4AA63a2,0x9531C059098e3d194fF87FebB587aB07B30B1306,0x00080706a7D99CBC163D52dcF435205B1aD940D1,0x6612213880f80b298aB66375789E8Ef15e98604E,0x53390590476dC98860316e4B46Bb9842AF55efc4,0xB476Ee7D610DAe7B23B671EBC7Bd6112E9772969,0xBCA48834b3653ec795411EB0FCBE4038F8527d62,0xc6FD734790E83820e311211B6d9A682BCa4ac97b,0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1,0xe126b3E5d052f1F575828f61fEBA4f4f2603652a,0xF01CEe26213d1A6eaF16422241AE81f7C17B9f98,0xF41a98D4F2E52aa1ccB48F0b6539e955707b8F7a,0xf7253A0E87E39d2cD6365919D4a3D56D431D0041 - PROPOSALS_CID: QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf + PROPOSALS_ADDRESSES: 0x09A38B6187a2c44B6ba71c277c50764B5878b824,0x5597cD8d55D2Db56b10FF4F8fe69C8922BF6C537,0x3250c2CEE20FA34D1c4F68eAA87E53512e95A62a,0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95,0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f,0xe7d4Ac3c77cF3683E0d15C15eaba7CDB8c092D98,0xa83a92297B3d80A70cC396bf74424971A9890704,0x7380A42137D16a0E7684578d8b3d32e1fbD021B5,0x53390590476dC98860316e4B46Bb9842AF55efc4,0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20,0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714,0x4C6fd545fc18C6538eC304Ae549717CA58f0D6eb,0xfFbD35255008F86322051F2313D4b343540e0e00,0x9be7267002CAD0b8501f7322d50612CB13788Bcf,0x7Dd488f03E0A043b550E82D3C2685aA83B96407C,0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9,0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1,0x08e40e1C0681D072a54Fc5868752c02bb3996FFA,0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29,0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953,0x9531C059098e3d194fF87FebB587aB07B30B1306,0xBCA48834b3653ec795411EB0FCBE4038F8527d62,0xa095Ee27B11FCAac8e1be84891ab62C74F08C854,0xF41a98D4F2E52aa1ccB48F0b6539e955707b8F7a,0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05,0x1337E2624ffEC537087c6774e9A18031CFEAf0a9,0x00080706a7D99CBC163D52dcF435205B1aD940D1,0x992A3a242D6471d24783b4C2C6AF3EC7df871761,0xFC1436689F68079Fc17A931666b7947789229ed8,0xfcBf17200C64E860F6639aa12B525015d115F863 + PROPOSALS_CID: QmdtFLK3sB7EwQTNaqtmBnZqnN2pYZcu6GmUSTrpvb9wcq MULTISIG_ADDRESS: "${{ vars.MULTISIG_ADDRESS }}" TESTNET_DEPLOYER_PRIVATE_KEY: "${{ secrets.TESTNET_DEPLOYER_PRIVATE_KEY }}" TESTNET_MULTISIG_PRIVATE_KEY: "${{ secrets.TESTNET_MULTISIG_PRIVATE_KEY }}" TESTNET_RPC_URL: "${{ secrets.TESTNET_RPC_URL }}" ETHERSCAN_API_KEY: "${{ secrets.ETHERSCAN_API_KEY }}" VITE_ALCHEMY_ID: "${{ secrets.VITE_ALCHEMY_ID }}" + IPFS_GATEWAYS: "${{ inputs.ipfs-gateways }}" MULTIDEPLOYER_ENABLED: ${{ inputs.multideployer-enabled }} SUBGRAPH_DEPLOY: ${{ inputs.subgraph-deploy }} GRAPH_HEALTCHECKER_ENABLED: ${{ inputs.graph-healtchecker-enabled }} diff --git a/backend/README.md b/backend/README.md index 5f56dc3565..f83fbd0747 100644 --- a/backend/README.md +++ b/backend/README.md @@ -64,7 +64,7 @@ yarn apitest:run # in a second console When backend code is changed, just re-run `yarn apitest:run`. To run just one test, use standard pytest naming: ``` -yarn apitest:run tests/legacy/test_api_snapshot.py::test_pending_snapshot +yarn apitest:run tests/api-e2e/test_api_snapshot.py::test_pending_snapshot ``` To stop the env, run `yarn apitest:down` diff --git a/backend/app/__init__.py b/backend/app/__init__.py index 1608020af3..c5000845ff 100644 --- a/backend/app/__init__.py +++ b/backend/app/__init__.py @@ -7,7 +7,6 @@ db, migrate, cors, - socketio, cache, init_web3, api, @@ -47,7 +46,8 @@ def register_extensions(app): cors.init_app(app) db.init_app(app) migrate.init_app(app, db) - socketio.init_app(app) + # This is meant to be disabled because we migrate to FastAPI + # socketio.init_app(app) cache.init_app(app) init_scheduler(app) init_logger(app) diff --git a/backend/app/constants.py b/backend/app/constants.py index a30e418d9d..d62dcecfac 100644 --- a/backend/app/constants.py +++ b/backend/app/constants.py @@ -12,10 +12,11 @@ VALIDATOR_DEPOSIT_GWEI = 32_000000000 ZERO_ADDRESS = "0x0000000000000000000000000000000000000000" MR_FUNDING_CAP_PERCENT = Decimal("0.2") -LOW_UQ_SCORE = Decimal("0.2") +LOW_UQ_SCORE = Decimal("0.01") MAX_UQ_SCORE = Decimal("1.0") +NULLIFIED_UQ_SCORE = Decimal("0.0") UQ_THRESHOLD_NOT_MAINNET = 5 -UQ_THRESHOLD_MAINNET = 20 +UQ_THRESHOLD_MAINNET = 15 BEACONCHAIN_API = "https://beaconcha.in/api" ETHERSCAN_API = "https://api.etherscan.io/api" @@ -24,7 +25,7 @@ SAFE_API_MAINNET = "https://safe-transaction-mainnet.safe.global/api/v1" SAFE_API_SEPOLIA = "https://safe-transaction-sepolia.safe.global/api/v1" -DEFAULT_MAINNET_PROJECT_CIDS = "QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK,Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe,QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V" +DEFAULT_MAINNET_PROJECT_CIDS = "QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK,Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe,QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V,QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf" EPOCH0_SYBILS = [ "0xde19a6ce83cc934e5d4c4573f0f026c02c984fb2", @@ -87,478 +88,901 @@ "0xea809d3fb969d1d4de90c022c34b075b1fa5ec50", ] -GUEST_LIST = set( - [ - "0x16f3f2f0ba34973937A1ebb989a295Ca106b67C7", - "0xBB5935dAaFbacAE82c8D2CA8377F16073D70061a", - "0xba84B5cA750b33DfAdDBFdD1B7C6887885a34977", - "0x4e9A05226993F094A56A3472C8c816F2599423A6", - "0x40DE3299Bd8a10D8Ac3f32C1A55DE40640cF9B75", - "0xC33F87697EF41e0E95e7a55d1ec8180F04088578", - "0xCBc924183Bc32D02746Fa8D38843B5Ce08662eB4", - "0xDc9C5e34959eC3643AF1e1D34A83D6b251AAb1eF", - "0x762BBc211990D0a356F35E4D500843F59d223C2e", - "0x55187a1165EBB441A1BF227fff1EB0D32a65bc46", - "0x7aE59f3F2B2E5f3842B50a15bCb5247c5De881Be", - "0x59072B3a3287F4a75cadfb36D671A2f0d1959B09", - "0x4A5da2a1D3258dF8FFb431Cf0110FE9b98ADeEbf", - "0x514A9771Af8Afe71057666b680238dFBeA578d65", - "0xE70055e9575f15A6f51F3068901D73ac63952adF", - "0x9e831B58001e2b69F70C892e4F8ce9d2118B7E00", - "0x51c1C7f1e168a36Bf1FaBFD91E98b43476a6B14D", - "0x33878e070db7f70D2953Fe0278Cd32aDf8104572", - "0x3df13B9bd79158f0cccDDd0833cF774178e3d2e9", - "0xB9573982875b83aaDc1296726E2ae77D13D9B98F", - "0xE862E2C1ca94eAcfEDe3c95a217c15EF0086a29D", - "0x0442A9aBbc93058a873c371F21CC366338254A88", - "0x0194325BF525Be0D4fBB0856894cEd74Da3B8356", - "0x399e0Ae23663F27181Ebb4e66Ec504b3AAB25541", - "0x9f729294b308f79243285348A7Be3f58ae5ED31A", - "0xb62E762Af637b49Eb4870BCe8fE21bffF189e495", - "0x5725a458b319d73B8Ec84c47de80620E7B191B0C", - "0x57Ccc081824b43B75986727875929AF3A6Ad721C", - "0xf13e477365B0FAa64130DA2FF663aAb20d32d929", - "0xFD868dB0696ef762351F8421535cC5f9F423B23C", - "0x30043aAbBCeBbD887437Ec4F0Cfe6d4c0eB5CC64", - "0xAa01DeC5307CF17F20881A3286dcaA062578cea7", - "0x3FFD0C300fa4a021364Ae7e85a7b0d3a02133f99", - "0xBEa26DE685Ef828b60cA53b40Ecc9Bab35645fDF", - "0x4103CFcb300599dFcB31dBc95d919592619B4EAc", - "0x22bAac1E95efC010E35D5eD643BB16c9dB254a11", - "0x686A484bc2E2bE79f358c7055e8539A69413A3Ed", - "0x073a360C372FD51Bd6E56B4a4d73790fDAec4641", - "0xdd0206010CA82fF22303b58863b3a6f3006C86C4", - "0x25FA68A4c340202737EDBC67fD1a2Ec8DE872dB6", - "0x5b655EDa7D101f98934392Cc3610BcB25b633789", - "0x32cEfb2dC869BBfe636f7547CDa43f561Bf88d5A", - "0xA4369e39e3ED13593Adb0142A1ea5d08AbdF99C4", - "0xA8F0048A0d1A04663Ca5010d0bEaC5BCAEeA0eef", - "0x65F632cfe8015B7ae6976e549645ed04cde60fe4", - "0xb35E0a0D00c640ab75fAD3cf3B83264bC64D23eC", - "0xafA3E6E29D99337b166b83fB24bA17b19764B49D", - "0x57DD1517c12659365E59F71129Fa9B1611Dd18AF", - "0x9120FfD5d04ca4B26AaBCe611989A8F026dc099a", - "0x2AC6A3561a43f06d62602eF9728C2B9eEc393326", - "0x297Aa50D0557c865F6C9B0AA0a91f41C26E55eE6", - "0x9Ff46343d0b652D6e766F85f9aE91653869349a5", - "0xEd36bf0b2b17768E782Db2ece6A327055b2f3e9C", - "0xC28D2fDFE6d5a482d32f855457Bb5F8cAcdB32b1", - "0x1d44404C1C53991Ec33095225da173d544Cd4Af3", - "0x5d9fbd984B9CeC714a4B14c38Ea83bBC82d06d69", - "0x5Bc0AEbdbab698e12FD33A2E133e6858fe6Cdd76", - "0x66805D8B82664Acab4CbE0C0498889dDE9aF7841", - "0xaF7610578F54c7De7563655AaF461E2CbeCB08C6", - "0x6c3F373Baec5D2d0Fb3C82C4f3Db5E48873ae363", - "0x015122A625b45f68E6D795C0Ab99fC7107e4c3B9", - "0x508A4F07B60BA0940283Cd4e32d5DEb0CC38AdF7", - "0xb150c9bEd10a8C62997d58a81c4e1fA75160643e", - "0x212647c56BA10ee429a838bc567dFb03A8D054Ba", - "0x73306dAb0D39A4D47df4972c7022CB2cac075D4e", - "0x914D5d84aAA064207C2c31014426227405edab41", - "0x3FBcEC42405391B1fb377664daA5AE7Bc9Ba7BF5", - "0x8c8296a0042E842Cb865DfFD94678c941fD24bAE", - "0xf5c2087877218AA979Dd0e2e5108837199aF44D2", - "0x529dc928E67D8A43133D10769B308F1D5A629401", - "0xF1bb436c29E46B1987bC825879ffc9c34Ab97f99", - "0xFDDE7aE208B3596f1982D66F6BAe4cDabF29244b", - "0x02e4Cc9ffF7566563618fb21B3BB10Eab4B3D726", - "0xd8821dbbcb8ea0c14Bd1F0aCbFBeBB3Fd984269b", - "0x31d23825aFbda5B6B1690Bbdbbb8117B5ea0f8E4", - "0x731022D6De647991864203a35dFaD1A192240d07", - "0xEb5e0B8e80FCe271c13F533fA728D7bB03cafa4c", - "0xFC967DE4e029fdcD16B418DaC2147d282C93085b", - "0x801a6d6dBC1e40466E131aA21D951629A9efAB4e", - "0x4892139De0e73141438D9E55D593171C0Cc6B143", - "0x8124eFC94c951cF41D4B0B42794C678458a00726", - "0x81cc36DdA894256aa95458F78B4029381b09BDfb", - "0x4Dcb2BCA3450B427F3d1b424C885259D05363080", - "0x7Dd8030F9d33Af4a40ee074f990892E825132e61", - "0x432C53218A11bEd08d238Cf84ff547CE4fe933ab", - "0xE77ad9c5af60332D24E5531B51A6B7f61D0B8703", - "0x0f792e55668AD78476d4B563E6EB1228D636a71e", - "0x583bBaDA56bb535BCBb31877A620A6ff2A25CeA5", - "0x5C0E777dC8F3De6b0911b44DbBDD8Bf71b2E8e38", - "0x8a4a50B13Fd2cb36FeB96c408CB98B4c9F2b8F25", - "0x1e55C85801a2C4F0beC57c84742a8eF3d72dE57B", - "0x26d3bE736aB6b5D8A3266fFCC0895dDc1bc19a38", - "0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714", - "0xfB94f39B150Ae661F85762154c0CadC65E083791", - "0x4B7C0Da1C299Ce824f55A0190Efb13663442FA2c", - "0x77E64560Bd6C323c075F206a5AB9dD6850F31609", - "0x0F46540678c7e6D2eF983B382CC07Fa815AB148c", - "0x82073f802547fEeEc0fd49719a3D7697fB66076a", - "0xBAab83De8DbA764bF02a530cad33555bD23eba22", - "0x890a0047f8D573347872cB6C019F86552f2367d6", - "0x14D92832265eeAFDEF9e526356FEfc90105966c3", - "0x512B436cB2Ed6016e80d4F89ca578F99DBBccb61", - "0x696Ee4AE0b15feae8ED1AfC865930e0ea65b1f3F", - "0xbb4D885fD41c807e8eCC2dD9e6295a7F96Adb0EB", - "0xB1dE969883b1FdD90a43fF475A5171a3CfEfe76d", - "0x7DBF6820D32cFBd5D656bf9BFf0deF229B37cF0E", - "0xfE2e3cCEE9714b29Ab2FB4E940e52672194815fC", - "0x57fb3f4b027fbaDbd8d20Eb5E48feb1e2b02DF30", - "0x9AE494FBAc34682c122A1D4e3D6ae1Eb5404A469", - "0xb2b9300475aF157676C44eE64d39a5eB3C294DbD", - "0x01Bc28E036b6e75247Fe8F49f0a8b9410b19d851", - "0xcCE9A28b570946123f392Cf1DbfA6D2D5e636a1f", - "0xb2a3b5B9d2C0f07cBA328b58737147cfc172EB9f", - "0xCC3d7F9fE6946979215A901BbA385a88FdabBBf4", - "0x38f80f8f76B1C44B2beeFB63bb561F570fb6ddB6", - "0xd82803b7B9A5EB1D5FC558FD619afC6c031cd0B1", - "0x844AeeD1B294Ef9632c18E73F57ef77D0A23D0e2", - "0x9cD7D1981B3e15a2DEE4d512ac60E0579Ae18546", - "0xEBCd250474C27cBaD3C56f3F34e08F97b370AC2d", - "0xDA47bdcC48f26FB4709f90316341D9104cB1fb89", - "0x5cbB6ad79008908aA125667D1300558D9253B589", - "0x1078DaA844CDF1EDB51E5189c8b113B80a6A6957", - "0x8341c4106523b49fc247f84e412Bb2AF5597038f", - "0xCe57ebEd9aC38402DcAA44f65a1c9b04e26b8283", - "0x2dd2036C9Db2ADA2739509AF0047c00C8b9291EF", - "0xa77294828d42B538890fa6E97AdFfE9305536171", - "0x8dA48e5846c06B558970ACd42EDc7Da8799481E4", - "0x50418699cB44BfDa9c9afc9B7a0b0d244d8927D2", - "0x936d69AbCD9acdC89455EEFAf744044fFC1CA660", - "0x90C32e6B29794Fd7f5BbA2BBEE74e924078B3f9b", - "0x362B7e0599E950b921ca9D86336ca409208FFDEC", - "0xd98aD1Fd4aa0E1c876d91968D1385aa9E1Aa98df", - "0xD2602C7bDFC9F413974e944280BbFae275d1B1b6", - "0x731A2e51ebfAeBacF8477E992CDEB1E8eacf519C", - "0x072d63796C4FE69B306a23E1D01156d51F7B3e16", - "0x051010142A0B9de7F0Fd8fb31d085407287F6381", - "0x8498843f6D9046f7b59482978E152D61869203bC", - "0xB48ef8e4e7Bef79ddF64d4424151f003a59BfbfB", - "0xb423A138fD171c28d90A5883A01ec92fF3D63609", - "0xAfA3a2528E8baAd576a83ffC52dB9f100dEbe307", - "0x055fdA7Eb509cc338C898b0F698B7624387AB813", - "0x0B3BD83E857997b370FaDC8504fB712244F6786C", - "0x8D12A71Cb933A4222d42feCBb4ba9c15e455305b", - "0xDEF3D19ff35a42F5b8E3c706c8fD287De72e6D15", - "0x19a2BC678785BAD6A947A87494D480DAD57711c6", - "0x2c3E79D3DCE90FB0886C89Ec602E61757E589a94", - "0xe8aa836a597a66724D678860D105561c13E95bFa", - "0x3352a3277d2B74A773Fa6E68a625FcB18E4Fc282", - "0x2df292AF809Fd693D94C7D17E36BE352e15Bb98a", - "0x269Aa10398Aaa695259C3E8211ab27a15004110C", - "0x02d9c84a495986b8b3C3347Ad16849DCB1b9793e", - "0x8FDA1Daa6a674C1726d1896E3054B9a82d123F12", - "0x1021e61f2cDd8bB295b0e64A20eBB7D8ec3734bf", - "0x58d7d9c971A613117E493062bEC1A6A5484f2780", - "0x2bb96f44b9709b02189A50B377755edC30bc65C7", - "0x7bE20B02095944657275eD608615A39931d783F2", - "0x4AA51a723882ee676FeC444D4561c5eE16c339E9", - "0x1B243D42F53924118646EFaec5b3f6116b563960", - "0x01b7348EC3fb20Ab1f40b97Cc82df44aeD360768", - "0xbf4C0104dbfb028f3484CfAC17BB22aa15E5c7E2", - "0xCc3B817D4ABa7698EaafB4C68E7688CF61B0BF46", - "0x572E1b86471c900Cd16AFa9cBB7701862D0e70cB", - "0x602Ac8C3f61b351be325FEeb58842EF557431c2e", - "0x8d0CD1AB81EaDa4F92C7cb5c8DBc25C69cc296AD", - "0xAE2C7AB762317DB453317b70f1f40145755fAfb7", - "0x7bdae9AAbE238188c4882D48a3aEE21288A38eD0", - "0x96e4152f00894f677d860023b9784d578bC1c145", - "0xF572C9b11E757d3580C7C7310630cd488E8EA736", - "0x3769092DBfa6eb34434fB5B7cf0eB06E710728F3", - "0xCA72c93172BA6EfF168E59e7F17C3C7A8FeA9B62", - "0x1c0AcCc24e1549125b5b3c14D999D3a496Afbdb1", - "0x7fC80faD32Ec41fd5CfcC14EeE9C31953b6B4a8B", - "0x5d36a202687fD6Bd0f670545334bF0B4827Cc1E2", - "0xe64113140960528f6AF928d7cA4f45d192286a7a", - "0xf6B6F07862A02C85628B3A9688beae07fEA9C863", - "0xD779aFEE481e3Df5cd0544F0e4353Cf534FD99Db", - "0x183bDB344A07Ee3D27f07AC4799A56E4A2fE5439", - "0xA8cadC2268B01395f8573682fb9DD00Bd582E8A0", - "0x75535661Ab25a468Dfb3137320a7568FeCda4832", - "0xd37ED782323A82e5BD55A92500E48FF5eFcc415E", - "0x03bB5bC3c8fdAB212A6b2B347a049133DfCB3A47", - "0x61987699055394c65355F2797D3e4e589f7FaBf4", - "0x2bC12061C8912505978472C21d4a23dB43AF62aA", - "0xad7575AEFd4d64520c3269FD24eae1b0E13dbE7B", - "0x0D89421D6eec0A4385F95f410732186A2Ab45077", - "0x04c0cD38B8c203b14ef2b7B8d736D69B938AFF71", - "0x0CF30daf2Fb962Ed1d5D19C97F5f6651F3b691c1", - "0x6EEb37b9757DcA963120f61c7E0e0160469A44D3", - "0x616caD18642F45d3fa5FCaaD0a2d81764A9cBa84", - "0xdC1d963D21C9c1bFf7b6Bea6e10080dAa9b4fc51", - "0x8073639B11994C549eDa58fC3cd7132a72aaDF10", - "0xe52C39327FF7576bAEc3DBFeF0787bd62dB6d726", - "0x8f21bD39FcAeA3A729D46339A383081ecB7E84E0", - "0x8Fb7087336678F36E42313f6130567A109a8e73d", - "0x276E69CdD336001afEF07075859A93078496C3c1", - "0x954F716e6de059360d278B773138f8e046696721", - "0x997D410b26CdD17b0750F2c1751e59cBcfaE446f", - "0xE8b6b71f3b1E6d2ad406D2cf36B1f2C567342dF1", - "0x83108A0653a14EAeB8301E7b10a37CfAc39C82f6", - "0xF95D9549b3Ab9470d306a6413Aa45082e8B66043", - "0x82d92494f6fFFB17A1DDFfd9B7d88D1d0a360009", - "0xA19947DA8B916f64Ac6F362cEC9001D8BCBeEe93", - "0x7ef5e4062dcCaD29A6F8d5458590160536056C81", - "0x653d973b36137A5cB2fc304996E0af1F1afCC628", - "0x5F319CA6Ecf072A4d183edAa711Cd04dC225df19", - "0x4D32D90D6535bD4e7eaBaa27EE72932cB214BbfA", - "0x73b9f6a6e52aCE2797F0a6E52AAc530Ed1F2a2Af", - "0xaA3600788b72863ff51C8f0dB5F10bB65fbFeAB4", - "0xf93F0b770784602fC3079eb1D2fB1Ff488Bb02B0", - "0xC8Ddd59c496D04C4C060Ab5038d03d661DDC2617", - "0xc42c77b6B2A2B220b9502F357bBf51334Db3C93f", - "0x2615214F8200B526a7B1eACe03971F2672B48CF2", - "0x9d8d7220D060fd12Ca33336B7239688e366327dE", - "0x9e602c1920443F01Cb100a57A7F894df8Eb42f66", - "0x7e651F5f597436cD0fa941F5FF2cD45Ef3F2Fda8", - "0x8e30Dc2AEF957B1F7dd67B1b7bC651fFe7E17a06", - "0x597dC4159a4b85c086c3C679a0B6c8Fe2836886F", - "0x7fBdE8B27D2B4F164B66F2a9dc02bbD6697e5b19", - "0xf5819cC26F0481c9b86294B4c24027518a04BD5B", - "0x8e7D20638947132B0e6E1aFdE2da1B103aFF9280", - "0xCBA711BEF21496Cfd66323d9AEA8C8EFd0F43e9d", - "0xdfBaeeF21396BF205D4B7D23345155489072Cf9B", - "0x3B981fA5dD50237dAb6F96A417A6690B6f20FcC4", - "0x6C31212a23040998E1D1c157ACe3982aBDBE3154", - "0xCDdF772F8A3295C89DC37510E16e360ee2d29789", - "0x002B5dfB3C71E1dC97A2e5A0A7f69F3e7b83F269", - "0xAD7A185b2456d5AFD85838A50C7d8aCE3aB2f871", - "0x7993F18C91A9f68593d308C5846f380A2a374F46", - "0xc5d82775c9bc5272B1225DB8D62b7034e064BA91", - "0x8bfcF8cb383149D4Ef37e7A609cEc195CDCbE099", - "0xA515F7fB260095eebC860425493b8761B4FC9abd", - "0xaA95cA26c92b0634dF7a1A1504f579F13bFB7f9d", - "0xC2812325caD4C4C782CbbC1164e9373371D31dB2", - "0x4831DdB6502ca45dbEEDf58B47292061Cdb6050B", - "0x6733c60E6E02f9C8FA221Db1aeA018d80D949861", - "0xCaD3887923B39cD2b0B6d13538C4ecB7C5EE9825", - "0x4520cD8BC085B962eF8c0ec696ac9D3Ef1d8bf55", - "0x7D85fCbB505D48E6176483733b62b51704e0bF95", - "0x27259b0F4209e76f8C6Cf27106C9FF83BdC2E831", - "0xE04885c3f1419C6E8495C33bDCf5F8387cd88846", - "0x23ee51e614cBF138e4cAbA9EC5ed4fF7D27A8596", - "0x2cab4d881962D247218356B32aBc4AA5c46bA0d2", - "0x1c0A032954f20761E59138feE236204bECbb8bdb", - "0x701d0ECB3BA780De7b2b36789aEC4493A426010a", - "0x1Ec3C1f70E1D6bBDC84092ae86eAaDE495fdDB9b", - "0xB53b0255895c4F9E3a185E484e5B674bCCfbc076", - "0x770569f85346B971114e11E4Bb5F7aC776673469", - "0x8289432ACD5EB0214B1C2526A5EDB480Aa06A9ab", - "0xdca6F7CB3cF361C8dF8FDE119370F1b21b2fFf63", - "0x117e1EbB7D05545064850513021dF6ADe3C1690B", - "0x7fb43C99a26a9EA8ba841d58390BF1C2996EDFB0", - "0x84B5a60Df2d7e3397B3A4A3c6282f090304Aca26", - "0x72F434Fa010929656AeF58695dab85447E51Fbc6", - "0xA29b0D2F3b4555359A1bF684d700753b1b06cBc4", - "0x4318cC449b1cbE6d64dd82E16abE58C79E076C2B", - "0x8F48282e50B0210bd7c7DD69C54205E98b9Ef5D9", - "0xa305B293e44A82f3Cd489b5fB26084647bb5D8ae", - "0xd9e5De13eF1dBC4DFE0Ee1BB76276228b9B23d0f", - "0x4AcEEB7bF9ec8104CC2379f1E8D648Ee47249FCb", - "0x0743542070891051861f8D0a4550f97B43B0B89a", - "0x58aD805f26272C5Ba06D24Bd0E43c8a2d1c634D9", - "0xE6ED9C681967a4EA7Cef4486942b800139DfB000", - "0x51b9C1Df35B044b5c0099D1fD07EAb7cE38f325d", - "0x55DFFA17578e6bAcE42e4Bf8687A11A85cCfEF97", - "0x1FAE8f99E9F932BdBA910061590C2156eE512A91", - "0xA25207Bb8f8EC2423E2ddf2686A0CD2048352f3E", - "0x746bb7beFD31D9052BB8EbA7D5dD74C9aCf54C6d", - "0x38bc91AA6Aa434c4fae7E666F68C859292deEd95", - "0xA3aD5CFb4FF4B68e37A338Da200BA441C1850B5b", - "0x4bfb2c232F70c83136a3F206cd26Df2A0B605cEC", - "0xf5AB6B4a8d578807491ef59cE855982990932617", - "0x1Fdd220E14b59E26bf1888e8267C4C221983a0A6", - "0xE2D6AFF297b41881c1aEA9599F68AEDFAB38C651", - "0x7d547666209755FB833f9B37EebEa38eBF513Abb", - "0xb681B19bb1F7e9F3C2AE0EDeab368c2afaa4e590", - "0x7Eb84E42059F0D44269C50f4D3A280Fd307a6824", - "0x84f0620A547a4D14A7987770c4F5C25d488d6335", - "0x4Ae6a8A28c87b75e935a90D6128F2649C969c0D8", - "0xb79223E868871DBAc27E8E301f73734abd4Cc628", - "0x6F219Bd1167568aB67494A9067CbbB5679bf0022", - "0x9Ff548c1B3eA3dd123AFE39C759dDA548009B6C8", - "0x3085051F89666E7124e7Ab95b693Fc1E09770907", - "0xa25211B64D041F690C0c818183E32f28ba9647Dd", - "0x6166E1964447E0959bC7c8d543DB3ab82dB65044", - "0x76E059C6FF6bf9FFFD5f33AFdf4AB2FD511C9DF4", - "0x4CC9E6fABb800F083a2685501d1A30CdAbb4B2De", - "0x5f3371793285920351344a1EaaAA48d45e600652", - "0xAFE2b51592b89095A4cFb18da2B5914b528f4c01", - "0xe3F4F3aD70C1190EC480554bbc3Ed30285aE0610", - "0xE0D8926A51F9A1dD8E089D9a3DD88F88fFb2F1Dc", - "0xa6c366D97cb64708211f24310dFAd5363BC96a04", - "0xB7562F12E41C762CeCDA99d62Bd6EAC7b0C3B4c1", - "0x301605C95acbED7A1fD9C2c0DeEe964e2AFBd0C3", - "0x5d47e5D242a8F66a6286b0a2353868875F5d6068", - "0x0ea26051F7657d59418da186137141CeA90D0652", - "0x88f1706c20d94A4d1551C5F799C9E3380A24C3AC", - "0xFB40932271Fc9Db9DbF048E80697E2Da4AA57250", - "0x40Db8365d1252bcb06598927698238a99D39228E", - "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", - "0x144c4E5027B69f7798B2B162D924BcAE5c149f15", - "0xeeE844540644b204f0005c063Ae95F244BF06a84", - "0x014607F2d6477bADD9d74bF2c5D6356e29a9b957", - "0x1E8eE48D0621289297693fC98914DA2EfDcE1477", - "0x4AdA1B9D9fe28aBd9585f58cfEeD2169A39e1c6b", - "0x31460f49EEA93Ef8255b42be019FB96F89Cf0c49", - "0x63A32F1595a68E811496D820680B74A5ccA303c5", - "0x022ca32d31da3Ef85922AAFD9aD29C5b2418172C", - "0x93B109C3c279bcBbB673Ed1ae1A8BB2dE8eEf9da", - "0x689476323Eb5e9A5DEd342F54B562fc2c156A522", - "0x1C9F765C579F94f6502aCd9fc356171d85a1F8D0", - "0xe0144FA05A0d32B5B1De10CcEe7211616B3E3EF0", - "0x6C965b656C450259a6D4d95A2E68Fb4319EecBc0", - "0xE36BD8C15a83b89E2E49806d7312846069755C63", - "0x59DDA36bD196Ec849838CE2163E6821f946b37Dc", - "0xDd31dB93082a3A71b98D37ba26230f8734Bd63C3", - "0x83c98211C50480e457a0dF930d2A56a891BC4d4b", - "0x11FA934f6754076AEb7Cf0A72a1c2D2518aA4C77", - "0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12", - "0x2383A8b8cC8561a65871F1d2783B7C52e22B62c1", - "0xCED608Aa29bB92185D9b6340Adcbfa263DAe075b", - "0x841AD0AbAb2D33520ca236A2F5D8b038adDc12BA", - "0x76d2DDCe6b781e66c4B184C82Fbf4F94346Cfb0D", - "0xf21e38ac177B48fDE02dB7F2CA97466AE8Eae87D", - "0x7537Cb0AEe6a3483a7601ebf1084eD4df73166Ab", - "0x5f0bD06A71E038206ef3e5090eB448E9a9773772", - "0x3C0c7B44c1F9366271F5c491121a1F7d55d33eF5", - "0xa96a437eFb71bAF50A59027C340FA3362ef703F7", - "0x55bA9c90c37e3206570AC9dc872c0f053d155F77", - "0xC68bba423525576C7684e7ea25E7D5F079b1361E", - "0x78E87757861185Ec5e8C0EF6BF0C69Fa7832df6C", - "0xCb36F8580A36788A48518dEC95Ea458357E64E30", - "0x25854e2a49A6CDAeC7f0505b4179834509038549", - "0x639749b7b08aEe65039c21d8a411103C6ceBEBF0", - "0xF517529866d371F04780885923F739bc17694BFb", - "0xC728DEa8B2972E6e07493BE8DC2F0314F7dC3E98", - "0x33f6EE932cEa603Fafd6854827259bE172C91Da4", - "0x6D97d65aDfF6771b31671443a6b9512104312d3D", - "0xB7BaBe35CE543e2Cf2F615CB1c792a2025feb572", - "0x4D9e86a5AC368Aa4Df0473eF07e13Ec2Fbe04025", - "0xaa79B87DC8B046A5E4f7D03F1562D7fe5BF98737", - "0xE71FbB197BC8fD11090FA657C100d52Dbb407662", - "0xB22981bA3FE1De2325935c91a3B717168fB86714", - "0xf389dD1F828525b449D63D14157f2d3A25eE0a41", - "0x877B37D3E5467B4aAE7687Dd3480A46C8D3e16Be", - "0xf9e1D1e9F22c96752356AdFd377231528c7E851E", - "0x187089B33E5812310Ed32A57F53B3fAD0383a19D", - "0xF1659A2FD5007192314F9676e6a4a39FD1202160", - "0xFdd210ce1b829E837D9e034DAE0F0312F176cef6", - "0xaCE1f1c6c5c89AE3Fc3209ff92e7120fb74445aA", - "0x6Ceb397b68059Ca73049874D0a30c62500aE9877", - "0xC46c67Bb7E84490D7EbdD0b8ecDaca68Cf3823F4", - "0xbb2eb4c7eB36ECce7A3E6bc501590CE12c9c1050", - "0x9Cf251A782cE7310D5bec0fe0a1C2B826d962545", - "0x43930Ff04D18a5B59805151c1Eb403C55870641E", - "0xA270f5649A42feDfE66ddb3b0b50bebAe1e3DDB0", - "0xd3488EA0c1DC99a5d72F75c84004224f8b58694E", - "0x7aBa691D12D8eF8793F1643eBa66b69C70EC72f1", - "0x8558f502887a9a52c4B265d72327E0E529Ff790d", - "0xA906c85B7e809b79c5e69d485693B44d65B1B252", - "0x3abdC9ed5f5dE6A74CFeb42a82087C853E160E76", - "0x30C7F4F7504D6366916f669cd8E731ED4dF6C702", - "0xed8DB37778804A913670d9367aAf4F043AAd938b", - "0xc191a29203a83eec8e846c26340f828C68835715", - "0xa32aECda752cF4EF89956e83d60C04835d4FA867", - "0x059F7da59Ad1EB412B4d2fFc12E9B50Da91cFdb6", - "0x85BEad65c61dB8cF230b3ec30552B8b3E6388570", - "0xF3Ad97364bcCC3eA0582Ede58C363888f8C4ec85", - "0x3F87755E2974534888Ddb20A52dCE45Ef9f204AB", - "0x757CC91CcBB88cB0d78d6798D20051d39E5A7296", - "0xF553C8223cA8542Af9Db7b916Fe9dc7c28b73751", - "0x40f9bf922c23c43acdad71Ab4425280C0ffBD697", - "0x9600e2eE6377DAdad7299B120026661c336A5e6d", - "0x516fCA170bfE24BFC54e01F215EF85Fe9B5B798A", - "0x61C820e261717A5A0555488872F78ac7b9CE77Ec", - "0xEb263241eB948Cc0eB53A58bf743289D074F474F", - "0x841C11b14c428dd591093348B8Afa2652C863988", - "0x3c114973c0260290C2dbD40323327d996972FCeB", - "0x765a16ca391A6b9249cfA65bf2D14C38722198e3", - "0xC3268DDB8E38302763fFdC9191FCEbD4C948fe1b", - "0x6B92686c40747C85809a6772D0eda8e22a77C60c", - "0xc799bE8De03F20B2D3b101E6F6516D614e6fFe06", - "0x40Dc654af5cE40C122ffDC679fa8E8ca8b91556A", - "0xCE8D52c38d74B77a0aA361c48Fdce6b220A3370e", - "0xEfa4c696Ea2505ec038c9dDC849b1bf817d7f69d", - "0xf7253A0E87E39d2cD6365919D4a3D56D431D0041", - "0xcf79C7EaEC5BDC1A9e32D099C5D6BdF67E4cF6e8", - "0xff75E131c711e4310C045317779d39B3B4f718C4", - "0xdE2BE7C9C542c55a7a77489A3A7745493988947F", - "0xFeB3E0f50107f6cfB2EC8C2bC8287f2707E0E2Ea", - "0x6b759Bf480407D19c8903c16023c706868c29a2A", - "0x6E38911dA6Dd0379F1CaC396F74387c95A1f0D21", - "0x5a5D9aB7b1bD978F80909503EBb828879daCa9C3", - "0xe96056A9936C58e89D1703cF6bD97F134341EE44", - "0x4dD6720D2Bb8721A46bdF9a528704164578E03B9", - "0xE83B9A1B9056B21a01b85162E77AD76a42A1c64B", - "0xbeC48f1cCf82d8e4C983Ee00Ac2eC6B03B81d710", - "0xEFEdaf9c07E6eB56BB8F82f30018e4461B1c5F4c", - "0xB68da7fbF71383Afab240839287878539cFFf20b", - "0xfBDDB719cC7c795a1D9b7EA7aC11494A19b3231F", - "0x07506a5F48D71fDB34D3900fB086D43EF1B58FF9", - "0xa85cdd5478B7E525a808eF9707c3e33432cE1e7F", - "0xCf7C21DeD40f2Df85A564207A89b3379780d9CE3", - "0xd26b76e50f6510cdD4bf45d59279705f36946d23", - "0xdb7a41e39807E8C988859f150296Db92674b7dc7", - "0x719028736f10164c838Ef129936779eD739312f2", - "0xaBCdef0AbbA5D0106595174213156797bc0DB33E", - "0x3D2b8879f97e413b2609F9844A5fc8dB8FE4f6F8", - "0x81EbE8Ee7b51741fD5DaD31F6987E626A9bb8111", - "0x1D45c8fa65F6b18E7dAe04b2efEa332c55696DaA", - "0x978eB534b26CB8749D352a2C94EC21e659e4248d", - "0xa7CA400d49BBa87EB606ee05af93689BD21FaB99", - "0x65ad2BF7E09af2597C140dF6386a3003d0F5f8Ee", - "0x835918a3fBDf946364a9aee3114173865b712663", - "0x6073cFfc1D46b1eA57BA89A28074cA734aCD7003", - "0x2B13D52dFd33E2eBd13232866fDf96088e77d596", - "0x55F5601357f6e0B10a3386914c93916c6C9A368A", - "0xA1D5D2d931b532A0503e97f540f65ed256f374e6", - "0x6C9258895FFBE2178b3EdEfE09AF304a1e99bF2F", - "0x973375b099943cDdFd390022CeA90D4F1d0c493c", - "0x8A8C879D39A74fCE0593714956bB7Ed048A5c1BF", - "0x9c42B0c70D0dAF1211f3aab2A1E6EC5E717dE12a", - "0x81a6383041593c556d1c8e69e2749b35b5008F09", - "0xF41b98a4C32bB61468C8001E0C69Ef64ce6DEa57", - "0x8FaE81bb674c89cCDE35a386587333D074b57786", - "0xa8258ED271BB9be9d7E16c5818E45eF6F2577d92", - "0x1e90474D2E83e7B7dD45553156Beb316845E66A4", - "0x2cCbbC4c10F5d807FDd447219B57D0b883a28DC8", - "0x1bBeAc736875c5043486A8a4374E6B5616eC8883", - "0x95add3DfEF3AE0A832607Dc71C4A9C6A6C2D7Eb7", - "0x744c6Eed427aF293b0106B46700fdDD3C9f62Eef", - "0x743Ec55fc166D24D2FD0211fb6Ce53926D0Ff3b1", - "0xDd03d2434C02c6BfFb097b7130528F9568b6C70d", - "0x97C12EFA574923E3ee445370d2dE432332857110", - "0xB69951a0642b55CD5731535ed5B290Fa49D3454A", - "0xBA56878729540404dE2aa14561b451aE2350744a", - "0x8D247f4Fbbe81429d3D164a5c9Ae0063210edBdC", - "0x850a146D7478dAAa98Fc26Fd85e6A24e50846A9d", - "0xfE1552DA65FAcAaC5B50b73CEDa4C993e16d4694", - "0x9705FE3586a7D768Fee061aAfE9384b1D4B8F2D8", - "0x5554672e67bA866B9861701D0e0494AB324aD19A", - "0xacc5c1e73d70F7F9622De2d574885Ce8E6981033", - "0xbe9E7b0ed19526544B55b697107231f9467a805f", - "0x172DBab6f5E62A1FE7E2bA5eA1624ADB33e0aa14", - "0x96725Fa2F9A0b5bAf80fC36C20C2cA79d86424ed", - "0xa392cCadABFf735dbFF69dC93d7C13f34A30611b", - "0xEbF0e04E47F726D0f44801dFEC5e705aDcd6694b", - "0xC0891e8FCeA09680BFe9170809fad1BCCa10b96b", - "0xA21000E7A5A2A2Bd9329428A859f9d7dcE0f0961", - "0x9A387307F7508DE113092BaFC5CB4B3AE0706521", - "0xBA719E0197470A790726075fD98EDEF04E2467af", - "0xda08BE028304db1A73a13Bce7C943127C2E393dB", - "0xfB4a965A35603010FeAcC648cA022Cb6A12D33F5", - "0x3Aa73ed90e9f0CEd87ff99CB60cA79019279e6CE", - "0x150bB505A9259b0be44FFb15415C79199E83c445", - "0xB170A41F2523220A12F84f17A54bD31953D98027", - "0x2Fcd65d9c8078644adCf1CB0cd70A1b61F3F9C5b", - "0x73006C818880d07dD510e165C3De3E74F2407187", - "0x747e6ABc102222f1dF65C662540dDf471241a644", - "0xeeEe5D271A56Aa09C4F8862aF514ADD3E882857c", - "0x98Ad82AB467bc8c70e0CC183a5826d903751b7d8", - "0xC624434420f6CbE835D6358A8223b78432773cEd", - "0x848e313d4b25bC0B48CaFdB6A72391E892E6A247", - "0x0025Ab2d69F6c2C3Ffac32Ab6A16e18c807518B8", - "0x2efe744ecc4F6BD55538da57D09DAE895C95b223", - "0xBc6d82D8d6632938394905Bb0217Ad9c673015d1", - "0xe1555c6EE61366a3f90135Dc704Acd25C3247ACA", - "0x2f51E78ff8aeC6A941C4CEeeb26B4A1f03737c50", - ] -) +GUEST_LIST = { + "0x16f3f2f0ba34973937a1ebb989a295ca106b67c7", + "0xbb5935daafbacae82c8d2ca8377f16073d70061a", + "0xba84b5ca750b33dfaddbfdd1b7c6887885a34977", + "0x4e9a05226993f094a56a3472c8c816f2599423a6", + "0x40de3299bd8a10d8ac3f32c1a55de40640cf9b75", + "0xc33f87697ef41e0e95e7a55d1ec8180f04088578", + "0xcbc924183bc32d02746fa8d38843b5ce08662eb4", + "0xdc9c5e34959ec3643af1e1d34a83d6b251aab1ef", + "0x762bbc211990d0a356f35e4d500843f59d223c2e", + "0x55187a1165ebb441a1bf227fff1eb0d32a65bc46", + "0x7ae59f3f2b2e5f3842b50a15bcb5247c5de881be", + "0x59072b3a3287f4a75cadfb36d671a2f0d1959b09", + "0x4a5da2a1d3258df8ffb431cf0110fe9b98adeebf", + "0x514a9771af8afe71057666b680238dfbea578d65", + "0xe70055e9575f15a6f51f3068901d73ac63952adf", + "0x9e831b58001e2b69f70c892e4f8ce9d2118b7e00", + "0x51c1c7f1e168a36bf1fabfd91e98b43476a6b14d", + "0x33878e070db7f70d2953fe0278cd32adf8104572", + "0x3df13b9bd79158f0cccddd0833cf774178e3d2e9", + "0xb9573982875b83aadc1296726e2ae77d13d9b98f", + "0xe862e2c1ca94eacfede3c95a217c15ef0086a29d", + "0x0442a9abbc93058a873c371f21cc366338254a88", + "0x0194325bf525be0d4fbb0856894ced74da3b8356", + "0x399e0ae23663f27181ebb4e66ec504b3aab25541", + "0x9f729294b308f79243285348a7be3f58ae5ed31a", + "0xb62e762af637b49eb4870bce8fe21bfff189e495", + "0x5725a458b319d73b8ec84c47de80620e7b191b0c", + "0x57ccc081824b43b75986727875929af3a6ad721c", + "0xf13e477365b0faa64130da2ff663aab20d32d929", + "0xfd868db0696ef762351f8421535cc5f9f423b23c", + "0x30043aabbcebbd887437ec4f0cfe6d4c0eb5cc64", + "0xaa01dec5307cf17f20881a3286dcaa062578cea7", + "0xbea26de685ef828b60ca53b40ecc9bab35645fdf", + "0x4103cfcb300599dfcb31dbc95d919592619b4eac", + "0x22baac1e95efc010e35d5ed643bb16c9db254a11", + "0x686a484bc2e2be79f358c7055e8539a69413a3ed", + "0x073a360c372fd51bd6e56b4a4d73790fdaec4641", + "0xdd0206010ca82ff22303b58863b3a6f3006c86c4", + "0x25fa68a4c340202737edbc67fd1a2ec8de872db6", + "0x5b655eda7d101f98934392cc3610bcb25b633789", + "0x32cefb2dc869bbfe636f7547cda43f561bf88d5a", + "0xa4369e39e3ed13593adb0142a1ea5d08abdf99c4", + "0x65f632cfe8015b7ae6976e549645ed04cde60fe4", + "0xb35e0a0d00c640ab75fad3cf3b83264bc64d23ec", + "0xafa3e6e29d99337b166b83fb24ba17b19764b49d", + "0x57dd1517c12659365e59f71129fa9b1611dd18af", + "0x9120ffd5d04ca4b26aabce611989a8f026dc099a", + "0x2ac6a3561a43f06d62602ef9728c2b9eec393326", + "0x297aa50d0557c865f6c9b0aa0a91f41c26e55ee6", + "0x9ff46343d0b652d6e766f85f9ae91653869349a5", + "0xed36bf0b2b17768e782db2ece6a327055b2f3e9c", + "0xc28d2fdfe6d5a482d32f855457bb5f8cacdb32b1", + "0x1d44404c1c53991ec33095225da173d544cd4af3", + "0x5d9fbd984b9cec714a4b14c38ea83bbc82d06d69", + "0x5bc0aebdbab698e12fd33a2e133e6858fe6cdd76", + "0x66805d8b82664acab4cbe0c0498889dde9af7841", + "0xaf7610578f54c7de7563655aaf461e2cbecb08c6", + "0x6c3f373baec5d2d0fb3c82c4f3db5e48873ae363", + "0x015122a625b45f68e6d795c0ab99fc7107e4c3b9", + "0x508a4f07b60ba0940283cd4e32d5deb0cc38adf7", + "0xb150c9bed10a8c62997d58a81c4e1fa75160643e", + "0x212647c56ba10ee429a838bc567dfb03a8d054ba", + "0x73306dab0d39a4d47df4972c7022cb2cac075d4e", + "0x914d5d84aaa064207c2c31014426227405edab41", + "0xfdde7ae208b3596f1982d66f6bae4cdabf29244b", + "0x02e4cc9fff7566563618fb21b3bb10eab4b3d726", + "0x31d23825afbda5b6b1690bbdbbb8117b5ea0f8e4", + "0x731022d6de647991864203a35dfad1a192240d07", + "0xeb5e0b8e80fce271c13f533fa728d7bb03cafa4c", + "0xfc967de4e029fdcd16b418dac2147d282c93085b", + "0x801a6d6dbc1e40466e131aa21d951629a9efab4e", + "0x4892139de0e73141438d9e55d593171c0cc6b143", + "0x8124efc94c951cf41d4b0b42794c678458a00726", + "0x81cc36dda894256aa95458f78b4029381b09bdfb", + "0x4dcb2bca3450b427f3d1b424c885259d05363080", + "0x7dd8030f9d33af4a40ee074f990892e825132e61", + "0x432c53218a11bed08d238cf84ff547ce4fe933ab", + "0xe77ad9c5af60332d24e5531b51a6b7f61d0b8703", + "0x0f792e55668ad78476d4b563e6eb1228d636a71e", + "0x583bbada56bb535bcbb31877a620a6ff2a25cea5", + "0x5c0e777dc8f3de6b0911b44dbbdd8bf71b2e8e38", + "0x8a4a50b13fd2cb36feb96c408cb98b4c9f2b8f25", + "0x1e55c85801a2c4f0bec57c84742a8ef3d72de57b", + "0x26d3be736ab6b5d8a3266ffcc0895ddc1bc19a38", + "0xfb94f39b150ae661f85762154c0cadc65e083791", + "0x4b7c0da1c299ce824f55a0190efb13663442fa2c", + "0x77e64560bd6c323c075f206a5ab9dd6850f31609", + "0x0f46540678c7e6d2ef983b382cc07fa815ab148c", + "0x82073f802547feeec0fd49719a3d7697fb66076a", + "0xbaab83de8dba764bf02a530cad33555bd23eba22", + "0x890a0047f8d573347872cb6c019f86552f2367d6", + "0x14d92832265eeafdef9e526356fefc90105966c3", + "0x512b436cb2ed6016e80d4f89ca578f99dbbccb61", + "0x696ee4ae0b15feae8ed1afc865930e0ea65b1f3f", + "0xbb4d885fd41c807e8ecc2dd9e6295a7f96adb0eb", + "0xb1de969883b1fdd90a43ff475a5171a3cfefe76d", + "0x7dbf6820d32cfbd5d656bf9bff0def229b37cf0e", + "0xfe2e3ccee9714b29ab2fb4e940e52672194815fc", + "0x57fb3f4b027fbadbd8d20eb5e48feb1e2b02df30", + "0x9ae494fbac34682c122a1d4e3d6ae1eb5404a469", + "0xb2b9300475af157676c44ee64d39a5eb3c294dbd", + "0x01bc28e036b6e75247fe8f49f0a8b9410b19d851", + "0xcce9a28b570946123f392cf1dbfa6d2d5e636a1f", + "0xb2a3b5b9d2c0f07cba328b58737147cfc172eb9f", + "0xcc3d7f9fe6946979215a901bba385a88fdabbbf4", + "0x38f80f8f76b1c44b2beefb63bb561f570fb6ddb6", + "0xd82803b7b9a5eb1d5fc558fd619afc6c031cd0b1", + "0x844aeed1b294ef9632c18e73f57ef77d0a23d0e2", + "0x9cd7d1981b3e15a2dee4d512ac60e0579ae18546", + "0xebcd250474c27cbad3c56f3f34e08f97b370ac2d", + "0xda47bdcc48f26fb4709f90316341d9104cb1fb89", + "0x5cbb6ad79008908aa125667d1300558d9253b589", + "0x1078daa844cdf1edb51e5189c8b113b80a6a6957", + "0x8341c4106523b49fc247f84e412bb2af5597038f", + "0xce57ebed9ac38402dcaa44f65a1c9b04e26b8283", + "0x2dd2036c9db2ada2739509af0047c00c8b9291ef", + "0xa77294828d42b538890fa6e97adffe9305536171", + "0x8da48e5846c06b558970acd42edc7da8799481e4", + "0x50418699cb44bfda9c9afc9b7a0b0d244d8927d2", + "0x936d69abcd9acdc89455eefaf744044ffc1ca660", + "0x90c32e6b29794fd7f5bba2bbee74e924078b3f9b", + "0x362b7e0599e950b921ca9d86336ca409208ffdec", + "0xd98ad1fd4aa0e1c876d91968d1385aa9e1aa98df", + "0xd2602c7bdfc9f413974e944280bbfae275d1b1b6", + "0x731a2e51ebfaebacf8477e992cdeb1e8eacf519c", + "0x072d63796c4fe69b306a23e1d01156d51f7b3e16", + "0x051010142a0b9de7f0fd8fb31d085407287f6381", + "0x8498843f6d9046f7b59482978e152d61869203bc", + "0xb48ef8e4e7bef79ddf64d4424151f003a59bfbfb", + "0xb423a138fd171c28d90a5883a01ec92ff3d63609", + "0xafa3a2528e8baad576a83ffc52db9f100debe307", + "0x055fda7eb509cc338c898b0f698b7624387ab813", + "0x0b3bd83e857997b370fadc8504fb712244f6786c", + "0x8d12a71cb933a4222d42fecbb4ba9c15e455305b", + "0xdef3d19ff35a42f5b8e3c706c8fd287de72e6d15", + "0x19a2bc678785bad6a947a87494d480dad57711c6", + "0x2c3e79d3dce90fb0886c89ec602e61757e589a94", + "0xe8aa836a597a66724d678860d105561c13e95bfa", + "0x3352a3277d2b74a773fa6e68a625fcb18e4fc282", + "0x2df292af809fd693d94c7d17e36be352e15bb98a", + "0x269aa10398aaa695259c3e8211ab27a15004110c", + "0x02d9c84a495986b8b3c3347ad16849dcb1b9793e", + "0x8fda1daa6a674c1726d1896e3054b9a82d123f12", + "0x1021e61f2cdd8bb295b0e64a20ebb7d8ec3734bf", + "0x58d7d9c971a613117e493062bec1a6a5484f2780", + "0x2bb96f44b9709b02189a50b377755edc30bc65c7", + "0x7be20b02095944657275ed608615a39931d783f2", + "0x4aa51a723882ee676fec444d4561c5ee16c339e9", + "0x1b243d42f53924118646efaec5b3f6116b563960", + "0x01b7348ec3fb20ab1f40b97cc82df44aed360768", + "0xbf4c0104dbfb028f3484cfac17bb22aa15e5c7e2", + "0xcc3b817d4aba7698eaafb4c68e7688cf61b0bf46", + "0x572e1b86471c900cd16afa9cbb7701862d0e70cb", + "0x602ac8c3f61b351be325feeb58842ef557431c2e", + "0x8d0cd1ab81eada4f92c7cb5c8dbc25c69cc296ad", + "0xae2c7ab762317db453317b70f1f40145755fafb7", + "0x7bdae9aabe238188c4882d48a3aee21288a38ed0", + "0x96e4152f00894f677d860023b9784d578bc1c145", + "0xf572c9b11e757d3580c7c7310630cd488e8ea736", + "0x3769092dbfa6eb34434fb5b7cf0eb06e710728f3", + "0xca72c93172ba6eff168e59e7f17c3c7a8fea9b62", + "0x7fc80fad32ec41fd5cfcc14eee9c31953b6b4a8b", + "0x5d36a202687fd6bd0f670545334bf0b4827cc1e2", + "0xe64113140960528f6af928d7ca4f45d192286a7a", + "0xf6b6f07862a02c85628b3a9688beae07fea9c863", + "0xd779afee481e3df5cd0544f0e4353cf534fd99db", + "0x183bdb344a07ee3d27f07ac4799a56e4a2fe5439", + "0xa8cadc2268b01395f8573682fb9dd00bd582e8a0", + "0x75535661ab25a468dfb3137320a7568fecda4832", + "0xd37ed782323a82e5bd55a92500e48ff5efcc415e", + "0x03bb5bc3c8fdab212a6b2b347a049133dfcb3a47", + "0x61987699055394c65355f2797d3e4e589f7fabf4", + "0x2bc12061c8912505978472c21d4a23db43af62aa", + "0xad7575aefd4d64520c3269fd24eae1b0e13dbe7b", + "0x0d89421d6eec0a4385f95f410732186a2ab45077", + "0x04c0cd38b8c203b14ef2b7b8d736d69b938aff71", + "0x0cf30daf2fb962ed1d5d19c97f5f6651f3b691c1", + "0x6eeb37b9757dca963120f61c7e0e0160469a44d3", + "0x616cad18642f45d3fa5fcaad0a2d81764a9cba84", + "0xdc1d963d21c9c1bff7b6bea6e10080daa9b4fc51", + "0x8073639b11994c549eda58fc3cd7132a72aadf10", + "0xe52c39327ff7576baec3dbfef0787bd62db6d726", + "0x8f21bd39fcaea3a729d46339a383081ecb7e84e0", + "0x8fb7087336678f36e42313f6130567a109a8e73d", + "0x276e69cdd336001afef07075859a93078496c3c1", + "0x954f716e6de059360d278b773138f8e046696721", + "0x997d410b26cdd17b0750f2c1751e59cbcfae446f", + "0xe8b6b71f3b1e6d2ad406d2cf36b1f2c567342df1", + "0x83108a0653a14eaeb8301e7b10a37cfac39c82f6", + "0xf95d9549b3ab9470d306a6413aa45082e8b66043", + "0x82d92494f6fffb17a1ddffd9b7d88d1d0a360009", + "0xa19947da8b916f64ac6f362cec9001d8bcbeee93", + "0x7ef5e4062dccad29a6f8d5458590160536056c81", + "0x653d973b36137a5cb2fc304996e0af1f1afcc628", + "0x5f319ca6ecf072a4d183edaa711cd04dc225df19", + "0x4d32d90d6535bd4e7eabaa27ee72932cb214bbfa", + "0x73b9f6a6e52ace2797f0a6e52aac530ed1f2a2af", + "0xaa3600788b72863ff51c8f0db5f10bb65fbfeab4", + "0xf93f0b770784602fc3079eb1d2fb1ff488bb02b0", + "0xc8ddd59c496d04c4c060ab5038d03d661ddc2617", + "0xc42c77b6b2a2b220b9502f357bbf51334db3c93f", + "0x2615214f8200b526a7b1eace03971f2672b48cf2", + "0x9d8d7220d060fd12ca33336b7239688e366327de", + "0x9e602c1920443f01cb100a57a7f894df8eb42f66", + "0x7e651f5f597436cd0fa941f5ff2cd45ef3f2fda8", + "0x8e30dc2aef957b1f7dd67b1b7bc651ffe7e17a06", + "0x597dc4159a4b85c086c3c679a0b6c8fe2836886f", + "0x7fbde8b27d2b4f164b66f2a9dc02bbd6697e5b19", + "0xf5819cc26f0481c9b86294b4c24027518a04bd5b", + "0x8e7d20638947132b0e6e1afde2da1b103aff9280", + "0xcba711bef21496cfd66323d9aea8c8efd0f43e9d", + "0xdfbaeef21396bf205d4b7d23345155489072cf9b", + "0x3b981fa5dd50237dab6f96a417a6690b6f20fcc4", + "0x6c31212a23040998e1d1c157ace3982abdbe3154", + "0xcddf772f8a3295c89dc37510e16e360ee2d29789", + "0x002b5dfb3c71e1dc97a2e5a0a7f69f3e7b83f269", + "0xad7a185b2456d5afd85838a50c7d8ace3ab2f871", + "0x7993f18c91a9f68593d308c5846f380a2a374f46", + "0xc5d82775c9bc5272b1225db8d62b7034e064ba91", + "0x8bfcf8cb383149d4ef37e7a609cec195cdcbe099", + "0xa515f7fb260095eebc860425493b8761b4fc9abd", + "0xaa95ca26c92b0634df7a1a1504f579f13bfb7f9d", + "0xc2812325cad4c4c782cbbc1164e9373371d31db2", + "0x4831ddb6502ca45dbeedf58b47292061cdb6050b", + "0x6733c60e6e02f9c8fa221db1aea018d80d949861", + "0xcad3887923b39cd2b0b6d13538c4ecb7c5ee9825", + "0x4520cd8bc085b962ef8c0ec696ac9d3ef1d8bf55", + "0x27259b0f4209e76f8c6cf27106c9ff83bdc2e831", + "0x23ee51e614cbf138e4caba9ec5ed4ff7d27a8596", + "0x2cab4d881962d247218356b32abc4aa5c46ba0d2", + "0x1c0a032954f20761e59138fee236204becbb8bdb", + "0x701d0ecb3ba780de7b2b36789aec4493a426010a", + "0x1ec3c1f70e1d6bbdc84092ae86eaade495fddb9b", + "0xb53b0255895c4f9e3a185e484e5b674bccfbc076", + "0x770569f85346b971114e11e4bb5f7ac776673469", + "0x8289432acd5eb0214b1c2526a5edb480aa06a9ab", + "0xdca6f7cb3cf361c8df8fde119370f1b21b2fff63", + "0x117e1ebb7d05545064850513021df6ade3c1690b", + "0x7fb43c99a26a9ea8ba841d58390bf1c2996edfb0", + "0x84b5a60df2d7e3397b3a4a3c6282f090304aca26", + "0x72f434fa010929656aef58695dab85447e51fbc6", + "0xa29b0d2f3b4555359a1bf684d700753b1b06cbc4", + "0x4318cc449b1cbe6d64dd82e16abe58c79e076c2b", + "0x8f48282e50b0210bd7c7dd69c54205e98b9ef5d9", + "0xa305b293e44a82f3cd489b5fb26084647bb5d8ae", + "0xd9e5de13ef1dbc4dfe0ee1bb76276228b9b23d0f", + "0x4aceeb7bf9ec8104cc2379f1e8d648ee47249fcb", + "0x0743542070891051861f8d0a4550f97b43b0b89a", + "0x58ad805f26272c5ba06d24bd0e43c8a2d1c634d9", + "0xe6ed9c681967a4ea7cef4486942b800139dfb000", + "0x51b9c1df35b044b5c0099d1fd07eab7ce38f325d", + "0x55dffa17578e6bace42e4bf8687a11a85ccfef97", + "0x1fae8f99e9f932bdba910061590c2156ee512a91", + "0xa25207bb8f8ec2423e2ddf2686a0cd2048352f3e", + "0x746bb7befd31d9052bb8eba7d5dd74c9acf54c6d", + "0x38bc91aa6aa434c4fae7e666f68c859292deed95", + "0xa3ad5cfb4ff4b68e37a338da200ba441c1850b5b", + "0x4bfb2c232f70c83136a3f206cd26df2a0b605cec", + "0xf5ab6b4a8d578807491ef59ce855982990932617", + "0x1fdd220e14b59e26bf1888e8267c4c221983a0a6", + "0xe2d6aff297b41881c1aea9599f68aedfab38c651", + "0x7d547666209755fb833f9b37eebea38ebf513abb", + "0xb681b19bb1f7e9f3c2ae0edeab368c2afaa4e590", + "0x7eb84e42059f0d44269c50f4d3a280fd307a6824", + "0x84f0620a547a4d14a7987770c4f5c25d488d6335", + "0x4ae6a8a28c87b75e935a90d6128f2649c969c0d8", + "0xb79223e868871dbac27e8e301f73734abd4cc628", + "0x6f219bd1167568ab67494a9067cbbb5679bf0022", + "0x9ff548c1b3ea3dd123afe39c759dda548009b6c8", + "0x3085051f89666e7124e7ab95b693fc1e09770907", + "0x6166e1964447e0959bc7c8d543db3ab82db65044", + "0x76e059c6ff6bf9fffd5f33afdf4ab2fd511c9df4", + "0x4cc9e6fabb800f083a2685501d1a30cdabb4b2de", + "0x5f3371793285920351344a1eaaaa48d45e600652", + "0xafe2b51592b89095a4cfb18da2b5914b528f4c01", + "0xe3f4f3ad70c1190ec480554bbc3ed30285ae0610", + "0xe0d8926a51f9a1dd8e089d9a3dd88f88ffb2f1dc", + "0xa6c366d97cb64708211f24310dfad5363bc96a04", + "0xb7562f12e41c762cecda99d62bd6eac7b0c3b4c1", + "0x301605c95acbed7a1fd9c2c0deee964e2afbd0c3", + "0x5d47e5d242a8f66a6286b0a2353868875f5d6068", + "0x88f1706c20d94a4d1551c5f799c9e3380a24c3ac", + "0xfb40932271fc9db9dbf048e80697e2da4aa57250", + "0x40db8365d1252bcb06598927698238a99d39228e", + "0xacf4c2950107ef9b1c37faa1f9a866c8f0da88b9", + "0x144c4e5027b69f7798b2b162d924bcae5c149f15", + "0xeee844540644b204f0005c063ae95f244bf06a84", + "0x014607f2d6477badd9d74bf2c5d6356e29a9b957", + "0x1e8ee48d0621289297693fc98914da2efdce1477", + "0x4ada1b9d9fe28abd9585f58cfeed2169a39e1c6b", + "0x31460f49eea93ef8255b42be019fb96f89cf0c49", + "0x63a32f1595a68e811496d820680b74a5cca303c5", + "0x022ca32d31da3ef85922aafd9ad29c5b2418172c", + "0x93b109c3c279bcbbb673ed1ae1a8bb2de8eef9da", + "0x689476323eb5e9a5ded342f54b562fc2c156a522", + "0xe0144fa05a0d32b5b1de10ccee7211616b3e3ef0", + "0x6c965b656c450259a6d4d95a2e68fb4319eecbc0", + "0xe36bd8c15a83b89e2e49806d7312846069755c63", + "0x59dda36bd196ec849838ce2163e6821f946b37dc", + "0x83c98211c50480e457a0df930d2a56a891bc4d4b", + "0x11fa934f6754076aeb7cf0a72a1c2d2518aa4c77", + "0x2b888954421b424c5d3d9ce9bb67c9bd47537d12", + "0x2383a8b8cc8561a65871f1d2783b7c52e22b62c1", + "0x841ad0abab2d33520ca236a2f5d8b038addc12ba", + "0x76d2ddce6b781e66c4b184c82fbf4f94346cfb0d", + "0xf21e38ac177b48fde02db7f2ca97466ae8eae87d", + "0x7537cb0aee6a3483a7601ebf1084ed4df73166ab", + "0x5f0bd06a71e038206ef3e5090eb448e9a9773772", + "0x3c0c7b44c1f9366271f5c491121a1f7d55d33ef5", + "0xa96a437efb71baf50a59027c340fa3362ef703f7", + "0x55ba9c90c37e3206570ac9dc872c0f053d155f77", + "0xc68bba423525576c7684e7ea25e7d5f079b1361e", + "0x78e87757861185ec5e8c0ef6bf0c69fa7832df6c", + "0xcb36f8580a36788a48518dec95ea458357e64e30", + "0x25854e2a49a6cdaec7f0505b4179834509038549", + "0x639749b7b08aee65039c21d8a411103c6cebebf0", + "0xf517529866d371f04780885923f739bc17694bfb", + "0xc728dea8b2972e6e07493be8dc2f0314f7dc3e98", + "0x33f6ee932cea603fafd6854827259be172c91da4", + "0x6d97d65adff6771b31671443a6b9512104312d3d", + "0xb7babe35ce543e2cf2f615cb1c792a2025feb572", + "0x4d9e86a5ac368aa4df0473ef07e13ec2fbe04025", + "0xaa79b87dc8b046a5e4f7d03f1562d7fe5bf98737", + "0xe71fbb197bc8fd11090fa657c100d52dbb407662", + "0xf389dd1f828525b449d63d14157f2d3a25ee0a41", + "0x877b37d3e5467b4aae7687dd3480a46c8d3e16be", + "0xf9e1d1e9f22c96752356adfd377231528c7e851e", + "0x187089b33e5812310ed32a57f53b3fad0383a19d", + "0xf1659a2fd5007192314f9676e6a4a39fd1202160", + "0xfdd210ce1b829e837d9e034dae0f0312f176cef6", + "0xace1f1c6c5c89ae3fc3209ff92e7120fb74445aa", + "0x6ceb397b68059ca73049874d0a30c62500ae9877", + "0xc46c67bb7e84490d7ebdd0b8ecdaca68cf3823f4", + "0xbb2eb4c7eb36ecce7a3e6bc501590ce12c9c1050", + "0x9cf251a782ce7310d5bec0fe0a1c2b826d962545", + "0x43930ff04d18a5b59805151c1eb403c55870641e", + "0xa270f5649a42fedfe66ddb3b0b50bebae1e3ddb0", + "0xd3488ea0c1dc99a5d72f75c84004224f8b58694e", + "0x7aba691d12d8ef8793f1643eba66b69c70ec72f1", + "0x8558f502887a9a52c4b265d72327e0e529ff790d", + "0x3abdc9ed5f5de6a74cfeb42a82087c853e160e76", + "0x30c7f4f7504d6366916f669cd8e731ed4df6c702", + "0xed8db37778804a913670d9367aaf4f043aad938b", + "0xc191a29203a83eec8e846c26340f828c68835715", + "0xa32aecda752cf4ef89956e83d60c04835d4fa867", + "0x059f7da59ad1eb412b4d2ffc12e9b50da91cfdb6", + "0x85bead65c61db8cf230b3ec30552b8b3e6388570", + "0x3f87755e2974534888ddb20a52dce45ef9f204ab", + "0x757cc91ccbb88cb0d78d6798d20051d39e5a7296", + "0xf553c8223ca8542af9db7b916fe9dc7c28b73751", + "0x40f9bf922c23c43acdad71ab4425280c0ffbd697", + "0x9600e2ee6377dadad7299b120026661c336a5e6d", + "0x516fca170bfe24bfc54e01f215ef85fe9b5b798a", + "0x61c820e261717a5a0555488872f78ac7b9ce77ec", + "0xeb263241eb948cc0eb53a58bf743289d074f474f", + "0x841c11b14c428dd591093348b8afa2652c863988", + "0x3c114973c0260290c2dbd40323327d996972fceb", + "0x765a16ca391a6b9249cfa65bf2d14c38722198e3", + "0xc3268ddb8e38302763ffdc9191fcebd4c948fe1b", + "0x6b92686c40747c85809a6772d0eda8e22a77c60c", + "0xc799be8de03f20b2d3b101e6f6516d614e6ffe06", + "0x40dc654af5ce40c122ffdc679fa8e8ca8b91556a", + "0xce8d52c38d74b77a0aa361c48fdce6b220a3370e", + "0xefa4c696ea2505ec038c9ddc849b1bf817d7f69d", + "0xcf79c7eaec5bdc1a9e32d099c5d6bdf67e4cf6e8", + "0xff75e131c711e4310c045317779d39b3b4f718c4", + "0xde2be7c9c542c55a7a77489a3a7745493988947f", + "0xfeb3e0f50107f6cfb2ec8c2bc8287f2707e0e2ea", + "0x6b759bf480407d19c8903c16023c706868c29a2a", + "0x6e38911da6dd0379f1cac396f74387c95a1f0d21", + "0x5a5d9ab7b1bd978f80909503ebb828879daca9c3", + "0xe96056a9936c58e89d1703cf6bd97f134341ee44", + "0x4dd6720d2bb8721a46bdf9a528704164578e03b9", + "0xe83b9a1b9056b21a01b85162e77ad76a42a1c64b", + "0xbec48f1ccf82d8e4c983ee00ac2ec6b03b81d710", + "0xefedaf9c07e6eb56bb8f82f30018e4461b1c5f4c", + "0xb68da7fbf71383afab240839287878539cfff20b", + "0xfbddb719cc7c795a1d9b7ea7ac11494a19b3231f", + "0x07506a5f48d71fdb34d3900fb086d43ef1b58ff9", + "0xa85cdd5478b7e525a808ef9707c3e33432ce1e7f", + "0xcf7c21ded40f2df85a564207a89b3379780d9ce3", + "0xd26b76e50f6510cdd4bf45d59279705f36946d23", + "0xdb7a41e39807e8c988859f150296db92674b7dc7", + "0x719028736f10164c838ef129936779ed739312f2", + "0xabcdef0abba5d0106595174213156797bc0db33e", + "0x3d2b8879f97e413b2609f9844a5fc8db8fe4f6f8", + "0x81ebe8ee7b51741fd5dad31f6987e626a9bb8111", + "0x1d45c8fa65f6b18e7dae04b2efea332c55696daa", + "0x978eb534b26cb8749d352a2c94ec21e659e4248d", + "0xa7ca400d49bba87eb606ee05af93689bd21fab99", + "0x65ad2bf7e09af2597c140df6386a3003d0f5f8ee", + "0x835918a3fbdf946364a9aee3114173865b712663", + "0x6073cffc1d46b1ea57ba89a28074ca734acd7003", + "0x2b13d52dfd33e2ebd13232866fdf96088e77d596", + "0x55f5601357f6e0b10a3386914c93916c6c9a368a", + "0xa1d5d2d931b532a0503e97f540f65ed256f374e6", + "0x6c9258895ffbe2178b3edefe09af304a1e99bf2f", + "0x973375b099943cddfd390022cea90d4f1d0c493c", + "0x8a8c879d39a74fce0593714956bb7ed048a5c1bf", + "0x9c42b0c70d0daf1211f3aab2a1e6ec5e717de12a", + "0x81a6383041593c556d1c8e69e2749b35b5008f09", + "0xf41b98a4c32bb61468c8001e0c69ef64ce6dea57", + "0x8fae81bb674c89ccde35a386587333d074b57786", + "0xa8258ed271bb9be9d7e16c5818e45ef6f2577d92", + "0x2ccbbc4c10f5d807fdd447219b57d0b883a28dc8", + "0x1bbeac736875c5043486a8a4374e6b5616ec8883", + "0x95add3dfef3ae0a832607dc71c4a9c6a6c2d7eb7", + "0x744c6eed427af293b0106b46700fddd3c9f62eef", + "0x743ec55fc166d24d2fd0211fb6ce53926d0ff3b1", + "0xdd03d2434c02c6bffb097b7130528f9568b6c70d", + "0x97c12efa574923e3ee445370d2de432332857110", + "0xb69951a0642b55cd5731535ed5b290fa49d3454a", + "0xba56878729540404de2aa14561b451ae2350744a", + "0x8d247f4fbbe81429d3d164a5c9ae0063210edbdc", + "0x850a146d7478daaa98fc26fd85e6a24e50846a9d", + "0xfe1552da65facaac5b50b73ceda4c993e16d4694", + "0x9705fe3586a7d768fee061aafe9384b1d4b8f2d8", + "0x5554672e67ba866b9861701d0e0494ab324ad19a", + "0xacc5c1e73d70f7f9622de2d574885ce8e6981033", + "0xbe9e7b0ed19526544b55b697107231f9467a805f", + "0x172dbab6f5e62a1fe7e2ba5ea1624adb33e0aa14", + "0x96725fa2f9a0b5baf80fc36c20c2ca79d86424ed", + "0xa392ccadabff735dbff69dc93d7c13f34a30611b", + "0xebf0e04e47f726d0f44801dfec5e705adcd6694b", + "0xc0891e8fcea09680bfe9170809fad1bcca10b96b", + "0xa21000e7a5a2a2bd9329428a859f9d7dce0f0961", + "0x9a387307f7508de113092bafc5cb4b3ae0706521", + "0xba719e0197470a790726075fd98edef04e2467af", + "0xda08be028304db1a73a13bce7c943127c2e393db", + "0xfb4a965a35603010feacc648ca022cb6a12d33f5", + "0x3aa73ed90e9f0ced87ff99cb60ca79019279e6ce", + "0x150bb505a9259b0be44ffb15415c79199e83c445", + "0x2fcd65d9c8078644adcf1cb0cd70a1b61f3f9c5b", + "0x73006c818880d07dd510e165c3de3e74f2407187", + "0x747e6abc102222f1df65c662540ddf471241a644", + "0xeeee5d271a56aa09c4f8862af514add3e882857c", + "0x98ad82ab467bc8c70e0cc183a5826d903751b7d8", + "0x848e313d4b25bc0b48cafdb6a72391e892e6a247", + "0x0025ab2d69f6c2c3ffac32ab6a16e18c807518b8", + "0x2efe744ecc4f6bd55538da57d09dae895c95b223", + "0xbc6d82d8d6632938394905bb0217ad9c673015d1", + "0xe1555c6ee61366a3f90135dc704acd25c3247aca", + "0x2f51e78ff8aec6a941c4ceeeb26b4a1f03737c50", + "0x0b924785d2bbbc8834cbd5fc63d10543ff799d4d", + "0xa50064d462e17f7091ee62baebeb18bfebe21507", + "0x6ead98ac60771343a361cf7558e8493b32fc8304", + "0x433485b5951f250cefdcbf197cb0f60fdbe55513", + "0x6dd6934452eb4e6d87b9c874ae0ef83ec3bd5803", + "0x29e1a61fccd40408f489336993e798d14d57d77f", + "0xfa7448891b9a58a7f71cc2aee436690ae1bfdb68", + "0x29185eb8cfd22aa719529217bfbade61677e0ad2", + "0x73bea65145f19702da07d8908e994a193b5855a1", + "0x140d3f60ac840571a3e08e218b823094d4715564", + "0x82cb2305388c853ecfe9ea83a1604acf58466659", + "0xb13a55d8512aec4bf7b15067d733c4c3c68f2439", + "0x2842decf9baeb5ec76988d1261325329848522ae", + "0x5fe4326d06d901886d9a11e6a7134f5c9707f0ad", + "0xa8f0048a0d1a04663ca5010d0beac5bcaeea0eef", + "0x6d465d2081b799770d0ce7e755d8db1665903ffb", + "0x837667c7c5cb49609ed5bfa5d3b2f1bbae81d8ba", + "0x77fb4fa1aba92576942ad34bc47834059b84e693", + "0xf31263051c09bcc2853dac78185e1e5c59f4ee56", + "0x3fad8bcd2aea732d02a203c156b19205253f2a06", + "0x09b43a8fdadddade8e75c18a20a6fc0dbe4a1f68", + "0x7e475a846af78e49aa56f05706530bc5ebe58ea8", + "0x38b826a4426a0d4d9b4377ac57c9af0308281c5d", + "0x84e1056ed1b76fb03b43e924ef98833dba394b2b", + "0xd0ccfeef904cce8e0c70014db37e5133a6a8aa1c", + "0x5c7291e18c3ecf30e14ccf1dcd646c0ca3309113", + "0x2dbfd11c5073095670e0eec58e8d2313fac9e494", + "0xfd37f4625ca5816157d55a5b3f7dd8dd5f8a0c2f", + "0x2d4ac9c27fffcd87d7fa2619f537c7eb0db96fb7", + "0x9e6cdaff95f4bb1a21ce2f81f7655e2aac53840a", + "0x9dcba70b2dfe5807e2a847e065ebb666791f8b8a", + "0xff6d6a7718a234a84f740480dbb07f6c09cf1cd5", + "0x69dc230b06a15796e3f42baf706e0e55d4d5eaa1", + "0x535b3fc92c40823b5b28df471491f6b5dd5fc4e8", + "0x2d122fef1613e82c0c90f443b59e54468e16525c", + "0x5ff677061159e4d0ae380607c2572e7a0e4f0d75", + "0x865528d9897d990cffca5fd1c90f0cab919c9794", + "0x6bd6109fb3bf59f67c86cab3bc09adb8b77485b7", + "0x6c05ecd97c3a2b1e15f832093aa15efb7e9924fb", + "0x867adfebee5dc35c8353cd1475a9ba92f819c10b", + "0x9f4fbbbcbe8b8e90449dec9032bd5cce65bb52e5", + "0xf449ae59575b9f2d276a532da9e835ecd1d33a42", + "0x1b13d62de363e964ce8a9981f58dd293c005e2b4", + "0x2c053707f844bdd9167d20a385641f5d55b42a70", + "0x6ba54cf6b626f1108a2f6c16767f8397060022c0", + "0xe02e99b7c9094a04c5571c07320b9fcf85a5e4c4", + "0xe040fbff9d04bd20a0c76fe44b046d170348e2e8", + "0xdeda35dfaf6e2b26d6fb9c92037bd86f9ed3bf21", + "0x72578e136e72a18a832be6762230a820f514d180", + "0x591a406cfa71bb26935ed8a644d108f0cc3d94b4", + "0x3a37cc1096865987546d237f42e18737acdeb2c8", + "0xbecb225ed2f25c5973f5b2c8455aef2491177ee5", + "0x899eb407e36965d58f300e1435e262281909f7be", + "0xb19ca68eefd8378e34329d5f31b855f808820890", + "0x26cc56986a3f0f278536194b868ba88be1c8e8ca", + "0xd67b7c3ba9304ac3f80654b8bb95895a9d0a8ebd", + "0x0d72724bf16877bd814b8ec52b2b65a4a385d7fe", + "0xd700b177296cdbbf3d404147fe3dac3900370a0a", + "0x13f823967682151bee6e571adf8363dc41274d0d", + "0x038a4e7c11193ebdf6fe574bd9ecf6989c8beafe", + "0x5fdce124662b8fa60f2a6852939ccc997f9d8e60", + "0x6e72dd10375c221d1be5a71bee11cf1c7105ec7c", + "0xf1b915f886efe71e1574da2c0c3fcd79fae5035b", + "0xcb0424775ce3f6d1cd15d5622085cc7e7e47ff76", + "0xe1896d5e7547d63e79861d53a3dacb066769dfb1", + "0xa8e5d871de077d625cfaea1a5d163389fa091e1b", + "0x93082bfea9424363861ab35110ab65b8a5dfe9df", + "0x7a129f1b0550e6dc115dda6428003adf9bc7bf90", + "0x7a122100f101e8819b23f237d62a3bb7480a6ad0", + "0x2fe31177bbbf34f9b80be48b4dc56b1eb88192ad", + "0x707e2302149294584f570d53c666d3008d39d24e", + "0xdc1fc1361efc9efef65ff81ff3acadd421a577e1", + "0xa85de57d5f22560e7c98012bfb8604af20f35933", + "0xb76bc3ad9c150cc6f83984d02ab8e42225e4d0c2", + "0x98f1bda2d45ffaca73fcf2154e9e007871e14934", + "0x90d3c980149ccbd039292ceee75b3186da05c998", + "0xb335d18843137b21db0c9ebc23e7ed9f0d33f14d", + "0x38a02b64ccd5b157062cd2bad9ecd7f85d0b8480", + "0x61c4a9107de33e75ed6fe29550ae2cc29ced2264", + "0x20e903db0a973f6d400e227d7fb7708d45fd5577", + "0x25a411f95ad3b558a783f6730e7c7d8e297b0fee", + "0x1c0accc24e1549125b5b3c14d999d3a496afbdb1", + "0x282d656a9d95c64522f6bc1a42ee759de81e8dc0", + "0x57af2c4249761f4b266dbd80fe44061faf4780c3", + "0x2387c1d431afc94600b1d7262ad90eb61a25a6e8", + "0xb9479331df573d6bca8b924f48e8cacb84664ce7", + "0xf04aebaa5c9e6ab7f2ae303f76504e00e7ed43cf", + "0x2937d75b326bae98fd823f3e5e4ea01fe9de37e3", + "0x0276dece5594a6db72e044c85488519a18f9e462", + "0x6710287cee1a9d1a00251fdc9a12795ec7a86189", + "0x9ae8912ea6562957043c8d048641aa9b450c397f", + "0x300da191248a500b2174aed992d6697bf97f9139", + "0x2d240c5d58a8445e99dadf3fd41a492dfeaf7e4c", + "0x42e0793e4f029557d4c16a05ae0d989a173ec6aa", + "0x18b54c6c9960bd16b1f7568ae9f39143c2d3390c", + "0x3bfb159c76833bc019e7c93708f26f318ad61504", + "0x2a87c1345024ab463acc26417124c433b3069fdd", + "0xd8b41b798a7ce8d30cbc87f007ecb95e1d3c876c", + "0xa831d11c0c280b118c5f44b192a327cf64d9ee91", + "0xa13ac077544dfd51193f8b15641a0c7a3e0286b7", + "0x93e4297ea2b1368274ef91aabb900debfd915d7e", + "0x0bf61023640b5ffa269ebaba895cc3662793a4c9", + "0x3e6c23cdaa52b1b6621dbb30c367d16ace21f760", + "0xe5adabbd71225e8074e30ff6ce2a66c9319c071b", + "0xd11256d99f8833bee0b99203ddcfe4cd6c823d8d", + "0x7f96a6269b00c56cdc319721be80bf8c290324a5", + "0xd1da06878d2817a358498c384418417a752dfa68", + "0x072001b736b049934eb1e66bcbbc281d010fe7b6", + "0x8d4c0ebb98b259f40f783924a78c1b1affde200e", + "0xee549cc0a0ab1aeea4bb326a1de9fd1684dd83b6", + "0x4d5b28d08e8c00e041053b0dc584e00e6997daa3", + "0x72337d25034d5a27fc10dce0c893fd29c1301d5b", + "0x65c20b3e50fc7575fdea5a76f99a2c4140155531", + "0x58f7ff14700a754a652b3e6f5edcb51e617c4e86", + "0xf40cca17034fa8a698bc5fa7e7a5caf0b298452f", + "0xbe4cf8e9d23cd50cdc3d56e6d6fcb75bfbbd2f67", + "0xacd59e854adf632d2322404198624f757c868c97", + "0x9ced5f42f2f8fec2eddc7fbb360ee17ffd6bdc21", + "0x1e8c64fd8f94da1d0e23853118b7f73a7b467209", + "0x00a66045ccc58a3e79999d6de0f49809c577ed3c", + "0x2aa64e6d80390f5c017f0313cb908051be2fd35e", + "0x3f649dbfafbe454940b8a82c5058b8d176dd3871", + "0x7d85fcbb505d48e6176483733b62b51704e0bf95", + "0xfe6afe37c8563c653bacd30924d2251ea5bb9740", + "0xb2206f941a5206b55bfa33e665687523a8bced80", + "0x5b0ffbe6218a68208ee61bbc1282c2637f0bca16", + "0xf11704511975cc5908f6dbd89be922f5c86c1055", + "0xf9903c5d11e8f4bfebcf02747d53319e9c4a10c6", + "0xc8d92326bd4dd3bb1d4eae8355fca6833d64ea80", + "0xfbd39c29dbbbe1f3e563e2a8233262c29040efef", + "0xd74e6428cb1c29cd6ef8ce4da4932f47e88a0946", + "0x5bb73e04b810527b14b87c37eff3d62481f2d416", + "0xbd56efc637f8cb7133e304b3f929df9a6fa35468", + "0x488abaa18b5af3f24e9d69ca0eb3d6fd6ae4fbf1", + "0xaca2f322d69e07993e073c8730180fb139ca4446", + "0xb3313b023e68cda95d7b625200e1b0fe6335a0c2", + "0x778549eb292ac98a96a05e122967f22efa003707", + "0xcb77e0c9ca70c750fbe49c44f094e1f2dd4f599c", + "0x74d5c70117e9ea382b199c140ee0f05fbf9defd0", + "0x99272db5353e13c18b711d99d3a8f36ba459f747", + "0x3566f9562d1bc28df6c5dc744ab40ef11c56e4a5", + "0xef8df6d246cc9802ebf5afd55e77df152497fd7f", + "0x64bf8d9a93a5642ca93d98b5db88b2cc953c8426", + "0x5a6a84fb9063af1d83d0ac4809697a40715a1530", + "0xa9a3266f73d629c19189f461ac8ca42801b2288c", + "0xd5df3f5022572f461459fc62dbbfaa54191fc551", + "0xc971c4e2c7c2eac9a8358f6b9c3b923a93009f8f", + "0xb2e1462847e6244f9931915ea2294005643b4861", + "0x21339e2ce5ce1d7a19a28e774513619c0c6259da", + "0xc793ee64e6ac6239c527fd041a78a8fcaa245e4f", + "0xba6e1c26be2f8b46d9e7ab7573b99921ee81acf5", + "0x29e4c293963b6767755a84726a41d05117f16b7a", + "0x4acb075efd8540850d0a86c156e2c7778022a9ea", + "0xa5a44fafdddfd87038549fefb6c65fff8c59317e", + "0x228f308e90c36ef1acee0bec99061f9de65dbcdd", + "0xb5196c5a415d26672e9392d1fd9a37d43a9089b2", + "0x20429f7a5689c86a0f9a93124efa5cff37574dbf", + "0x5c0b7a237c91f28881afd7e0ccb1d33937ac3271", + "0x6543c99d0e073c140fd08a741c6cfdcd1449da94", + "0x770ab2db2aad19fde1ce844d159624a6476438c8", + "0x026b727b60d336806b87d60e95b6d7fad2443dd6", + "0xdbeb3b980ef88e9f5e59c8c9bb4d261d6736d0c4", + "0xc624434420f6cbe835d6358a8223b78432773ced", + "0x64c5c700e87b6485f51f096a8d06fab8084c8632", + "0x08e3f287adba42a26904919917bc7907f1f0df18", + "0x2e57674ea14fa08a6dda865dd139ce3924ab5951", + "0xcee0feb4b08bad3e86bdc3f01a5cdd9e7ca72427", + "0x2619c03753bbfebfe2a5b4b3b7ca6d1a042af9ce", + "0x8b580433568e521ad351b92b98150c0c65ce69b7", + "0x28d604a4205549ea2510d03f2addc673ded2187f", + "0xf60581c6dba9a2bd660b7a83f73705a5788158b6", + "0x1ee8e6a0e949ecab1f66636601d6f36488f16b78", + "0xc34d7c4ddeafb22f22a482a71676e00edc58790a", + "0x2812b1e084a0214cc15f0c873f60514c0902ff8a", + "0xe3dd2e59f26f9793f50183c1c405c0134b638c2d", + "0x0bb5e2eab6cb24c40392783847e23e760b8b986b", + "0x255f7d91b2dddabbe044c64840460a1b7285ad5f", + "0xb3bcaee28ecd97745737e756cf1bafed9dbca88d", + "0xe5caf4d7cbc6b80ae5fd072ab17aa4e91a5dd495", + "0xcc3cb5c01a3cdcd5eb8ebf87e2ec088147bada8a", + "0xfa2199d17b5fae567bf82a0806921a20ddffe8d8", + "0x8b577d9f981da6fc6e33de495a1465ee302f67a5", + "0x8f791f061d7f36dc07de081ad88f87d71be1585e", + "0x7a749c38f16388085e3e2356d7a074f18117d780", + "0xd2bc5cb641ae6f7a880c3dd5aee0450b5210be23", + "0xc163041d404e7657530491577665e40da84c9bd7", + "0x1375847014ba64517210cbfe185808232cf45d4a", + "0x8f51dc0791cddddce08052fff939eb7cf0c17856", + "0x9f75582d2be13b5ee454161ee394daada8b39efa", + "0x302e2a0d4291ac14aa1160504ca45a0a1f2e7a5c", + "0x11d86e90aba195ca20905ab2d3745ed9faad5a2d", + "0x4461eabec259e164ad5da873d5fc4c0e1fe8e6b5", + "0xcbd4d0241481b49158077e8833afdcaec7d9d804", + "0xdfbecc0b4aef80b96da27ab483feb0892472eac2", + "0x6c19c7ee70a280693199ffd79daa7d114bbfa898", + "0xe73a198e7c07c418d7dcfd42f5e953074abbd125", + "0x7d03c5c37f77fd01211334b9115ca108c84e8f3b", + "0xbad8bcc9eb5749829cf12189fdd5c1230d6c85e8", + "0x76a6d08b82034b397e7e09dae4377c18f132bbb8", + "0xd0bfb40f057322ff734a75cbe2f79b9e5c7e4cb3", + "0xe04885c3f1419c6e8495c33bdcf5f8387cd88846", + "0x87690be28b65f13394741c2c2be5a6bdb0505039", + "0xd26a3f686d43f2a62ba9eae2ff77e9f516d945b9", + "0x884ff907d5fb8bae239b64aa8ad18ba3f8196038", + "0xbc5b552641e5d203f0a6c230aa9dc14da7450053", + "0xca3589b6dc6b6ae9da653b82b5cf1dd2806db087", + "0xf4c6a5df9050b15a21aabccbc84ccb31fbdc0846", + "0xd6b97e042d03edbdc100eb55fbe43eb75f2e3036", + "0x2c8467da8b7b3d0a7e3886f6cb6697c49571ff66", + "0xd9bb77e8a0095ba2b3c5649d21ea91221e010abc", + "0x1d3bf13f8f7a83390d03db5e23a950778e1d1309", + "0xd662fa474c0a1346a26374bb4581d1f6d3fb2d94", + "0xd714dd60e22bbb1cbafd0e40de5cfa7bbdd3f3c8", + "0x1df428833f2c9fb1ef098754e5d710432450d706", + "0xcf61ebd3613684a53e48ee85d8fbc80c5156c479", + "0x67243d6c3c3bdc2f59d2f74ba1949a02973a529d", + "0x03f11c7a45ba8219c87f312eecb07287c2095302", + "0x0cb27e883e207905ad2a94f9b6ef0c7a99223c37", + "0x7b8b83e68c73aa4f00e6ed1a505de15147ad5764", + "0x83ab8e31df35aa3281d630529c6f4bf5ac7f7abf", + "0xff16d64179a02d6a56a1183a28f1d6293646e2dd", + "0xced608aa29bb92185d9b6340adcbfa263dae075b", + "0x1c9f765c579f94f6502acd9fc356171d85a1f8d0", + "0x05a1ff0a32bc24265bcb39499d0c5d9a6cb2011c", + "0xd1e19c472bf71fcb1016705d2d4cda041bbf0a06", + "0x303a6b190768f91c5823d24c267fe1778ac6c56a", + "0x516cafd745ec780d20f61c0d71fe258ea765222d", + "0x7136fbddd4dffa2369a9283b6e90a040318011ca", + "0xe4b420f15d6d878dcd0df7120ac0fc1509ee9cab", + "0x843a3c4ed93fb1f1335d5d174745551468106715", + "0x4286d9da0d8085b8a7a34fa49a87b900845cc2f8", + "0x84674aa65cc7734f12b690ce46aec1027b434211", + "0xd705eab54190c7943115607f2b7573e115a413a0", + "0x644b63e356291d549ceae65c8be6fe2f9ad7dcd6", + "0xaa163c47065c22d17ed1c47e3e244337d2056c17", + "0x7e1b9a73a1deaa3fdfaf80a3c3a545936d79eda5", + "0xf145929a2f094840a797851cd306ae70c2b51986", + "0x6379828b3c5acdc2badc6bbca10357fb0e36aeab", + "0x438615976c27b2b44f57ced3ac73b5ac113b8730", + "0xf54f4815f62ccc360963329789d62d3497a121ae", + "0x4fd6b3204a6ab48978018b25821682f090840333", + "0xe31bc121ef5b129a01b0f22ed2b214122c4abb3e", + "0x4e934aa13be79944e5cb53257c41a3798ad221c8", + "0x4339769e5d64228a063a19ea5244633caeeccdaf", + "0x2dcd860489cd099fb966d1729f890b8c5c997908", + "0xd90a9ab5fee2c5d3d4cd1fd508ac6e1b88c7315e", + "0x6f95e0fe96c1cfb979dc5c6528d4c50b13c9f52c", + "0x9bdf2c3bb976a6072dd287dac8e762e4c7760ef8", + "0x19b4effb433abacca77e79c7e7b3a33cc2996c5c", + "0x6d92b4284e31a0e30bceecab483e058a0089a29f", + "0xc003a79dc66c11e2567f8ae8493116abed6a67a0", + "0x4638349a09683bcebfc3c62c3dbe41b4b70e407b", + "0x47887e3695cae28f028772082d88598cd07bef12", + "0xd592d65f52db0323a93a929462a0f63b540cb324", + "0xa25211b64d041f690c0c818183e32f28ba9647dd", + "0x7650c86ba29782182e4b111f8daa8de8a3c8e22d", + "0x06582c5cd9ebe65982850f506bc9acb9b252e478", + "0xb548e7b80d321cc904cd8a2d9f774c59d96d83f7", + "0xcb3af325e2d0bafc3f082dcc6501f0140f4315ce", + "0x4c71f7b3e2f5140e933420b65459430c6eb368dc", + "0x2afba221c12c35cb25a27966dbd5e3770e6194e5", + "0x75de9f13184a3dd1cf32843fe2db973e275b035e", + "0x595c51223860baf322837269d69dc76016c6eb1d", + "0xb067c8705d9b59fcc73b3abd11d3aed36172aee3", + "0xdd31db93082a3a71b98d37ba26230f8734bd63c3", + "0x0ea26051f7657d59418da186137141cea90d0652", + "0xa906c85b7e809b79c5e69d485693b44d65b1b252", + "0x22300aadf0cce33a4b993e5d2a1bb24409bab8e1", + "0xc2fb4b3ea53e10c88d193e709a81c4dc7aec902e", + "0x86213f1cf0a501857b70df35c1cb3c2ecf112844", + "0x7899d9b1181cbb427b0b1be0684c096c260f7474", + "0x6ab7b4550698fe1af0c7cd9c1ed69eb383c48257", + "0xa31895c358d899030a1eb78feaadf28c8000cf47", + "0x03d8ac6b92aac24549eb583b969d93bc49f9ddb3", + "0x6718c0b9061159823440abf4635707b2ed8e5ca8", + "0xc78a95019c52ad9856aa764d6908e88d5b7930ee", + "0x2b88fd6240e0ce71e3048b612088e76ab8d7d634", + "0xc8b0d32bc09fb11c12c82582825c1e6b624822b8", + "0xb749a586080436e616f097f193ba9cb6a25e7ea6", + "0xa9d20b435a85faaa002f32d66f7d21564130e9cf", + "0xf3ad97364bccc3ea0582ede58c363888f8c4ec85", + "0x63c3329720b993dec8af0018fb083dcba94e49e5", + "0x99c9bc20d9f57a0c19136e10987fb234abdf3a5c", + "0x7986debcbc2709435461cbe6e0d73f62c19bfa95", + "0x5833869fdeb4d371b854d7474f5f84b43320fd05", + "0x01242a2e4f27b42d564d3668a58d98baa751b3ce", + "0x986e92868a27548a31e88f7692e746cd7e86f39a", + "0xaeb99a255c3a243ab3e4f654041e9bf5340cf313", + "0x8270a0febba70899419e76a98bf2b5dc15fe2ff4", + "0x0d0c0941e2578da2b9e2ad03619615f7eebc48f7", + "0x7c1e9430655963129a0b6edecd8398c06429209f", + "0x48e206fd64ddbb9714be058ca6ea6f1479515624", + "0xe7a8dea3c2521a883d01e1b714b4480e64322462", + "0x8bb2884ee943111aa5119a36b30085a15a66c972", + "0x2399a1758afd2e061134b78ad1965e4204dbc0b8", + "0x4059f3c0064cd380276de8dbab6935005535eed6", + "0x7df96b926f4e321a2358bb8d9ecaba88fe7ff9d5", + "0x9eca7b82e857e92b05b6c7d904e5a26b4b020687", + "0xfe324b463ba927af7d08c34fd4abbd267b133ef0", + "0x21fa7f8238a84565dafc7681eda644009ca15ae7", + "0x092cff73c77a9de794d25b0088ded0e430733dbb", + "0x0a10998913b2e1786b7d0e376611781b28387cc4", + "0xd388aa414e0788f5d27fb8f2cc3f1dadd7b3b507", + "0x3bb9ef68c0e8a291f7730bb6da37ad682331f380", + "0xd164572b07a7a022facd4b0a63a72c9280ad78e2", + "0x63c3774531ef83631111fe2cf01520fb3f5a68f7", + "0x51f956b281b282fceb2635f295200157a22b920b", + "0x5c05740df496ddc881642566c431b88bdf60842e", + "0x09988e9aeb8c0b835619305abfe2ce68fea17722", + "0x54becc7560a7be76d72ed76a1f5fee6c5a2a7ab6", + "0x92ed343af03bae63a7ac938edf570084a3392742", + "0x775af9b7c214fe8792ab5f5da61a8708591d517e", + "0xb170a41f2523220a12f84f17a54bd31953d98027", + "0x461acd846b4cc249b96242f1ab7c72a39d94747c", + "0xfc9265a28f66cf4561d74a4e25d7bbd3f482b8e6", + "0x3a2fac2ab3cf4bc342910eac744cc68c8b9a69bf", + "0xc6e70206e0ab82d82933339b820feb124ec7ec5c", + "0x924657b110248cc5456562232351c49f795914c9", + "0x7bc48221928f11184b376da7a57650768dfd3332", + "0x23c0c5e0d894d006e4fe4520b6c3d91b7f9ef7ab", + "0x1e90474d2e83e7b7dd45553156beb316845e66a4", + "0x9f000bfb33b0b63fce685b538b1a8af079b57d93", + "0x99e94142353411264731105fe38ff02dfe1f3c3a", + "0xb22981ba3fe1de2325935c91a3b717168fb86714", + "0xa61ed6d962af39adda5c405ce76f625b5c3f81dd", + "0x19d8da2674e8a025154153297ea3ab918debf96d", + "0x5cc2ec9378e4fd3e5b91a6fa338edcc8edd36ff8", + "0xbd4d5116d4795fd6f785f524c4dc9f28bc5ab308", + "0x44571d865d879ba75d8eabb4a08c01cc3fc36d3f", + "0xca85460d23bfb2e0079bab5aac84921d9d0762a4", + "0xfa9b7d81a8fcebe6e0ce2938963db2ccd3cd178f", + "0x41b0b75802b25fbc14e0157f9341473843c28e1e", + "0xacdbb883880850b7528edc58ce506d10fa5205c6", + "0xd1825dd9a5e49791fb7961ac3c4170deed5710b4", + "0xde0e98f0ffb36ec69df57b8cc7ac886f506e9ae6", + "0x4ef22a7356d96dbf95a7fdead4a196a668e75b38", + "0x7d2e4d645c0acc5a6bf596b612cab351864f4090", + "0xa64f2228ccec96076c82abb903021c33859082f8", + "0xbaba775a0400a5e442335ceaa4820edb1ded8f73", + "0xd6fcd2f85fe975bb9b0f3c1b1c6802bb09d33e43", + "0xc583789751910e39fd2ddb988ad05567bcd81334", + "0x745f933c33ab89d8f8437eacf176a2c8c0dd63a0", + "0x222cf246a3d56d61030ca49f5e6ffd6c20c3dc4f", +} +TIMEOUT_LIST = { + "0x85c7d244c6057d42c770ad85ae198fc5f47957fc", + "0xcd0d4cdb238eec15fcf4ff9d13d5a59051e507d7", + "0xab4e4f9c9c394d72dbae834075fd4519d7290456", + "0x76b4b27b47f211448964ed2cf92f731412602700", + "0x341c2f0e48087ce4e21ef0c75cd062629f496df8", + "0x50544864a4fe04fca8401937b849b48e1e53bcd8", + "0xc9c32a8c745bb66094b8b27a74be48e84fd1a8f3", + "0xd46e61d448568d00483d4e82b822381ee62c5774", + "0x87704a8cecbbf36f3d2f785ef953ed9affc82b65", + "0xacc3d48b216eafbe0f943167c3087e7f0c642d6e", + "0xebcf41fffa0aac1dc1b1d4d7c92a7d88694c4486", + "0xd454ed303748bb5a433388f9508433ba5d507030", + "0xa231a5ae629a0f15e1c1eb3ffbf813589e206926", + "0xb7025e51a5dcca0cb1c03f66fe31f5dcffd599aa", + "0x5c0e1377edf9a8e4f0430118ae5381024186f12d", + "0x5c1389dea2c418c1c0c10ef210851942a1d67fc4", + "0xd364055a2b9df6ca96970cbd6827991d741ee66a", + "0x6c16d2ba2464fa8026f26c0d5085f163279fb8ce", + "0xdb3926b5d93057b7ee5e16b7e8bc3778a78a3521", + "0x3c861c7ed852e584c671fabed429a03afc6a5513", + "0x3bece46ea4cb308b1b8d8f50cb1728d6a4a20808", + "0x4506568e4733f3062cfa9cdb952ba7904fc7d8bb", + "0xc6e18de9930a1b90f339e09c574a4123c3eace83", + "0x1cb82ccbb3965e4df7c446b44c20f90b1c6ce9b2", + "0xb986a0263b045b45765dad38a000334c1e10b6d7", + "0xa73064a00512aa5b6b9119cb4eb3fe3229269993", + "0x175e0ed70627644c35f310288fc8d6061f75ac76", + "0x3b6cce32ba37fa5689fab313371dcf044ecef536", + "0x529dc928e67d8a43133d10769b308f1d5a629401", + "0xf1bb436c29e46b1987bc825879ffc9c34ab97f99", + "0x14ddf441721297b97a4d5aa3304fb845bc35ec56", + "0xb38877b39b1f2221d36b11a82d7bb1e05084d67b", + "0x9bf829eb2dea8109b098353e7c91629c7fc77f2a", + "0x9030352e478cb1afcdf7235f806a3a13dc963d52", + "0xea8caeb4f367844eec65fa917ce10a91ee3e69d8", + "0x8c2b3e9946f57820cb2f11b30ce8010f8a74e8ec", + "0xf2c601710cedc357b752408b7e5023b84819f953", + "0x01935087b1da357a00cbdd1010b2b9647991fcf1", + "0x7c8afbbd7239fed03aaacfaf1e4e30503e90c2d0", + "0x93f38a40e451b17be5dc88898ece26c43a75b1c8", + "0xe2e464938c866203228aca1603c77d0279bda31d", + "0xd3fd8a8c4ff9c06c796d21658c6a1fcd91b2e1bb", + "0x26fa48591030e6e93f69877986a777feb0626ee8", + "0xec3f542313ac266968ce61a2e4e33b95dbee7397", + "0xc1f5bb071de011206781c71a8eebea7ada8d0df3", + "0xa8776899d2601a59a94e3aa057af778b8647f85c", + "0x387c4cc5cf37e917d307c957eaf7e5227e1bdaaa", + "0x3bc3dfa9ac77c38bb24f4283d9189ed6387158d1", + "0xe7eac2cb20079752875bc1c498ce53a5ed025ab9", + "0x8ddb01727e1b4e870d2d5643e653b74ca552dbe8", + "0x84c9fac77e6909450a625cbf11d1be9addc62e77", + "0x44c0fb885dd9f77693b44704dc40c4e31a308eea", + "0x1ee45e962af18a7a7bf3200d561269174b7b3b1c", + "0x71d0c96d7f574d1a734f5803a8f1ae839aa7a5d4", + "0xb4c8737dd9fd2a50f201ce2494d74c2c9bca3bb5", + "0x6cab1406d2e04cc3b442f7d585a85bacfb07ac6b", + "0xadd7b1e78f0e59d96135c5240e1175d5a7dc210d", + "0xac176d9a8dbae960e7b539f4a6e16ad9003ab37a", + "0xcff9d27eba4b7ffba8e9d33fc3f894cb095b54f7", + "0x92af24fac1326517ce2e536e2afaf72419103422", + "0x0c1be517a0d9d888c6c686ef4e4b74bbe496d047", + "0xc19c63d295e19d8e3be7d4fd32b6e29105bbacba", + "0xab1c327651a974890a21d79fac2c6150d44c3d10", + "0xe04f0abe1933499ff1d075fa10738ce7aad26b1a", + "0xf7a3bc9e0962e15a0a4dd5260263cf0deea0808a", + "0x65cbb0d8dae3d9e0145ef4ed72b6e9e8367fc1af", + "0xc0373876c2cc38fc2f75f4db8bf5d973fa4d7e29", + "0x6e0ef43c2b5bda0e145033a4164d93f8d06d8538", + "0x76e10e91772c99b862ff005b7fa934322d0c3df6", + "0x9d5b61a09065622ac073fcebd8058121fc34f755", + "0xcba6f488f384be185fe2cf77915c3d7a3e9d5c57", + "0x3c01c79bf796196e1d8ccb6e486c6d246f4a48a8", + "0xb2fd1ff783e179144374d478436d6f2bb297a06a", + "0x1e3832d739cc563da1a98d0e3ff903e49baba86c", + "0x869d091c2c558810cb22a210c6a7f964898d72a1", + "0xcf544ed05e9c29c3ea3335111739e21d98945260", + "0x4ea8e77c5812e15ce572be6df0715b80a069f420", + "0x9b6f53d2bec7416722412a8d1e48aa3bae84325f", + "0x0acc85d306d141ddf6ada4622f1854422cb542e6", + "0x642c0b9eddd83dcfa735976dd1cc844c0b1204c6", + "0x10343b076e2bcac3a66dc7e72227c6591f5b7df4", + "0x7a62729d31945904deb5cdd646532e4683ed70c0", + "0xf48478f4d08387611ec93fa8b25c5e1ab80cf122", + "0x7e5f476de2886bb37ed92cec195cb9ce311bfe50", + "0x3fe4a67c56f2899c878b05af8944398bbfa85156", + "0x3afcb2b25ce7c76c15bb4c4c43780f76b6181479", + "0xcdec2c8682840d593cdb221634c16dc6aca77fe9", + "0x9052d9d9e30afa79a8445b25acfc6141b2075737", + "0xef96945175b17d7c13f5e534f6953fa94f0b7695", + "0x080b0a14d0d84cee92ad2edc8a69e494c842a6a5", + "0xd08afa644e562e1b3d6e2385fdeb67912410f04f", + "0x990b415088ad6df8db9b7ed7a561b6c9b8af5875", + "0x2fee53687906ba239602ed42cceee2ac8a4ada36", + "0x98940dd82b5e66d8ec340110d6e15be8a4086b64", + "0x8100b643fa91d7a05704913a1242b2e2c2b7fb7c", + "0x3fbcec42405391b1fb377664daa5ae7bc9ba7bf5", + "0x8c8296a0042e842cb865dffd94678c941fd24bae", + "0xf5c2087877218aa979dd0e2e5108837199af44d2", + "0x3ffd0c300fa4a021364ae7e85a7b0d3a02133f99", +} +TIMEOUT_LIST_NOT_MAINNET = { + "0xdf486eec7b89c390569194834a2f7a71da05ee13", + "0x689f1a51c177cce66e3afdca4b1ded7721f531f9", + "0x018d43ac91432d00c4ad1531c98b6ccd2b352538", +} GUEST_LIST_STAMP_PROVIDERS = [ "AllowList#OctantFinal", @@ -566,3 +990,12 @@ "AllowList#OctantEpochOne", "AllowList#OctantEpochThree", ] + +GTC_STAKING_STAMP_PROVIDERS_AND_SCORES = { + "SelfStakingBronze": 1.0, + "SelfStakingSilver": 2.0, + "SelfStakingGold": 3.0, + "BeginnerCommunityStaker": 1.5, + "ExperiencedCommunityStaker": 2.5, + "TrustedCitizen": 4.0, +} diff --git a/backend/app/engine/octant_rewards/leftover/__init__.py b/backend/app/engine/octant_rewards/leftover/__init__.py index 144069a783..e9295e963c 100644 --- a/backend/app/engine/octant_rewards/leftover/__init__.py +++ b/backend/app/engine/octant_rewards/leftover/__init__.py @@ -18,3 +18,6 @@ class Leftover(ABC): @abstractmethod def calculate_leftover(self, payload: LeftoverPayload) -> int: pass + + def extract_unused_matched_rewards(self, *args, **kwargs) -> int: + return 0 diff --git a/backend/app/engine/octant_rewards/leftover/with_ppf_and_unused.py b/backend/app/engine/octant_rewards/leftover/with_ppf_and_unused.py index 3faf8085b2..3f0d57acc3 100644 --- a/backend/app/engine/octant_rewards/leftover/with_ppf_and_unused.py +++ b/backend/app/engine/octant_rewards/leftover/with_ppf_and_unused.py @@ -15,3 +15,14 @@ def calculate_leftover(self, payload: LeftoverPayload) -> int: - payload.total_withdrawals + unused_matched_rewards ) + + def extract_unused_matched_rewards(self, leftover, payload) -> int: + extra_individual_rewards = int(payload.ppf / 2) + return ( + leftover + - payload.staking_proceeds + + payload.operational_cost + + payload.community_fund + + extra_individual_rewards + + payload.total_withdrawals + ) diff --git a/backend/app/engine/projects/rewards/__init__.py b/backend/app/engine/projects/rewards/__init__.py index 95d704bd68..5c88af2a5f 100644 --- a/backend/app/engine/projects/rewards/__init__.py +++ b/backend/app/engine/projects/rewards/__init__.py @@ -1,6 +1,7 @@ from abc import ABC, abstractmethod +from collections import namedtuple from dataclasses import dataclass, field -from typing import List, Optional +from typing import List, Optional, Dict from dataclass_wizard import JSONWizard @@ -41,6 +42,11 @@ class ProjectRewardsResult: threshold: Optional[int] = None +AllocationsBelowThreshold = namedtuple( + "AllocationsBelowThreshold", ["below_threshold", "total"] +) + + @dataclass class ProjectRewards(ABC): projects_allocations: ProjectAllocations = field(init=False) @@ -54,3 +60,12 @@ def calculate_project_rewards( def calculate_threshold(self, total_allocated: int, projects: List[str]) -> None: return None + + def get_total_allocations_below_threshold( + self, allocations: Dict[str, List[AllocationItem]] + ) -> AllocationsBelowThreshold: + allocations_sum = sum( + sum(int(item.amount) for item in project_allocations) + for project_allocations in allocations.values() + ) + return AllocationsBelowThreshold(0, allocations_sum) diff --git a/backend/app/engine/projects/rewards/allocations/__init__.py b/backend/app/engine/projects/rewards/allocations/__init__.py index 5a6856393a..3123dca6ae 100644 --- a/backend/app/engine/projects/rewards/allocations/__init__.py +++ b/backend/app/engine/projects/rewards/allocations/__init__.py @@ -33,10 +33,9 @@ def _calc_allocations( ) -> Union[int, Decimal]: ... - def group_allocations_by_projects( + def segregate_allocations( self, payload: ProjectAllocationsPayload - ) -> (Dict[str, List], List[ProjectSumAllocationsDTO], Union[int, Decimal]): - result_allocations = [] + ) -> Dict[str, List]: grouped_allocations = { key: list(group) for key, group in groupby( @@ -44,7 +43,14 @@ def group_allocations_by_projects( key=lambda a: a.project_address, ) } + return grouped_allocations + + def group_allocations_by_projects( + self, payload: ProjectAllocationsPayload + ) -> (Dict[str, List], List[ProjectSumAllocationsDTO], Union[int, Decimal]): + grouped_allocations = self.segregate_allocations(payload) + result_allocations = [] total_plain_qf = 0 for project_address, project_allocations in grouped_allocations.items(): project_allocations = self._calc_allocations(project_allocations) diff --git a/backend/app/engine/projects/rewards/preliminary.py b/backend/app/engine/projects/rewards/preliminary.py index dd6fdc52b6..6f5c5866ba 100644 --- a/backend/app/engine/projects/rewards/preliminary.py +++ b/backend/app/engine/projects/rewards/preliminary.py @@ -1,20 +1,23 @@ from dataclasses import field, dataclass from decimal import Decimal -from typing import List +from typing import List, Dict from app.engine.projects.rewards import ( ProjectRewardsPayload, ProjectRewardsResult, ProjectRewards, ProjectRewardDTO, + AllocationsBelowThreshold, ) from app.engine.projects.rewards.allocations import ( ProjectAllocations, ProjectAllocationsPayload, + AllocationItem, ) from app.engine.projects.rewards.allocations.preliminary import ( PreliminaryProjectAllocations, ) +from app.engine.projects.rewards.leverage.preliminary import PreliminaryLeverage from app.engine.projects.rewards.threshold import ( ProjectThreshold, ProjectThresholdPayload, @@ -22,7 +25,6 @@ from app.engine.projects.rewards.threshold.preliminary import ( PreliminaryProjectThreshold, ) -from app.engine.projects.rewards.leverage.preliminary import PreliminaryLeverage @dataclass @@ -42,6 +44,13 @@ def calculate_threshold(self, total_allocated: int, projects: List[str]) -> int: ) ) + def get_total_allocations_below_threshold( + self, allocations: Dict[str, List[AllocationItem]] + ) -> AllocationsBelowThreshold: + return self.projects_threshold.get_total_allocations_below_threshold( + allocations + ) + def calculate_project_rewards( self, payload: ProjectRewardsPayload ) -> ProjectRewardsResult: diff --git a/backend/app/engine/projects/rewards/threshold/__init__.py b/backend/app/engine/projects/rewards/threshold/__init__.py index 4295633921..0e9d4ada16 100644 --- a/backend/app/engine/projects/rewards/threshold/__init__.py +++ b/backend/app/engine/projects/rewards/threshold/__init__.py @@ -1,5 +1,9 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from typing import List, Dict + +from app.engine.projects.rewards import AllocationsBelowThreshold +from app.engine.projects.rewards import AllocationItem @dataclass @@ -13,3 +17,9 @@ class ProjectThreshold(ABC): @abstractmethod def calculate_threshold(self, payload: ProjectThresholdPayload) -> int: pass + + @abstractmethod + def get_total_allocations_below_threshold( + self, allocations: Dict[str, List[AllocationItem]] + ) -> AllocationsBelowThreshold: + pass diff --git a/backend/app/engine/projects/rewards/threshold/preliminary.py b/backend/app/engine/projects/rewards/threshold/preliminary.py index 2c68c221da..534ae6deb0 100644 --- a/backend/app/engine/projects/rewards/threshold/preliminary.py +++ b/backend/app/engine/projects/rewards/threshold/preliminary.py @@ -1,9 +1,12 @@ from dataclasses import dataclass +from typing import List, Dict +from app.engine.projects.rewards import AllocationsBelowThreshold from app.engine.projects.rewards.threshold import ( ProjectThreshold, ProjectThresholdPayload, ) +from app.engine.projects.rewards import AllocationItem @dataclass @@ -19,3 +22,24 @@ def calculate_threshold(self, payload: ProjectThresholdPayload) -> int: if payload.projects_count else 0 ) + + def get_total_allocations_below_threshold( + self, allocations: Dict[str, List[AllocationItem]] + ) -> AllocationsBelowThreshold: + summed_allocations = { + project: sum(map(lambda value: int(value.amount), values)) + for project, values in allocations.items() + } + total_allocations = sum(summed_allocations.values()) + no_projects = len(allocations.keys()) + + threshold = self.calculate_threshold( + ProjectThresholdPayload(total_allocations, no_projects) + ) + + allocations_below_threshold = 0 + for allocations_sum_for_project in summed_allocations.values(): + if allocations_sum_for_project < threshold: + allocations_below_threshold += allocations_sum_for_project + + return AllocationsBelowThreshold(allocations_below_threshold, total_allocations) diff --git a/backend/app/exceptions.py b/backend/app/exceptions.py index 40329daea8..29546003f3 100644 --- a/backend/app/exceptions.py +++ b/backend/app/exceptions.py @@ -346,3 +346,27 @@ class InvalidDelegationForLockingAddress(OctantException): def __init__(self): super().__init__(self.description, self.code) + + +class InvalidDelegationRequest(OctantException): + code = 400 + description = "Invalid delegation request" + + def __init__(self): + super().__init__(self.description, self.code) + + +class InvalidAddressFormat(OctantException): + code = 400 + description = "Invalid address format" + + def __init__(self): + super().__init__(self.description, self.code) + + +class InvalidProjectDetailsInput(OctantException): + code = 400 + description = "Invalid input for projects details" + + def __init__(self): + super().__init__(self.description, self.code) diff --git a/backend/app/infrastructure/__init__.py b/backend/app/infrastructure/__init__.py index fe9c5b9b8b..509cdecc49 100644 --- a/backend/app/infrastructure/__init__.py +++ b/backend/app/infrastructure/__init__.py @@ -11,6 +11,8 @@ from app.infrastructure.exception_handler import ExceptionHandler from flask import current_app as app +from app.exceptions import InvalidAddressFormat + default_decorators = { "delete": ExceptionHandler.print_stacktrace_on_exception(True, True), "get": ExceptionHandler.print_stacktrace_on_exception(False, True), @@ -30,8 +32,12 @@ def _add_address_canonization(handler): def _decorated(*args, **kwargs): field_value = kwargs.get(field_name) if force or field_value is not None: - updated_field = to_checksum_address(field_value) - kwargs.update([(field_name, updated_field)]) + try: + updated_field = to_checksum_address(field_value) + except ValueError: + raise InvalidAddressFormat() + else: + kwargs.update([(field_name, updated_field)]) return handler(*args, **kwargs) diff --git a/backend/app/infrastructure/apscheduler.py b/backend/app/infrastructure/apscheduler.py index 35430ba1f0..223d97f7ca 100644 --- a/backend/app/infrastructure/apscheduler.py +++ b/backend/app/infrastructure/apscheduler.py @@ -1,5 +1,6 @@ from flask import current_app as app +from app import exceptions from app.legacy.core import glm from app.legacy.core import vault from app.extensions import scheduler @@ -16,7 +17,10 @@ def vault_confirm_withdrawals(): with scheduler.app.app_context(): if app.config["VAULT_CONFIRM_WITHDRAWALS_ENABLED"]: app.logger.debug("Confirming withdrawals in Vault contract...") - vault.confirm_withdrawals() + try: + vault.confirm_withdrawals() + except exceptions.MissingSnapshot: + app.logger.warning("No snapshot found") @scheduler.task( diff --git a/backend/app/infrastructure/database/allocations.py b/backend/app/infrastructure/database/allocations.py index 9dec498d88..6307726c55 100644 --- a/backend/app/infrastructure/database/allocations.py +++ b/backend/app/infrastructure/database/allocations.py @@ -259,12 +259,13 @@ def get_allocation_request_by_user_and_epoch( def get_user_last_allocation_request(user_address: str) -> AllocationRequest | None: - return ( + result = ( AllocationRequest.query.join(User, User.id == AllocationRequest.user_id) .filter(User.address == user_address) .order_by(AllocationRequest.nonce.desc()) .first() ) + return result def get_user_allocation_epoch_count(user_address: str) -> int: diff --git a/backend/app/infrastructure/database/models.py b/backend/app/infrastructure/database/models.py index cada6115b5..4b3f4aae33 100644 --- a/backend/app/infrastructure/database/models.py +++ b/backend/app/infrastructure/database/models.py @@ -204,3 +204,19 @@ class UniquenessQuotient(BaseModel): @property def validated_score(self): return Decimal(self.score) + + +class ProjectsDetails(BaseModel): + """ + This model represents the details of a project that is consistent with data on the IPFS node. + Records here are created only by migrations. + """ + + __tablename__ = "project_details" + + id = Column(db.Integer, primary_key=True) + address = Column(db.String(42), nullable=False) + name = Column(db.String, nullable=False) + epoch = Column(db.Integer, nullable=False) + + __table_args__ = (UniqueConstraint("address", "epoch", name="uq_address_epoch"),) diff --git a/backend/app/infrastructure/database/projects_details.py b/backend/app/infrastructure/database/projects_details.py new file mode 100644 index 0000000000..018ba587b1 --- /dev/null +++ b/backend/app/infrastructure/database/projects_details.py @@ -0,0 +1,7 @@ +from typing import List + +from app.infrastructure.database.models import ProjectsDetails + + +def get_projects_details_for_epoch(epoch: int) -> List[ProjectsDetails]: + return ProjectsDetails.query.filter_by(epoch=epoch).all() diff --git a/backend/app/infrastructure/events.py b/backend/app/infrastructure/events.py index 78cc07ded7..fcff9bee44 100644 --- a/backend/app/infrastructure/events.py +++ b/backend/app/infrastructure/events.py @@ -34,6 +34,13 @@ def handle_connect(): {"project": project.address, "donors": _serialize_donors(donors)}, ) + for project in project_rewards: + donors = controller.get_all_donations_by_project(project.address) + emit( + "project_donors", + {"project": project.address, "donors": _serialize_donors(donors)}, + ) + @socketio.on("disconnect") def handle_disconnect(): diff --git a/backend/app/infrastructure/external_api/common.py b/backend/app/infrastructure/external_api/common.py index 462fd66e48..eaa545c7d7 100644 --- a/backend/app/infrastructure/external_api/common.py +++ b/backend/app/infrastructure/external_api/common.py @@ -1,4 +1,5 @@ import time +from http import HTTPStatus from typing import Callable, Dict from app.exceptions import ExternalApiException @@ -6,7 +7,7 @@ def retry_request( req_func: Callable, - status_code: int, + status_code: HTTPStatus, no_retries: int = 3, sleep_time: int = 1, **kwargs, diff --git a/backend/app/infrastructure/external_api/etherscan/helpers.py b/backend/app/infrastructure/external_api/etherscan/helpers.py index eadff16c23..a8d3a21e24 100644 --- a/backend/app/infrastructure/external_api/etherscan/helpers.py +++ b/backend/app/infrastructure/external_api/etherscan/helpers.py @@ -5,11 +5,15 @@ class EtherScanAPIMessages(StrEnum): STATUS_OK = "OK" + NO_TRANSACTIONS = "No transactions found" def raise_for_status(resp: requests.Response): resp.raise_for_status() - if resp.json()["message"] != EtherScanAPIMessages.STATUS_OK.value: + if resp.json()["message"] not in { + EtherScanAPIMessages.STATUS_OK.value, + EtherScanAPIMessages.NO_TRANSACTIONS.value, + }: raise requests.exceptions.RequestException( "Message not OK in the API response!" ) diff --git a/backend/app/infrastructure/routes/epochs.py b/backend/app/infrastructure/routes/epochs.py index 5c1968b12a..1c4b30fca2 100644 --- a/backend/app/infrastructure/routes/epochs.py +++ b/backend/app/infrastructure/routes/epochs.py @@ -3,7 +3,10 @@ from app.extensions import api, epochs from app.infrastructure import OctantResource, graphql -from app.modules.octant_rewards.controller import get_octant_rewards +from app.modules.octant_rewards.controller import ( + get_octant_rewards, + get_epoch_rewards_rate, +) ns = Namespace("epochs", description="Octant epochs") api.add_namespace(ns) @@ -106,6 +109,19 @@ def get(self): required=False, description="Community fund for the given epoch. It's calculated from staking proceeds directly.", ), + "donatedToProjects": fields.String( + required=False, + description="The amount of funds donated to projects. Includes MR and allocations.", + ), + }, +) + +epoch_rewards_rate_model = api.model( + "EpochRewardsRate", + { + "rewardsRate": fields.Float( + required=True, description="Rewards rate for the given epoch." + ) }, ) @@ -127,3 +143,18 @@ def get(self, epoch: int): app.logger.debug(f"Got: {stats}") return stats.to_dict() + + +@ns.route("/rewards-rate/") +@ns.doc( + description="Returns a rewards rate for given epoch. Returns data for all states of epochs.", + params={ + "epoch": "Epoch number", + }, +) +class EpochRewardsRate(OctantResource): + @ns.marshal_with(epoch_rewards_rate_model) + @ns.response(200, "Epoch's rewards rate successfully retrieved.") + def get(self, epoch: int): + app.logger.debug(f"Getting rewards rate for epoch {epoch}") + return {"rewardsRate": get_epoch_rewards_rate(epoch)} diff --git a/backend/app/infrastructure/routes/projects.py b/backend/app/infrastructure/routes/projects.py index bd2489a943..30263c8d86 100644 --- a/backend/app/infrastructure/routes/projects.py +++ b/backend/app/infrastructure/routes/projects.py @@ -1,4 +1,4 @@ -from flask import current_app as app +from flask import current_app as app, request from flask_restx import Namespace, fields from app.extensions import api @@ -7,6 +7,10 @@ from app.modules.projects.metadata.controller import ( get_projects_metadata, ) +from app.modules.projects.details import controller as projects_details_controller +from app.infrastructure.routes.validations.project_details_input import ( + validate_project_details_input, +) ns = Namespace("projects", description="Octant projects") api.add_namespace(ns) @@ -23,6 +27,26 @@ }, ) +project_model = api.model( + "Project", + { + "name": fields.String(required=True, description="Project name"), + "address": fields.String(required=True, description="Project address"), + "epoch": fields.String(required=True, description="Project epoch"), + }, +) + +projects_details_model = api.model( + "ProjectsDetails", + { + "projectsDetails": fields.List( + fields.Nested(project_model), + required=False, + description="Projects details", + ), + }, +) + @ns.route("/epoch/") @ns.doc( @@ -43,3 +67,40 @@ def get(self, epoch): "projectsAddresses": projects_metadata.projects_addresses, "projectsCid": projects_metadata.projects_cid, } + + +@ns.route("/details") +@ns.doc( + description="Returns projects details for a given epoch and searchPhrase.", + params={ + "epochs": "Epochs numbers (query parameter)", + "searchPhrases": "Search phrase (query parameter)", + }, +) +class ProjectsDetails(OctantResource): + @ns.marshal_with(projects_details_model) + @ns.response(200, "Projects metadata is successfully retrieved") + def get(self): + search_phrases = request.args.get("searchPhrases", "").split(",") + epochs = validate_project_details_input(request.args.get("epochs", "")) + + app.logger.debug( + f"Getting projects details for epochs {epochs} and search phrase {search_phrases}" + ) + projects_details = ( + projects_details_controller.get_projects_details_for_multiple_params( + epochs, search_phrases + ) + ) + app.logger.debug(f"Projects details for epochs {epochs}: {projects_details}") + + return { + "projectsDetails": [ + { + "name": project["name"], + "address": project["address"], + "epoch": project["epoch"], + } + for project in projects_details + ] + } diff --git a/backend/app/infrastructure/routes/user.py b/backend/app/infrastructure/routes/user.py index e43cfd2297..046b63fcf0 100644 --- a/backend/app/infrastructure/routes/user.py +++ b/backend/app/infrastructure/routes/user.py @@ -24,13 +24,17 @@ "UserAntisybilStatus", { "status": fields.String(required=True, description="Unknown or Known"), - "expires_at": fields.String( + "expiresAt": fields.String( required=False, description="Expiry date, unix timestamp" ), "score": fields.String( required=False, description="Score, parses as a float", ), + "isOnTimeOutList": fields.Boolean( + required=False, + description="Flag indicating whether user is on timeout list", + ), }, ) @@ -192,15 +196,25 @@ class AntisybilStatus(OctantResource): @ns.response(200, "User's cached antisybil status retrieved") def get(self, user_address: str): app.logger.debug(f"Getting user {user_address} cached antisybil status") + antisybil_status = get_user_antisybil_status(user_address) + app.logger.debug(f"User {user_address} antisybil status: {antisybil_status}") + if antisybil_status is None: return {"status": "Unknown"}, 404 - score, expires_at = antisybil_status + + score, expires_at, is_on_timeout_list = ( + antisybil_status.score, + antisybil_status.expires_at, + antisybil_status.is_on_timeout_list, + ) + return { "status": "Known", "score": score, - "expires_at": int(expires_at.timestamp()), + "expiresAt": int(expires_at.timestamp()), + "isOnTimeOutList": is_on_timeout_list, }, 200 @ns.doc( @@ -211,9 +225,9 @@ def get(self, user_address: str): @ns.response(504, "Could not refresh antisybil status. Upstream is unavailable.") def put(self, user_address: str): app.logger.info(f"Updating user {user_address} antisybil status") - score, expires_at = update_user_antisybil_status(user_address) + result = update_user_antisybil_status(user_address) app.logger.info( - f"User {user_address} antisybil status refreshed {[score, expires_at]}" + f"User {user_address} antisybil status refreshed {[result.score, result.expires_at]}" ) return {}, 204 diff --git a/backend/app/infrastructure/routes/validations/project_details_input.py b/backend/app/infrastructure/routes/validations/project_details_input.py new file mode 100644 index 0000000000..cdb28b7d83 --- /dev/null +++ b/backend/app/infrastructure/routes/validations/project_details_input.py @@ -0,0 +1,12 @@ +from typing import List + +from app.exceptions import InvalidProjectDetailsInput + + +def validate_project_details_input(epochs: str) -> List[int]: + try: + epochs = list(map(int, epochs.split(","))) + except ValueError: + raise InvalidProjectDetailsInput + + return epochs diff --git a/backend/app/logging.py b/backend/app/logging.py index 958eb9c0eb..239b8ec059 100644 --- a/backend/app/logging.py +++ b/backend/app/logging.py @@ -58,6 +58,11 @@ def config(app_level): "apscheduler.executors.default": { "level": "WARNING", }, + "uvicorn": { # Adding for the uvicorn logger (FastAPI) + "level": app_level, + "handlers": ["stdout", "stderr"], + "propagate": 0, + }, }, } diff --git a/backend/app/modules/common/synchronized.py b/backend/app/modules/common/synchronized.py new file mode 100644 index 0000000000..9a242c4d4d --- /dev/null +++ b/backend/app/modules/common/synchronized.py @@ -0,0 +1,14 @@ +from threading import Lock +from functools import wraps +from typing import Callable + + +def synchronized(wrapped: Callable): + lock = Lock() + + @wraps(wrapped) + def _wrap(*args, **kwargs): + with lock: + return wrapped(*args, **kwargs) + + return _wrap diff --git a/backend/app/modules/dto.py b/backend/app/modules/dto.py index 0bdbcf87c2..31247420be 100644 --- a/backend/app/modules/dto.py +++ b/backend/app/modules/dto.py @@ -53,6 +53,8 @@ class OctantRewardsDTO(JSONWizard): # Data available starting from Epoch 3 ppf: Optional[int] = None community_fund: Optional[int] = None + # Added after moving metrics from FE to BE + donated_to_projects: Optional[int] = None @dataclass(frozen=True) diff --git a/backend/app/modules/facades/confirm_multisig.py b/backend/app/modules/facades/confirm_multisig.py index a867db91dc..4281ad2ce4 100644 --- a/backend/app/modules/facades/confirm_multisig.py +++ b/backend/app/modules/facades/confirm_multisig.py @@ -7,8 +7,10 @@ ) from app.modules.user.allocations import controller as allocations_controller from app.modules.user.tos import controller as tos_controller +from app.modules.common.synchronized import synchronized +@synchronized def confirm_multisig(): """ This is a facade function that is used to confirm (i.e approve and apply) multisig approvals. diff --git a/backend/app/modules/modules_factory/current.py b/backend/app/modules/modules_factory/current.py index a95a8fa744..7ad35cbe47 100644 --- a/backend/app/modules/modules_factory/current.py +++ b/backend/app/modules/modules_factory/current.py @@ -15,6 +15,7 @@ UserAllocationNonceProtocol, ScoreDelegation, UniquenessQuotients, + ProjectsDetailsService, ) from app.modules.modules_factory.protocols import SimulatePendingSnapshots from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures @@ -44,7 +45,15 @@ from app.modules.withdrawals.service.finalized import FinalizedWithdrawals from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes -from app.constants import UQ_THRESHOLD_MAINNET, UQ_THRESHOLD_NOT_MAINNET +from app.constants import ( + UQ_THRESHOLD_MAINNET, + UQ_THRESHOLD_NOT_MAINNET, + TIMEOUT_LIST_NOT_MAINNET, + TIMEOUT_LIST, +) +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class CurrentUserDeposits(UserEffectiveDeposits, TotalEffectiveDeposits, Protocol): @@ -61,6 +70,7 @@ class CurrentServices(Model): simulated_pending_snapshot_service: SimulatePendingSnapshots multisig_signatures_service: MultisigSignatures projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService user_budgets_service: UpcomingUserBudgets score_delegation_service: ScoreDelegation uniqueness_quotients: UniquenessQuotients @@ -97,7 +107,10 @@ def create(chain_id: int) -> "CurrentServices": user_allocations = SavedUserAllocations() user_allocations_nonce = SavedUserAllocationsNonce() user_withdrawals = FinalizedWithdrawals() - user_antisybil_service = GitcoinPassportAntisybil() + + timeout_list = TIMEOUT_LIST if is_mainnet else TIMEOUT_LIST_NOT_MAINNET + user_antisybil_service = GitcoinPassportAntisybil(timeout_list=timeout_list) + tos_verifier = InitialUserTosVerifier() user_tos = InitialUserTos(verifier=tos_verifier) patron_donations = EventsBasedUserPatronMode() @@ -113,6 +126,7 @@ def create(chain_id: int) -> "CurrentServices": verifier=score_delegation_verifier, antisybil=user_antisybil_service, user_deposits_service=user_deposits, + timeout_list=timeout_list, ) multisig_signatures = OffchainMultisigSignatures( @@ -129,7 +143,7 @@ def create(chain_id: int) -> "CurrentServices": ) uq_threshold = UQ_THRESHOLD_MAINNET if is_mainnet else UQ_THRESHOLD_NOT_MAINNET uniqueness_quotients = PreliminaryUQ( - antisybil=GitcoinPassportAntisybil(), + antisybil=GitcoinPassportAntisybil(timeout_list=timeout_list), budgets=user_budgets, uq_threshold=uq_threshold, ) @@ -144,6 +158,7 @@ def create(chain_id: int) -> "CurrentServices": user_tos_service=user_tos, user_antisybil_service=user_antisybil_service, projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), user_budgets_service=user_budgets, score_delegation_service=score_delegation, uniqueness_quotients=uniqueness_quotients, diff --git a/backend/app/modules/modules_factory/finalized.py b/backend/app/modules/modules_factory/finalized.py index d5b470c3d6..20cb433f2b 100644 --- a/backend/app/modules/modules_factory/finalized.py +++ b/backend/app/modules/modules_factory/finalized.py @@ -13,6 +13,7 @@ WithdrawalsService, SavedProjectRewardsService, ProjectsMetadataService, + ProjectsDetailsService, ) from app.modules.octant_rewards.general.service.finalized import FinalizedOctantRewards from app.modules.projects.rewards.service.saved import SavedProjectRewards @@ -26,6 +27,9 @@ StaticProjectsMetadataService, ) from app.pydantic import Model +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class FinalizedOctantRewardsProtocol(OctantRewards, Leverage, Protocol): @@ -52,6 +56,7 @@ class FinalizedServices(Model): withdrawals_service: WithdrawalsService project_rewards_service: SavedProjectRewardsService projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService @staticmethod def create() -> "FinalizedServices": @@ -75,4 +80,5 @@ def create() -> "FinalizedServices": withdrawals_service=withdrawals_service, project_rewards_service=SavedProjectRewards(), projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), ) diff --git a/backend/app/modules/modules_factory/finalizing.py b/backend/app/modules/modules_factory/finalizing.py index 44e409221c..3c38584c7f 100644 --- a/backend/app/modules/modules_factory/finalizing.py +++ b/backend/app/modules/modules_factory/finalizing.py @@ -13,6 +13,7 @@ WithdrawalsService, SavedProjectRewardsService, ProjectsMetadataService, + ProjectsDetailsService, ) from app.modules.octant_rewards.general.service.pending import PendingOctantRewards from app.modules.octant_rewards.matched.pending import PendingOctantMatchedRewards @@ -29,6 +30,9 @@ StaticProjectsMetadataService, ) from app.pydantic import Model +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class FinalizingOctantRewards(OctantRewards, Leverage, Protocol): @@ -50,6 +54,7 @@ class FinalizingServices(Model): withdrawals_service: WithdrawalsService project_rewards_service: SavedProjectRewardsService projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService @staticmethod def create() -> "FinalizingServices": @@ -90,4 +95,5 @@ def create() -> "FinalizingServices": withdrawals_service=withdrawals_service, project_rewards_service=SavedProjectRewards(), projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), ) diff --git a/backend/app/modules/modules_factory/future.py b/backend/app/modules/modules_factory/future.py index 51fd029936..64ea69165c 100644 --- a/backend/app/modules/modules_factory/future.py +++ b/backend/app/modules/modules_factory/future.py @@ -1,4 +1,8 @@ -from app.modules.modules_factory.protocols import OctantRewards, ProjectsMetadataService +from app.modules.modules_factory.protocols import ( + OctantRewards, + ProjectsMetadataService, + ProjectsDetailsService, +) from app.modules.octant_rewards.general.service.calculated import ( CalculatedOctantRewards, ) @@ -10,11 +14,15 @@ StaticProjectsMetadataService, ) from app.pydantic import Model +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class FutureServices(Model): octant_rewards_service: OctantRewards projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService @staticmethod def create() -> "FutureServices": @@ -24,4 +32,5 @@ def create() -> "FutureServices": effective_deposits=ContractBalanceUserDeposits(), ), projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), ) diff --git a/backend/app/modules/modules_factory/pending.py b/backend/app/modules/modules_factory/pending.py index 5989d42d98..6b76c46406 100644 --- a/backend/app/modules/modules_factory/pending.py +++ b/backend/app/modules/modules_factory/pending.py @@ -20,6 +20,7 @@ MultisigSignatures, ProjectsMetadataService, UniquenessQuotients, + ProjectsDetailsService, ) from app.modules.multisig_signatures.service.offchain import OffchainMultisigSignatures from app.modules.octant_rewards.general.service.pending import PendingOctantRewards @@ -48,7 +49,15 @@ from app.modules.withdrawals.service.pending import PendingWithdrawals from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes -from app.constants import UQ_THRESHOLD_MAINNET, UQ_THRESHOLD_NOT_MAINNET +from app.constants import ( + UQ_THRESHOLD_MAINNET, + UQ_THRESHOLD_NOT_MAINNET, + TIMEOUT_LIST, + TIMEOUT_LIST_NOT_MAINNET, +) +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class PendingOctantRewardsService(OctantRewards, Leverage, Protocol): @@ -87,6 +96,7 @@ class PendingServices(Model): project_rewards_service: PendingProjectRewardsProtocol multisig_signatures_service: MultisigSignatures projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService uniqueness_quotients: UniquenessQuotients @staticmethod @@ -98,8 +108,9 @@ def create(chain_id: int) -> "PendingServices": is_mainnet = compare_blockchain_types(chain_id, ChainTypes.MAINNET) uq_threshold = UQ_THRESHOLD_MAINNET if is_mainnet else UQ_THRESHOLD_NOT_MAINNET + timeout_list = TIMEOUT_LIST if is_mainnet else TIMEOUT_LIST_NOT_MAINNET uniqueness_quotients = PreliminaryUQ( - antisybil=GitcoinPassportAntisybil(), + antisybil=GitcoinPassportAntisybil(timeout_list=timeout_list), budgets=saved_user_budgets, uq_threshold=uq_threshold, ) @@ -159,5 +170,6 @@ def create(chain_id: int) -> "PendingServices": project_rewards_service=project_rewards, multisig_signatures_service=multisig_signatures, projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), uniqueness_quotients=uniqueness_quotients, ) diff --git a/backend/app/modules/modules_factory/pre_pending.py b/backend/app/modules/modules_factory/pre_pending.py index 46867a8be2..e5bfd8c63b 100644 --- a/backend/app/modules/modules_factory/pre_pending.py +++ b/backend/app/modules/modules_factory/pre_pending.py @@ -9,6 +9,7 @@ UserEffectiveDeposits, SavedProjectRewardsService, ProjectsMetadataService, + ProjectsDetailsService, ) from app.modules.octant_rewards.general.service.calculated import ( CalculatedOctantRewards, @@ -24,6 +25,9 @@ ) from app.pydantic import Model from app.shared.blockchain_types import compare_blockchain_types, ChainTypes +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, +) class PrePendingUserDeposits(UserEffectiveDeposits, AllUserEffectiveDeposits, Protocol): @@ -36,6 +40,7 @@ class PrePendingServices(Model): pending_snapshots_service: PendingSnapshots project_rewards_service: SavedProjectRewardsService projects_metadata_service: ProjectsMetadataService + projects_details_service: ProjectsDetailsService @staticmethod def create(chain_id: int) -> "PrePendingServices": @@ -63,4 +68,5 @@ def create(chain_id: int) -> "PrePendingServices": pending_snapshots_service=pending_snapshots_service, project_rewards_service=SavedProjectRewards(), projects_metadata_service=StaticProjectsMetadataService(), + projects_details_service=StaticProjectsDetailsService(), ) diff --git a/backend/app/modules/modules_factory/protocols.py b/backend/app/modules/modules_factory/protocols.py index f761b02585..0bb8f4957d 100644 --- a/backend/app/modules/modules_factory/protocols.py +++ b/backend/app/modules/modules_factory/protocols.py @@ -19,6 +19,7 @@ ) from app.modules.history.dto import UserHistoryDTO from app.modules.multisig_signatures.dto import Signature +from app.modules.projects.details.service.projects_details import ProjectsDetailsDTO @runtime_checkable @@ -226,6 +227,14 @@ def get_projects_metadata(self, context: Context) -> ProjectsMetadata: ... +@runtime_checkable +class ProjectsDetailsService(Protocol): + def get_projects_details_by_search_phrase( + self, context: Context, search_phrase: str + ) -> ProjectsDetailsDTO: + ... + + @runtime_checkable class UserAllocationNonceProtocol(Protocol): def get_user_next_nonce(self, user_address: str) -> int: diff --git a/backend/app/modules/multisig_signatures/service/offchain.py b/backend/app/modules/multisig_signatures/service/offchain.py index 6a5c50c141..f0716f7b07 100644 --- a/backend/app/modules/multisig_signatures/service/offchain.py +++ b/backend/app/modules/multisig_signatures/service/offchain.py @@ -1,4 +1,5 @@ -from typing import List +from http import HTTPStatus +from typing import List, Dict from app.context.manager import Context from app.exceptions import InvalidMultisigSignatureRequest, InvalidMultisigAddress @@ -6,8 +7,8 @@ from app.infrastructure import database from app.infrastructure.database.models import MultisigSignatures from app.infrastructure.database.multisig_signature import SigStatus, MultisigFilters - from app.infrastructure.external_api.common import retry_request +from app.infrastructure.external_api.safe.message_details import get_message_details from app.modules.common.allocations.deserializer import deserialize_payload from app.modules.common.crypto.eip1271 import get_message_hash from app.modules.common.crypto.signature import ( @@ -23,16 +24,13 @@ ) from app.modules.multisig_signatures.dto import Signature from app.pydantic import Model -from app.infrastructure.external_api.safe.message_details import get_message_details class OffchainMultisigSignatures(Model): is_mainnet: bool = False - verifiers: dict[SignatureOpType, Verifier] + verifiers: Dict[SignatureOpType, Verifier] - staged_signatures: list[ - MultisigSignatures - ] = [] # TODO make it invulnerable for data race & race conditions + staged_signatures: List[MultisigSignatures] = [] def get_last_pending_signature( self, _: Context, user_address: str, op_type: SignatureOpType @@ -104,7 +102,7 @@ def save_pending_signature( def _verify_owner(self, user_address: str, message_hash: str): message_details = retry_request( req_func=get_message_details, - status_code=404, + status_code=HTTPStatus.NOT_FOUND, message_hash=message_hash, is_mainnet=self.is_mainnet, ) diff --git a/backend/app/modules/octant_rewards/controller.py b/backend/app/modules/octant_rewards/controller.py index b6cdd22cb0..7f92a4ec0d 100644 --- a/backend/app/modules/octant_rewards/controller.py +++ b/backend/app/modules/octant_rewards/controller.py @@ -3,6 +3,7 @@ from app.exceptions import NotImplementedForGivenEpochState from app.modules.dto import OctantRewardsDTO from app.modules.registry import get_services +from app.modules.octant_rewards import core def get_octant_rewards(epoch_num: int) -> OctantRewardsDTO: @@ -24,3 +25,8 @@ def get_last_finalized_epoch_leverage() -> float: service = get_services(context.epoch_state).octant_rewards_service return service.get_leverage(context) + + +def get_epoch_rewards_rate(epoch_num: int) -> float: + context = epoch_context(epoch_num) + return core.get_rewards_rate(context.epoch_details.epoch_num) diff --git a/backend/app/modules/octant_rewards/core.py b/backend/app/modules/octant_rewards/core.py index f6cb96d699..37476b43ce 100644 --- a/backend/app/modules/octant_rewards/core.py +++ b/backend/app/modules/octant_rewards/core.py @@ -7,6 +7,7 @@ from app.engine.octant_rewards.operational_cost import OperationalCostPayload from app.engine.octant_rewards.ppf import PPFPayload from app.engine.octant_rewards.total_and_individual import TotalAndAllIndividualPayload +from app.modules.staking.proceeds.core import ESTIMATED_STAKING_REWARDS_RATE @dataclass @@ -66,3 +67,11 @@ def calculate_rewards( ppf_value=ppf_value, community_fund=cf_value, ) + + +def get_rewards_rate(_: int) -> float: + """ + Returns the rewards rate for the given epoch. + """ + # TODO Staking Rewards Rate is a static value for now but it may be calculated dynamically in the future: https://linear.app/golemfoundation/issue/OCT-1916/make-apr-a-dynamically-computed-one + return ESTIMATED_STAKING_REWARDS_RATE diff --git a/backend/app/modules/octant_rewards/general/service/finalized.py b/backend/app/modules/octant_rewards/general/service/finalized.py index ddb3bda5e0..2edfb3d85e 100644 --- a/backend/app/modules/octant_rewards/general/service/finalized.py +++ b/backend/app/modules/octant_rewards/general/service/finalized.py @@ -4,6 +4,8 @@ from app.infrastructure import database from app.modules.dto import OctantRewardsDTO from app.pydantic import Model +from app.engine.octant_rewards.leftover import LeftoverPayload +from app.engine.projects.rewards.allocations import ProjectAllocationsPayload class FinalizedOctantRewards(Model): @@ -15,6 +17,37 @@ def get_octant_rewards(self, context: Context) -> OctantRewardsDTO: context.epoch_details.epoch_num ) + leftover_payload = LeftoverPayload( + staking_proceeds=int(pending_snapshot.eth_proceeds), + operational_cost=int(pending_snapshot.operational_cost), + community_fund=pending_snapshot.validated_community_fund, + ppf=pending_snapshot.validated_ppf, + total_withdrawals=int(finalized_snapshot.total_withdrawals), + ) + + unused_matched_rewards = context.epoch_settings.octant_rewards.leftover.extract_unused_matched_rewards( + int(finalized_snapshot.leftover), leftover_payload + ) + + allocations_for_epoch = database.allocations.get_all_by_epoch( + context.epoch_details.epoch_num + ) + grouped_allocations = context.epoch_settings.project.rewards.projects_allocations.segregate_allocations( + ProjectAllocationsPayload(allocations=allocations_for_epoch) + ) + allocations_result = context.epoch_settings.project.rewards.get_total_allocations_below_threshold( + grouped_allocations + ) + allocations_sum = allocations_result.total + allocations_below_threshold = allocations_result.below_threshold + + donated_to_projects = ( + int(finalized_snapshot.matched_rewards) + - unused_matched_rewards + + allocations_sum + - allocations_below_threshold + ) + return OctantRewardsDTO( staking_proceeds=int(pending_snapshot.eth_proceeds), locked_ratio=Decimal(pending_snapshot.locked_ratio), @@ -28,6 +61,7 @@ def get_octant_rewards(self, context: Context) -> OctantRewardsDTO: total_withdrawals=int(finalized_snapshot.total_withdrawals), ppf=pending_snapshot.validated_ppf, community_fund=pending_snapshot.validated_community_fund, + donated_to_projects=donated_to_projects, ) def get_leverage(self, context: Context) -> float: diff --git a/backend/app/modules/octant_rewards/general/service/pending.py b/backend/app/modules/octant_rewards/general/service/pending.py index 16717819ad..52a13f54ad 100644 --- a/backend/app/modules/octant_rewards/general/service/pending.py +++ b/backend/app/modules/octant_rewards/general/service/pending.py @@ -27,8 +27,8 @@ class ProjectRewards(Protocol): def get_finalized_project_rewards( self, context: Context, - allocations: list[AllocationDTO], - all_projects: list[str], + allocations: List[AllocationDTO], + all_projects: List[str], matched_rewards: int, ) -> FinalizedProjectRewards: ... @@ -51,6 +51,7 @@ def get_octant_rewards(self, context: Context) -> OctantRewardsDTO: context.epoch_details.epoch_num ) matched_rewards = self.octant_matched_rewards.get_matched_rewards(context) + project_rewards = self._get_project_rewards(context, matched_rewards) return OctantRewardsDTO( staking_proceeds=int(pending_snapshot.eth_proceeds), @@ -63,7 +64,10 @@ def get_octant_rewards(self, context: Context) -> OctantRewardsDTO: ppf=pending_snapshot.validated_ppf, matched_rewards=matched_rewards, patrons_rewards=self.patrons_mode.get_patrons_rewards(context), - leftover=self.get_leftover(context, pending_snapshot, matched_rewards), + leftover=self._get_leftover( + context, pending_snapshot, matched_rewards, project_rewards + ), + donated_to_projects=self._get_donated_to_projects(project_rewards), ) def get_matched_rewards(self, context: Context) -> int: @@ -79,19 +83,14 @@ def get_leverage(self, context: Context) -> float: matched_rewards, allocations_sum ) - def get_leftover( + def _get_leftover( self, context: Context, pending_snapshot: PendingEpochSnapshot, matched_rewards: int, + project_rewards: FinalizedProjectRewards, ) -> int: - allocations = database.allocations.get_all_with_uqs( - context.epoch_details.epoch_num - ) _, user_rewards = self.user_rewards.get_claimed_rewards(context) - project_rewards = self.project_rewards.get_finalized_project_rewards( - context, allocations, context.projects_details.projects, matched_rewards - ) return context.epoch_settings.octant_rewards.leftover.calculate_leftover( LeftoverPayload( @@ -106,3 +105,19 @@ def get_leftover( used_matched_rewards=sum(r.matched for r in project_rewards.rewards), ) ) + + def _get_donated_to_projects(self, project_rewards: FinalizedProjectRewards) -> int: + total_user_donations_with_used_matched_rewards = sum( + r.amount for r in project_rewards.rewards + ) + + return total_user_donations_with_used_matched_rewards + + def _get_project_rewards(self, context: Context, matched_rewards: int): + allocations = database.allocations.get_all_with_uqs( + context.epoch_details.epoch_num + ) + project_rewards = self.project_rewards.get_finalized_project_rewards( + context, allocations, context.projects_details.projects, matched_rewards + ) + return project_rewards diff --git a/backend/app/modules/projects/details/controller.py b/backend/app/modules/projects/details/controller.py new file mode 100644 index 0000000000..ffb7ba1eee --- /dev/null +++ b/backend/app/modules/projects/details/controller.py @@ -0,0 +1,34 @@ +from typing import List, Dict + +from app.context.manager import epoch_context +from app.modules.registry import get_services +from app.modules.projects.details.service.projects_details import ProjectsDetailsDTO + + +def get_projects_details_for_multiple_params( + epochs: List[int], search_phrases: List[str] +) -> List[Dict[str, str]]: + searched_projects = [] + for epoch in epochs: + for search_phrase in search_phrases: + project_details = get_projects_details(epoch, search_phrase) + searched_projects.extend(project_details) + return searched_projects + + +def get_projects_details(epoch: int, search_phrase: str) -> List[Dict[str, str]]: + context = epoch_context(epoch) + + service = get_services(context.epoch_state).projects_details_service + + filtered_projects: ProjectsDetailsDTO = ( + service.get_projects_details_by_search_phrase(context, search_phrase) + ) + return [ + { + "name": project["name"], + "address": project["address"], + "epoch": project["epoch"], + } + for project in filtered_projects.projects_details + ] diff --git a/backend/app/modules/projects/details/core.py b/backend/app/modules/projects/details/core.py new file mode 100644 index 0000000000..42d30dbd56 --- /dev/null +++ b/backend/app/modules/projects/details/core.py @@ -0,0 +1,20 @@ +from typing import List + +from app.infrastructure.database.models import ProjectsDetails + + +def filter_projects_details( + projects_details: List[ProjectsDetails], search_phrase: str +) -> List[ProjectsDetails]: + search_phrase = search_phrase.strip().lower() + + filtered_project_details = [] + + for project_details in projects_details: + if ( + search_phrase in project_details.name.lower() + or search_phrase in project_details.address.lower() + ): + filtered_project_details.append(project_details) + + return filtered_project_details diff --git a/backend/app/modules/projects/details/service/projects_details.py b/backend/app/modules/projects/details/service/projects_details.py new file mode 100644 index 0000000000..b179c8217c --- /dev/null +++ b/backend/app/modules/projects/details/service/projects_details.py @@ -0,0 +1,32 @@ +from dataclasses import dataclass +from typing import List, Dict + +from app.pydantic import Model +from app.context.manager import Context +from app.infrastructure.database.projects_details import get_projects_details_for_epoch +from app.modules.projects.details.core import filter_projects_details + + +@dataclass +class ProjectsDetailsDTO: + projects_details: List[Dict[str, str]] + + +class StaticProjectsDetailsService(Model): + def get_projects_details_by_search_phrase( + self, context: Context, search_phrase: str + ) -> ProjectsDetailsDTO: + epoch = context.epoch_details.epoch_num + + projects_details = get_projects_details_for_epoch(epoch) + + filtered_projects_details = filter_projects_details( + projects_details, search_phrase + ) + + return ProjectsDetailsDTO( + projects_details=[ + {"name": project.name, "address": project.address, "epoch": str(epoch)} + for project in filtered_projects_details + ] + ) diff --git a/backend/app/modules/projects/rewards/service/finalizing.py b/backend/app/modules/projects/rewards/service/finalizing.py index 726256fb6c..90a9331f16 100644 --- a/backend/app/modules/projects/rewards/service/finalizing.py +++ b/backend/app/modules/projects/rewards/service/finalizing.py @@ -1,3 +1,5 @@ +from typing import List + from app.context.manager import Context from app.modules.common.project_rewards import get_projects_rewards, AllocationsPayload from app.modules.dto import AllocationDTO, ProjectAccountFundsDTO @@ -9,8 +11,8 @@ class FinalizingProjectRewards(Model): def get_finalized_project_rewards( self, context: Context, - allocations: list[AllocationDTO], - all_projects: list[str], + allocations: List[AllocationDTO], + all_projects: List[str], matched_rewards: int, ) -> FinalizedProjectRewards: allocations_payload = AllocationsPayload( diff --git a/backend/app/modules/score_delegation/core.py b/backend/app/modules/score_delegation/core.py index b77a155308..57c25e607a 100644 --- a/backend/app/modules/score_delegation/core.py +++ b/backend/app/modules/score_delegation/core.py @@ -9,6 +9,7 @@ AntisybilScoreTooLow, InvalidRecalculationRequest, InvalidDelegationForLockingAddress, + InvalidDelegationRequest, ) from app.modules.common.crypto.signature import ( encode_for_signing, @@ -53,7 +54,10 @@ def verify_score_delegation( score: float, secondary_budget: int, action: ActionType, + primary_addr: str, + timeout_list: set, ): + _verify_timeout_list(primary_addr, timeout_list) _verify_hashed_addresses(action, hashed_addresses, all_hashes) _verify_score(score) _verify_non_locking(secondary_budget) @@ -87,6 +91,11 @@ def verify_signatures(payload: ScoreDelegationPayload, action: ActionType): raise InvalidSignature(payload.secondary_addr, payload.secondary_addr_signature) +def _verify_timeout_list(primary_addr: str, timeout_list: set): + if primary_addr.lower() in timeout_list: + raise InvalidDelegationRequest() + + def _verify_hashed_addresses( action: ActionType, hashed_addresses: HashedAddresses, all_hashes: Set[str] ): diff --git a/backend/app/modules/score_delegation/service/simple_obfuscation.py b/backend/app/modules/score_delegation/service/simple_obfuscation.py index 33c7ee8550..5272e0282a 100644 --- a/backend/app/modules/score_delegation/service/simple_obfuscation.py +++ b/backend/app/modules/score_delegation/service/simple_obfuscation.py @@ -36,15 +36,30 @@ def update_antisybil_status( class SimpleObfuscationDelegationVerifier(Verifier, Model): def _verify_logic(self, context: Context, **kwargs): - hashed_addresses, action_type, score, secondary_budget = ( + ( + hashed_addresses, + action_type, + score, + secondary_budget, + primary_addr, + timeout_list, + ) = ( kwargs["hashed_addresses"], kwargs["action_type"], kwargs["score"], kwargs["secondary_budget"], + kwargs["primary_addr"], + kwargs["timeout_list"], ) get_all_delegations = database.score_delegation.get_all_delegations() core.verify_score_delegation( - hashed_addresses, get_all_delegations, score, secondary_budget, action_type + hashed_addresses, + get_all_delegations, + score, + secondary_budget, + action_type, + primary_addr, + timeout_list, ) def _verify_signature(self, _: Context, **kwargs): @@ -56,6 +71,7 @@ class SimpleObfuscationDelegation(Model): verifier: Verifier antisybil: Antisybil user_deposits_service: UserEffectiveDeposits + timeout_list: set def delegate(self, context: Context, payload: ScoreDelegationPayload): primary, secondary, both = get_hashed_addresses( @@ -98,6 +114,8 @@ def _delegation( score=score, secondary_budget=secondary_budget, action_type=action, + primary_addr=payload.primary_addr, + timeout_list=self.timeout_list, ) self.antisybil.update_antisybil_status( context, payload.primary_addr, score, expires_at, stamps diff --git a/backend/app/modules/staking/proceeds/core.py b/backend/app/modules/staking/proceeds/core.py index 5067607990..3bcb293953 100644 --- a/backend/app/modules/staking/proceeds/core.py +++ b/backend/app/modules/staking/proceeds/core.py @@ -8,17 +8,17 @@ ESTIMATED_STAKED_AMOUNT = 100000_000000000_000000000 # TODO call an API to get a real value instead of hardcoding: https://linear.app/golemfoundation/issue/OCT-902/api-call-to-get-validators-api -ESTIMATED_STAKING_APR = 0.038 +ESTIMATED_STAKING_REWARDS_RATE = 0.038 def estimate_staking_proceeds( epoch_duration_sec: int, staked_amount=ESTIMATED_STAKED_AMOUNT, - apr=ESTIMATED_STAKING_APR, + staking_rewards=ESTIMATED_STAKING_REWARDS_RATE, ) -> int: if epoch_duration_sec <= 0: return 0 - return int(int(staked_amount * apr) / 31536000 * epoch_duration_sec) + return int(int(staked_amount * staking_rewards) / 31536000 * epoch_duration_sec) def sum_mev( diff --git a/backend/app/modules/uq/core.py b/backend/app/modules/uq/core.py index b5943ef9d4..c327415326 100644 --- a/backend/app/modules/uq/core.py +++ b/backend/app/modules/uq/core.py @@ -1,11 +1,16 @@ from decimal import Decimal from typing import List, Tuple -from app.constants import LOW_UQ_SCORE, MAX_UQ_SCORE +from app.constants import LOW_UQ_SCORE, MAX_UQ_SCORE, NULLIFIED_UQ_SCORE from app.infrastructure.database.uniqueness_quotient import get_all_uqs_by_epoch -def calculate_uq(gp_score: float, uq_threshold: int) -> Decimal: +def calculate_uq( + gp_score: float, uq_threshold: int, *, is_on_timeout_list: bool = False +) -> Decimal: + if is_on_timeout_list is True: + return NULLIFIED_UQ_SCORE + if gp_score >= uq_threshold: return MAX_UQ_SCORE diff --git a/backend/app/modules/uq/service/preliminary.py b/backend/app/modules/uq/service/preliminary.py index 3779f34bc1..fc9e293e2c 100644 --- a/backend/app/modules/uq/service/preliminary.py +++ b/backend/app/modules/uq/service/preliminary.py @@ -1,6 +1,5 @@ -from datetime import datetime from decimal import Decimal -from typing import Protocol, Optional, Tuple, runtime_checkable +from typing import Protocol, Optional, runtime_checkable, Tuple from app.context.manager import Context from app.infrastructure.database.uniqueness_quotient import ( @@ -8,6 +7,7 @@ save_uq_from_address, ) from app.modules.uq.core import calculate_uq +from app.modules.user.antisybil.dto import AntisybilStatusDTO from app.pydantic import Model @@ -15,7 +15,7 @@ class Antisybil(Protocol): def get_antisybil_status( self, _: Context, user_address: str - ) -> Optional[Tuple[float, datetime]]: + ) -> Optional[AntisybilStatusDTO]: ... @@ -51,12 +51,14 @@ def retrieve( return uq_score def calculate(self, context: Context, user_address: str) -> Decimal: - gp_score = self._get_gp_score(context, user_address) + gp_score, is_on_timeout_list = self._get_gp_score(context, user_address) - return calculate_uq(gp_score, self.uq_threshold) + return calculate_uq( + gp_score, self.uq_threshold, is_on_timeout_list=is_on_timeout_list + ) - def _get_gp_score(self, context: Context, address: str) -> float: + def _get_gp_score(self, context: Context, address: str) -> Tuple[float, bool]: antisybil_status = self.antisybil.get_antisybil_status(context, address) if antisybil_status is None: - return 0.0 - return antisybil_status[0] + return 0.0, False + return antisybil_status.score, antisybil_status.is_on_timeout_list diff --git a/backend/app/modules/user/antisybil/controller.py b/backend/app/modules/user/antisybil/controller.py index 34951e20e7..537b92fca1 100644 --- a/backend/app/modules/user/antisybil/controller.py +++ b/backend/app/modules/user/antisybil/controller.py @@ -1,18 +1,18 @@ -from datetime import datetime -from typing import Tuple +from typing import Optional from app.context.epoch_state import EpochState from app.context.manager import state_context from app.modules.registry import get_services +from app.modules.user.antisybil.dto import AntisybilStatusDTO -def get_user_antisybil_status(user_address: str) -> Tuple[int, datetime]: +def get_user_antisybil_status(user_address: str) -> Optional[AntisybilStatusDTO]: context = state_context(EpochState.CURRENT) service = get_services(context.epoch_state).user_antisybil_service return service.get_antisybil_status(context, user_address) -def update_user_antisybil_status(user_address: str) -> Tuple[int, datetime]: +def update_user_antisybil_status(user_address: str) -> Optional[AntisybilStatusDTO]: context = state_context(EpochState.CURRENT) service = get_services(context.epoch_state).user_antisybil_service diff --git a/backend/app/modules/user/antisybil/core.py b/backend/app/modules/user/antisybil/core.py new file mode 100644 index 0000000000..6a74dd4be8 --- /dev/null +++ b/backend/app/modules/user/antisybil/core.py @@ -0,0 +1,67 @@ +import json +from typing import Optional + +from app.modules.user.antisybil.dto import AntisybilStatusDTO +from app.constants import ( + GUEST_LIST, + GUEST_LIST_STAMP_PROVIDERS, + GTC_STAKING_STAMP_PROVIDERS_AND_SCORES, +) +from app.infrastructure.database.models import GPStamps + + +def determine_antisybil_score( + score: GPStamps, user_address: str, timeout_list: set +) -> Optional[AntisybilStatusDTO]: + """ + Determine the antisybil score for a user. + 1. Timeout list users will always have a score of 0.0 and has a higher priority than guest list users. + 2. Guest list users will have a score increased by 21.0 if they have not been stamped by a guest list provider. + 3. If user has any stamps related to GTC staking, scores of those stamps are subtracted. + """ + if score is None: + return None + + potential_score = _apply_gtc_staking_stamp_nullification(score.score, score) + + if user_address.lower() in timeout_list: + return AntisybilStatusDTO( + score=0.0, expires_at=score.expires_at, is_on_timeout_list=True + ) + elif user_address.lower() in GUEST_LIST and not _has_guest_stamp_applied_by_gp( + score + ): + return AntisybilStatusDTO( + score=potential_score + 21.0, + expires_at=score.expires_at, + is_on_timeout_list=False, + ) + + return AntisybilStatusDTO( + score=potential_score, expires_at=score.expires_at, is_on_timeout_list=False + ) + + +def _get_provider(stamp) -> str: + return stamp["credential"]["credentialSubject"]["provider"] + + +def _has_guest_stamp_applied_by_gp(score: GPStamps) -> bool: + all_stamps = json.loads(score.stamps) + stamps = [ + stamp + for stamp in all_stamps + if _get_provider(stamp) in GUEST_LIST_STAMP_PROVIDERS + ] + return len(stamps) > 0 + + +def _apply_gtc_staking_stamp_nullification(score: int, stamps: GPStamps) -> int: + "Take score and stamps as returned by Passport and remove score associated with GTC staking" + delta = 0 + all_stamps = json.loads(stamps.stamps) + providers = [_get_provider(stamp) for stamp in all_stamps] + for provider in providers: + if provider in GTC_STAKING_STAMP_PROVIDERS_AND_SCORES.keys(): + delta = delta + GTC_STAKING_STAMP_PROVIDERS_AND_SCORES[provider] + return score - delta diff --git a/backend/app/modules/user/antisybil/dto.py b/backend/app/modules/user/antisybil/dto.py new file mode 100644 index 0000000000..eba87c843e --- /dev/null +++ b/backend/app/modules/user/antisybil/dto.py @@ -0,0 +1,9 @@ +from dataclasses import dataclass +from datetime import datetime + + +@dataclass +class AntisybilStatusDTO: + score: float + expires_at: datetime + is_on_timeout_list: bool diff --git a/backend/app/modules/user/antisybil/service/guest_list.py b/backend/app/modules/user/antisybil/service/guest_list.py deleted file mode 100644 index 6343a683e7..0000000000 --- a/backend/app/modules/user/antisybil/service/guest_list.py +++ /dev/null @@ -1,471 +0,0 @@ -guest_list = set( - [ - "0x16f3f2f0ba34973937A1ebb989a295Ca106b67C7", - "0xBB5935dAaFbacAE82c8D2CA8377F16073D70061a", - "0xba84B5cA750b33DfAdDBFdD1B7C6887885a34977", - "0x4e9A05226993F094A56A3472C8c816F2599423A6", - "0x40DE3299Bd8a10D8Ac3f32C1A55DE40640cF9B75", - "0xC33F87697EF41e0E95e7a55d1ec8180F04088578", - "0xCBc924183Bc32D02746Fa8D38843B5Ce08662eB4", - "0xDc9C5e34959eC3643AF1e1D34A83D6b251AAb1eF", - "0x762BBc211990D0a356F35E4D500843F59d223C2e", - "0x55187a1165EBB441A1BF227fff1EB0D32a65bc46", - "0x7aE59f3F2B2E5f3842B50a15bCb5247c5De881Be", - "0x59072B3a3287F4a75cadfb36D671A2f0d1959B09", - "0x4A5da2a1D3258dF8FFb431Cf0110FE9b98ADeEbf", - "0x514A9771Af8Afe71057666b680238dFBeA578d65", - "0xE70055e9575f15A6f51F3068901D73ac63952adF", - "0x9e831B58001e2b69F70C892e4F8ce9d2118B7E00", - "0x51c1C7f1e168a36Bf1FaBFD91E98b43476a6B14D", - "0x33878e070db7f70D2953Fe0278Cd32aDf8104572", - "0x3df13B9bd79158f0cccDDd0833cF774178e3d2e9", - "0xB9573982875b83aaDc1296726E2ae77D13D9B98F", - "0xE862E2C1ca94eAcfEDe3c95a217c15EF0086a29D", - "0x0442A9aBbc93058a873c371F21CC366338254A88", - "0x0194325BF525Be0D4fBB0856894cEd74Da3B8356", - "0x399e0Ae23663F27181Ebb4e66Ec504b3AAB25541", - "0x9f729294b308f79243285348A7Be3f58ae5ED31A", - "0xb62E762Af637b49Eb4870BCe8fE21bffF189e495", - "0x5725a458b319d73B8Ec84c47de80620E7B191B0C", - "0x57Ccc081824b43B75986727875929AF3A6Ad721C", - "0xf13e477365B0FAa64130DA2FF663aAb20d32d929", - "0xFD868dB0696ef762351F8421535cC5f9F423B23C", - "0x30043aAbBCeBbD887437Ec4F0Cfe6d4c0eB5CC64", - "0xAa01DeC5307CF17F20881A3286dcaA062578cea7", - "0x3FFD0C300fa4a021364Ae7e85a7b0d3a02133f99", - "0xBEa26DE685Ef828b60cA53b40Ecc9Bab35645fDF", - "0x4103CFcb300599dFcB31dBc95d919592619B4EAc", - "0x22bAac1E95efC010E35D5eD643BB16c9dB254a11", - "0x686A484bc2E2bE79f358c7055e8539A69413A3Ed", - "0x073a360C372FD51Bd6E56B4a4d73790fDAec4641", - "0xdd0206010CA82fF22303b58863b3a6f3006C86C4", - "0x25FA68A4c340202737EDBC67fD1a2Ec8DE872dB6", - "0x5b655EDa7D101f98934392Cc3610BcB25b633789", - "0x32cEfb2dC869BBfe636f7547CDa43f561Bf88d5A", - "0xA4369e39e3ED13593Adb0142A1ea5d08AbdF99C4", - "0xA8F0048A0d1A04663Ca5010d0bEaC5BCAEeA0eef", - "0x65F632cfe8015B7ae6976e549645ed04cde60fe4", - "0xb35E0a0D00c640ab75fAD3cf3B83264bC64D23eC", - "0xafA3E6E29D99337b166b83fB24bA17b19764B49D", - "0x57DD1517c12659365E59F71129Fa9B1611Dd18AF", - "0x9120FfD5d04ca4B26AaBCe611989A8F026dc099a", - "0x2AC6A3561a43f06d62602eF9728C2B9eEc393326", - "0x297Aa50D0557c865F6C9B0AA0a91f41C26E55eE6", - "0x9Ff46343d0b652D6e766F85f9aE91653869349a5", - "0xEd36bf0b2b17768E782Db2ece6A327055b2f3e9C", - "0xC28D2fDFE6d5a482d32f855457Bb5F8cAcdB32b1", - "0x1d44404C1C53991Ec33095225da173d544Cd4Af3", - "0x5d9fbd984B9CeC714a4B14c38Ea83bBC82d06d69", - "0x5Bc0AEbdbab698e12FD33A2E133e6858fe6Cdd76", - "0x66805D8B82664Acab4CbE0C0498889dDE9aF7841", - "0xaF7610578F54c7De7563655AaF461E2CbeCB08C6", - "0x6c3F373Baec5D2d0Fb3C82C4f3Db5E48873ae363", - "0x015122A625b45f68E6D795C0Ab99fC7107e4c3B9", - "0x508A4F07B60BA0940283Cd4e32d5DEb0CC38AdF7", - "0xb150c9bEd10a8C62997d58a81c4e1fA75160643e", - "0x212647c56BA10ee429a838bc567dFb03A8D054Ba", - "0x73306dAb0D39A4D47df4972c7022CB2cac075D4e", - "0x914D5d84aAA064207C2c31014426227405edab41", - "0x3FBcEC42405391B1fb377664daA5AE7Bc9Ba7BF5", - "0x8c8296a0042E842Cb865DfFD94678c941fD24bAE", - "0xf5c2087877218AA979Dd0e2e5108837199aF44D2", - "0x529dc928E67D8A43133D10769B308F1D5A629401", - "0xF1bb436c29E46B1987bC825879ffc9c34Ab97f99", - "0xFDDE7aE208B3596f1982D66F6BAe4cDabF29244b", - "0x02e4Cc9ffF7566563618fb21B3BB10Eab4B3D726", - "0xd8821dbbcb8ea0c14Bd1F0aCbFBeBB3Fd984269b", - "0x31d23825aFbda5B6B1690Bbdbbb8117B5ea0f8E4", - "0x731022D6De647991864203a35dFaD1A192240d07", - "0xEb5e0B8e80FCe271c13F533fA728D7bB03cafa4c", - "0xFC967DE4e029fdcD16B418DaC2147d282C93085b", - "0x801a6d6dBC1e40466E131aA21D951629A9efAB4e", - "0x4892139De0e73141438D9E55D593171C0Cc6B143", - "0x8124eFC94c951cF41D4B0B42794C678458a00726", - "0x81cc36DdA894256aa95458F78B4029381b09BDfb", - "0x4Dcb2BCA3450B427F3d1b424C885259D05363080", - "0x7Dd8030F9d33Af4a40ee074f990892E825132e61", - "0x432C53218A11bEd08d238Cf84ff547CE4fe933ab", - "0xE77ad9c5af60332D24E5531B51A6B7f61D0B8703", - "0x0f792e55668AD78476d4B563E6EB1228D636a71e", - "0x583bBaDA56bb535BCBb31877A620A6ff2A25CeA5", - "0x5C0E777dC8F3De6b0911b44DbBDD8Bf71b2E8e38", - "0x8a4a50B13Fd2cb36FeB96c408CB98B4c9F2b8F25", - "0x1e55C85801a2C4F0beC57c84742a8eF3d72dE57B", - "0x26d3bE736aB6b5D8A3266fFCC0895dDc1bc19a38", - "0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714", - "0xfB94f39B150Ae661F85762154c0CadC65E083791", - "0x4B7C0Da1C299Ce824f55A0190Efb13663442FA2c", - "0x77E64560Bd6C323c075F206a5AB9dD6850F31609", - "0x0F46540678c7e6D2eF983B382CC07Fa815AB148c", - "0x82073f802547fEeEc0fd49719a3D7697fB66076a", - "0xBAab83De8DbA764bF02a530cad33555bD23eba22", - "0x890a0047f8D573347872cB6C019F86552f2367d6", - "0x14D92832265eeAFDEF9e526356FEfc90105966c3", - "0x512B436cB2Ed6016e80d4F89ca578F99DBBccb61", - "0x696Ee4AE0b15feae8ED1AfC865930e0ea65b1f3F", - "0xbb4D885fD41c807e8eCC2dD9e6295a7F96Adb0EB", - "0xB1dE969883b1FdD90a43fF475A5171a3CfEfe76d", - "0x7DBF6820D32cFBd5D656bf9BFf0deF229B37cF0E", - "0xfE2e3cCEE9714b29Ab2FB4E940e52672194815fC", - "0x57fb3f4b027fbaDbd8d20Eb5E48feb1e2b02DF30", - "0x9AE494FBAc34682c122A1D4e3D6ae1Eb5404A469", - "0xb2b9300475aF157676C44eE64d39a5eB3C294DbD", - "0x01Bc28E036b6e75247Fe8F49f0a8b9410b19d851", - "0xcCE9A28b570946123f392Cf1DbfA6D2D5e636a1f", - "0xb2a3b5B9d2C0f07cBA328b58737147cfc172EB9f", - "0xCC3d7F9fE6946979215A901BbA385a88FdabBBf4", - "0x38f80f8f76B1C44B2beeFB63bb561F570fb6ddB6", - "0xd82803b7B9A5EB1D5FC558FD619afC6c031cd0B1", - "0x844AeeD1B294Ef9632c18E73F57ef77D0A23D0e2", - "0x9cD7D1981B3e15a2DEE4d512ac60E0579Ae18546", - "0xEBCd250474C27cBaD3C56f3F34e08F97b370AC2d", - "0xDA47bdcC48f26FB4709f90316341D9104cB1fb89", - "0x5cbB6ad79008908aA125667D1300558D9253B589", - "0x1078DaA844CDF1EDB51E5189c8b113B80a6A6957", - "0x8341c4106523b49fc247f84e412Bb2AF5597038f", - "0xCe57ebEd9aC38402DcAA44f65a1c9b04e26b8283", - "0x2dd2036C9Db2ADA2739509AF0047c00C8b9291EF", - "0xa77294828d42B538890fa6E97AdFfE9305536171", - "0x8dA48e5846c06B558970ACd42EDc7Da8799481E4", - "0x50418699cB44BfDa9c9afc9B7a0b0d244d8927D2", - "0x936d69AbCD9acdC89455EEFAf744044fFC1CA660", - "0x90C32e6B29794Fd7f5BbA2BBEE74e924078B3f9b", - "0x362B7e0599E950b921ca9D86336ca409208FFDEC", - "0xd98aD1Fd4aa0E1c876d91968D1385aa9E1Aa98df", - "0xD2602C7bDFC9F413974e944280BbFae275d1B1b6", - "0x731A2e51ebfAeBacF8477E992CDEB1E8eacf519C", - "0x072d63796C4FE69B306a23E1D01156d51F7B3e16", - "0x051010142A0B9de7F0Fd8fb31d085407287F6381", - "0x8498843f6D9046f7b59482978E152D61869203bC", - "0xB48ef8e4e7Bef79ddF64d4424151f003a59BfbfB", - "0xb423A138fD171c28d90A5883A01ec92fF3D63609", - "0xAfA3a2528E8baAd576a83ffC52dB9f100dEbe307", - "0x055fdA7Eb509cc338C898b0F698B7624387AB813", - "0x0B3BD83E857997b370FaDC8504fB712244F6786C", - "0x8D12A71Cb933A4222d42feCBb4ba9c15e455305b", - "0xDEF3D19ff35a42F5b8E3c706c8fD287De72e6D15", - "0x19a2BC678785BAD6A947A87494D480DAD57711c6", - "0x2c3E79D3DCE90FB0886C89Ec602E61757E589a94", - "0xe8aa836a597a66724D678860D105561c13E95bFa", - "0x3352a3277d2B74A773Fa6E68a625FcB18E4Fc282", - "0x2df292AF809Fd693D94C7D17E36BE352e15Bb98a", - "0x269Aa10398Aaa695259C3E8211ab27a15004110C", - "0x02d9c84a495986b8b3C3347Ad16849DCB1b9793e", - "0x8FDA1Daa6a674C1726d1896E3054B9a82d123F12", - "0x1021e61f2cDd8bB295b0e64A20eBB7D8ec3734bf", - "0x58d7d9c971A613117E493062bEC1A6A5484f2780", - "0x2bb96f44b9709b02189A50B377755edC30bc65C7", - "0x7bE20B02095944657275eD608615A39931d783F2", - "0x4AA51a723882ee676FeC444D4561c5eE16c339E9", - "0x1B243D42F53924118646EFaec5b3f6116b563960", - "0x01b7348EC3fb20Ab1f40b97Cc82df44aeD360768", - "0xbf4C0104dbfb028f3484CfAC17BB22aa15E5c7E2", - "0xCc3B817D4ABa7698EaafB4C68E7688CF61B0BF46", - "0x572E1b86471c900Cd16AFa9cBB7701862D0e70cB", - "0x602Ac8C3f61b351be325FEeb58842EF557431c2e", - "0x8d0CD1AB81EaDa4F92C7cb5c8DBc25C69cc296AD", - "0xAE2C7AB762317DB453317b70f1f40145755fAfb7", - "0x7bdae9AAbE238188c4882D48a3aEE21288A38eD0", - "0x96e4152f00894f677d860023b9784d578bC1c145", - "0xF572C9b11E757d3580C7C7310630cd488E8EA736", - "0x3769092DBfa6eb34434fB5B7cf0eB06E710728F3", - "0xCA72c93172BA6EfF168E59e7F17C3C7A8FeA9B62", - "0x1c0AcCc24e1549125b5b3c14D999D3a496Afbdb1", - "0x7fC80faD32Ec41fd5CfcC14EeE9C31953b6B4a8B", - "0x5d36a202687fD6Bd0f670545334bF0B4827Cc1E2", - "0xe64113140960528f6AF928d7cA4f45d192286a7a", - "0xf6B6F07862A02C85628B3A9688beae07fEA9C863", - "0xD779aFEE481e3Df5cd0544F0e4353Cf534FD99Db", - "0x183bDB344A07Ee3D27f07AC4799A56E4A2fE5439", - "0xA8cadC2268B01395f8573682fb9DD00Bd582E8A0", - "0x75535661Ab25a468Dfb3137320a7568FeCda4832", - "0xd37ED782323A82e5BD55A92500E48FF5eFcc415E", - "0x03bB5bC3c8fdAB212A6b2B347a049133DfCB3A47", - "0x61987699055394c65355F2797D3e4e589f7FaBf4", - "0x2bC12061C8912505978472C21d4a23dB43AF62aA", - "0xad7575AEFd4d64520c3269FD24eae1b0E13dbE7B", - "0x0D89421D6eec0A4385F95f410732186A2Ab45077", - "0x04c0cD38B8c203b14ef2b7B8d736D69B938AFF71", - "0x0CF30daf2Fb962Ed1d5D19C97F5f6651F3b691c1", - "0x6EEb37b9757DcA963120f61c7E0e0160469A44D3", - "0x616caD18642F45d3fa5FCaaD0a2d81764A9cBa84", - "0xdC1d963D21C9c1bFf7b6Bea6e10080dAa9b4fc51", - "0x8073639B11994C549eDa58fC3cd7132a72aaDF10", - "0xe52C39327FF7576bAEc3DBFeF0787bd62dB6d726", - "0x8f21bD39FcAeA3A729D46339A383081ecB7E84E0", - "0x8Fb7087336678F36E42313f6130567A109a8e73d", - "0x276E69CdD336001afEF07075859A93078496C3c1", - "0x954F716e6de059360d278B773138f8e046696721", - "0x997D410b26CdD17b0750F2c1751e59cBcfaE446f", - "0xE8b6b71f3b1E6d2ad406D2cf36B1f2C567342dF1", - "0x83108A0653a14EAeB8301E7b10a37CfAc39C82f6", - "0xF95D9549b3Ab9470d306a6413Aa45082e8B66043", - "0x82d92494f6fFFB17A1DDFfd9B7d88D1d0a360009", - "0xA19947DA8B916f64Ac6F362cEC9001D8BCBeEe93", - "0x7ef5e4062dcCaD29A6F8d5458590160536056C81", - "0x653d973b36137A5cB2fc304996E0af1F1afCC628", - "0x5F319CA6Ecf072A4d183edAa711Cd04dC225df19", - "0x4D32D90D6535bD4e7eaBaa27EE72932cB214BbfA", - "0x73b9f6a6e52aCE2797F0a6E52AAc530Ed1F2a2Af", - "0xaA3600788b72863ff51C8f0dB5F10bB65fbFeAB4", - "0xf93F0b770784602fC3079eb1D2fB1Ff488Bb02B0", - "0xC8Ddd59c496D04C4C060Ab5038d03d661DDC2617", - "0xc42c77b6B2A2B220b9502F357bBf51334Db3C93f", - "0x2615214F8200B526a7B1eACe03971F2672B48CF2", - "0x9d8d7220D060fd12Ca33336B7239688e366327dE", - "0x9e602c1920443F01Cb100a57A7F894df8Eb42f66", - "0x7e651F5f597436cD0fa941F5FF2cD45Ef3F2Fda8", - "0x8e30Dc2AEF957B1F7dd67B1b7bC651fFe7E17a06", - "0x597dC4159a4b85c086c3C679a0B6c8Fe2836886F", - "0x7fBdE8B27D2B4F164B66F2a9dc02bbD6697e5b19", - "0xf5819cC26F0481c9b86294B4c24027518a04BD5B", - "0x8e7D20638947132B0e6E1aFdE2da1B103aFF9280", - "0xCBA711BEF21496Cfd66323d9AEA8C8EFd0F43e9d", - "0xdfBaeeF21396BF205D4B7D23345155489072Cf9B", - "0x3B981fA5dD50237dAb6F96A417A6690B6f20FcC4", - "0x6C31212a23040998E1D1c157ACe3982aBDBE3154", - "0xCDdF772F8A3295C89DC37510E16e360ee2d29789", - "0x002B5dfB3C71E1dC97A2e5A0A7f69F3e7b83F269", - "0xAD7A185b2456d5AFD85838A50C7d8aCE3aB2f871", - "0x7993F18C91A9f68593d308C5846f380A2a374F46", - "0xc5d82775c9bc5272B1225DB8D62b7034e064BA91", - "0x8bfcF8cb383149D4Ef37e7A609cEc195CDCbE099", - "0xA515F7fB260095eebC860425493b8761B4FC9abd", - "0xaA95cA26c92b0634dF7a1A1504f579F13bFB7f9d", - "0xC2812325caD4C4C782CbbC1164e9373371D31dB2", - "0x4831DdB6502ca45dbEEDf58B47292061Cdb6050B", - "0x6733c60E6E02f9C8FA221Db1aeA018d80D949861", - "0xCaD3887923B39cD2b0B6d13538C4ecB7C5EE9825", - "0x4520cD8BC085B962eF8c0ec696ac9D3Ef1d8bf55", - "0x7D85fCbB505D48E6176483733b62b51704e0bF95", - "0x27259b0F4209e76f8C6Cf27106C9FF83BdC2E831", - "0xE04885c3f1419C6E8495C33bDCf5F8387cd88846", - "0x23ee51e614cBF138e4cAbA9EC5ed4fF7D27A8596", - "0x2cab4d881962D247218356B32aBc4AA5c46bA0d2", - "0x1c0A032954f20761E59138feE236204bECbb8bdb", - "0x701d0ECB3BA780De7b2b36789aEC4493A426010a", - "0x1Ec3C1f70E1D6bBDC84092ae86eAaDE495fdDB9b", - "0xB53b0255895c4F9E3a185E484e5B674bCCfbc076", - "0x770569f85346B971114e11E4Bb5F7aC776673469", - "0x8289432ACD5EB0214B1C2526A5EDB480Aa06A9ab", - "0xdca6F7CB3cF361C8dF8FDE119370F1b21b2fFf63", - "0x117e1EbB7D05545064850513021dF6ADe3C1690B", - "0x7fb43C99a26a9EA8ba841d58390BF1C2996EDFB0", - "0x84B5a60Df2d7e3397B3A4A3c6282f090304Aca26", - "0x72F434Fa010929656AeF58695dab85447E51Fbc6", - "0xA29b0D2F3b4555359A1bF684d700753b1b06cBc4", - "0x4318cC449b1cbE6d64dd82E16abE58C79E076C2B", - "0x8F48282e50B0210bd7c7DD69C54205E98b9Ef5D9", - "0xa305B293e44A82f3Cd489b5fB26084647bb5D8ae", - "0xd9e5De13eF1dBC4DFE0Ee1BB76276228b9B23d0f", - "0x4AcEEB7bF9ec8104CC2379f1E8D648Ee47249FCb", - "0x0743542070891051861f8D0a4550f97B43B0B89a", - "0x58aD805f26272C5Ba06D24Bd0E43c8a2d1c634D9", - "0xE6ED9C681967a4EA7Cef4486942b800139DfB000", - "0x51b9C1Df35B044b5c0099D1fD07EAb7cE38f325d", - "0x55DFFA17578e6bAcE42e4Bf8687A11A85cCfEF97", - "0x1FAE8f99E9F932BdBA910061590C2156eE512A91", - "0xA25207Bb8f8EC2423E2ddf2686A0CD2048352f3E", - "0x746bb7beFD31D9052BB8EbA7D5dD74C9aCf54C6d", - "0x38bc91AA6Aa434c4fae7E666F68C859292deEd95", - "0xA3aD5CFb4FF4B68e37A338Da200BA441C1850B5b", - "0x4bfb2c232F70c83136a3F206cd26Df2A0B605cEC", - "0xf5AB6B4a8d578807491ef59cE855982990932617", - "0x1Fdd220E14b59E26bf1888e8267C4C221983a0A6", - "0xE2D6AFF297b41881c1aEA9599F68AEDFAB38C651", - "0x7d547666209755FB833f9B37EebEa38eBF513Abb", - "0xb681B19bb1F7e9F3C2AE0EDeab368c2afaa4e590", - "0x7Eb84E42059F0D44269C50f4D3A280Fd307a6824", - "0x84f0620A547a4D14A7987770c4F5C25d488d6335", - "0x4Ae6a8A28c87b75e935a90D6128F2649C969c0D8", - "0xb79223E868871DBAc27E8E301f73734abd4Cc628", - "0x6F219Bd1167568aB67494A9067CbbB5679bf0022", - "0x9Ff548c1B3eA3dd123AFE39C759dDA548009B6C8", - "0x3085051F89666E7124e7Ab95b693Fc1E09770907", - "0xa25211B64D041F690C0c818183E32f28ba9647Dd", - "0x6166E1964447E0959bC7c8d543DB3ab82dB65044", - "0x76E059C6FF6bf9FFFD5f33AFdf4AB2FD511C9DF4", - "0x4CC9E6fABb800F083a2685501d1A30CdAbb4B2De", - "0x5f3371793285920351344a1EaaAA48d45e600652", - "0xAFE2b51592b89095A4cFb18da2B5914b528f4c01", - "0xe3F4F3aD70C1190EC480554bbc3Ed30285aE0610", - "0xE0D8926A51F9A1dD8E089D9a3DD88F88fFb2F1Dc", - "0xa6c366D97cb64708211f24310dFAd5363BC96a04", - "0xB7562F12E41C762CeCDA99d62Bd6EAC7b0C3B4c1", - "0x301605C95acbED7A1fD9C2c0DeEe964e2AFBd0C3", - "0x5d47e5D242a8F66a6286b0a2353868875F5d6068", - "0x0ea26051F7657d59418da186137141CeA90D0652", - "0x88f1706c20d94A4d1551C5F799C9E3380A24C3AC", - "0xFB40932271Fc9Db9DbF048E80697E2Da4AA57250", - "0x40Db8365d1252bcb06598927698238a99D39228E", - "0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9", - "0x144c4E5027B69f7798B2B162D924BcAE5c149f15", - "0xeeE844540644b204f0005c063Ae95F244BF06a84", - "0x014607F2d6477bADD9d74bF2c5D6356e29a9b957", - "0x1E8eE48D0621289297693fC98914DA2EfDcE1477", - "0x4AdA1B9D9fe28aBd9585f58cfEeD2169A39e1c6b", - "0x31460f49EEA93Ef8255b42be019FB96F89Cf0c49", - "0x63A32F1595a68E811496D820680B74A5ccA303c5", - "0x022ca32d31da3Ef85922AAFD9aD29C5b2418172C", - "0x93B109C3c279bcBbB673Ed1ae1A8BB2dE8eEf9da", - "0x689476323Eb5e9A5DEd342F54B562fc2c156A522", - "0x1C9F765C579F94f6502aCd9fc356171d85a1F8D0", - "0xe0144FA05A0d32B5B1De10CcEe7211616B3E3EF0", - "0x6C965b656C450259a6D4d95A2E68Fb4319EecBc0", - "0xE36BD8C15a83b89E2E49806d7312846069755C63", - "0x59DDA36bD196Ec849838CE2163E6821f946b37Dc", - "0xDd31dB93082a3A71b98D37ba26230f8734Bd63C3", - "0x83c98211C50480e457a0dF930d2A56a891BC4d4b", - "0x11FA934f6754076AEb7Cf0A72a1c2D2518aA4C77", - "0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12", - "0x2383A8b8cC8561a65871F1d2783B7C52e22B62c1", - "0xCED608Aa29bB92185D9b6340Adcbfa263DAe075b", - "0x841AD0AbAb2D33520ca236A2F5D8b038adDc12BA", - "0x76d2DDCe6b781e66c4B184C82Fbf4F94346Cfb0D", - "0xf21e38ac177B48fDE02dB7F2CA97466AE8Eae87D", - "0x7537Cb0AEe6a3483a7601ebf1084eD4df73166Ab", - "0x5f0bD06A71E038206ef3e5090eB448E9a9773772", - "0x3C0c7B44c1F9366271F5c491121a1F7d55d33eF5", - "0xa96a437eFb71bAF50A59027C340FA3362ef703F7", - "0x55bA9c90c37e3206570AC9dc872c0f053d155F77", - "0xC68bba423525576C7684e7ea25E7D5F079b1361E", - "0x78E87757861185Ec5e8C0EF6BF0C69Fa7832df6C", - "0xCb36F8580A36788A48518dEC95Ea458357E64E30", - "0x25854e2a49A6CDAeC7f0505b4179834509038549", - "0x639749b7b08aEe65039c21d8a411103C6ceBEBF0", - "0xF517529866d371F04780885923F739bc17694BFb", - "0xC728DEa8B2972E6e07493BE8DC2F0314F7dC3E98", - "0x33f6EE932cEa603Fafd6854827259bE172C91Da4", - "0x6D97d65aDfF6771b31671443a6b9512104312d3D", - "0xB7BaBe35CE543e2Cf2F615CB1c792a2025feb572", - "0x4D9e86a5AC368Aa4Df0473eF07e13Ec2Fbe04025", - "0xaa79B87DC8B046A5E4f7D03F1562D7fe5BF98737", - "0xE71FbB197BC8fD11090FA657C100d52Dbb407662", - "0xB22981bA3FE1De2325935c91a3B717168fB86714", - "0xf389dD1F828525b449D63D14157f2d3A25eE0a41", - "0x877B37D3E5467B4aAE7687Dd3480A46C8D3e16Be", - "0xf9e1D1e9F22c96752356AdFd377231528c7E851E", - "0x187089B33E5812310Ed32A57F53B3fAD0383a19D", - "0xF1659A2FD5007192314F9676e6a4a39FD1202160", - "0xFdd210ce1b829E837D9e034DAE0F0312F176cef6", - "0xaCE1f1c6c5c89AE3Fc3209ff92e7120fb74445aA", - "0x6Ceb397b68059Ca73049874D0a30c62500aE9877", - "0xC46c67Bb7E84490D7EbdD0b8ecDaca68Cf3823F4", - "0xbb2eb4c7eB36ECce7A3E6bc501590CE12c9c1050", - "0x9Cf251A782cE7310D5bec0fe0a1C2B826d962545", - "0x43930Ff04D18a5B59805151c1Eb403C55870641E", - "0xA270f5649A42feDfE66ddb3b0b50bebAe1e3DDB0", - "0xd3488EA0c1DC99a5d72F75c84004224f8b58694E", - "0x7aBa691D12D8eF8793F1643eBa66b69C70EC72f1", - "0x8558f502887a9a52c4B265d72327E0E529Ff790d", - "0xA906c85B7e809b79c5e69d485693B44d65B1B252", - "0x3abdC9ed5f5dE6A74CFeb42a82087C853E160E76", - "0x30C7F4F7504D6366916f669cd8E731ED4dF6C702", - "0xed8DB37778804A913670d9367aAf4F043AAd938b", - "0xc191a29203a83eec8e846c26340f828C68835715", - "0xa32aECda752cF4EF89956e83d60C04835d4FA867", - "0x059F7da59Ad1EB412B4d2fFc12E9B50Da91cFdb6", - "0x85BEad65c61dB8cF230b3ec30552B8b3E6388570", - "0xF3Ad97364bcCC3eA0582Ede58C363888f8C4ec85", - "0x3F87755E2974534888Ddb20A52dCE45Ef9f204AB", - "0x757CC91CcBB88cB0d78d6798D20051d39E5A7296", - "0xF553C8223cA8542Af9Db7b916Fe9dc7c28b73751", - "0x40f9bf922c23c43acdad71Ab4425280C0ffBD697", - "0x9600e2eE6377DAdad7299B120026661c336A5e6d", - "0x516fCA170bfE24BFC54e01F215EF85Fe9B5B798A", - "0x61C820e261717A5A0555488872F78ac7b9CE77Ec", - "0xEb263241eB948Cc0eB53A58bf743289D074F474F", - "0x841C11b14c428dd591093348B8Afa2652C863988", - "0x3c114973c0260290C2dbD40323327d996972FCeB", - "0x765a16ca391A6b9249cfA65bf2D14C38722198e3", - "0xC3268DDB8E38302763fFdC9191FCEbD4C948fe1b", - "0x6B92686c40747C85809a6772D0eda8e22a77C60c", - "0xc799bE8De03F20B2D3b101E6F6516D614e6fFe06", - "0x40Dc654af5cE40C122ffDC679fa8E8ca8b91556A", - "0xCE8D52c38d74B77a0aA361c48Fdce6b220A3370e", - "0xEfa4c696Ea2505ec038c9dDC849b1bf817d7f69d", - "0xf7253A0E87E39d2cD6365919D4a3D56D431D0041", - "0xcf79C7EaEC5BDC1A9e32D099C5D6BdF67E4cF6e8", - "0xff75E131c711e4310C045317779d39B3B4f718C4", - "0xdE2BE7C9C542c55a7a77489A3A7745493988947F", - "0xFeB3E0f50107f6cfB2EC8C2bC8287f2707E0E2Ea", - "0x6b759Bf480407D19c8903c16023c706868c29a2A", - "0x6E38911dA6Dd0379F1CaC396F74387c95A1f0D21", - "0x5a5D9aB7b1bD978F80909503EBb828879daCa9C3", - "0xe96056A9936C58e89D1703cF6bD97F134341EE44", - "0x4dD6720D2Bb8721A46bdF9a528704164578E03B9", - "0xE83B9A1B9056B21a01b85162E77AD76a42A1c64B", - "0xbeC48f1cCf82d8e4C983Ee00Ac2eC6B03B81d710", - "0xEFEdaf9c07E6eB56BB8F82f30018e4461B1c5F4c", - "0xB68da7fbF71383Afab240839287878539cFFf20b", - "0xfBDDB719cC7c795a1D9b7EA7aC11494A19b3231F", - "0x07506a5F48D71fDB34D3900fB086D43EF1B58FF9", - "0xa85cdd5478B7E525a808eF9707c3e33432cE1e7F", - "0xCf7C21DeD40f2Df85A564207A89b3379780d9CE3", - "0xd26b76e50f6510cdD4bf45d59279705f36946d23", - "0xdb7a41e39807E8C988859f150296Db92674b7dc7", - "0x719028736f10164c838Ef129936779eD739312f2", - "0xaBCdef0AbbA5D0106595174213156797bc0DB33E", - "0x3D2b8879f97e413b2609F9844A5fc8dB8FE4f6F8", - "0x81EbE8Ee7b51741fD5DaD31F6987E626A9bb8111", - "0x1D45c8fa65F6b18E7dAe04b2efEa332c55696DaA", - "0x978eB534b26CB8749D352a2C94EC21e659e4248d", - "0xa7CA400d49BBa87EB606ee05af93689BD21FaB99", - "0x65ad2BF7E09af2597C140dF6386a3003d0F5f8Ee", - "0x835918a3fBDf946364a9aee3114173865b712663", - "0x6073cFfc1D46b1eA57BA89A28074cA734aCD7003", - "0x2B13D52dFd33E2eBd13232866fDf96088e77d596", - "0x55F5601357f6e0B10a3386914c93916c6C9A368A", - "0xA1D5D2d931b532A0503e97f540f65ed256f374e6", - "0x6C9258895FFBE2178b3EdEfE09AF304a1e99bF2F", - "0x973375b099943cDdFd390022CeA90D4F1d0c493c", - "0x8A8C879D39A74fCE0593714956bB7Ed048A5c1BF", - "0x9c42B0c70D0dAF1211f3aab2A1E6EC5E717dE12a", - "0x81a6383041593c556d1c8e69e2749b35b5008F09", - "0xF41b98a4C32bB61468C8001E0C69Ef64ce6DEa57", - "0x8FaE81bb674c89cCDE35a386587333D074b57786", - "0xa8258ED271BB9be9d7E16c5818E45eF6F2577d92", - "0x1e90474D2E83e7B7dD45553156Beb316845E66A4", - "0x2cCbbC4c10F5d807FDd447219B57D0b883a28DC8", - "0x1bBeAc736875c5043486A8a4374E6B5616eC8883", - "0x95add3DfEF3AE0A832607Dc71C4A9C6A6C2D7Eb7", - "0x744c6Eed427aF293b0106B46700fdDD3C9f62Eef", - "0x743Ec55fc166D24D2FD0211fb6Ce53926D0Ff3b1", - "0xDd03d2434C02c6BfFb097b7130528F9568b6C70d", - "0x97C12EFA574923E3ee445370d2dE432332857110", - "0xB69951a0642b55CD5731535ed5B290Fa49D3454A", - "0xBA56878729540404dE2aa14561b451aE2350744a", - "0x8D247f4Fbbe81429d3D164a5c9Ae0063210edBdC", - "0x850a146D7478dAAa98Fc26Fd85e6A24e50846A9d", - "0xfE1552DA65FAcAaC5B50b73CEDa4C993e16d4694", - "0x9705FE3586a7D768Fee061aAfE9384b1D4B8F2D8", - "0x5554672e67bA866B9861701D0e0494AB324aD19A", - "0xacc5c1e73d70F7F9622De2d574885Ce8E6981033", - "0xbe9E7b0ed19526544B55b697107231f9467a805f", - "0x172DBab6f5E62A1FE7E2bA5eA1624ADB33e0aa14", - "0x96725Fa2F9A0b5bAf80fC36C20C2cA79d86424ed", - "0xa392cCadABFf735dbFF69dC93d7C13f34A30611b", - "0xEbF0e04E47F726D0f44801dFEC5e705aDcd6694b", - "0xC0891e8FCeA09680BFe9170809fad1BCCa10b96b", - "0xA21000E7A5A2A2Bd9329428A859f9d7dcE0f0961", - "0x9A387307F7508DE113092BaFC5CB4B3AE0706521", - "0xBA719E0197470A790726075fD98EDEF04E2467af", - "0xda08BE028304db1A73a13Bce7C943127C2E393dB", - "0xfB4a965A35603010FeAcC648cA022Cb6A12D33F5", - "0x3Aa73ed90e9f0CEd87ff99CB60cA79019279e6CE", - "0x150bB505A9259b0be44FFb15415C79199E83c445", - "0xB170A41F2523220A12F84f17A54bD31953D98027", - "0x2Fcd65d9c8078644adCf1CB0cd70A1b61F3F9C5b", - "0x73006C818880d07dD510e165C3De3E74F2407187", - "0x747e6ABc102222f1dF65C662540dDf471241a644", - "0xeeEe5D271A56Aa09C4F8862aF514ADD3E882857c", - "0x98Ad82AB467bc8c70e0CC183a5826d903751b7d8", - "0xC624434420f6CbE835D6358A8223b78432773cEd", - "0x848e313d4b25bC0B48CaFdB6A72391E892E6A247", - "0x0025Ab2d69F6c2C3Ffac32Ab6A16e18c807518B8", - "0x2efe744ecc4F6BD55538da57D09DAE895C95b223", - "0xBc6d82D8d6632938394905Bb0217Ad9c673015d1", - "0xe1555c6EE61366a3f90135Dc704Acd25C3247ACA", - "0x2f51E78ff8aeC6A941C4CEeeb26B4A1f03737c50", - ] -) diff --git a/backend/app/modules/user/antisybil/service/initial.py b/backend/app/modules/user/antisybil/service/initial.py index a9ed1f5a5a..d39f7ab563 100644 --- a/backend/app/modules/user/antisybil/service/initial.py +++ b/backend/app/modules/user/antisybil/service/initial.py @@ -1,32 +1,33 @@ -from flask import current_app as app - -from eth_utils.address import to_checksum_address - -from typing import Dict, Optional, Tuple - -import json from datetime import datetime +from http import HTTPStatus from typing import List +from typing import Optional, Tuple + +from eth_utils.address import to_checksum_address +from flask import current_app as app -from app.constants import GUEST_LIST, GUEST_LIST_STAMP_PROVIDERS -from app.extensions import db -from app.exceptions import ExternalApiException, UserNotFound, AddressAlreadyDelegated from app.context.manager import Context +from app.exceptions import ExternalApiException, UserNotFound, AddressAlreadyDelegated +from app.extensions import db from app.infrastructure import database -from app.modules.common.delegation import get_hashed_addresses -from app.pydantic import Model from app.infrastructure.external_api.common import retry_request from app.infrastructure.external_api.gc_passport.score import ( issue_address_for_scoring, fetch_score, fetch_stamps, ) +from app.modules.common.delegation import get_hashed_addresses +from app.modules.user.antisybil.core import determine_antisybil_score +from app.modules.user.antisybil.dto import AntisybilStatusDTO +from app.pydantic import Model class GitcoinPassportAntisybil(Model): + timeout_list: set + def get_antisybil_status( self, _: Context, user_address: str - ) -> Optional[Tuple[float, datetime]]: + ) -> Optional[AntisybilStatusDTO]: user_address = to_checksum_address(user_address) try: score = database.user_antisybil.get_score_by_address(user_address) @@ -35,11 +36,8 @@ def get_antisybil_status( f"User {user_address} antisybil status: except UserNotFound" ) raise ex - if score is not None: - if user_address in GUEST_LIST and not _has_guest_stamp_applied_by_gp(score): - score.score = score.score + 21.0 - return score.score, score.expires_at - return None + + return determine_antisybil_score(score, user_address, self.timeout_list) def fetch_antisybil_status( self, _: Context, user_address: str @@ -52,7 +50,7 @@ def _retry_fetch(): raise ExternalApiException("GP: scoring is not completed yet", 503) if score["status"] != "DONE": - score = retry_request(_retry_fetch, 200) + score = retry_request(_retry_fetch, HTTPStatus.OK) all_stamps = fetch_stamps(user_address)["items"] cutoff = datetime.now() @@ -62,6 +60,10 @@ def _retry_fetch(): expires_at = _parse_expiration_date( min([stamp["credential"]["expirationDate"] for stamp in valid_stamps]) ) + + if user_address in self.timeout_list: + score["score"] = 0.0 + return float(score["score"]), expires_at, all_stamps def update_antisybil_status( @@ -83,19 +85,6 @@ def _verify_address_is_not_delegated(self, user_address: str): raise AddressAlreadyDelegated() -def _has_guest_stamp_applied_by_gp(score: Dict) -> bool: - def get_provider(stamp) -> str: - return stamp["credential"]["credentialSubject"]["provider"] - - all_stamps = json.loads(score.stamps) - stamps = [ - stamp - for stamp in all_stamps - if get_provider(stamp) in GUEST_LIST_STAMP_PROVIDERS - ] - return len(stamps) > 0 - - def _parse_expiration_date(timestamp_str: str) -> datetime: gp_api_formats = ["%Y-%m-%dT%H:%M:%S.%fZ", "%Y-%m-%dT%H:%M:%SZ"] for format_str in gp_api_formats: diff --git a/backend/app/settings.py b/backend/app/settings.py index 58988673a5..75c37cb388 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -28,7 +28,6 @@ class Config(object): GC_PASSPORT_SCORER_ID = os.getenv("GC_PASSPORT_SCORER_ID") GC_PASSPORT_SCORER_API_KEY = os.getenv("GC_PASSPORT_SCORER_API_KEY") SCHEDULER_ENABLED = parse_bool(os.getenv("SCHEDULER_ENABLED")) - CACHE_TYPE = "SimpleCache" DELEGATION_SALT = os.getenv("DELEGATION_SALT") DELEGATION_SALT_PRIMARY = os.getenv("DELEGATION_SALT_PRIMARY") @@ -93,6 +92,11 @@ class ProdConfig(Config): "pool_pre_ping": True, } X_REAL_IP_REQUIRED = parse_bool(os.getenv("X_REAL_IP_REQUIRED", "true")) + CACHE_TYPE = "RedisCache" + CACHE_REDIS_HOST = os.getenv("CACHE_REDIS_HOST") + CACHE_REDIS_PORT = os.getenv("CACHE_REDIS_PORT") + CACHE_REDIS_PASSWORD = os.getenv("CACHE_REDIS_PASSWORD") + CACHE_REDIS_DB = os.getenv("CACHE_REDIS_DB") class DevConfig(Config): @@ -108,6 +112,7 @@ class DevConfig(Config): SQLALCHEMY_DATABASE_URI = f"sqlite:///{DB_PATH}" SUBGRAPH_RETRY_TIMEOUT_SEC = 2 X_REAL_IP_REQUIRED = parse_bool(os.getenv("X_REAL_IP_REQUIRED", "false")) + CACHE_TYPE = "SimpleCache" class ComposeConfig(Config): @@ -117,6 +122,7 @@ class ComposeConfig(Config): DEBUG = True SQLALCHEMY_DATABASE_URI = os.getenv("DB_URI") X_REAL_IP_REQUIRED = parse_bool(os.getenv("X_REAL_IP_REQUIRED", "false")) + CACHE_TYPE = "SimpleCache" class TestConfig(Config): @@ -141,6 +147,7 @@ class TestConfig(Config): DELEGATION_SALT = "salt" DELEGATION_SALT_PRIMARY = "salt_primary" SUBGRAPH_RETRY_TIMEOUT_SEC = 2 + CACHE_TYPE = "SimpleCache" def get_config(): diff --git a/backend/migrations/__init__.py b/backend/migrations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/migrations/ipfs_integration/config.py b/backend/migrations/ipfs_integration/config.py new file mode 100644 index 0000000000..40c56de030 --- /dev/null +++ b/backend/migrations/ipfs_integration/config.py @@ -0,0 +1,17 @@ +from enum import StrEnum + +from core import build_filename + + +class Config: + FILENAME_PREFIX = "ipfs_projects_details" + EPOCH = 5 # change corresponding to the epoch + JSON_FILEPATH = f"files/{build_filename(FILENAME_PREFIX, EPOCH)}" + CID = [ + "QmdtFLK3sB7EwQTNaqtmBnZqnN2pYZcu6GmUSTrpvb9wcq" + ] # change corresponding to the epoch + GATEWAY_URL = "https://octant.infura-ipfs.io/ipfs/" + + +class ProjectDetails(StrEnum): + NAME = "name" diff --git a/backend/migrations/ipfs_integration/core.py b/backend/migrations/ipfs_integration/core.py new file mode 100644 index 0000000000..e1895f10bb --- /dev/null +++ b/backend/migrations/ipfs_integration/core.py @@ -0,0 +1,19 @@ +import re + + +def build_filename(prefix: str, epoch: int) -> str: + json_filename = f"{prefix}_epoch_{epoch}.json" + + return json_filename + + +def is_valid_ethereum_address(address: str) -> bool: + """ + Validates if the provided string is a valid Ethereum address. + + :param address: The address string to validate. + :return: True if valid, False otherwise. + """ + # Ethereum addresses are 42 characters long (including '0x') and hexadecimal + pattern = re.compile(r"^0x[a-fA-F0-9]{40}$") + return bool(pattern.match(address)) diff --git a/backend/migrations/ipfs_integration/files/cids.md b/backend/migrations/ipfs_integration/files/cids.md new file mode 100644 index 0000000000..52ce08c164 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/cids.md @@ -0,0 +1,6 @@ +# CIDs in history +- Epoch1: QmSQEFD35gKxdPEmngNt1CWe3kSwiiGqBn1Z3FZvWb8mvK +- Epoch2: Qmds9N5y2vkMuPTD6M4EBxNXnf3bjTDmzWBGnCkQGsMMGe +- Epoch3: QmSXcT18anMXKACTueom8GXw8zrxTBbHGB71atitf6gZ9V +- Epoch4: QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf +- Epoch5: QmdtFLK3sB7EwQTNaqtmBnZqnN2pYZcu6GmUSTrpvb9wcq diff --git a/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_1.json b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_1.json new file mode 100644 index 0000000000..0fb5c23963 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_1.json @@ -0,0 +1,100 @@ +[ + [ + { + "name": "Ethereum Cat Herders", + "address": "0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953" + }, + { + "name": "Praise", + "address": "0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95" + }, + { + "name": "RADAR Launch", + "address": "0x149D46eC060e75AE188876AdB6b24024637003C7" + }, + { + "name": "Tor Project", + "address": "0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f" + }, + { + "name": "DAO Drops", + "address": "0x1c01595f9534E33d411035AE99a4317faeC4f6Fe" + }, + { + "name": "OnionDAO", + "address": "0x20a1B17087482de88Fac6D7B5aE23A7175fd1395" + }, + { + "name": "Hypercerts", + "address": "0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9" + }, + { + "name": "MetaGame", + "address": "0x3455FbB4D34C6b47999B66c83aA7BD8FDDade638" + }, + { + "name": "PublicHAUS", + "address": "0x4A9a27d614a74Ee5524909cA27bdBcBB7eD3b315" + }, + { + "name": "Unitap", + "address": "0x4ADc8CC149A03F44386Bee80bab36F9e8022b195" + }, + { + "name": "Giveth", + "address": "0x6e8873085530406995170Da467010565968C7C62" + }, + { + "name": "BrightID", + "address": "0x78e084445C3F1006617e1f36794dd2261ecE4AE3" + }, + { + "name": "Kernel", + "address": "0x7DAC9Fc15C1Db4379D75A6E3f330aE849dFfcE18" + }, + { + "name": "Glo Dollar", + "address": "0x8c89a6bf53cCF63e7B4465Cc1b1330723B4BdcB7" + }, + { + "name": "Rotki", + "address": "0x9531C059098e3d194fF87FebB587aB07B30B1306" + }, + { + "name": "Clr.fund", + "address": "0xAb6D6a37c5110d1377832c451C33e4fA16A9BA05" + }, + { + "name": "BanklessDAO", + "address": "0xCf3efCE169acEC1B281C05E863F78acCF62BD944" + }, + { + "name": "EthStaker", + "address": "0xD165df4296C85e780509fa1eace0150d945d49Fd" + }, + { + "name": "GoodDollar", + "address": "0xF0652a820dd39EC956659E0018Da022132f2f40a" + }, + { + "name": "Protocol Guild", + "address": "0xF6CBDd6Ea6EC3C4359e33de0Ac823701Cc56C6c4" + }, + { + "name": "Drips", + "address": "0xcC7d34C76A9d08aa0109F7Bae35f29C1CE35355A" + }, + { + "name": "Pairwise", + "address": "0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1" + }, + { + "name": "Gitcoin", + "address": "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6" + }, + { + "name": "Gravity DAO", + "address": "0xfFbD35255008F86322051F2313D4b343540e0e00" + } + ] +] \ No newline at end of file diff --git a/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_2.json b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_2.json new file mode 100644 index 0000000000..8b373b2a63 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_2.json @@ -0,0 +1,100 @@ +[ + [ + { + "name": "Ethereum Cat Herders", + "address": "0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953" + }, + { + "name": "Praise", + "address": "0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95" + }, + { + "name": "L2BEAT", + "address": "0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29" + }, + { + "name": "Tor Project", + "address": "0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f" + }, + { + "name": "DAO Drops", + "address": "0x1c01595f9534E33d411035AE99a4317faeC4f6Fe" + }, + { + "name": "Hypercerts", + "address": "0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9" + }, + { + "name": "MetaGame", + "address": "0x3455FbB4D34C6b47999B66c83aA7BD8FDDade638" + }, + { + "name": "PublicHAUS", + "address": "0x4A9a27d614a74Ee5524909cA27bdBcBB7eD3b315" + }, + { + "name": "Funding the Commons", + "address": "0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20" + }, + { + "name": "Giveth", + "address": "0x6e8873085530406995170Da467010565968C7C62" + }, + { + "name": "BrightID", + "address": "0x78e084445C3F1006617e1f36794dd2261ecE4AE3" + }, + { + "name": "Kernel", + "address": "0x7DAC9Fc15C1Db4379D75A6E3f330aE849dFfcE18" + }, + { + "name": "Open Source Observer", + "address": "0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05" + }, + { + "name": "Glo Dollar", + "address": "0x8c89a6bf53cCF63e7B4465Cc1b1330723B4BdcB7" + }, + { + "name": "Rotki", + "address": "0x9531C059098e3d194fF87FebB587aB07B30B1306" + }, + { + "name": "Clr.fund", + "address": "0xAb6D6a37c5110d1377832c451C33e4fA16A9BA05" + }, + { + "name": "Shielded Voting", + "address": "0xB476Ee7D610DAe7B23B671EBC7Bd6112E9772969" + }, + { + "name": "EthStaker", + "address": "0xD165df4296C85e780509fa1eace0150d945d49Fd" + }, + { + "name": "Token Engineering Commons", + "address": "0xE2f413190Bb5D6AAcB4A056F1B5E1fD5B8141045" + }, + { + "name": "Protocol Guild", + "address": "0xF6CBDd6Ea6EC3C4359e33de0Ac823701Cc56C6c4" + }, + { + "name": "Drips", + "address": "0xcC7d34C76A9d08aa0109F7Bae35f29C1CE35355A" + }, + { + "name": "Pairwise", + "address": "0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1" + }, + { + "name": "Gitcoin", + "address": "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6" + }, + { + "name": "Revoke.cash", + "address": "0xe126b3E5d052f1F575828f61fEBA4f4f2603652a" + } + ] +] \ No newline at end of file diff --git a/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_3.json b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_3.json new file mode 100644 index 0000000000..3c3ff16819 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_3.json @@ -0,0 +1,124 @@ +[ + [ + { + "name": "StateOfEth", + "address": "0x0194325BF525Be0D4fBB0856894cEd74Da3B8356" + }, + { + "name": "Ethereum Cat Herders", + "address": "0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953" + }, + { + "name": "Praise", + "address": "0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95" + }, + { + "name": "L2BEAT", + "address": "0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29" + }, + { + "name": "Tor Project", + "address": "0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f" + }, + { + "name": "DAO Drops", + "address": "0x1c01595f9534E33d411035AE99a4317faeC4f6Fe" + }, + { + "name": "Hypercerts", + "address": "0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9" + }, + { + "name": "MetaGame", + "address": "0x3455FbB4D34C6b47999B66c83aA7BD8FDDade638" + }, + { + "name": "Web3.js", + "address": "0x4C6fd545fc18C6538eC304Ae549717CA58f0D6eb" + }, + { + "name": "Boring Security", + "address": "0x52C45Bab6d0827F44a973899666D9Cd18Fd90bCF" + }, + { + "name": "Web3.py", + "address": "0x5597cD8d55D2Db56b10FF4F8fe69C8922BF6C537" + }, + { + "name": "Funding the Commons", + "address": "0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20" + }, + { + "name": "Giveth", + "address": "0x6e8873085530406995170Da467010565968C7C62" + }, + { + "name": "ReFi DAO", + "address": "0x7340F1a1e4e38F43d2FCC85cdb2b764de36B40c0" + }, + { + "name": "Gardens", + "address": "0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714" + }, + { + "name": "Open Source Observer", + "address": "0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05" + }, + { + "name": "Glo Dollar", + "address": "0x8c89a6bf53cCF63e7B4465Cc1b1330723B4BdcB7" + }, + { + "name": "GrowThePie", + "address": "0x9438b8B447179740cD97869997a2FCc9b4AA63a2" + }, + { + "name": "Rotki", + "address": "0x9531C059098e3d194fF87FebB587aB07B30B1306" + }, + { + "name": "MetaGov", + "address": "0x9be7267002CAD0b8501f7322d50612CB13788Bcf" + }, + { + "name": "NiceNode", + "address": "0x9cce47E9cF12C6147c9844adBB81fE85880c4df4" + }, + { + "name": "Shielded Voting", + "address": "0xB476Ee7D610DAe7B23B671EBC7Bd6112E9772969" + }, + { + "name": "Ethereum Attestation Service", + "address": "0xBCA48834b3653ec795411EB0FCBE4038F8527d62" + }, + { + "name": "ETH Daily", + "address": "0xEB40A065854bd90126A4E697aeA0976BA51b2eE7" + }, + { + "name": "EthStaker", + "address": "0xF01CEe26213d1A6eaF16422241AE81f7C17B9f98" + }, + { + "name": "Protocol Guild", + "address": "0xF6CBDd6Ea6EC3C4359e33de0Ac823701Cc56C6c4" + }, + { + "name": "Drips", + "address": "0xcC7d34C76A9d08aa0109F7Bae35f29C1CE35355A" + }, + { + "name": "Pairwise", + "address": "0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1" + }, + { + "name": "Gitcoin", + "address": "0xde21F729137C5Af1b01d73aF1dC21eFfa2B8a0d6" + }, + { + "name": "Revoke.cash", + "address": "0xe126b3E5d052f1F575828f61fEBA4f4f2603652a" + } + ] +] \ No newline at end of file diff --git a/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_4.json b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_4.json new file mode 100644 index 0000000000..d28f6e8686 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_4.json @@ -0,0 +1,124 @@ +[ + [ + { + "name": "BuidlGuidl", + "address": "0x00080706a7D99CBC163D52dcF435205B1aD940D1" + }, + { + "name": "Ethereum Cat Herders", + "address": "0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953" + }, + { + "name": "Praise", + "address": "0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95" + }, + { + "name": "Vyper", + "address": "0x0c9dc7622aE5f56491aB4cCe060d6002450B79D2" + }, + { + "name": "L2BEAT", + "address": "0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29" + }, + { + "name": "Tor Project", + "address": "0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f" + }, + { + "name": "DAO Drops", + "address": "0x1c01595f9534E33d411035AE99a4317faeC4f6Fe" + }, + { + "name": "Blockscout", + "address": "0x242ba6d68FfEb4a098B591B32d370F973FF882B7" + }, + { + "name": "Hypercerts", + "address": "0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9" + }, + { + "name": "Protocol Guild", + "address": "0x3250c2CEE20FA34D1c4F68eAA87E53512e95A62a" + }, + { + "name": "Web3.js", + "address": "0x4C6fd545fc18C6538eC304Ae549717CA58f0D6eb" + }, + { + "name": "Dappnode", + "address": "0x53390590476dC98860316e4B46Bb9842AF55efc4" + }, + { + "name": "Web3.py", + "address": "0x5597cD8d55D2Db56b10FF4F8fe69C8922BF6C537" + }, + { + "name": "Funding the Commons", + "address": "0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20" + }, + { + "name": "TogetherCrew", + "address": "0x6612213880f80b298aB66375789E8Ef15e98604E" + }, + { + "name": "EcosynthesisX", + "address": "0x7380A42137D16a0E7684578d8b3d32e1fbD021B5" + }, + { + "name": "DeSci LATAM", + "address": "0x7Dd488f03E0A043b550E82D3C2685aA83B96407C" + }, + { + "name": "Gardens", + "address": "0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714" + }, + { + "name": "Open Source Observer", + "address": "0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05" + }, + { + "name": "growthepie", + "address": "0x9438b8B447179740cD97869997a2FCc9b4AA63a2" + }, + { + "name": "Rotki", + "address": "0x9531C059098e3d194fF87FebB587aB07B30B1306" + }, + { + "name": "NiceNode", + "address": "0x9cce47E9cF12C6147c9844adBB81fE85880c4df4" + }, + { + "name": "Shielded Voting", + "address": "0xB476Ee7D610DAe7B23B671EBC7Bd6112E9772969" + }, + { + "name": "Ethereum Attestation Service", + "address": "0xBCA48834b3653ec795411EB0FCBE4038F8527d62" + }, + { + "name": "EthStaker", + "address": "0xF01CEe26213d1A6eaF16422241AE81f7C17B9f98" + }, + { + "name": "PizzaDAO", + "address": "0xF41a98D4F2E52aa1ccB48F0b6539e955707b8F7a" + }, + { + "name": "Abundance Protocol", + "address": "0xc6FD734790E83820e311211B6d9A682BCa4ac97b" + }, + { + "name": "Pairwise", + "address": "0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1" + }, + { + "name": "Revoke.cash", + "address": "0xe126b3E5d052f1F575828f61fEBA4f4f2603652a" + }, + { + "name": "Trustful", + "address": "0xf7253A0E87E39d2cD6365919D4a3D56D431D0041" + } + ] +] diff --git a/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_5.json b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_5.json new file mode 100644 index 0000000000..c483d5aa98 --- /dev/null +++ b/backend/migrations/ipfs_integration/files/ipfs_projects_details_epoch_5.json @@ -0,0 +1,124 @@ +[ + [ + { + "name": "BuidlGuidl", + "address": "0x00080706a7D99CBC163D52dcF435205B1aD940D1" + }, + { + "name": "Ethereum Cat Herders", + "address": "0x02Cb3C150BEdca124d0aE8CcCb72fefbe705c953" + }, + { + "name": "Regens Unite", + "address": "0x08e40e1C0681D072a54Fc5868752c02bb3996FFA" + }, + { + "name": "Doots Podcast", + "address": "0x09A38B6187a2c44B6ba71c277c50764B5878b824" + }, + { + "name": "Praise", + "address": "0x0B7246eF74Ca7b37Fdc3D15be4f0b49876622F95" + }, + { + "name": "L2BEAT", + "address": "0x0cbF31Ef6545EE30f47651D1A991Bf0aeB03DF29" + }, + { + "name": "Kiwi News", + "address": "0x1337E2624ffEC537087c6774e9A18031CFEAf0a9" + }, + { + "name": "Tor Project", + "address": "0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f" + }, + { + "name": "Hypercerts", + "address": "0x2DCDF80f439843D7E0aD1fEF9E7a439B7917eAc9" + }, + { + "name": "Protocol Guild", + "address": "0x3250c2CEE20FA34D1c4F68eAA87E53512e95A62a" + }, + { + "name": "Web3.js", + "address": "0x4C6fd545fc18C6538eC304Ae549717CA58f0D6eb" + }, + { + "name": "Dappnode", + "address": "0x53390590476dC98860316e4B46Bb9842AF55efc4" + }, + { + "name": "Web3.py", + "address": "0x5597cD8d55D2Db56b10FF4F8fe69C8922BF6C537" + }, + { + "name": "Funding the Commons", + "address": "0x576edCed7475D8F64a5e2D5227c93Ca57d7f5d20" + }, + { + "name": "EcoSynthesisX", + "address": "0x7380A42137D16a0E7684578d8b3d32e1fbD021B5" + }, + { + "name": "DeSci LATAM", + "address": "0x7Dd488f03E0A043b550E82D3C2685aA83B96407C" + }, + { + "name": "Gardens", + "address": "0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714" + }, + { + "name": "Open Source Observer", + "address": "0x87fEEd6162CB7dFe6B62F64366742349bF4D1B05" + }, + { + "name": "Rotki", + "address": "0x9531C059098e3d194fF87FebB587aB07B30B1306" + }, + { + "name": "Greenpill Dev Guild", + "address": "0x992A3a242D6471d24783b4C2C6AF3EC7df871761" + }, + { + "name": "MetaGov", + "address": "0x9be7267002CAD0b8501f7322d50612CB13788Bcf" + }, + { + "name": "Ethereum Attestation Service", + "address": "0xBCA48834b3653ec795411EB0FCBE4038F8527d62" + }, + { + "name": "PizzaDAO", + "address": "0xF41a98D4F2E52aa1ccB48F0b6539e955707b8F7a" + }, + { + "name": "B<>rder/ess Developers", + "address": "0xFC1436689F68079Fc17A931666b7947789229ed8" + }, + { + "name": "Aestus MEV Relay", + "address": "0xa095Ee27B11FCAac8e1be84891ab62C74F08C854" + }, + { + "name": "EthStaker", + "address": "0xa83a92297B3d80A70cC396bf74424971A9890704" + }, + { + "name": "Pairwise", + "address": "0xd1B8dB70Ded72dB850713b2ce7e1A4FfAfAD95d1" + }, + { + "name": "LottoPGF", + "address": "0xe7d4Ac3c77cF3683E0d15C15eaba7CDB8c092D98" + }, + { + "name": "Gravity DAO", + "address": "0xfFbD35255008F86322051F2313D4b343540e0e00" + }, + { + "name": "Revoke.cash", + "address": "0xfcBf17200C64E860F6639aa12B525015d115F863" + } + ] +] diff --git a/backend/migrations/ipfs_integration/logic.py b/backend/migrations/ipfs_integration/logic.py new file mode 100644 index 0000000000..af677bd9af --- /dev/null +++ b/backend/migrations/ipfs_integration/logic.py @@ -0,0 +1,127 @@ +import json +import os +from typing import List, Optional, Dict + +import requests +from bs4 import BeautifulSoup + +from config import Config, ProjectDetails +from core import is_valid_ethereum_address + + +def get_addresses_from_cid(cid: str) -> List[str]: + """ + Retrieves a list of Ethereum addresses from the CID by parsing the HTML content. + + :param cid: The CID to fetch. + :param gateway_url: The base URL of the IPFS gateway. + :return: A list of Ethereum addresses found under the CID. + """ + gateway_url = Config.GATEWAY_URL + url = f"{gateway_url}{cid}/" # Ensure the trailing slash + try: + response = requests.get(url) + response.raise_for_status() + html_content = response.text + + soup = BeautifulSoup(html_content, "html.parser") + addresses = [] + for link in soup.find_all("a"): + href = link.get("href") + if href and href not in ("../", "?", ""): + address = os.path.basename(href) + address = address.strip("/") + + if is_valid_ethereum_address(address): + addresses.append(address) + return addresses + except requests.exceptions.RequestException as e: + print(f"Error fetching CID {cid}: {e}") + return [] + + +def get_json_data_from_address( + cid: str, address: str, gateway_url: str = Config.GATEWAY_URL +) -> Optional[str]: + """ + Fetches the JSON data from the given Ethereum address under the CID. + + :param cid: The CID under which the address resides. + :param address: The Ethereum address (file name) to fetch. + :param gateway_url: The base URL of the IPFS gateway. + :return: A dictionary containing the JSON data, or None if an error occurs. + """ + url = f"{gateway_url}{cid}/{address}" + try: + response = requests.get(url) + response.raise_for_status() + content_type = response.headers.get("Content-Type", "") + + if "application/json" in content_type: + json_data = response.json() + else: + json_data = json.loads(response.text) + return json_data + except requests.exceptions.RequestException as e: + print(f"Error fetching address {address} under CID {cid}: {e}") + return None + except json.JSONDecodeError as e: + print(f"Error decoding JSON from address {address} under CID {cid}: {e}") + return None + + +def extract_details_from_json( + json_data: Dict, *, details_to_extract: List[ProjectDetails] +) -> Dict: + """ + Extracts the 'name' field from the JSON data. + + :param json_data: The JSON data dictionary. + :return: The 'name' value, or None if not found. + """ + output = {} + for detail_to_extract in details_to_extract: + detail_to_extract = detail_to_extract.value + output[detail_to_extract] = json_data.get(detail_to_extract) + return output + + +def main(): + all_projects_details = [] + + for cid in Config.CID: + print(f"\nProcessing CID: {cid}") + addresses = get_addresses_from_cid(cid) + if not addresses: + print(f"No Ethereum addresses found under CID {cid}.") + continue + + projects_details = [] + for address in addresses: + json_data = get_json_data_from_address(cid, address) + + if not json_data: + print(f"Failed to retrieve JSON data for address: {address}") + continue + + project_details = extract_details_from_json( + json_data, details_to_extract=[ProjectDetails.NAME] + ) + project_details["address"] = address + + name = project_details.get(ProjectDetails.NAME.value) + projects_details.append(project_details) + + print(f"Project name for address {address}: {name}") + + print(f"Number of projects for {cid}", len(projects_details)) + all_projects_details.append(projects_details) + + print(f"All projects details that are saved in a JSON file: {all_projects_details}") + + with open(Config.JSON_FILEPATH, "w") as f: + json.dump(all_projects_details, f, indent=4) + + +if __name__ == "__main__": + main() diff --git a/backend/migrations/ipfs_integration/migration_helpers.py b/backend/migrations/ipfs_integration/migration_helpers.py new file mode 100644 index 0000000000..433a31caf1 --- /dev/null +++ b/backend/migrations/ipfs_integration/migration_helpers.py @@ -0,0 +1,53 @@ +import json +import os +from datetime import datetime +from typing import Dict +from alembic import op + + +def _load_json_data(filepath: str) -> Dict: + with open(filepath, "r") as file: + data = json.load(file) + return data[0] + + +def _get_json_path(filename: str) -> str: + json_filename = os.path.join(os.path.dirname(__file__), "files", filename) + return json_filename + + +def _prepare_project_upsert_query(project: dict, epoch: int) -> str: + current_time = datetime.utcnow() + + return f""" + INSERT INTO project_details (name, address, created_at, epoch) + VALUES ('{project["name"]}', '{project["address"]}', '{current_time}', {epoch}) + ON CONFLICT (address, epoch) + DO UPDATE SET + name = EXCLUDED.name, + created_at = '{current_time}'; + """ + + +def _prepare_delete_project_query(project: dict, epoch: int) -> str: + return f""" + DELETE FROM project_details WHERE address = '{project["address"]}' AND epoch = {epoch} + """ + + +def upgrade(filename: str, epoch: int): + json_filepath = _get_json_path(filename) + project_data = _load_json_data(json_filepath) + + for project in project_data: + query = _prepare_project_upsert_query(project, epoch) + op.execute(query) + + +def downgrade(filename: str, epoch: int): + json_filepath = _get_json_path(filename) + project_data = _load_json_data(json_filepath) + + for project in project_data: + query = _prepare_delete_project_query(project, epoch) + op.execute(query) diff --git a/backend/migrations/versions/2060b4c232c6_add_projects_details_from_ipfs_for_.py b/backend/migrations/versions/2060b4c232c6_add_projects_details_from_ipfs_for_.py new file mode 100644 index 0000000000..cd989c3483 --- /dev/null +++ b/backend/migrations/versions/2060b4c232c6_add_projects_details_from_ipfs_for_.py @@ -0,0 +1,28 @@ +"""Add projects details from IPFS for Epoch 5 + +Revision ID: 2060b4c232c6 +Revises: c34003767fa8 +Create Date: 2024-10-09 22:58:18.845671 + +""" +from migrations.ipfs_integration import migration_helpers as ipfs_migration + +revision = "2060b4c232c6" +down_revision = "c34003767fa8" +branch_labels = None +depends_on = None + +FILENAME = "ipfs_projects_details_epoch_{}.json" +EPOCHS = (5,) + + +def upgrade(): + for epoch in EPOCHS: + filename = FILENAME.format(epoch) + ipfs_migration.upgrade(filename, epoch) + + +def downgrade(): + for epoch in EPOCHS: + filename = FILENAME.format(epoch) + ipfs_migration.downgrade(filename, epoch) diff --git a/backend/migrations/versions/5398b1538a31_remove_existing_delegations_before_e5_s_.py b/backend/migrations/versions/5398b1538a31_remove_existing_delegations_before_e5_s_.py new file mode 100644 index 0000000000..c2e14b5938 --- /dev/null +++ b/backend/migrations/versions/5398b1538a31_remove_existing_delegations_before_e5_s_.py @@ -0,0 +1,23 @@ +"""Remove existing delegations before E5's AW + +Revision ID: 5398b1538a31 +Revises: 2060b4c232c6 +Create Date: 2024-10-11 12:58:19.806125 + +""" +from alembic import op + + +revision = "5398b1538a31" +down_revision = "2060b4c232c6" +branch_labels = None +depends_on = None + + +def upgrade(): + op.execute("TRUNCATE TABLE score_delegation") + op.execute("TRUNCATE TABLE gitcoin_passport_stamps") + + +def downgrade(): + pass diff --git a/backend/migrations/versions/87b2cefcfa11_add_projectsdetails_table.py b/backend/migrations/versions/87b2cefcfa11_add_projectsdetails_table.py new file mode 100644 index 0000000000..823fa45ec9 --- /dev/null +++ b/backend/migrations/versions/87b2cefcfa11_add_projectsdetails_table.py @@ -0,0 +1,32 @@ +"""Add ProjectsDetails table + +Revision ID: 87b2cefcfa11 +Revises: 8b425b454a86 +Create Date: 2024-09-20 11:12:33.753739 + +""" +from alembic import op +import sqlalchemy as sa + + +revision = "87b2cefcfa11" +down_revision = "8b425b454a86" +branch_labels = None +depends_on = None + + +def upgrade(): + op.create_table( + "project_details", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("address", sa.String(length=42), nullable=False), + sa.Column("name", sa.String(), nullable=False), + sa.Column("epoch", sa.Integer(), nullable=False), + sa.Column("created_at", sa.TIMESTAMP(), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("address", "epoch", name="uq_address_epoch"), + ) + + +def downgrade(): + op.drop_table("project_details") diff --git a/backend/migrations/versions/8b425b454a86_fix_created_at_field.py b/backend/migrations/versions/8b425b454a86_fix_created_at_field.py new file mode 100644 index 0000000000..0d8c01b0aa --- /dev/null +++ b/backend/migrations/versions/8b425b454a86_fix_created_at_field.py @@ -0,0 +1,28 @@ +"""Fix created_at field + +Revision ID: 8b425b454a86 +Revises: 999999999999 +Create Date: 2024-08-02 12:32:50.490759 + +""" +from alembic import op +import sqlalchemy as sa + +revision = "8b425b454a86" +down_revision = "999999999999" +branch_labels = None +depends_on = None + +HASH1 = "a1ee927c11efc35ffef40fa51547e0770df76aab9085da332311ac9d629fa518" +HASH2 = "f6b78725294faab4442f38aedb97ff7bc8fcaf9d73edf9845e1c57496e6d2913" +HASH3 = "93bf4d5bb695b96edd45c0d4eae59fe3f5ecc657f7137407288fd82834476a0b" + + +def upgrade(): + query = f"UPDATE score_delegation SET created_at = make_date(2024, 7, 17) WHERE hashed_addr IN ('{HASH1}', '{HASH2}', '{HASH3}');" + op.execute(query) + + +def downgrade(): + query = f"UPDATE score_delegation SET created_at = NULL WHERE hashed_addr IN ('{HASH1}', '{HASH2}', '{HASH3}');" + op.execute(query) diff --git a/backend/migrations/versions/c34003767fa8_add_projects_details_from_ipfs_for_.py b/backend/migrations/versions/c34003767fa8_add_projects_details_from_ipfs_for_.py new file mode 100644 index 0000000000..45519e4156 --- /dev/null +++ b/backend/migrations/versions/c34003767fa8_add_projects_details_from_ipfs_for_.py @@ -0,0 +1,28 @@ +"""Add projects details from IPFS for Epoch 1, 2, 3 and 4 + +Revision ID: c34003767fa8 +Revises: 87b2cefcfa11 +Create Date: 2024-09-20 11:14:31.965331 + +""" +from migrations.ipfs_integration import migration_helpers as ipfs_migration + +revision = "c34003767fa8" +down_revision = "87b2cefcfa11" +branch_labels = None +depends_on = None + +FILENAME = "ipfs_projects_details_epoch_{}.json" +EPOCHS = (1, 2, 3, 4) + + +def upgrade(): + for epoch in EPOCHS: + filename = FILENAME.format(epoch) + ipfs_migration.upgrade(filename, epoch) + + +def downgrade(): + for epoch in EPOCHS: + filename = FILENAME.format(epoch) + ipfs_migration.downgrade(filename, epoch) diff --git a/backend/poetry.lock b/backend/poetry.lock index 6aa8c0c014..64a06f342d 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -1,10 +1,9 @@ -# This file is automatically @generated by Poetry 1.4.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.0 and should not be changed by hand. [[package]] name = "aiohttp" version = "3.9.5" description = "Async http client/server framework (asyncio)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -100,7 +99,6 @@ speedups = ["Brotli", "aiodns", "brotlicffi"] name = "aiosignal" version = "1.3.1" description = "aiosignal: a list of registered asynchronous callbacks" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -111,11 +109,28 @@ files = [ [package.dependencies] frozenlist = ">=1.1.0" +[[package]] +name = "aiosqlite" +version = "0.20.0" +description = "asyncio bridge to the standard sqlite3 module" +optional = false +python-versions = ">=3.8" +files = [ + {file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"}, + {file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"}, +] + +[package.dependencies] +typing_extensions = ">=4.0" + +[package.extras] +dev = ["attribution (==1.7.0)", "black (==24.2.0)", "coverage[toml] (==7.4.1)", "flake8 (==7.0.0)", "flake8-bugbear (==24.2.6)", "flit (==3.9.0)", "mypy (==1.8.0)", "ufmt (==2.3.0)", "usort (==1.0.8.post1)"] +docs = ["sphinx (==7.2.6)", "sphinx-mdinclude (==0.5.3)"] + [[package]] name = "alembic" version = "1.13.1" description = "A database migration tool for SQLAlchemy." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -135,7 +150,6 @@ tz = ["backports.zoneinfo"] name = "aniso8601" version = "9.0.1" description = "A library for parsing ISO 8601 strings." -category = "main" optional = false python-versions = "*" files = [ @@ -150,7 +164,6 @@ dev = ["black", "coverage", "isort", "pre-commit", "pyenchant", "pylint"] name = "annotated-types" version = "0.7.0" description = "Reusable constraint types to use with typing.Annotated" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -162,7 +175,6 @@ files = [ name = "anyio" version = "4.4.0" description = "High level compatibility layer for multiple asynchronous event loop implementations" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -183,7 +195,6 @@ trio = ["trio (>=0.23)"] name = "apscheduler" version = "3.10.4" description = "In-process task scheduler with Cron-like capabilities" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -194,7 +205,7 @@ files = [ [package.dependencies] pytz = "*" six = ">=1.4.0" -tzlocal = ">=2.0,<3.0.0 || >=4.0.0" +tzlocal = ">=2.0,<3.dev0 || >=4.dev0" [package.extras] doc = ["sphinx", "sphinx-rtd-theme"] @@ -208,11 +219,78 @@ tornado = ["tornado (>=4.3)"] twisted = ["twisted"] zookeeper = ["kazoo"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + +[[package]] +name = "asyncpg" +version = "0.29.0" +description = "An asyncio PostgreSQL driver" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "asyncpg-0.29.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72fd0ef9f00aeed37179c62282a3d14262dbbafb74ec0ba16e1b1864d8a12169"}, + {file = "asyncpg-0.29.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52e8f8f9ff6e21f9b39ca9f8e3e33a5fcdceaf5667a8c5c32bee158e313be385"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a9e6823a7012be8b68301342ba33b4740e5a166f6bbda0aee32bc01638491a22"}, + {file = "asyncpg-0.29.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:746e80d83ad5d5464cfbf94315eb6744222ab00aa4e522b704322fb182b83610"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:ff8e8109cd6a46ff852a5e6bab8b0a047d7ea42fcb7ca5ae6eaae97d8eacf397"}, + {file = "asyncpg-0.29.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:97eb024685b1d7e72b1972863de527c11ff87960837919dac6e34754768098eb"}, + {file = "asyncpg-0.29.0-cp310-cp310-win32.whl", hash = "sha256:5bbb7f2cafd8d1fa3e65431833de2642f4b2124be61a449fa064e1a08d27e449"}, + {file = "asyncpg-0.29.0-cp310-cp310-win_amd64.whl", hash = "sha256:76c3ac6530904838a4b650b2880f8e7af938ee049e769ec2fba7cd66469d7772"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4900ee08e85af01adb207519bb4e14b1cae8fd21e0ccf80fac6aa60b6da37b4"}, + {file = "asyncpg-0.29.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a65c1dcd820d5aea7c7d82a3fdcb70e096f8f70d1a8bf93eb458e49bfad036ac"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b52e46f165585fd6af4863f268566668407c76b2c72d366bb8b522fa66f1870"}, + {file = "asyncpg-0.29.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dc600ee8ef3dd38b8d67421359779f8ccec30b463e7aec7ed481c8346decf99f"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:039a261af4f38f949095e1e780bae84a25ffe3e370175193174eb08d3cecab23"}, + {file = "asyncpg-0.29.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6feaf2d8f9138d190e5ec4390c1715c3e87b37715cd69b2c3dfca616134efd2b"}, + {file = "asyncpg-0.29.0-cp311-cp311-win32.whl", hash = "sha256:1e186427c88225ef730555f5fdda6c1812daa884064bfe6bc462fd3a71c4b675"}, + {file = "asyncpg-0.29.0-cp311-cp311-win_amd64.whl", hash = "sha256:cfe73ffae35f518cfd6e4e5f5abb2618ceb5ef02a2365ce64f132601000587d3"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:6011b0dc29886ab424dc042bf9eeb507670a3b40aece3439944006aafe023178"}, + {file = "asyncpg-0.29.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b544ffc66b039d5ec5a7454667f855f7fec08e0dfaf5a5490dfafbb7abbd2cfb"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d84156d5fb530b06c493f9e7635aa18f518fa1d1395ef240d211cb563c4e2364"}, + {file = "asyncpg-0.29.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54858bc25b49d1114178d65a88e48ad50cb2b6f3e475caa0f0c092d5f527c106"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bde17a1861cf10d5afce80a36fca736a86769ab3579532c03e45f83ba8a09c59"}, + {file = "asyncpg-0.29.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:37a2ec1b9ff88d8773d3eb6d3784dc7e3fee7756a5317b67f923172a4748a175"}, + {file = "asyncpg-0.29.0-cp312-cp312-win32.whl", hash = "sha256:bb1292d9fad43112a85e98ecdc2e051602bce97c199920586be83254d9dafc02"}, + {file = "asyncpg-0.29.0-cp312-cp312-win_amd64.whl", hash = "sha256:2245be8ec5047a605e0b454c894e54bf2ec787ac04b1cb7e0d3c67aa1e32f0fe"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0009a300cae37b8c525e5b449233d59cd9868fd35431abc470a3e364d2b85cb9"}, + {file = "asyncpg-0.29.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5cad1324dbb33f3ca0cd2074d5114354ed3be2b94d48ddfd88af75ebda7c43cc"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:012d01df61e009015944ac7543d6ee30c2dc1eb2f6b10b62a3f598beb6531548"}, + {file = "asyncpg-0.29.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:000c996c53c04770798053e1730d34e30cb645ad95a63265aec82da9093d88e7"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e0bfe9c4d3429706cf70d3249089de14d6a01192d617e9093a8e941fea8ee775"}, + {file = "asyncpg-0.29.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:642a36eb41b6313ffa328e8a5c5c2b5bea6ee138546c9c3cf1bffaad8ee36dd9"}, + {file = "asyncpg-0.29.0-cp38-cp38-win32.whl", hash = "sha256:a921372bbd0aa3a5822dd0409da61b4cd50df89ae85150149f8c119f23e8c408"}, + {file = "asyncpg-0.29.0-cp38-cp38-win_amd64.whl", hash = "sha256:103aad2b92d1506700cbf51cd8bb5441e7e72e87a7b3a2ca4e32c840f051a6a3"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5340dd515d7e52f4c11ada32171d87c05570479dc01dc66d03ee3e150fb695da"}, + {file = "asyncpg-0.29.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e17b52c6cf83e170d3d865571ba574577ab8e533e7361a2b8ce6157d02c665d3"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f100d23f273555f4b19b74a96840aa27b85e99ba4b1f18d4ebff0734e78dc090"}, + {file = "asyncpg-0.29.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48e7c58b516057126b363cec8ca02b804644fd012ef8e6c7e23386b7d5e6ce83"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:f9ea3f24eb4c49a615573724d88a48bd1b7821c890c2effe04f05382ed9e8810"}, + {file = "asyncpg-0.29.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8d36c7f14a22ec9e928f15f92a48207546ffe68bc412f3be718eedccdf10dc5c"}, + {file = "asyncpg-0.29.0-cp39-cp39-win32.whl", hash = "sha256:797ab8123ebaed304a1fad4d7576d5376c3a006a4100380fb9d517f0b59c1ab2"}, + {file = "asyncpg-0.29.0-cp39-cp39-win_amd64.whl", hash = "sha256:cce08a178858b426ae1aa8409b5cc171def45d4293626e7aa6510696d46decd8"}, + {file = "asyncpg-0.29.0.tar.gz", hash = "sha256:d1c49e1f44fffafd9a55e1a9b101590859d881d639ea2922516f5d9c512d354e"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_version < \"3.12.0\""} + +[package.extras] +docs = ["Sphinx (>=5.3.0,<5.4.0)", "sphinx-rtd-theme (>=1.2.2)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["flake8 (>=6.1,<7.0)", "uvloop (>=0.15.3)"] + [[package]] name = "attrs" version = "23.2.0" description = "Classes Without Boilerplate" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -232,7 +310,6 @@ tests-no-zope = ["attrs[tests-mypy]", "cloudpickle", "hypothesis", "pympler", "p name = "backoff" version = "2.2.1" description = "Function decoration for backoff and retry" -category = "main" optional = false python-versions = ">=3.7,<4.0" files = [ @@ -244,7 +321,6 @@ files = [ name = "bidict" version = "0.23.1" description = "The bidirectional mapping library for Python." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -256,7 +332,6 @@ files = [ name = "bitarray" version = "2.9.2" description = "efficient arrays of booleans -- C extension" -category = "main" optional = false python-versions = "*" files = [ @@ -388,7 +463,6 @@ files = [ name = "black" version = "23.12.1" description = "The uncompromising code formatter." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -433,7 +507,6 @@ uvloop = ["uvloop (>=0.15.2)"] name = "blinker" version = "1.8.2" description = "Fast, simple object-to-object and broadcast signaling" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -445,7 +518,6 @@ files = [ name = "cachelib" version = "0.9.0" description = "A collection of cache libraries in the same API interface." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -457,7 +529,6 @@ files = [ name = "certifi" version = "2024.6.2" description = "Python package for providing Mozilla's CA Bundle." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -469,7 +540,6 @@ files = [ name = "charset-normalizer" version = "3.3.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -569,7 +639,6 @@ files = [ name = "ckzg" version = "1.0.2" description = "Python bindings for C-KZG-4844" -category = "main" optional = false python-versions = "*" files = [ @@ -664,7 +733,6 @@ files = [ name = "click" version = "8.1.7" description = "Composable command line interface toolkit" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -679,7 +747,6 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -691,7 +758,6 @@ files = [ name = "coverage" version = "7.5.3" description = "Code coverage measurement for Python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -756,7 +822,6 @@ toml = ["tomli"] name = "cytoolz" version = "0.12.3" description = "Cython implementation of Toolz: High performance functional utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -876,7 +941,6 @@ cython = ["cython"] name = "dataclass-wizard" version = "0.22.3" description = "Marshal dataclasses to/from JSON. Use field properties with initial values. Construct a dataclass schema with JSON input." -category = "main" optional = false python-versions = "*" files = [ @@ -893,7 +957,6 @@ yaml = ["PyYAML (>=5.3)"] name = "dnspython" version = "2.6.1" description = "DNS toolkit" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -914,7 +977,6 @@ wmi = ["wmi (>=1.5.1)"] name = "epc" version = "0.0.5" description = "EPC (RPC stack for Emacs Lisp) implementation in Python" -category = "dev" optional = false python-versions = "*" files = [ @@ -928,7 +990,6 @@ sexpdata = ">=0.0.3" name = "eth-abi" version = "4.2.1" description = "eth_abi: Python utilities for working with Ethereum ABI definitions, especially encoding and decoding" -category = "main" optional = false python-versions = ">=3.7.2, <4" files = [ @@ -952,7 +1013,6 @@ tools = ["hypothesis (>=4.18.2,<5.0.0)"] name = "eth-account" version = "0.11.2" description = "eth-account: Sign Ethereum transactions and messages with local private keys" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -980,7 +1040,6 @@ test = ["coverage", "hypothesis (>=4.18.0,<5)", "pytest (>=7.0.0)", "pytest-xdis name = "eth-hash" version = "0.7.0" description = "eth-hash: The Ethereum hashing function, keccak256, sometimes (erroneously) called sha3" -category = "main" optional = false python-versions = ">=3.8, <4" files = [ @@ -1002,7 +1061,6 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-keyfile" version = "0.8.1" description = "eth-keyfile: A library for handling the encrypted keyfiles used to store ethereum private keys" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -1024,7 +1082,6 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-keys" version = "0.5.1" description = "eth-keys: Common API for Ethereum key operations" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -1046,7 +1103,6 @@ test = ["asn1tools (>=0.146.2)", "eth-hash[pysha3]", "factory-boy (>=3.0.1)", "h name = "eth-rlp" version = "1.0.1" description = "eth-rlp: RLP definitions for common Ethereum objects in Python" -category = "main" optional = false python-versions = ">=3.8, <4" files = [ @@ -1069,7 +1125,6 @@ test = ["eth-hash[pycryptodome]", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-typing" version = "4.3.1" description = "eth-typing: Common type annotations for ethereum python packages" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -1089,7 +1144,6 @@ test = ["pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] name = "eth-utils" version = "4.1.1" description = "eth-utils: Common utility functions for python code that interacts with Ethereum" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -1112,7 +1166,6 @@ test = ["hypothesis (>=4.43.0)", "mypy (==1.5.1)", "pytest (>=7.0.0)", "pytest-x name = "eventlet" version = "0.33.3" description = "Highly concurrent networking library" -category = "main" optional = false python-versions = "*" files = [ @@ -1125,11 +1178,30 @@ dnspython = ">=1.15.0" greenlet = ">=0.3" six = ">=1.10.0" +[[package]] +name = "fastapi" +version = "0.112.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.112.0-py3-none-any.whl", hash = "sha256:3487ded9778006a45834b8c816ec4a48d522e2631ca9e75ec5a774f1b052f821"}, + {file = "fastapi-0.112.0.tar.gz", hash = "sha256:d262bc56b7d101d1f4e8fc0ad2ac75bb9935fec504d2b7117686cec50710cf05"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.37.2,<0.38.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +standard = ["email_validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.5)", "httpx (>=0.23.0)", "jinja2 (>=2.11.2)", "python-multipart (>=0.0.7)", "uvicorn[standard] (>=0.12.0)"] + [[package]] name = "flake8" version = "6.1.0" description = "the modular source code checker: pep8 pyflakes and co" -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -1146,7 +1218,6 @@ pyflakes = ">=3.1.0,<3.2.0" name = "flake8-bugbear" version = "23.12.2" description = "A plugin for flake8 finding likely bugs and design problems in your program. Contains warnings that don't belong in pyflakes and pycodestyle." -category = "dev" optional = false python-versions = ">=3.8.1" files = [ @@ -1165,7 +1236,6 @@ dev = ["coverage", "hypothesis", "hypothesmith (>=0.2)", "pre-commit", "pytest", name = "flake8-pyproject" version = "1.2.3" description = "Flake8 plug-in loading the configuration from pyproject.toml" -category = "dev" optional = false python-versions = ">= 3.6" files = [ @@ -1182,7 +1252,6 @@ dev = ["pyTest", "pyTest-cov"] name = "flask" version = "2.3.3" description = "A simple framework for building complex web applications." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1205,7 +1274,6 @@ dotenv = ["python-dotenv"] name = "flask-apscheduler" version = "1.13.1" description = "Adds APScheduler support to Flask" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1221,7 +1289,6 @@ python-dateutil = ">=2.4.2" name = "flask-caching" version = "2.3.0" description = "Adds caching support to Flask applications." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1237,7 +1304,6 @@ Flask = "*" name = "flask-cors" version = "4.0.1" description = "A Flask extension adding a decorator for CORS support" -category = "main" optional = false python-versions = "*" files = [ @@ -1252,7 +1318,6 @@ Flask = ">=0.9" name = "flask-migrate" version = "4.0.7" description = "SQLAlchemy database migrations for Flask applications using Alembic." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1269,7 +1334,6 @@ Flask-SQLAlchemy = ">=1.0" name = "flask-restx" version = "1.3.0" description = "Fully featured framework for fast, easy and documented API development with Flask" -category = "main" optional = false python-versions = "*" files = [ @@ -1294,7 +1358,6 @@ test = ["Faker (==2.0.0)", "blinker", "invoke (==2.2.0)", "mock (==3.0.5)", "pyt name = "flask-socketio" version = "5.3.6" description = "Socket.IO integration for Flask applications" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -1313,7 +1376,6 @@ docs = ["sphinx"] name = "flask-sqlalchemy" version = "3.1.1" description = "Add SQLAlchemy support to your Flask application." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1329,7 +1391,6 @@ sqlalchemy = ">=2.0.16" name = "freezegun" version = "1.5.1" description = "Let your Python tests travel through time" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1344,7 +1405,6 @@ python-dateutil = ">=2.7" name = "frozenlist" version = "1.4.1" description = "A list-like structure which implements collections.abc.MutableSequence" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1431,7 +1491,6 @@ files = [ name = "gmpy2" version = "2.1.5" description = "gmpy2 interface to GMP/MPIR, MPFR, and MPC for Python 2.7 and 3.5+" -category = "main" optional = false python-versions = "*" files = [ @@ -1492,7 +1551,6 @@ files = [ name = "gql" version = "3.5.0" description = "GraphQL client for Python" -category = "main" optional = false python-versions = "*" files = [ @@ -1525,7 +1583,6 @@ websockets = ["websockets (>=10,<12)"] name = "graphql-core" version = "3.2.3" description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL." -category = "main" optional = false python-versions = ">=3.6,<4" files = [ @@ -1537,7 +1594,6 @@ files = [ name = "greenlet" version = "3.0.3" description = "Lightweight in-process concurrent programming" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1609,7 +1665,6 @@ test = ["objgraph", "psutil"] name = "h11" version = "0.14.0" description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1621,7 +1676,6 @@ files = [ name = "hexbytes" version = "0.3.1" description = "hexbytes: Python `bytes` subclass that decodes hex, with a readable console output" -category = "main" optional = false python-versions = ">=3.7, <4" files = [ @@ -1635,11 +1689,58 @@ doc = ["sphinx (>=5.0.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] lint = ["black (>=22)", "flake8 (==6.0.0)", "flake8-bugbear (==23.3.23)", "isort (>=5.10.1)", "mypy (==0.971)", "pydocstyle (>=5.0.0)"] test = ["eth-utils (>=1.0.1,<3)", "hypothesis (>=3.44.24,<=6.31.6)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + [[package]] name = "idna" version = "3.7" description = "Internationalized Domain Names in Applications (IDNA)" -category = "main" optional = false python-versions = ">=3.5" files = [ @@ -1651,7 +1752,6 @@ files = [ name = "importlib-resources" version = "6.4.0" description = "Read resources from Python packages" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1667,7 +1767,6 @@ testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "p name = "importmagic" version = "0.1.7" description = "Python Import Magic - automagically add, remove and manage imports" -category = "dev" optional = false python-versions = "*" files = [ @@ -1681,7 +1780,6 @@ setuptools = ">=0.6b1" name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -1689,11 +1787,24 @@ files = [ {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, ] +[[package]] +name = "isort" +version = "5.13.2" +description = "A Python utility / library to sort Python imports." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, + {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, +] + +[package.extras] +colors = ["colorama (>=0.4.6)"] + [[package]] name = "itsdangerous" version = "2.2.0" description = "Safely pass data to untrusted environments and back." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1705,7 +1816,6 @@ files = [ name = "jinja2" version = "3.1.4" description = "A very fast and expressive template engine." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1723,7 +1833,6 @@ i18n = ["Babel (>=2.7)"] name = "jsonschema" version = "4.17.3" description = "An implementation of JSON Schema validation for Python" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1743,7 +1852,6 @@ format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339- name = "lru-dict" version = "1.2.0" description = "An Dict like LRU container." -category = "main" optional = false python-versions = "*" files = [ @@ -1838,7 +1946,6 @@ test = ["pytest"] name = "mako" version = "1.3.5" description = "A super-fast templating language that borrows the best ideas from the existing templating languages." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -1858,7 +1965,6 @@ testing = ["pytest"] name = "markupsafe" version = "2.1.5" description = "Safely add untrusted strings to HTML/XML markup." -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -1928,7 +2034,6 @@ files = [ name = "mccabe" version = "0.7.0" description = "McCabe checker, plugin for flake8" -category = "dev" optional = false python-versions = ">=3.6" files = [ @@ -1940,7 +2045,6 @@ files = [ name = "multidict" version = "6.0.5" description = "multidict implementation" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -2040,7 +2144,6 @@ files = [ name = "multiproof" version = "0.1.2" description = "A Python library to generate merkle trees and merkle proofs." -category = "main" optional = false python-versions = "^3.10" files = [] @@ -2056,11 +2159,56 @@ url = "https://github.com/stakewise/multiproof.git" reference = "v0.1.2" resolved_reference = "e1f3633a10cb5929cc08d4f261effd170976e7b9" +[[package]] +name = "mypy" +version = "1.11.2" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.11.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d42a6dd818ffce7be66cce644f1dff482f1d97c53ca70908dff0b9ddc120b77a"}, + {file = "mypy-1.11.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:801780c56d1cdb896eacd5619a83e427ce436d86a3bdf9112527f24a66618fef"}, + {file = "mypy-1.11.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:41ea707d036a5307ac674ea172875f40c9d55c5394f888b168033177fce47383"}, + {file = "mypy-1.11.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:6e658bd2d20565ea86da7d91331b0eed6d2eee22dc031579e6297f3e12c758c8"}, + {file = "mypy-1.11.2-cp310-cp310-win_amd64.whl", hash = "sha256:478db5f5036817fe45adb7332d927daa62417159d49783041338921dcf646fc7"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:75746e06d5fa1e91bfd5432448d00d34593b52e7e91a187d981d08d1f33d4385"}, + {file = "mypy-1.11.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a976775ab2256aadc6add633d44f100a2517d2388906ec4f13231fafbb0eccca"}, + {file = "mypy-1.11.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd953f221ac1379050a8a646585a29574488974f79d8082cedef62744f0a0104"}, + {file = "mypy-1.11.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:57555a7715c0a34421013144a33d280e73c08df70f3a18a552938587ce9274f4"}, + {file = "mypy-1.11.2-cp311-cp311-win_amd64.whl", hash = "sha256:36383a4fcbad95f2657642a07ba22ff797de26277158f1cc7bd234821468b1b6"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:e8960dbbbf36906c5c0b7f4fbf2f0c7ffb20f4898e6a879fcf56a41a08b0d318"}, + {file = "mypy-1.11.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:06d26c277962f3fb50e13044674aa10553981ae514288cb7d0a738f495550b36"}, + {file = "mypy-1.11.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6e7184632d89d677973a14d00ae4d03214c8bc301ceefcdaf5c474866814c987"}, + {file = "mypy-1.11.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:3a66169b92452f72117e2da3a576087025449018afc2d8e9bfe5ffab865709ca"}, + {file = "mypy-1.11.2-cp312-cp312-win_amd64.whl", hash = "sha256:969ea3ef09617aff826885a22ece0ddef69d95852cdad2f60c8bb06bf1f71f70"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:37c7fa6121c1cdfcaac97ce3d3b5588e847aa79b580c1e922bb5d5d2902df19b"}, + {file = "mypy-1.11.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:4a8a53bc3ffbd161b5b2a4fff2f0f1e23a33b0168f1c0778ec70e1a3d66deb86"}, + {file = "mypy-1.11.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2ff93107f01968ed834f4256bc1fc4475e2fecf6c661260066a985b52741ddce"}, + {file = "mypy-1.11.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:edb91dded4df17eae4537668b23f0ff6baf3707683734b6a818d5b9d0c0c31a1"}, + {file = "mypy-1.11.2-cp38-cp38-win_amd64.whl", hash = "sha256:ee23de8530d99b6db0573c4ef4bd8f39a2a6f9b60655bf7a1357e585a3486f2b"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:801ca29f43d5acce85f8e999b1e431fb479cb02d0e11deb7d2abb56bdaf24fd6"}, + {file = "mypy-1.11.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:af8d155170fcf87a2afb55b35dc1a0ac21df4431e7d96717621962e4b9192e70"}, + {file = "mypy-1.11.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7821776e5c4286b6a13138cc935e2e9b6fde05e081bdebf5cdb2bb97c9df81d"}, + {file = "mypy-1.11.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:539c570477a96a4e6fb718b8d5c3e0c0eba1f485df13f86d2970c91f0673148d"}, + {file = "mypy-1.11.2-cp39-cp39-win_amd64.whl", hash = "sha256:3f14cd3d386ac4d05c5a39a51b84387403dadbd936e17cb35882134d4f8f0d24"}, + {file = "mypy-1.11.2-py3-none-any.whl", hash = "sha256:b499bc07dbdcd3de92b0a8b29fdf592c111276f6a12fe29c30f6c417dd546d12"}, + {file = "mypy-1.11.2.tar.gz", hash = "sha256:7f9993ad3e0ffdc95c2a14b66dee63729f021968bff8ad911867579c65d13a79"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +typing-extensions = ">=4.6.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + [[package]] name = "mypy-extensions" version = "1.0.0" description = "Type system extensions for programs checked with the mypy type checker." -category = "dev" optional = false python-versions = ">=3.5" files = [ @@ -2072,7 +2220,6 @@ files = [ name = "nodeenv" version = "1.9.1" description = "Node.js virtual environment builder" -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" files = [ @@ -2084,7 +2231,6 @@ files = [ name = "numpy" version = "2.0.0" description = "Fundamental package for array computing in Python" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2139,7 +2285,6 @@ files = [ name = "packaging" version = "24.1" description = "Core utilities for Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2151,7 +2296,6 @@ files = [ name = "pandas" version = "2.2.2" description = "Powerful data structures for data analysis, time series, and statistics" -category = "main" optional = false python-versions = ">=3.9" files = [ @@ -2224,7 +2368,6 @@ xml = ["lxml (>=4.9.2)"] name = "parsimonious" version = "0.9.0" description = "(Soon to be) the fastest pure-Python PEG parser I could muster" -category = "main" optional = false python-versions = "*" files = [ @@ -2238,7 +2381,6 @@ regex = ">=2022.3.15" name = "pathspec" version = "0.12.1" description = "Utility library for gitignore style pattern matching of file paths." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2250,7 +2392,6 @@ files = [ name = "platformdirs" version = "4.2.2" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2267,7 +2408,6 @@ type = ["mypy (>=1.8)"] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2283,7 +2423,6 @@ testing = ["pytest", "pytest-benchmark"] name = "protobuf" version = "5.27.1" description = "" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2304,7 +2443,6 @@ files = [ name = "psycopg2-binary" version = "2.9.9" description = "psycopg2 - Python-PostgreSQL Database Adapter" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2386,7 +2524,6 @@ files = [ name = "pycodestyle" version = "2.11.1" description = "Python style guide checker" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2398,7 +2535,6 @@ files = [ name = "pycryptodome" version = "3.20.0" description = "Cryptographic library for Python" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" files = [ @@ -2440,7 +2576,6 @@ files = [ name = "pydantic" version = "2.7.4" description = "Data validation using Python type hints" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2460,7 +2595,6 @@ email = ["email-validator (>=2.0.0)"] name = "pydantic-core" version = "2.18.4" description = "Core functionality for Pydantic validation and serialization" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2548,11 +2682,30 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pydantic-settings" +version = "2.4.0" +description = "Settings management using Pydantic" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_settings-2.4.0-py3-none-any.whl", hash = "sha256:bb6849dc067f1687574c12a639e231f3a6feeed0a12d710c1382045c5db1c315"}, + {file = "pydantic_settings-2.4.0.tar.gz", hash = "sha256:ed81c3a0f46392b4d7c0a565c05884e6e54b3456e6f0fe4d8814981172dc9a88"}, +] + +[package.dependencies] +pydantic = ">=2.7.0" +python-dotenv = ">=0.21.0" + +[package.extras] +azure-key-vault = ["azure-identity (>=1.16.0)", "azure-keyvault-secrets (>=4.8.0)"] +toml = ["tomli (>=2.0.1)"] +yaml = ["pyyaml (>=6.0.1)"] + [[package]] name = "pyflakes" version = "3.1.0" description = "passive checker of Python programs" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2564,7 +2717,6 @@ files = [ name = "pylookup" version = "0.2.2" description = "PyLookup - Fuzzy-matching table autofill tool" -category = "dev" optional = false python-versions = "*" files = [ @@ -2581,7 +2733,6 @@ rapidfuzz = "*" name = "pyright" version = "1.1.368" description = "Command line wrapper for pyright" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2600,7 +2751,6 @@ dev = ["twine (>=3.4.1)"] name = "pyrsistent" version = "0.20.0" description = "Persistent/Functional/Immutable data structures" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2642,7 +2792,6 @@ files = [ name = "pytest" version = "7.4.4" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2663,7 +2812,6 @@ testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "no name = "pytest-cov" version = "4.1.0" description = "Pytest plugin for measuring coverage." -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -2682,7 +2830,6 @@ testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtuale name = "pytest-mock" version = "3.14.0" description = "Thin-wrapper around the mock package for easier use with pytest" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2700,7 +2847,6 @@ dev = ["pre-commit", "pytest-asyncio", "tox"] name = "python-dateutil" version = "2.9.0.post0" description = "Extensions to the standard Python datetime module" -category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" files = [ @@ -2715,7 +2861,6 @@ six = ">=1.5" name = "python-dotenv" version = "1.0.1" description = "Read key-value pairs from a .env file and set them as environment variables" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -2730,7 +2875,6 @@ cli = ["click (>=5.0)"] name = "python-engineio" version = "4.9.1" description = "Engine.IO server and client for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2748,14 +2892,13 @@ docs = ["sphinx"] [[package]] name = "python-socketio" -version = "5.11.3" +version = "5.11.4" description = "Socket.IO server and client for Python" -category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "python_socketio-5.11.3-py3-none-any.whl", hash = "sha256:2a923a831ff70664b7c502df093c423eb6aa93c1ce68b8319e840227a26d8b69"}, - {file = "python_socketio-5.11.3.tar.gz", hash = "sha256:194af8cdbb7b0768c2e807ba76c7abc288eb5bb85559b7cddee51a6bc7a65737"}, + {file = "python_socketio-5.11.4-py3-none-any.whl", hash = "sha256:42efaa3e3e0b166fc72a527488a13caaac2cefc76174252486503bd496284945"}, + {file = "python_socketio-5.11.4.tar.gz", hash = "sha256:8b0b8ff2964b2957c865835e936310190639c00310a47d77321a594d1665355e"}, ] [package.dependencies] @@ -2771,7 +2914,6 @@ docs = ["sphinx"] name = "pytz" version = "2024.1" description = "World timezone definitions, modern and historical" -category = "main" optional = false python-versions = "*" files = [ @@ -2783,7 +2925,6 @@ files = [ name = "pyunormalize" version = "15.1.0" description = "Unicode normalization forms (NFC, NFKC, NFD, NFKD). A library independent from the Python core Unicode database." -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -2794,7 +2935,6 @@ files = [ name = "pywin32" version = "306" description = "Python for Window Extensions" -category = "main" optional = false python-versions = "*" files = [ @@ -2814,11 +2954,72 @@ files = [ {file = "pywin32-306-cp39-cp39-win_amd64.whl", hash = "sha256:39b61c15272833b5c329a2989999dcae836b1eed650252ab1b7bfbe1d59f30f4"}, ] +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + [[package]] name = "rapidfuzz" version = "3.9.3" description = "rapid fuzzy string matching" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -2920,11 +3121,28 @@ files = [ [package.extras] full = ["numpy"] +[[package]] +name = "redis" +version = "5.0.7" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "regex" version = "2024.5.15" description = "Alternative regular expression module, to replace re." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3013,7 +3231,6 @@ files = [ name = "requests" version = "2.32.3" description = "Python HTTP for Humans." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3035,7 +3252,6 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "requests-toolbelt" version = "1.0.0" description = "A utility belt for advanced users of python-requests" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" files = [ @@ -3050,7 +3266,6 @@ requests = ">=2.0.1,<3.0.0" name = "rlp" version = "4.0.1" description = "rlp: A package for Recursive Length Prefix encoding and decoding" -category = "main" optional = false python-versions = "<4,>=3.8" files = [ @@ -3067,11 +3282,37 @@ docs = ["sphinx (>=6.0.0)", "sphinx-autobuild (>=2021.3.14)", "sphinx-rtd-theme rust-backend = ["rusty-rlp (>=0.2.1)"] test = ["hypothesis (==5.19.0)", "pytest (>=7.0.0)", "pytest-xdist (>=2.4.0)"] +[[package]] +name = "ruff" +version = "0.6.2" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.6.2-py3-none-linux_armv6l.whl", hash = "sha256:5c8cbc6252deb3ea840ad6a20b0f8583caab0c5ef4f9cca21adc5a92b8f79f3c"}, + {file = "ruff-0.6.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:17002fe241e76544448a8e1e6118abecbe8cd10cf68fde635dad480dba594570"}, + {file = "ruff-0.6.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3dbeac76ed13456f8158b8f4fe087bf87882e645c8e8b606dd17b0b66c2c1158"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:094600ee88cda325988d3f54e3588c46de5c18dae09d683ace278b11f9d4d534"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:316d418fe258c036ba05fbf7dfc1f7d3d4096db63431546163b472285668132b"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d72b8b3abf8a2d51b7b9944a41307d2f442558ccb3859bbd87e6ae9be1694a5d"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:2aed7e243be68487aa8982e91c6e260982d00da3f38955873aecd5a9204b1d66"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d371f7fc9cec83497fe7cf5eaf5b76e22a8efce463de5f775a1826197feb9df8"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a8f310d63af08f583363dfb844ba8f9417b558199c58a5999215082036d795a1"}, + {file = "ruff-0.6.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7db6880c53c56addb8638fe444818183385ec85eeada1d48fc5abe045301b2f1"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1175d39faadd9a50718f478d23bfc1d4da5743f1ab56af81a2b6caf0a2394f23"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939f9c86d51635fe486585389f54582f0d65b8238e08c327c1534844b3bb9a"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d0d62ca91219f906caf9b187dea50d17353f15ec9bb15aae4a606cd697b49b4c"}, + {file = "ruff-0.6.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:7438a7288f9d67ed3c8ce4d059e67f7ed65e9fe3aa2ab6f5b4b3610e57e3cb56"}, + {file = "ruff-0.6.2-py3-none-win32.whl", hash = "sha256:279d5f7d86696df5f9549b56b9b6a7f6c72961b619022b5b7999b15db392a4da"}, + {file = "ruff-0.6.2-py3-none-win_amd64.whl", hash = "sha256:d9f3469c7dd43cd22eb1c3fc16926fb8258d50cb1b216658a07be95dd117b0f2"}, + {file = "ruff-0.6.2-py3-none-win_arm64.whl", hash = "sha256:f28fcd2cd0e02bdf739297516d5643a945cc7caf09bd9bcb4d932540a5ea4fa9"}, + {file = "ruff-0.6.2.tar.gz", hash = "sha256:239ee6beb9e91feb8e0ec384204a763f36cb53fb895a1a364618c6abb076b3be"}, +] + [[package]] name = "sentry-sdk" version = "2.6.0" description = "Python client for Sentry (https://sentry.io)" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3125,7 +3366,6 @@ tornado = ["tornado (>=5)"] name = "setuptools" version = "70.1.0" description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" optional = false python-versions = ">=3.8" files = [ @@ -3141,7 +3381,6 @@ testing = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "importlib-metad name = "sexpdata" version = "1.0.2" description = "S-expression parser for Python" -category = "dev" optional = false python-versions = ">=3.7" files = [ @@ -3153,7 +3392,6 @@ files = [ name = "simple-websocket" version = "1.0.0" description = "Simple WebSocket server and client for Python" -category = "main" optional = false python-versions = ">=3.6" files = [ @@ -3171,7 +3409,6 @@ docs = ["sphinx"] name = "six" version = "1.16.0" description = "Python 2 and 3 compatibility utilities" -category = "main" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" files = [ @@ -3183,7 +3420,6 @@ files = [ name = "sniffio" version = "1.3.1" description = "Sniff out which async library your code is running under" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3195,7 +3431,6 @@ files = [ name = "sqlalchemy" version = "2.0.31" description = "Database Abstraction Library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3251,7 +3486,7 @@ files = [ ] [package.dependencies] -greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and platform_machine == \"aarch64\" or python_version < \"3.13\" and platform_machine == \"ppc64le\" or python_version < \"3.13\" and platform_machine == \"x86_64\" or python_version < \"3.13\" and platform_machine == \"amd64\" or python_version < \"3.13\" and platform_machine == \"AMD64\" or python_version < \"3.13\" and platform_machine == \"win32\" or python_version < \"3.13\" and platform_machine == \"WIN32\""} +greenlet = {version = "!=0.4.17", markers = "python_version < \"3.13\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"} typing-extensions = ">=4.6.0" [package.extras] @@ -3279,11 +3514,27 @@ postgresql-psycopgbinary = ["psycopg[binary] (>=3.0.7)"] pymysql = ["pymysql"] sqlcipher = ["sqlcipher3_binary"] +[[package]] +name = "starlette" +version = "0.37.2" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.37.2-py3-none-any.whl", hash = "sha256:6fe59f29268538e5d0d182f2791a479a0c64638e6935d1c6989e63fb2699c6ee"}, + {file = "starlette-0.37.2.tar.gz", hash = "sha256:9af890290133b79fc3db55474ade20f6220a364a0402e0b556e7cd5e1e093823"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + [[package]] name = "toolz" version = "0.12.1" description = "List processing tools and functional utilities" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3295,7 +3546,6 @@ files = [ name = "typing-extensions" version = "4.12.2" description = "Backported and Experimental Type Hints for Python 3.8+" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3307,7 +3557,6 @@ files = [ name = "tzdata" version = "2024.1" description = "Provider of IANA time zone data" -category = "main" optional = false python-versions = ">=2" files = [ @@ -3319,7 +3568,6 @@ files = [ name = "tzlocal" version = "5.2" description = "tzinfo object for the local timezone" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3337,7 +3585,6 @@ devenv = ["check-manifest", "pytest (>=4.3)", "pytest-cov", "pytest-mock (>=3.3) name = "urllib3" version = "2.2.2" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3351,25 +3598,189 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "uvicorn" +version = "0.31.0" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.31.0-py3-none-any.whl", hash = "sha256:cac7be4dd4d891c363cd942160a7b02e69150dcbc7a36be04d5f4af4b17c8ced"}, + {file = "uvicorn-0.31.0.tar.gz", hash = "sha256:13bc21373d103859f68fe739608e2eb054a816dea79189bc3ca08ea89a275906"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "uvloop" +version = "0.20.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:9ebafa0b96c62881d5cafa02d9da2e44c23f9f0cd829f3a32a6aff771449c996"}, + {file = "uvloop-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:35968fc697b0527a06e134999eef859b4034b37aebca537daeb598b9d45a137b"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b16696f10e59d7580979b420eedf6650010a4a9c3bd8113f24a103dfdb770b10"}, + {file = "uvloop-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9b04d96188d365151d1af41fa2d23257b674e7ead68cfd61c725a422764062ae"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:94707205efbe809dfa3a0d09c08bef1352f5d3d6612a506f10a319933757c006"}, + {file = "uvloop-0.20.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:89e8d33bb88d7263f74dc57d69f0063e06b5a5ce50bb9a6b32f5fcbe655f9e73"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:e50289c101495e0d1bb0bfcb4a60adde56e32f4449a67216a1ab2750aa84f037"}, + {file = "uvloop-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:e237f9c1e8a00e7d9ddaa288e535dc337a39bcbf679f290aee9d26df9e72bce9"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:746242cd703dc2b37f9d8b9f173749c15e9a918ddb021575a0205ec29a38d31e"}, + {file = "uvloop-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82edbfd3df39fb3d108fc079ebc461330f7c2e33dbd002d146bf7c445ba6e756"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:80dc1b139516be2077b3e57ce1cb65bfed09149e1d175e0478e7a987863b68f0"}, + {file = "uvloop-0.20.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4f44af67bf39af25db4c1ac27e82e9665717f9c26af2369c404be865c8818dcf"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:4b75f2950ddb6feed85336412b9a0c310a2edbcf4cf931aa5cfe29034829676d"}, + {file = "uvloop-0.20.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:77fbc69c287596880ecec2d4c7a62346bef08b6209749bf6ce8c22bbaca0239e"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6462c95f48e2d8d4c993a2950cd3d31ab061864d1c226bbf0ee2f1a8f36674b9"}, + {file = "uvloop-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:649c33034979273fa71aa25d0fe120ad1777c551d8c4cd2c0c9851d88fcb13ab"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:3a609780e942d43a275a617c0839d85f95c334bad29c4c0918252085113285b5"}, + {file = "uvloop-0.20.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:aea15c78e0d9ad6555ed201344ae36db5c63d428818b4b2a42842b3870127c00"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:f0e94b221295b5e69de57a1bd4aeb0b3a29f61be6e1b478bb8a69a73377db7ba"}, + {file = "uvloop-0.20.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:fee6044b64c965c425b65a4e17719953b96e065c5b7e09b599ff332bb2744bdf"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:265a99a2ff41a0fd56c19c3838b29bf54d1d177964c300dad388b27e84fd7847"}, + {file = "uvloop-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10c2956efcecb981bf9cfb8184d27d5d64b9033f917115a960b83f11bfa0d6b"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:e7d61fe8e8d9335fac1bf8d5d82820b4808dd7a43020c149b63a1ada953d48a6"}, + {file = "uvloop-0.20.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:2beee18efd33fa6fdb0976e18475a4042cd31c7433c866e8a09ab604c7c22ff2"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d8c36fdf3e02cec92aed2d44f63565ad1522a499c654f07935c8f9d04db69e95"}, + {file = "uvloop-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a0fac7be202596c7126146660725157d4813aa29a4cc990fe51346f75ff8fde7"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d0fba61846f294bce41eb44d60d58136090ea2b5b99efd21cbdf4e21927c56a"}, + {file = "uvloop-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95720bae002ac357202e0d866128eb1ac82545bcf0b549b9abe91b5178d9b541"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:36c530d8fa03bfa7085af54a48f2ca16ab74df3ec7108a46ba82fd8b411a2315"}, + {file = "uvloop-0.20.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e97152983442b499d7a71e44f29baa75b3b02e65d9c44ba53b10338e98dedb66"}, + {file = "uvloop-0.20.0.tar.gz", hash = "sha256:4603ca714a754fc8d9b197e325db25b2ea045385e8a3ad05d3463de725fdf469"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "watchfiles" +version = "0.24.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchfiles-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:083dc77dbdeef09fa44bb0f4d1df571d2e12d8a8f985dccde71ac3ac9ac067a0"}, + {file = "watchfiles-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e94e98c7cb94cfa6e071d401ea3342767f28eb5a06a58fafdc0d2a4974f4f35c"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82ae557a8c037c42a6ef26c494d0631cacca040934b101d001100ed93d43f361"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:acbfa31e315a8f14fe33e3542cbcafc55703b8f5dcbb7c1eecd30f141df50db3"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b74fdffce9dfcf2dc296dec8743e5b0332d15df19ae464f0e249aa871fc1c571"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:449f43f49c8ddca87c6b3980c9284cab6bd1f5c9d9a2b00012adaaccd5e7decd"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4abf4ad269856618f82dee296ac66b0cd1d71450fc3c98532d93798e73399b7a"}, + {file = "watchfiles-0.24.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9f895d785eb6164678ff4bb5cc60c5996b3ee6df3edb28dcdeba86a13ea0465e"}, + {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:7ae3e208b31be8ce7f4c2c0034f33406dd24fbce3467f77223d10cd86778471c"}, + {file = "watchfiles-0.24.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:2efec17819b0046dde35d13fb8ac7a3ad877af41ae4640f4109d9154ed30a188"}, + {file = "watchfiles-0.24.0-cp310-none-win32.whl", hash = "sha256:6bdcfa3cd6fdbdd1a068a52820f46a815401cbc2cb187dd006cb076675e7b735"}, + {file = "watchfiles-0.24.0-cp310-none-win_amd64.whl", hash = "sha256:54ca90a9ae6597ae6dc00e7ed0a040ef723f84ec517d3e7ce13e63e4bc82fa04"}, + {file = "watchfiles-0.24.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:bdcd5538e27f188dd3c804b4a8d5f52a7fc7f87e7fd6b374b8e36a4ca03db428"}, + {file = "watchfiles-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2dadf8a8014fde6addfd3c379e6ed1a981c8f0a48292d662e27cabfe4239c83c"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6509ed3f467b79d95fc62a98229f79b1a60d1b93f101e1c61d10c95a46a84f43"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8360f7314a070c30e4c976b183d1d8d1585a4a50c5cb603f431cebcbb4f66327"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:316449aefacf40147a9efaf3bd7c9bdd35aaba9ac5d708bd1eb5763c9a02bef5"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:73bde715f940bea845a95247ea3e5eb17769ba1010efdc938ffcb967c634fa61"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3770e260b18e7f4e576edca4c0a639f704088602e0bc921c5c2e721e3acb8d15"}, + {file = "watchfiles-0.24.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa0fd7248cf533c259e59dc593a60973a73e881162b1a2f73360547132742823"}, + {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d7a2e3b7f5703ffbd500dabdefcbc9eafeff4b9444bbdd5d83d79eedf8428fab"}, + {file = "watchfiles-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d831ee0a50946d24a53821819b2327d5751b0c938b12c0653ea5be7dea9c82ec"}, + {file = "watchfiles-0.24.0-cp311-none-win32.whl", hash = "sha256:49d617df841a63b4445790a254013aea2120357ccacbed00253f9c2b5dc24e2d"}, + {file = "watchfiles-0.24.0-cp311-none-win_amd64.whl", hash = "sha256:d3dcb774e3568477275cc76554b5a565024b8ba3a0322f77c246bc7111c5bb9c"}, + {file = "watchfiles-0.24.0-cp311-none-win_arm64.whl", hash = "sha256:9301c689051a4857d5b10777da23fafb8e8e921bcf3abe6448a058d27fb67633"}, + {file = "watchfiles-0.24.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:7211b463695d1e995ca3feb38b69227e46dbd03947172585ecb0588f19b0d87a"}, + {file = "watchfiles-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b8693502d1967b00f2fb82fc1e744df128ba22f530e15b763c8d82baee15370"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cdab9555053399318b953a1fe1f586e945bc8d635ce9d05e617fd9fe3a4687d6"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:34e19e56d68b0dad5cff62273107cf5d9fbaf9d75c46277aa5d803b3ef8a9e9b"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:41face41f036fee09eba33a5b53a73e9a43d5cb2c53dad8e61fa6c9f91b5a51e"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5148c2f1ea043db13ce9b0c28456e18ecc8f14f41325aa624314095b6aa2e9ea"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7e4bd963a935aaf40b625c2499f3f4f6bbd0c3776f6d3bc7c853d04824ff1c9f"}, + {file = "watchfiles-0.24.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c79d7719d027b7a42817c5d96461a99b6a49979c143839fc37aa5748c322f234"}, + {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:32aa53a9a63b7f01ed32e316e354e81e9da0e6267435c7243bf8ae0f10b428ef"}, + {file = "watchfiles-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ce72dba6a20e39a0c628258b5c308779b8697f7676c254a845715e2a1039b968"}, + {file = "watchfiles-0.24.0-cp312-none-win32.whl", hash = "sha256:d9018153cf57fc302a2a34cb7564870b859ed9a732d16b41a9b5cb2ebed2d444"}, + {file = "watchfiles-0.24.0-cp312-none-win_amd64.whl", hash = "sha256:551ec3ee2a3ac9cbcf48a4ec76e42c2ef938a7e905a35b42a1267fa4b1645896"}, + {file = "watchfiles-0.24.0-cp312-none-win_arm64.whl", hash = "sha256:b52a65e4ea43c6d149c5f8ddb0bef8d4a1e779b77591a458a893eb416624a418"}, + {file = "watchfiles-0.24.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:3d2e3ab79a1771c530233cadfd277fcc762656d50836c77abb2e5e72b88e3a48"}, + {file = "watchfiles-0.24.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:327763da824817b38ad125dcd97595f942d720d32d879f6c4ddf843e3da3fe90"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bd82010f8ab451dabe36054a1622870166a67cf3fce894f68895db6f74bbdc94"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d64ba08db72e5dfd5c33be1e1e687d5e4fcce09219e8aee893a4862034081d4e"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1cf1f6dd7825053f3d98f6d33f6464ebdd9ee95acd74ba2c34e183086900a827"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:43e3e37c15a8b6fe00c1bce2473cfa8eb3484bbeecf3aefbf259227e487a03df"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88bcd4d0fe1d8ff43675360a72def210ebad3f3f72cabfeac08d825d2639b4ab"}, + {file = "watchfiles-0.24.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:999928c6434372fde16c8f27143d3e97201160b48a614071261701615a2a156f"}, + {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:30bbd525c3262fd9f4b1865cb8d88e21161366561cd7c9e1194819e0a33ea86b"}, + {file = "watchfiles-0.24.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:edf71b01dec9f766fb285b73930f95f730bb0943500ba0566ae234b5c1618c18"}, + {file = "watchfiles-0.24.0-cp313-none-win32.whl", hash = "sha256:f4c96283fca3ee09fb044f02156d9570d156698bc3734252175a38f0e8975f07"}, + {file = "watchfiles-0.24.0-cp313-none-win_amd64.whl", hash = "sha256:a974231b4fdd1bb7f62064a0565a6b107d27d21d9acb50c484d2cdba515b9366"}, + {file = "watchfiles-0.24.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ee82c98bed9d97cd2f53bdb035e619309a098ea53ce525833e26b93f673bc318"}, + {file = "watchfiles-0.24.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:fd92bbaa2ecdb7864b7600dcdb6f2f1db6e0346ed425fbd01085be04c63f0b05"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f83df90191d67af5a831da3a33dd7628b02a95450e168785586ed51e6d28943c"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fca9433a45f18b7c779d2bae7beeec4f740d28b788b117a48368d95a3233ed83"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b995bfa6bf01a9e09b884077a6d37070464b529d8682d7691c2d3b540d357a0c"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ed9aba6e01ff6f2e8285e5aa4154e2970068fe0fc0998c4380d0e6278222269b"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e5171ef898299c657685306d8e1478a45e9303ddcd8ac5fed5bd52ad4ae0b69b"}, + {file = "watchfiles-0.24.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4933a508d2f78099162da473841c652ad0de892719043d3f07cc83b33dfd9d91"}, + {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95cf3b95ea665ab03f5a54765fa41abf0529dbaf372c3b83d91ad2cfa695779b"}, + {file = "watchfiles-0.24.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:01def80eb62bd5db99a798d5e1f5f940ca0a05986dcfae21d833af7a46f7ee22"}, + {file = "watchfiles-0.24.0-cp38-none-win32.whl", hash = "sha256:4d28cea3c976499475f5b7a2fec6b3a36208656963c1a856d328aeae056fc5c1"}, + {file = "watchfiles-0.24.0-cp38-none-win_amd64.whl", hash = "sha256:21ab23fdc1208086d99ad3f69c231ba265628014d4aed31d4e8746bd59e88cd1"}, + {file = "watchfiles-0.24.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b665caeeda58625c3946ad7308fbd88a086ee51ccb706307e5b1fa91556ac886"}, + {file = "watchfiles-0.24.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:5c51749f3e4e269231510da426ce4a44beb98db2dce9097225c338f815b05d4f"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:82b2509f08761f29a0fdad35f7e1638b8ab1adfa2666d41b794090361fb8b855"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9a60e2bf9dc6afe7f743e7c9b149d1fdd6dbf35153c78fe3a14ae1a9aee3d98b"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f7d9b87c4c55e3ea8881dfcbf6d61ea6775fffed1fedffaa60bd047d3c08c430"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:78470906a6be5199524641f538bd2c56bb809cd4bf29a566a75051610bc982c3"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:07cdef0c84c03375f4e24642ef8d8178e533596b229d32d2bbd69e5128ede02a"}, + {file = "watchfiles-0.24.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d337193bbf3e45171c8025e291530fb7548a93c45253897cd764a6a71c937ed9"}, + {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:ec39698c45b11d9694a1b635a70946a5bad066b593af863460a8e600f0dff1ca"}, + {file = "watchfiles-0.24.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2e28d91ef48eab0afb939fa446d8ebe77e2f7593f5f463fd2bb2b14132f95b6e"}, + {file = "watchfiles-0.24.0-cp39-none-win32.whl", hash = "sha256:7138eff8baa883aeaa074359daabb8b6c1e73ffe69d5accdc907d62e50b1c0da"}, + {file = "watchfiles-0.24.0-cp39-none-win_amd64.whl", hash = "sha256:b3ef2c69c655db63deb96b3c3e587084612f9b1fa983df5e0c3379d41307467f"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:632676574429bee8c26be8af52af20e0c718cc7f5f67f3fb658c71928ccd4f7f"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a2a9891723a735d3e2540651184be6fd5b96880c08ffe1a98bae5017e65b544b"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4a7fa2bc0efef3e209a8199fd111b8969fe9db9c711acc46636686331eda7dd4"}, + {file = "watchfiles-0.24.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:01550ccf1d0aed6ea375ef259706af76ad009ef5b0203a3a4cce0f6024f9b68a"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:96619302d4374de5e2345b2b622dc481257a99431277662c30f606f3e22f42be"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:85d5f0c7771dcc7a26c7a27145059b6bb0ce06e4e751ed76cdf123d7039b60b5"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:951088d12d339690a92cef2ec5d3cfd957692834c72ffd570ea76a6790222777"}, + {file = "watchfiles-0.24.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49fb58bcaa343fedc6a9e91f90195b20ccb3135447dc9e4e2570c3a39565853e"}, + {file = "watchfiles-0.24.0.tar.gz", hash = "sha256:afb72325b74fa7a428c009c1b8be4b4d7c2afedafb2982827ef2156646df2fe1"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + [[package]] name = "web3" -version = "6.19.0" +version = "6.20.3" description = "web3.py" -category = "main" optional = false python-versions = ">=3.7.2" files = [ - {file = "web3-6.19.0-py3-none-any.whl", hash = "sha256:fb39683d6aa7586ce0ab0be4be392f8acb62c2503958079d61b59f2a0b883718"}, - {file = "web3-6.19.0.tar.gz", hash = "sha256:d27fbd4ac5aa70d0e0c516bd3e3b802fbe74bc159b407c34052d9301b400f757"}, + {file = "web3-6.20.3-py3-none-any.whl", hash = "sha256:529fbb33f2476ce8185f7a2ed7e2e07c4c28621b0e89b845fbfdcaea9571286d"}, + {file = "web3-6.20.3.tar.gz", hash = "sha256:c69dbf1a61ace172741d06990e60afc7f55f303eac087e7235f382df3047d017"}, ] [package.dependencies] aiohttp = ">=3.7.4.post0" +ckzg = "<2" eth-abi = ">=4.0.0" eth-account = ">=0.8.0,<0.13" eth-hash = {version = ">=0.5.1", extras = ["pycryptodome"]} -eth-typing = ">=3.0.0,<4.2.0 || >4.2.0" -eth-utils = ">=2.1.0" +eth-typing = ">=3.0.0,<4.2.0 || >4.2.0,<5.0.0" +eth-utils = ">=2.1.0,<5" hexbytes = ">=0.1.0,<0.4.0" jsonschema = ">=4.0.0" lru-dict = ">=1.1.6,<1.3.0" @@ -3381,16 +3792,15 @@ typing-extensions = ">=4.0.1" websockets = ">=10.0.0" [package.extras] -dev = ["build (>=0.9.0)", "bumpversion", "eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1)", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "importlib-metadata (<5.0)", "ipfshttpclient (==0.8.0a2)", "pre-commit (>=2.21.0)", "py-geth (>=3.14.0)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.21.2,<0.23)", "pytest-mock (>=1.10)", "pytest-watch (>=4.2)", "pytest-xdist (>=1.29)", "setuptools (>=38.6.0)", "sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=3.18.0)", "tqdm (>4.32)", "twine (>=1.13)", "when-changed (>=0.3.0)"] +dev = ["build (>=0.9.0)", "bumpversion", "eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1)", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1)", "flaky (>=3.7.0)", "hypothesis (>=3.31.2)", "importlib-metadata (<5.0)", "ipfshttpclient (==0.8.0a2)", "pre-commit (>=2.21.0)", "py-geth (>=3.14.0,<4)", "pytest (>=7.0.0)", "pytest-asyncio (>=0.21.2,<0.23)", "pytest-mock (>=1.10)", "pytest-watch (>=4.2)", "pytest-xdist (>=1.29)", "setuptools (>=38.6.0)", "sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)", "tox (>=3.18.0)", "tqdm (>4.32)", "twine (>=1.13)", "when-changed (>=0.3.0)"] docs = ["sphinx (>=5.3.0)", "sphinx-rtd-theme (>=1.0.0)", "towncrier (>=21,<22)"] ipfs = ["ipfshttpclient (==0.8.0a2)"] -tester = ["eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1)", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1)", "py-geth (>=3.14.0)"] +tester = ["eth-tester[py-evm] (>=0.11.0b1,<0.12.0b1)", "eth-tester[py-evm] (>=0.9.0b1,<0.10.0b1)", "py-geth (>=3.14.0,<4)"] [[package]] name = "websockets" version = "12.0" description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3472,7 +3882,6 @@ files = [ name = "werkzeug" version = "3.0.3" description = "The comprehensive WSGI web application library." -category = "main" optional = false python-versions = ">=3.8" files = [ @@ -3490,7 +3899,6 @@ watchdog = ["watchdog (>=2.3)"] name = "wsproto" version = "1.2.0" description = "WebSockets state-machine based protocol implementation" -category = "main" optional = false python-versions = ">=3.7.0" files = [ @@ -3505,7 +3913,6 @@ h11 = ">=0.9.0,<1" name = "yarl" version = "1.9.4" description = "Yet another URL library" -category = "main" optional = false python-versions = ">=3.7" files = [ @@ -3608,4 +4015,4 @@ multidict = ">=4.0" [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "cc98480e58adeae7f2c86a6a5ead4e27e1844c026f069cae21d1e19f4d2937b5" +content-hash = "bc7f7d04b03d2aeaafe48b29faba1ac5cce81d9d6ab1869452c170efffd91b47" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index bdf79fe88d..a7d143fed7 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -28,6 +28,15 @@ pydantic = "^2.6.0" pandas = "^2.2.0" gmpy2 = "^2.1.5" sentry-sdk = {extras = ["flask"], version = "^2.5.1"} +redis = "^5.0.7" +fastapi = "^0.112.0" +mypy = "^1.11.2" +isort = "^5.13.2" +pydantic-settings = "^2.4.0" +uvicorn = {extras = ["standard"], version = "^0.31.0"} +asyncpg = "^0.29.0" +uvloop = "^0.20.0" +python-socketio = "^5.11.4" [tool.poetry.group.dev.dependencies] pytest = "^7.3.1" @@ -42,6 +51,10 @@ pyright = "^1.1.366" pylookup = "^0.2.2" importmagic = "^0.1.7" epc = "^0.0.5" +isort = "^5.13.2" +mypy = "^1.11.2" +ruff = "^0.6.2" +aiosqlite = "^0.20.0" [tool.poetry.group.prod] optional = true diff --git a/backend/startup.py b/backend/startup.py index f100cc84d9..9947a2aa48 100644 --- a/backend/startup.py +++ b/backend/startup.py @@ -1,43 +1,86 @@ -# !!! IMPORTANT: DO NOT REARRANGE IMPORTS IN THIS FILE !!! -# The eventlet monkey patch needs to be applied before importing the Flask application for the following reasons: -# 1. Enabling Asynchronous I/O: The monkey patch is required to activate eventlet’s asynchronous and non-blocking I/O capabilities. -# Without this patch, the app's I/O requests might be blocked, which is not desirable for our API's performance. -# 2. Import Order Significance: The monkey patch must be applied before importing the Flask application to ensure that the app utilizes -# the asynchronous versions of standard library modules that have been patched by eventlet. If not done in this order, we might experience issues similar to -# what is reported in the following eventlet issue: https://github.com/eventlet/eventlet/issues/371 -# This comment provides additional insight and helped resolve our specific problem: https://github.com/eventlet/eventlet/issues/371#issuecomment-779967181 -# 3. Issue with dnspython: If dnspython is present in the environment, eventlet monkeypatches socket.getaddrinfo(), -# which breaks dns functionality. By setting the EVENTLET_NO_GREENDNS environment variable before importing eventlet, -# we prevent this monkeypatching - import os +from fastapi import Request +from fastapi.middleware.wsgi import WSGIMiddleware + -os.environ["EVENTLET_NO_GREENDNS"] = "yes" -import eventlet # noqa +from starlette.middleware.base import BaseHTTPMiddleware -eventlet.monkey_patch() +from app import create_app as create_flask_app +from app.extensions import db as flask_db if os.getenv("SENTRY_DSN"): import sentry_sdk + def sentry_before_send(event, hint): + exceptions = event.get("exception", []) + if not exceptions: + return event + + exc = exceptions[-1] + mechanism = exc.get("mechanism", {}) + + if mechanism.get("handled"): + return None + + return event + exceptions = event["exception"] + if exceptions: + exc = exceptions[-1] + mechanism = exc.get("mechanism") + if mechanism: + if mechanism.get("handled"): + return None + + return event + print("[+] Starting sentry") sentry_sdk.init( traces_sample_rate=1.0, profiles_sample_rate=1.0, enable_tracing=True, + before_send=sentry_before_send, ) -from app import create_app # noqa -from app.extensions import db # noqa -app = create_app() +flask_app = create_flask_app() -@app.teardown_request +@flask_app.teardown_request def teardown_session(*args, **kwargs): - db.session.remove() + flask_db.session.remove() + + +# I'm importing it here to make sure that the flask initializes before the fastapi one +from v2.main import app as fastapi_app # noqa + + +# Middleware to check if the path exists in FastAPI +# If it does, proceed with the request +# If it doesn't, modify the request to forward to the Flask app +class PathCheckMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + path = request.url.path + + for route in fastapi_app.routes: + if path == route.path: + # If path exists, proceed with the request + return await call_next(request) + + # If path does not exist, modify the request to forward to the Flask app + if path.startswith("/flask"): + return await call_next(request) + request.scope["path"] = "/flask" + path # Adjust the path as needed + response = await call_next(request) + return response + + +# Setup the pass-through to Flask app +fastapi_app.add_middleware(PathCheckMiddleware) +fastapi_app.mount("/flask", WSGIMiddleware(flask_app)) if __name__ == "__main__": - eventlet.wsgi.server(eventlet.listen(("0.0.0.0", 5000)), app, log=app.logger) + import uvicorn + + uvicorn.run(fastapi_app, host="0.0.0.0", port=5000) diff --git a/backend/tests/api-e2e/conftest.py b/backend/tests/api-e2e/conftest.py new file mode 100644 index 0000000000..21c5da8ba0 --- /dev/null +++ b/backend/tests/api-e2e/conftest.py @@ -0,0 +1,14 @@ +import pytest + +from app.modules.dto import ScoreDelegationPayload +from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS + + +@pytest.fixture() +def payload(): + return ScoreDelegationPayload( + primary_addr=USER1_ADDRESS, + secondary_addr=USER2_ADDRESS, + primary_addr_signature="0x4c7f3b8d06ef3abbe6f5c0762fda01517c62709a3e0bde7ae19a945d3359b0673197db2dabeb20babb9b71c2cbb7e83cfa4cb3078c9bcdc284dcd605ebe89ddc1b", + secondary_addr_signature="0x5e7e86d5acea5cc431b8d148842e21584a7afe16b7de3b5586d20f5de97179f549726baa021dcaf6220ee5116c579df9d40375fa58d3480390289df6a088b9ec1b", + ) diff --git a/backend/tests/api-e2e/test_api_allocations.py b/backend/tests/api-e2e/test_api_allocations.py index 730912bab8..ceba1d6ba0 100644 --- a/backend/tests/api-e2e/test_api_allocations.py +++ b/backend/tests/api-e2e/test_api_allocations.py @@ -1,9 +1,9 @@ import pytest - from flask import current_app as app + from app.legacy.core.projects import get_projects_addresses from tests.conftest import Client, UserAccount -from tests.helpers.constants import STARTING_EPOCH +from tests.helpers.constants import STARTING_EPOCH, LOW_UQ_SCORE @pytest.mark.api @@ -49,24 +49,21 @@ def test_allocations( assert len(unique_proposals) == 3 -@pytest.mark.api -def test_allocations_basics( - client: Client, - deployer: UserAccount, - ua_alice: UserAccount, - ua_bob: UserAccount, - setup_funds, +def _check_allocations_logic( + client: Client, ua_alice: UserAccount, target_pending_epoch: int ): alice_proposals = get_projects_addresses(1)[:3] - # lock GLM from one account - ua_alice.lock(10000) + i = 0 + for i in range(0, target_pending_epoch + 1): + if i > 0: + client.move_to_next_epoch(STARTING_EPOCH + i) - # forward time to the beginning of the epoch 2 - client.move_to_next_epoch(STARTING_EPOCH + 1) + if STARTING_EPOCH + i == target_pending_epoch: + ua_alice.lock(10000) # wait for indexer to catch up - epoch_no = client.wait_for_sync(STARTING_EPOCH + 1) + epoch_no = client.wait_for_sync(STARTING_EPOCH + i) app.logger.debug(f"indexed epoch: {epoch_no}") # make a snapshot @@ -84,7 +81,7 @@ def test_allocations_basics( allocation_response_code == 201 ), "Allocation status code is different than 201" - epoch_allocations, status_code = client.get_epoch_allocations(STARTING_EPOCH) + epoch_allocations, status_code = client.get_epoch_allocations(target_pending_epoch) assert len(epoch_allocations["allocations"]) == len(alice_proposals) for allocation in epoch_allocations["allocations"]: @@ -97,14 +94,14 @@ def test_allocations_basics( # Check user donations user_allocations, status_code = client.get_user_allocations( - STARTING_EPOCH, ua_alice.address + target_pending_epoch, ua_alice.address ) app.logger.debug(f"User allocations: {user_allocations}") assert user_allocations["allocations"], "User allocations for given epoch are empty" assert status_code == 200, "Status code is different than 200" # Check donors - donors, status_code = client.get_donors(STARTING_EPOCH) + donors, status_code = client.get_donors(target_pending_epoch) app.logger.debug(f"Donors: {donors}") for donor in donors["donors"]: assert donor == ua_alice.address, "Donor address is wrong" @@ -113,7 +110,7 @@ def test_allocations_basics( proposal_address = alice_proposals[0] # Check donors of particular proposal proposal_donors, status_code = client.get_proposal_donors( - STARTING_EPOCH, proposal_address + target_pending_epoch, proposal_address ) app.logger.debug(f"Proposal donors: {proposal_donors}") for proposal_donor in proposal_donors: @@ -133,3 +130,31 @@ def test_allocations_basics( assert matched["value"], "Leverage value is empty" assert status_code == 200, "Status code is different than 200" + + +@pytest.mark.api +def test_allocations_basics( + client: Client, + deployer: UserAccount, + ua_alice: UserAccount, + ua_bob: UserAccount, + setup_funds, +): + _check_allocations_logic(client, ua_alice, target_pending_epoch=1) + + +@pytest.mark.api +def test_qf_and_uq_allocations(client: Client, ua_alice: UserAccount): + """ + Test for QF and UQ allocations. + This test checks if we use the QF alongside with UQ functionality properly. + Introduced in E4. + """ + PENDING_EPOCH = STARTING_EPOCH + 3 + + _check_allocations_logic(client, ua_alice, target_pending_epoch=PENDING_EPOCH) + + # Check if UQ is saved in the database after the allocation properly + res, code = client.get_user_uq(ua_alice.address, 4) + assert code == 200 + assert res["uniquenessQuotient"] == str(LOW_UQ_SCORE) diff --git a/backend/tests/api-e2e/test_api_antisybil.py b/backend/tests/api-e2e/test_api_antisybil.py index afbd8a5cec..cc57ee2480 100644 --- a/backend/tests/api-e2e/test_api_antisybil.py +++ b/backend/tests/api-e2e/test_api_antisybil.py @@ -19,7 +19,7 @@ def test_antisybil(client: Client, ua_alice: UserAccount): assert code == 200 # score available assert score["status"] == "Known" assert float(score["score"]) > 0 - assert int(score["expires_at"]) > 0 + assert int(score["expiresAt"]) > 0 # flow for a brand new address, which couldn't be scored by GP yet ua_jane = UserAccount(w3.eth.account.create(), client) @@ -34,4 +34,4 @@ def test_antisybil(client: Client, ua_alice: UserAccount): assert code == 200 # score available assert score["status"] == "Known" assert float(score["score"]) == 0.0 - assert int(score["expires_at"]) > 0 + assert int(score["expiresAt"]) > 0 diff --git a/backend/tests/api-e2e/test_api_delegation.py b/backend/tests/api-e2e/test_api_delegation.py new file mode 100644 index 0000000000..ccb0bc774a --- /dev/null +++ b/backend/tests/api-e2e/test_api_delegation.py @@ -0,0 +1,155 @@ +import pytest +from flask import current_app as app + +from app.modules.dto import ScoreDelegationPayload +from app.infrastructure import database +from tests.conftest import Client +from tests.helpers.constants import STARTING_EPOCH, USER1_ADDRESS, USER2_ADDRESS + + +@pytest.mark.api +def test_delegation(client: Client, payload: ScoreDelegationPayload): + client.move_to_next_epoch(STARTING_EPOCH + 1) + client.move_to_next_epoch(STARTING_EPOCH + 2) + client.move_to_next_epoch(STARTING_EPOCH + 3) + + epoch_no = client.wait_for_sync(STARTING_EPOCH + 3) + app.logger.debug(f"indexed epoch: {epoch_no}") + + database.user.add_user(USER1_ADDRESS) + database.user.add_user(USER2_ADDRESS) + + # refresh scores for users + _, code = client.refresh_antisybil_score(USER1_ADDRESS) + assert code == 204 + + _, code = client.refresh_antisybil_score(USER2_ADDRESS) + assert code == 204 + + # retrieve a delegator's score + delegator_score, code = client.get_antisybil_score(USER2_ADDRESS) + assert code == 200 + assert delegator_score["status"] == "Known" + assert float(delegator_score["score"]) > 0 + assert int(delegator_score["expiresAt"]) > 0 + + # retrieve a delegatee's score + delegatee_score, code = client.get_antisybil_score(USER1_ADDRESS) + assert code == 200 + assert delegator_score["status"] == "Known" + assert float(delegator_score["score"]) > 0 + assert int(delegator_score["expiresAt"]) > 0 + + # check that scores are different before delegation + assert float(delegator_score["score"]) != float(delegatee_score["score"]) + + _, status = client.delegate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + assert status == 201 + + # check that scores are the same after delegation + delegatee_score, code = client.get_antisybil_score(payload.secondary_addr) + assert code == 200 + assert delegatee_score["status"] == "Known" + assert float(delegatee_score["score"]) == float(delegator_score["score"]) + + # check if the secondary address is actually used off + resp, code = client.delegate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + + assert code == 400 + assert resp["message"] == "Delegation already exists" + + +@pytest.mark.api +def test_recalculate_in_delegation(client: Client, payload: ScoreDelegationPayload): + """ + Recalculation can actually return two different results: + - if the delegation does not exist, it will return 400 + - if the delegation exists, i.e. secondary address exists in the database, it will return 400 + it's due to the fact that the recalculation is already stoned for a secondary address in our implementation + """ + client.move_to_next_epoch(STARTING_EPOCH + 1) + client.move_to_next_epoch(STARTING_EPOCH + 2) + client.move_to_next_epoch(STARTING_EPOCH + 3) + + epoch_no = client.wait_for_sync(STARTING_EPOCH + 3) + app.logger.debug(f"indexed epoch: {epoch_no}") + + database.user.add_user(USER1_ADDRESS) + database.user.add_user(USER2_ADDRESS) + + # try to recalculate before delegation + data, status = client.delegation_recalculate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + assert data["message"] == "Delegation does not exists" + assert status == 400 + + # make a delegation + _, status = client.delegate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + assert status == 201 + + # recalculate after delegation + data, status = client.delegation_recalculate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + + assert data["message"] == "Invalid recalculation request" + assert status == 400 + + +@pytest.mark.api +def test_check_delegation(client: Client, payload: ScoreDelegationPayload): + client.move_to_next_epoch(STARTING_EPOCH + 1) + client.move_to_next_epoch(STARTING_EPOCH + 2) + client.move_to_next_epoch(STARTING_EPOCH + 3) + + epoch_no = client.wait_for_sync(STARTING_EPOCH + 3) + app.logger.debug(f"indexed epoch: {epoch_no}") + + database.user.add_user(USER1_ADDRESS) + database.user.add_user(USER2_ADDRESS) + + # check if invalid request is handled correctly + addresses = [payload.primary_addr] * 12 + _, status = client.check_delegation(*addresses) + assert status == 400 + + # check that obfuscated delegation does not exist + _, status = client.check_delegation(payload.primary_addr, payload.secondary_addr) + assert status == 400 + + # conduct a delegation + _, status = client.delegate( + primary_address=payload.primary_addr, + secondary_address=payload.secondary_addr, + primary_address_signature=payload.primary_addr_signature, + secondary_address_signature=payload.secondary_addr_signature, + ) + assert status == 201 + + # check if given addresses are used for delegation + resp, status = client.check_delegation(payload.primary_addr, payload.secondary_addr) + assert status == 200 + assert resp["primary"] == payload.primary_addr + assert resp["secondary"] == payload.secondary_addr diff --git a/backend/tests/api-e2e/test_api_history.py b/backend/tests/api-e2e/test_api_history.py new file mode 100644 index 0000000000..f5e719b2d2 --- /dev/null +++ b/backend/tests/api-e2e/test_api_history.py @@ -0,0 +1,50 @@ +import pytest + +from tests.conftest import Client, UserAccount +from app.legacy.core.projects import get_projects_addresses +from tests.helpers.constants import STARTING_EPOCH +from flask import current_app as app + + +@pytest.mark.api +def test_history_basics( + client: Client, + deployer: UserAccount, + ua_alice: UserAccount, +): + # Check user history before allocation + user_history, status_code = client.get_user_history(ua_alice.address) + assert len(user_history["history"]) == 0, "User history should be empty" + assert status_code == 200 + + # Get alice proposals + alice_proposals = get_projects_addresses(1)[:3] + + # lock GLM for one account + ua_alice.lock(10000) + + # forward time to the beginning of the epoch 2 + client.move_to_next_epoch(STARTING_EPOCH + 1) + + # wait for indexer to catch up + epoch_no = client.wait_for_sync(STARTING_EPOCH + 1) + app.logger.debug(f"indexed epoch: {epoch_no}") + + # make a snapshot + res = client.pending_snapshot() + assert res["epoch"] == STARTING_EPOCH + + allocation_response_code = ua_alice.allocate(1000, alice_proposals) + assert ( + allocation_response_code == 201 + ), "Allocation status code is different than 201" + + # Check user history after allocation + user_history, status_code = client.get_user_history(ua_alice.address) + assert ( + user_history["history"][0]["type"] == "allocation" + ), "Type of history record should be 'allocation'" + assert ( + len(user_history["history"][0]["eventData"]["allocations"]) == 3 + ), "Number of allocations should be 3" + assert status_code == 200 diff --git a/backend/tests/api-e2e/test_api_info.py b/backend/tests/api-e2e/test_api_info.py new file mode 100644 index 0000000000..01bd44fcd2 --- /dev/null +++ b/backend/tests/api-e2e/test_api_info.py @@ -0,0 +1,35 @@ +import pytest + +from tests.conftest import Client +from tests.helpers.constants import STARTING_EPOCH + + +@pytest.mark.api +def test_info_basics( + client: Client, +): + # Check chain_info + chain_info, status_code = client.get_chain_info() + assert "chainName" in chain_info + assert "chainId" in chain_info + assert "smartContracts" in chain_info + assert status_code == 200 + + # Check healthcheck + healthcheck, status_code = client.get_healthcheck() + assert healthcheck["blockchain"] == "UP" + assert healthcheck["db"] == "UP" + assert healthcheck["subgraph"] == "UP" + assert status_code == 200 + + # Check version + version, status_code = client.get_version() + assert "id" in version + assert "env" in version + assert "chain" in version + assert status_code == 200 + + # Check sync_status + sync_status, status_code = client.sync_status() + assert sync_status["blockchainEpoch"] == STARTING_EPOCH + assert sync_status["indexedEpoch"] == STARTING_EPOCH diff --git a/backend/tests/api-e2e/test_api_rewards.py b/backend/tests/api-e2e/test_api_rewards.py new file mode 100644 index 0000000000..58fd8fad8d --- /dev/null +++ b/backend/tests/api-e2e/test_api_rewards.py @@ -0,0 +1,118 @@ +import pytest +import time + +from flask import current_app as app +from app.extensions import vault +from app.legacy.core.projects import get_projects_addresses +from tests.conftest import Client, UserAccount +from tests.helpers.constants import STARTING_EPOCH +from app.legacy.core import vault as vault_core + + +@pytest.mark.api +def test_rewards_basic( + client: Client, + deployer: UserAccount, + ua_alice: UserAccount, + ua_bob: UserAccount, + setup_funds, +): + alice_proposals = get_projects_addresses(1)[:3] + + # lock GLM from two accounts + ua_alice.lock(10000) + ua_bob.lock(15000) + + # forward time to the beginning of the epoch 2 + client.move_to_next_epoch(STARTING_EPOCH + 1) + + # fund the vault (amount here is arbitrary) + vault.fund(deployer._account, 1000 * 10**18) + + # wait for indexer to catch up + epoch_no = client.wait_for_sync(STARTING_EPOCH + 1) + app.logger.debug(f"indexed epoch: {epoch_no}") + + # make a snapshot + res = client.pending_snapshot() + assert res["epoch"] > 0 + + # check if both users have a budget + res = client.get_user_rewards_in_epoch( + address=ua_alice.address, epoch=STARTING_EPOCH + ) + alice_budget = int(res["budget"]) + assert alice_budget > 0 + + res = client.get_user_rewards_in_epoch(address=ua_bob.address, epoch=STARTING_EPOCH) + bob_budget = int(res["budget"]) + assert bob_budget > 0 + + # check if both users budgets are displayed in global budget endpoints + res = client.get_total_users_rewards_in_epoch(epoch=STARTING_EPOCH) + all_user_budgets = res["budgets"] + assert any(budget["amount"] == str(alice_budget) for budget in all_user_budgets) + assert any(budget["amount"] == str(bob_budget) for budget in all_user_budgets) + + ua_alice.allocate(1000, alice_proposals) + + # check estimated projects rewards before finalized snapshot + res = client.get_estimated_projects_rewards() + assert res["rewards"][0]["allocated"] == "1000" + + # TODO replace with helper to wait until end of voting + client.move_to_next_epoch(STARTING_EPOCH + 2) + epoch_no = client.wait_for_sync(STARTING_EPOCH + 2) + app.logger.debug(f"indexed epoch: {epoch_no}") + + # make a finalized snapshot + res = client.finalized_snapshot() + assert res["epoch"] == STARTING_EPOCH + + # get estimated budget by number of epochs + res = client.get_estimated_budget_by_epochs(1, 10000000000000000000000) + one_epoch_budget_estimation = int(res["budget"]) + assert one_epoch_budget_estimation > 0 + + res = client.get_estimated_budget_by_epochs(2, 10000000000000000000000) + two_epochs_budget_estimation = int(res["budget"]) + assert two_epochs_budget_estimation > 0 + assert two_epochs_budget_estimation > one_epoch_budget_estimation + + # get estimated budget by number of days + res = client.get_estimated_budget_by_days(200, 10000000000000000000000) + two_hundreds_days_budget_estimation = int(res["budget"]) + assert two_hundreds_days_budget_estimation > 0 + + res = client.get_estimated_budget_by_days(300, 10000000000000000000000) + three_hundreds_days_budget_estimation = int(res["budget"]) + assert three_hundreds_days_budget_estimation > 0 + assert three_hundreds_days_budget_estimation > two_hundreds_days_budget_estimation + + # write merkle root for withdrawals + vault_core.confirm_withdrawals() + + while not vault.is_merkle_root_set(STARTING_EPOCH): + time.sleep(1) + + # check rewards for all projects are returned in proper schema + res = client.get_projects_with_matched_rewards_in_epoch(epoch=STARTING_EPOCH) + assert len(res[0]["rewards"]) == 3 + for reward in res[0]["rewards"]: + assert "address" in reward + assert "allocated" in reward + assert "matched" in reward + + # check unused rewards + res = client.get_unused_rewards(epoch=STARTING_EPOCH) + assert int(res["value"]) == bob_budget + + # check epoch merkle root exists + res = client.get_rewards_merkle_tree(epoch=STARTING_EPOCH) + assert len(res["leaves"]) == 4 + assert res["leafEncoding"] == ["address", "uint256"] + + # check epoch leverage + res = client.get_rewards_leverage(epoch=STARTING_EPOCH) + epoch_leverage = int(res["leverage"]) + assert epoch_leverage > 0 diff --git a/backend/tests/api-e2e/test_api_snapshot.py b/backend/tests/api-e2e/test_api_snapshot.py index 9fd7a2fc2c..44a5c3cffa 100644 --- a/backend/tests/api-e2e/test_api_snapshot.py +++ b/backend/tests/api-e2e/test_api_snapshot.py @@ -30,11 +30,13 @@ def test_pending_snapshot( assert res["epoch"] > 0 # check if both users have a budget - res = client.get_rewards_budget(address=ua_alice.address, epoch=STARTING_EPOCH) + res = client.get_user_rewards_in_epoch( + address=ua_alice.address, epoch=STARTING_EPOCH + ) alice_budget = int(res["budget"]) assert alice_budget > 0 - res = client.get_rewards_budget(address=ua_bob.address, epoch=STARTING_EPOCH) + res = client.get_user_rewards_in_epoch(address=ua_bob.address, epoch=STARTING_EPOCH) bob_budget = int(res["budget"]) assert bob_budget > 0 diff --git a/backend/tests/api-e2e/test_api_uq.py b/backend/tests/api-e2e/test_api_uq.py new file mode 100644 index 0000000000..fecd24e2a0 --- /dev/null +++ b/backend/tests/api-e2e/test_api_uq.py @@ -0,0 +1,57 @@ +import pytest +from flask import current_app as app + +from app.infrastructure import database +from app.legacy.core.projects import get_projects_addresses +from tests.conftest import UserAccount, Client +from tests.helpers.constants import STARTING_EPOCH, LOW_UQ_SCORE, MAX_UQ_SCORE + + +@pytest.mark.api +def test_uq_for_user(client: Client, ua_alice: UserAccount): + client.move_to_next_epoch(STARTING_EPOCH + 1) + client.move_to_next_epoch(STARTING_EPOCH + 2) + client.move_to_next_epoch(STARTING_EPOCH + 3) + + epoch_no = client.wait_for_sync(STARTING_EPOCH + 3) + app.logger.debug(f"indexed epoch: {epoch_no}") + + USER_NOT_FOUND = 404 + _, code = client.get_user_uq(ua_alice.address, 4) + assert code == USER_NOT_FOUND + + database.user.add_user(ua_alice.address) + + res, code = client.get_user_uq(ua_alice.address, 4) + assert code == 200 + assert res["uniquenessQuotient"] in [str(LOW_UQ_SCORE), str(MAX_UQ_SCORE)] + + +@pytest.mark.api +def test_uq_for_all_users(client: Client, ua_alice: UserAccount, ua_bob, setup_funds): + client.move_to_next_epoch(STARTING_EPOCH + 1) + client.move_to_next_epoch(STARTING_EPOCH + 2) + client.move_to_next_epoch(STARTING_EPOCH + 3) + ua_alice.lock(10000) + ua_bob.lock(10000) + client.move_to_next_epoch(STARTING_EPOCH + 4) + + epoch_no = client.wait_for_sync(STARTING_EPOCH + 4) + app.logger.debug(f"indexed epoch: {epoch_no}") + + res = client.pending_snapshot() + assert res["epoch"] > 0 + + # make an allocation during AW since it saves the uq to the database + alice_bob_proposals = get_projects_addresses(4)[:3] + + ua_alice.allocate(1000, alice_bob_proposals) + ua_bob.allocate(1000, alice_bob_proposals) + + res, code = client.get_all_uqs(4) + assert code == 200 + assert type(res["uqsInfo"]) is list + assert len(res["uqsInfo"]) == 2 + + assert res["uqsInfo"][0]["userAddress"] == ua_alice.address + assert res["uqsInfo"][1]["userAddress"] == ua_bob.address diff --git a/backend/tests/api-e2e/test_api_withdrawals.py b/backend/tests/api-e2e/test_api_withdrawals.py index 352f10ca3f..2bbc290f29 100644 --- a/backend/tests/api-e2e/test_api_withdrawals.py +++ b/backend/tests/api-e2e/test_api_withdrawals.py @@ -43,21 +43,25 @@ def test_withdrawals( assert res["epoch"] > 0 # save account budget for assertion - res = client.get_rewards_budget(address=ua_alice.address, epoch=STARTING_EPOCH) + res = client.get_user_rewards_in_epoch( + address=ua_alice.address, epoch=STARTING_EPOCH + ) alice_budget = int(res["budget"]) # make empty vote to get personal rewards ua_alice.allocate(0, alice_proposals) # save account budget for assertion - res = client.get_rewards_budget(address=ua_bob.address, epoch=STARTING_EPOCH) + res = client.get_user_rewards_in_epoch(address=ua_bob.address, epoch=STARTING_EPOCH) bob_budget = int(res["budget"]) # make empty vote to get personal rewards ua_bob.allocate(0, bob_proposals) # save account budget for assertion - res = client.get_rewards_budget(address=ua_carol.address, epoch=STARTING_EPOCH) + res = client.get_user_rewards_in_epoch( + address=ua_carol.address, epoch=STARTING_EPOCH + ) carol_budget = int(res["budget"]) # make empty vote to get personal rewards diff --git a/backend/tests/conftest.py b/backend/tests/conftest.py index 36febcaa09..285bd962ab 100644 --- a/backend/tests/conftest.py +++ b/backend/tests/conftest.py @@ -1,29 +1,30 @@ from __future__ import annotations import datetime -from http import HTTPStatus import json -import logging import os import time +import urllib.error import urllib.request +from http import HTTPStatus from unittest.mock import MagicMock, Mock import gql -from gql.transport.exceptions import TransportQueryError import pytest from flask import current_app from flask import g as request_context from flask.testing import FlaskClient +from gql.transport.exceptions import TransportQueryError from requests import RequestException from web3 import Web3 +import logging from app import create_app from app.engine.user.effective_deposit import DepositEvent, EventType, UserDeposit from app.exceptions import ExternalApiException from app.extensions import db, deposits, glm, gql_factory, w3, vault, epochs -from app.infrastructure import database from app.infrastructure import Client as GQLClient +from app.infrastructure import database from app.infrastructure.contracts.epochs import Epochs from app.infrastructure.contracts.erc20 import ERC20 from app.infrastructure.contracts.projects import Projects @@ -122,6 +123,13 @@ def mock_gitcoin_passport_issue_address_for_scoring(*args, **kwargs): "score": "22.0", "status": "DONE", } + # GTC staker, Carol + elif args[0] == "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": + return { + "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "score": str(4 + 0.5), + "status": "DONE", + } else: return {"status": "DONE", "score": "0.0"} @@ -153,225 +161,42 @@ def mock_gitcoin_passport_fetch_score(*args, **kwargs): "score": "22.0", "status": "DONE", } + # GTC staker, Carol + elif args[0] == "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": + return { + "address": "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC", + "score": str(4 + 0.5), + "status": "DONE", + } else: return {"status": "DONE", "score": "0.0"} def mock_gitcoin_passport_fetch_stamps(*args, **kwargs): + "Returns structure resembling GP stamps, but only with relevant fields" if args[0] == "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266": return { - "next": None, - "prev": None, "items": [ { - "version": "1.0.0", "credential": { - "type": ["VerifiableCredential"], - "proof": { - "type": "EthereumEip712Signature2021", - "created": "2024-03-12T14:28:53.877Z", - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - "proofValue": "0x5ef4c6d9ff1116c66d45c5bc65cf83ed1220b6faa4b6f78d8f057bb88470be8d4622f7bc8846accdd1057413c42408dbcce5cd4e55362fc6ac581b2f9536ec2c1b", - "eip712Domain": { - "types": { - "Proof": [ - {"name": "@context", "type": "string"}, - {"name": "created", "type": "string"}, - {"name": "proofPurpose", "type": "string"}, - {"name": "type", "type": "string"}, - { - "name": "verificationMethod", - "type": "string", - }, - ], - "@context": [ - {"name": "hash", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - "Document": [ - {"name": "@context", "type": "string[]"}, - { - "name": "credentialSubject", - "type": "CredentialSubject", - }, - {"name": "expirationDate", "type": "string"}, - {"name": "issuanceDate", "type": "string"}, - {"name": "issuer", "type": "string"}, - {"name": "proof", "type": "Proof"}, - {"name": "type", "type": "string[]"}, - ], - "EIP712Domain": [ - {"name": "name", "type": "string"} - ], - "CredentialSubject": [ - {"name": "@context", "type": "@context"}, - {"name": "hash", "type": "string"}, - {"name": "id", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - }, - "domain": {"name": "VerifiableCredential"}, - "primaryType": "Document", - }, - "proofPurpose": "assertionMethod", - "verificationMethod": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb#controller", - }, - "issuer": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb", - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ], - "issuanceDate": "2024-03-12T14:28:53.876Z", "expirationDate": "2090-01-01T00:00:00.000Z", "credentialSubject": { - "id": "did:pkh:eip155:1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8", - "hash": "v0.0.0:pQzBR3arZrlQXpJ6KRGxKEjhR03DyQ05ois9EmRNrAQ=", - "@context": { - "hash": "https://schema.org/Text", - "provider": "https://schema.org/Text", - }, "provider": "Linkedin", }, }, }, { - "version": "1.0.0", "credential": { - "type": ["VerifiableCredential"], - "proof": { - "type": "EthereumEip712Signature2021", - "created": "2024-03-12T14:24:07.018Z", - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - "proofValue": "0x2547250aca7112a8488eb45a62dfabc8f5f6e4ecc1bf24f8e28839ce1ff7e786496cf5eb5ffb9eaa27bbcf58ecd66bc966d20844b7b5a7666d4fbbc38f609b641c", - "eip712Domain": { - "types": { - "Proof": [ - {"name": "@context", "type": "string"}, - {"name": "created", "type": "string"}, - {"name": "proofPurpose", "type": "string"}, - {"name": "type", "type": "string"}, - { - "name": "verificationMethod", - "type": "string", - }, - ], - "@context": [ - {"name": "hash", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - "Document": [ - {"name": "@context", "type": "string[]"}, - { - "name": "credentialSubject", - "type": "CredentialSubject", - }, - {"name": "expirationDate", "type": "string"}, - {"name": "issuanceDate", "type": "string"}, - {"name": "issuer", "type": "string"}, - {"name": "proof", "type": "Proof"}, - {"name": "type", "type": "string[]"}, - ], - "EIP712Domain": [ - {"name": "name", "type": "string"} - ], - "CredentialSubject": [ - {"name": "@context", "type": "@context"}, - {"name": "hash", "type": "string"}, - {"name": "id", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - }, - "domain": {"name": "VerifiableCredential"}, - "primaryType": "Document", - }, - "proofPurpose": "assertionMethod", - "verificationMethod": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb#controller", - }, - "issuer": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb", - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ], - "issuanceDate": "2024-03-12T14:24:07.018Z", "expirationDate": "2099-01-01T00:00:00.000Z", "credentialSubject": { - "id": "did:pkh:eip155:1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8", - "hash": "v0.0.0:PM/AuRacZWQ3McP8Dr6Ux+yb8PcVjeS7rlVcc6ry/2Q=", - "@context": { - "hash": "https://schema.org/Text", - "provider": "https://schema.org/Text", - }, "provider": "Discord", }, }, }, { - "version": "1.0.0", "credential": { - "type": ["VerifiableCredential"], - "proof": { - "type": "EthereumEip712Signature2021", - "created": "2024-03-12T14:24:07.018Z", - "@context": "https://w3id.org/security/suites/eip712sig-2021/v1", - "proofValue": "0x2547250aca7112a8488eb45a62dfabc8f5f6e4ecc1bf24f8e28839ce1ff7e786496cf5eb5ffb9eaa27bbcf58ecd66bc966d20844b7b5a7666d4fbbc38f609b641c", - "eip712Domain": { - "types": { - "Proof": [ - {"name": "@context", "type": "string"}, - {"name": "created", "type": "string"}, - {"name": "proofPurpose", "type": "string"}, - {"name": "type", "type": "string"}, - { - "name": "verificationMethod", - "type": "string", - }, - ], - "@context": [ - {"name": "hash", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - "Document": [ - {"name": "@context", "type": "string[]"}, - { - "name": "credentialSubject", - "type": "CredentialSubject", - }, - {"name": "expirationDate", "type": "string"}, - {"name": "issuanceDate", "type": "string"}, - {"name": "issuer", "type": "string"}, - {"name": "proof", "type": "Proof"}, - {"name": "type", "type": "string[]"}, - ], - "EIP712Domain": [ - {"name": "name", "type": "string"} - ], - "CredentialSubject": [ - {"name": "@context", "type": "@context"}, - {"name": "hash", "type": "string"}, - {"name": "id", "type": "string"}, - {"name": "provider", "type": "string"}, - ], - }, - "domain": {"name": "VerifiableCredential"}, - "primaryType": "Document", - }, - "proofPurpose": "assertionMethod", - "verificationMethod": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb#controller", - }, - "issuer": "did:ethr:0xd6f8d6ca86aa01e551a311d670a0d1bd8577e5fb", - "@context": [ - "https://www.w3.org/2018/credentials/v1", - "https://w3id.org/vc/status-list/2021/v1", - ], - "issuanceDate": "2024-03-12T14:24:07.018Z", "expirationDate": "2024-06-10T14:24:07.018Z", "credentialSubject": { - "id": "did:pkh:eip155:1:0x70997970C51812dc3A010C7d01b50e0d17dc79C8", - "hash": "v0.0.0:PM/AuRacZWQ3McP8Dr6Ux+yb8PcVjeS7rlVcc6ry/2Q=", - "@context": { - "hash": "https://schema.org/Text", - "provider": "https://schema.org/Text", - }, "provider": "Discord", }, }, @@ -383,7 +208,6 @@ def mock_gitcoin_passport_fetch_stamps(*args, **kwargs): return { "items": [ { - "version": "1.0.0", "credential": { "expirationDate": "2099-09-22T15:04:05.073Z", "credentialSubject": {"provider": "AllowList#OctantEpochTwo"}, @@ -391,6 +215,24 @@ def mock_gitcoin_passport_fetch_stamps(*args, **kwargs): } ] } + # GTC staker, Carol + elif args[0] == "0x3C44CdDdB6a900fa2b585dd299e03d12FA4293BC": + return { + "items": [ + { + "credential": { + "expirationDate": "2099-09-22T15:04:05.073Z", + "credentialSubject": {"provider": "Discord"}, + } + }, + { + "credential": { + "expirationDate": "2099-09-22T15:04:05.073Z", + "credentialSubject": {"provider": "TrustedCitizen"}, + }, + }, + ] + } else: return {"next": None, "prev": None, "items": []} @@ -703,7 +545,6 @@ def wait_for_sync(self, target, timeout_s=20, check_interval=0.5): timeout = datetime.timedelta(seconds=timeout_s) start = datetime.datetime.now() while True: - res = {} try: res, status_code = self.sync_status() current_app.logger.debug(f"sync_status returns {res}") @@ -804,6 +645,64 @@ def get_rewards_budget(self, address: str, epoch: int): rv = self._flask_client.get(f"/rewards/budget/{address}/epoch/{epoch}") return json.loads(rv.text) + def get_user_rewards_in_upcoming_epoch(self, address: str): + rv = self._flask_client.get(f"/rewards/budget/{address}/upcoming") + current_app.logger.debug( + "get_user_rewards_in_upcoming_epoch :", + self._flask_client.get(f"/rewards/budget/{address}/upcoming").request, + ) + return json.loads(rv.text) + + def get_user_rewards_in_epoch(self, address: str, epoch: int): + rv = self._flask_client.get(f"/rewards/budget/{address}/epoch/{epoch}") + current_app.logger.debug( + "get_rewards_budget :", + self._flask_client.get(f"/rewards/budget/{address}/epoch/{epoch}").request, + ) + return json.loads(rv.text) + + def get_total_users_rewards_in_epoch(self, epoch): + rv = self._flask_client.get(f"/rewards/budgets/epoch/{epoch}") + return json.loads(rv.text) + + def get_estimated_budget_by_days(self, number_of_days, amount): + rv = self._flask_client.post( + "/rewards/estimated_budget/by_days", + json={"days": number_of_days, "glmAmount": amount}, + ) + return json.loads(rv.text) + + def get_estimated_budget_by_epochs(self, number_of_epochs, amount): + rv = self._flask_client.post( + "/rewards/estimated_budget", + json={"numberOfEpochs": number_of_epochs, "glmAmount": amount}, + ) + return json.loads(rv.text) + + def get_rewards_leverage(self, epoch): + rv = self._flask_client.get(f"/rewards/leverage/{epoch}") + return json.loads(rv.text) + + def get_rewards_merkle_tree(self, epoch): + rv = self._flask_client.get(f"/rewards/merkle_tree/{epoch}") + return json.loads(rv.text) + + def get_projects_with_matched_rewards_in_epoch(self, epoch): + rv = self._flask_client.get(f"/rewards/projects/epoch/{epoch}") + return json.loads(rv.text), rv.status_code + + def get_estimated_projects_rewards(self): + rv = self._flask_client.get("/rewards/projects/estimated") + return json.loads(rv.text) + + def get_proposals_treshold_in_epoch(self, epoch) -> tuple[dict, int]: + rv = self._flask_client.get(f"/rewards/threshold/{epoch}") + return json.loads(rv.text), rv.status_code + + def get_unused_rewards(self, epoch): + rv = self._flask_client.get(f"/rewards/unused/{epoch}") + return json.loads(rv.text) + def get_withdrawals_for_address(self, address: str): rv = self._flask_client.get(f"/withdrawals/{address}").text return json.loads(rv) @@ -901,6 +800,18 @@ def accept_tos(self, user_address, signature): ) return json.loads(rv.text), rv.status_code + def get_user_history(self, user_address: str) -> tuple[dict, int]: + rv = self._flask_client.get(f"/history/{user_address}") + return json.loads(rv.text), rv.status_code + + def get_user_uq(self, user_address: str, epoch: int) -> tuple[dict, int]: + rv = self._flask_client.get(f"/user/{user_address}/uq/{epoch}") + return json.loads(rv.text), rv.status_code + + def get_all_uqs(self, epoch: int) -> tuple[dict, int]: + rv = self._flask_client.get(f"user/uq/{epoch}/all") + return json.loads(rv.text), rv.status_code + def get_antisybil_score(self, user_address: str) -> (any, int): rv = self._flask_client.get(f"/user/{user_address}/antisybil-status") return json.loads(rv.text), rv.status_code @@ -909,6 +820,59 @@ def refresh_antisybil_score(self, user_address: str) -> (str | None, int): rv = self._flask_client.put(f"/user/{user_address}/antisybil-status") return rv.text, rv.status_code + def get_chain_info(self) -> tuple[dict, int]: + rv = self._flask_client.get("/info/chain-info") + return json.loads(rv.text), rv.status_code + + def get_version(self) -> tuple[dict, int]: + rv = self._flask_client.get("/info/version") + return json.loads(rv.text), rv.status_code + + def get_healthcheck(self) -> tuple[dict, int]: + rv = self._flask_client.get("/info/healthcheck") + return json.loads(rv.text), rv.status_code + + def check_delegation(self, *addresses) -> tuple[dict, int]: + addresses = ",".join(addresses) + rv = self._flask_client.get(f"/delegation/check/{addresses}") + return json.loads(rv.text), rv.status_code + + def delegate( + self, + primary_address: str, + secondary_address: str, + primary_address_signature: str, + secondary_address_signature: str, + ) -> tuple[dict, int]: + rv = self._flask_client.post( + "/delegation/delegate", + json={ + "primaryAddr": primary_address, + "secondaryAddr": secondary_address, + "primaryAddrSignature": primary_address_signature, + "secondaryAddrSignature": secondary_address_signature, + }, + ) + return json.loads(rv.text), rv.status_code + + def delegation_recalculate( + self, + primary_address: str, + secondary_address: str, + primary_address_signature: str, + secondary_address_signature: str, + ) -> tuple[dict, int]: + rv = self._flask_client.put( + "/delegation/recalculate", + json={ + "primaryAddr": primary_address, + "secondaryAddr": secondary_address, + "primaryAddrSignature": primary_address_signature, + "secondaryAddrSignature": secondary_address_signature, + }, + ) + return json.loads(rv.text), rv.status_code + @property def config(self): return self._flask_client.application.config @@ -1249,10 +1213,14 @@ def mock_users_db(app, user_accounts): @pytest.fixture(scope="function") def mock_pending_epoch_snapshot_db_since_epoch3( - app, mock_users_db, ppf=PPF, cf=COMMUNITY_FUND + app, + mock_users_db, + ppf=PPF, + cf=COMMUNITY_FUND, + epoch=MOCKED_EPOCH_NO_AFTER_OVERHAUL, ): create_pending_snapshot( - epoch_nr=MOCKED_EPOCH_NO_AFTER_OVERHAUL, + epoch_nr=epoch, mock_users_db=mock_users_db, optional_ppf=ppf, optional_cf=cf, @@ -1280,22 +1248,9 @@ def mock_finalized_epoch_snapshot_db_since_epoch3(app, user_accounts): @pytest.fixture(scope="function") -def mock_finalized_epoch_snapshot_db(app, user_accounts): - database.finalized_epoch_snapshot.save_snapshot( - MOCKED_FINALIZED_EPOCH_NO, - MATCHED_REWARDS, - NO_PATRONS_REWARDS, - LEFTOVER, - total_withdrawals=TOTAL_WITHDRAWALS, - ) - - db.session.commit() - - -@pytest.fixture(scope="function") -def mock_allocations_db(app, mock_users_db, project_accounts): - prev_epoch_context = get_context(MOCKED_PENDING_EPOCH_NO - 1) - pending_epoch_context = get_context(MOCKED_PENDING_EPOCH_NO) +def mock_allocations_db(mock_users_db, project_accounts, epoch=MOCKED_PENDING_EPOCH_NO): + prev_epoch_context = get_context(epoch - 1) + pending_epoch_context = get_context(epoch) user1, user2, _ = mock_users_db user1_allocations = [ diff --git a/backend/tests/helpers/constants.py b/backend/tests/helpers/constants.py index b16ea4dfd9..d38f05ab40 100644 --- a/backend/tests/helpers/constants.py +++ b/backend/tests/helpers/constants.py @@ -7,6 +7,7 @@ MOCKED_PENDING_EPOCH_NO = 1 MOCKED_FINALIZED_EPOCH_NO = 1 MOCKED_EPOCH_NO_AFTER_OVERHAUL = 3 +MOCKED_EPOCH_NO_WITH_CAPPED_MR = 4 MOCKED_CURRENT_EPOCH_NO = 2 NO_PATRONS_REWARDS = 0 ETH_PROCEEDS = 402_410958904_110000000 @@ -31,6 +32,13 @@ ETH_PROCEEDS, LOCKED_RATIO, USER2_BUDGET ) COMMUNITY_FUND = int(Decimal("0.05") * ETH_PROCEEDS) +LEFTOVER_WITH_PPF_UNUSED_MR = ( + ETH_PROCEEDS + - OPERATIONAL_COST + - COMMUNITY_FUND + - int(0.5 * PPF) + - TOTAL_WITHDRAWALS +) USER1_ADDRESS = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" USER2_ADDRESS = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" @@ -58,8 +66,11 @@ MULTISIG_ADDRESS = "0xa40FcB633d0A6c0d27aA9367047635Ff656229B0" MR_FUNDING_CAP_PERCENT = Decimal("0.2") -MAX_UQ_SCORE = Decimal("1") -LOW_UQ_SCORE = Decimal("0.2") +MAX_UQ_SCORE = Decimal("1.0") +LOW_UQ_SCORE = Decimal("0.01") +NULLIFIED_UQ_SCORE = Decimal("0.0") UQ_THRESHOLD_NOT_MAINNET = 5 -UQ_THRESHOLD_MAINNET = 20 +UQ_THRESHOLD_MAINNET = 15 + +TIMEOUT_LIST = set() diff --git a/backend/tests/helpers/finalized_snapshots.py b/backend/tests/helpers/finalized_snapshots.py new file mode 100644 index 0000000000..aee99bc3e3 --- /dev/null +++ b/backend/tests/helpers/finalized_snapshots.py @@ -0,0 +1,18 @@ +from app.extensions import db +from app.infrastructure import database +from tests.helpers.constants import ( + NO_PATRONS_REWARDS, + TOTAL_WITHDRAWALS, +) + + +def create_finalized_snapshot(epoch_nr: int, matched_rewards: int, leftover: int): + database.finalized_epoch_snapshot.save_snapshot( + epoch_nr, + matched_rewards, + NO_PATRONS_REWARDS, + leftover, + total_withdrawals=TOTAL_WITHDRAWALS, + ) + + db.session.commit() diff --git a/backend/tests/modules/modules_factory/test_modules_factory.py b/backend/tests/modules/modules_factory/test_modules_factory.py index faf3452858..282133fd8d 100644 --- a/backend/tests/modules/modules_factory/test_modules_factory.py +++ b/backend/tests/modules/modules_factory/test_modules_factory.py @@ -52,7 +52,7 @@ from app.shared.blockchain_types import ChainTypes from app.modules.user.budgets.service.upcoming import UpcomingUserBudgets from app.modules.snapshots.pending.service.simulated import SimulatedPendingSnapshots -from tests.helpers.constants import UQ_THRESHOLD_MAINNET +from app.constants import UQ_THRESHOLD_MAINNET, TIMEOUT_LIST def test_future_services_factory(): @@ -141,7 +141,7 @@ def test_pending_services_factory(): saved_user_budgets = SavedUserBudgets() user_nonce = SavedUserAllocationsNonce() uniqueness_quotients = PreliminaryUQ( - antisybil=GitcoinPassportAntisybil(), + antisybil=GitcoinPassportAntisybil(timeout_list=TIMEOUT_LIST), budgets=saved_user_budgets, uq_threshold=UQ_THRESHOLD_MAINNET, ) diff --git a/backend/tests/modules/octant_rewards/conftest.py b/backend/tests/modules/octant_rewards/conftest.py new file mode 100644 index 0000000000..8895ce9fe2 --- /dev/null +++ b/backend/tests/modules/octant_rewards/conftest.py @@ -0,0 +1,42 @@ +import pytest + +from tests.helpers.constants import ( + MOCKED_EPOCH_NO_WITH_CAPPED_MR, + COMMUNITY_FUND, + PPF, + MOCKED_FINALIZED_EPOCH_NO, + MATCHED_REWARDS_AFTER_OVERHAUL, + MATCHED_REWARDS, + LEFTOVER, + LEFTOVER_WITH_PPF_UNUSED_MR, +) +from tests.helpers.finalized_snapshots import create_finalized_snapshot +from tests.helpers.pending_snapshot import create_pending_snapshot + + +@pytest.fixture(scope="function") +def mock_pending_epoch_snapshot_with_uq_scores( + mock_users_db_with_scores, ppf=PPF, cf=COMMUNITY_FUND +): + user1_, user2, user3 = mock_users_db_with_scores + create_pending_snapshot( + epoch_nr=MOCKED_EPOCH_NO_WITH_CAPPED_MR, + optional_ppf=ppf, + optional_cf=cf, + mock_users_db=mock_users_db_with_scores, + ) + return user1_, user2, user3 + + +@pytest.fixture(scope="function") +def mock_finalized_epoch_snapshot_db(app, user_accounts): + create_finalized_snapshot(MOCKED_FINALIZED_EPOCH_NO, MATCHED_REWARDS, LEFTOVER) + + +@pytest.fixture(scope="function") +def mock_finalized_epoch_snapshot_db_for_e4(app, user_accounts): + create_finalized_snapshot( + MOCKED_EPOCH_NO_WITH_CAPPED_MR, + MATCHED_REWARDS_AFTER_OVERHAUL, + LEFTOVER_WITH_PPF_UNUSED_MR, + ) diff --git a/backend/tests/modules/octant_rewards/helpers/checker.py b/backend/tests/modules/octant_rewards/helpers/checker.py index 28450dea6b..d090fbec5e 100644 --- a/backend/tests/modules/octant_rewards/helpers/checker.py +++ b/backend/tests/modules/octant_rewards/helpers/checker.py @@ -16,6 +16,7 @@ def check_octant_rewards( matched_rewards: int = None, total_withdrawals: int = None, patrons_rewards: int = None, + donated_to_projects: int = None, ): assert rewards.staking_proceeds == ETH_PROCEEDS assert rewards.locked_ratio == LOCKED_RATIO @@ -29,3 +30,4 @@ def check_octant_rewards( assert rewards.leftover == leftover assert rewards.community_fund == community_fund assert rewards.ppf == ppf + assert rewards.donated_to_projects == donated_to_projects diff --git a/backend/tests/modules/octant_rewards/test_apr_core.py b/backend/tests/modules/octant_rewards/test_apr_core.py new file mode 100644 index 0000000000..4b27c5b0c1 --- /dev/null +++ b/backend/tests/modules/octant_rewards/test_apr_core.py @@ -0,0 +1,17 @@ +import pytest + +from app.modules.octant_rewards.core import get_rewards_rate +from app.modules.staking.proceeds.core import ESTIMATED_STAKING_REWARDS_RATE + + +@pytest.fixture(autouse=True) +def before(app): + pass + + +@pytest.mark.parametrize("epoch_num", [1, 2, 3, 4, 5]) +def test_get_epoch_rewards_rate_return_valid_value(epoch_num: int): + actual_result = get_rewards_rate(epoch_num) + expected_result = ESTIMATED_STAKING_REWARDS_RATE + + assert actual_result == expected_result diff --git a/backend/tests/modules/octant_rewards/test_calculated_octant_rewards.py b/backend/tests/modules/octant_rewards/test_calculated_octant_rewards.py index 0a174fbdcc..69200be707 100644 --- a/backend/tests/modules/octant_rewards/test_calculated_octant_rewards.py +++ b/backend/tests/modules/octant_rewards/test_calculated_octant_rewards.py @@ -41,6 +41,7 @@ def test_calculate_octant_rewards_before_overhaul( assert result.total_rewards == expected_tr assert result.vanilla_individual_rewards == expected_ir assert result.operational_cost == expected_operational_cost + assert result.donated_to_projects is None def test_calculate_octant_rewards_after_overhaul( @@ -69,3 +70,4 @@ def test_calculate_octant_rewards_after_overhaul( assert result.ppf == overhaul_formulas.ppf( result.staking_proceeds, result.vanilla_individual_rewards, LOCKED_RATIO ) + assert result.donated_to_projects is None diff --git a/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py b/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py index c1b48079f2..2b4a94690b 100644 --- a/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py +++ b/backend/tests/modules/octant_rewards/test_finalized_octant_rewards.py @@ -31,6 +31,7 @@ def test_finalized_octant_rewards_before_overhaul( matched_rewards=MATCHED_REWARDS, total_withdrawals=TOTAL_WITHDRAWALS, patrons_rewards=NO_PATRONS_REWARDS, + donated_to_projects=MATCHED_REWARDS, ) @@ -51,6 +52,7 @@ def test_finalized_octant_rewards_after_overhaul( matched_rewards=MATCHED_REWARDS_AFTER_OVERHAUL, total_withdrawals=TOTAL_WITHDRAWALS, patrons_rewards=NO_PATRONS_REWARDS, + donated_to_projects=MATCHED_REWARDS_AFTER_OVERHAUL, ) @@ -70,3 +72,13 @@ def test_finalized_get_leverage( result = service.get_leverage(context) assert result == 144160.63189897747 + + +def test_donated_to_projects_in_octant_rewards_for_capped_mr( + mock_pending_epoch_snapshot_with_uq_scores, mock_finalized_epoch_snapshot_db_for_e4 +): + context = get_context(epoch_num=4) + service = FinalizedOctantRewards() + result = service.get_octant_rewards(context) + + assert result.donated_to_projects == 140849434135859019815 diff --git a/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py b/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py index 2f2e2c5ee2..f0aec7ccce 100644 --- a/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py +++ b/backend/tests/modules/octant_rewards/test_pending_octant_rewards.py @@ -4,6 +4,7 @@ from app.modules.octant_rewards.general.service.pending import PendingOctantRewards from app.modules.octant_rewards.matched.pending import PendingOctantMatchedRewards from app.modules.projects.rewards.service.finalizing import FinalizingProjectRewards +from tests.helpers import make_user_allocation from tests.helpers.constants import ( USER1_BUDGET, MOCKED_EPOCH_NO_AFTER_OVERHAUL, @@ -13,7 +14,6 @@ MATCHED_REWARDS, MATCHED_REWARDS_AFTER_OVERHAUL, ) -from tests.helpers import make_user_allocation from tests.helpers.context import get_context from tests.helpers.pending_snapshot import create_pending_snapshot from tests.modules.octant_rewards.helpers.checker import check_octant_rewards @@ -43,6 +43,7 @@ def test_pending_octant_rewards_before_overhaul( patrons_rewards=USER2_BUDGET, matched_rewards=MATCHED_REWARDS + USER2_BUDGET, leftover=321928766823288000000, + donated_to_projects=0, ) @@ -60,6 +61,7 @@ def test_pending_octant_rewards_after_overhaul( patrons_rewards=USER2_BUDGET, matched_rewards=MATCHED_REWARDS_AFTER_OVERHAUL, leftover=282293485473756640672, + donated_to_projects=0, ) @@ -117,3 +119,28 @@ def test_pending_get_leverage( result = service.get_leverage(context) assert result == 144164.29856550877 + + +def test_donated_to_projects_in_octant_rewards_for_capped_mr( + mock_pending_epoch_snapshot_with_uq_scores, service +): + user1, _, _ = mock_pending_epoch_snapshot_with_uq_scores + context = get_context(epoch_num=4) + make_user_allocation( + context, + user1, + allocation_items=[ + AllocationItem(context.projects_details.projects[0], USER1_BUDGET) + ], + ) + result = service.get_octant_rewards(context) + + check_octant_rewards( + result, + community_fund=COMMUNITY_FUND, + ppf=PPF, + patrons_rewards=USER2_BUDGET, + matched_rewards=MATCHED_REWARDS_AFTER_OVERHAUL, + leftover=366801619086282814574, + donated_to_projects=28171413696161041950, + ) diff --git a/backend/tests/modules/projects/conftest.py b/backend/tests/modules/projects/conftest.py new file mode 100644 index 0000000000..6ac250455e --- /dev/null +++ b/backend/tests/modules/projects/conftest.py @@ -0,0 +1,11 @@ +import pytest + +from tests.modules.projects.helpers import sample_projects_details + + +@pytest.fixture(scope="function") +def patch_projects_details(monkeypatch): + monkeypatch.setattr( + "app.modules.projects.details.service.projects_details.get_projects_details_for_epoch", + sample_projects_details, + ) diff --git a/backend/tests/modules/projects/details/test_filtering_core.py b/backend/tests/modules/projects/details/test_filtering_core.py new file mode 100644 index 0000000000..26fc1f7e6c --- /dev/null +++ b/backend/tests/modules/projects/details/test_filtering_core.py @@ -0,0 +1,71 @@ +from app.modules.projects.details.core import filter_projects_details +from tests.modules.projects.helpers import sample_projects_details + +SAMPLE_PROJECTS_DETAILS = sample_projects_details() + + +def test_filter_projects_partial_match_in_name(): + """Test that partial matches in the project name are correctly filtered.""" + search_phrase = "Octant" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 2 + assert filtered_projects[0].name == "OctantProject3" + assert filtered_projects[1].name == "OctantTestProject4" + + +def test_filter_projects_partial_match_in_address(): + """Test that partial matches in the project address are correctly filtered.""" + search_phrase = "0x111" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 1 + assert filtered_projects[0].address == "0x111" + assert filtered_projects[0].name == "TEST1" + + +def test_filter_projects_empty_search_phrase(): + """Test that all projects are returned when search phrase is empty.""" + search_phrase = "" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == len(SAMPLE_PROJECTS_DETAILS) + assert all(project in filtered_projects for project in SAMPLE_PROJECTS_DETAILS) + + +def test_filter_projects_special_characters_in_search_phrase(): + """Test that search phrases with special characters work correctly.""" + search_phrase = "0x444" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 1 + assert filtered_projects[0].address == "0x444" + assert filtered_projects[0].name == "OctantTestProject4" + + +def test_filter_projects_search_phrase_not_in_name_or_address(): + """Test that no projects are returned when search phrase doesn't match anything.""" + search_phrase = "NonExistentProject" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 0 + + +def test_filter_projects_multiple_matches(): + """Test that multiple projects are returned when search phrase matches multiple names/addresses.""" + search_phrase = "Project" + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 2 + assert filtered_projects[0].name == "OctantProject3" + assert filtered_projects[1].name == "OctantTestProject4" + + +def test_filter_projects_whitespace_in_search_phrase(): + """Test that leading/trailing whitespace in search phrase is handled correctly.""" + search_phrase = " TEST1 " + filtered_projects = filter_projects_details(SAMPLE_PROJECTS_DETAILS, search_phrase) + + assert len(filtered_projects) == 1 + assert filtered_projects[0].name == "TEST1" + assert filtered_projects[0].address == "0x111" diff --git a/backend/tests/modules/projects/details/test_projects_filtering.py b/backend/tests/modules/projects/details/test_projects_filtering.py new file mode 100644 index 0000000000..71e9f28e70 --- /dev/null +++ b/backend/tests/modules/projects/details/test_projects_filtering.py @@ -0,0 +1,37 @@ +import pytest + +from app.modules.projects.details.service.projects_details import ( + StaticProjectsDetailsService, + ProjectsDetailsDTO, +) +from tests.helpers.context import get_context + + +@pytest.fixture(autouse=True) +def before(app, patch_projects_details): + pass + + +@pytest.mark.parametrize("search_phrase", ["Octant", "TEST", "AnyName"]) +def test_get_projects_details_by_search_phrase(search_phrase): + epoch = 4 + context = get_context(epoch) + service = StaticProjectsDetailsService() + projects_details: ProjectsDetailsDTO = ( + service.get_projects_details_by_search_phrase(context, search_phrase) + ) + + for project in projects_details.projects_details: + assert search_phrase.lower() in project["name"].lower() + assert project["epoch"] == str(epoch) + assert "address" in project and project["address"] is not None + + +def test_get_projects_details_by_search_phrase_not_found(): + context = get_context(4) + service = StaticProjectsDetailsService() + projects_details: ProjectsDetailsDTO = ( + service.get_projects_details_by_search_phrase(context, "NOT_FOUND") + ) + + assert projects_details.projects_details == [] diff --git a/backend/tests/modules/projects/helpers.py b/backend/tests/modules/projects/helpers.py new file mode 100644 index 0000000000..8ed3b97201 --- /dev/null +++ b/backend/tests/modules/projects/helpers.py @@ -0,0 +1,12 @@ +from typing import List + +from app.infrastructure.database.models import ProjectsDetails + + +def sample_projects_details(*args, **kwargs) -> List[ProjectsDetails]: + return [ + ProjectsDetails(id=1, address="0x111", name="TEST1", epoch=4), + ProjectsDetails(id=2, address="0x222", name="AnyName2", epoch=4), + ProjectsDetails(id=3, address="0x333", name="OctantProject3", epoch=4), + ProjectsDetails(id=4, address="0x444", name="OctantTestProject4", epoch=4), + ] diff --git a/backend/tests/modules/projects/metadata/test_projects_retrieval.py b/backend/tests/modules/projects/metadata/test_projects_retrieval.py index 9f91b87041..8dfc1cff15 100644 --- a/backend/tests/modules/projects/metadata/test_projects_retrieval.py +++ b/backend/tests/modules/projects/metadata/test_projects_retrieval.py @@ -14,7 +14,6 @@ def before(app, patch_projects): MOCK_PROJECTS.get_project_cid.return_value = ( "QmXbFKrMGJUbXupmTQsQhoy9zkzXDBHZkPAzKC4yiaLt5n" ) - pass def test_get_projects_metadata_epoch_1(): @@ -70,6 +69,21 @@ def test_get_projects_metadata_epoch_4(): context, is_mainnet=True ) + assert ( + projects_metadata.projects_cid + == "QmXomSdCCwt4FtBp3pidqSz3PtaiV2EyQikU6zRGWeCAsf" + ) + assert projects_metadata.projects_addresses == ["0x0", "0x1"] + + +def test_get_projects_metadata_epoch_5(): + context = get_context(5) + + service = StaticProjectsMetadataService() + projects_metadata: ProjectsMetadata = service.get_projects_metadata( + context, is_mainnet=True + ) + assert ( projects_metadata.projects_cid == "QmXbFKrMGJUbXupmTQsQhoy9zkzXDBHZkPAzKC4yiaLt5n" diff --git a/backend/tests/modules/score_delegation/test_score_delegation_core.py b/backend/tests/modules/score_delegation/test_score_delegation_core.py index feb6c85b22..591dea14e3 100644 --- a/backend/tests/modules/score_delegation/test_score_delegation_core.py +++ b/backend/tests/modules/score_delegation/test_score_delegation_core.py @@ -7,8 +7,13 @@ from app import exceptions from app.modules.common.delegation import hash_addresses from app.modules.score_delegation import core -from app.exceptions import InvalidDelegationForLockingAddress -from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS, USER3_ADDRESS +from app.exceptions import InvalidDelegationForLockingAddress, InvalidDelegationRequest +from tests.helpers.constants import ( + USER1_ADDRESS, + USER2_ADDRESS, + USER3_ADDRESS, + TIMEOUT_LIST, +) def test_score_delegation_passes(): @@ -18,7 +23,13 @@ def test_score_delegation_passes(): USER1_ADDRESS, USER2_ADDRESS, "salt", "salt_primary" ) core.verify_score_delegation( - hashed_addresses, {hashed_carol}, 20, 0, core.ActionType.DELEGATION + hashed_addresses, + {hashed_carol}, + 20, + 0, + core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -27,7 +38,13 @@ def test_score_delegation_passes_when_there_are_no_other_delegations(): USER1_ADDRESS, USER2_ADDRESS, "salt", "salt_primary" ) core.verify_score_delegation( - hashed_addresses, set(), 20, 0, core.ActionType.DELEGATION + hashed_addresses, + set(), + 20, + 0, + core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -43,6 +60,25 @@ def test_score_delegation_fails_when_secondary_is_locking(): 20, BUDGET_GREATER_THAN_ZERO, core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, + ) + + +def test_score_delegation_fails_when_primary_is_timeouted(): + hashed_addresses = hash_addresses( + USER1_ADDRESS, USER2_ADDRESS, "salt", "salt_primary" + ) + TIMEOUT_LIST = {USER1_ADDRESS.lower()} + with pytest.raises(InvalidDelegationRequest): + core.verify_score_delegation( + hashed_addresses, + set(), + 20, + 100, + core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -58,6 +94,8 @@ def test_score_delegation_fails(): 20, 0, core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) with pytest.raises(exceptions.DelegationAlreadyExists): @@ -67,6 +105,8 @@ def test_score_delegation_fails(): 20, 0, core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -80,6 +120,8 @@ def test_score_recalculation_passes(): 20, 0, core.ActionType.RECALCULATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -89,7 +131,13 @@ def test_score_recalculation_fails_when_there_are_no_other_delegations(): ) with pytest.raises(exceptions.DelegationDoesNotExist): core.verify_score_delegation( - hashed_addresses, set(), 20, 0, core.ActionType.RECALCULATION + hashed_addresses, + set(), + 20, + 0, + core.ActionType.RECALCULATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -110,6 +158,8 @@ def test_score_recalculation_fails(): 20, 0, core.ActionType.RECALCULATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -120,7 +170,13 @@ def test_score_is_too_low(): with pytest.raises(exceptions.AntisybilScoreTooLow): core.verify_score_delegation( - hashed_addresses, set(), 19.9, 0, core.ActionType.DELEGATION + hashed_addresses, + set(), + 19.9, + 0, + core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -129,7 +185,13 @@ def test_score_is_sufficient(): USER1_ADDRESS, USER2_ADDRESS, "salt", "salt_primary" ) core.verify_score_delegation( - hashed_addresses, set(), 20, 0, core.ActionType.DELEGATION + hashed_addresses, + set(), + 20, + 0, + core.ActionType.DELEGATION, + USER1_ADDRESS, + TIMEOUT_LIST, ) @@ -163,16 +225,16 @@ def check(addresses): assert set() == check([ALICE, ALICE]) assert set() == check([CAROL, BOB]) assert set() == check([CAROL, ALICE]) - assert set([(BOB, ALICE)]) == check([ALICE, BOB]) - assert set([(BOB, ALICE)]) == check([BOB, ALICE]) - assert set([(BOB, ALICE)]) == check([BOB, ALICE, EVE]) - assert set([(BOB, ALICE), (EVE, CAROL)]) == check([BOB, EVE, ALICE, CAROL]) + assert {(BOB, ALICE)} == check([ALICE, BOB]) + assert {(BOB, ALICE)} == check([BOB, ALICE]) + assert {(BOB, ALICE)} == check([BOB, ALICE, EVE]) + assert {(BOB, ALICE), (EVE, CAROL)} == check([BOB, EVE, ALICE, CAROL]) start = datetime.now() - assert set([(BOB, ALICE), (EVE, CAROL), (ALICE, FRANK)]) == check( + assert {(BOB, ALICE), (EVE, CAROL), (ALICE, FRANK)} == check( [ALICE, BOB, CAROL, EVE, FRANK, HEIDI, IVAN, JUDY, MALLORY, NICK] ) finish = datetime.now() assert finish - start < timedelta(seconds=2) # check if address checksumming works as expected - assert set([(BOB, ALICE.lower())]) == check([ALICE.lower(), BOB]) + assert {(BOB, ALICE.lower())} == check([ALICE.lower(), BOB]) diff --git a/backend/tests/modules/score_delegation/test_simple_obfuscation.py b/backend/tests/modules/score_delegation/test_simple_obfuscation.py index 44c7cf9824..d9906416e4 100644 --- a/backend/tests/modules/score_delegation/test_simple_obfuscation.py +++ b/backend/tests/modules/score_delegation/test_simple_obfuscation.py @@ -14,7 +14,12 @@ SimpleObfuscationDelegationVerifier, ) from app.modules.user.deposits.service.calculated import CalculatedUserDeposits -from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS, USER3_ADDRESS +from tests.helpers.constants import ( + USER1_ADDRESS, + USER2_ADDRESS, + USER3_ADDRESS, + TIMEOUT_LIST, +) @pytest.fixture(autouse=True) @@ -44,7 +49,10 @@ def test_delegation( ["stamp"], ) service = SimpleObfuscationDelegation( - verifier=verifier, antisybil=antisybil, user_deposits_service=user_deposits + verifier=verifier, + antisybil=antisybil, + user_deposits_service=user_deposits, + timeout_list=TIMEOUT_LIST, ) service.delegate(context, payload) @@ -70,7 +78,10 @@ def test_delegation_disabled_when_secondary_is_locking( secondary_addr_signature="0x5e7e86d5acea5cc431b8d148842e21584a7afe16b7de3b5586d20f5de97179f549726baa021dcaf6220ee5116c579df9d40375fa58d3480390289df6a088b9ec1b", ) service = SimpleObfuscationDelegation( - verifier=verifier, antisybil=antisybil, user_deposits_service=user_deposits + verifier=verifier, + antisybil=antisybil, + user_deposits_service=user_deposits, + timeout_list=TIMEOUT_LIST, ) with pytest.raises(InvalidDelegationForLockingAddress): service.delegate(context, payload) @@ -88,7 +99,10 @@ def test_disable_recalculation_when_secondary_address_is_used( ["stamp"], ) service = SimpleObfuscationDelegation( - verifier=verifier, antisybil=antisybil, user_deposits_service=user_deposits + verifier=verifier, + antisybil=antisybil, + user_deposits_service=user_deposits, + timeout_list=TIMEOUT_LIST, ) service.delegate(context, payload) @@ -120,7 +134,10 @@ def test_recalculation_when_delegation_is_not_done( ["stamp"], ) service = SimpleObfuscationDelegation( - verifier=verifier, antisybil=antisybil, user_deposits_service=user_deposits + verifier=verifier, + antisybil=antisybil, + user_deposits_service=user_deposits, + timeout_list=TIMEOUT_LIST, ) service.delegate(context, payload) diff --git a/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py b/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py index 9129dd996f..fbccc322d8 100644 --- a/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py +++ b/backend/tests/modules/snapshots/finalized/test_finalizing_snapshots.py @@ -8,7 +8,11 @@ from app.modules.snapshots.finalized.service.finalizing import FinalizingSnapshots from tests.helpers import make_user_allocation from tests.helpers.allocations import make_user_allocation_with_uq_score -from tests.helpers.constants import MATCHED_REWARDS, USER2_BUDGET, LOW_UQ_SCORE +from tests.helpers.constants import ( + MATCHED_REWARDS, + USER2_BUDGET, + MR_FUNDING_CAP_PERCENT, +) from tests.helpers.context import get_context @@ -84,8 +88,8 @@ def test_create_finalized_snapshots_with_rewards_and_user_uq_score( assert rewards[0].amount == str(200_000000000) assert rewards[0].matched is None assert rewards[1].address == projects[0] - assert rewards[1].amount == str(int(LOW_UQ_SCORE * MATCHED_REWARDS + 100)) - assert rewards[1].matched == str(int(LOW_UQ_SCORE * MATCHED_REWARDS)) + assert rewards[1].amount == str(int(MR_FUNDING_CAP_PERCENT * MATCHED_REWARDS + 100)) + assert rewards[1].matched == str(int(MR_FUNDING_CAP_PERCENT * MATCHED_REWARDS)) assert rewards[2].address == alice.address assert rewards[2].amount == str(100_000000000) assert rewards[2].matched is None @@ -93,7 +97,7 @@ def test_create_finalized_snapshots_with_rewards_and_user_uq_score( snapshot = database.finalized_epoch_snapshot.get_by_epoch_num(result) assert snapshot.matched_rewards == str(MATCHED_REWARDS) assert snapshot.total_withdrawals == str( - int(LOW_UQ_SCORE * MATCHED_REWARDS) + 100 + 300_000000000 + int(MR_FUNDING_CAP_PERCENT * MATCHED_REWARDS) + 100 + 300_000000000 ) assert snapshot.patrons_rewards == str(USER2_BUDGET) assert snapshot.leftover == str(414362124463057389617) diff --git a/backend/tests/modules/uq/conftest.py b/backend/tests/modules/uq/conftest.py index 1a46984a88..668c85a7ee 100644 --- a/backend/tests/modules/uq/conftest.py +++ b/backend/tests/modules/uq/conftest.py @@ -4,13 +4,16 @@ import pytest from app.modules.uq.service.preliminary import PreliminaryUQ +from app.modules.user.antisybil.dto import AntisybilStatusDTO from tests.helpers.constants import UQ_THRESHOLD_MAINNET @pytest.fixture def mock_antisybil(): mock = Mock() - mock.get_antisybil_status.return_value = (10.0, datetime.now()) + mock.get_antisybil_status.return_value = AntisybilStatusDTO( + score=10.0, expires_at=datetime.now(), is_on_timeout_list=False + ) return mock diff --git a/backend/tests/modules/uq/test_preliminary_uq.py b/backend/tests/modules/uq/test_preliminary_uq.py index 69bc81c767..a628c68f47 100644 --- a/backend/tests/modules/uq/test_preliminary_uq.py +++ b/backend/tests/modules/uq/test_preliminary_uq.py @@ -7,8 +7,9 @@ from app.extensions import db from app.infrastructure import database from app.modules.uq import core +from app.modules.user.antisybil.dto import AntisybilStatusDTO from tests.helpers.allocations import mock_request -from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS +from tests.helpers.constants import USER1_ADDRESS, USER2_ADDRESS, LOW_UQ_SCORE from tests.helpers.context import get_context @@ -18,14 +19,16 @@ def before(app): def test_calculate_uq_above_threshold(context, mock_antisybil, service): - mock_antisybil.get_antisybil_status.return_value = (20.0, datetime.now()) + mock_antisybil.get_antisybil_status.return_value = AntisybilStatusDTO( + score=20.0, expires_at=datetime.now(), is_on_timeout_list=False + ) result = service.calculate(context, USER1_ADDRESS) assert result == 1.0 def test_calculate_uq_below_threshold(context, service): result = service.calculate(context, USER1_ADDRESS) - assert result == Decimal("0.2") + assert result == LOW_UQ_SCORE def test_retrieve_uq_when_score_in_the_db(service, mock_users_db_with_scores): @@ -46,7 +49,7 @@ def test_retrieve_uq_when_score_calculated_dynamically(context, service, mock_us db.session.commit() result = service.retrieve(context, USER1_ADDRESS) - assert result == Decimal("0.2") + assert result == LOW_UQ_SCORE def test_get_all_user_uq_pairs(context, service, mock_users_db): @@ -60,4 +63,4 @@ def test_get_all_user_uq_pairs(context, service, mock_users_db): db.session.commit() result = core.get_all_uqs(1) - assert result == [(alice.address, Decimal("0.2")), (bob.address, Decimal("0.2"))] + assert result == [(alice.address, LOW_UQ_SCORE), (bob.address, LOW_UQ_SCORE)] diff --git a/backend/tests/modules/uq/test_uq_score.py b/backend/tests/modules/uq/test_uq_score.py index dcb0488841..2c653e1776 100644 --- a/backend/tests/modules/uq/test_uq_score.py +++ b/backend/tests/modules/uq/test_uq_score.py @@ -3,19 +3,35 @@ import pytest from app.modules.uq.core import calculate_uq -from tests.helpers.constants import UQ_THRESHOLD_MAINNET +from tests.helpers.constants import ( + UQ_THRESHOLD_MAINNET, + LOW_UQ_SCORE, + MAX_UQ_SCORE, + NULLIFIED_UQ_SCORE, +) @pytest.mark.parametrize( "gp_score, expected_output", [ - (1, 0.2), - (19, 0.2), - (20, 1.0), - (27, 1.0), + (1, LOW_UQ_SCORE), + (14, LOW_UQ_SCORE), + (15, MAX_UQ_SCORE), + (27, MAX_UQ_SCORE), ], ) def test_calculate_uq(gp_score, expected_output): assert calculate_uq(gp_score, uq_threshold=UQ_THRESHOLD_MAINNET) == Decimal( str(expected_output) ) + + +def test_calculate_uq_when_address_on_timeout_list(): + gp_score = 0 + + assert ( + calculate_uq( + gp_score, uq_threshold=UQ_THRESHOLD_MAINNET, is_on_timeout_list=True + ) + == NULLIFIED_UQ_SCORE + ) diff --git a/backend/tests/modules/user/allocations/pending/test_allocations.py b/backend/tests/modules/user/allocations/pending/test_allocations.py index 926fffcb57..e57caf5fc2 100644 --- a/backend/tests/modules/user/allocations/pending/test_allocations.py +++ b/backend/tests/modules/user/allocations/pending/test_allocations.py @@ -27,7 +27,7 @@ create_payload, deserialize_allocations, ) -from tests.helpers.constants import MATCHED_REWARDS +from tests.helpers.constants import MATCHED_REWARDS, LOW_UQ_SCORE from tests.helpers.context import get_context @@ -416,7 +416,7 @@ def test_uq_added_while_allocating(project_accounts, tos_users): ) uq_1 = get_uq_by_address(user_addr, context.epoch_details.epoch_num) - assert uq_1.score == "0.2" + assert uq_1.score == str(LOW_UQ_SCORE) # Allocate for the second time payload = create_payload(project_accounts[0:4], None, 1) diff --git a/backend/tests/modules/user/allocations/pending/test_allocations_epoch4.py b/backend/tests/modules/user/allocations/pending/test_allocations_epoch4.py index bcafe6adce..162ba7291f 100644 --- a/backend/tests/modules/user/allocations/pending/test_allocations_epoch4.py +++ b/backend/tests/modules/user/allocations/pending/test_allocations_epoch4.py @@ -11,7 +11,11 @@ PendingUserAllocationsVerifier, ) from tests.helpers.allocations import make_user_allocation_with_uq_score -from tests.helpers.constants import MATCHED_REWARDS, LOW_UQ_SCORE +from tests.helpers.constants import ( + MATCHED_REWARDS, + LOW_UQ_SCORE, + MR_FUNDING_CAP_PERCENT, +) from tests.helpers.context import get_context @@ -66,7 +70,9 @@ def test_simulate_allocation_with_user_uq_score(service, mock_users_db): ProjectRewardDTO(sorted_projects[0], 0, 0), ProjectRewardDTO(sorted_projects[1], 0, 0), ProjectRewardDTO( - sorted_projects[2], 200000000000, int(MATCHED_REWARDS * LOW_UQ_SCORE) + sorted_projects[2], + 200000000000, + int(MATCHED_REWARDS * MR_FUNDING_CAP_PERCENT), ), ProjectRewardDTO(sorted_projects[3], 0, 0), ProjectRewardDTO(sorted_projects[4], 0, 0), @@ -107,7 +113,9 @@ def test_simulate_allocation_user_uq_score_with_passed_param(service, mock_users ProjectRewardDTO(sorted_projects[0], 0, 0), ProjectRewardDTO(sorted_projects[1], 0, 0), ProjectRewardDTO( - sorted_projects[2], 200000000000, int(MATCHED_REWARDS * LOW_UQ_SCORE) + sorted_projects[2], + 200000000000, + int(MATCHED_REWARDS * MR_FUNDING_CAP_PERCENT), ), ProjectRewardDTO(sorted_projects[3], 0, 0), ProjectRewardDTO(sorted_projects[4], 0, 0), diff --git a/backend/tests/modules/user/antisybil/test_antisybil.py b/backend/tests/modules/user/antisybil/test_antisybil.py index 40a2f30838..0d624a0ee9 100644 --- a/backend/tests/modules/user/antisybil/test_antisybil.py +++ b/backend/tests/modules/user/antisybil/test_antisybil.py @@ -1,10 +1,13 @@ +from datetime import datetime + import pytest + from app import exceptions, db -from datetime import datetime from app.exceptions import UserNotFound from app.infrastructure import database from app.modules.common.delegation import get_hashed_addresses from app.modules.user.antisybil.service.initial import GitcoinPassportAntisybil +from tests.helpers.constants import TIMEOUT_LIST from tests.helpers.context import get_context @@ -21,7 +24,7 @@ def test_antisybil_service( ): context = get_context(4) - service = GitcoinPassportAntisybil() + service = GitcoinPassportAntisybil(timeout_list=TIMEOUT_LIST) unknown_address = "0xa0Ee7A142d267C1f36714E4a8F75612F20a79720" try: @@ -39,43 +42,66 @@ def test_antisybil_service( service.update_antisybil_status(context, alice.address, score, expires_at, stamps) - score, _ = service.get_antisybil_status(context, alice.address) + result = service.get_antisybil_status(context, alice.address) + score = result.score assert score == 2.572 -def test_guest_stamp_score_bump_for_both_gp_and_octant_side_application( +def test_gtc_staking_stamp_nullification( patch_gitcoin_passport_issue_address_for_scoring, patch_gitcoin_passport_fetch_score, patch_gitcoin_passport_fetch_stamps, mock_users_db, ): context = get_context(4) - - service = GitcoinPassportAntisybil() - alice, _, _ = mock_users_db - - score, expires_at, stamps = service.fetch_antisybil_status(context, alice.address) - service.update_antisybil_status(context, alice.address, score, expires_at, stamps) - score, _ = service.get_antisybil_status(context, alice.address) - assert score == 2.572 # guest list score bonus not applied - - guest_address = "0x2f51E78ff8aeC6A941C4CEeeb26B4A1f03737c50" - database.user.add_user(guest_address) - score, expires_at, stamps = service.fetch_antisybil_status(context, guest_address) - service.update_antisybil_status(context, guest_address, score, expires_at, stamps) - score, _ = service.get_antisybil_status(context, guest_address) - assert (not stamps) and ( - score == 21.0 - ) # is on guest list, no stamps, applying 21 score bonus manually - - stamp_address = "0xBc6d82D8d6632938394905Bb0217Ad9c673015d1" - database.user.add_user(stamp_address) - score, expires_at, stamps = service.fetch_antisybil_status(context, stamp_address) - service.update_antisybil_status(context, stamp_address, score, expires_at, stamps) - score, _ = service.get_antisybil_status(context, stamp_address) - assert (stamps) and ( - score == 22.0 - ) # is on guest list, HAS GUEST LIST STAMP, score is from fetch + service = GitcoinPassportAntisybil(timeout_list=TIMEOUT_LIST) + _, _, carol = mock_users_db + score, expires_at, stamps = service.fetch_antisybil_status(context, carol.address) + service.update_antisybil_status(context, carol.address, score, expires_at, stamps) + + result = service.get_antisybil_status(context, carol.address) + + assert result.score == 0.5 + + +# TODO - OCT-2095: Fix this test as not to rely on the GP API but mock the response +# def test_guest_stamp_score_bump_for_both_gp_and_octant_side_application( +# patch_gitcoin_passport_issue_address_for_scoring, +# patch_gitcoin_passport_fetch_score, +# patch_gitcoin_passport_fetch_stamps, +# mock_users_db, +# ): +# context = get_context(4) +# +# service = GitcoinPassportAntisybil(timeout_list=TIMEOUT_LIST) +# alice, _, _ = mock_users_db +# +# score, expires_at, stamps = service.fetch_antisybil_status(context, alice.address) +# service.update_antisybil_status(context, alice.address, score, expires_at, stamps) +# result = service.get_antisybil_status(context, alice.address) +# score = result.score +# assert score == 2.572 # guest list score bonus not applied +# +# guest_address = "0xe6ed9c681967a4ea7cef4486942b800139dfb000" +# database.user.add_user(guest_address) +# score, expires_at, stamps = service.fetch_antisybil_status(context, guest_address) +# print("YYY", score, expires_at, stamps) +# service.update_antisybil_status(context, guest_address, score, expires_at, stamps) +# result = service.get_antisybil_status(context, guest_address) +# score = result.score +# assert (not stamps) and ( +# score == 21.0 +# ) # is on guest list, no stamps, applying 21 score bonus manually +# +# stamp_address = "0xBc6d82D8d6632938394905Bb0217Ad9c673015d1" +# database.user.add_user(stamp_address) +# score, expires_at, stamps = service.fetch_antisybil_status(context, stamp_address) +# service.update_antisybil_status(context, stamp_address, score, expires_at, stamps) +# result = service.get_antisybil_status(context, stamp_address) +# score = result.score +# assert (stamps) and ( +# score == 22.0 +# ) # is on guest list, HAS GUEST LIST STAMP, score is from fetch def test_antisybil_cant_be_update_when_address_is_delegated(alice, bob): @@ -85,7 +111,7 @@ def test_antisybil_cant_be_update_when_address_is_delegated(alice, bob): database.score_delegation.save_delegation(primary, secondary, both) db.session.commit() - service = GitcoinPassportAntisybil() + service = GitcoinPassportAntisybil(timeout_list=TIMEOUT_LIST) with pytest.raises(exceptions.AddressAlreadyDelegated): service.update_antisybil_status( @@ -94,3 +120,43 @@ def test_antisybil_cant_be_update_when_address_is_delegated(alice, bob): with pytest.raises(exceptions.AddressAlreadyDelegated): service.update_antisybil_status(context, bob.address, score, datetime.now(), {}) + + +def test_antisybil_score_is_nullified_when_address_on_timeout_list( + patch_gitcoin_passport_issue_address_for_scoring, + patch_gitcoin_passport_fetch_score, + patch_gitcoin_passport_fetch_stamps, + mock_users_db, +): + context = get_context(4) + + service = GitcoinPassportAntisybil( + timeout_list={"0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266"} + ) + alice, _, _ = mock_users_db + timeout_address = alice.address + score, expires_at, stamps = service.fetch_antisybil_status(context, timeout_address) + service.update_antisybil_status(context, timeout_address, score, expires_at, stamps) + + result = service.get_antisybil_status(context, timeout_address) + + assert result.score == 0.0 + assert result.is_on_timeout_list is True + + +def test_fetch_antisybil_return_0_when_address_on_timeout_list( + patch_gitcoin_passport_issue_address_for_scoring, + patch_gitcoin_passport_fetch_score, + patch_gitcoin_passport_fetch_stamps, + mock_users_db, +): + context = get_context(4) + + service = GitcoinPassportAntisybil( + timeout_list={"0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266"} + ) + alice, _, _ = mock_users_db + timeout_address = alice.address + score, expires_at, stamps = service.fetch_antisybil_status(context, timeout_address) + + assert score == 0.0 diff --git a/backend/v2/__init__.py b/backend/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/allocations/__init__.py b/backend/v2/allocations/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/allocations/dependencies.py b/backend/v2/allocations/dependencies.py new file mode 100644 index 0000000000..df1ea54741 --- /dev/null +++ b/backend/v2/allocations/dependencies.py @@ -0,0 +1,45 @@ +from typing import Annotated + +from fastapi import Depends +from v2.allocations.services import Allocator +from v2.allocations.validators import SignatureVerifier +from v2.core.dependencies import GetChainSettings, GetSession +from v2.epochs.dependencies import GetEpochsSubgraph, GetOpenAllocationWindowEpochNumber +from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator +from v2.projects.dependencies import GetProjectsContracts +from v2.uniqueness_quotients.dependencies import GetUQScoreGetter + + +def get_signature_verifier( + session: GetSession, + epochs_subgraph: GetEpochsSubgraph, + projects_contracts: GetProjectsContracts, + settings: GetChainSettings, +) -> SignatureVerifier: + return SignatureVerifier( + session, epochs_subgraph, projects_contracts, settings.chain_id + ) + + +GetSignatureVerifier = Annotated[SignatureVerifier, Depends(get_signature_verifier)] + + +async def get_allocator( + epoch_number: GetOpenAllocationWindowEpochNumber, + session: GetSession, + signature_verifier: GetSignatureVerifier, + uq_score_getter: GetUQScoreGetter, + projects_contracts: GetProjectsContracts, + matched_rewards_estimator: GetMatchedRewardsEstimator, +) -> Allocator: + return Allocator( + session, + signature_verifier, + uq_score_getter, + projects_contracts, + matched_rewards_estimator, + epoch_number, + ) + + +GetAllocator = Annotated[Allocator, Depends(get_allocator)] diff --git a/backend/v2/allocations/repositories.py b/backend/v2/allocations/repositories.py new file mode 100644 index 0000000000..eac0d715c0 --- /dev/null +++ b/backend/v2/allocations/repositories.py @@ -0,0 +1,174 @@ +from datetime import datetime +from decimal import Decimal + +from app.infrastructure.database.models import Allocation +from app.infrastructure.database.models import AllocationRequest as AllocationRequestDB +from app.infrastructure.database.models import UniquenessQuotient, User +from eth_utils import to_checksum_address +from sqlalchemy import Numeric, cast, func, select, update +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.orm import joinedload +from sqlalchemy.sql.functions import coalesce +from v2.allocations.schemas import ( + AllocationWithUserUQScore, + ProjectDonation, + UserAllocationRequest, +) +from v2.core.types import Address +from v2.users.repositories import get_user_by_address + + +async def sum_allocations_by_epoch(session: AsyncSession, epoch_number: int) -> int: + """Get the sum of all allocations for a given epoch. We only consider the allocations that have not been deleted.""" + + result = await session.execute( + select(coalesce(func.sum(cast(Allocation.amount, Numeric)), 0)) + .filter(Allocation.epoch == epoch_number) + .filter(Allocation.deleted_at.is_(None)) + ) + count = result.scalar() + + if count is None: + return 0 + + return int(count) + + +async def get_allocations_with_user_uqs( + session: AsyncSession, epoch_number: int +) -> list[AllocationWithUserUQScore]: + """Get all allocations for a given epoch, including the uniqueness quotients of the users.""" + + result = await session.execute( + select( + Allocation.project_address, + Allocation.amount, + User.address.label("user_address"), + UniquenessQuotient.score, + ) + .join(User, Allocation.user_id == User.id) + .join(UniquenessQuotient, UniquenessQuotient.user_id == User.id) + .filter(Allocation.epoch == epoch_number) + .filter(Allocation.deleted_at.is_(None)) + .filter(UniquenessQuotient.epoch == epoch_number) + ) + + rows = result.all() + + return [ + AllocationWithUserUQScore( + projectAddress=project_address, + amount=amount, + userAddress=user_address, + userUqScore=Decimal(uq_score), + ) + for project_address, amount, user_address, uq_score in rows + ] + + +async def soft_delete_user_allocations_by_epoch( + session: AsyncSession, + user_address: Address, + epoch_number: int, +) -> None: + """Soft delete all user allocations for a given epoch.""" + + # Find all the allocations for the user and epoch that have not been deleted + user = await get_user_by_address(session, user_address) + + if user is None: + return None + + now = datetime.utcnow() + + # Perform a batch update to soft delete the allocations + await session.execute( + update(Allocation) + .where( + Allocation.epoch == epoch_number, + Allocation.user_id == user.id, + Allocation.deleted_at.is_(None), + ) + .values(deleted_at=now) + ) + + +async def store_allocation_request( + session: AsyncSession, + user_address: Address, + epoch_number: int, + request: UserAllocationRequest, + leverage: float, +) -> None: + """Store an allocation request in the database.""" + + user = await get_user_by_address(session, user_address) + if user is None: + return None + + new_allocations = [ + Allocation( + epoch=epoch_number, + user_id=user.id, + nonce=request.nonce, + project_address=to_checksum_address(a.project_address), + amount=str(a.amount), + ) + for a in request.allocations + ] + + allocation_request = AllocationRequestDB( + user_id=user.id, + epoch=epoch_number, + nonce=request.nonce, + signature=request.signature, + is_manually_edited=request.is_manually_edited, + leverage=leverage, + ) + + session.add(allocation_request) + session.add_all(new_allocations) + + +async def get_last_allocation_request_nonce( + session: AsyncSession, + user_address: Address, +) -> int | None: + """Get the last nonce of the allocation requests for a user.""" + + user = await get_user_by_address(session, user_address) + if user is None: + return None + + return await session.scalar( + select(func.max(AllocationRequestDB.nonce)).filter( + AllocationRequestDB.user_id == user.id + ) + ) + + +async def get_donations_by_project( + session: AsyncSession, + project_address: str, + epoch_number: int, +) -> list[ProjectDonation]: + """Get all donations for a project in a given epoch.""" + + result = await session.execute( + select(Allocation) + .options(joinedload(Allocation.user)) + .filter(Allocation.project_address == project_address) + .filter(Allocation.epoch == epoch_number) + .filter(Allocation.deleted_at.is_(None)) + ) + + allocations = result.scalars().all() + + return [ + ProjectDonation( + amount=a.amount, + donorAddress=a.user.address, + projectAddress=a.project_address, + ) + for a in allocations + ] diff --git a/backend/v2/allocations/router.py b/backend/v2/allocations/router.py new file mode 100644 index 0000000000..dba8cc4782 --- /dev/null +++ b/backend/v2/allocations/router.py @@ -0,0 +1,29 @@ +from fastapi import APIRouter +from v2.allocations.dependencies import GetAllocator +from v2.allocations.schemas import UserAllocationRequest, UserAllocationRequestV1 + +api = APIRouter(prefix="/allocations", tags=["Allocations"]) + + +@api.post("/allocate", status_code=201) +async def allocate( + # Component dependencies + allocator: GetAllocator, + # Request Parameters + allocation_request: UserAllocationRequestV1, +) -> None: + """ + Request an allocation for the user. + Only available during the allocation window. + """ + + # TODO: We should ideally move to the newer version of the schema as it's simpler + request = UserAllocationRequest( + userAddress=allocation_request.user_address, + allocations=allocation_request.payload.allocations, + nonce=allocation_request.payload.nonce, + signature=allocation_request.signature, + isManuallyEdited=allocation_request.is_manually_edited, + ) + + await allocator.handle(request) diff --git a/backend/v2/allocations/schemas.py b/backend/v2/allocations/schemas.py new file mode 100644 index 0000000000..983cdcb166 --- /dev/null +++ b/backend/v2/allocations/schemas.py @@ -0,0 +1,43 @@ +from decimal import Decimal + +from pydantic import Field +from v2.core.types import Address, BigInteger, OctantModel + + +class AllocationWithUserUQScore(OctantModel): + project_address: Address + amount: BigInteger + user_address: Address + user_uq_score: Decimal + + +class AllocationRequest(OctantModel): + project_address: Address = Field(..., alias="proposalAddress") + amount: BigInteger + + +class UserAllocationRequestPayloadV1(OctantModel): + allocations: list[AllocationRequest] + nonce: int + + +class UserAllocationRequestV1(OctantModel): + user_address: Address + payload: UserAllocationRequestPayloadV1 + signature: str + is_manually_edited: bool + + +class UserAllocationRequest(OctantModel): + user_address: Address + allocations: list[AllocationRequest] + nonce: int + signature: str + + is_manually_edited: bool + + +class ProjectDonation(OctantModel): + amount: BigInteger + donor_address: Address # user address + project_address: Address diff --git a/backend/v2/allocations/services.py b/backend/v2/allocations/services.py new file mode 100644 index 0000000000..04d227534e --- /dev/null +++ b/backend/v2/allocations/services.py @@ -0,0 +1,140 @@ +import asyncio +from dataclasses import dataclass + +from app import exceptions +from sqlalchemy.ext.asyncio import AsyncSession +from v2.allocations.repositories import ( + get_allocations_with_user_uqs, + soft_delete_user_allocations_by_epoch, + store_allocation_request, +) +from v2.allocations.schemas import AllocationWithUserUQScore, UserAllocationRequest +from v2.allocations.validators import SignatureVerifier +from v2.matched_rewards.services import MatchedRewardsEstimator +from v2.project_rewards.capped_quadriatic import cqf_simulate_leverage +from v2.projects.contracts import ProjectsContracts +from v2.uniqueness_quotients.dependencies import UQScoreGetter +from v2.users.repositories import get_user_by_address + + +@dataclass +class Allocator: + session: AsyncSession + signature_verifier: SignatureVerifier + uq_score_getter: UQScoreGetter + projects_contracts: ProjectsContracts + matched_rewards_estimator: MatchedRewardsEstimator + + epoch_number: int + + async def handle( + self, + # epoch_number: int, + request: UserAllocationRequest, + ) -> str: + """ + Make an allocation for the user. + """ + return await allocate( + session=self.session, + signature_verifier=self.signature_verifier, + uq_score_getter=self.uq_score_getter, + projects_contracts=self.projects_contracts, + matched_rewards_estimator=self.matched_rewards_estimator, + epoch_number=self.epoch_number, + request=request, + ) + + +async def allocate( + # Component dependencies + session: AsyncSession, + signature_verifier: SignatureVerifier, + uq_score_getter: UQScoreGetter, + projects_contracts: ProjectsContracts, + matched_rewards_estimator: MatchedRewardsEstimator, + epoch_number: int, + # Arguments + request: UserAllocationRequest, +) -> str: + # Verify the signature + await signature_verifier.verify( + epoch_number=epoch_number, + request=request, + ) + + # Get or calculate UQ score of the user + user_uq_score = await uq_score_getter.get_or_calculate( + epoch_number=epoch_number, + user_address=request.user_address, + ) + + # Calculate leverage by simulating the allocation + new_allocations = [ + AllocationWithUserUQScore( + projectAddress=a.project_address, + amount=a.amount, + userAddress=request.user_address, + userUqScore=user_uq_score, + ) + for a in request.allocations + ] + + leverage = await simulate_leverage( + session, + projects_contracts, + matched_rewards_estimator, + epoch_number, + new_allocations, + ) + + await soft_delete_user_allocations_by_epoch( + session, request.user_address, epoch_number + ) + + # Get user and update allocation nonce + user = await get_user_by_address(session, request.user_address) + if user is None: + raise exceptions.UserNotFound(request.user_address) + + user.allocation_nonce = request.nonce + + await store_allocation_request( + session, + request.user_address, + epoch_number, + request, + leverage, + ) + + # Commit the transaction + await session.commit() + + return request.user_address + + +async def simulate_leverage( + # Component dependencies + session: AsyncSession, + projects_contracts: ProjectsContracts, + matched_rewards_estimator: MatchedRewardsEstimator, + # Arguments + epoch_number: int, + new_allocations: list[AllocationWithUserUQScore], +) -> float: + """ + Calculate leverage of the allocation made by the user. + """ + + all_projects, matched_rewards, existing_allocations = await asyncio.gather( + projects_contracts.get_project_addresses(epoch_number), + matched_rewards_estimator.get(), + get_allocations_with_user_uqs(session, epoch_number), + ) + + return cqf_simulate_leverage( + existing_allocations=existing_allocations, + new_allocations=new_allocations, + matched_rewards=matched_rewards, + project_addresses=all_projects, + ) diff --git a/backend/v2/allocations/socket.py b/backend/v2/allocations/socket.py new file mode 100644 index 0000000000..23d9dbecfc --- /dev/null +++ b/backend/v2/allocations/socket.py @@ -0,0 +1,376 @@ +import asyncio +import logging +from contextlib import asynccontextmanager +from typing import AsyncGenerator, Tuple + +import socketio +from app.exceptions import OctantException +from sqlalchemy.ext.asyncio import AsyncSession +from v2.allocations.dependencies import get_allocator, get_signature_verifier +from v2.allocations.repositories import get_donations_by_project +from v2.allocations.schemas import UserAllocationRequest, UserAllocationRequestV1 +from v2.allocations.services import Allocator +from v2.core.dependencies import ( + get_chain_settings, + get_database_settings, + get_sessionmaker, + get_w3, + get_web3_provider_settings, +) +from v2.core.exceptions import AllocationWindowClosed +from v2.epochs.dependencies import ( + get_epochs_contracts, + get_epochs_settings, + get_epochs_subgraph, + get_epochs_subgraph_settings, + get_open_allocation_window_epoch_number, +) +from v2.matched_rewards.dependencies import ( + get_matched_rewards_estimator, + get_matched_rewards_estimator_settings, +) +from v2.project_rewards.dependencies import get_project_rewards_estimator +from v2.project_rewards.services import ProjectRewardsEstimator +from v2.projects.dependencies import ( + get_projects_allocation_threshold_getter, + get_projects_allocation_threshold_settings, + get_projects_contracts, + get_projects_settings, +) +from v2.projects.services import ProjectsAllocationThresholdGetter +from v2.uniqueness_quotients.dependencies import ( + get_uq_score_getter, + get_uq_score_settings, +) + + +@asynccontextmanager +async def create_dependencies_on_connect() -> AsyncGenerator[ + Tuple[AsyncSession, ProjectsAllocationThresholdGetter, ProjectRewardsEstimator], + None, +]: + """ + Create and return all service dependencies. + """ + w3 = get_w3(get_web3_provider_settings()) + epochs_contracts = get_epochs_contracts(w3, get_epochs_settings()) + + # We do not handle requests outside of pending epoch state (Allocation Window) + # This will raise an exception if the allocation window is closed and connection does not happen + epoch_number = await get_open_allocation_window_epoch_number(epochs_contracts) + + projects_contracts = get_projects_contracts(w3, get_projects_settings()) + epochs_subgraph = get_epochs_subgraph(get_epochs_subgraph_settings()) + + # For safety, we create separate sessions for each dependency + # (to avoid any potential issues with session sharing in async task context) + + sessionmaker = get_sessionmaker(get_database_settings()) + + async with ( + sessionmaker() as s1, + sessionmaker() as s2, + sessionmaker() as s3, + sessionmaker() as s4, + ): + try: + threshold_getter = get_projects_allocation_threshold_getter( + epoch_number, + s1, + projects_contracts, + get_projects_allocation_threshold_settings(), + ) + estimated_matched_rewards = await get_matched_rewards_estimator( + epoch_number, + s2, + epochs_subgraph, + get_matched_rewards_estimator_settings(), + ) + estimated_project_rewards = await get_project_rewards_estimator( + epoch_number, + s3, + projects_contracts, + estimated_matched_rewards, + ) + + # Yield the dependencies to the on_connect handler + yield (s4, threshold_getter, estimated_project_rewards) + + except Exception as e: + await cleanup_sessions(s1, s2, s3, s4) + raise e + + +@asynccontextmanager +async def create_dependencies_on_allocate() -> AsyncGenerator[ + Tuple[ + AsyncSession, + Allocator, + ProjectsAllocationThresholdGetter, + ProjectRewardsEstimator, + ], + None, +]: + """ + Create and return all service dependencies. + """ + + w3 = get_w3(get_web3_provider_settings()) + epochs_contracts = get_epochs_contracts(w3, get_epochs_settings()) + + # We do not handle requests outside of pending epoch state (Allocation Window) + # This will raise an exception if the allocation window is closed and connection does not happen + epoch_number = await get_open_allocation_window_epoch_number(epochs_contracts) + + projects_contracts = get_projects_contracts(w3, get_projects_settings()) + epochs_subgraph = get_epochs_subgraph(get_epochs_subgraph_settings()) + + # For safety, we create separate sessions for each dependency + # (to avoid any potential issues with session sharing in async task context) + sessionmaker = get_sessionmaker(get_database_settings()) + + async with ( + sessionmaker() as s1, + sessionmaker() as s2, + sessionmaker() as s3, + sessionmaker() as s4, + sessionmaker() as s5, + sessionmaker() as s6, + sessionmaker() as s7, + ): + try: + threshold_getter = get_projects_allocation_threshold_getter( + epoch_number, + s1, + projects_contracts, + get_projects_allocation_threshold_settings(), + ) + estimated_matched_rewards = await get_matched_rewards_estimator( + epoch_number, + s2, + epochs_subgraph, + get_matched_rewards_estimator_settings(), + ) + estimated_project_rewards = await get_project_rewards_estimator( + epoch_number, + s3, + projects_contracts, + estimated_matched_rewards, + ) + + signature_verifier = get_signature_verifier( + s4, + epochs_subgraph, + projects_contracts, + get_chain_settings(), + ) + + uq_score_getter = get_uq_score_getter( + s5, get_uq_score_settings(), get_chain_settings() + ) + + allocations = await get_allocator( + epoch_number, + s6, + signature_verifier, + uq_score_getter, + projects_contracts, + estimated_matched_rewards, + ) + + # Yield the dependencies to the on_allocate handler + yield ( + s7, + allocations, + threshold_getter, + estimated_project_rewards, + ) + + except Exception as e: + await cleanup_sessions(s1, s2, s3, s4, s5, s6, s7) + raise e + + +class AllocateNamespace(socketio.AsyncNamespace): + async def handle_on_connect(self, sid: str, environ: dict): + async with create_dependencies_on_connect() as ( + session, + threshold_getter, + estimated_project_rewards, + ): + logging.debug("Client connected") + + # Get the allocation threshold and send it to the client + allocation_threshold = await threshold_getter.get() + + await self.emit( + "threshold", {"threshold": str(allocation_threshold)}, to=sid + ) + + # Get the estimated project rewards and send them to the client + project_rewards = await estimated_project_rewards.get() + + await self.emit( + "project_rewards", + [ + p.model_dump(by_alias=True) + for p in project_rewards.project_fundings.values() + ], + to=sid, + ) + + for project_address in project_rewards.project_fundings: + donations = await get_donations_by_project( + session=session, + project_address=project_address, + epoch_number=estimated_project_rewards.epoch_number, + ) + + await self.emit( + "project_donors", + { + "project": project_address, + "donors": [ + { + "address": d.donor_address, + "amount": str(d.amount), + } + for d in donations + ], + }, + ) + + async def on_connect(self, sid: str, environ: dict): + try: + await self.handle_on_connect(sid, environ) + + except AllocationWindowClosed: + logging.info("Allocation window is closed, connection not established") + + except OctantException as e: + logging.error(f"OctantException({e.__class__.__name__}): {e}") + + except Exception as e: + logging.error(f"Error handling on_connect ({e.__class__.__name__}): {e}") + + async def on_disconnect(self, sid): + logging.debug("Client disconnected") + + async def handle_on_allocate(self, sid: str, data: str): + async with create_dependencies_on_allocate() as ( + session, + allocations, + threshold_getter, + estimated_project_rewards, + ): + request = from_dict(data) + + await allocations.handle(request) + + logging.debug("Allocation request handled") + + # Get the allocation threshold and send it to the client + allocation_threshold = await threshold_getter.get() + + await self.emit( + "threshold", {"threshold": str(allocation_threshold)}, to=sid + ) + + # Get the estimated project rewards and send them to the client + project_rewards = await estimated_project_rewards.get() + await self.emit( + "project_rewards", + [ + p.model_dump(by_alias=True) + for p in project_rewards.project_fundings.values() + ], + to=sid, + ) + + for project_address in project_rewards.project_fundings: + donations = await get_donations_by_project( + session=session, + project_address=project_address, + epoch_number=estimated_project_rewards.epoch_number, + ) + + await self.emit( + "project_donors", + { + "project": project_address, + "donors": [ + { + "address": d.donor_address, + "amount": str(d.amount), + } + for d in donations + ], + }, + ) + + async def on_allocate(self, sid: str, data: str): + try: + await self.handle_on_allocate(sid, data) + + except AllocationWindowClosed: + logging.info("Allocation window is closed, connection not established") + + except OctantException as e: + logging.error(f"OctantException({e.__class__.__name__}): {e.message}") + + except Exception as e: + logging.error(f"Error handling on_allocate ({e.__class__.__name__}): {e}") + + +def from_dict(data: str) -> UserAllocationRequest: + """ + Example of data: + { + "userAddress": "0x123", + "payload": { + "allocations": [ + { + "proposalAddress": "0x456", + "amount": 100 + }, + { + "proposalAddress": "0x789", + "amount": 200 + } + ], + "nonce": 1, + "signature": "0xabc" + }, + "isManuallyEdited": False + } + """ + + # TODO: maybe we can switcht to UserAllocationRequest from V1 ? + # parse the incoming data as UserAllocationRequestV1 + requestV1 = UserAllocationRequestV1.model_validate_json(data) + request = UserAllocationRequest( + userAddress=requestV1.user_address, + allocations=requestV1.payload.allocations, + nonce=requestV1.payload.nonce, + signature=requestV1.signature, + isManuallyEdited=requestV1.is_manually_edited, + ) + return request + + +async def safe_session_cleanup(session): + try: + await session.rollback() + except Exception: + # Log the rollback error, but don't raise it + logging.exception("Error during session rollback") + finally: + try: + await session.close() + except Exception: + # Log the close error, but don't raise it + logging.exception("Error during session close") + + +async def cleanup_sessions(*sessions): + await asyncio.gather(*(safe_session_cleanup(s) for s in sessions)) diff --git a/backend/v2/allocations/validators.py b/backend/v2/allocations/validators.py new file mode 100644 index 0000000000..3311b4c555 --- /dev/null +++ b/backend/v2/allocations/validators.py @@ -0,0 +1,252 @@ +import asyncio +from dataclasses import dataclass + +from app import exceptions +from app.modules.common.crypto.signature import EncodingStandardFor, encode_for_signing +from sqlalchemy.ext.asyncio import AsyncSession +from v2.allocations.repositories import get_last_allocation_request_nonce +from v2.allocations.schemas import UserAllocationRequest +from v2.core.types import Address +from v2.crypto.signatures import verify_signed_message +from v2.epochs.subgraphs import EpochsSubgraph +from v2.projects.contracts import ProjectsContracts +from v2.user_patron_mode.repositories import ( + get_budget_by_user_address_and_epoch, + user_is_patron_with_budget, +) +from web3 import AsyncWeb3 + + +@dataclass +class SignatureVerifier: + session: AsyncSession + epochs_subgraph: EpochsSubgraph + projects_contracts: ProjectsContracts + chain_id: int + + async def verify(self, epoch_number: int, request: UserAllocationRequest) -> None: + await asyncio.gather( + verify_logic( + session=self.session, + epoch_subgraph=self.epochs_subgraph, + projects_contracts=self.projects_contracts, + epoch_number=epoch_number, + payload=request, + ), + verify_signature( + w3=self.projects_contracts.w3, + chain_id=self.chain_id, + user_address=request.user_address, + payload=request, + ), + ) + + +async def verify_logic( + # Component dependencies + session: AsyncSession, + epoch_subgraph: EpochsSubgraph, + projects_contracts: ProjectsContracts, + # Arguments + epoch_number: int, + payload: UserAllocationRequest, +): + # Check if the epoch is in the decision window + # epoch_details = await epoch_subgraph.get_epoch_by_number(epoch_number) + # if epoch_details.state != "PENDING": + # raise exceptions.NotInDecision + + # Check if the allocations are not empty + if not payload.allocations: + raise exceptions.EmptyAllocations() + + async def _check_database(): + await _provided_nonce_matches_expected( + session, payload.user_address, payload.nonce + ) + await _user_is_not_patron( + session, epoch_subgraph, payload.user_address, epoch_number + ) + await _user_has_budget(session, payload, epoch_number) + + await asyncio.gather( + _check_database(), + _provided_projects_are_correct(projects_contracts, epoch_number, payload), + ) + + +async def _provided_nonce_matches_expected( + # Component dependencies + session: AsyncSession, + # Arguments + user_address: Address, + nonce: int, +) -> None: + """ + Check if the nonce is as expected. + """ + # Get the next nonce + next_nonce = await get_next_user_nonce(session, user_address) + + # Check if the nonce is as expected + if nonce != next_nonce: + raise exceptions.WrongAllocationsNonce(nonce, next_nonce) + + +async def _user_is_not_patron( + # Component dependencies + session: AsyncSession, + epoch_subgraph: EpochsSubgraph, + # Arguments + user_address: Address, + epoch_number: int, +) -> None: + """ + Check if the user is not a patron. + """ + # Check if the user is not a patron + epoch_details = await epoch_subgraph.get_epoch_by_number(epoch_number) + is_patron = await user_is_patron_with_budget( + session, + user_address, + epoch_number, + epoch_details.finalized_timestamp.datetime(), + ) + if is_patron: + raise exceptions.NotAllowedInPatronMode(user_address) + + +async def get_next_user_nonce( + # Component dependencies + session: AsyncSession, + # Arguments + user_address: Address, +) -> int: + """ + Get the next expected nonce for the user. + It's a simple increment of the last nonce, or 0 if there is no previous nonce. + """ + # Get the last allocation request of the user + last_allocation_request = await get_last_allocation_request_nonce( + session, user_address + ) + + # Calculate the next nonce + if last_allocation_request is None: + return 0 + + # Increment the last nonce + return last_allocation_request + 1 + + +async def _provided_projects_are_correct( + # Component dependencies + projects_contracts: ProjectsContracts, + # Arguments + epoch_number: int, + payload: UserAllocationRequest, +) -> None: + """ + Check if the projects in the allocation request are correct. + """ + + # Check if the user is not a project + all_projects = await projects_contracts.get_project_addresses(epoch_number) + if payload.user_address in all_projects: + raise exceptions.ProjectAllocationToSelf() + + project_addresses = [a.project_address for a in payload.allocations] + + # Check if the projects are valid + invalid_projects = set(project_addresses) - set(all_projects) + if invalid_projects: + raise exceptions.InvalidProjects(invalid_projects) + + # Check if there are no duplicates + duplicates = [p for p in project_addresses if project_addresses.count(p) > 1] + if duplicates: + raise exceptions.DuplicatedProjects(duplicates) + + +async def _user_has_budget( + # Component dependencies + session: AsyncSession, + # Arguments + payload: UserAllocationRequest, + epoch_number: int, +) -> None: + """ + Check if the user has enough budget for the allocation. + Check if the sum of the allocations is within the user's budget. + """ + + # Get the user's budget + user_budget = await get_budget_by_user_address_and_epoch( + session, payload.user_address, epoch_number + ) + + if user_budget is None: + raise exceptions.BudgetNotFound(payload.user_address, epoch_number) + + # Check if the allocations are within the budget + if sum(a.amount for a in payload.allocations) > user_budget: + raise exceptions.RewardsBudgetExceeded() + + +async def verify_signature( + w3: AsyncWeb3, chain_id: int, user_address: Address, payload: UserAllocationRequest +) -> None: + eip712_encoded = build_allocations_eip712_structure(chain_id, payload) + encoded_msg = encode_for_signing(EncodingStandardFor.DATA, eip712_encoded) + + # Verify the signature + is_valid = await verify_signed_message( + w3, user_address, encoded_msg, payload.signature + ) + if not is_valid: + raise exceptions.InvalidSignature(user_address, payload.signature) + + +def build_allocations_eip712_structure(chain_id: int, payload: UserAllocationRequest): + message = {} + message["allocations"] = [ + {"proposalAddress": a.project_address, "amount": a.amount} + for a in payload.allocations + ] + message["nonce"] = payload.nonce # type: ignore + return build_allocations_eip712_data(chain_id, message) + + +def build_allocations_eip712_data(chain_id: int, message: dict) -> dict: + # Convert amount value to int + message["allocations"] = [ + {**allocation, "amount": int(allocation["amount"])} + for allocation in message["allocations"] + ] + + allocation_types = { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + ], + "Allocation": [ + {"name": "proposalAddress", "type": "address"}, + {"name": "amount", "type": "uint256"}, + ], + "AllocationPayload": [ + {"name": "allocations", "type": "Allocation[]"}, + {"name": "nonce", "type": "uint256"}, + ], + } + + return { + "types": allocation_types, + "domain": { + "name": "Octant", + "version": "1.0.0", + "chainId": chain_id, + }, + "primaryType": "AllocationPayload", + "message": message, + } diff --git a/backend/v2/core/__init__.py b/backend/v2/core/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/core/contracts.py b/backend/v2/core/contracts.py new file mode 100644 index 0000000000..e73c01d134 --- /dev/null +++ b/backend/v2/core/contracts.py @@ -0,0 +1,11 @@ +from eth_typing import ChecksumAddress +from web3 import AsyncWeb3 +from web3.contract import AsyncContract +from web3.types import ABI + + +class SmartContract: + def __init__(self, w3: AsyncWeb3, abi: ABI, address: ChecksumAddress) -> None: + self.abi = abi + self.w3 = w3 + self.contract: AsyncContract = w3.eth.contract(address=address, abi=abi) diff --git a/backend/v2/core/dependencies.py b/backend/v2/core/dependencies.py new file mode 100644 index 0000000000..3fbd366444 --- /dev/null +++ b/backend/v2/core/dependencies.py @@ -0,0 +1,146 @@ +from functools import lru_cache +from typing import Annotated, AsyncGenerator + +from fastapi import Depends +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict +from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine +from web3 import AsyncHTTPProvider, AsyncWeb3 +from web3.middleware import async_geth_poa_middleware + + +class OctantSettings(BaseSettings): + model_config = SettingsConfigDict(env_file=".env", extra="ignore", frozen=True) + + +class Web3ProviderSettings(OctantSettings): + eth_rpc_provider_url: str + + +def get_web3_provider_settings() -> Web3ProviderSettings: + return Web3ProviderSettings() # type: ignore[call-arg] + + +def get_w3( + settings: Annotated[Web3ProviderSettings, Depends(get_web3_provider_settings)] +) -> AsyncWeb3: + w3 = AsyncWeb3(provider=AsyncHTTPProvider(settings.eth_rpc_provider_url)) + if async_geth_poa_middleware not in w3.middleware_onion: + w3.middleware_onion.inject(async_geth_poa_middleware, layer=0) + + return w3 + + +Web3 = Annotated[AsyncWeb3, Depends(get_w3)] + + +class DatabaseSettings(OctantSettings): + db_uri: str = Field(..., alias="db_uri") + # TODO other settings of the database + + @property + def sqlalchemy_database_uri(self) -> str: + return self.db_uri.replace("postgresql://", "postgresql+asyncpg://") + + +def get_database_settings() -> DatabaseSettings: + return DatabaseSettings() # type: ignore[call-arg] + + +@lru_cache(1) +def get_sessionmaker( + settings: Annotated[DatabaseSettings, Depends(get_database_settings)] +) -> async_sessionmaker[AsyncSession]: + engine = create_async_engine( + settings.sqlalchemy_database_uri, + echo=False, # Disable SQL query logging (for performance) + pool_size=100, # Initial pool size (default is 5) + max_overflow=10, # Extra connections if pool is exhausted + pool_timeout=30, # Timeout before giving up on a connection + pool_recycle=3600, # Recycle connections after 1 hour (for long-lived connections) + pool_pre_ping=True, # Check if the connection is alive before using it + future=True, # Use the future-facing SQLAlchemy 2.0 style + # connect_args={"options": "-c timezone=utc"} # Ensures timezone is UTC + ) + + sessionmaker = async_sessionmaker( + autocommit=False, autoflush=False, bind=engine, class_=AsyncSession + ) + + return sessionmaker + + +# @asynccontextmanager +async def get_db_session( + sessionmaker: Annotated[async_sessionmaker[AsyncSession], Depends(get_sessionmaker)] +) -> AsyncGenerator[AsyncSession, None]: + # Create an async SQLAlchemy engine + + # logging.error("Creating database engine") + + # engine = create_async_engine( + # settings.sqlalchemy_database_uri, + # echo=False, # Disable SQL query logging (for performance) + # pool_size=20, # Initial pool size (default is 5) + # max_overflow=10, # Extra connections if pool is exhausted + # pool_timeout=30, # Timeout before giving up on a connection + # pool_recycle=3600, # Recycle connections after 1 hour (for long-lived connections) + # pool_pre_ping=True, # Check if the connection is alive before using it + # future=True, # Use the future-facing SQLAlchemy 2.0 style + # # connect_args={"options": "-c timezone=utc"} # Ensures timezone is UTC + # ) + + # # Create a sessionmaker with AsyncSession class + # async_session = async_sessionmaker( + # autocommit=False, autoflush=False, bind=engine, class_=AsyncSession + # ) + + # logging.error("Opening session", async_session) + + # scoped_session = async_scoped_session(sessionmaker, scopefunc=current_task) + + # Create a new session + async with sessionmaker() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + + +GetSession = Annotated[AsyncSession, Depends(get_db_session, use_cache=False)] + + +class ChainSettings(OctantSettings): + chain_id: int = Field( + default=11155111, + description="The chain id to use for the signature verification.", + ) + + +def get_chain_settings() -> ChainSettings: + return ChainSettings() + + +GetChainSettings = Annotated[ChainSettings, Depends(get_chain_settings)] + + +class SocketioSettings(OctantSettings): + host: str = Field(..., alias="SOCKETIO_REDIS_HOST") + port: int = Field(..., alias="SOCKETIO_REDIS_PORT") + password: str = Field(..., alias="SOCKETIO_REDIS_PASSWORD") + db: int = Field(..., alias="SOCKETIO_REDIS_DB") + + @property + def url(self) -> str: + return f"redis://:{self.password}@{self.host}:{self.port}/{self.db}" + + +def get_socketio_settings() -> SocketioSettings: + return SocketioSettings() # type: ignore[call-arg] + + +GetSocketioSettings = Annotated[SocketioSettings, Depends(get_socketio_settings)] diff --git a/backend/v2/core/exceptions.py b/backend/v2/core/exceptions.py new file mode 100644 index 0000000000..cf58d0c51a --- /dev/null +++ b/backend/v2/core/exceptions.py @@ -0,0 +1,9 @@ +from app.exceptions import OctantException + + +class AllocationWindowClosed(OctantException): + code = 403 # Forbidden + description = "This action is available only during the allocation window." + + def __init__(self): + super().__init__(self.description, self.code) diff --git a/backend/v2/core/types.py b/backend/v2/core/types.py new file mode 100644 index 0000000000..83ae158eb3 --- /dev/null +++ b/backend/v2/core/types.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from eth_utils import to_checksum_address +from pydantic import BaseModel, ConfigDict +from pydantic.alias_generators import to_camel +from pydantic.functional_serializers import WrapSerializer +from pydantic.functional_validators import AfterValidator + + +class OctantModel(BaseModel): + model_config = ConfigDict(frozen=True, alias_generator=to_camel) + + +# Address is a checksummed Ethereum address. +Address = Annotated[str, AfterValidator(to_checksum_address)] + +BigInteger = Annotated[ + int, AfterValidator(int), WrapSerializer(lambda x, y, z: str(x), str) +] diff --git a/backend/v2/crypto/__init__.py b/backend/v2/crypto/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/crypto/contracts.py b/backend/v2/crypto/contracts.py new file mode 100644 index 0000000000..101d1584b4 --- /dev/null +++ b/backend/v2/crypto/contracts.py @@ -0,0 +1,40 @@ +import logging + +from app.constants import EIP1271_MAGIC_VALUE_BYTES +from v2.core.contracts import SmartContract + + +class GnosisSafeContracts(SmartContract): + async def is_valid_signature(self, msg_hash: str, signature: str) -> bool: + logging.info( + f"[Gnosis Safe Contract] checking if a message with hash: {msg_hash} is already signed by {self.contract.address}" + ) + + result = await self.contract.functions.isValidSignature( + msg_hash, signature + ).call() + return result == bytes.fromhex(EIP1271_MAGIC_VALUE_BYTES) + + async def get_message_hash(self, message: bytes) -> str: + return await self.contract.functions.getMessageHash(message).call() + + +GNOSIS_SAFE = [ + { + "inputs": [ + {"internalType": "bytes", "name": "_data", "type": "bytes"}, + {"internalType": "bytes", "name": "_signature", "type": "bytes"}, + ], + "name": "isValidSignature", + "outputs": [{"internalType": "bytes4", "name": "", "type": "bytes4"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "bytes", "name": "message", "type": "bytes"}], + "name": "getMessageHash", + "outputs": [{"internalType": "bytes32", "name": "", "type": "bytes32"}], + "stateMutability": "view", + "type": "function", + }, +] diff --git a/backend/v2/crypto/signatures.py b/backend/v2/crypto/signatures.py new file mode 100644 index 0000000000..372cb22390 --- /dev/null +++ b/backend/v2/crypto/signatures.py @@ -0,0 +1,64 @@ +from eth_account import Account +from eth_account.messages import SignableMessage, _hash_eip191_message +from eth_keys.exceptions import BadSignature +from eth_utils import to_checksum_address +from v2.core.types import Address +from v2.crypto.contracts import GNOSIS_SAFE, GnosisSafeContracts +from web3 import AsyncWeb3 +from web3.exceptions import ContractLogicError + + +async def verify_signed_message( + w3: AsyncWeb3, + user_address: Address, + encoded_msg: SignableMessage, + signature: str, +) -> bool: + contract = await is_contract(w3, user_address) + if contract: + return await _verify_multisig(w3, user_address, encoded_msg, signature) + + return _verify_eoa(user_address, encoded_msg, signature) + + +async def is_contract(w3: AsyncWeb3, address: str) -> bool: + """ + Check if the given address is a contract. + + Args: + - address (str): Ethereum address to check. + """ + address = to_checksum_address(address) + is_address = w3.is_address(address) + + if not is_address: + raise ValueError(f"{address} is not a valid Ethereum address!") + + code = await w3.eth.get_code(address) + + return code.hex() != "0x" + + +def hash_signable_message(encoded_msg: SignableMessage) -> str: + return "0x" + _hash_eip191_message(encoded_msg).hex() + + +async def _verify_multisig( + w3: AsyncWeb3, user_address: Address, encoded_msg: SignableMessage, signature: str +) -> bool: + msg_hash = hash_signable_message(encoded_msg) + try: + gnosis_safe = GnosisSafeContracts(w3=w3, abi=GNOSIS_SAFE, address=user_address) # type: ignore[arg-type] + return await gnosis_safe.is_valid_signature(msg_hash, signature) + except ContractLogicError: + return False + + +def _verify_eoa( + user_address: Address, encoded_msg: SignableMessage, signature: str +) -> bool: + try: + recovered_address = Account.recover_message(encoded_msg, signature=signature) + except BadSignature: + return False + return recovered_address == user_address diff --git a/backend/v2/deposits/__init__.py b/backend/v2/deposits/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/deposits/contracts.py b/backend/v2/deposits/contracts.py new file mode 100644 index 0000000000..664eb8492b --- /dev/null +++ b/backend/v2/deposits/contracts.py @@ -0,0 +1,39 @@ +from typing import Protocol + +from v2.core.contracts import SmartContract + + +class AddressKey(Protocol): + address: str + key: str + + +class DepositsContracts(SmartContract): + async def lock(self, account: AddressKey, amount: int): + nonce = await self.w3.eth.get_transaction_count(account.address) + transaction = await self.contract.functions.lock(amount).build_transaction( + {"from": account.address, "nonce": nonce} + ) + signed_tx = self.w3.eth.account.sign_transaction(transaction, account.key) + return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + async def balance_of(self, owner_address: str) -> int: + return await self.contract.functions.deposits(owner_address).call() + + +DEPOSITS_ABI = [ + { + "inputs": [{"internalType": "uint256", "name": "amount", "type": "uint256"}], + "name": "lock", + "outputs": [], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "", "type": "address"}], + "name": "deposits", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, +] diff --git a/backend/v2/deposits/dependencies.py b/backend/v2/deposits/dependencies.py new file mode 100644 index 0000000000..cbe9a36500 --- /dev/null +++ b/backend/v2/deposits/dependencies.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends +from v2.core.dependencies import OctantSettings, Web3 +from v2.deposits.contracts import DEPOSITS_ABI, DepositsContracts + + +class DepositsSettings(OctantSettings): + deposits_contract_address: str + + +def get_deposits_settings() -> DepositsSettings: + return DepositsSettings() # type: ignore[call-arg] + + +def get_deposits_contracts( + w3: Web3, settings: Annotated[DepositsSettings, Depends(get_deposits_settings)] +) -> DepositsContracts: + return DepositsContracts(w3, DEPOSITS_ABI, settings.deposits_contract_address) # type: ignore[arg-type] diff --git a/backend/v2/epoch_snapshots/__init__.py b/backend/v2/epoch_snapshots/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/epoch_snapshots/repositories.py b/backend/v2/epoch_snapshots/repositories.py new file mode 100644 index 0000000000..b1e3117d90 --- /dev/null +++ b/backend/v2/epoch_snapshots/repositories.py @@ -0,0 +1,12 @@ +from app.infrastructure.database.models import PendingEpochSnapshot +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession + + +async def get_pending_epoch_snapshot( + session: AsyncSession, epoch_number: int +) -> PendingEpochSnapshot | None: + result = await session.execute( + select(PendingEpochSnapshot).filter(PendingEpochSnapshot.epoch == epoch_number) + ) + return result.scalar_one_or_none() diff --git a/backend/v2/epochs/__init__.py b/backend/v2/epochs/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/epochs/contracts.py b/backend/v2/epochs/contracts.py new file mode 100644 index 0000000000..58d919369f --- /dev/null +++ b/backend/v2/epochs/contracts.py @@ -0,0 +1,151 @@ +import logging +from typing import Dict, Optional + +from v2.core.contracts import SmartContract +from web3 import exceptions + + +class EpochsContracts(SmartContract): + async def is_decision_window_open(self) -> bool: + logging.debug("[Epochs contract] Checking if decision window is open") + return await self.contract.functions.isDecisionWindowOpen().call() + + async def get_decision_window(self) -> bool: + logging.debug("[Epochs contract] Checking decision window length") + return await self.contract.functions.getDecisionWindow().call() + + async def get_current_epoch(self) -> int: + try: + logging.debug("[Epochs contract] Getting current epoch") + return await self.contract.functions.getCurrentEpoch().call() + except exceptions.ContractLogicError: + logging.warning("[Epochs contract] Current epoch not started yet") + # HN:Epochs/not-started-yet + return 0 + + async def get_pending_epoch(self) -> Optional[int]: + try: + logging.debug("[Epochs contract] Getting pending epoch") + return await self.contract.functions.getPendingEpoch().call() + except exceptions.ContractLogicError: + logging.warning("[Epochs contract] No pending epoch") + # HN:Epochs/not-pending + return None + + async def get_finalized_epoch(self) -> int: + try: + logging.debug("[Epochs contract] Getting finalized epoch") + return await self.contract.functions.getFinalizedEpoch().call() + except exceptions.ContractLogicError: + logging.warning("[Epochs contract] No finalized epoch") + # HN:Epochs/not-finalized + return 0 + + async def get_current_epoch_end(self) -> int: + logging.debug("[Epochs contract] Checking when current epoch ends") + return await self.contract.functions.getCurrentEpochEnd().call() + + async def get_epoch_duration(self) -> int: + logging.debug("[Epochs contract] Checking epoch duration") + return await self.contract.functions.getEpochDuration().call() + + async def get_future_epoch_props(self) -> Dict: + logging.debug("[Epochs contract] Getting epoch props index") + index = await self.contract.functions.epochPropsIndex().call() + logging.debug("[Epochs contract] Getting next epoch props") + return await self.contract.functions.epochProps(index).call() + + async def is_started(self) -> bool: + logging.debug("[Epochs contract] Checking if first epoch has started") + return await self.contract.functions.isStarted().call() + + async def start(self) -> int: + logging.debug("[Epochs contract] Checking when first epochs starts") + return await self.contract.functions.start().call() + + +EPOCHS_ABI = [ + { + "inputs": [], + "name": "getCurrentEpoch", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getCurrentEpochEnd", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getPendingEpoch", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getFinalizedEpoch", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getEpochDuration", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "getDecisionWindow", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "isDecisionWindowOpen", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "name": "epochProps", + "outputs": [ + {"internalType": "uint32", "name": "from", "type": "uint32"}, + {"internalType": "uint32", "name": "to", "type": "uint32"}, + {"internalType": "uint64", "name": "fromTs", "type": "uint64"}, + {"internalType": "uint64", "name": "duration", "type": "uint64"}, + {"internalType": "uint64", "name": "decisionWindow", "type": "uint64"}, + ], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "epochPropsIndex", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "isStarted", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "start", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, +] diff --git a/backend/v2/epochs/dependencies.py b/backend/v2/epochs/dependencies.py new file mode 100644 index 0000000000..3315a7e386 --- /dev/null +++ b/backend/v2/epochs/dependencies.py @@ -0,0 +1,67 @@ +from typing import Annotated + +from fastapi import Depends +from v2.core.dependencies import OctantSettings, Web3 +from v2.core.exceptions import AllocationWindowClosed +from v2.epochs.contracts import EPOCHS_ABI, EpochsContracts +from v2.epochs.subgraphs import EpochsSubgraph + + +class EpochsSettings(OctantSettings): + epochs_contract_address: str + + +def get_epochs_settings() -> EpochsSettings: + return EpochsSettings() # type: ignore[call-arg] + + +def get_epochs_contracts( + w3: Web3, settings: Annotated[EpochsSettings, Depends(get_epochs_settings)] +) -> EpochsContracts: + return EpochsContracts(w3, EPOCHS_ABI, settings.epochs_contract_address) # type: ignore[arg-type] + + +GetEpochsContracts = Annotated[ + EpochsContracts, + Depends(get_epochs_contracts), +] + + +async def get_open_allocation_window_epoch_number( + epochs_contracts: GetEpochsContracts, +) -> int: + """Returns the current epoch number only if the allocation window is open, + otherwise raises AllocationWindowClosed. + """ + + epoch_number = await epochs_contracts.get_pending_epoch() + if epoch_number is None: + raise AllocationWindowClosed() + + return epoch_number + + +GetOpenAllocationWindowEpochNumber = Annotated[ + int, + Depends(get_open_allocation_window_epoch_number), +] + + +class EpochsSubgraphSettings(OctantSettings): + subgraph_endpoint: str + + +def get_epochs_subgraph_settings() -> EpochsSubgraphSettings: + return EpochsSubgraphSettings() # type: ignore[call-arg] + + +def get_epochs_subgraph( + settings: Annotated[EpochsSubgraphSettings, Depends(get_epochs_subgraph_settings)] +) -> EpochsSubgraph: + return EpochsSubgraph(settings.subgraph_endpoint) + + +GetEpochsSubgraph = Annotated[ + EpochsSubgraph, + Depends(get_epochs_subgraph), +] diff --git a/backend/v2/epochs/subgraphs.py b/backend/v2/epochs/subgraphs.py new file mode 100644 index 0000000000..d7f8e6cf72 --- /dev/null +++ b/backend/v2/epochs/subgraphs.py @@ -0,0 +1,136 @@ +import logging +from dataclasses import dataclass +from typing import Callable, Sequence, Type, Union + +import backoff +from app import exceptions +from app.context.epoch.details import EpochDetails +from gql import Client, gql +from gql.transport.aiohttp import AIOHTTPTransport +from gql.transport.exceptions import TransportQueryError + +# def lookup_max_time(): +# return config.SUBGRAPH_RETRY_TIMEOUT_SEC + + +exception_type = TransportQueryError + + +def is_graph_error_permanent(error: TransportQueryError) -> bool: + # TODO: if we differentiate between reasons for the error, + # we can differentiate between transient and permanent ones, + # so we can return True for permanent ones saving + # up to SUBGRAPH_RETRY_TIMEOUT_SEC. + # Look for these prints in logs and find + # "the chain was reorganized while executing the query" line. + logging.debug("going through giveup...") + logging.debug(f"got TransportQueryError.query_id: {error.query_id}") + logging.debug(f"got TransportQueryError.errors: {error.errors}") + logging.debug(f"got TransportQueryError.data: {error.data}") + logging.debug(f"got TransportQueryError.extensions: {error.extensions}") + return False + + +# url = config["SUBGRAPH_ENDPOINT"] + + +@dataclass +class BackoffParams: + exception: Union[Type[Exception], Sequence[Type[Exception]]] + max_time: int + giveup: Callable[[Exception], bool] = lambda e: False + + +class EpochsSubgraph: + def __init__( + self, + url: str, + backoff_params: BackoffParams | None = None, + ): + self.url = url + self.gql_client = Client( + transport=AIOHTTPTransport(url=self.url, timeout=2), + fetch_schema_from_transport=False, + ) + + if backoff_params is not None: + backoff_decorator = backoff.on_exception( + backoff.expo, + backoff_params.exception, + max_time=backoff_params.max_time, + giveup=backoff_params.giveup, + ) + + self.gql_client.execute_async = backoff_decorator( + self.gql_client.execute_async + ) + + async def get_epoch_by_number(self, epoch_number: int) -> EpochDetails: + """Get EpochDetails from the subgraph for a given epoch number.""" + + logging.debug( + f"[Subgraph] Getting epoch properties for epoch number: {epoch_number}" + ) + + # Prepare query and variables + query = gql( + """\ + query GetEpoch($epochNo: Int!) { + epoches(where: {epoch: $epochNo}) { + epoch + fromTs + toTs + duration + decisionWindow + } + } + """ + ) + variables = {"epochNo": epoch_number} + + # Execute query + response = await self.gql_client.execute_async(query, variable_values=variables) + + # Raise exception if no data received + data = response["epoches"] + if not data: + logging.warning( + f"[Subgraph] No epoch properties received for epoch number: {epoch_number}" + ) + raise exceptions.EpochNotIndexed(epoch_number) + + # Parse response and return result + logging.debug(f"[Subgraph] Received epoch properties: {data[0]}") + + epoch_details = data[0] + + return EpochDetails( + epoch_num=epoch_details["epoch"], + start=epoch_details["fromTs"], + duration=epoch_details["duration"], + decision_window=epoch_details["decisionWindow"], + remaining_sec=0, + ) + + +# def get_epochs(): +# query = gql( +# """ +# query { +# epoches(first: 1000) { +# epoch +# fromTs +# toTs +# } +# _meta { +# block { +# number +# } +# } +# } +# """ +# ) + +# app.logger.debug("[Subgraph] Getting list of all epochs") +# data = gql_factory.build().execute(query) +# return data diff --git a/backend/v2/glms/__init__.py b/backend/v2/glms/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/glms/contracts.py b/backend/v2/glms/contracts.py new file mode 100644 index 0000000000..f5e9b26273 --- /dev/null +++ b/backend/v2/glms/contracts.py @@ -0,0 +1,84 @@ +from typing import Protocol + +from v2.core.contracts import SmartContract + + +class AddressKey(Protocol): + address: str + key: str + + +class GLMContracts(SmartContract): + # def glm_fund(self, to_address, nonce): + # transaction = self.contract.functions.transfer( + # to_address, app.config["GLM_WITHDRAWAL_AMOUNT"] + # ).build_transaction({"from": app.config["GLM_SENDER_ADDRESS"], "nonce": nonce}) + # signed_tx = self.w3.eth.account.sign_transaction( + # transaction, app.config["GLM_SENDER_PRIVATE_KEY"] + # ) + # return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + # def transfer(self, sender, receiver: str, amount: int): + # async def transfer(self, sender_address: str, receiver: str, amount: int): + async def transfer( + self, sender: AddressKey, receiver_address: str, amount: int + ) -> None: + nonce = await self.w3.eth.get_transaction_count(sender) + transaction = self.contract.functions.transfer( + receiver_address, amount + ).build_transaction({"from": sender.address, "nonce": nonce}) + signed_tx = self.w3.eth.account.sign_transaction(transaction, sender.key) + await self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + async def approve(self, owner: AddressKey, benefactor_address, wad: int): + print("owner of lock: ", owner) + print("owner address: ", owner.address) + print("owner key: ", owner.key) + print("benefactor of lock: ", benefactor_address) + nonce = await self.w3.eth.get_transaction_count(owner.address) + transaction = await self.contract.functions.approve( + benefactor_address, wad + ).build_transaction({"from": owner.address, "nonce": nonce}) + signed_tx = self.w3.eth.account.sign_transaction(transaction, owner.key) + return self.w3.eth.send_raw_transaction(signed_tx.rawTransaction) + + # def balance_of(self, owner: str) -> int: + # return self.contract.functions.balanceOf(owner).call() + + +ERC20_ABI = [ + { + "inputs": [], + "name": "totalSupply", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [{"internalType": "address", "name": "account", "type": "address"}], + "name": "balanceOf", + "outputs": [{"internalType": "uint256", "name": "", "type": "uint256"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "to", "type": "address"}, + {"internalType": "uint256", "name": "amount", "type": "uint256"}, + ], + "name": "transfer", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + }, + { + "inputs": [ + {"internalType": "address", "name": "usr", "type": "address"}, + {"internalType": "uint256", "name": "wad", "type": "uint256"}, + ], + "name": "approve", + "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], + "stateMutability": "nonpayable", + "type": "function", + }, +] diff --git a/backend/v2/glms/dependencies.py b/backend/v2/glms/dependencies.py new file mode 100644 index 0000000000..44debdc9ce --- /dev/null +++ b/backend/v2/glms/dependencies.py @@ -0,0 +1,19 @@ +from typing import Annotated + +from fastapi import Depends +from v2.core.dependencies import OctantSettings, Web3 +from v2.glms.contracts import ERC20_ABI, GLMContracts + + +class GLMSettings(OctantSettings): + glm_contract_address: str + + +def get_glm_settings() -> GLMSettings: + return GLMSettings() # type: ignore[call-arg] + + +def get_glm_contracts( + w3: Web3, settings: Annotated[GLMSettings, Depends(get_glm_settings)] +) -> GLMContracts: + return GLMContracts(w3, ERC20_ABI, settings.glm_contract_address) # type: ignore[arg-type] diff --git a/backend/v2/main.py b/backend/v2/main.py new file mode 100644 index 0000000000..122bf435cb --- /dev/null +++ b/backend/v2/main.py @@ -0,0 +1,68 @@ +# Create FastAPI app +import logging +import os + +import redis +import socketio +from app.exceptions import OctantException +from fastapi import FastAPI +from fastapi.responses import JSONResponse +from sqlalchemy.exc import SQLAlchemyError +from v2.allocations.router import api as allocations_api +from v2.allocations.socket import AllocateNamespace +from v2.core.dependencies import get_socketio_settings +from v2.project_rewards.router import api as project_rewards_api + +app = FastAPI() + + +@app.exception_handler(OctantException) +async def handle_octant_exception(request, ex: OctantException): + return JSONResponse( + status_code=ex.status_code, + content={"message": ex.message}, + ) + + +@app.exception_handler(SQLAlchemyError) +async def handle_sqlalchemy_exception(request, ex: SQLAlchemyError): + logging.error(f"SQLAlchemyError: {ex}") + return JSONResponse( + status_code=500, + content={"message": "Internal server error"}, + ) + + +def get_socketio_manager() -> socketio.AsyncRedisManager | None: + if os.environ.get("SOCKETIO_MANAGER_TYPE") != "redis": + logging.info("Initializing socketio manager to default in-memory manager") + return None + + settings = get_socketio_settings() + try: + # Attempt to create a Redis connection + redis_client = redis.Redis.from_url(settings.url) + # Test the connection + redis_client.ping() + # If successful, return the AsyncRedisManager + logging.info( + f"Initialized socketio manager to redis://{settings.host}:{settings.port}/{settings.db}" + ) + return socketio.AsyncRedisManager(settings.url) + except Exception as e: + logging.error(f"Failed to establish Redis connection: {str(e)}") + raise + + +mgr = get_socketio_manager() +sio = socketio.AsyncServer( + cors_allowed_origins="*", async_mode="asgi", client_manager=mgr +) +sio.register_namespace(AllocateNamespace("/")) +sio_asgi_app = socketio.ASGIApp(socketio_server=sio, other_asgi_app=app) + +app.add_route("/socket.io/", route=sio_asgi_app) +app.add_websocket_route("/socket.io/", sio_asgi_app) + +app.include_router(allocations_api) +app.include_router(project_rewards_api) diff --git a/backend/v2/matched_rewards/__init__.py b/backend/v2/matched_rewards/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/matched_rewards/dependencies.py b/backend/v2/matched_rewards/dependencies.py new file mode 100644 index 0000000000..cdafa0ed3f --- /dev/null +++ b/backend/v2/matched_rewards/dependencies.py @@ -0,0 +1,53 @@ +from decimal import Decimal +from typing import Annotated + +from fastapi import Depends +from pydantic import Field +from v2.core.dependencies import GetSession, OctantSettings +from v2.epochs.dependencies import ( + GetOpenAllocationWindowEpochNumber, + get_epochs_subgraph, +) +from v2.epochs.subgraphs import EpochsSubgraph +from v2.matched_rewards.services import MatchedRewardsEstimator + + +class MatchedRewardsEstimatorSettings(OctantSettings): + TR_PERCENT: Decimal = Field( + default=Decimal("0.7"), description="The percentage of the TR rewards." + ) + IRE_PERCENT: Decimal = Field( + default=Decimal("0.35"), description="The percentage of the IRE rewards." + ) + MATCHED_REWARDS_PERCENT: Decimal = Field( + default=Decimal("0.35"), description="The percentage of the matched rewards." + ) + + +def get_matched_rewards_estimator_settings() -> MatchedRewardsEstimatorSettings: + return MatchedRewardsEstimatorSettings() + + +async def get_matched_rewards_estimator( + epoch_number: GetOpenAllocationWindowEpochNumber, + session: GetSession, + epochs_subgraph: Annotated[EpochsSubgraph, Depends(get_epochs_subgraph)], + settings: Annotated[ + MatchedRewardsEstimatorSettings, + Depends(get_matched_rewards_estimator_settings), + ], +) -> MatchedRewardsEstimator: + return MatchedRewardsEstimator( + session=session, + epochs_subgraph=epochs_subgraph, + tr_percent=settings.TR_PERCENT, + ire_percent=settings.IRE_PERCENT, + matched_rewards_percent=settings.MATCHED_REWARDS_PERCENT, + epoch_number=epoch_number, + ) + + +GetMatchedRewardsEstimator = Annotated[ + MatchedRewardsEstimator, + Depends(get_matched_rewards_estimator), +] diff --git a/backend/v2/matched_rewards/services.py b/backend/v2/matched_rewards/services.py new file mode 100644 index 0000000000..f26aa50f19 --- /dev/null +++ b/backend/v2/matched_rewards/services.py @@ -0,0 +1,83 @@ +from dataclasses import dataclass +from decimal import Decimal + +from sqlalchemy.ext.asyncio import AsyncSession +from v2.epoch_snapshots.repositories import get_pending_epoch_snapshot +from v2.epochs.subgraphs import EpochsSubgraph +from v2.user_patron_mode.repositories import get_patrons_rewards + + +@dataclass +class MatchedRewardsEstimator: + # Dependencies + session: AsyncSession + epochs_subgraph: EpochsSubgraph + # Parameters + tr_percent: Decimal + ire_percent: Decimal + matched_rewards_percent: Decimal + epoch_number: int + + async def get(self) -> int: + return await get_estimated_project_matched_rewards_pending( + session=self.session, + epochs_subgraph=self.epochs_subgraph, + tr_percent=self.tr_percent, + ire_percent=self.ire_percent, + matched_rewards_percent=self.matched_rewards_percent, + epoch_number=self.epoch_number, + ) + + +async def get_estimated_project_matched_rewards_pending( + # Dependencies + session: AsyncSession, + epochs_subgraph: EpochsSubgraph, + # Settings + tr_percent: Decimal, + ire_percent: Decimal, + matched_rewards_percent: Decimal, + # Arguments + epoch_number: int, +) -> int: + """ + Get the estimated matched rewards for the pending epoch. + """ + + pending_snapshot = await get_pending_epoch_snapshot(session, epoch_number) + if pending_snapshot is None: + raise ValueError(f"No pending snapshot for epoch {epoch_number}") + + epoch_details = await epochs_subgraph.get_epoch_by_number(epoch_number) + patrons_rewards = await get_patrons_rewards( + session, epoch_details.finalized_timestamp.datetime(), epoch_number + ) + + return _calculate_percentage_matched_rewards( + locked_ratio=Decimal(pending_snapshot.locked_ratio), + tr_percent=tr_percent, + ire_percent=ire_percent, + staking_proceeds=int(pending_snapshot.eth_proceeds), + patrons_rewards=patrons_rewards, + matched_rewards_percent=matched_rewards_percent, + ) + + +def _calculate_percentage_matched_rewards( + locked_ratio: Decimal, + tr_percent: Decimal, + ire_percent: Decimal, + staking_proceeds: int, + patrons_rewards: int, + matched_rewards_percent: Decimal, # Config +) -> int: + if locked_ratio > tr_percent: + raise ValueError("Invalid Strategy - locked_ratio > tr_percent") + + if locked_ratio < ire_percent: + return int(matched_rewards_percent * staking_proceeds + patrons_rewards) + + if ire_percent <= locked_ratio < tr_percent: + return int((tr_percent - locked_ratio) * staking_proceeds + patrons_rewards) + + return patrons_rewards diff --git a/backend/v2/project_rewards/__init__.py b/backend/v2/project_rewards/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/project_rewards/capped_quadriatic.py b/backend/v2/project_rewards/capped_quadriatic.py new file mode 100644 index 0000000000..8fa3197c94 --- /dev/null +++ b/backend/v2/project_rewards/capped_quadriatic.py @@ -0,0 +1,198 @@ +from collections import defaultdict +from decimal import Decimal +from math import sqrt +from typing import Dict, NamedTuple + +from v2.allocations.schemas import AllocationWithUserUQScore +from v2.core.types import Address +from v2.project_rewards.schemas import ProjectFundingSummary + + +class CappedQuadriaticFunding(NamedTuple): + project_fundings: dict[Address, ProjectFundingSummary] + amounts_total: Decimal # Sum of all allocation amounts for all projects + matched_total: Decimal # Sum of all matched rewards for all projects + + +MR_FUNDING_CAP_PERCENT = Decimal("0.2") + + +def capped_quadriatic_funding( + allocations: list[AllocationWithUserUQScore], + matched_rewards: int, + project_addresses: list[str], + MR_FUNDING_CAP_PERCENT: Decimal = MR_FUNDING_CAP_PERCENT, +) -> CappedQuadriaticFunding: + """ + Calculate capped quadratic funding based on a list of allocations. + + Args: + allocations (list[AllocationItem]): A list of allocation items, each containing a project address and an amount. + matched_rewards (int): The total amount of matched rewards available for distribution. + project_addresses (list[str] | None, optional): A list of project addresses to consider. If None, all projects in allocations are considered. Defaults to None. + MR_FUNDING_CAP_PERCENT (float, optional): The maximum percentage of matched rewards that any single project can receive. Defaults to MR_FUNDING_CAP_PERCENT. + + Returns: + CappedQuadriaticFunding: A named tuple containing the total and per-project amounts and matched rewards. + """ + + # Group allocations by project + per_project_allocations: Dict[str, list[AllocationWithUserUQScore]] = defaultdict( + list + ) + for allocation in allocations: + per_project_allocations[allocation.project_address].append(allocation) + + # Variables necessary for calculation of quadratic funding + total_qf = Decimal(0) + qf_by_project: Dict[str, Decimal] = {} + + # Aggregate variables for amounts & matched rewards + amount_by_project: Dict[str, Decimal] = { + project_address: Decimal(0) for project_address in project_addresses + } + matched_by_project: Dict[str, Decimal] = { + project_address: Decimal(0) for project_address in project_addresses + } + matched_total = Decimal(0) + amounts_total = Decimal(0) + + # Calculate quadratic funding for each project + for project_address, allocations in per_project_allocations.items(): + qf = ( + sum( + ( + Decimal(sqrt(allocation.user_uq_score * allocation.amount)) + for allocation in allocations + ), + start=Decimal(0), + ) + ** 2 + ) + + total_qf += qf + qf_by_project[project_address] = qf + + # Aggregate amount by project + sum_amount = sum( + (Decimal(allocation.amount) for allocation in allocations), start=Decimal(0) + ) + amount_by_project[project_address] = sum_amount + amounts_total += sum_amount + + # Calculate funding cap + max_matched_reward = matched_rewards * MR_FUNDING_CAP_PERCENT + + # Calculate matched rewards for each project + for project_address, qf in qf_by_project.items(): + # Calculate matched rewards as proportion of quadratic funding + matched = qf / total_qf * matched_rewards if total_qf != 0 else Decimal(0) + + # Apply funding cap + matched_capped = min(matched, max_matched_reward) + + # Update matched rewards and total rewards + matched_by_project[project_address] = matched_capped + matched_total += matched_capped + + project_fundings = { + project_address: ProjectFundingSummary( + address=project_address, + allocated=int(amount_by_project[project_address]), + matched=int(matched_by_project[project_address]), + ) + for project_address in project_addresses + } + + return CappedQuadriaticFunding( + project_fundings=project_fundings, + amounts_total=amounts_total, + matched_total=matched_total, + ) + + +def cqf_calculate_total_leverage(matched_rewards: int, total_allocated: int) -> float: + if total_allocated == 0: + return 0.0 + + return matched_rewards / total_allocated + + +def cqf_calculate_individual_leverage( + new_allocations_amount: int, + project_addresses: list[Address], + before_allocation: CappedQuadriaticFunding, + after_allocation: CappedQuadriaticFunding, +) -> float: + """Calculate the leverage of a user's new allocations in capped quadratic funding. + + This is a ratio of the sum of the absolute differences between the capped matched rewards before and after the user's allocation, to the total amount of the user's new allocations. + """ + + if new_allocations_amount == 0: + return 0.0 + + total_difference = Decimal(0) + for project_address in project_addresses: + if project_address in before_allocation.project_fundings: + before = Decimal( + before_allocation.project_fundings[project_address].matched + ) + else: + before = Decimal(0) + + # before = before_allocation_matched.get(project_address, 0) + after = after_allocation.project_fundings[project_address].matched + # after = after_allocation_matched[project_address] + + difference = abs(before - after) + total_difference += difference + + leverage = total_difference / new_allocations_amount + + return float(leverage) + + +def cqf_simulate_leverage( + existing_allocations: list[AllocationWithUserUQScore], + new_allocations: list[AllocationWithUserUQScore], + matched_rewards: int, + project_addresses: list[str], + MR_FUNDING_CAP_PERCENT: Decimal = MR_FUNDING_CAP_PERCENT, +) -> float: + """Simulate the leverage of a user's new allocations in capped quadratic funding.""" + + if not new_allocations: + raise ValueError("No new allocations provided") + + # Get the user address associated with the allocations + user_address = new_allocations[0].user_address + + # Remove allocations made by this user (as they will be removed in a second) + allocations_without_user = [ + a for a in existing_allocations if a.user_address != user_address + ] + + # Calculate capped quadratic funding before and after the user's allocation + before_allocation = capped_quadriatic_funding( + allocations_without_user, + matched_rewards, + project_addresses, + MR_FUNDING_CAP_PERCENT, + ) + after_allocation = capped_quadriatic_funding( + allocations_without_user + new_allocations, + matched_rewards, + project_addresses, + MR_FUNDING_CAP_PERCENT, + ) + + # Calculate leverage + leverage = cqf_calculate_individual_leverage( + new_allocations_amount=sum(a.amount for a in new_allocations), + project_addresses=[a.project_address for a in new_allocations], + before_allocation=before_allocation, + after_allocation=after_allocation, + ) + + return leverage diff --git a/backend/v2/project_rewards/dependencies.py b/backend/v2/project_rewards/dependencies.py new file mode 100644 index 0000000000..fad325071e --- /dev/null +++ b/backend/v2/project_rewards/dependencies.py @@ -0,0 +1,28 @@ +from typing import Annotated + +from fastapi import Depends +from v2.core.dependencies import GetSession +from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber +from v2.matched_rewards.dependencies import GetMatchedRewardsEstimator +from v2.project_rewards.services import ProjectRewardsEstimator +from v2.projects.dependencies import GetProjectsContracts + + +async def get_project_rewards_estimator( + epoch_number: GetOpenAllocationWindowEpochNumber, + session: GetSession, + projects_contracts: GetProjectsContracts, + estimated_project_matched_rewards: GetMatchedRewardsEstimator, +) -> ProjectRewardsEstimator: + return ProjectRewardsEstimator( + session=session, + projects_contracts=projects_contracts, + matched_rewards_estimator=estimated_project_matched_rewards, + epoch_number=epoch_number, + ) + + +GetProjectRewardsEstimator = Annotated[ + ProjectRewardsEstimator, + Depends(get_project_rewards_estimator), +] diff --git a/backend/v2/project_rewards/router.py b/backend/v2/project_rewards/router.py new file mode 100644 index 0000000000..7a68f8e531 --- /dev/null +++ b/backend/v2/project_rewards/router.py @@ -0,0 +1,22 @@ +from fastapi import APIRouter +from v2.project_rewards.dependencies import GetProjectRewardsEstimator +from v2.project_rewards.schemas import EstimatedProjectRewardsResponse + +api = APIRouter(prefix="/rewards", tags=["Allocations"]) + + +@api.get("/projects/estimated") +async def get_estimated_project_rewards( + project_rewards_estimator: GetProjectRewardsEstimator, +) -> EstimatedProjectRewardsResponse: + """ + Returns foreach project current allocation sum and estimated matched rewards. + + This endpoint is available only for the pending epoch state. + """ + + estimated_funding = await project_rewards_estimator.get() + + return EstimatedProjectRewardsResponse( + rewards=[f for f in estimated_funding.project_fundings.values()] + ) diff --git a/backend/v2/project_rewards/schemas.py b/backend/v2/project_rewards/schemas.py new file mode 100644 index 0000000000..5c53fce910 --- /dev/null +++ b/backend/v2/project_rewards/schemas.py @@ -0,0 +1,46 @@ +from pydantic import Field +from v2.core.types import Address, BigInteger, OctantModel + + +class ProjectFundingSummary(OctantModel): + address: Address = Field(..., description="The address of the project") + allocated: BigInteger = Field( + ..., description="Sum of all allocation amounts for the project" + ) + matched: BigInteger = Field( + ..., description="Sum of matched rewards for the project" + ) + + +class EstimatedProjectRewardsResponse(OctantModel): + rewards: list[ProjectFundingSummary] = Field( + ..., description="List of project funding summaries" + ) + + +# project_rewards = await project_rewards_estimator.get(pending_epoch_number) +# rewards = [ +# { +# "address": project_address, +# "allocated": str(project_rewards.amounts_by_project[project_address]), +# "matched": str(project_rewards.matched_by_project[project_address]), +# } +# for project_address in project_rewards.amounts_by_project.keys() +# ] + +# @ns.doc( +# description="Returns project rewards with estimated matched rewards for the pending epoch" +# ) +# @ns.response( +# 200, +# "", +# ) +# @ns.route("/projects/estimated") +# class EstimatedProjectRewards(OctantResource): +# @ns.marshal_with(projects_rewards_model) +# def get(self): +# app.logger.debug("Getting project rewards for the pending epoch") +# project_rewards = get_estimated_project_rewards().rewards +# app.logger.debug(f"Project rewards in the pending epoch: {project_rewards}") + +# return {"rewards": project_rewards} diff --git a/backend/v2/project_rewards/services.py b/backend/v2/project_rewards/services.py new file mode 100644 index 0000000000..76e6b43734 --- /dev/null +++ b/backend/v2/project_rewards/services.py @@ -0,0 +1,37 @@ +import asyncio +from dataclasses import dataclass + +from sqlalchemy.ext.asyncio import AsyncSession +from v2.allocations.repositories import get_allocations_with_user_uqs +from v2.matched_rewards.services import MatchedRewardsEstimator +from v2.project_rewards.capped_quadriatic import ( + CappedQuadriaticFunding, + capped_quadriatic_funding, +) +from v2.projects.contracts import ProjectsContracts + + +@dataclass +class ProjectRewardsEstimator: + # Dependencies + session: AsyncSession + projects_contracts: ProjectsContracts + matched_rewards_estimator: MatchedRewardsEstimator + + # Parameters + epoch_number: int + + async def get(self) -> CappedQuadriaticFunding: + # Gather all the necessary data for the calculation + all_projects, matched_rewards, allocations = await asyncio.gather( + self.projects_contracts.get_project_addresses(self.epoch_number), + self.matched_rewards_estimator.get(), + get_allocations_with_user_uqs(self.session, self.epoch_number), + ) + + # Calculate using the Capped Quadriatic Funding formula + return capped_quadriatic_funding( + project_addresses=all_projects, + allocations=allocations, + matched_rewards=matched_rewards, + ) diff --git a/backend/v2/projects/__init__.py b/backend/v2/projects/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/projects/contracts.py b/backend/v2/projects/contracts.py new file mode 100644 index 0000000000..e2ae34df55 --- /dev/null +++ b/backend/v2/projects/contracts.py @@ -0,0 +1,33 @@ +import logging + +from v2.core.contracts import SmartContract + + +class ProjectsContracts(SmartContract): + async def get_project_addresses(self, epoch_number: int) -> list[str]: + logging.debug( + f"[Projects contract] Getting project addresses for epoch: {epoch_number}" + ) + return await self.contract.functions.getProposalAddresses(epoch_number).call() + + async def get_project_cid(self): + logging.debug("[Projects contract] Getting projects CID") + return await self.contract.functions.cid().call() + + +PROJECTS_ABI = [ + { + "inputs": [{"internalType": "uint256", "name": "_epoch", "type": "uint256"}], + "name": "getProposalAddresses", + "outputs": [{"internalType": "address[]", "name": "", "type": "address[]"}], + "stateMutability": "view", + "type": "function", + }, + { + "inputs": [], + "name": "cid", + "outputs": [{"internalType": "string", "name": "", "type": "string"}], + "stateMutability": "view", + "type": "function", + }, +] diff --git a/backend/v2/projects/dependencies.py b/backend/v2/projects/dependencies.py new file mode 100644 index 0000000000..3cf6bfef4e --- /dev/null +++ b/backend/v2/projects/dependencies.py @@ -0,0 +1,55 @@ +from typing import Annotated + +from fastapi import Depends +from pydantic import Field +from v2.core.dependencies import GetSession, OctantSettings, Web3 +from v2.epochs.dependencies import GetOpenAllocationWindowEpochNumber +from v2.projects.contracts import PROJECTS_ABI, ProjectsContracts +from v2.projects.services import ProjectsAllocationThresholdGetter + + +class ProjectsSettings(OctantSettings): + projects_contract_address: str = Field( + validation_alias="proposals_contract_address" + ) + + +def get_projects_settings() -> ProjectsSettings: + return ProjectsSettings() # type: ignore[call-arg] + + +def get_projects_contracts( + w3: Web3, settings: Annotated[ProjectsSettings, Depends(get_projects_settings)] +) -> ProjectsContracts: + return ProjectsContracts(w3, PROJECTS_ABI, settings.projects_contract_address) # type: ignore[arg-type] + + +GetProjectsContracts = Annotated[ + ProjectsContracts, + Depends(get_projects_contracts), +] + + +class ProjectsAllocationThresholdSettings(OctantSettings): + project_count_multiplier: int = Field( + default=1, + description="The multiplier to the number of projects to calculate the allocation threshold.", + ) + + +def get_projects_allocation_threshold_settings() -> ProjectsAllocationThresholdSettings: + return ProjectsAllocationThresholdSettings() + + +def get_projects_allocation_threshold_getter( + epoch_number: GetOpenAllocationWindowEpochNumber, + session: GetSession, + projects: GetProjectsContracts, + settings: Annotated[ + ProjectsAllocationThresholdSettings, + Depends(get_projects_allocation_threshold_settings), + ], +) -> ProjectsAllocationThresholdGetter: + return ProjectsAllocationThresholdGetter( + epoch_number, session, projects, settings.project_count_multiplier + ) diff --git a/backend/v2/projects/services.py b/backend/v2/projects/services.py new file mode 100644 index 0000000000..7421f2c30f --- /dev/null +++ b/backend/v2/projects/services.py @@ -0,0 +1,57 @@ +import asyncio +from dataclasses import dataclass + +from sqlalchemy.ext.asyncio import AsyncSession +from v2.allocations.repositories import sum_allocations_by_epoch +from v2.projects.contracts import ProjectsContracts + + +@dataclass +class ProjectsAllocationThresholdGetter: + # Parameters + epoch_number: int + + # Dependencies + session: AsyncSession + projects: ProjectsContracts + project_count_multiplier: int = 1 + + async def get(self) -> int: + return await get_projects_allocation_threshold( + session=self.session, + projects=self.projects, + epoch_number=self.epoch_number, + project_count_multiplier=self.project_count_multiplier, + ) + + +async def get_projects_allocation_threshold( + # Dependencies + session: AsyncSession, + projects: ProjectsContracts, + # Arguments + epoch_number: int, + project_count_multiplier: int = 1, +) -> int: + # PROJECTS_COUNT_MULTIPLIER = 1 # TODO: from settings? + + total_allocated, project_addresses = await asyncio.gather( + sum_allocations_by_epoch(session, epoch_number), + projects.get_project_addresses(epoch_number), + ) + + return _calculate_threshold( + total_allocated, len(project_addresses), project_count_multiplier + ) + + +def _calculate_threshold( + total_allocated: int, + projects_count: int, + project_count_multiplier: int, +) -> int: + return ( + int(total_allocated / (projects_count * project_count_multiplier)) + if projects_count + else 0 + ) diff --git a/backend/v2/uniqueness_quotients/__init__.py b/backend/v2/uniqueness_quotients/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/uniqueness_quotients/dependencies.py b/backend/v2/uniqueness_quotients/dependencies.py new file mode 100644 index 0000000000..a5c110a7e2 --- /dev/null +++ b/backend/v2/uniqueness_quotients/dependencies.py @@ -0,0 +1,68 @@ +from decimal import Decimal +from typing import Annotated + +from app.constants import ( + GUEST_LIST, + TIMEOUT_LIST, + TIMEOUT_LIST_NOT_MAINNET, + UQ_THRESHOLD_MAINNET, + UQ_THRESHOLD_NOT_MAINNET, +) +from app.shared.blockchain_types import ChainTypes +from fastapi import Depends +from pydantic import Field, TypeAdapter +from v2.core.dependencies import GetChainSettings, GetSession, OctantSettings +from v2.core.types import Address +from v2.uniqueness_quotients.services import UQScoreGetter + + +class UQScoreSettings(OctantSettings): + uq_score_threshold: float = Field( + default=15.0, + description="The Gitcoin Passport score threshold above which the UQ score is set to the maximum UQ score.", + ) + low_uq_score: Decimal = Field( + default=Decimal("0.01"), + description="The UQ score to be returned if the Gitcoin Passport score is below the threshold.", + ) + max_uq_score: Decimal = Field( + default=Decimal("1.0"), + description="The UQ score to be returned if the Gitcoin Passport score is above the threshold.", + ) + null_uq_score: Decimal = Field( + default=Decimal("0.0"), + description="The UQ score to be returned if the user is on the timeout list.", + ) + + +def get_uq_score_settings() -> UQScoreSettings: + return UQScoreSettings() + + +def get_uq_score_getter( + session: GetSession, + settings: Annotated[UQScoreSettings, Depends(get_uq_score_settings)], + chain_settings: GetChainSettings, +) -> UQScoreGetter: + # TODO: this should be a much nicer dependency :) + is_mainnet = chain_settings.chain_id == ChainTypes.MAINNET + + uq_threshold = UQ_THRESHOLD_MAINNET if is_mainnet else UQ_THRESHOLD_NOT_MAINNET + timeout_list = TIMEOUT_LIST if is_mainnet else TIMEOUT_LIST_NOT_MAINNET + + address_set_validator = TypeAdapter(set[Address]) + timeout_set = address_set_validator.validate_python(timeout_list) + guest_set = address_set_validator.validate_python(GUEST_LIST) + + return UQScoreGetter( + session=session, + uq_score_threshold=uq_threshold, + max_uq_score=settings.max_uq_score, + low_uq_score=settings.low_uq_score, + null_uq_score=settings.null_uq_score, + guest_list=guest_set, + timeout_list=timeout_set, + ) + + +GetUQScoreGetter = Annotated[UQScoreGetter, Depends(get_uq_score_getter)] diff --git a/backend/v2/uniqueness_quotients/repositories.py b/backend/v2/uniqueness_quotients/repositories.py new file mode 100644 index 0000000000..bb4653b6df --- /dev/null +++ b/backend/v2/uniqueness_quotients/repositories.py @@ -0,0 +1,66 @@ +from decimal import Decimal +from typing import Optional + +from app.infrastructure.database.models import GPStamps, UniquenessQuotient, User +from eth_utils import to_checksum_address +from sqlalchemy import select +from sqlalchemy.ext.asyncio import AsyncSession +from v2.core.types import Address +from v2.users.repositories import get_user_by_address + + +async def get_uq_score_by_user_address( + session: AsyncSession, user_address: Address, epoch_number: int +) -> Optional[Decimal]: + """Returns saved UQ score for a user in a given epoch. + None if the UQ score is not saved (allocation not made yet). + """ + + result = await session.execute( + select(UniquenessQuotient) + .join(User) + .filter(User.address == to_checksum_address(user_address)) + .filter(UniquenessQuotient.epoch == epoch_number) + ) + + uq = result.scalars().first() + return uq.validated_score if uq else None + + +async def save_uq_score_for_user_address( + session: AsyncSession, user_address: Address, epoch_number: int, score: Decimal +): + """Saves UQ score for a user in a given epoch.""" + + user = await get_user_by_address(session, user_address) + + if not user: + return None + + uq_score = UniquenessQuotient( + epoch=epoch_number, + user_id=user.id, + score=str(score), + ) + + session.add(uq_score) + await session.commit() + + +async def get_gp_stamps_by_address( + session: AsyncSession, user_address: Address +) -> GPStamps | None: + """Gets the latest GitcoinPassport Stamps record for a user.""" + + user = await get_user_by_address(session, user_address) + if user is None: + return None + + result = await session.scalar( + select(GPStamps) + .filter(GPStamps.user_id == user.id) + .order_by(GPStamps.created_at.desc()) + .limit(1) + ) + + return result diff --git a/backend/v2/uniqueness_quotients/services.py b/backend/v2/uniqueness_quotients/services.py new file mode 100644 index 0000000000..9f31ad3bda --- /dev/null +++ b/backend/v2/uniqueness_quotients/services.py @@ -0,0 +1,90 @@ +from dataclasses import dataclass +from decimal import Decimal + +from app.modules.user.antisybil.core import ( + _apply_gtc_staking_stamp_nullification, + _has_guest_stamp_applied_by_gp, +) +from eth_utils import to_checksum_address +from sqlalchemy.ext.asyncio import AsyncSession +from v2.core.types import Address +from v2.uniqueness_quotients.repositories import ( + get_gp_stamps_by_address, + get_uq_score_by_user_address, + save_uq_score_for_user_address, +) + + +@dataclass +class UQScoreGetter: + session: AsyncSession + uq_score_threshold: float + max_uq_score: Decimal + low_uq_score: Decimal + null_uq_score: Decimal + guest_list: set[Address] + timeout_list: set[Address] + + async def get_or_calculate( + self, epoch_number: int, user_address: Address + ) -> Decimal: + """Get or calculate the UQ score for a user in a given epoch. + If the UQ score is already calculated, it will be returned. + Otherwise, it will be calculated based on the Gitcoin Passport score and saved for future reference. + """ + + # Check if the UQ score is already calculated and saved + uq_score = await get_uq_score_by_user_address( + self.session, user_address, epoch_number + ) + if uq_score: + return uq_score + + # Otherwise, calculate the UQ score + uq_score = await self._calculate_uq_score(user_address) + + # Save the UQ score for future reference + await save_uq_score_for_user_address( + self.session, user_address, epoch_number, uq_score + ) + + return uq_score + + async def _calculate_uq_score(self, user_address: Address) -> Decimal: + gp_score = await get_gitcoin_passport_score( + self.session, user_address, self.guest_list + ) + + if user_address in self.timeout_list: + return self.null_uq_score + + if gp_score >= self.uq_score_threshold: + return self.max_uq_score + + return self.low_uq_score + + +async def get_gitcoin_passport_score( + session: AsyncSession, user_address: Address, guest_list: set[Address] +) -> float: + """Gets saved Gitcoin Passport score for a user. + Returns None if the score is not saved. + If the user is in the GUEST_LIST, the score will be adjusted to include the guest stamp. + """ + + user_address = to_checksum_address(user_address) + + stamps = await get_gp_stamps_by_address(session, user_address) + + # We have no information about the user's score + if stamps is None: + return 0.0 + + # We remove score associated with GTC staking + potential_score = _apply_gtc_staking_stamp_nullification(stamps.score, stamps) + + # If the user is in the guest list and has not been stamped by a guest list provider, increase the score by 21.0 + if user_address in guest_list and not _has_guest_stamp_applied_by_gp(stamps): + return potential_score + 21.0 + + return potential_score diff --git a/backend/v2/user_patron_mode/__init__.py b/backend/v2/user_patron_mode/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/user_patron_mode/repositories.py b/backend/v2/user_patron_mode/repositories.py new file mode 100644 index 0000000000..903660a5ef --- /dev/null +++ b/backend/v2/user_patron_mode/repositories.py @@ -0,0 +1,107 @@ +from datetime import datetime + +from app.infrastructure.database.models import Budget, PatronModeEvent, User +from sqlalchemy import Numeric, cast, func +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from v2.core.types import Address +from v2.users.repositories import get_user_by_address + + +async def get_all_patrons_at_timestamp( + session: AsyncSession, dt: datetime +) -> list[str]: + """ + Get all the user addresses that at given timestamp have patron_mode_enabled=True. + """ + + results = await session.execute( + select(PatronModeEvent.user_address) + .filter(PatronModeEvent.created_at <= dt) + .group_by(PatronModeEvent.user_address) + .having( + func.max(PatronModeEvent.created_at).filter( + PatronModeEvent.patron_mode_enabled + ) + == func.max(PatronModeEvent.created_at) + ) + ) + + return [row[0] for row in results.all()] + + +async def get_budget_sum_by_users_addresses_and_epoch( + session: AsyncSession, users_addresses: list[str], epoch_number: int +) -> int: + """ + Sum the budgets of given users for a given epoch. + """ + result = await session.execute( + select(func.sum(cast(Budget.budget, Numeric))) + .join(User) + .filter(User.address.in_(users_addresses), Budget.epoch == epoch_number) + ) + total_budget = result.scalar() + + if total_budget is None: + return 0 + + return int(total_budget) + + +async def get_patrons_rewards( + session: AsyncSession, finalized_timestamp: datetime, epoch_number: int +) -> int: + """ + Patron rewards are the sum of budgets of all patrons for a given epoch. + """ + + patrons = await get_all_patrons_at_timestamp(session, finalized_timestamp) + return await get_budget_sum_by_users_addresses_and_epoch( + session, patrons, epoch_number + ) + + +async def get_budget_by_user_address_and_epoch( + session: AsyncSession, user_address: Address, epoch: int +) -> int | None: + """ + Get the budget of a user for a given epoch. + """ + + user = await get_user_by_address(session, user_address) + if user is None: + return None + + result = await session.execute( + select(Budget.budget) + .filter(Budget.user_id == user.id) + .filter(Budget.epoch == epoch) + ) + + budget = result.scalar() + + if budget is None: + return None + + return int(budget) + + +async def user_is_patron_with_budget( + session: AsyncSession, + user_address: Address, + epoch_number: int, + finalized_timestamp: datetime, +) -> bool: + """ + Check if a user is a patron with a budget for a given epoch. + """ + + patrons = await get_all_patrons_at_timestamp(session, finalized_timestamp) + if user_address not in patrons: + return False + + budget = await get_budget_by_user_address_and_epoch( + session, user_address, epoch_number + ) + return budget is not None diff --git a/backend/v2/users/__init__.py b/backend/v2/users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/backend/v2/users/repositories.py b/backend/v2/users/repositories.py new file mode 100644 index 0000000000..f103df0e6c --- /dev/null +++ b/backend/v2/users/repositories.py @@ -0,0 +1,15 @@ +from app.infrastructure.database.models import User +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from v2.core.types import Address + + +async def get_user_by_address( + session: AsyncSession, user_address: Address +) -> User | None: + """Get a user object by their address. Useful for all other operations related to a user.""" + + result = await session.scalar( + select(User).filter(User.address == user_address).limit(1) + ) + return result diff --git a/ci/Dockerfile.contracts-v1 b/ci/Dockerfile.contracts-v1 index e9bca8497e..3da2b887b2 100644 --- a/ci/Dockerfile.contracts-v1 +++ b/ci/Dockerfile.contracts-v1 @@ -1,4 +1,4 @@ -FROM local-docker-registry.wildland.dev:80/library/node:16-alpine AS root +FROM local-docker-registry.wildland.dev:80/library/node:18-alpine AS root WORKDIR /app diff --git a/ci/Dockerfile.multideployer b/ci/Dockerfile.multideployer index cae93cd928..16ed6724fb 100644 --- a/ci/Dockerfile.multideployer +++ b/ci/Dockerfile.multideployer @@ -9,8 +9,10 @@ WORKDIR /app RUN mkdir /hardhat/ COPY --from=hardhat /app/ /hardhat/ -COPY --chmod=+x entrypoint.sh . -COPY --chmod=+x wait_for_subgraph.sh . +COPY entrypoint.sh . +RUN chmod +x ./entrypoint.sh +COPY wait_for_subgraph.sh . +RUN chmod +x ./wait_for_subgraph.sh COPY server.py /app/server.py ENTRYPOINT ["./entrypoint.sh"] diff --git a/ci/argocd/contracts/master.env b/ci/argocd/contracts/master.env index 6d3aa84242..37bbad3a5b 100644 --- a/ci/argocd/contracts/master.env +++ b/ci/argocd/contracts/master.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=6153543 +BLOCK_NUMBER=6922385 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0x738413d47E9670757D662497bEb38B69b26ddC4E -DEPOSITS_CONTRACT_ADDRESS=0x3Ba9caeAc79b784708DfdDF936F2aaAf9CF39884 -EPOCHS_CONTRACT_ADDRESS=0xb918ce1c1966208720C1F0F80767C534D227e164 -PROPOSALS_CONTRACT_ADDRESS=0x5454A1Fa39c16af307FDf0B2E9B3dbB97EcF98fD -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x0f9C752bdB7A4727dD21F44f1CE4dA6413517CcB -VAULT_CONTRACT_ADDRESS=0x7af367B58d851cE54DB86F907c380a0C98102685 +AUTH_CONTRACT_ADDRESS=0x74105046Cdc99C961E749F12A006be91402aC389 +DEPOSITS_CONTRACT_ADDRESS=0x8f73ae171b1399edD1383a18C3cF4c5C8F435721 +EPOCHS_CONTRACT_ADDRESS=0x67e7A37f882653e94C9717e3C63765Aa401E0fC2 +PROPOSALS_CONTRACT_ADDRESS=0xD5a4f7f332688915466403c4e9bfB3cDDbFf539c +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0xF16b96e4707Cdf564e75eCB40E49Ce8BeB6cca47 +VAULT_CONTRACT_ADDRESS=0x04615C996871112BDd45492eF4e7740368c3e71e diff --git a/ci/argocd/contracts/uat.env b/ci/argocd/contracts/uat.env index 984dc43cd4..bdfe11eccc 100644 --- a/ci/argocd/contracts/uat.env +++ b/ci/argocd/contracts/uat.env @@ -1,8 +1,8 @@ -BLOCK_NUMBER=6440834 +BLOCK_NUMBER=6851295 GLM_CONTRACT_ADDRESS=0x71432DD1ae7DB41706ee6a22148446087BdD0906 -AUTH_CONTRACT_ADDRESS=0xab594179cD009616fD183259F3b9d68270faA053 -DEPOSITS_CONTRACT_ADDRESS=0xe6E9332995F0469D33fa6296D0A432bD9fe8Db60 -EPOCHS_CONTRACT_ADDRESS=0x323F991FC3659507Ef5eA9385D72bF31cb468033 -PROPOSALS_CONTRACT_ADDRESS=0xead93af66E5C2228Cd047Cd70891405111fA3548 -WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x077265B94EeF6c40B725c0426331bcc8DBFC7Ee0 -VAULT_CONTRACT_ADDRESS=0x0c6c38FB0e5222727389E6efe0E654bD5b9D4484 +AUTH_CONTRACT_ADDRESS=0xC3312A14BdBF97Aae2320ae3F81D7710326E4766 +DEPOSITS_CONTRACT_ADDRESS=0x9422bbDa8ca726a1C8b3f16C08b9877cBdbD5100 +EPOCHS_CONTRACT_ADDRESS=0xe60942BD4C90B59EdADc222cc6CA8Fb28Ef697D2 +PROPOSALS_CONTRACT_ADDRESS=0xB09737E89102ECC6352BE7ddA42bCF84dcB9A811 +WITHDRAWALS_TARGET_CONTRACT_ADDRESS=0x352AB9226750365E3d6d0256E494CE35110073cC +VAULT_CONTRACT_ADDRESS=0x89da228fE0bFe8D55ee5af86252BAa2Baa06fF80 diff --git a/ci/argocd/templates/octant-application.yaml b/ci/argocd/templates/octant-application.yaml index 8c0809c598..6e39116b4b 100644 --- a/ci/argocd/templates/octant-application.yaml +++ b/ci/argocd/templates/octant-application.yaml @@ -15,7 +15,7 @@ spec: namespace: $DEPLOYMENT_ID sources: - repoURL: 'https://gitlab.com/api/v4/projects/48137258/packages/helm/devel' - targetRevision: 0.2.64 + targetRevision: 0.2.66 chart: octant helm: parameters: @@ -34,7 +34,7 @@ spec: - name: 'webClient.hideCurrentProjectsOutsideAW' value: 'false' - name: 'webClient.ipfsGateways' - value: 'https://ipfs.octant.wildland.dev/ipfs/' + value: '$IPFS_GATEWAYS' ## Graph Node - name: graphNode.graph.env.NETWORK value: '$NETWORK_NAME' diff --git a/client/cypress/e2e/_2makePendingSnapshot.cy.ts b/client/cypress/e2e/_2makePendingSnapshot.cy.ts deleted file mode 100644 index 9f23bc0da3..0000000000 --- a/client/cypress/e2e/_2makePendingSnapshot.cy.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; -import { mutateAsyncMakeSnapshot } from 'cypress/utils/moveTime'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -// In E2E snapshotter is disabled. Before the first test can be run, pending snapshot needs to be done. -describe('Make pending snapshot', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.playground.absolute); - }); - - it('make pending snapshot', () => { - cy.window().then(async win => { - cy.wrap(null).then(() => { - return mutateAsyncMakeSnapshot(win, 'pending').then(str => { - expect(str).to.eq(true); - }); - }); - cy.get('[data-test=SyncView]', { timeout: 60000 }).should('not.exist'); - }); - }); -}); diff --git a/client/cypress/e2e/_3onboardingTOSNotAccepted.cy.ts b/client/cypress/e2e/_3onboardingTOSNotAccepted.cy.ts deleted file mode 100644 index 1bc289b944..0000000000 --- a/client/cypress/e2e/_3onboardingTOSNotAccepted.cy.ts +++ /dev/null @@ -1,103 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - beforeSetup, - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, - checkChangeStepsWithArrowKeys, - checkCurrentElement, - checkProgressStepperSlimIsCurrentAndClickNext, - connectWalletOnboarding, -} from 'cypress/utils/onboarding'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { - getStepsDecisionWindowClosed, - getStepsDecisionWindowOpen, -} from 'src/hooks/helpers/useOnboardingSteps/steps'; - -chai.use(chaiColors); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }, index, arr) => { - describe(`onboarding (TOS not accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - connectWalletOnboarding(); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('onboarding TOS step should be first and active', () => { - checkCurrentElement(0, true); - cy.get('[data-test=ModalOnboardingTOS]').should('be.visible'); - }); - - it('user is not able to click through entire onboarding flow', () => { - cy.window().then(win => { - const isDecisionWindowOpen = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - - const onboardingSteps = isDecisionWindowOpen - ? getStepsDecisionWindowOpen('2', '16 Jan') - : getStepsDecisionWindowClosed('2', '16 Jan'); - - for (let i = 1; i < onboardingSteps.length; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i, i === 1); - } - }); - }); - - it('user is not able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click({ force: true }); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders every time page is refreshed', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('user cannot change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(false); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(false); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(false); - }); - - it('user cannot change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(false); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(false); - }); - - if (index === arr.length - 1) { - it('TOS acceptance changes onboarding step to next step and allows the user to close the modal by clicking button in the top-right', () => { - checkCurrentElement(0, true); - cy.get('[data-test=TOS_InputCheckbox]').check(); - cy.switchToMetamaskNotification(); - cy.confirmMetamaskSignatureRequest(); - checkCurrentElement(1, true); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - } - }); -}); diff --git a/client/cypress/e2e/_4onboardingTOSAccepted.cy.ts b/client/cypress/e2e/_4onboardingTOSAccepted.cy.ts deleted file mode 100644 index 080728357a..0000000000 --- a/client/cypress/e2e/_4onboardingTOSAccepted.cy.ts +++ /dev/null @@ -1,216 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { navigateWithCheck } from 'cypress/utils/e2e'; -import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; -import { - beforeSetup, - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px, - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px, - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px, - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px, - checkChangeStepsWithArrowKeys, - checkProgressStepperSlimIsCurrentAndClickNext, - connectWalletOnboarding, -} from 'cypress/utils/onboarding'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { HAS_ONBOARDING_BEEN_CLOSED, IS_ONBOARDING_DONE } from 'src/constants/localStorageKeys'; -import { - getStepsDecisionWindowClosed, - getStepsDecisionWindowOpen, -} from 'src/hooks/helpers/useOnboardingSteps/steps'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -chai.use(chaiColors); - -describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is open, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.true; - }); - }); - }); -}); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`onboarding (TOS accepted): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - beforeSetup(); - }); - - beforeEach(() => { - cy.clearLocalStorage(); - connectWalletOnboarding(); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('user is able to click through entire onboarding flow', () => { - cy.window().then(win => { - const isDecisionWindowOpen = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - - const onboardingSteps = isDecisionWindowOpen - ? getStepsDecisionWindowOpen('2', '16 Jan') - : getStepsDecisionWindowClosed('2', '16 Jan'); - - for (let i = 1; i < onboardingSteps.length - 1; i++) { - checkProgressStepperSlimIsCurrentAndClickNext(i); - } - - cy.get('[data-test=ModalOnboarding__ProgressStepperSlim__element]') - .eq(onboardingSteps.length - 1) - .click(); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - }); - - it('user is able to close the modal by clicking button in the top-right', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=ProjectsView__ProjectsList]').should('be.visible'); - }); - - it('renders every time page is refreshed when "Always show Allocate onboarding" option is checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check().should('be.checked'); - cy.reload(); - // For the unknown reason reloads sometimes cause app to disconnect in E2E env. - cy.disconnectMetamaskWalletFromAllDapps(); - connectWalletOnboarding(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it('renders only once when "Always show Allocate onboarding" option is not checked', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('user can change steps with arrow keys (left, right)', () => { - checkChangeStepsWithArrowKeys(true); - }); - - it('user can change steps by clicking the edge of the screen (up to 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenUpTo25px(true); - }); - - it('user cannot change steps by clicking the edge of the screen (more than 25px from each edge)', () => { - checkChangeStepsByClickingEdgeOfTheScreenMoreThan25px(true); - }); - - it('user can change steps by swiping on screen (difference more than or equal 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceMoreThanOrEqual5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user cannot change steps by swiping on screen (difference less than 5px)', () => { - checkChangeStepsBySwipingOnScreenDifferenceLessThanl5px(true); - }); - - it('user is able to close the onboarding, and after page reload, onboarding does not show up again', () => { - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - }); - - it('Onboarding stepper is visible after closing onboarding modal without going to the last step', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').should('be.visible'); - }); - - it('Onboarding stepper opens onboarding modal', () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=OnboardingStepper]').click(); - cy.get('[data-test=ModalOnboarding]').should('be.visible'); - }); - - it(`Onboarding stepper is not visible if "${IS_ONBOARDING_DONE}" is set to "true"`, () => { - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - cy.reload(); - cy.get('[data-test=ModalOnboarding]').should('not.exist'); - cy.get('[data-test=OnboardingStepper]').should('not.exist'); - }); - - if (isDesktop) { - it(`Onboarding stepper has tooltip`, () => { - cy.get('[data-test=ModalOnboarding__Button]').click(); - cy.get('[data-test=OnboardingStepper]').trigger('mouseover'); - cy.get('[data-test=OnboardingStepper__Tooltip__content]').should('be.visible'); - cy.get('[data-test=OnboardingStepper__Tooltip__content]') - .invoke('text') - .should('eq', 'Reopen onboarding'); - }); - } - - it('Onboarding stepper has right amount of steps and highlights correct amount of passed steps', () => { - const onboardingSteps = getStepsDecisionWindowOpen('2', '16 Jan'); - - cy.get('[data-test=ModalOnboarding__Button]').click(); - - cy.get(`[data-test*=OnboardingStepper__circle]`).should( - 'have.length', - onboardingSteps.length, - ); - - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 0 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(1); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 1 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(2); - cy.get('[data-test=ModalOnboarding__Button]').click(); - for (let i = 0; i < onboardingSteps.length - 1; i++) { - cy.get(`[data-test=OnboardingStepper__circle--${i}]`) - .then($el => $el.css('stroke')) - .should('be.colored', i > 2 ? '#ffffff' : '#2d9b87'); - } - cy.get('[data-test=OnboardingStepper]').click(); - checkProgressStepperSlimIsCurrentAndClickNext(3); - cy.get('[data-test=ModalOnboarding__Button]').click(); - - cy.get('[data-test=OnboardingStepper]').should('not.exist'); - }); - }); -}); diff --git a/client/cypress/e2e/allocationItemWindowClosed.cy.ts b/client/cypress/e2e/allocationItemWindowClosed.cy.ts deleted file mode 100644 index 5188990442..0000000000 --- a/client/cypress/e2e/allocationItemWindowClosed.cy.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { - connectWallet, - visitWithLoader, - mockCoinPricesServer, - navigateWithCheck, - checkProjectsViewLoaded, -} from 'cypress/utils/e2e'; -import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { - ALLOCATION_ITEMS_KEY, - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -describe('allocation (allocation window closed)', () => { - describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is closed, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowClosed').then(() => { - cy.get('[data-test=PlaygroundView]').should('be.visible'); - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.false; - }); - }); - }); - }); - - Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`test cases: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - - checkProjectsViewLoaded(); - cy.get('[data-test^=ProjectsView__ProjectsListItem]') - .eq(0) - .should('be.visible') - .find('[data-test=ProjectsListItem__name]') - .then($text => { - cy.wrap($text.text()).as('projectName'); - }); - - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(0) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .click(); - navigateWithCheck(ROOT_ROUTES.allocation.absolute); - }); - - it('AllocationItem shows all the elements', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__name]') - .then($allocationItemName => { - cy.get('@projectName').then(projectName => { - expect(projectName).to.eq($allocationItemName.text()); - }); - }); - - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__imageProfile]') - .should(isDesktop ? 'be.visible' : 'not.be.visible'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItemRewards__value]') - .contains('0 ETH'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItemRewardsDonors]') - .contains('0'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .should('be.disabled'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText__suffix]') - .contains('GWEI'); - }); - }); - }); -}); diff --git a/client/cypress/e2e/allocationItemWindowOpen.cy.ts b/client/cypress/e2e/allocationItemWindowOpen.cy.ts deleted file mode 100644 index d295448be9..0000000000 --- a/client/cypress/e2e/allocationItemWindowOpen.cy.ts +++ /dev/null @@ -1,207 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - visitWithLoader, - mockCoinPricesServer, - navigateWithCheck, - connectWallet, - checkProjectsViewLoaded, - ETH_USD, -} from 'cypress/utils/e2e'; -import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { - ALLOCATION_ITEMS_KEY, - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -chai.use(chaiColors); - -const budget = '10000000000000000'; // 0.01 ETH - -const changeMainValueToFiat = () => { - cy.get('[data-test=Navbar__Button--Settings]').click(); - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').uncheck(); - cy.get('[data-test=Navbar__Button--Allocate]').click(); -}; - -describe('allocation (allocation window open)', () => { - describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is open, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.true; - }); - }); - }); - }); - - Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`test cases: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.intercept('GET', '/rewards/budget/*/epoch/*', { body: { budget } }); - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - localStorage.setItem(ALLOCATION_ITEMS_KEY, '[]'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - connectWallet({ isPatronModeEnabled: false }); - - checkProjectsViewLoaded(); - cy.get('[data-test^=ProjectsView__ProjectsListItem]') - .eq(0) - .should('be.visible') - .find('[data-test=ProjectsListItem__name]') - .then($text => { - cy.wrap($text.text()).as('projectName'); - }); - - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(0) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .click(); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(1) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .click(); - navigateWithCheck(ROOT_ROUTES.allocation.absolute); - cy.get('[data-test=AllocationItemSkeleton]').should('not.exist'); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('AllocationItem shows all the elements', () => { - cy.get('@projectName').then(projectName => { - cy.get('[data-test=AllocationItem__name]').contains(projectName).should('be.visible'); - }); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__imageProfile]') - .should(isDesktop ? 'be.visible' : 'not.be.visible'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .should('be.enabled'); - }); - - it('AllocationItem__InputText correctly changes background color on focus', () => { - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .focus(); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .should('have.focus'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .should('have.css', 'background-color') - .and('be.colored', '#f1faf8'); - }); - - it('AllocationItem__InputText correctly changes background color on error', () => { - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText__suffix]') - .contains('ETH'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .type('99'); - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .should('have.css', 'background-color') - .and('be.colored', '#f1faf8'); - }); - - it('AllocationItem__InputText has correct suffix', () => { - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText__suffix]') - .contains('ETH'); - - changeMainValueToFiat(); - - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText__suffix]') - .contains('USD'); - }); - - it(`User can change allocation item value manually (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .clear() - .type('0.005'); - cy.get('[data-test=AllocationItem]') - .eq(1) - .find('[data-test=AllocationItem__InputText]') - .clear() - .type('0.002'); - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', '0.007 ETH'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', '0.003 ETH'); - }); - - it(`User can change allocation item value manually (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeMainValueToFiat(); - - cy.get('[data-test=AllocationItem]') - .eq(0) - .find('[data-test=AllocationItem__InputText]') - .clear() - .type(`${(0.005 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationItem]') - .eq(1) - .find('[data-test=AllocationItem__InputText]') - .clear() - .type(`${(0.002 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', `$${(0.007 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', `$${(0.003 * ETH_USD).toFixed(2)}`); - }); - }); - }); -}); diff --git a/client/cypress/e2e/allocationRewardsBox.cy.ts b/client/cypress/e2e/allocationRewardsBox.cy.ts deleted file mode 100644 index 5bddda1bcc..0000000000 --- a/client/cypress/e2e/allocationRewardsBox.cy.ts +++ /dev/null @@ -1,585 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - visitWithLoader, - mockCoinPricesServer, - connectWallet, - ETH_USD, - changeMainValueToFiat, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -chai.use(chaiColors); - -const splitTheValueUsingSlider = (isCryptoAsAMainValue: boolean) => { - if (!isCryptoAsAMainValue) { - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - } - - cy.get('[data-test=AllocationRewardsBox__title]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__Slider]').then($sliderEl => { - const { width: sliderElWidth } = $sliderEl[0].getBoundingClientRect(); - - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - // track 0 is hidden under the thumb - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]').should( - 'have.css', - 'width', - `${sliderButtonDimensions.width}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]').should( - 'have.css', - 'width', - `${sliderElWidth}px`, - ); - - const pageX0 = sliderButtonDimensions.left; - const pageX50 = - sliderButtonDimensions.left - sliderButtonDimensions.width / 2 + sliderElWidth / 2; - const pageX100 = - sliderButtonDimensions.left - sliderButtonDimensions.width / 2 + sliderElWidth; - - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]') - .trigger('mousedown', { - pageX: pageX0, - }) - .trigger('mousemove', { - pageX: pageX50, - }) - .trigger('mouseup', { - pageX: pageX50, - }); - - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]').should( - 'have.css', - 'width', - `${(sliderButtonDimensions.width + sliderElWidth) / 2}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]') - .then($el => $el.css('background-color')) - .should('be.colored', '#2d9b87'); - - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]').should( - 'have.css', - 'width', - `${(sliderButtonDimensions.width + sliderElWidth) / 2}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]') - .then($el => $el.css('background-color')) - .should('be.colored', '#ff9601'); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]') - .trigger('mousedown', { - pageX: pageX50, - }) - .trigger('mousemove', { - pageX: pageX100, - }) - .trigger('mouseup', { - pageX: pageX100, - }); - - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]').should( - 'have.css', - 'width', - `${sliderElWidth}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]') - .then($el => $el.css('background-color')) - .should('be.colored', '#2d9b87'); - - // track 1 is hidden under the thumb - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]').should( - 'have.css', - 'width', - `${sliderButtonDimensions.width}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]') - .then($el => $el.css('background-color')) - .should('be.colored', '#ff9601'); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - }); - }); -}; - -const changeDonateManually = (isCryptoAsAMainValue: boolean) => { - if (!isCryptoAsAMainValue) { - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - } - - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - cy.get('[data-test=ModalAllocationValuesEdit__header]').invoke('text').should('eq', 'Donate 0%'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0' : '0.00', - ); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should('be.focused'); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#2d9b87'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '0'); - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('not.be.focused'); - - // 0.01 ETH - cy.get('[data-test=AllocationInputs__InputText--crypto]').type( - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#2d9b87'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '100'); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Donate 100%'); - - // 0.1 ETH - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .clear() - .type(isCryptoAsAMainValue ? '0.1' : `${(0.1 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#FF6157'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '100'); - - cy.get('[data-test=AllocationInputs__Button]').should('be.disabled'); - - cy.get('[data-test=AllocationInputs__InputText--crypto]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '0'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0' : '0.00', - ); - - // 50 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('50'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.005' : `${((0.01 * ETH_USD) / 2).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]').invoke('text').should('eq', 'Donate 50%'); - - // 100 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('100'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Donate 100%'); - - // 1000 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('1000'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); -}; - -const changePersonalManually = (isCryptoAsAMainValue: boolean) => { - if (!isCryptoAsAMainValue) { - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - } - - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Personal 100%'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should('be.focused'); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#2d9b87'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '100'); - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('not.be.focused'); - - // 0.01 ETH - cy.get('[data-test=AllocationInputs__InputText--crypto]').type( - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#2d9b87'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '100'); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Personal 100%'); - - // 0.1 ETH - cy.get('[data-test=AllocationInputs__InputText--crypto]').type( - isCryptoAsAMainValue ? '0.1' : `${(0.1 * ETH_USD).toFixed(2)}`, - ); - cy.get('[data-test=AllocationInputs__InputText--crypto]') - .then($el => $el.css('border-color')) - .should('be.colored', '#FF6157'); - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '100'); - - cy.get('[data-test=AllocationInputs__Button]').should('be.disabled'); - - cy.get('[data-test=AllocationInputs__InputText--crypto]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').should('have.value', '0'); - - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0' : '0.00', - ); - - // 50 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('50'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.005' : `${((0.01 * ETH_USD) / 2).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.005 ETH' : `$${((0.01 * ETH_USD) / 2).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Personal 50%'); - - // 100 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('100'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); - - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - - cy.get('[data-test=ModalAllocationValuesEdit__header]') - .invoke('text') - .should('eq', 'Personal 100%'); - - // 1000 % - cy.get('[data-test=AllocationInputs__InputText--percentage]').clear(); - cy.get('[data-test=AllocationInputs__InputText--percentage]').type('1000'); - cy.get('[data-test=AllocationInputs__InputText--crypto]').should( - 'have.value', - isCryptoAsAMainValue ? '0.01' : `${(0.01 * ETH_USD).toFixed(2)}`, - ); - - cy.get('[data-test=AllocationInputs__Button]').should('not.be.disabled'); - cy.get('[data-test=AllocationInputs__Button]').click(); - - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0.01 ETH' : `$${(0.01 * ETH_USD).toFixed(2)}`); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe( - `allocation rewards box (disabled): ${device}`, - { viewportHeight, viewportWidth }, - () => { - beforeEach(() => { - mockCoinPricesServer(); - visitWithLoader(ROOT_ROUTES.allocation.absolute); - }); - - it('is visible', () => { - cy.get('[data-test=AllocationRewardsBox]').should('be.visible'); - }); - - it(`has each field with value 0 ETH (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - cy.get('[data-test=AllocationRewardsBox__title]').invoke('text').should('eq', '0 ETH'); - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', '0 ETH'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', '0 ETH'); - }); - - it(`has each field with value $0.00 (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - - cy.get('[data-test=AllocationRewardsBox__title]').invoke('text').should('eq', '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--0]') - .invoke('text') - .should('eq', '$0.00'); - cy.get('[data-test=AllocationRewardsBox__section__value--1]') - .invoke('text') - .should('eq', '$0.00'); - }); - - it('shows "No rewards yet" message below rewards value', () => { - cy.get('[data-test=AllocationRewardsBox__subtitle]') - .invoke('text') - .should('eq', 'No rewards yet'); - }); - - it('Clicking on `Donate` label or value doesn`t open modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('not.exist'); - }); - - it('Clicking on `Personal` label or value doesn`t open modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('not.exist'); - }); - - it('slider is visible', () => { - cy.get('[data-test=AllocationRewardsBox__Slider]').should('be.visible'); - }); - - it('slider thumb exists but isn`t visible', () => { - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]').should('exist'); - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]').should('not.be.visible'); - }); - - it('slider track should be filled in 50%', () => { - cy.get('[data-test=AllocationRewardsBox__Slider]').then($sliderEl => { - const { width } = $sliderEl[0].getBoundingClientRect(); - - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]').should( - 'have.css', - 'width', - `${width / 2}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--0]') - .then($el => $el.css('background-color')) - .should('be.colored', '#9ea39e'); - - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]').should( - 'have.css', - 'width', - `${width / 2}px`, - ); - cy.get('[data-test=AllocationRewardsBox__Slider__track--1]') - .then($el => $el.css('background-color')) - .should('be.colored', '#cdd1cd'); - }); - }); - - it('Clicking on `Personal` label or value doesn`t open modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('not.exist'); - }); - - it('Clicking on `Personal` label or value doesn`t open modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('not.exist'); - }); - }, - ); - - describe(`allocation rewards box (enabled): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.allocation.absolute); - cy.intercept('GET', '/rewards/budget/*/epoch/*', { body: { budget: '10000000000000000' } }); - connectWallet({ isPatronModeEnabled: false }); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('is visible', () => { - cy.get('[data-test=AllocationRewardsBox]').should('be.visible'); - }); - - it('shows "Available now" message below rewards value', () => { - cy.get('[data-test=AllocationRewardsBox__subtitle]') - .invoke('text') - .should('eq', 'Available now'); - }); - - it(`user has 0.01 ETH rewards (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - cy.get('[data-test=AllocationRewardsBox__title]').invoke('text').should('eq', '0.01 ETH'); - }); - - it(`user has $20.42 (0.01 ETH) rewards (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - - cy.get('[data-test=AllocationRewardsBox__title]') - .invoke('text') - .should('eq', `$${(0.01 * ETH_USD).toFixed(2)}`); - }); - - it('slider thumb exists and is visible', () => { - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]').should('exist'); - cy.get('[data-test=AllocationRewardsBox__Slider__thumb]').should('be.visible'); - }); - - it('Clicking on `Donate` label or value opens modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--0]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('exist').should('be.visible'); - }); - - it('Clicking on `Personal` label or value opens modal to editing value', () => { - cy.get('[data-test=AllocationRewardsBox__section--1]').click(); - cy.get('[data-test=ModalAllocationValuesEdit]').should('exist').should('be.visible'); - }); - - it(`user can split the value by using slider from 0/100 to 50/50 and then from 50/50 to 100/0 (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - splitTheValueUsingSlider(true); - }); - - it(`user can split the value by using slider from 0/100 to 50/50 and then from 50/50 to 100/0 (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - splitTheValueUsingSlider(false); - }); - - it(`user can change 'Donate' value manually in modal (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - changeDonateManually(true); - }); - - it(`user can change 'Donate' value manually in modal (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeDonateManually(false); - }); - - it(`user can change 'Personal' value manually in modal ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - changePersonalManually(true); - }); - - it(`user can change 'Personal' value manually in modal ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changePersonalManually(false); - }); - }); -}); diff --git a/client/cypress/e2e/allocationSummary.cy.ts b/client/cypress/e2e/allocationSummary.cy.ts deleted file mode 100644 index 63a616e47b..0000000000 --- a/client/cypress/e2e/allocationSummary.cy.ts +++ /dev/null @@ -1,99 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - visitWithLoader, - mockCoinPricesServer, - connectWallet, - changeMainValueToFiat, - ETH_USD, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -chai.use(chaiColors); - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`allocation summary: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.allocation.absolute); - cy.intercept('GET', '/rewards/budget/*/epoch/*', { body: { budget: '10000000000000000' } }); - cy.intercept('GET', '/allocations/user/*/epoch/*', { - body: { - allocations: [ - { - address: '0x15c941a44a343B8c46a28F2BB9aFc7a54E255A4f', - amount: '5000000000000000', - }, - { - address: '0x1c01595f9534E33d411035AE99a4317faeC4f6Fe', - amount: '2000000000000000', - }, - ], - isManuallyEdited: true, - }, - }); - connectWallet({ isPatronModeEnabled: false }); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('Allocation summary section shows correct values', () => { - cy.get('[data-test=AllocationSummary]').should('be.visible'); - cy.get('[data-test=AllocationSummary__personalRewardBox]').should('be.visible'); - cy.get('[data-test=AllocationSummaryProject]').should('have.length', 2); - cy.get('[data-test=AllocationSummaryProject]') - .eq(0) - .find('[data-test=AllocationSummaryProject__donation]') - .invoke('text') - .should('eq', '0.005'); - cy.get('[data-test=AllocationSummaryProject]') - .eq(1) - .find('[data-test=AllocationSummaryProject__donation]') - .invoke('text') - .should('eq', '0.002'); - - cy.get('[data-test=AllocationSummary__personalReward]') - .invoke('text') - .should('eq', '0.003 ETH'); - - changeMainValueToFiat(ROOT_ROUTES.allocation.absolute); - - cy.get('[data-test=AllocationSummaryProject]') - .eq(0) - .find('[data-test=AllocationSummaryProject__donation]') - .invoke('text') - .should('eq', `$${(0.005 * ETH_USD).toFixed(2)}`); - cy.get('[data-test=AllocationSummaryProject]') - .eq(1) - .find('[data-test=AllocationSummaryProject__donation]') - .invoke('text') - .should('eq', `$${(0.002 * ETH_USD).toFixed(2)}`); - - cy.get('[data-test=AllocationSummary__personalReward]') - .invoke('text') - .should('eq', `$${(0.003 * ETH_USD).toFixed(2)}`); - }); - }); -}); diff --git a/client/cypress/e2e/earn.cy.ts b/client/cypress/e2e/earn.cy.ts deleted file mode 100644 index e080d151af..0000000000 --- a/client/cypress/e2e/earn.cy.ts +++ /dev/null @@ -1,364 +0,0 @@ -import { - visitWithLoader, - mockCoinPricesServer, - connectWallet, - GLM_USD, - changeMainValueToFiat, -} from 'cypress/utils/e2e'; -import { moveTime } from 'cypress/utils/moveTime'; -import { ConnectWalletParameters } from 'cypress/utils/types'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -const checkValues = (isCryptoAsAMainValue: boolean) => { - if (!isCryptoAsAMainValue) { - changeMainValueToFiat(ROOT_ROUTES.earn.absolute); - } - - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 GLM' : '$0.00'); - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__secondary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 GLM'); - - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 GLM' : '$0.00'); - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__secondary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 GLM'); - - cy.get('[data-test=BoxPersonalAllocation__Section--pending__DoubleValue__primary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=BoxPersonalAllocation__Section--pending__DoubleValue__secondary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); - - cy.get('[data-test=BoxPersonalAllocation__Section--availableNow__DoubleValue__primary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=BoxPersonalAllocation__Section--availableNow__DoubleValue__secondary]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }, idx) => { - describe(`earn: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.earn.absolute); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('renders "Locked balance" box', () => { - cy.get('[data-test=BoxGlmLock__BoxRounded]').should('be.visible'); - }); - - it('renders "Personal allocation" box', () => { - cy.get('[data-test=BoxPersonalAllocation]').should('be.visible'); - }); - - it('renders "History"', () => { - cy.get('[data-test=History]').should('be.visible'); - }); - - it('"Lock GLM" button is visible', () => { - cy.get('[data-test=BoxGlmLock__Button]').should('be.visible'); - }); - - it('"Lock GLM" button is disabled', () => { - cy.get('[data-test=BoxGlmLock__Button]').should('be.disabled'); - }); - - it('"Withdraw to wallet" button is visible', () => { - cy.get('[data-test=BoxPersonalAllocation__Button]').should('be.visible'); - }); - - it('"Withdraw to wallet" button is disabled', () => { - cy.get('[data-test=BoxPersonalAllocation__Button]').should('be.disabled'); - }); - - it('"Effective" section has tooltip', () => { - cy.get('[data-test=BoxGlmLock__Section--effective]').should('be.visible'); - }); - - if (!isDesktop) { - it('"Effective" section tooltip svg icon opens "TooltipEffectiveLockedBalance"', () => { - cy.get('[data-test=BoxGlmLock__Section--effective__Svg]').click(); - cy.get('[data-test=TooltipEffectiveLockedBalance]').should('be.visible'); - }); - } - - it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button is active', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=BoxGlmLock__Button]').should('not.be.disabled'); - }); - - it('Wallet connected: "Lock GLM" / "Edit Locked GLM" button opens "ModalGlmLock"', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=ModalGlmLock]').should('be.visible'); - }); - - it('Wallet connected: "ModalGlmLock" has overflow', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=ModalGlmLock__overflow]').should('exist'); - }); - - it('Wallet connected: inputs allow to type multiple characters without focus problems', () => { - /** - * In EarnGlmLock there are multiple autofocus rules set. - * This test checks if user is still able to type without any autofocus disruption. - */ - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=ModalGlmLock]').should('be.visible'); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').clear().type('100'); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.value', '100'); - cy.get('[data-test=EarnGlmLockTabs__tab--1]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').clear().type('100'); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.value', '100'); - }); - - it('Wallet connected: "ModalGlmLock" - changing tabs keep focus on first input', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=ModalGlmLock]').should('be.visible'); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); - cy.get('[data-test=EarnGlmLockTabs__tab--1]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); - cy.get('[data-test=EarnGlmLockTabs__tab--0]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').should('have.focus'); - }); - - it('Wallet connected: Lock 1 GLM', () => { - connectWallet({ isPatronModeEnabled: false }); - - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') - .invoke('text') - .then(text => { - const amountToLock = 1; - const lockedGlms = parseInt(text, 10); - - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').clear().type(`${amountToLock}`); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Lock'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Waiting for confirmation'); - cy.confirmMetamaskPermissionToSpend({ - spendLimit: '99999999999999999999', - }); - // Workaround for two notifications during first transaction. - // 1. Allow the third party to spend TKN from your current balance. - // 2. Confirm permission to spend - if (Cypress.env('CI') === 'true' && idx === 0) { - cy.wait(1000); - cy.confirmMetamaskPermissionToSpend({ - spendLimit: '99999999999999999999', - }); - } - cy.get('[data-test=GlmLockTabs__Button]', { timeout: 180000 }).should( - 'have.text', - 'Close', - ); - cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get( - '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', - // Small timeout ensures skeleton shows up quickly after the transaction. - { timeout: 1000 }, - ).should('be.visible'); - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterLock = parseInt(nextText, 10); - expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); - }); - cy.get('[data-test=HistoryItem__title]').first().should('have.text', 'Locked GLM'); - cy.get('[data-test=HistoryItem__DoubleValue__primary]') - .first() - .should('have.text', '1 GLM'); - }); - }); - - it('Wallet connected: Unlock 1 GLM', () => { - connectWallet({ isPatronModeEnabled: false }); - - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') - .invoke('text') - .then(text => { - const amountToUnlock = 1; - const lockedGlms = parseInt(text, 10); - - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=EarnGlmLockTabs__tab--1]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]') - .clear() - .type(`${amountToUnlock}`); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Unlock'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Waiting for confirmation'); - cy.confirmMetamaskPermissionToSpend({ - shouldWaitForPopupClosure: true, - spendLimit: '99999999999999999999', - }); - cy.get('[data-test=GlmLockTabs__Button]', { timeout: 60000 }).should( - 'have.text', - 'Close', - ); - cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get( - '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', - // Small timeout ensures skeleton shows up quickly after the transaction. - { timeout: 1000 }, - ).should('be.visible'); - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterUnlock = parseInt(nextText, 10); - expect(lockedGlms - amountToUnlock).to.be.eq(lockedGlmsAfterUnlock); - }); - cy.get('[data-test=HistoryItem__title]').first().should('have.text', 'Unlocked GLM'); - cy.get('[data-test=HistoryItem__DoubleValue__primary]') - .first() - .should('have.text', '1 GLM'); - cy.get('[data-test=HistoryItem__DoubleValue__secondary]') - .first() - .should('have.text', `$${(1 * GLM_USD).toFixed(2)}`); - - cy.get('[data-test=HistoryItem]').first().click(); - cy.get('[data-test=EarnHistoryItemDetailsModal]').should('be.visible'); - - cy.get('[data-test=EarnHistoryItemDetailsRest__amount__DoubleValue__primary]') - .invoke('text') - .should('eq', '1 GLM'); - cy.get('[data-test=EarnHistoryItemDetailsRest__amount__DoubleValue__secondary]') - .invoke('text') - .should('eq', `$${(1 * GLM_USD).toFixed(2)}`); - - cy.get('[data-test=EarnHistoryItemDetailsModal__Button]').click(); - cy.get('[data-test=EarnHistoryItemDetailsModal]').should('not.be.visible'); - - changeMainValueToFiat(ROOT_ROUTES.earn.absolute); - - cy.get('[data-test=HistoryItem__DoubleValue__primary]') - .first() - .should('have.text', `$${(1 * GLM_USD).toFixed(2)}`); - cy.get('[data-test=HistoryItem__DoubleValue__secondary]') - .first() - .should('have.text', '1 GLM'); - - cy.get('[data-test=HistoryItem]').first().click(); - cy.get('[data-test=EarnHistoryItemDetailsModal]').should('be.visible'); - - cy.get('[data-test=EarnHistoryItemDetailsRest__amount__DoubleValue__primary]') - .invoke('text') - .should('eq', `$${(1 * GLM_USD).toFixed(2)}`); - cy.get('[data-test=EarnHistoryItemDetailsRest__amount__DoubleValue__secondary]') - .invoke('text') - .should('eq', `1 GLM`); - }); - }); - - it('Wallet connected: Effective deposit after locking 1000 GLM and moving epoch is equal to current deposit', () => { - const connectWalletParameters: ConnectWalletParameters = { - isPatronModeEnabled: false, - }; - connectWallet(connectWalletParameters); - - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]') - .invoke('text') - .then(text => { - const amountToLock = 1000; - const lockedGlms = parseInt(text.replace(/\u200a/g, ''), 10); - - cy.get('[data-test=BoxGlmLock__Button]').click(); - cy.get('[data-test=InputsCryptoFiat__InputText--crypto]').clear().type(`${amountToLock}`); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Lock'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get('[data-test=GlmLockTabs__Button]').should('have.text', 'Waiting for confirmation'); - cy.confirmMetamaskPermissionToSpend({ - spendLimit: '99999999999999999999', - }); - cy.get('[data-test=GlmLockTabs__Button]', { timeout: 180000 }).should( - 'have.text', - 'Close', - ); - cy.get('[data-test=GlmLockNotification--success]').should('be.visible'); - cy.get('[data-test=GlmLockTabs__Button]').click(); - cy.get( - '[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', - // Small timeout ensures skeleton shows up quickly after the transaction. - { timeout: 1000 }, - ).should('be.visible'); - // Waiting for skeletons to disappear ensures Graph indexed lock/unlock. - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__DoubleValueSkeleton]', { - timeout: 60000, - }).should('not.exist'); - cy.window().then(async win => { - cy.wrap(null).then(() => { - return moveTime(win, 'nextEpochDecisionWindowClosed', connectWalletParameters).then( - () => { - cy.get('[data-test=BoxGlmLock__Section--current__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); - expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); - }); - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]', { - timeout: 60000, - }) - .invoke('text') - .then(nextText => { - const lockedGlmsAfterLock = parseInt(nextText.replace(/\u200a/g, ''), 10); - expect(lockedGlms + amountToLock).to.be.eq(lockedGlmsAfterLock); - }); - }, - ); - }); - }); - }); - }); - - it(`check boxes values ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true`, () => { - checkValues(true); - }); - - it(`check boxes values ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false`, () => { - checkValues(false); - }); - }); -}); diff --git a/client/cypress/e2e/layout.cy.ts b/client/cypress/e2e/layout.cy.ts deleted file mode 100644 index 20c22785e3..0000000000 --- a/client/cypress/e2e/layout.cy.ts +++ /dev/null @@ -1,151 +0,0 @@ -import { - navigateWithCheck, - mockCoinPricesServer, - connectWallet, - visitWithLoader, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`layout: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - cy.clearLocalStorage(); - cy.setupMetamask(); - }); - - beforeEach(() => { - mockCoinPricesServer(); - cy.disconnectMetamaskWalletFromAllDapps(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('renders top bar', () => { - cy.get('[data-test=MainLayout__Header]').should('be.visible'); - }); - - it('Clicking on Octant logo scrolls view to the top on logo click (projects view)', () => { - cy.scrollTo(0, 500); - cy.get('[data-test=MainLayout__Logo]').click(); - // waiting for scrolling to finish - cy.wait(2000); - cy.window().then(cyWindow => { - expect(cyWindow.scrollY).to.be.eq(0); - }); - }); - - it('Clicking on Octant logo redirects to projects view (outside projects view)', () => { - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsView]').should('be.visible'); - cy.get('[data-test=MainLayout__Logo]').click(); - cy.get('[data-test=ProjectsView]').should('be.visible'); - }); - - it('Clicking on Octant logo redirects to projects view (outside projects view) with memorized scrollY', () => { - cy.scrollTo(0, 500); - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsView]').should('be.visible'); - cy.get('[data-test=MainLayout__Logo]').click(); - cy.get('[data-test=ProjectsView]').should('be.visible'); - cy.window().then(cyWindow => { - expect(cyWindow.scrollY).to.be.eq(500); - }); - }); - - it('renders bottom navbar', () => { - cy.get('[data-test=Navbar]').should('be.visible'); - }); - - it('bottom navbar allows to change views', () => { - navigationTabs.forEach(({ to }) => { - navigateWithCheck(to); - }); - }); - - it('"Connect" button is visible when wallet is disconnected', () => { - cy.get('[data-test=MainLayout__Button--connect]').should('be.visible'); - cy.get('[data-test=MainLayout__Button--connect]').click(); - }); - - it('"Connect" button opens "ModalConnectWallet"', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet]').should('be.visible'); - }); - - it('"ModalConnectWallet" always shows "WalletConnect" option', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet]').within(() => { - cy.get('[data-test=ConnectWallet__BoxRounded--walletConnect]').should('be.visible'); - }); - }); - - it('"ModalConnectWallet" shows "Browser wallet" and "WalletConnect" options (MetaMask wallet detected)', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet]').within(() => { - cy.get('[data-test=ConnectWallet__BoxRounded--walletConnect]').should('be.visible'); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').should('be.visible'); - }); - }); - - it('"ModalConnectWallet" has overflow enabled', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet__overflow]').should('exist'); - }); - - it('Clicking background when "ModalConnectWallet" is open, closes Modal', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet__overflow]').click({ force: true }); - cy.get('[data-test=ModalConnectWallet]').should('not.exist'); - }); - - it('"ModalConnectWallet" has "cross" icon button in header', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet__Button]').should('be.visible'); - }); - - it('Clicking on "X" mark in "ModalConnectWallet", closes Modal', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ModalConnectWallet__Button]').click(); - cy.get('[data-test=ModalConnectWallet]').should('not.exist'); - }); - - // eslint-disable-next-line no-only-tests/no-only-tests - it('Clicking on "WalletConnect" option, opens WalletConnect modal', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - // Wait for RainbowKit to load WalletConnect. - cy.wait(2000); - cy.get('[data-test=ConnectWallet__BoxRounded--walletConnect]').click(); - cy.get('div:contains("Need the WalletConnect modal?")').should('be.visible'); - }); - - it('Clicking on "Browser wallet" option connects with MetaMask wallet', () => { - cy.get('[data-test=MainLayout__Button--connect]').click(); - cy.get('[data-test=ConnectWallet__BoxRounded--browserWallet]').click(); - cy.switchToMetamaskNotification(); - cy.acceptMetamaskAccess(); - cy.get('[data-test=MainLayout__Button--connect]').should('not.exist'); - cy.get('[data-test=ProfileInfo]').should('exist'); - }); - - it('Wallet address is clickable and has href attribute', () => { - connectWallet({ isPatronModeEnabled: false }); - cy.get('[data-test=ProfileInfo]').click(); - cy.get('[data-test=LayoutWallet__Button--address]').should('be.visible'); - cy.get('[data-test=LayoutWallet__Button--address]').should('have.attr', 'href'); - cy.get('[data-test=LayoutWallet__Button--address]').click(); - }); - }); -}); diff --git a/client/cypress/e2e/metrics.cy.ts b/client/cypress/e2e/metrics.cy.ts deleted file mode 100644 index b7bbd0c3c2..0000000000 --- a/client/cypress/e2e/metrics.cy.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { - changeMainValueToFiat, - connectWallet, - mockCoinPricesServer, - visitWithLoader, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -const rendersTilesWithCorrectValues = (isCryptoAsAMainValue: boolean) => { - connectWallet({ isPatronModeEnabled: false }); - if (!isCryptoAsAMainValue) { - changeMainValueToFiat(ROOT_ROUTES.metrics.absolute); - } - - cy.get('[data-test=MetricsEpochGridTopProjects__list__item__value]') - .eq(0) - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0' : '$0.00'); - - cy.get('[data-test=MetricsEpochGridTotalDonationsAndPersonal__totalDonations__value]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=MetricsEpochGridTotalDonationsAndPersonal__totalDonations__subvalue]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); - cy.get('[data-test=MetricsEpochGridTotalDonationsAndPersonal__totalPersonal__value]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=MetricsEpochGridTotalDonationsAndPersonal__totalPersonal__subvalue]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); - - cy.get('[data-test=MetricsEpochGridRewardsUnusedAndUnallocatedValue__unallocatedValue__value]') - .invoke('text') - .should(isCryptoAsAMainValue ? 'not.include' : 'include', '$'); - cy.get('[data-test=MetricsEpochGridRewardsUnusedAndUnallocatedValue__unallocatedValue__subvalue]') - .invoke('text') - .should(isCryptoAsAMainValue ? 'include' : 'not.include', '$'); - - cy.get('[data-test=MetricsEpochGridFundsUsage__total]') - .invoke('text') - .should(isCryptoAsAMainValue ? 'not.include' : 'include', '$'); - - cy.get('[data-test=MetricsPersonalGridTotalRewardsWithdrawals__totalRewards__value]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=MetricsPersonalGridTotalRewardsWithdrawals__totalRewards__subvalue]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); - cy.get('[data-test=MetricsPersonalGridTotalRewardsWithdrawals__totalWithdrawals__value]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '0 ETH' : '$0.00'); - cy.get('[data-test=MetricsPersonalGridTotalRewardsWithdrawals__totalWithdrawals__subvalue]') - .invoke('text') - .should('eq', isCryptoAsAMainValue ? '$0.00' : '0 ETH'); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`metrics: ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.metrics.absolute); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('renders total projects tile', () => { - cy.get('[data-test=MetricsGeneralGridTotalProjects]').should('be.visible'); - }); - - it('renders total eth staked tile', () => { - cy.get('[data-test=MetricsGeneralGridTotalEthStaked]').should('be.visible'); - }); - - it('renders tile with total glm locked and % of 1B total supply groups', () => { - cy.get('[data-test=MetricsGeneralGridTotalGlmLockedAndTotalSupply]').should('be.visible'); - cy.get('[data-test=MetricsGeneralGridTotalGlmLockedAndTotalSupply]') - .children() - .should('have.length', 2); - }); - - it('renders wallet with glm locked graph tile', () => { - cy.get('[data-test=MetricsGeneralGridWalletsWithGlmLocked]').should('be.visible'); - }); - - it('renders cumulative glm locked graph tile', () => { - cy.get('[data-test=MetricsGeneralGridCumulativeGlmLocked]').should('be.visible'); - }); - - it('renders tiles in correct order', () => { - const metricsEpochGridTilesDataTest = [ - 'MetricsEpochGridTopProjects', - 'MetricsEpochGridTotalDonationsAndPersonal', - 'MetricsEpochGridDonationsVsPersonalAllocations', - 'MetricsEpochGridFundsUsage', - 'MetricsEpochGridTotalUsers', - 'MetricsEpochGridPatrons', - 'MetricsEpochGridCurrentDonors', - 'MetricsEpochGridAverageLeverage', - 'MetricsEpochGridRewardsUnusedAndUnallocatedValue', - ]; - - const metricsGeneralGridTilesDataTest = [ - 'MetricsGeneralGridTotalGlmLockedAndTotalSupply', - 'MetricsGeneralGridTotalProjects', - 'MetricsGeneralGridTotalEthStaked', - 'MetricsGeneralGridCumulativeGlmLocked', - 'MetricsGeneralGridWalletsWithGlmLocked', - ]; - - cy.get('[data-test=MetricsEpoch__MetricsGrid]') - .children() - .should('have.length', metricsEpochGridTilesDataTest.length); - - for (let i = 0; i < metricsEpochGridTilesDataTest.length; i++) { - cy.get('[data-test=MetricsEpoch__MetricsGrid]') - .children() - .eq(i) - .invoke('data', 'test') - .should('eq', metricsEpochGridTilesDataTest[i]); - } - - cy.get('[data-test=MetricsGeneral__MetricsGrid]') - .children() - .should('have.length', metricsGeneralGridTilesDataTest.length); - - for (let i = 0; i < metricsGeneralGridTilesDataTest.length; i++) { - cy.get('[data-test=MetricsGeneral__MetricsGrid]') - .children() - .eq(i) - .invoke('data', 'test') - .should('eq', metricsGeneralGridTilesDataTest[i]); - } - }); - - it('renders grid with 4 columns on desktop or with 2 columns on other devices', () => { - cy.get('[data-test=MetricsEpoch__MetricsGrid]').then(el => { - const width = parseInt(el.css('width'), 10); - const rowGap = parseInt(el.css('rowGap'), 10); - - const columnWidth = isDesktop ? (width - 3 * rowGap) / 4 : (width - rowGap) / 2; - - cy.get('[data-test=MetricsEpoch__MetricsGrid]').should( - 'have.css', - 'grid-template-columns', - isDesktop - ? `${columnWidth}px ${columnWidth}px ${columnWidth}px ${columnWidth}px` - : `${columnWidth}px ${columnWidth}px`, - ); - }); - - cy.get('[data-test=MetricsGeneral__MetricsGrid]').then(el => { - const width = parseInt(el.css('width'), 10); - const rowGap = parseInt(el.css('rowGap'), 10); - - const columnWidth = isDesktop ? (width - 3 * rowGap) / 4 : (width - rowGap) / 2; - - cy.get('[data-test=MetricsGeneral__MetricsGrid]').should( - 'have.css', - 'grid-template-columns', - isDesktop - ? `${columnWidth}px ${columnWidth}px ${columnWidth}px ${columnWidth}px` - : `${columnWidth}px ${columnWidth}px`, - ); - }); - }); - - it(`renders tiles values in correct order ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true`, () => { - rendersTilesWithCorrectValues(true); - }); - - it(`renders tiles values in correct order ${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false`, () => { - rendersTilesWithCorrectValues(false); - }); - }); -}); diff --git a/client/cypress/e2e/patronMode.cy.ts b/client/cypress/e2e/patronMode.cy.ts deleted file mode 100644 index 7122413999..0000000000 --- a/client/cypress/e2e/patronMode.cy.ts +++ /dev/null @@ -1,745 +0,0 @@ -import { - connectWallet, - mockCoinPricesServer, - navigateWithCheck, - visitWithLoader, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`patron mode (disabled): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.settings.absolute); - connectWallet({ isPatronModeEnabled: false }); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('patron badge should not exist ', () => { - cy.get('[data-test=ProfileInfo__badge]').should('not.exist'); - }); - - it('Patron mode toggle is not checked', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').should('not.be.checked'); - }); - - if (isDesktop) { - it('Patron mode tooltip is visible on hover and has correct text', () => { - cy.get('[data-test=SettingsPatronModeBox__Tooltip]').trigger('mouseover'); - cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]').should('be.visible'); - cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]') - .invoke('text') - .should( - 'eq', - 'Patron mode is for token holders who want to support Octant. It disables allocation to yourself or projects. All rewards go directly to the matching fund with no action required by the patron.', - ); - }); - } - - it('Checking patron mode opens patron mode modal', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=ModalPatronMode]').should('be.visible'); - }); - - it('Patron mode modal last paragraph has correct text', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=SettingsPatronMode__fourthParagraph]') - .invoke('text') - .should('eq', 'Slide the switch below all the way to the right to enable patron mode.'); - }); - - it('Slider is visible', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=PatronModeSlider]').should('be.visible'); - }); - - it('Slider has correct label', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=PatronModeSlider__label]') - .invoke('text') - .should('eq', 'Slide right to confirm'); - }); - - it('Slider button is visible ', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=PatronModeSlider__button]').should('be.visible'); - }); - - it('Slider button has right arrow inside', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=PatronModeSlider__button__arrow]') - .should('be.visible') - .should('have.css', 'transform', 'none'); - }); - - it('Slider button is on the left side', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderLeftDistance = sliderEl[0].getBoundingClientRect().left; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonLeftDistance = sliderButtonEl[0].getBoundingClientRect().left; - - expect(sliderButtonLeftDistance).to.be.eq(sliderLeftDistance + sliderLeftPadding); - }); - }); - }); - - it('Slider button returns to the starting point if user drops it below or equal 50% of slider width', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const pointerDownPageX = sliderButtonDimensions.x; - const pointerMovePageX = sliderButtonDimensions.x + sliderTrackWidth / 2; - - cy.get('[data-test=PatronModeSlider__button]') - .trigger('pointerdown', { - pageX: pointerDownPageX, - }) - .trigger('pointermove', { - pageX: pointerMovePageX, - }) - .wait(1000) - .then(sliderButtonElAfterPointerMove => { - const sliderButtonDimensionsAfterPointerMove = - sliderButtonElAfterPointerMove[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerMove.x).eq(pointerMovePageX); - }) - .trigger('pointerup') - .wait(1000) - .then(sliderButtonElAfterPointerUp => { - const sliderButtonDimensionsAfterPointerUp = - sliderButtonElAfterPointerUp[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerUp.x).eq(pointerDownPageX); - }); - }); - }); - }); - - it('Slider elements change color while moving slider button', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const slider0PercentagePageX = sliderButtonDimensions.x; - const slider25PercentagePageX = sliderButtonDimensions.x + sliderTrackWidth / 4; - const slider50PercentagePageX = sliderButtonDimensions.x + sliderTrackWidth / 2; - const slider75PercentagePageX = sliderButtonDimensions.x + 3 * (sliderTrackWidth / 4); - const slider100PercentagePageX = sliderButtonDimensions.x + sliderTrackWidth; - - // 0% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointerdown', { - pageX: slider0PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgb(243, 243, 243)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(255, 255, 255)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(23, 23, 23)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '1'); - - // 25% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider25PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(212, 224, 221, 0.95)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(222, 234, 231)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(129, 129, 129)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.75'); - - // 50% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider50PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(175, 204, 197, 0.9)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(183, 211, 204)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(181, 181, 181)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.5'); - - // 75% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider75PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(128, 181, 169, 0.85)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(133, 185, 173)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(221, 221, 221)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.25'); - - // 100% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider100PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(45, 155, 135, 0.8)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(45, 155, 135)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(255, 255, 255)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0'); - }); - }); - }); - - it('Slider button goes to the end of track if user drops it above 50% of slider width + animation after signature', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').check(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const pointerDownPageX = sliderButtonDimensions.x; - const pointerMovePageX = sliderButtonDimensions.x + (sliderTrackWidth / 2 + 1); - const pointerUpPageX = sliderDimensions.right - sliderRightPadding - sliderButtonWidth; - - cy.get('[data-test=PatronModeSlider__button]') - .trigger('pointerdown', { - pageX: pointerDownPageX, - }) - .trigger('pointermove', { - pageX: pointerMovePageX, - }) - .wait(1000) - .then(sliderButtonElAfterPointerMove => { - const sliderButtonDimensionsAfterPointerMove = - sliderButtonElAfterPointerMove[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerMove.x).eq(pointerMovePageX); - }) - .trigger('pointerup') - .wait(1000) - .then(sliderButtonElAfterPointerUp => { - const sliderButtonDimensionsAfterPointerUp = - sliderButtonElAfterPointerUp[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerUp.x).eq(pointerUpPageX); - }); - - cy.confirmMetamaskSignatureRequest(); - cy.switchToCypressWindow(); - cy.get('[data-test=PatronModeSlider__button]').should('not.exist'); - cy.get('[data-test=PatronModeSlider__label]').should('not.exist'); - cy.get('[data-test=PatronModeSlider__status-label]') - .invoke('text') - .should('eq', 'Patron mode enabled'); - cy.wait(500); - cy.get('[data-test=ModalPatronMode]').should('not.exist'); - }); - }); - }); - }); - - describe(`patron mode (enabled): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.settings.absolute); - connectWallet({ isPatronModeEnabled: true }); - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('patron badge is visible and has correct label, background and text-transform prop', () => { - cy.get('[data-test=ProfileInfo__badge]').should('be.visible'); - cy.get('[data-test=ProfileInfo__badge]').invoke('text').should('eq', 'Patron'); - cy.get('[data-test=ProfileInfo__badge]') - .should('have.css', 'background-color', 'rgb(104, 91, 138)') - .should('have.css', 'text-transform', 'uppercase'); - }); - - it('Navbar has 4 items - projects, earn, metrics, settings', () => { - const navbarChildrenDataTest = [ - 'Navbar__Button--Projects', - 'Navbar__Button--Earn', - 'Navbar__Button--Metrics', - 'Navbar__Button--Settings', - ]; - - cy.get('[data-test=Navbar__buttons]') - .children() - .should('have.length', navbarChildrenDataTest.length); - - for (let i = 0; i < navbarChildrenDataTest.length; i++) { - cy.get('[data-test=Navbar__buttons]') - .children() - .eq(i) - .invoke('data', 'test') - .should('eq', navbarChildrenDataTest[i]); - } - }); - - it('route /allocate redirects to /projects', () => { - visitWithLoader(ROOT_ROUTES.allocation.absolute, ROOT_ROUTES.projects.absolute); - cy.get('[data-test=ProjectsView]').should('be.visible'); - }); - - it('BoxPersonalAllocation has correct title and sections labels', () => { - navigateWithCheck(ROOT_ROUTES.earn.absolute); - cy.get('[data-test=BoxPersonalAllocation__title]') - .invoke('text') - .should('eq', 'Patron earnings'); - cy.get('[data-test=BoxPersonalAllocation__Section__label]') - .eq(0) - .invoke('text') - .should('eq', 'Current epoch'); - cy.get('[data-test=BoxPersonalAllocation__Section__label]') - .eq(1) - .invoke('text') - .should('eq', 'All time'); - }); - - it('Patron mode toggle is checked', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').should('be.checked'); - }); - - if (isDesktop) { - it('Patron mode tooltip is visible on hover and has correct text', () => { - cy.get('[data-test=SettingsPatronModeBox__Tooltip]').trigger('mouseover'); - cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]').should('be.visible'); - cy.get('[data-test=SettingsPatronModeBox__Tooltip__content]') - .invoke('text') - .should( - 'eq', - 'Patron mode is for token holders who want to support Octant. It disables allocation to yourself or projects. All rewards go directly to the matching fund with no action required by the patron.', - ); - }); - } - - it('Unchecking patron mode opens patron mode modal', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=ModalPatronMode]').should('be.visible'); - }); - - it('Patron mode modal last paragraph has correct text', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=SettingsPatronMode__fourthParagraph]') - .invoke('text') - .should('eq', 'Slide the switch below all the way to the left to disable patron mode.'); - }); - - it('Slider is visible', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=PatronModeSlider]').should('be.visible'); - }); - - it('Slider has correct label', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=PatronModeSlider__label]') - .invoke('text') - .should('eq', 'Slide left to confirm'); - }); - - it('Slider button is visible', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=PatronModeSlider__button]').should('be.visible'); - }); - - it('Slider button has left arrow inside', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=PatronModeSlider__button__arrow]') - .should('be.visible') - .should('have.css', 'transform', 'matrix(-1, 0, 0, -1, 0, 0)'); - }); - - it('Slider button is on the right side', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderRightDistance = sliderEl[0].getBoundingClientRect().right; - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonRightDistance = sliderButtonEl[0].getBoundingClientRect().right; - - expect(sliderButtonRightDistance).to.be.eq(sliderRightDistance - sliderRightPadding); - }); - }); - }); - - it('Slider button returns to the starting point if user drops it below or equal 50% of slider width', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const pointerDownPageX = sliderButtonDimensions.right; - const pointerMovePageX = sliderButtonDimensions.right - sliderTrackWidth / 2; - - cy.get('[data-test=PatronModeSlider__button]') - .trigger('pointerdown', { - pageX: pointerDownPageX, - }) - .trigger('pointermove', { - pageX: pointerMovePageX, - }) - .wait(1000) - .then(sliderButtonElAfterPointerMove => { - const sliderButtonDimensionsAfterPointerMove = - sliderButtonElAfterPointerMove[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerMove.right).eq(pointerMovePageX); - }) - .trigger('pointerup') - .wait(1000) - .then(sliderButtonElAfterPointerUp => { - const sliderButtonDimensionsAfterPointerUp = - sliderButtonElAfterPointerUp[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerUp.right).eq(pointerDownPageX); - }); - }); - }); - }); - - it('Slider elements change color while moving slider button', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const slider0PercentagePageX = sliderButtonDimensions.right; - const slider25PercentagePageX = sliderButtonDimensions.right - sliderTrackWidth / 4; - const slider50PercentagePageX = sliderButtonDimensions.right - sliderTrackWidth / 2; - const slider75PercentagePageX = sliderButtonDimensions.right - 3 * (sliderTrackWidth / 4); - const slider100PercentagePageX = sliderButtonDimensions.right - sliderTrackWidth; - - // 0% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointerdown', { - pageX: slider0PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgb(243, 243, 243)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(255, 255, 255)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(23, 23, 23)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '1'); - - // 25% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider25PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(212, 224, 221, 0.95)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(222, 234, 231)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(129, 129, 129)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.75'); - - // 50% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider50PercentagePageX, - waitForAnimations: true, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(175, 204, 197, 0.9)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(183, 211, 204)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(181, 181, 181)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.5'); - - // 75% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider75PercentagePageX, - waitForAnimations: true, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(128, 181, 169, 0.85)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(133, 185, 173)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(221, 221, 221)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0.25'); - - // 100% - cy.get('[data-test=PatronModeSlider__button]').trigger('pointermove', { - pageX: slider100PercentagePageX, - }); - cy.get('[data-test=PatronModeSlider]').should( - 'have.css', - 'background-color', - 'rgba(45, 155, 135, 0.8)', - ); - cy.get('[data-test=PatronModeSlider__button]').should( - 'have.css', - 'background-color', - 'rgb(45, 155, 135)', - ); - cy.get('[data-test=PatronModeSlider__button__arrow__path]').should( - 'have.css', - 'fill', - 'rgb(255, 255, 255)', - ); - cy.get('[data-test=PatronModeSlider__label]') - .should('have.css', 'color', 'rgb(158, 163, 158)') - .should('have.css', 'opacity', '0'); - }); - }); - }); - - it('Slider button goes to the end of track if user drops it above 50% of slider width + animation after signature', () => { - cy.get('[data-test=SettingsPatronModeBox__InputToggle]').uncheck(); - - cy.get('[data-test=PatronModeSlider]').then(sliderEl => { - const sliderDimensions = sliderEl[0].getBoundingClientRect(); - const sliderWidth = sliderDimensions.width; - const sliderLeftPadding = parseInt(sliderEl.css('paddingLeft'), 10); - const sliderRightPadding = parseInt(sliderEl.css('paddingRight'), 10); - - cy.get('[data-test=PatronModeSlider__button]').then(sliderButtonEl => { - const sliderButtonDimensions = sliderButtonEl[0].getBoundingClientRect(); - - const sliderButtonWidth = sliderButtonDimensions.width; - - const sliderTrackWidth = - sliderWidth - sliderLeftPadding - sliderRightPadding - sliderButtonWidth; - - const pointerDownPageX = sliderButtonDimensions.right; - const pointerMovePageX = sliderButtonDimensions.right - (sliderTrackWidth / 2 + 1); - const pointerUpPageX = sliderDimensions.left + sliderLeftPadding; - - cy.get('[data-test=PatronModeSlider__button]') - .trigger('pointerdown', { - pageX: pointerDownPageX, - }) - .trigger('pointermove', { - pageX: pointerMovePageX, - }) - .wait(1000) - .then(sliderButtonElAfterPointerMove => { - const sliderButtonDimensionsAfterPointerMove = - sliderButtonElAfterPointerMove[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerMove.right).eq(pointerMovePageX); - }) - .trigger('pointerup') - .wait(1000) - .then(sliderButtonElAfterPointerUp => { - const sliderButtonDimensionsAfterPointerUp = - sliderButtonElAfterPointerUp[0].getBoundingClientRect(); - expect(sliderButtonDimensionsAfterPointerUp.x).eq(pointerUpPageX); - }); - - cy.confirmMetamaskSignatureRequest(); - cy.switchToCypressWindow(); - cy.get('[data-test=PatronModeSlider__button]').should('not.exist'); - cy.get('[data-test=PatronModeSlider__label]').should('not.exist'); - cy.get('[data-test=PatronModeSlider__status-label]') - .invoke('text') - .should('eq', 'Patron mode disabled'); - cy.wait(500); - cy.get('[data-test=ModalPatronMode]').should('not.exist'); - }); - }); - }); - - it('when entering project view, button icon changes to chevronLeft', () => { - navigateWithCheck(ROOT_ROUTES.projects.absolute); - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - cy.get('[data-test=Navbar__Button--Projects]') - .find('svg') - // HTML tag can't be self-closing in CY. - .should( - 'have.html', - '', - ); - }); - }); -}); diff --git a/client/cypress/e2e/project.cy.ts b/client/cypress/e2e/project.cy.ts deleted file mode 100644 index 32f4e42119..0000000000 --- a/client/cypress/e2e/project.cy.ts +++ /dev/null @@ -1,230 +0,0 @@ -import { - changeMainValueToFiat, - checkProjectsViewLoaded, - connectWallet, - mockCoinPricesServer, - visitWithLoader, -} from 'cypress/utils/e2e'; -import { getNamesOfProjects } from 'cypress/utils/projects'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; - -const getButtonAddToAllocate = (): Chainable => { - const projectListItemFirst = cy.get('[data-test=ProjectListItem').first(); - - return projectListItemFirst.find('[data-test=ProjectListItemHeader__ButtonAddToAllocate]'); -}; - -const checkProjectItemElements = (): Chainable => { - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - const projectListItemFirst = cy.get('[data-test=ProjectListItem').first(); - projectListItemFirst.get('[data-test=ProjectListItemHeader__Img]').should('be.visible'); - projectListItemFirst.get('[data-test=ProjectListItemHeader__name]').should('be.visible'); - getButtonAddToAllocate().should('be.visible'); - projectListItemFirst.get('[data-test=ProjectListItemHeader__Button]').should('be.visible'); - projectListItemFirst.get('[data-test=ProjectListItem__Description]').should('be.visible'); - - cy.get('[data-test=ProjectListItem__Donors]') - .first() - .scrollIntoView({ offset: { left: 0, top: 100 } }); - - cy.get('[data-test=ProjectListItem__Donors]').first().should('be.visible'); - cy.get('[data-test=ProjectListItem__Donors__DonorsHeader__count]') - .first() - .should('be.visible') - .should('have.text', '0'); - return cy.get('[data-test=ProjectListItem__Donors__noDonationsYet]').first().should('be.visible'); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`project: ${device}`, { viewportHeight, viewportWidth }, () => { - let projectNames: string[] = []; - - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - checkProjectsViewLoaded(); - - /** - * This could be done in before hook, but CY wipes the state after each test - * (could be disabled, but creates other problems) - */ - if (projectNames.length === 0) { - projectNames = getNamesOfProjects(); - } - }); - - it('entering project view directly renders content', () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - cy.reload(); - const projectListItemFirst = cy.get('[data-test=ProjectListItem').first(); - projectListItemFirst.get('[data-test=ProjectListItemHeader__Img]').should('be.visible'); - projectListItemFirst.get('[data-test=ProjectListItemHeader__name]').should('be.visible'); - }); - - it('entering project view renders all its elements', () => { - checkProjectItemElements(); - }); - - it('entering project view renders all its elements with fallback IPFS provider', () => { - cy.intercept('GET', '**/ipfs/**', req => { - if (req.url.includes('infura')) { - req.destroy(); - } - }); - - checkProjectItemElements(); - }); - - it('entering project view allows to add it to allocation and remove, triggering change of the icon, change of the number in navbar', () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - - getButtonAddToAllocate().click(); - - // cy.get('@buttonAddToAllocate').click(); - cy.get('[data-test=Navbar__numberOfAllocations]').contains(1); - getButtonAddToAllocate().click(); - cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); - }); - - it('Entering project view allows scroll only to the last project', () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').first().click(); - - for (let i = 0; i < projectNames.length; i++) { - cy.get('[data-test=ProjectListItem]').should( - 'have.length.greaterThan', - i === projectNames.length - 1 ? projectNames.length - 1 : i, - ); - cy.get('[data-test=ProjectListItemHeader__name]') - .eq(i) - .scrollIntoView({ offset: { left: 0, top: -150 } }) - .should('be.visible'); - cy.get('[data-test=ProjectListItem__Donors]') - .eq(i) - .scrollIntoView({ offset: { left: 0, top: -150 } }) - .should('be.visible'); - } - }); - - it('"Back to top" button is displayed if the user has scrolled past the start of the final project description', () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').first().click(); - - for (let i = 0; i < projectNames.length - 1; i++) { - cy.get('[data-test=ProjectListItem__Donors]') - .eq(i) - .scrollIntoView({ offset: { left: 0, top: 100 } }); - - if (i === projectNames.length - 1) { - cy.get('[data-test=ProjectBackToTopButton__Button]').should('be.visible'); - } - } - }); - - it('Clicking on "Back to top" button scrolls to the top of view (first project is visible)', () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').first().click(); - - for (let i = 0; i < projectNames.length - 1; i++) { - cy.get('[data-test=ProjectListItem__Donors]') - .eq(i) - .scrollIntoView({ offset: { left: 0, top: 100 } }); - - if (i === projectNames.length - 1) { - cy.get('[data-test=ProjectBackToTopButton__Button]').click(); - cy.get('[data-test=ProjectListItem]').eq(0).should('be.visible'); - } - } - }); - - it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - cy.get('[data-test=ProjectRewards__currentTotal__number]') - .first() - .invoke('text') - .should('eq', '0 ETH'); - }); - it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeMainValueToFiat(ROOT_ROUTES.projects.absolute); - cy.get('[data-test^=ProjectsView__ProjectsListItem').first().click(); - cy.get('[data-test=ProjectRewards__currentTotal__number]') - .first() - .invoke('text') - .should('eq', '$0.00'); - }); - }); - - describe(`projects (IPFS failure): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - cy.intercept('GET', '**/ipfs/**', req => { - req.destroy(); - }); - - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - }); - - it('entering project view shows Toast with info about IPFS failure when all providers fail', () => { - cy.get('[data-test=Toast--ipfsMessage').should('be.visible'); - }); - }); - - describe(`project (patron mode): ${device}`, { viewportHeight, viewportWidth }, () => { - let projectNames: string[] = []; - - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - connectWallet({ isPatronModeEnabled: true }); - checkProjectsViewLoaded(); - - /** - * This could be done in before hook, but CY wipes the state after each test - * (could be disabled, but creates other problems) - */ - if (projectNames.length === 0) { - projectNames = getNamesOfProjects(); - } - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('button "add to allocate" is disabled', () => { - for (let i = 0; i < projectNames.length; i++) { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').eq(i).click(); - getButtonAddToAllocate().should('be.visible').should('be.disabled'); - cy.go('back'); - } - }); - }); -}); diff --git a/client/cypress/e2e/projects.cy.ts b/client/cypress/e2e/projects.cy.ts deleted file mode 100644 index 7d8c010057..0000000000 --- a/client/cypress/e2e/projects.cy.ts +++ /dev/null @@ -1,310 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - connectWallet, - mockCoinPricesServer, - visitWithLoader, - navigateWithCheck, - checkProjectsViewLoaded, - changeMainValueToFiat, -} from 'cypress/utils/e2e'; -import { moveTime, setupAndMoveToPlayground } from 'cypress/utils/moveTime'; -import { getNamesOfProjects } from 'cypress/utils/projects'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import getMilestones from 'src/constants/milestones'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -import Chainable = Cypress.Chainable; - -chai.use(chaiColors); - -describe('move time', () => { - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - it('allocation window is open, when it is not, move time', () => { - setupAndMoveToPlayground(); - - cy.window().then(async win => { - moveTime(win, 'nextEpochDecisionWindowOpen').then(() => { - const isDecisionWindowOpenAfter = win.clientReactQuery.getQueryData( - QUERY_KEYS.isDecisionWindowOpen, - ); - expect(isDecisionWindowOpenAfter).to.be.true; - }); - }); - }); -}); - -function checkProjectItemElements(index, name, isPatronMode = false): Chainable { - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__imageProfile]') - .should('be.visible'); - cy.get('[data-test^=ProjectsView__ProjectsListItem]') - .eq(index) - .should('be.visible') - .find('[data-test=ProjectsListItem__name]') - .should('be.visible') - .contains(name); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__IntroDescription]') - .should('be.visible'); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .should('be.visible'); - - if (isPatronMode) { - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .should('be.disabled'); - } - - return cy - .get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectRewards]') - .should('be.visible'); - // TODO OCT-663 Make CY check if rewards are available (Epoch 2, decision window open). - // return cy - // .get('[data-test^=ProjectsView__ProjectsListItem') - // .eq(index) - // .find('[data-test=ProjectRewards__currentTotal__label]') - // .should('be.visible'); -} - -function addProjectToAllocate(index, numberOfAddedProjects): Chainable { - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__imageProfile]') - .should('be.visible'); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__IntroDescription]') - .should('be.visible'); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .scrollIntoView(); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .click(); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .find('svg') - .find('path') - .then($el => $el.css('fill')) - .should('be.colored', '#FF6157'); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .find('svg') - .find('path') - .then($el => $el.css('stroke')) - .should('be.colored', '#FF6157'); - cy.get('[data-test=Navbar__numberOfAllocations]').contains(numberOfAddedProjects + 1); - navigateWithCheck(ROOT_ROUTES.allocation.absolute); - cy.get('[data-test=AllocationItem]').should('have.length', numberOfAddedProjects + 1); - return cy.go('back'); -} - -function removeProjectFromAllocate(numberOfProjects, numberOfAddedProjects, index): Chainable { - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .scrollIntoView(); - cy.get('[data-test^=ProjectsView__ProjectsListItem') - .eq(index) - .find('[data-test=ProjectsListItem__ButtonAddToAllocate]') - .click(); - navigateWithCheck(ROOT_ROUTES.allocation.absolute); - cy.get('[data-test=AllocationItem]').should('have.length', numberOfAddedProjects - 1); - if (index < numberOfProjects - 1) { - cy.get('[data-test=Navbar__numberOfAllocations]').contains(numberOfAddedProjects - 1); - } else { - cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); - } - return cy.go('back'); -} - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`projects: ${device}`, { viewportHeight, viewportWidth }, () => { - let projectNames: string[] = []; - - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - checkProjectsViewLoaded(); - - /** - * This could be done in before hook, but CY wipes the state after each test - * (could be disabled, but creates other problems) - */ - if (projectNames.length === 0) { - projectNames = getNamesOfProjects(); - } - }); - - it('user is able to see all the projects in the view', () => { - for (let i = 0; i < projectNames.length; i++) { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').eq(i).scrollIntoView(); - checkProjectItemElements(i, projectNames[i]); - } - }); - - it('user is able to add & remove the first and the last project to/from allocation, triggering change of the icon, change of the number in navbar', () => { - // This test checks the first and the last elements only to save time. - cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); - - addProjectToAllocate(0, 0); - addProjectToAllocate(projectNames.length - 1, 1); - removeProjectFromAllocate(projectNames.length, 2, 0); - removeProjectFromAllocate(projectNames.length, 1, projectNames.length - 1); - }); - - it('user is able to add project to allocation in ProjectsView and remove it from allocation in AllocationView', () => { - cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); - addProjectToAllocate(0, 0); - navigateWithCheck(ROOT_ROUTES.allocation.absolute); - cy.get('[data-test=AllocationItemSkeleton]').should('not.exist'); - cy.get('[data-test=AllocationItem]').then(el => { - const { x } = el[0].getBoundingClientRect(); - cy.get('[data-test=AllocationItem]') - .trigger('pointerdown') - .trigger('pointermove', { pageX: x - 20 }) - .trigger('pointerup', { pageX: x - 40 }); - cy.wait(500); - cy.get('[data-test=AllocationItem__removeButton]').should('be.visible'); - cy.get('[data-test=AllocationItem__removeButton]').click(); - cy.get('[data-test=AllocationItem__removeButton]').should('not.exist'); - cy.get('[data-test=AllocationItem]').should('not.exist'); - cy.get('[data-test=Navbar__numberOfAllocations]').should('not.exist'); - }); - }); - - it('ProjectsTimelineWidgetItem with href opens link when clicked without mouse movement', () => { - const milestones = getMilestones(); - cy.get('[data-test=ProjectsTimelineWidget]').should('be.visible'); - cy.get('[data-test=ProjectsTimelineWidgetItem]').should('have.length', milestones.length); - for (let i = 0; i < milestones.length; i++) { - if (milestones[i].href) { - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .within(() => { - cy.get('[data-test=ProjectsTimelineWidgetItem__Svg--arrowTopRight]').should( - 'be.visible', - ); - }); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .then(el => { - const { x } = el[0].getBoundingClientRect(); - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup', { clientX: x + 10 }); - cy.location('pathname').should('eq', ROOT_ROUTES.projects.absolute); - - cy.get('[data-test=ProjectsTimelineWidgetItem]') - .eq(i) - .trigger('mousedown') - .trigger('mouseup'); - cy.location('pathname').should('not.eq', ROOT_ROUTES.projects.absolute); - }); - } - } - }); - - it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: true)`, () => { - cy.get('[data-test=ProjectRewards__currentTotal__number]') - .first() - .invoke('text') - .should('eq', '0 ETH'); - }); - - it(`shows current total (${IS_CRYPTO_MAIN_VALUE_DISPLAY}: false)`, () => { - changeMainValueToFiat(ROOT_ROUTES.projects.absolute); - - cy.get('[data-test=ProjectRewards__currentTotal__number]') - .first() - .invoke('text') - .should('eq', '$0.00'); - }); - - it('search field -- results should show project', () => { - cy.get('[data-test=ProjectsList__InputText]').clear().type(projectNames[0]); - cy.get('[data-test=ProjectsView__ProjectsList]') - .find('[data-test^=ProjectsView__ProjectsListItem]') - .should('have.length', 1); - }); - - it('search field -- no results should show no results image & text', () => { - cy.get('[data-test=ProjectsList__InputText]') - .clear() - .type('there-is-no-way-there-will-ever-be-a-project-with-such-a-name'); - cy.get('[data-test=ProjectsList__noSearchResults]').should('be.visible'); - cy.get('[data-test=ProjectsList__noSearchResults__Img]').should('be.visible'); - }); - }); - - describe(`projects (patron mode): ${device}`, { viewportHeight, viewportWidth }, () => { - let projectNames: string[] = []; - - before(() => { - /** - * Global Metamask setup done by Synpress is not always done. - * Since Synpress needs to have valid provider to fetch the data from contracts, - * setupMetamask is required in each test suite. - */ - cy.setupMetamask(); - }); - - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - connectWallet({ isPatronModeEnabled: true }); - checkProjectsViewLoaded(); - /** - * This could be done in before hook, but CY wipes the state after each test - * (could be disabled, but creates other problems) - */ - if (projectNames.length === 0) { - projectNames = getNamesOfProjects(); - } - }); - - after(() => { - cy.disconnectMetamaskWalletFromAllDapps(); - }); - - it('button "add to allocate" is disabled', () => { - for (let i = 0; i < projectNames.length; i++) { - cy.get('[data-test^=ProjectsView__ProjectsListItem]').eq(i).scrollIntoView(); - checkProjectItemElements(i, projectNames[i], true); - } - }); - }); -}); diff --git a/client/cypress/e2e/projectsArchive.cy.ts b/client/cypress/e2e/projectsArchive.cy.ts deleted file mode 100644 index be0fbb7611..0000000000 --- a/client/cypress/e2e/projectsArchive.cy.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { - checkLocationWithLoader, - visitWithLoader, - checkProjectsViewLoaded, -} from 'cypress/utils/e2e'; -import { moveTime } from 'cypress/utils/moveTime'; -import viewports from 'cypress/utils/viewports'; -import { QUERY_KEYS } from 'src/api/queryKeys'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -let wasEpochMoved = false; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`projects archive: ${device}`, { viewportHeight, viewportWidth }, () => { - beforeEach(() => { - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.projects.absolute); - }); - - it('moves to the next epoch', () => { - // Move time only once, for the first device. - if (!wasEpochMoved) { - cy.window().then(async win => { - const currentEpochBefore = Number( - win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), - ); - - cy.wrap(null).then(() => { - return moveTime(win, 'nextEpochDecisionWindowClosed').then(() => { - const currentEpochAfter = Number( - win.clientReactQuery.getQueryData(QUERY_KEYS.currentEpoch), - ); - wasEpochMoved = true; - expect(currentEpochBefore + 1).to.eq(currentEpochAfter); - }); - }); - }); - } else { - expect(true).to.be.true; - } - }); - - it('renders archive elements + clicking on epoch archive ProjectsListItem opens ProjectView for particular epoch and project', () => { - cy.get('[data-test=MainLayout__body]').then(el => { - const mainLayoutPaddingTop = parseInt(el.css('paddingTop'), 10); - - checkProjectsViewLoaded(); - cy.get('[data-test=ProjectsView__ProjectsList]') - .should('be.visible') - .children() - .then(children => { - children[children.length - 1].scrollIntoView(); - cy.window().then(window => window.scrollTo(0, window.scrollY - mainLayoutPaddingTop)); - cy.wait(1000); - // header test - cy.get('[data-test=ProjectsView__ProjectsList__header--archive]').should('be.visible'); - - // list test - cy.get('[data-test=ProjectsView__ProjectsList--archive]').first().should('be.visible'); - checkProjectsViewLoaded(); - cy.get('[data-test=ProjectsView__ProjectsList--archive]') - .first() - .children() - .then(childrenArchive => { - const numberOfArchivedProjects = childrenArchive.length - 2; // archived projects tiles - (header + divider)[2] - for (let i = 0; i < numberOfArchivedProjects; i++) { - cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) - .first() - .scrollIntoView(); - cy.window().then(window => - window.scrollTo(0, window.scrollY - mainLayoutPaddingTop), - ); - // list item test - cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) - .first() - .should('be.visible') - .within(() => { - // rewards test - cy.get('[data-test=ProjectRewards]').should('be.visible'); - }); - - if (numberOfArchivedProjects - 1) { - cy.get('[data-test=ProjectsView__ProjectsList--archive]') - .first() - .should('have.length', 1); - } - - checkProjectsViewLoaded(); - cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) - .first() - .invoke('data', 'address') - .then(address => { - cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) - .first() - .invoke('data', 'epoch') - .then(epoch => { - cy.get(`[data-test=ProjectsView__ProjectsListItem--archive--${i}]`) - .first() - .click(); - checkLocationWithLoader( - `${ROOT_ROUTES.project.absolute}/${epoch}/${address}`, - ); - cy.go('back'); - checkLocationWithLoader(ROOT_ROUTES.projects.absolute); - }); - }); - } - }); - }); - }); - }); - }); -}); diff --git a/client/cypress/e2e/rewardsCalculator.cy.ts b/client/cypress/e2e/rewardsCalculator.cy.ts deleted file mode 100644 index 931b7f48ba..0000000000 --- a/client/cypress/e2e/rewardsCalculator.cy.ts +++ /dev/null @@ -1,392 +0,0 @@ -// eslint-disable-next-line import/no-extraneous-dependencies -import chaiColors from 'chai-colors'; - -import { - ETH_USD, - GLM_USD, - changeMainValueToFiat, - mockCoinPricesServer, - visitWithLoader, - changeMainValueToCrypto, -} from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { - HAS_ONBOARDING_BEEN_CLOSED, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; -import getValueCryptoToDisplay from 'src/utils/getValueCryptoToDisplay'; -import getValueFiatToDisplay from 'src/utils/getValueFiatToDisplay'; -import { parseUnitsBigInt } from 'src/utils/parseUnitsBigInt'; - -import Chainable = Cypress.Chainable; - -chai.use(chaiColors); - -const rendersWithCorrectValues = ({ - isCryptoAsAMainValue, - onAfterInterceptCallback, - onAfterOpenCallback, - postAlias, -}: { - isCryptoAsAMainValue: boolean; - onAfterInterceptCallback?: () => Chainable; - onAfterOpenCallback?: () => Chainable; - postAlias: string; -}) => { - cy.intercept('POST', '/rewards/estimated_budget').as(postAlias); - - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - - if (onAfterOpenCallback) { - onAfterOpenCallback(); - } - - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsValue--skeleton]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue--skeleton]').should( - 'be.visible', - ); - - cy.wait(`@${postAlias}`); - - cy.get(`@${postAlias}`).then( - ({ - response: { - body: { budget, matchedFunding }, - }, - }) => { - const rewardsCrypto = getValueCryptoToDisplay({ - cryptoCurrency: 'ethereum', - valueCrypto: parseUnitsBigInt(budget, 'wei'), - }).fullString; - const rewardsFiat = getValueFiatToDisplay({ - cryptoCurrency: 'ethereum', - cryptoValues: { ethereum: { usd: ETH_USD }, golem: { usd: GLM_USD } }, - displayCurrency: 'usd', - valueCrypto: parseUnitsBigInt(budget, 'wei'), - }); - - const matchFundingCrypto = getValueCryptoToDisplay({ - cryptoCurrency: 'ethereum', - valueCrypto: parseUnitsBigInt(matchedFunding, 'wei'), - }).fullString; - const matchFundingFiat = getValueFiatToDisplay({ - cryptoCurrency: 'ethereum', - cryptoValues: { ethereum: { usd: ETH_USD }, golem: { usd: GLM_USD } }, - displayCurrency: 'usd', - valueCrypto: parseUnitsBigInt(matchedFunding, 'wei'), - }); - const rewards = isCryptoAsAMainValue ? rewardsCrypto : rewardsFiat; - const matchFunding = isCryptoAsAMainValue ? matchFundingCrypto : matchFundingFiat; - - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsValue--skeleton]').should( - 'not.exist', - ); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue--skeleton]').should( - 'not.exist', - ); - - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsValue') - .invoke('text') - .should('eq', rewards); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue]') - .invoke('text') - .should('eq', matchFunding); - - if (onAfterInterceptCallback) { - onAfterInterceptCallback(); - } - }, - ); - - cy.get('[data-test=ModalRewardsCalculator__Button]').click(); -}; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`rewards calculator: ${device}`, { viewportHeight, viewportWidth }, () => { - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.earn.absolute); - }); - - it('renders calculator icon inside box', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').should('be.visible'); - }); - - if (isDesktop) { - it('tooltip is visible on calculator icon hover and has correct text', () => { - cy.get('[data-test=Tooltip__rewardsCalculator').trigger('mouseover'); - cy.get('[data-test=Tooltip__rewardsCalculator__content') - .should('be.visible') - .invoke('text') - .should('eq', 'Calculate rewards'); - }); - } - - it('clicking on rewards calculator icon opens rewards calculator modal', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=ModalRewardsCalculator]').should('be.visible'); - }); - - it('GLM amount input is visible, has "5000" as a default value and a "GLM" suffix', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm]') - .invoke('val') - .should('eq', '5000'); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm__suffix]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm__suffix]') - .invoke('text') - .should('eq', 'GLM'); - }); - - it('Days selector is visible, has 3 options (90, 180, 270), "90" as a default value and "DAYS" suffix', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculatorEpochDaysSelector]').should('be.visible'); - cy.get('[data-test*=EarnRewardsCalculatorEpochDaysSelector__label]') - .invoke('text') - .should('eq', 'Lock for 1 epoch'); - cy.get('[data-test*=EarnRewardsCalculatorEpochDaysSelector__option--]').then(options => { - for (let i = 1; i <= options.length; i++) { - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__option--${i}]`) - .then($el => $el.css('color')) - .should('be.colored', i === 1 ? '#171717' : '#cdd1cd'); - cy.get( - `[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--${i}]`, - ).should(i === 1 ? 'exist' : 'not.exist'); - - if (i === 1) { - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--${i}]`) - .then($el => $el.css('background-color')) - .should('be.colored', '#ebebeb'); - } - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionLabel--${i}]`) - .invoke('text') - .should('eq', `${i * 90}`); - } - }); - cy.get('[data-test*=EarnRewardsCalculatorEpochDaysSelector__suffix]') - .invoke('text') - .should('eq', 'Days'); - }); - - it('UQ selector is visible, has 2 options (Yes, No), "Yes" as a default value', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculatorUqSelector]').should('be.visible'); - cy.get('[data-test*=EarnRewardsCalculatorUqSelector__option--]').then(options => { - for (let i = 0; i < options.length; i++) { - cy.get(`[data-test=EarnRewardsCalculatorUqSelector__option--${i}]`) - .then($el => $el.css('color')) - .should('be.colored', i === 0 ? '#171717' : '#cdd1cd'); - cy.get(`[data-test=EarnRewardsCalculatorUqSelector__optionBackground--${i}]`).should( - i === 0 ? 'exist' : 'not.exist', - ); - - if (i === 0) { - cy.get(`[data-test=EarnRewardsCalculatorUqSelector__optionBackground--${i}]`) - .then($el => $el.css('background-color')) - .should('be.colored', '#ebebeb'); - } - cy.get(`[data-test=EarnRewardsCalculatorUqSelector__optionLabel--${i}]`) - .invoke('text') - .should('eq', i === 0 ? 'Yes' : 'No'); - } - }); - }); - - it('Estimates box is visible and has "Rewards" and "Match funding" fields', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculatorEstimates]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculatorEstimates__label]') - .invoke('text') - .should('eq', 'Estimates'); - - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewards]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsLabel]') - .invoke('text') - .should('eq', 'Rewards '); - - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFunding]').should('be.visible'); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingLabel]') - .invoke('text') - .should('eq', 'Match funding'); - }); - - it('User can change days selector value', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculatorEpochDaysSelector__option--2]').click(); - cy.wait(500); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__option--2]`) - .then($el => $el.css('color')) - .should('be.colored', '#171717'); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--2]`).should( - 'exist', - ); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--2]`) - .then($el => $el.css('background-color')) - .should('be.colored', '#ebebeb'); - - cy.get('[data-test=EarnRewardsCalculatorEpochDaysSelector__option--3]').click(); - cy.wait(500); - - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__option--2]`) - .then($el => $el.css('color')) - .should('be.colored', '#cdd1cd'); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--2]`).should( - 'not.exist', - ); - - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__option--3]`) - .then($el => $el.css('color')) - .should('be.colored', '#171717'); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--3]`).should( - 'exist', - ); - cy.get(`[data-test=EarnRewardsCalculatorEpochDaysSelector__optionBackground--3]`) - .then($el => $el.css('background-color')) - .should('be.colored', '#ebebeb'); - }); - - it('Calculator shows "Rewards" and "Match funding" in USD based on GLM input value and days selector option', () => { - changeMainValueToCrypto(ROOT_ROUTES.earn.absolute); - - rendersWithCorrectValues({ - isCryptoAsAMainValue: true, - postAlias: 'postEstimatedRewards-true', - }); - rendersWithCorrectValues({ - isCryptoAsAMainValue: true, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').type('500000'); - }, - postAlias: 'postEstimatedRewardsGlmValueChange-true', - }); - rendersWithCorrectValues({ - isCryptoAsAMainValue: true, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculatorEpochDaysSelector__option--2]').click(); - }, - postAlias: 'postEstimatedRewardsDaysValueChange-true', - }); - - changeMainValueToFiat(ROOT_ROUTES.earn.absolute); - - rendersWithCorrectValues({ - isCryptoAsAMainValue: false, - postAlias: 'postEstimatedRewards-false', - }); - rendersWithCorrectValues({ - isCryptoAsAMainValue: false, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').type('500000'); - }, - postAlias: 'postEstimatedRewardsGlmValueChange-false', - }); - rendersWithCorrectValues({ - isCryptoAsAMainValue: false, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculatorEpochDaysSelector__option--2]').click(); - }, - postAlias: 'postEstimatedRewardsDaysValueChange-false', - }); - }); - - it('If GLM input is empty estimates section fields are empty too', () => { - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').clear(); - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsValue--skeleton]').should( - 'not.exist', - ); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue--skeleton]').should( - 'not.exist', - ); - // Debouce prevents value from being immediately loaded. Let's give it a chance to load. - cy.wait(5000); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue]') - .invoke('text') - .should('eq', ''); - }); - - it('Max GLM amount is 1000000000', () => { - changeMainValueToCrypto(ROOT_ROUTES.earn.absolute); - - rendersWithCorrectValues({ - isCryptoAsAMainValue: true, - onAfterInterceptCallback: () => { - cy.get('[data-test=EarnRewardsCalculator__InputText--glm]') - .clear() - .type('1000000001') - .should('have.css', 'border-color', 'rgb(255, 97, 87)'); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm__error]') - .should('be.visible') - .invoke('text') - .should('eq', 'That isn’t a valid amount'); - }, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').type('1000000000'); - }, - postAlias: 'postEstimatedRewards-true', - }); - - changeMainValueToFiat(ROOT_ROUTES.earn.absolute); - - rendersWithCorrectValues({ - isCryptoAsAMainValue: false, - onAfterInterceptCallback: () => { - cy.get('[data-test=EarnRewardsCalculator__InputText--glm]') - .clear() - .type('1000000001') - .should('have.css', 'border-color', 'rgb(255, 97, 87)'); - cy.get('[data-test=EarnRewardsCalculator__InputText--glm__error]') - .should('be.visible') - .invoke('text') - .should('eq', 'That isn’t a valid amount'); - }, - onAfterOpenCallback: () => { - return cy.get('[data-test=EarnRewardsCalculator__InputText--glm]').type('1000000000'); - }, - postAlias: 'postEstimatedRewards-false', - }); - }); - - it('Closing the modal successfully cancels the request /estimated_budget', () => { - cy.window().then(win => { - cy.spy(win.console, 'error').as('consoleErrSpy'); - }); - - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - - cy.get('[data-test=ModalRewardsCalculator__Button]').click(); - cy.get('[data-test=ModalRewardsCalculator').should('not.be.visible'); - - cy.on('uncaught:exception', error => { - expect(error.code).to.equal('ERR_CANCELED'); - }); - }); - - it('Estimates section shows correct fiat values', () => { - changeMainValueToFiat(ROOT_ROUTES.earn.absolute); - - cy.intercept('POST', '/rewards/estimated_budget', { - body: { budget: '18829579190901', matchedFunding: '18829579190901' }, - delay: 500, - }).as('postEstimatedRewards'); - - cy.get('[data-test=Tooltip__rewardsCalculator__body]').click(); - cy.wait('@postEstimatedRewards'); - - cy.get('@postEstimatedRewards').then(() => { - cy.get('[data-test=EarnRewardsCalculatorEstimates__rewardsValue') - .invoke('text') - .should('eq', '$0.04'); - cy.get('[data-test=EarnRewardsCalculatorEstimates__matchFundingValue]') - .invoke('text') - .should('eq', '$0.04'); - }); - }); - }); -}); diff --git a/client/cypress/e2e/routes.cy.ts b/client/cypress/e2e/routes.cy.ts deleted file mode 100644 index 1d2dc113b8..0000000000 --- a/client/cypress/e2e/routes.cy.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { mockCoinPricesServer, visitWithLoader } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { ROOT, ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight }) => { - describe(`routes (wallet not connected): ${device}`, { viewportHeight, viewportWidth }, () => { - before(() => { - mockCoinPricesServer(); - cy.clearLocalStorage(); - }); - - it('empty route redirects to projects view', () => { - visitWithLoader(ROOT.absolute, ROOT_ROUTES.projects.absolute); - cy.get('[data-test=ProjectsView]').should('be.visible'); - }); - - it('allocation route redirects to allocation view', () => { - visitWithLoader(ROOT_ROUTES.allocation.absolute); - cy.get('[data-test=AllocationView]').should('be.visible'); - }); - - it('earn route redirects to earn view', () => { - visitWithLoader(ROOT_ROUTES.earn.absolute); - cy.get('[data-test=EarnView]').should('be.visible'); - }); - - it('metrics route redirects to metrics view', () => { - visitWithLoader(ROOT_ROUTES.metrics.absolute); - cy.get('[data-test=MetricsView]').should('be.visible'); - }); - - it('projects route redirects to projects view', () => { - visitWithLoader(ROOT_ROUTES.projects.absolute); - cy.get('[data-test=ProjectsView]').should('be.visible'); - }); - - it('settings route redirects to settings view', () => { - visitWithLoader(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsView]').should('be.visible'); - }); - }); -}); diff --git a/client/cypress/e2e/settings.cy.ts b/client/cypress/e2e/settings.cy.ts deleted file mode 100644 index 36e4b2ca43..0000000000 --- a/client/cypress/e2e/settings.cy.ts +++ /dev/null @@ -1,209 +0,0 @@ -import { visitWithLoader, navigateWithCheck, mockCoinPricesServer } from 'cypress/utils/e2e'; -import viewports from 'cypress/utils/viewports'; -import { FIAT_CURRENCIES_SYMBOLS, DISPLAY_CURRENCIES } from 'src/constants/currencies'; -import { - ARE_OCTANT_TIPS_ALWAYS_VISIBLE, - DISPLAY_CURRENCY, - HAS_ONBOARDING_BEEN_CLOSED, - IS_CRYPTO_MAIN_VALUE_DISPLAY, - IS_ONBOARDING_ALWAYS_VISIBLE, - IS_ONBOARDING_DONE, -} from 'src/constants/localStorageKeys'; -import { OCTANT_BUILD_LINK, OCTANT_DOCS, DISCORD_LINK, TERMS_OF_USE } from 'src/constants/urls'; -import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; -import getValueCryptoToDisplay from 'src/utils/getValueCryptoToDisplay'; - -Object.values(viewports).forEach(({ device, viewportWidth, viewportHeight, isDesktop }) => { - describe(`settings: ${device}`, { viewportHeight, viewportWidth }, () => { - beforeEach(() => { - mockCoinPricesServer(); - localStorage.setItem(IS_ONBOARDING_ALWAYS_VISIBLE, 'false'); - localStorage.setItem(IS_ONBOARDING_DONE, 'true'); - localStorage.setItem(HAS_ONBOARDING_BEEN_CLOSED, 'true'); - visitWithLoader(ROOT_ROUTES.settings.absolute); - }); - - it('"Always show Allocate onboarding" option toggle works', () => { - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').check(); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('true'); - }); - - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').click(); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('not.be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('false'); - }); - - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').click(); - cy.get('[data-test=SettingsShowOnboardingBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_ONBOARDING_ALWAYS_VISIBLE)).eq('true'); - }); - }); - - it(`${IS_CRYPTO_MAIN_VALUE_DISPLAY} option is checked by default`, () => { - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); - }); - }); - - it(`${IS_CRYPTO_MAIN_VALUE_DISPLAY} option toggle works`, () => { - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').check(); - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); - }); - - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').click(); - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('not.be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('false'); - }); - - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').click(); - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(IS_CRYPTO_MAIN_VALUE_DISPLAY)).eq('true'); - }); - }); - - it(`${IS_CRYPTO_MAIN_VALUE_DISPLAY} option by default displays crypto value as primary in DoubleValue component`, () => { - navigateWithCheck(ROOT_ROUTES.earn.absolute); - - const cryptoValue = getValueCryptoToDisplay({ - cryptoCurrency: 'golem', - valueCrypto: BigInt(0), - }).fullString; - - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]') - .invoke('text') - .should('eq', cryptoValue); - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__secondary]') - .invoke('text') - .should('not.eq', cryptoValue); - }); - - it(`${IS_CRYPTO_MAIN_VALUE_DISPLAY} option changes DoubleValue sections order`, () => { - cy.get('[data-test=SettingsCryptoMainValueBox__InputToggle]').uncheck(); - navigateWithCheck(ROOT_ROUTES.earn.absolute); - - const cryptoValue = getValueCryptoToDisplay({ - cryptoCurrency: 'golem', - valueCrypto: BigInt(0), - }).fullString; - - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__primary]') - .invoke('text') - .should('not.eq', cryptoValue); - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__secondary]') - .invoke('text') - .should('eq', cryptoValue); - }); - - it('"Choose a display currency" option works', () => { - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(DISPLAY_CURRENCY)).eq('"usd"'); - }); - - for (let i = 0; i < DISPLAY_CURRENCIES.length - 1; i++) { - const displayCurrency = DISPLAY_CURRENCIES[i]; - const displayCurrencyToUppercase = displayCurrency.toUpperCase(); - const nextDisplayCurrencyToUppercase = - i < DISPLAY_CURRENCIES.length - 1 ? DISPLAY_CURRENCIES[i + 1].toUpperCase() : undefined; - - cy.get('[data-test=SettingsCurrencyBox__InputSelect--currency__SingleValue]').contains( - displayCurrencyToUppercase, - ); - navigateWithCheck(ROOT_ROUTES.earn.absolute); - - if (FIAT_CURRENCIES_SYMBOLS[displayCurrency]) { - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__secondary]').contains( - FIAT_CURRENCIES_SYMBOLS[displayCurrency], - ); - } else { - cy.get('[data-test=BoxGlmLock__Section--effective__DoubleValue__secondary]').contains( - displayCurrencyToUppercase, - ); - } - - navigateWithCheck(ROOT_ROUTES.settings.absolute); - cy.get('[data-test=SettingsCurrencyBox__InputSelect--currency]').click(); - cy.get( - `[data-test=SettingsCurrencyBox__InputSelect--currency__Option--${nextDisplayCurrencyToUppercase}]`, - ).click(); - } - }); - - it('"Always show Octant tips" option toggle works', () => { - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').check(); - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('true'); - }); - - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').click(); - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('not.be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('false'); - }); - - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').click(); - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').should('be.checked'); - cy.getAllLocalStorage().then(() => { - expect(localStorage.getItem(ARE_OCTANT_TIPS_ALWAYS_VISIBLE)).eq('true'); - }); - }); - - it('"Always show Octant tips" works (checked)', () => { - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').check(); - - navigateWithCheck(ROOT_ROUTES.earn.absolute); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('exist'); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('be.visible'); - - cy.get('[data-test=EarnView__TipTile--connectWallet__Button]').click(); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('not.exist'); - - cy.reload(); - - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('exist'); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('be.visible'); - }); - - it('"Always show Octant tips" works (unchecked)', () => { - cy.get('[data-test=SettingsShowTipsBox__InputToggle]').uncheck(); - - navigateWithCheck(ROOT_ROUTES.earn.absolute); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('exist'); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('be.visible'); - - cy.get('[data-test=EarnView__TipTile--connectWallet__Button]').click(); - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('not.exist'); - - cy.reload(); - - cy.get('[data-test=EarnView__TipTile--connectWallet]').should('not.exist'); - }); - - it('should show correct setting links', () => { - cy.get('[data-test=SettingsLinkBoxes__Button]').each(($button, index) => { - const expectedOrderAndContentLinksMobile = [ - { href: OCTANT_BUILD_LINK, text: isDesktop ? 'Visit the website' : 'Website' }, - { href: OCTANT_DOCS, text: isDesktop ? 'Check out the docs' : 'Docs' }, - { href: DISCORD_LINK, text: isDesktop ? 'Join our Discord' : 'Discord' }, - ]; - - cy.wrap($button) - .should('have.text', expectedOrderAndContentLinksMobile[index].text) - .should('have.attr', 'href', expectedOrderAndContentLinksMobile[index].href); - - cy.get('[data-test=SettingsMainInfoBox__Button]') - .should('have.text', 'Terms & Conditions') - .and('have.attr', 'href', TERMS_OF_USE); - }); - }); - }); -}); diff --git a/client/cypress/utils/e2e.ts b/client/cypress/utils/e2e.ts index fdd1e57b06..8797be5874 100644 --- a/client/cypress/utils/e2e.ts +++ b/client/cypress/utils/e2e.ts @@ -1,4 +1,6 @@ -import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; +// TODO: https://linear.app/golemfoundation/issue/OCT-1891/e2e-layout +// import { navigationTabs } from 'src/constants/navigationTabs/navigationTabs'; + import { ROOT_ROUTES } from 'src/routes/RootRoutes/routes'; import { ConnectWalletParameters } from './types'; @@ -29,7 +31,9 @@ export const visitWithLoader = ( }; export const navigateWithCheck = (urlEnter: string): Chainable => { - const { label } = navigationTabs.find(({ to }) => to === urlEnter)!; + // TODO: https://linear.app/golemfoundation/issue/OCT-1891/e2e-layout + // const { label } = navigationTabs.find(({ to }) => to === urlEnter)!; + const label = 'Home'; cy.get(`[data-test=Navbar__Button--${label}]`).click(); return checkLocationWithLoader(urlEnter); }; diff --git a/client/index.html b/client/index.html index 200b3618fb..1ac9ae4759 100644 --- a/client/index.html +++ b/client/index.html @@ -1,7 +1,6 @@ - + + + Octant App diff --git a/client/package.json b/client/package.json index ff8d2f17aa..0f96742172 100644 --- a/client/package.json +++ b/client/package.json @@ -40,6 +40,8 @@ "@sentry/vite-plugin": "^2.17.0", "@tanstack/react-query": "^5.37.1", "@wagmi/core": "^2.13.4", + "@uiw/react-markdown-preview": "^5.1.3", + "@vimeo/player": "^2.24.0", "axios": "^1.7.2", "classnames": "^2.5.1", "date-fns": "^3.6.0", @@ -78,6 +80,7 @@ "@types/react-dom": "^18.3.0", "@types/react-slider": "^1.3.6", "@types/react-vis": "^1.11.15", + "@types/vimeo__player": "^2.18.3", "@typescript-eslint/eslint-plugin": "^7.8.0", "@typescript-eslint/parser": "^7.8.0", "@vitejs/plugin-react": "4.3.0", diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-100.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-100.woff2 new file mode 100644 index 0000000000..bfaf238b85 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-100.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-100italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-100italic.woff2 new file mode 100644 index 0000000000..744464311e Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-100italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-200.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-200.woff2 new file mode 100644 index 0000000000..3731913899 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-200.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-200italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-200italic.woff2 new file mode 100644 index 0000000000..ec40e352ae Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-200italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-300.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-300.woff2 new file mode 100644 index 0000000000..c47c5189a3 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-300.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-300italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-300italic.woff2 new file mode 100644 index 0000000000..7b59c6f3e2 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-300italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-500.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-500.woff2 new file mode 100644 index 0000000000..c5aba164fa Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-500.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-500italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-500italic.woff2 new file mode 100644 index 0000000000..75992c428c Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-500italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-600.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-600.woff2 new file mode 100644 index 0000000000..86a4abfc87 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-600.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-600italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-600italic.woff2 new file mode 100644 index 0000000000..2a7a02557f Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-600italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-700.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-700.woff2 new file mode 100644 index 0000000000..ba0b293881 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-700.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-700italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-700italic.woff2 new file mode 100644 index 0000000000..8ea3d1ed27 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-700italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-800.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-800.woff2 new file mode 100644 index 0000000000..b9b41ea178 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-800.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-800italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-800italic.woff2 new file mode 100644 index 0000000000..2e403d1933 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-800italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-900.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-900.woff2 new file mode 100644 index 0000000000..6edf0f1952 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-900.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-900italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-900italic.woff2 new file mode 100644 index 0000000000..8d7cccab51 Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-900italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-italic.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-italic.woff2 new file mode 100644 index 0000000000..bee95b75ce Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-italic.woff2 differ diff --git a/client/public/fonts/inter-tight/inter-tight-v7-latin-regular.woff2 b/client/public/fonts/inter-tight/inter-tight-v7-latin-regular.woff2 new file mode 100644 index 0000000000..308f3c02df Binary files /dev/null and b/client/public/fonts/inter-tight/inter-tight-v7-latin-regular.woff2 differ diff --git a/client/public/images/funds_swept.webp b/client/public/images/funds_swept.webp deleted file mode 100644 index f7b3fc1c11..0000000000 Binary files a/client/public/images/funds_swept.webp and /dev/null differ diff --git a/client/public/images/leafBlower.webp b/client/public/images/leafBlower.webp new file mode 100644 index 0000000000..9fd25392a2 Binary files /dev/null and b/client/public/images/leafBlower.webp differ diff --git a/client/public/images/lock-glm-desktop.webp b/client/public/images/lock-glm-desktop.webp deleted file mode 100644 index 3aeb02a0b6..0000000000 Binary files a/client/public/images/lock-glm-desktop.webp and /dev/null differ diff --git a/client/public/images/lock-glm-mobile.webp b/client/public/images/lock-glm-mobile.webp deleted file mode 100644 index da185519a1..0000000000 Binary files a/client/public/images/lock-glm-mobile.webp and /dev/null differ diff --git a/client/public/images/modalEffectiveLockedBalance.webp b/client/public/images/modalEffectiveLockedBalance.webp deleted file mode 100644 index 746d0205c3..0000000000 Binary files a/client/public/images/modalEffectiveLockedBalance.webp and /dev/null differ diff --git a/client/public/images/swept.webp b/client/public/images/swept.webp new file mode 100644 index 0000000000..3384063874 Binary files /dev/null and b/client/public/images/swept.webp differ diff --git a/client/public/images/sybil.webp b/client/public/images/sybil.webp new file mode 100644 index 0000000000..4ce1d528aa Binary files /dev/null and b/client/public/images/sybil.webp differ diff --git a/client/public/images/tip-connect-wallet.webp b/client/public/images/tip-connect-wallet.webp deleted file mode 100644 index 963b70f33d..0000000000 Binary files a/client/public/images/tip-connect-wallet.webp and /dev/null differ diff --git a/client/public/images/tip-withdraw.webp b/client/public/images/tip-withdraw.webp deleted file mode 100644 index e0ce55c116..0000000000 Binary files a/client/public/images/tip-withdraw.webp and /dev/null differ diff --git a/client/public/images/window-with-dog.webp b/client/public/images/window-with-dog.webp new file mode 100644 index 0000000000..3ef76a701c Binary files /dev/null and b/client/public/images/window-with-dog.webp differ diff --git a/client/src/App.tsx b/client/src/App.tsx index 0d88e6d9c1..2fd5bbb4de 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -3,7 +3,9 @@ import React, { ReactElement, useState, Fragment } from 'react'; import { useAccount } from 'wagmi'; import AppLoader from 'components/shared/AppLoader'; +import Layout from 'components/shared/Layout'; import ModalOnboarding from 'components/shared/ModalOnboarding/ModalOnboarding'; +import ModalTimeoutListPresence from 'components/shared/ModalTimeoutListPresence'; import OnboardingStepper from 'components/shared/OnboardingStepper'; import useAppConnectManager from 'hooks/helpers/useAppConnectManager'; import useAppIsLoading from 'hooks/helpers/useAppIsLoading'; @@ -44,8 +46,11 @@ const App = (): ReactElement => { return ( - + + + {!isSyncingInProgress && !isProjectAdminMode && } + {isConnected && !isOnboardingDone && !isOnboardingModalOpen && } diff --git a/client/src/api/calls/antisybilStatus.ts b/client/src/api/calls/antisybilStatus.ts index 75a8d5b209..50e9af5a7b 100644 --- a/client/src/api/calls/antisybilStatus.ts +++ b/client/src/api/calls/antisybilStatus.ts @@ -3,6 +3,7 @@ import apiService from 'services/apiService'; export type Response = { expires_at: string; + isOnTimeOutList: boolean; score: string; status: string; }; diff --git a/client/src/api/calls/epochInfo.ts b/client/src/api/calls/epochInfo.ts index ee377d6533..84ae716cec 100644 --- a/client/src/api/calls/epochInfo.ts +++ b/client/src/api/calls/epochInfo.ts @@ -5,6 +5,7 @@ import apiService from 'services/apiService'; export type Response = { communityFund: string | null; + donatedToProjects: string | null; leftover: string | null; matchedRewards: string | null; operationalCost: string; diff --git a/client/src/api/calls/karmaGap.ts b/client/src/api/calls/karmaGap.ts new file mode 100644 index 0000000000..6e666565c2 --- /dev/null +++ b/client/src/api/calls/karmaGap.ts @@ -0,0 +1,45 @@ +import { API_ENDPOINT } from 'constants/karmaGap'; +import apiService from 'services/apiService'; + +export type GrantsPerProgram = { + data: { + milestones: { + createdAt: string; + data: { + description: string; + endsAt: number; + title: string; // Timestamp. + }; + updatedAt: string; + }[]; + project: { + details: { + data: { + description: string; + slug: string; + title: string; + }; + recipient: string; + }; + // externalAddresses is set only when recipient does not match project has in Octant. + externalAddresses?: { + octant: string; + }; + recipient: string; + }; + recipient: string; + // Date; + uid: string; + updatedAt: string; + }[]; +}; + +export async function apiGetGrantsPerProgram( + selectedProgramIds: string, +): Promise { + return apiService + .get( + `${API_ENDPOINT}communities/octant/grants?page=0&pageLimit=100&selectedProgramIds=${selectedProgramIds}`, + ) + .then(({ data }) => data); +} diff --git a/client/src/api/calls/projects.ts b/client/src/api/calls/projects.ts index 509973bb43..b487f0a873 100644 --- a/client/src/api/calls/projects.ts +++ b/client/src/api/calls/projects.ts @@ -30,3 +30,20 @@ export type Projects = { export async function apiGetProjects(epoch: number): Promise { return apiService.get(`${env.serverEndpoint}projects/epoch/${epoch}`).then(({ data }) => data); } + +export type ProjectsSearchResults = { + projectsDetails: { + address: string; + epoch: string; + name: string; + }[]; +}; + +export async function apiGetProjectsSearch( + epochs: string, + searchPhrases: string, +): Promise { + return apiService + .get(`${env.serverEndpoint}projects/details?epochs=${epochs}&searchPhrases=${searchPhrases}`) + .then(({ data }) => data); +} diff --git a/client/src/api/calls/rewardsRate.ts b/client/src/api/calls/rewardsRate.ts new file mode 100644 index 0000000000..faad479cb5 --- /dev/null +++ b/client/src/api/calls/rewardsRate.ts @@ -0,0 +1,17 @@ +import { GenericAbortSignal } from 'axios'; + +import env from 'env'; +import apiService from 'services/apiService'; + +export type Response = { + rewardsRate: number; +}; + +export async function apiGetRewardsRate( + epoch: number, + signal?: GenericAbortSignal, +): Promise { + return apiService + .get(`${env.serverEndpoint}epochs/rewards-rate/${epoch}`, { signal }) + .then(({ data }) => data); +} diff --git a/client/src/api/calls/userAllocations.ts b/client/src/api/calls/userAllocations.ts index bc33fc6912..7f463588fb 100644 --- a/client/src/api/calls/userAllocations.ts +++ b/client/src/api/calls/userAllocations.ts @@ -1,7 +1,7 @@ import env from 'env'; import apiService from 'services/apiService'; -export type Response = { +export type GetUserAllocationsResponse = { allocations: { address: string; // Funds allocated by user for the project in WEI @@ -10,8 +10,25 @@ export type Response = { isManuallyEdited: boolean | null; }; -export async function apiGetUserAllocations(address: string, epoch: number): Promise { +export async function apiGetUserAllocations( + address: string, + epoch: number, +): Promise { return apiService .get(`${env.serverEndpoint}allocations/user/${address}/epoch/${epoch}`) .then(({ data }) => data); } + +export type AllocationsPerProjectResponse = { + address: string; + amount: string; +}[]; + +export async function apiGetAllocationsPerProject( + projectAddress: string, + epoch: number, +): Promise { + return apiService + .get(`${env.serverEndpoint}allocations/project/${projectAddress}/epoch/${epoch}`) + .then(({ data }) => data); +} diff --git a/client/src/api/calls/vimeo.ts b/client/src/api/calls/vimeo.ts new file mode 100644 index 0000000000..48b7c14070 --- /dev/null +++ b/client/src/api/calls/vimeo.ts @@ -0,0 +1,28 @@ +import apiService from 'services/apiService'; + +export type GetAlbumVideosResponse = { + name: string; + player_embed_url: string; + user: { + name: string; + }; +}[]; + +const vimeoApiEndpoint = 'https://api.vimeo.com'; +const vimeoUserId = 124198022; +const vimeoAlbumId = 11407049; +const publicAccessToken = '959f7b9a5a73689d684e0b60e979b6b4'; +const fields = [['name', 'player_embed_url', 'user.name']]; + +export async function vimeoApiGetAlbumVideos(): Promise { + return apiService + .get( + `${vimeoApiEndpoint}/users/${vimeoUserId}/albums/${vimeoAlbumId}/videos?fields=${fields.join(',')}&sort=manual&per_page=100`, + { + headers: { + Authorization: `bearer ${publicAccessToken}`, + }, + }, + ) + .then(({ data }) => data.data); +} diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index 1d3a11f67b..223c15c404 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -15,12 +15,15 @@ export const ROOTS: Root = { epochesEndTime: 'epochesEndTime', estimatedEffectiveDeposit: 'estimatedEffectiveDeposit', individualReward: 'individualReward', + karmaGapMilestonesPerProjectPerGrantPerProgram: 'karmaGapMilestonesPerProjectPerGrantPerProgram', matchedProjectRewards: 'matchedProjectRewards', patronMode: 'patronMode', projectRewardsThreshold: 'projectRewardsThreshold', projectsDonors: 'projectsDonors', projectsEpoch: 'projectsEpoch', projectsIpfsResults: 'projectsIpfsResults', + rewardsRate: 'rewardsRate', + searchResultsDetails: 'searchResultsDetails', upcomingBudget: 'upcomingBudget', uqScore: 'uqScore', userAllocationNonce: 'userAllocationNonce', @@ -51,6 +54,11 @@ export const QUERY_KEYS: QueryKeys = { individualProjectRewards: ['individualProjectRewards'], individualReward: epochNumber => [ROOTS.individualReward, epochNumber.toString()], isDecisionWindowOpen: ['isDecisionWindowOpen'], + karmaGapMilestonesPerProjectPerGrantPerProgram: (selectedProgramIds, projectAddress) => [ + ROOTS.karmaGapMilestonesPerProjectPerGrantPerProgram, + selectedProgramIds, + projectAddress, + ], largestLockedAmount: ['largestLockedAmount'], lockedSummaryLatest: ['lockedSummaryLatest'], lockedSummarySnapshots: ['lockedSummarySnapshots'], @@ -66,6 +74,9 @@ export const QUERY_KEYS: QueryKeys = { ], projectsMetadataAccumulateds: ['projectsMetadataAccumulateds'], projectsMetadataPerEpoches: ['projectsMetadataPerEpoches'], + rewardsRate: epochNumber => [ROOTS.rewardsRate, epochNumber.toString()], + searchResults: ['searchResults'], + searchResultsDetails: (address, epoch) => [ROOTS.searchResultsDetails, address, epoch.toString()], syncStatus: ['syncStatus'], totalAddresses: ['totalAddresses'], totalWithdrawals: ['totalWithdrawals'], @@ -75,5 +86,6 @@ export const QUERY_KEYS: QueryKeys = { userAllocationNonce: userAddress => [ROOTS.userAllocationNonce, userAddress], userAllocations: epochNumber => [ROOTS.userAllocations, epochNumber.toString()], userTOS: userAddress => [ROOTS.userTOS, userAddress], + vimeoVideos: ['vimeoVideos'], withdrawals: ['withdrawals'], }; diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index bf4f1e0851..1ba6f05f85 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -15,12 +15,15 @@ export type Root = { epochesEndTime: 'epochesEndTime'; estimatedEffectiveDeposit: 'estimatedEffectiveDeposit'; individualReward: 'individualReward'; + karmaGapMilestonesPerProjectPerGrantPerProgram: 'karmaGapMilestonesPerProjectPerGrantPerProgram'; matchedProjectRewards: 'matchedProjectRewards'; patronMode: 'patronMode'; projectRewardsThreshold: 'projectRewardsThreshold'; projectsDonors: 'projectsDonors'; projectsEpoch: 'projectsEpoch'; projectsIpfsResults: 'projectsIpfsResults'; + rewardsRate: 'rewardsRate'; + searchResultsDetails: 'searchResultsDetails'; upcomingBudget: 'upcomingBudget'; uqScore: 'uqScore'; userAllocationNonce: 'userAllocationNonce'; @@ -53,6 +56,10 @@ export type QueryKeys = { individualProjectRewards: ['individualProjectRewards']; individualReward: (epochNumber: number) => [Root['individualReward'], string]; isDecisionWindowOpen: ['isDecisionWindowOpen']; + karmaGapMilestonesPerProjectPerGrantPerProgram: ( + selectedProgramIds: string, + projectAddress: string, + ) => [Root['karmaGapMilestonesPerProjectPerGrantPerProgram'], string, string]; largestLockedAmount: ['largestLockedAmount']; lockedSummaryLatest: ['lockedSummaryLatest']; lockedSummarySnapshots: ['lockedSummarySnapshots']; @@ -67,6 +74,12 @@ export type QueryKeys = { ) => [Root['projectsIpfsResults'], string, string]; projectsMetadataAccumulateds: ['projectsMetadataAccumulateds']; projectsMetadataPerEpoches: ['projectsMetadataPerEpoches']; + rewardsRate: (epochNumber: number) => [Root['rewardsRate'], string]; + searchResults: ['searchResults']; + searchResultsDetails: ( + address: string, + epoch: number, + ) => [Root['searchResultsDetails'], string, string]; syncStatus: ['syncStatus']; totalAddresses: ['totalAddresses']; totalWithdrawals: ['totalWithdrawals']; @@ -76,5 +89,6 @@ export type QueryKeys = { userAllocationNonce: (userAddress: string) => [Root['userAllocationNonce'], string]; userAllocations: (epochNumber: number) => [Root['userAllocations'], string]; userTOS: (userAddress: string) => [Root['userTOS'], string]; + vimeoVideos: ['vimeoVideos']; withdrawals: ['withdrawals']; }; diff --git a/client/src/components/Allocation/Allocation.module.scss b/client/src/components/Allocation/Allocation.module.scss new file mode 100644 index 0000000000..d939886a0a --- /dev/null +++ b/client/src/components/Allocation/Allocation.module.scss @@ -0,0 +1,97 @@ +.root { + width: 100%; + display: flex; + flex-direction: column; + flex: 1; + + @media #{$desktop-up} { + padding-bottom: 0; + width: 68rem; + min-height: 0; + } + + .title { + @include fontBig($font-size-24); + padding: 0; + display: flex; + align-items: center; + min-height: 8.8rem; + width: 100%; + color: $color-octant-dark; + line-height: 1.4rem; + + @media #{$tablet-up} { + @include fontBig($font-size-32); + min-height: 14.4rem; + } + + @media #{$desktop-up} { + padding: 0 4rem; + } + } + + .box { + &:not(:last-child) { + margin: 0 auto 1.6rem; + } + } + + .boxesWrapper { + width: 100%; + flex: 1; + display: flex; + flex-direction: column; + + &.withMarginBottom { + margin-bottom: 12.8rem; + } + + &.withPaddingBottom { + padding-bottom: 4rem; + } + + @media #{$desktop-up} { + padding: 0 4rem; + overflow: auto; + } + } + + .emptyState { + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + flex: 1; + text-align: center; + + .emptyStateImage { + width: 12rem; + + @media #{$tablet-up} { + width: 15rem; + } + } + + .emptyStateText { + height: 7.2rem; + margin-top: 1.8rem; + color: $color-octant-grey5; + font-size: $font-size-14; + font-weight: $font-weight-semibold; + line-height: 2.2rem; + + .projectsLink { + font-size: $font-size-14; + } + + @media #{$tablet-up} { + font-size: $font-size-16; + line-height: 2.4rem; + + .projectsLink { + font-size: $font-size-16; + } + } + } + } +} diff --git a/client/src/components/Allocation/Allocation.tsx b/client/src/components/Allocation/Allocation.tsx new file mode 100644 index 0000000000..c76fd1caa0 --- /dev/null +++ b/client/src/components/Allocation/Allocation.tsx @@ -0,0 +1,718 @@ +import cx from 'classnames'; +import { AnimatePresence } from 'framer-motion'; +import debounce from 'lodash/debounce'; +import isEmpty from 'lodash/isEmpty'; +import React, { ReactElement, useCallback, useEffect, useMemo, useRef, useState } from 'react'; +import { createPortal } from 'react-dom'; +import { Trans, useTranslation } from 'react-i18next'; +import { useAccount } from 'wagmi'; + +import { SignatureOpType, apiGetPendingMultisigSignatures } from 'api/calls/multisigSignatures'; +import AllocationItem from 'components/Allocation/AllocationItem'; +import AllocationItemSkeleton from 'components/Allocation/AllocationItemSkeleton'; +import AllocationNavigation from 'components/Allocation/AllocationNavigation'; +import AllocationRewardsBox from 'components/Allocation/AllocationRewardsBox'; +import AllocationSummary from 'components/Allocation/AllocationSummary'; +import ModalAllocationLowUqScore from 'components/Allocation/ModalAllocationLowUqScore'; +import Button from 'components/ui/Button'; +import Img from 'components/ui/Img'; +import { DRAWER_TRANSITION_TIME } from 'constants/animations'; +import { LAYOUT_NAVBAR_ID } from 'constants/domElementsIds'; +import { UQ_MULTIPLIER_FOR_USERS_BELOW_THRESHOLD_FOR_LEVERAGE_1 } from 'constants/uq'; +import useAllocate from 'hooks/events/useAllocate'; +import useAllocationViewSetRewardsForProjects from 'hooks/helpers/useAllocationViewSetRewardsForProjects'; +import useIdsInAllocation from 'hooks/helpers/useIdsInAllocation'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useAllocateSimulate from 'hooks/mutations/useAllocateSimulate'; +import useRefreshAntisybilStatus from 'hooks/mutations/useRefreshAntisybilStatus'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useEpochAllocations from 'hooks/queries/useEpochAllocations'; +import useHistory from 'hooks/queries/useHistory'; +import useIndividualReward from 'hooks/queries/useIndividualReward'; +import useIsContract from 'hooks/queries/useIsContract'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useMatchedProjectRewards from 'hooks/queries/useMatchedProjectRewards'; +import useProjectsEpoch from 'hooks/queries/useProjectsEpoch'; +import useProjectsIpfsWithRewards from 'hooks/queries/useProjectsIpfsWithRewards'; +import useUpcomingBudget from 'hooks/queries/useUpcomingBudget'; +import useUqScore from 'hooks/queries/useUqScore'; +import useUserAllocationNonce from 'hooks/queries/useUserAllocationNonce'; +import useUserAllocations from 'hooks/queries/useUserAllocations'; +import useWithdrawals from 'hooks/queries/useWithdrawals'; +import { ROOT_ROUTES } from 'routes/RootRoutes/routes'; +import toastService from 'services/toastService'; +import useAllocationsStore from 'store/allocations/store'; +import { formatUnitsBigInt } from 'utils/formatUnitsBigInt'; +import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; + +import styles from './Allocation.module.scss'; +import AllocationSliderBox from './AllocationSliderBox'; +import { AllocationValue, AllocationValues, PercentageProportions } from './types'; +import { + getAllocationValuesInitialState, + getAllocationsWithRewards, + getAllocationValuesAfterManualChange, +} from './utils'; + +const Allocation = (): ReactElement => { + const { isConnected } = useAccount(); + const keyPrefix = 'components.allocation'; + const { t } = useTranslation('translation', { keyPrefix }); + const [allocationValues, setAllocationValues] = useState([]); + const [isManualMode, setIsManualMode] = useState(false); + const [addressesWithError, setAddressesWithError] = useState([]); + const [percentageProportions, setPercentageProportions] = useState({}); + const { data: projectsEpoch } = useProjectsEpoch(); + const { data: projectsIpfsWithRewards } = useProjectsIpfsWithRewards(); + const { isRewardsForProjectsSet } = useAllocationViewSetRewardsForProjects(); + const [isWaitingForFirstMultisigSignature, setIsWaitingForFirstMultisigSignature] = + useState(false); + const { + data: allocationSimulated, + mutateAsync: mutateAsyncAllocateSimulate, + isPending: isLoadingAllocateSimulate, + reset: resetAllocateSimulate, + } = useAllocateSimulate(); + const [isWaitingForAllMultisigSignatures, setIsWaitingForAllMultisigSignatures] = useState(false); + const { isFetching: isFetchingUpcomingBudget, isRefetching: isRefetchingUpcomingBudget } = + useUpcomingBudget(); + const { data: isContract } = useIsContract(); + const { address: walletAddress } = useAccount(); + const [showAllocationNav, setShowAllocationNav] = useState(false); + const [isEmptyStateImageVisible, setIsEmptyStateImageVisible] = useState(true); + + const navRef = useRef(document.getElementById(LAYOUT_NAVBAR_ID)); + const boxesWrapperRef = useRef(null); + const allocationEmptyStateRef = useRef(null); + const { data: currentEpoch } = useCurrentEpoch(); + const { refetch: refetchHistory } = useHistory(); + const { + data: userAllocationsOriginal, + isFetching: isFetchingUserAllocation, + refetch: refetchUserAllocations, + } = useUserAllocations(undefined, { refetchOnMount: true }); + + const userAllocations = userAllocationsOriginal && { + ...userAllocationsOriginal, + elements: userAllocationsOriginal.elements.map(element => ({ + ...element, + value: formatUnitsBigInt(element.value), + })), + }; + + const { data: individualReward } = useIndividualReward(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const { refetch: refetchWithdrawals } = useWithdrawals(); + const { + data: userNonce, + isFetching: isFetchingUserNonce, + refetch: refetchUserAllocationNonce, + } = useUserAllocationNonce(); + const { + mutateAsync: refreshAntisybilStatus, + isPending: isPendingRefreshAntisybilStatus, + isSuccess: isSuccessRefreshAntisybilStatus, + error: refreshAntisybilStatusError, + } = useRefreshAntisybilStatus(); + const { data: uqScore, isFetching: isFetchingUqScore } = useUqScore(currentEpoch!, { + enabled: + isSuccessRefreshAntisybilStatus || + (refreshAntisybilStatusError as null | { message: string })?.message === + 'Address is already used for delegation', + }); + const { refetch: refetchMatchedProjectRewards } = useMatchedProjectRewards(); + const [showLowUQScoreModal, setShowLowUQScoreModal] = useState(false); + const { refetch: refetchEpochAllocations } = useEpochAllocations( + isDecisionWindowOpen ? currentEpoch! - 1 : currentEpoch!, + { + enabled: isDecisionWindowOpen === true, + }, + ); + const { isMobile, isTablet, isDesktop } = useMediaQuery(); + + const { + currentView, + setCurrentView, + allocations, + rewardsForProjects, + setAllocations, + addAllocations, + removeAllocations, + setRewardsForProjects, + } = useAllocationsStore(state => ({ + addAllocations: state.addAllocations, + allocations: state.data.allocations, + currentView: state.data.currentView, + removeAllocations: state.removeAllocations, + rewardsForProjects: state.data.rewardsForProjects, + setAllocations: state.setAllocations, + setCurrentView: state.setCurrentView, + setRewardsForProjects: state.setRewardsForProjects, + })); + const { onAddRemoveFromAllocate } = useIdsInAllocation({ + addAllocations, + allocations, + isDecisionWindowOpen, + removeAllocations, + userAllocationsElements: userAllocationsOriginal?.elements, + }); + + const onAllocateSuccess = () => { + refetchMatchedProjectRewards(); + refetchUserAllocations(); + refetchUserAllocationNonce(); + refetchHistory(); + refetchWithdrawals(); + refetchEpochAllocations(); + toastService.hideToast('confirmChanges'); + setAllocations([ + ...allocations.filter(allocation => { + const allocationValue = allocationValues.find(({ address }) => address === allocation); + return allocationValue && allocationValue.value !== '0'; + }), + ]); + setCurrentView('summary'); + }; + + const allocateEvent = useAllocate({ + nonce: userNonce!, + onMultisigMessageSign: () => { + toastService.hideToast('allocationMultisigInitialSignature'); + setIsWaitingForFirstMultisigSignature(false); + setIsWaitingForAllMultisigSignatures(true); + }, + onSuccess: onAllocateSuccess, + }); + + // eslint-disable-next-line react-hooks/exhaustive-deps + const mutateAsyncAllocateSimulateDebounced = useCallback( + debounce( + _allocationValues => { + resetAllocateSimulate(); + mutateAsyncAllocateSimulate(_allocationValues); + }, + 250, + { trailing: true }, + ), + [], + ); + + const setPercentageProportionsWrapper = ( + allocationValuesNew: AllocationValues, + rewardsForProjectsNew: bigint, + ) => { + if (!individualReward) { + return; + } + const percentageProportionsNew = allocationValuesNew.reduce((acc, curr) => { + const valueAsPercentageOfRewardsForProjects = ['0', ''].includes(curr.value) // 0 from the user, empty when removed entirely. + ? '0' + : ( + (parseFloat(curr.value.toString()) * 100) / + parseFloat(formatUnitsBigInt(rewardsForProjectsNew)) + ).toFixed(); + return { + ...acc, + [curr.address]: valueAsPercentageOfRewardsForProjects, + }; + }, {}); + setPercentageProportions(percentageProportionsNew); + }; + + const onResetAllocationValues = ({ + allocationValuesNew = allocationValues, + rewardsForProjectsNew = rewardsForProjects, + shouldReset = false, + } = {}) => { + if ( + isFetchingUserAllocation || + !isRewardsForProjectsSet || + currentEpoch === undefined || + (isConnected && !userAllocations && isDecisionWindowOpen && currentEpoch > 1) + ) { + return; + } + + const userAllocationsAddresses = userAllocations?.elements.map(({ address }) => address); + if (shouldReset && userAllocationsAddresses) { + const userAllocationsAddressesToAdd = userAllocationsAddresses?.filter( + element => !allocations.includes(element), + ); + + userAllocationsAddressesToAdd?.forEach((element, index, array) => { + onAddRemoveFromAllocate(element, [...allocations, ...array.slice(0, index)]); + }); + } + + const allocationValuesNewSum = allocationValuesNew.reduce( + (acc, curr) => acc + parseUnitsBigInt(curr.value), + BigInt(0), + ); + const shouldIsManulModeBeChangedToFalse = allocationValuesNewSum === 0n; + + /** + * Manual needs to be changed to false when values are 0. + * Percentages cant be calculated from 0, equal split cant be maintained, causing the app to crash. + * Mode needs to change to "auto". + */ + if (shouldIsManulModeBeChangedToFalse) { + setIsManualMode(false); + } + + const allocationValuesReset = getAllocationValuesInitialState({ + allocationValues: allocationValuesNew, + allocations: + shouldReset && userAllocationsAddresses + ? [...new Set([...allocations, ...userAllocationsAddresses])] + : allocations, + isManualMode: shouldIsManulModeBeChangedToFalse ? false : isManualMode, + percentageProportions, + rewardsForProjects: rewardsForProjectsNew, + shouldReset, + userAllocationsElements: isDecisionWindowOpen ? userAllocations?.elements || [] : [], + }); + + if (shouldReset) { + const allocationValuesResetSum = allocationValuesReset.reduce( + (acc, curr) => acc + parseUnitsBigInt(curr.value), + BigInt(0), + ); + + setRewardsForProjects(allocationValuesResetSum); + setPercentageProportionsWrapper(allocationValuesReset, allocationValuesResetSum); + + const shouldIsManualModeBeChangedToFalseNew = allocationValuesResetSum === 0n; + if (!shouldIsManualModeBeChangedToFalseNew) { + setIsManualMode(userAllocations!.isManuallyEdited); + } else { + setIsManualMode(false); + } + } + + setAllocationValues(allocationValuesReset); + }; + + const onAllocate = (isProceedingToAllocateWithLowUQScore?: boolean) => { + if (userNonce === undefined || projectsEpoch === undefined) { + return; + } + /** + * Whenever user wants to send an empty allocation (no projects, or all of them value 0) + * Push one element with value 0. It should be fixed on BE by creating "personal all" endpoint, + * but there is no ticket for it yet. + */ + const allocationValuesNew = [...allocationValues]; + if (allocationValuesNew.length === 0) { + allocationValuesNew.push({ + address: projectsEpoch.projectsAddresses[0], + value: '0', + }); + } + + if ( + !userAllocations?.hasUserAlreadyDoneAllocation && + uqScore === UQ_MULTIPLIER_FOR_USERS_BELOW_THRESHOLD_FOR_LEVERAGE_1 && + !isProceedingToAllocateWithLowUQScore + ) { + setShowLowUQScoreModal(true); + return; + } + + if (isProceedingToAllocateWithLowUQScore) { + setShowLowUQScoreModal(false); + } + + // this condition must always be last due to ModalAllocationLowUqScore + // if uqScore == 1n, the signature request is triggered in ModalAllocationLowUqScore + if (isContract) { + setIsWaitingForFirstMultisigSignature(true); + toastService.showToast({ + message: t('multisigSignatureToast.message'), + name: 'allocationMultisigInitialSignature', + title: t('multisigSignatureToast.title'), + type: 'warning', + }); + } + allocateEvent.emit(allocationValuesNew, isManualMode); + }; + + useEffect(() => { + if (!walletAddress) { + return; + } + /** + * The initial value of UQ for every user is 0.01. + * It does not update automatically after delegation nor after change in Gitcoin Passport itself. + * + * We need to refreshAntisybilStatus to force BE to refetch current values from Gitcoin Passport + * and return true value. + */ + refreshAntisybilStatus(walletAddress!); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + if (!userAllocations || isManualMode) { + return; + } + if (userAllocationsOriginal?.isManuallyEdited) { + setIsManualMode(true); + return; + } + setIsManualMode(false); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [userAllocations?.isManuallyEdited]); + + useEffect(() => { + if (!isRewardsForProjectsSet || isFetchingUserAllocation) { + return; + } + + if (userAllocations && userAllocations.elements.length > 0) { + setAllocationValues(userAllocations.elements); + setPercentageProportionsWrapper(userAllocations.elements, rewardsForProjects); + onResetAllocationValues({ allocationValuesNew: userAllocations.elements }); + return; + } + onResetAllocationValues(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentEpoch, + allocations, + isRewardsForProjectsSet, + isFetchingUserAllocation, + userAllocations?.elements.length, + userNonce, + isRewardsForProjectsSet, + ]); + + useEffect(() => { + if ( + !currentEpoch || + !isDecisionWindowOpen || + !userAllocations || + currentEpoch < 2 || + !userAllocations.hasUserAlreadyDoneAllocation + ) { + return; + } + const userAllocationsAddresses = userAllocations.elements.map(({ address }) => address); + /** + * Whenever user did an allocation and removed/unhearted project they previously allocated to, + * land on edit. + * + * Otherwise, land on summary. + */ + if (allocations.length < userAllocationsAddresses.length) { + setCurrentView('edit'); + return; + } + + return () => { + if (!isDecisionWindowOpen || allocations.length < userAllocationsAddresses.length) { + return; + } + setCurrentView('summary'); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentEpoch, isDecisionWindowOpen, userAllocations?.elements.length]); + + useEffect(() => { + const areAllValuesZero = !allocationValues.some(element => element.value !== '0.0'); + if ( + allocationValues.length === 0 || + areAllValuesZero || + addressesWithError.length > 0 || + !isDecisionWindowOpen || + !isConnected + ) { + return; + } + mutateAsyncAllocateSimulateDebounced( + currentView === 'edit' ? allocationValues : userAllocations?.elements, + ); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + currentView, + mutateAsyncAllocateSimulateDebounced, + addressesWithError, + allocationValues, + isDecisionWindowOpen, + userAllocations?.elements?.length, + userNonce, + ]); + + const onChangeAllocationItemValue = ( + newAllocationValue: AllocationValue, + isManualModeEnforced = false, + ) => { + const { allocationValuesArrayNew, rewardsForProjectsNew } = + getAllocationValuesAfterManualChange({ + allocationValues, + individualReward, + // When deleting by button isManualMode does not trigger manual mode. When typing, it does. + isManualMode: isManualModeEnforced ? true : isManualMode, + newAllocationValue: newAllocationValue || '0', + rewardsForProjects, + setAddressesWithError, + }); + + setAllocationValues(allocationValuesArrayNew); + setRewardsForProjects(rewardsForProjectsNew); + + if (isManualModeEnforced) { + setPercentageProportionsWrapper(allocationValuesArrayNew, rewardsForProjectsNew); + } + + if (isManualModeEnforced) { + setIsManualMode(true); + } + }; + + const onRemoveAllocationElement = (address: string) => { + onAddRemoveFromAllocate(address); + onChangeAllocationItemValue({ address, value: '0' }); + }; + + const isLoading = + allocationValues === undefined || + (isConnected && isFetchingUserNonce) || + (isConnected && isFetchingUserAllocation) || + (isConnected && isPendingRefreshAntisybilStatus) || + (isConnected && isFetchingUqScore) || + (isFetchingUpcomingBudget && !isRefetchingUpcomingBudget); + + const areAllocationsAvailableOrAlreadyDone = + (allocationValues !== undefined && !isEmpty(allocations)) || + (!!userAllocations?.hasUserAlreadyDoneAllocation && userAllocations.elements.length > 0); + const hasUserIndividualReward = !!individualReward && individualReward !== 0n; + + const emptyStateI18nKey = useMemo(() => { + if (hasUserIndividualReward && isDecisionWindowOpen) { + if (isMobile) { + return `${keyPrefix}.emptyStateMobileAWOpen`; + } + return `${keyPrefix}.emptyStateAWOpen`; + } + + if (isMobile) { + return `${keyPrefix}.emptyStateMobile`; + } + + return `${keyPrefix}.emptyState`; + }, [hasUserIndividualReward, isDecisionWindowOpen, isMobile]); + + const allocationsWithRewards = getAllocationsWithRewards({ + allocationValues, + areAllocationsAvailableOrAlreadyDone, + projectsIpfsWithRewards, + userAllocationsElements: userAllocations?.elements, + }); + + const isEpoch1 = currentEpoch === 1; + + const areButtonsDisabled = + isEpoch1 || + isLoading || + !isConnected || + !isDecisionWindowOpen || + (!areAllocationsAvailableOrAlreadyDone && rewardsForProjects !== 0n) || + !hasUserIndividualReward || + isWaitingForFirstMultisigSignature; + + useEffect(() => { + if (!walletAddress || !isContract || isWaitingForFirstMultisigSignature) { + return; + } + const getPendingMultisigSignatures = () => { + apiGetPendingMultisigSignatures(walletAddress!, SignatureOpType.ALLOCATION).then(data => { + if (isWaitingForAllMultisigSignatures && !data.hash) { + onAllocateSuccess(); + } + setIsWaitingForAllMultisigSignatures(!!data.hash); + }); + }; + + if (!isWaitingForAllMultisigSignatures) { + getPendingMultisigSignatures(); + return; + } + + const intervalId = setInterval(getPendingMultisigSignatures, 2500); + + return () => { + clearInterval(intervalId); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + walletAddress, + isWaitingForAllMultisigSignatures, + isContract, + isWaitingForFirstMultisigSignature, + ]); + + useEffect(() => { + if (isDesktop) { + setTimeout(() => { + setShowAllocationNav(true); + }, DRAWER_TRANSITION_TIME * 1000); + } else { + setShowAllocationNav(true); + } + + return () => { + setShowAllocationNav(false); + }; + }, [isDesktop]); + + useEffect(() => { + navRef.current = document.getElementById(LAYOUT_NAVBAR_ID); + }, []); + + useEffect(() => { + if (!allocationEmptyStateRef.current || allocations.length) { + return; + } + const { height } = allocationEmptyStateRef.current.getBoundingClientRect(); + + // 200 px (mobile) / 226px (tablet, desktop, large desktop) -> min height of emptyState box to show image + text + if (height < (isMobile ? 200 : 226)) { + setIsEmptyStateImageVisible(false); + return; + } + + setIsEmptyStateImageVisible(true); + }, [allocations.length, isMobile, isTablet]); + + return ( +
+
{t('allocateRewards')}
+
+ {!isEpoch1 && ( + + )} + {hasUserIndividualReward && isDecisionWindowOpen && !isEpoch1 && ( + 0} + isLocked={currentView === 'summary'} + setRewardsForProjectsCallback={onResetAllocationValues} + /> + )} + {!allocations.length && currentView === 'edit' && ( +
+ {isEmptyStateImageVisible && ( + + )} +
+ , + ]} + i18nKey={emptyStateI18nKey} + /> +
+
+ )} + + {currentView === 'edit' ? ( + areAllocationsAvailableOrAlreadyDone && ( + + {allocationsWithRewards.length > 0 + ? allocationsWithRewards!.map( + ({ + address, + isAllocatedTo, + isLoadingError, + value, + profileImageSmall, + name, + }) => ( + 0} + name={name} + onChange={onChangeAllocationItemValue} + onRemoveAllocationElement={() => onRemoveAllocationElement(address)} + profileImageSmall={profileImageSmall} + rewardsProps={{ + isLoadingAllocateSimulate, + simulatedMatched: allocationSimulated?.matched.find( + element => element.address === address, + )?.value, + }} + setAddressesWithError={setAddressesWithError} + value={value} + /> + ), + ) + : allocations.map(allocation => ( + + ))} + + ) + ) : ( + + )} + setShowLowUQScoreModal(false), + }} + onAllocate={() => onAllocate(true)} + /> + {showAllocationNav && + (isDesktop ? boxesWrapperRef.current : navRef.current) && + createPortal( + onResetAllocationValues({ shouldReset: true })} + />, + isDesktop ? boxesWrapperRef.current! : navRef.current!, + )} +
+
+ ); +}; + +export default Allocation; diff --git a/client/src/components/Allocation/AllocationItem/AllocationItem.module.scss b/client/src/components/Allocation/AllocationItem/AllocationItem.module.scss index fc7786db98..d056f310cb 100644 --- a/client/src/components/Allocation/AllocationItem/AllocationItem.module.scss +++ b/client/src/components/Allocation/AllocationItem/AllocationItem.module.scss @@ -1,5 +1,5 @@ $removeButtonHeightWidthMobile: 8rem; -$removeButtonHeightWidthDesktop: 10.4rem; +$removeButtonHeightWidth: 10.4rem; .root { width: 100%; @@ -28,19 +28,19 @@ $removeButtonHeightWidthDesktop: 10.4rem; background-color: $color-octant-grey1; } - @media #{$desktop-up} { - height: $removeButtonHeightWidthDesktop; - width: $removeButtonHeightWidthDesktop; + @media #{$tablet-up} { + height: $removeButtonHeightWidth; + width: $removeButtonHeightWidth; } } .box { position: relative; z-index: $z-index-2; - padding: 2.4rem; + padding: 1.6rem 1.6rem 1.6rem 2.4rem; - @media #{$tablet-down} { - padding: 1.6rem 1.6rem 1.6rem 2.4rem; + @media #{$tablet-up } { + padding: 2.4rem; } } @@ -63,11 +63,12 @@ $removeButtonHeightWidthDesktop: 10.4rem; .image { margin-right: 2.4rem; border-radius: 50%; - height: 4.8rem; - width: 4.8rem; + height: 5.6rem; + width: 5.6rem; + display: none; - @media #{$tablet-down} { - display: none; + @media #{$tablet-up} { + display: initial; } } @@ -77,24 +78,25 @@ $removeButtonHeightWidthDesktop: 10.4rem; .name { min-width: 0; - font-size: $font-size-18; - line-height: 2.1rem; text-align: left; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - margin: 0 0 0.1rem 0; - - @media #{$tablet-down} { - font-size: $font-size-14; - margin: 0; - line-height: 1.6rem; + font-size: $font-size-14; + margin: 0; + line-height: 1.6rem; + + @media #{$tablet-up} { + font-size: $font-size-18; + line-height: 2.1rem; + margin: 0 0 0.1rem 0; } } .inputWrapper { flex: 1; - max-width: 26rem; + max-width: 14.2rem; + min-width: 14.2rem; &.isEpoch1 { opacity: 0.5; @@ -104,9 +106,8 @@ $removeButtonHeightWidthDesktop: 10.4rem; animation: horizontal-shaking 0.25s; } - @media #{$tablet-down} { - max-width: 14.2rem; - min-width: 14.2rem; + @media #{$tablet-up} { + max-width: 26rem; } } diff --git a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx index 0bbdff742b..b2f94e6e75 100644 --- a/client/src/components/Allocation/AllocationItem/AllocationItem.tsx +++ b/client/src/components/Allocation/AllocationItem/AllocationItem.tsx @@ -72,7 +72,7 @@ const AllocationItem: FC = ({ const { data: currentEpoch } = useCurrentEpoch(); const { isFetching: isFetchingRewardsThreshold } = useProjectRewardsThreshold(); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - const { isDesktop } = useMediaQuery(); + const { isMobile } = useMediaQuery(); const [ref, animate] = useAnimate(); const removeButtonRef = useRef(null); const inputRef = useRef(null); @@ -202,7 +202,7 @@ const AllocationItem: FC = ({ const itemHeight = ref.current.getBoundingClientRect().height; setConstraints([(itemHeight + removeButtonLeftPadding) * -1, 0]); - }, [ref, removeButtonRef, isDesktop, isLoading]); + }, [ref, removeButtonRef, isMobile, isLoading]); useEffect(() => { if (isError) { @@ -248,7 +248,7 @@ const AllocationItem: FC = ({ onClick={onRemoveAllocationElement} style={{ scale: removeButtonScaleTransform }} > - + )} { className?: string; isError: boolean; diff --git a/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx b/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx index 93bb16dc36..4e8718eb0b 100644 --- a/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx +++ b/client/src/components/Allocation/AllocationItemRewards/AllocationItemRewards.tsx @@ -4,13 +4,14 @@ import { useTranslation } from 'react-i18next'; import Svg from 'components/ui/Svg'; import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; import useProjectsDonors from 'hooks/queries/donors/useProjectsDonors'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useMatchedProjectRewards from 'hooks/queries/useMatchedProjectRewards'; import useUqScore from 'hooks/queries/useUqScore'; import useUserAllocations from 'hooks/queries/useUserAllocations'; -import { person } from 'svg/misc'; +import { hammer, person } from 'svg/misc'; import bigintAbs from 'utils/bigIntAbs'; import getRewardsSumWithValueAndSimulation from 'utils/getRewardsSumWithValueAndSimulation'; import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; @@ -98,7 +99,7 @@ const AllocationItemRewards: FC = ({ value, }) => { const { t } = useTranslation('translation', { - keyPrefix: 'views.allocation.allocationItem', + keyPrefix: 'components.allocation.allocationItem', }); const [isSimulateVisible, setIsSimulateVisible] = useState(false); const { data: currentEpoch } = useCurrentEpoch(); @@ -106,6 +107,7 @@ const AllocationItemRewards: FC = ({ const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); const { data: matchedProjectRewards } = useMatchedProjectRewards(); const { data: uqScore } = useUqScore(currentEpoch!); + const { isMobile } = useMediaQuery(); const { data: projectsDonors } = useProjectsDonors(); const projectDonors = projectsDonors?.[address]; @@ -137,9 +139,10 @@ const AllocationItemRewards: FC = ({ const getValuesToDisplay = useGetValuesToDisplay(); - const isNewSimulatedPositive = userAllocationToThisProject - ? parseUnitsBigInt(valueToUse) >= userAllocationToThisProject - : true; + const isNewSimulatedPositive = + isDecisionWindowOpen && userAllocationToThisProject + ? parseUnitsBigInt(valueToUse) >= userAllocationToThisProject + : true; const simulatedMatchedBigInt = simulatedMatched ? parseUnitsBigInt(simulatedMatched, 'wei') @@ -221,6 +224,27 @@ const AllocationItemRewards: FC = ({ userAllocationToThisProject={userAllocationToThisProject} valueToUse={valueToUse} /> + {!isMobile && !isLoadingAllocateSimulate && !isSimulateVisible && ( +
+ + {`${isNewSimulatedPositive ? '' : '-'}${yourImpactFormatted.primary}`} +
+ )} ); }; diff --git a/client/src/components/Allocation/AllocationLowUqScore/AllocationLowUqScore.tsx b/client/src/components/Allocation/AllocationLowUqScore/AllocationLowUqScore.tsx index a730348d73..31fb3c6a5e 100644 --- a/client/src/components/Allocation/AllocationLowUqScore/AllocationLowUqScore.tsx +++ b/client/src/components/Allocation/AllocationLowUqScore/AllocationLowUqScore.tsx @@ -1,24 +1,83 @@ -import React, { FC, useState } from 'react'; +import React, { FC, useEffect, useState } from 'react'; import { Trans, useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; +import { useAccount } from 'wagmi'; import BoxRounded from 'components/ui/BoxRounded'; import Button from 'components/ui/Button'; import InputCheckbox from 'components/ui/InputCheckbox'; -import { ROOT_ROUTES } from 'routes/RootRoutes/routes'; +import { GITCOIN_PASSPORT_CUSTOM_OCTANT_DASHBOARD } from 'constants/urls'; +import useRefreshAntisybilStatus from 'hooks/mutations/useRefreshAntisybilStatus'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useUqScore from 'hooks/queries/useUqScore'; +import toastService from 'services/toastService'; import styles from './AllocationLowUqScore.module.scss'; import AllocationLowUqScoreProps from './types'; -const AllocationLowUqScore: FC = ({ onAllocate }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.allocation.lowUQScoreModal' }); +const AllocationLowUqScore: FC = ({ onAllocate, onCloseModal }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.allocation.lowUQScoreModal', + }); const [isChecked, setIsChecked] = useState(false); - const navigate = useNavigate(); + const [isRefetchRequired, setIsRefetchRequired] = useState(false); + + const { address } = useAccount(); + + const { data: currentEpoch } = useCurrentEpoch(); + const { + data: uqScore, + refetch: refetchUqScore, + isFetching: isFetchingUqScore, + } = useUqScore(currentEpoch!); + const { mutateAsync: refreshAntisybilStatus, isPending: isPendingRefreshAntisybilStatus } = + useRefreshAntisybilStatus(); + + const windowOnFocus = () => { + if (!address) { + return; + } + refreshAntisybilStatus(address).then(() => { + refetchUqScore(); + setIsRefetchRequired(false); + }); + }; + + const windowOnBlur = () => { + setIsRefetchRequired(true); + }; + + useEffect(() => { + window.addEventListener('focus', windowOnFocus); + window.addEventListener('blur', windowOnBlur); + + return () => { + window.removeEventListener('focus', windowOnFocus); + window.removeEventListener('blur', windowOnBlur); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + useEffect(() => { + // When user manages to increase their score to what gives UQ score of 100, close the modal. + if (!uqScore || uqScore < 100n) { + return; + } + toastService.showToast({ + message: t('toasts.success.message'), + name: 'uqScoreSuccessfullyIncreased', + title: t('toasts.success.title'), + type: 'success', + }); + onCloseModal(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [uqScore]); + + const isFetching = isRefetchRequired || isFetchingUqScore || isPendingRefreshAntisybilStatus; return ( <>
- ]} i18nKey="views.allocation.lowUQScoreModal.text" /> + ]} i18nKey="components.allocation.lowUQScoreModal.text" />
= ({ onAllocate }) =>
-
); }; diff --git a/client/src/components/Allocation/AllocationNavigation/types.ts b/client/src/components/Allocation/AllocationNavigation/types.ts index 3e48ea5a3a..a3caaa5724 100644 --- a/client/src/components/Allocation/AllocationNavigation/types.ts +++ b/client/src/components/Allocation/AllocationNavigation/types.ts @@ -1,12 +1,8 @@ -import { CurrentView } from 'views/AllocationView/types'; - export default interface AllocationNavigationProps { areButtonsDisabled: boolean; - currentView: CurrentView; isLeftButtonDisabled: boolean; isLoading: boolean; isWaitingForAllMultisigSignatures?: boolean; onAllocate: () => void; onResetValues: () => void; - setCurrentView: (newView: CurrentView) => void; } diff --git a/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.module.scss b/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.module.scss index ff39724944..b74d49c81f 100644 --- a/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.module.scss +++ b/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.module.scss @@ -1,106 +1,52 @@ .root { position: relative; - color: $color-octant-grey5; - padding: 2rem 2.4rem 2.3rem; - - @media #{$tablet-down} { - padding: 1.6rem; - } -} - -.title { - font-size: $font-size-18; - margin: 0 0 0 0.8rem; - color: $color-octant-green; - line-height: 2.4rem; - - &.greyTitle { - color: $color-octant-grey5; - } - - @media #{$tablet-down} { - font-size: $font-size-16; - line-height: 2rem; - } -} - -.subtitle { - margin-left: 0.8rem; -} - -.isManualBadge { - position: absolute; - top: 2.4rem; - right: 2.4rem; - border: 0.1rem solid $color-octant-green; - padding: 0.2rem 0.5rem; - text-transform: uppercase; - border-radius: $border-radius-04; - color: $color-octant-green; - background: $color-octant-green5; - line-height: 1.4rem; -} - -.sliderWrapper { - display: flex; - align-items: center; - width: 100%; - height: 7.2rem; - background-color: $color-octant-grey6; - border-radius: $border-radius-08; - padding: 0 1.6rem; - margin: 1.6rem 0 1.2rem; + min-height: 7.2rem; + max-height: 7.2rem; + border-radius: $border-radius-16; + background-color: $color-octant-grey8; + padding: 1.6rem 1.6rem 0; + font-weight: $font-weight-bold; + text-align: left; + margin-bottom: 1.6rem; &.isManuallyEdited { background-color: $color-octant-green5; } - @media #{$tablet-down} { - margin: 1.1rem 0 0.8rem; - } -} - -.sections { - display: flex; - justify-content: space-between; - align-self: stretch; - margin: 0 0.8rem; -} - -.section { - &:not(.isDisabled):not(.isLocked) { - cursor: pointer; - } - - .header { - line-height: 1.6rem; - padding: 0.4rem 0 0.1rem 0; + @media #{$tablet-up} { + padding: 2rem 2.4rem 0; } .value { - line-height: 2rem; - font-size: $font-size-18; - color: $color-octant-dark; - height: 1.6rem; + color: $color-octant-green; + font-size: $font-size-16; + line-height: 2.4rem; - &.isGrey { - color: $color-octant-grey5; + @media #{$tablet-up} { + font-size: $font-size-18; } - @media #{$tablet-down} { - font-size: $font-size-16; + &.isGrey { + color: $color-octant-grey5; } } - &:first-child { - text-align: left; - } - - &:last-child { - text-align: right; + .label { + font-size: $font-size-12; + color: $color-octant-grey5; } - &.isDisabled { - color: $color-octant-grey5; + .manuallyEditedLabel { + position: absolute; + top: 50%; + transform: translate(0, -50%); + right: 2.2rem; + border: 0.1rem solid $color-octant-green; + padding: 0.2rem 0.5rem; + text-transform: uppercase; + border-radius: $border-radius-04; + color: $color-octant-green; + background: $color-octant-green5; + line-height: 1.4rem; } } diff --git a/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.tsx b/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.tsx index ca97574569..7ef95b77d7 100644 --- a/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.tsx +++ b/client/src/components/Allocation/AllocationRewardsBox/AllocationRewardsBox.tsx @@ -1,27 +1,19 @@ import cx from 'classnames'; -import throttle from 'lodash/throttle'; -import React, { FC, useState, useCallback, useMemo } from 'react'; +import React, { FC, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import ModalAllocationValuesEdit from 'components/Allocation/ModalAllocationValuesEdit'; -import BoxRounded from 'components/ui/BoxRounded'; -import Slider from 'components/ui/Slider'; import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; import useIndividualReward from 'hooks/queries/useIndividualReward'; import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useUpcomingBudget from 'hooks/queries/useUpcomingBudget'; -import useAllocationsStore from 'store/allocations/store'; import styles from './AllocationRewardsBox.module.scss'; import AllocationRewardsBoxProps from './types'; const AllocationRewardsBox: FC = ({ - className, isDisabled, - isManuallyEdited, isLocked, - isError, - setRewardsForProjectsCallback, + isManuallyEdited, }) => { const { i18n, t } = useTranslation('translation', { keyPrefix: 'components.dedicated.allocationRewardsBox', @@ -29,78 +21,12 @@ const AllocationRewardsBox: FC = ({ const { data: individualReward } = useIndividualReward(); const { data: upcomingBudget } = useUpcomingBudget(); const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - const [modalMode, setModalMode] = useState<'closed' | 'donate' | 'withdraw'>('closed'); - const { rewardsForProjects, setRewardsForProjects } = useAllocationsStore(state => ({ - rewardsForProjects: state.data.rewardsForProjects, - setRewardsForProjects: state.setRewardsForProjects, - })); const getValuesToDisplay = useGetValuesToDisplay(); const hasUserIndividualReward = !!individualReward && individualReward !== 0n; const isDecisionWindowOpenAndHasIndividualReward = hasUserIndividualReward && isDecisionWindowOpen; - const onSetRewardsForProjects = (rewardsForProjectsNew: bigint) => { - if (!individualReward || isDisabled) { - return; - } - setRewardsForProjects(rewardsForProjectsNew); - setRewardsForProjectsCallback({ rewardsForProjectsNew }); - }; - - const onUpdateValueModal = (newValue: bigint) => { - const rewardsForProjectsNew = modalMode === 'donate' ? newValue : individualReward! - newValue; - onSetRewardsForProjects(rewardsForProjectsNew); - }; - - const onUpdateValueSlider = (index: number) => { - if (!individualReward || isDisabled) { - return; - } - const rewardsForProjectsNew = (individualReward * BigInt(index)) / BigInt(100); - onSetRewardsForProjects(rewardsForProjectsNew); - }; - - // eslint-disable-next-line react-hooks/exhaustive-deps - const onUpdateValueSliderThrottled = useCallback( - throttle(onUpdateValueSlider, 250, { trailing: true }), - [isDisabled, individualReward, setRewardsForProjectsCallback], - ); - - const rewardsForProjectsFinal = isDecisionWindowOpenAndHasIndividualReward - ? rewardsForProjects - : BigInt(0); - const rewardsForWithdraw = isDecisionWindowOpenAndHasIndividualReward - ? individualReward - rewardsForProjects - : BigInt(0); - - const percentRewardsForProjects = isDecisionWindowOpenAndHasIndividualReward - ? Number((rewardsForProjects * BigInt(100)) / individualReward) - : 50; - const percentWithdraw = isDecisionWindowOpenAndHasIndividualReward - ? Number((rewardsForWithdraw * BigInt(100)) / individualReward) - : 50; - const sections = [ - { - header: isLocked ? t('donated') : t('donate'), - value: getValuesToDisplay({ - cryptoCurrency: 'ethereum', - showCryptoSuffix: true, - showLessThanOneCentFiat: !isDisabled, - valueCrypto: rewardsForProjectsFinal, - }).primary, - }, - { - header: i18n.t('common.personal'), - value: getValuesToDisplay({ - cryptoCurrency: 'ethereum', - showCryptoSuffix: true, - showLessThanOneCentFiat: !isDisabled, - valueCrypto: rewardsForWithdraw, - }).primary, - }, - ]; - const subtitle = useMemo(() => { if (isDecisionWindowOpen === false && upcomingBudget) { return t('availableDuringAllocation'); @@ -132,86 +58,22 @@ const AllocationRewardsBox: FC = ({ }, [isDecisionWindowOpen, individualReward, upcomingBudget]); return ( - - {!isDisabled && isManuallyEdited &&
{t('manual')}
} -
- -
-
- {sections.map(({ header, value }, index) => ( -
- isLocked || isDisabled ? {} : setModalMode(index === 0 ? 'donate' : 'withdraw') - } - > -
- {header} -
-
- {value} -
-
- ))} +
+
+
+ { + getValuesToDisplay({ + cryptoCurrency: 'ethereum', + showCryptoSuffix: true, + showLessThanOneCentFiat: !isDisabled, + valueCrypto: budget, + }).primary + } +
+
{subtitle}
- setModalMode('closed'), - }} - onUpdateValue={newValue => onUpdateValueModal(newValue)} - valueCryptoSelected={modalMode === 'donate' ? rewardsForProjects : rewardsForWithdraw} - valueCryptoTotal={individualReward!} - /> - + {isManuallyEdited &&
{t('manual')}
} +
); }; diff --git a/client/src/components/Allocation/AllocationRewardsBox/types.ts b/client/src/components/Allocation/AllocationRewardsBox/types.ts index c3f5c44759..fc6325543e 100644 --- a/client/src/components/Allocation/AllocationRewardsBox/types.ts +++ b/client/src/components/Allocation/AllocationRewardsBox/types.ts @@ -1,8 +1,5 @@ export default interface AllocationRewardsBoxProps { - className?: string; isDisabled?: boolean; - isError: boolean; isLocked?: boolean; - isManuallyEdited?: boolean; - setRewardsForProjectsCallback: ({ rewardsForProjectsNew }) => void; + isManuallyEdited: boolean; } diff --git a/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.module.scss b/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.module.scss new file mode 100644 index 0000000000..3b83c8a8d9 --- /dev/null +++ b/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.module.scss @@ -0,0 +1,70 @@ +.root { + position: relative; + color: $color-octant-grey5; + padding: 1.6rem; + + @media #{$tablet-up} { + padding: 2.4rem; + } +} + +.sliderWrapper { + display: flex; + align-items: center; + width: 100%; + background-color: $color-octant-grey6; + border-radius: $border-radius-08; + padding: 0 1.6rem; + margin-bottom: 0.8rem; + height: 7.2rem; + + @media #{$tablet-up} { + margin-bottom: 1.6rem; + height: 10.4rem; + } +} + +.sections { + display: flex; + justify-content: space-between; + align-self: stretch; + margin: 0 0.8rem; +} + +.section { + &:not(.isDisabled):not(.isLocked) { + cursor: pointer; + } + + .header { + font-size: $font-size-12; + line-height: 2.4rem; + } + + .value { + line-height: 2rem; + font-size: $font-size-16; + color: $color-octant-dark; + height: 1.6rem; + + &.isGrey { + color: $color-octant-grey5; + } + + @media #{$tablet-up} { + font-size: $font-size-18; + } + } + + &:first-child { + text-align: left; + } + + &:last-child { + text-align: right; + } + + &.isDisabled { + color: $color-octant-grey5; + } +} diff --git a/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.tsx b/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.tsx new file mode 100644 index 0000000000..a5da7c8952 --- /dev/null +++ b/client/src/components/Allocation/AllocationSliderBox/AllocationSliderBox.tsx @@ -0,0 +1,168 @@ +import cx from 'classnames'; +import throttle from 'lodash/throttle'; +import React, { FC, useState, useCallback } from 'react'; +import { useTranslation } from 'react-i18next'; + +import ModalAllocationValuesEdit from 'components/Allocation/ModalAllocationValuesEdit'; +import BoxRounded from 'components/ui/BoxRounded'; +import Slider from 'components/ui/Slider'; +import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; +import useIndividualReward from 'hooks/queries/useIndividualReward'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useAllocationsStore from 'store/allocations/store'; + +import styles from './AllocationSliderBox.module.scss'; +import AllocationSliderBoxProps from './types'; + +const AllocationSliderBox: FC = ({ + className, + isDisabled, + isLocked, + isError, + setRewardsForProjectsCallback, +}) => { + const { i18n, t } = useTranslation('translation', { + keyPrefix: 'components.dedicated.allocationRewardsBox', + }); + const { data: individualReward } = useIndividualReward(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const [modalMode, setModalMode] = useState<'closed' | 'donate' | 'withdraw'>('closed'); + const { rewardsForProjects, setRewardsForProjects } = useAllocationsStore(state => ({ + rewardsForProjects: state.data.rewardsForProjects, + setRewardsForProjects: state.setRewardsForProjects, + })); + const getValuesToDisplay = useGetValuesToDisplay(); + + const hasUserIndividualReward = !!individualReward && individualReward !== 0n; + const isDecisionWindowOpenAndHasIndividualReward = + hasUserIndividualReward && isDecisionWindowOpen; + + const onSetRewardsForProjects = (rewardsForProjectsNew: bigint) => { + if (!individualReward || isDisabled) { + return; + } + setRewardsForProjects(rewardsForProjectsNew); + setRewardsForProjectsCallback({ rewardsForProjectsNew }); + }; + + const onUpdateValueModal = (newValue: bigint) => { + const rewardsForProjectsNew = modalMode === 'donate' ? newValue : individualReward! - newValue; + onSetRewardsForProjects(rewardsForProjectsNew); + }; + + const onUpdateValueSlider = (index: number) => { + if (!individualReward || isDisabled) { + return; + } + const rewardsForProjectsNew = (individualReward * BigInt(index)) / BigInt(100); + onSetRewardsForProjects(rewardsForProjectsNew); + }; + + // eslint-disable-next-line react-hooks/exhaustive-deps + const onUpdateValueSliderThrottled = useCallback( + throttle(onUpdateValueSlider, 250, { trailing: true }), + [isDisabled, individualReward, setRewardsForProjectsCallback], + ); + + const rewardsForProjectsFinal = isDecisionWindowOpenAndHasIndividualReward + ? rewardsForProjects + : BigInt(0); + const rewardsForWithdraw = isDecisionWindowOpenAndHasIndividualReward + ? individualReward - rewardsForProjects + : BigInt(0); + + const percentRewardsForProjects = isDecisionWindowOpenAndHasIndividualReward + ? Number((rewardsForProjects * BigInt(100)) / individualReward) + : 50; + const percentWithdraw = isDecisionWindowOpenAndHasIndividualReward + ? Number((rewardsForWithdraw * BigInt(100)) / individualReward) + : 50; + const sections = [ + { + header: isLocked ? i18n.t('common.donated') : t('donate'), + value: getValuesToDisplay({ + cryptoCurrency: 'ethereum', + showCryptoSuffix: true, + showLessThanOneCentFiat: !isDisabled, + valueCrypto: rewardsForProjectsFinal, + }).primary, + }, + { + header: i18n.t('common.personal'), + value: getValuesToDisplay({ + cryptoCurrency: 'ethereum', + showCryptoSuffix: true, + showLessThanOneCentFiat: !isDisabled, + valueCrypto: rewardsForWithdraw, + }).primary, + }, + ]; + + return ( + +
+ +
+
+ {sections.map(({ header, value }, index) => ( +
+ isLocked || isDisabled ? {} : setModalMode(index === 0 ? 'donate' : 'withdraw') + } + > +
+ {header} +
+
+ {value} +
+
+ ))} +
+ setModalMode('closed'), + }} + onUpdateValue={newValue => onUpdateValueModal(newValue)} + valueCryptoSelected={modalMode === 'donate' ? rewardsForProjects : rewardsForWithdraw} + valueCryptoTotal={individualReward!} + /> +
+ ); +}; + +export default AllocationSliderBox; diff --git a/client/src/components/Allocation/AllocationSliderBox/index.tsx b/client/src/components/Allocation/AllocationSliderBox/index.tsx new file mode 100644 index 0000000000..1e04edb4f9 --- /dev/null +++ b/client/src/components/Allocation/AllocationSliderBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './AllocationSliderBox'; diff --git a/client/src/components/Allocation/AllocationSliderBox/types.ts b/client/src/components/Allocation/AllocationSliderBox/types.ts new file mode 100644 index 0000000000..2afa91b241 --- /dev/null +++ b/client/src/components/Allocation/AllocationSliderBox/types.ts @@ -0,0 +1,7 @@ +export default interface AllocationSliderBoxProps { + className?: string; + isDisabled?: boolean; + isError: boolean; + isLocked?: boolean; + setRewardsForProjectsCallback: ({ rewardsForProjectsNew }) => void; +} diff --git a/client/src/components/Allocation/AllocationSummary/AllocationSummary.module.scss b/client/src/components/Allocation/AllocationSummary/AllocationSummary.module.scss index 16e850bd66..c1f0a5d98a 100644 --- a/client/src/components/Allocation/AllocationSummary/AllocationSummary.module.scss +++ b/client/src/components/Allocation/AllocationSummary/AllocationSummary.module.scss @@ -86,6 +86,10 @@ .personalRewardBox { height: 6.4rem; + @media #{$desktop-up} { + margin-bottom: 1.6rem; + } + &.areAllocationValuesPositive { margin-top: 1.6rem; } diff --git a/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx b/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx index 3625725bf5..52f4780b27 100644 --- a/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx +++ b/client/src/components/Allocation/AllocationSummary/AllocationSummary.tsx @@ -7,7 +7,10 @@ import BoxRounded from 'components/ui/BoxRounded'; import Sections from 'components/ui/BoxRounded/Sections/Sections'; import { SectionProps } from 'components/ui/BoxRounded/Sections/types'; import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useEpochLeverage from 'hooks/queries/useEpochLeverage'; import useIndividualReward from 'hooks/queries/useIndividualReward'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; import useUserAllocations from 'hooks/queries/useUserAllocations'; import useAllocationsStore from 'store/allocations/store'; import { formatUnitsBigInt } from 'utils/formatUnitsBigInt'; @@ -24,6 +27,9 @@ const AllocationSummary: FC = ({ }); const { data: individualReward } = useIndividualReward(); const { data: userAllocations } = useUserAllocations(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const { data: currentEpoch } = useCurrentEpoch(); + const { data: epochLeverage } = useEpochLeverage(currentEpoch! - 1); const { rewardsForProjects } = useAllocationsStore(state => ({ rewardsForProjects: state.data.rewardsForProjects, })); @@ -48,19 +54,21 @@ const AllocationSummary: FC = ({ valueCrypto: rewardsForProjects, }); + const leverage = isDecisionWindowOpen ? allocationSimulated?.leverage : epochLeverage?.toString(); + const matchingFundSumToDisplay = - rewardsForProjects && allocationSimulated?.leverage + rewardsForProjects && leverage ? getValuesToDisplay({ cryptoCurrency: 'ethereum', - valueCrypto: rewardsForProjects * BigInt(parseInt(allocationSimulated.leverage, 10)), + valueCrypto: rewardsForProjects * BigInt(parseInt(leverage, 10)), }).primary : undefined; const totalImpactToDisplay = getValuesToDisplay({ cryptoCurrency: 'ethereum', showCryptoSuffix: true, valueCrypto: - rewardsForProjects && allocationSimulated - ? rewardsForProjects * BigInt(parseInt(allocationSimulated.leverage, 10) + 1) + rewardsForProjects && leverage + ? rewardsForProjects * BigInt(parseInt(leverage, 10) + 1) : rewardsForProjects, }).primary; const personalToDisplay = individualReward @@ -84,7 +92,7 @@ const AllocationSummary: FC = ({ isLoadingAllocateSimulate && styles.isLoading, )} > - {allocationSimulated ? `${parseInt(allocationSimulated.leverage, 10)}x` : undefined} + {leverage ? `${parseInt(leverage, 10)}x` : undefined}
diff --git a/client/src/components/Allocation/AllocationSummaryProject/AllocationSummaryProject.tsx b/client/src/components/Allocation/AllocationSummaryProject/AllocationSummaryProject.tsx index fec865d120..e69d565462 100644 --- a/client/src/components/Allocation/AllocationSummaryProject/AllocationSummaryProject.tsx +++ b/client/src/components/Allocation/AllocationSummaryProject/AllocationSummaryProject.tsx @@ -21,7 +21,7 @@ const AllocationSummaryProject: FC = ({ value, }) => { const { t } = useTranslation('translation', { - keyPrefix: 'views.allocation.allocationItem', + keyPrefix: 'components.allocation.allocationItem', }); const isDonationAboveThreshold = useIsDonationAboveThreshold({ projectAddress: address }); const { diff --git a/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.module.scss b/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.module.scss deleted file mode 100644 index 46ac8138a2..0000000000 --- a/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.module.scss +++ /dev/null @@ -1,20 +0,0 @@ -.bold { - color: $color-octant-green; - font-weight: $font-weight-bold; -} - -.uqTooLowImage { - height: 7.2rem; - - @media #{$desktop-up} { - height: 10rem; - } -} - -.rewardsImage { - height: 8.5rem; - - @media #{$desktop-up} { - height: 11rem; - } -} diff --git a/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.tsx b/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.tsx deleted file mode 100644 index 97e2fd2b3b..0000000000 --- a/client/src/components/Allocation/AllocationTipTiles/AllocationTipTiles.tsx +++ /dev/null @@ -1,124 +0,0 @@ -import React, { FC, Fragment, useEffect } from 'react'; -import { Trans, useTranslation } from 'react-i18next'; -import { useNavigate } from 'react-router-dom'; -import { useAccount } from 'wagmi'; - -import TipTile from 'components/shared/TipTile'; -import useMediaQuery from 'hooks/helpers/useMediaQuery'; -import useRefreshAntisybilStatus from 'hooks/mutations/useRefreshAntisybilStatus'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useIndividualReward from 'hooks/queries/useIndividualReward'; -import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; -import useUqScore from 'hooks/queries/useUqScore'; -import useUserAllocations from 'hooks/queries/useUserAllocations'; -import { ROOT_ROUTES } from 'routes/RootRoutes/routes'; -import useTipsStore from 'store/tips/store'; - -import styles from './AllocationTipTiles.module.scss'; -import AllocationTipTilesProps from './types'; - -const AllocationTipTiles: FC = ({ className }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.allocation.tip' }); - const navigate = useNavigate(); - const { isDesktop } = useMediaQuery(); - const { address, isConnected } = useAccount(); - const { - mutateAsync: refreshAntisybilStatus, - isPending: isPendingRefreshAntisybilStatus, - isSuccess: isSuccessRefreshAntisybilStatus, - error: refreshAntisybilStatusError, - } = useRefreshAntisybilStatus(); - const { data: currentEpoch } = useCurrentEpoch(); - const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - const { data: individualReward, isFetching: isFetchingIndividualReward } = useIndividualReward(); - const { data: userAllocations, isFetching: isFetchingUserAllocation } = useUserAllocations(); - const { data: uqScore, isFetching: isFetchingUqScore } = useUqScore(currentEpoch!, { - enabled: - isSuccessRefreshAntisybilStatus || - (refreshAntisybilStatusError as null | { message: string })?.message === - 'Address is already used for delegation', - }); - const { - wasRewardsAlreadyClosed, - setWasRewardsAlreadyClosed, - wasUqTooLowAlreadyClosed, - setWasUqTooLowAlreadyClosed, - } = useTipsStore(state => ({ - setWasConnectWalletAlreadyClosed: state.setWasConnectWalletAlreadyClosed, - setWasRewardsAlreadyClosed: state.setWasRewardsAlreadyClosed, - setWasUqTooLowAlreadyClosed: state.setWasUqTooLowAlreadyClosed, - wasConnectWalletAlreadyClosed: state.data.wasConnectWalletAlreadyClosed, - wasRewardsAlreadyClosed: state.data.wasRewardsAlreadyClosed, - wasUqTooLowAlreadyClosed: state.data.wasUqTooLowAlreadyClosed, - })); - - useEffect(() => { - if (!address) { - return; - } - /** - * The initial value of UQ for every user is 0.2. - * It does not update automatically after delegation nor after change in Gitcoin Passport itself. - * - * We need to refreshAntisybilStatus to force BE to refetch current values from Gitcoin Passport - * and return true value. - */ - refreshAntisybilStatus(address!); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - - const isEpoch1 = currentEpoch === 1; - - const isUqTooLowTipVisible = - !!isDecisionWindowOpen && - !isPendingRefreshAntisybilStatus && - !isFetchingUqScore && - uqScore === 20n && - !wasUqTooLowAlreadyClosed; - - const isRewardsTipVisible = - !isEpoch1 && - isConnected && - !isFetchingIndividualReward && - !!individualReward && - individualReward !== 0n && - !isFetchingUserAllocation && - !userAllocations?.hasUserAlreadyDoneAllocation && - !!isDecisionWindowOpen && - !wasRewardsAlreadyClosed; - - return ( - - navigate(ROOT_ROUTES.settings.absolute)} - onClose={() => setWasUqTooLowAlreadyClosed(true)} - text={ - ]} - i18nKey={ - isDesktop - ? 'views.allocation.tip.uqTooLow.text.desktop' - : 'views.allocation.tip.uqTooLow.text.mobile' - } - /> - } - title={isDesktop ? t('uqTooLow.title.desktop') : t('uqTooLow.title.mobile')} - /> - setWasRewardsAlreadyClosed(true)} - text={isDesktop ? t('rewards.text.desktop') : t('rewards.text.mobile')} - title={t('rewards.title')} - /> - - ); -}; - -export default AllocationTipTiles; diff --git a/client/src/components/Allocation/AllocationTipTiles/types.ts b/client/src/components/Allocation/AllocationTipTiles/types.ts deleted file mode 100644 index 6ce64c2c9c..0000000000 --- a/client/src/components/Allocation/AllocationTipTiles/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface AllocationTipTilesProps { - className?: string; -} diff --git a/client/src/components/Allocation/ModalAllocationLowUqScore/ModalAllocationLowUqScore.tsx b/client/src/components/Allocation/ModalAllocationLowUqScore/ModalAllocationLowUqScore.tsx index 01fa5a8e8d..9477c4439c 100644 --- a/client/src/components/Allocation/ModalAllocationLowUqScore/ModalAllocationLowUqScore.tsx +++ b/client/src/components/Allocation/ModalAllocationLowUqScore/ModalAllocationLowUqScore.tsx @@ -1,19 +1,20 @@ import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; +import AllocationLowUqScore from 'components/Allocation/AllocationLowUqScore'; import Img from 'components/ui/Img'; import Modal from 'components/ui/Modal'; import styles from './ModalAllocationLowUqScore.module.scss'; import ModalAllocationLowUqScoreProps from './types'; -import AllocationLowUqScore from '../AllocationLowUqScore'; - const ModalAllocationLowUqScore: FC = ({ modalProps, onAllocate, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.allocation.lowUQScoreModal' }); + const { t } = useTranslation('translation', { + keyPrefix: 'components.allocation.lowUQScoreModal', + }); return ( = ({ isOverflowOnClickDisabled {...modalProps} > - + ); }; diff --git a/client/src/components/Earn/EarnHistory/index.tsx b/client/src/components/Allocation/index.tsx similarity index 54% rename from client/src/components/Earn/EarnHistory/index.tsx rename to client/src/components/Allocation/index.tsx index fffff26b2a..9532b5daf5 100644 --- a/client/src/components/Earn/EarnHistory/index.tsx +++ b/client/src/components/Allocation/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistory'; +export { default } from './Allocation'; diff --git a/client/src/views/AllocationView/types.ts b/client/src/components/Allocation/types.ts similarity index 90% rename from client/src/views/AllocationView/types.ts rename to client/src/components/Allocation/types.ts index f0b7a2af45..dc9a9db4e7 100644 --- a/client/src/views/AllocationView/types.ts +++ b/client/src/components/Allocation/types.ts @@ -4,8 +4,6 @@ export type UserAllocationElementString = Omit & value: string; }; -export type CurrentView = 'edit' | 'summary'; - export type PercentageProportions = { [key: string]: number; }; diff --git a/client/src/views/AllocationView/utils.test.ts b/client/src/components/Allocation/utils.test.ts similarity index 100% rename from client/src/views/AllocationView/utils.test.ts rename to client/src/components/Allocation/utils.test.ts diff --git a/client/src/views/AllocationView/utils.ts b/client/src/components/Allocation/utils.ts similarity index 100% rename from client/src/views/AllocationView/utils.ts rename to client/src/components/Allocation/utils.ts diff --git a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.module.scss b/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.module.scss deleted file mode 100644 index 2708f078c1..0000000000 --- a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.module.scss +++ /dev/null @@ -1,17 +0,0 @@ -.tooltip { - width: 10.4rem !important; -} - -.calculateRewards { - display: flex; - align-items: center; - justify-content: center; - width: 3.2rem; - height: 3.2rem; - background: $color-octant-grey8; - border-radius: $border-radius-10; -} - -.effectiveTooltip { - padding: 1.8rem 1.4rem 1.4rem 2.4rem !important; -} diff --git a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx b/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx deleted file mode 100644 index c3801b49af..0000000000 --- a/client/src/components/Earn/EarnBoxGlmLock/EarnBoxGlmLock.tsx +++ /dev/null @@ -1,141 +0,0 @@ -import _first from 'lodash/first'; -import React, { FC, Fragment, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useAccount } from 'wagmi'; - -import ModalEarnGlmLock from 'components/Earn/ModalEarnGlmLock/ModalEarnGlmLock'; -import ModalEarnRewardsCalculator from 'components/Earn/ModalEarnRewardsCalculator'; -import BoxRounded from 'components/ui/BoxRounded'; -import Sections from 'components/ui/BoxRounded/Sections/Sections'; -import { SectionProps } from 'components/ui/BoxRounded/Sections/types'; -import Svg from 'components/ui/Svg'; -import Tooltip from 'components/ui/Tooltip'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useDepositValue from 'hooks/queries/useDepositValue'; -import useEstimatedEffectiveDeposit from 'hooks/queries/useEstimatedEffectiveDeposit'; -import useTransactionLocalStore from 'store/transactionLocal/store'; -import { calculator } from 'svg/misc'; -import getIsPreLaunch from 'utils/getIsPreLaunch'; - -import styles from './EarnBoxGlmLock.module.scss'; -import EarnBoxGlmLockProps from './types'; - -const EarnBoxGlmLock: FC = ({ classNameBox }) => { - const { t, i18n } = useTranslation('translation', { - keyPrefix: 'components.dedicated.boxGlmLock', - }); - const { isConnected } = useAccount(); - const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( - state => ({ - isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, - transactionsPending: state.data.transactionsPending, - }), - ); - - const [isModalGlmLockOpen, setIsModalGlmLockOpen] = useState(false); - const { data: estimatedEffectiveDeposit, isFetching: isFetchingEstimatedEffectiveDeposit } = - useEstimatedEffectiveDeposit(); - const [isModalRewardsCalculatorOpen, setIsModalRewardsCalculatorOpen] = useState(false); - const { data: depositsValue, isFetching: isFetchingDepositValue } = useDepositValue(); - const { data: currentEpoch } = useCurrentEpoch(); - - const isPreLaunch = getIsPreLaunch(currentEpoch); - - const sections: SectionProps[] = [ - { - dataTest: 'BoxGlmLock__Section--current', - doubleValueProps: { - cryptoCurrency: 'golem', - dataTest: 'BoxGlmLock__Section--current__DoubleValue', - isFetching: - isFetchingDepositValue || - (isAppWaitingForTransactionToBeIndexed && - _first(transactionsPending)?.type !== 'withdrawal'), - showCryptoSuffix: true, - valueCrypto: depositsValue, - }, - isDisabled: isPreLaunch && !isConnected, - label: t('current'), - }, - { - dataTest: 'BoxGlmLock__Section--effective', - doubleValueProps: { - coinPricesServerDowntimeText: '...', - cryptoCurrency: 'golem', - dataTest: 'BoxGlmLock__Section--effective__DoubleValue', - isFetching: - isFetchingEstimatedEffectiveDeposit || - (isAppWaitingForTransactionToBeIndexed && - _first(transactionsPending)?.type !== 'withdrawal'), - showCryptoSuffix: true, - valueCrypto: estimatedEffectiveDeposit, - }, - isDisabled: isPreLaunch && !isConnected, - label: t('effective'), - tooltipProps: { - dataTest: 'TooltipEffectiveLockedBalance', - position: 'bottom-right', - text: t('tooltipText'), - tooltipClassName: styles.effectiveTooltip, - }, - }, - ]; - - return ( - - setIsModalGlmLockOpen(true), - variant: 'cta', - }} - className={classNameBox} - dataTest="BoxGlmLock__BoxRounded" - hasSections - isVertical - title={t('lockedBalance')} - titleSuffix={ - -
setIsModalRewardsCalculatorOpen(true)} - > - -
-
- } - > - -
- setIsModalGlmLockOpen(false), - }} - /> - setIsModalRewardsCalculatorOpen(false), - }} - /> -
- ); -}; - -export default EarnBoxGlmLock; diff --git a/client/src/components/Earn/EarnBoxGlmLock/types.ts b/client/src/components/Earn/EarnBoxGlmLock/types.ts deleted file mode 100644 index 10b646c2a4..0000000000 --- a/client/src/components/Earn/EarnBoxGlmLock/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnBoxGlmLockProps { - classNameBox: string; -} diff --git a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.module.scss b/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.module.scss deleted file mode 100644 index 7130b1e153..0000000000 --- a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.module.scss +++ /dev/null @@ -1,15 +0,0 @@ -.pendingTooltip { - margin-left: -2.6rem; -} - -.pendingTooltipLabel { - font-size: $font-size-12; - font-weight: $font-weight-semibold; - line-height: 2rem; -} - -.pendingTooltipDate { - font-size: $font-size-20; - font-weight: $font-weight-semibold; - margin: 0.8rem 0 0.4rem; -} diff --git a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx b/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx deleted file mode 100644 index c2dfb6ee78..0000000000 --- a/client/src/components/Earn/EarnBoxPersonalAllocation/EarnBoxPersonalAllocation.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import { format } from 'date-fns'; -import _first from 'lodash/first'; -import React, { FC, Fragment, useMemo, useState } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useAccount } from 'wagmi'; - -import ModalEarnWithdrawEth from 'components/Earn/ModalEarnWithdrawEth'; -import BoxRounded from 'components/ui/BoxRounded'; -import Sections from 'components/ui/BoxRounded/Sections/Sections'; -import { SectionProps } from 'components/ui/BoxRounded/Sections/types'; -import useEpochAndAllocationTimestamps from 'hooks/helpers/useEpochAndAllocationTimestamps'; -import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; -import useTotalPatronDonations from 'hooks/helpers/useTotalPatronDonations'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useCurrentEpochProps from 'hooks/queries/useCurrentEpochProps'; -import useIndividualReward from 'hooks/queries/useIndividualReward'; -import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; -import useIsPatronMode from 'hooks/queries/useIsPatronMode'; -import useWithdrawals from 'hooks/queries/useWithdrawals'; -import useTransactionLocalStore from 'store/transactionLocal/store'; -import getIsPreLaunch from 'utils/getIsPreLaunch'; - -import styles from './EarnBoxPersonalAllocation.module.scss'; -import EarnBoxPersonalAllocationProps from './types'; - -const EarnBoxPersonalAllocation: FC = ({ className }) => { - const { i18n, t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.boxPersonalAllocation', - }); - const [isModalOpen, setIsModalOpen] = useState(false); - const { isConnected } = useAccount(); - const { data: currentEpoch } = useCurrentEpoch(); - const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - const { timeCurrentEpochStart, timeCurrentAllocationEnd } = useEpochAndAllocationTimestamps(); - const { data: currentEpochProps } = useCurrentEpochProps(); - const { data: withdrawals, isFetching: isFetchingWithdrawals } = useWithdrawals(); - const { data: individualReward, isFetching: isFetchingIndividualReward } = useIndividualReward(); - const { data: isPatronMode } = useIsPatronMode(); - const { data: totalPatronDonations, isFetching: isFetchingTotalPatronDonations } = - useTotalPatronDonations({ isEnabledAdditional: !!isPatronMode }); - const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( - state => ({ - isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, - transactionsPending: state.data.transactionsPending, - }), - ); - - const isPreLaunch = getIsPreLaunch(currentEpoch); - const isProjectAdminMode = useIsProjectAdminMode(); - - const sections: SectionProps[] = [ - ...(!isProjectAdminMode - ? [ - { - dataTest: 'BoxPersonalAllocation__Section', - doubleValueProps: { - cryptoCurrency: 'ethereum', - dataTest: 'BoxPersonalAllocation__Section--pending__DoubleValue', - isFetching: - (isPatronMode ? isFetchingIndividualReward : isFetchingWithdrawals) || - (isAppWaitingForTransactionToBeIndexed && - _first(transactionsPending)?.type === 'withdrawal'), - showCryptoSuffix: true, - valueCrypto: isPatronMode ? individualReward : withdrawals?.sums.pending, - }, - label: isPatronMode ? t('currentEpoch') : t('pending'), - tooltipProps: isPatronMode - ? undefined - : { - position: 'bottom-right', - text: ( -
-
- {t('pendingFundsAvailableAfter')} -
-
- {/* TODO OCT-1041 fetch next epoch props instead of assuming the same length */} - {currentEpochProps && timeCurrentEpochStart && timeCurrentAllocationEnd - ? format( - new Date( - isDecisionWindowOpen - ? timeCurrentAllocationEnd - : // When AW is closed, it's when the last AW closed. - timeCurrentEpochStart + currentEpochProps.decisionWindow, - ), - 'haaa z, d LLLL', - ) - : ''} -
-
- ), - }, - } as SectionProps, - ] - : []), - { - dataTest: 'BoxPersonalAllocation__Section', - doubleValueProps: { - coinPricesServerDowntimeText: !isProjectAdminMode ? '...' : undefined, - cryptoCurrency: 'ethereum', - dataTest: 'BoxPersonalAllocation__Section--availableNow__DoubleValue', - isFetching: - (isPatronMode ? isFetchingTotalPatronDonations : isFetchingWithdrawals) || - (isAppWaitingForTransactionToBeIndexed && - _first(transactionsPending)?.type === 'withdrawal'), - showCryptoSuffix: true, - valueCrypto: isPatronMode ? totalPatronDonations?.value : withdrawals?.sums.available, - }, - label: isPatronMode && !isProjectAdminMode ? t('allTime') : i18n.t('common.availableNow'), - }, - ]; - - const title = useMemo(() => { - if (isProjectAdminMode) { - return i18n.t('common.donations'); - } - if (isPatronMode) { - return t('patronEarnings'); - } - return i18n.t('common.personalAllocation'); - }, [isProjectAdminMode, isPatronMode, i18n, t]); - - return ( - - setIsModalOpen(true), - variant: isProjectAdminMode ? 'cta' : 'secondary', - } - } - className={className} - dataTest="BoxPersonalAllocation" - hasSections - isVertical - title={title} - > - - - setIsModalOpen(false), - }} - /> - - ); -}; - -export default EarnBoxPersonalAllocation; diff --git a/client/src/components/Earn/EarnBoxPersonalAllocation/index.tsx b/client/src/components/Earn/EarnBoxPersonalAllocation/index.tsx deleted file mode 100644 index dedc208a26..0000000000 --- a/client/src/components/Earn/EarnBoxPersonalAllocation/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnBoxPersonalAllocation'; diff --git a/client/src/components/Earn/EarnBoxPersonalAllocation/types.ts b/client/src/components/Earn/EarnBoxPersonalAllocation/types.ts deleted file mode 100644 index c73db828a2..0000000000 --- a/client/src/components/Earn/EarnBoxPersonalAllocation/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnBoxPersonalAllocationProps { - className?: string; -} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/EarnGlmLockBudget.tsx b/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/EarnGlmLockBudget.tsx deleted file mode 100644 index 2dc3545210..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/EarnGlmLockBudget.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import { useFormikContext } from 'formik'; -import React, { FC } from 'react'; - -import EarnGlmLockBudgetBox from 'components/Earn/EarnGlmLock/EarnGlmLockBudgetBox'; -import { FormFields } from 'components/Earn/EarnGlmLock/types'; - -import styles from './EarnGlmLockBudget.module.scss'; -import EarnGlmLockBudgetProps from './types'; - -const EarnGlmLockBudget: FC = ({ isVisible }) => { - const { errors } = useFormikContext(); - - if (!isVisible) { - return null; - } - - return ( - - ); -}; - -export default EarnGlmLockBudget; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/types.ts b/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/types.ts deleted file mode 100644 index 4be2791aed..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnGlmLockBudgetProps { - isVisible: boolean; -} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/index.tsx b/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/index.tsx deleted file mode 100644 index 6f753d1088..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLockNotification'; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/types.ts b/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/types.ts deleted file mode 100644 index 44c72a0b37..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { CurrentMode } from 'components/Earn/EarnGlmLock/types'; - -export default interface EarnGlmLockNotificationProps { - className?: string; - currentMode: CurrentMode; - isLockingApproved: boolean; - transactionHash?: string; - type: 'success' | 'info'; -} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/types.ts b/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/types.ts deleted file mode 100644 index 5eee7e952c..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/types.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { CurrentMode } from '../types'; - -export default interface EarnGlmLockStepperProps { - className?: string; - currentMode: CurrentMode; - step: 1 | 2 | 3; -} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/index.tsx b/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/index.tsx deleted file mode 100644 index 74a7f24751..0000000000 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLockTabsInputs'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistory.module.scss b/client/src/components/Earn/EarnHistory/EarnHistory.module.scss deleted file mode 100644 index 3d6d260a79..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistory.module.scss +++ /dev/null @@ -1,43 +0,0 @@ -.root { - width: 100%; - padding: 0 0.8rem; - - @media #{$desktop-up} { - max-height: 100%; - - &.isProjectAdminMode { - min-height: 22.5rem; - } - } -} - -.skeleton { - padding: 0 $historyHorizontalPadding; -} - -.title { - padding: 0 $historyHorizontalPadding; -} - -.childrenWrapper { - display: block; - - @media #{$desktop-up} { - overflow: auto; - } -} - -.loader { - margin: 0 auto; -} - -.header { - font-size: $font-size-16; - font-weight: $font-weight-bold; - margin: 2rem 0; - text-align: left; - - @media #{$desktop-up} { - display: none; - } -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistory.tsx b/client/src/components/Earn/EarnHistory/EarnHistory.tsx deleted file mode 100644 index 2391ad3646..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistory.tsx +++ /dev/null @@ -1,73 +0,0 @@ -import cx from 'classnames'; -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; -import InfiniteScroll from 'react-infinite-scroller'; - -import EarnHistoryList from 'components/Earn/EarnHistory/EarnHistoryList'; -import EarnHistorySkeleton from 'components/Earn/EarnHistory/EarnHistorySkeleton'; -import BoxRounded from 'components/ui/BoxRounded'; -import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useHistory from 'hooks/queries/useHistory'; -import useTransactionLocalStore from 'store/transactionLocal/store'; -import getIsPreLaunch from 'utils/getIsPreLaunch'; - -import styles from './EarnHistory.module.scss'; -import EarnHistoryProps from './types'; - -const EarnHistory: FC = ({ className }) => { - const { i18n } = useTranslation('translation'); - const { transactionsPending } = useTransactionLocalStore(state => ({ - transactionsPending: state.data.transactionsPending, - })); - - const { data: currentEpoch } = useCurrentEpoch(); - const { fetchNextPage, history, hasNextPage, isFetching: isFetchingHistory } = useHistory(); - const isProjectAdminMode = useIsProjectAdminMode(); - - const isPreLaunch = getIsPreLaunch(currentEpoch); - const showLoader = isFetchingHistory && !isPreLaunch && !history?.length; - - const transactionsPendingSorted = transactionsPending?.sort( - ({ timestamp: timestampA }, { timestamp: timestampB }) => { - if (timestampA < timestampB) { - return 1; - } - if (timestampA > timestampB) { - return -1; - } - return 0; - }, - ); - - return ( - - {showLoader ? ( -
- -
- ) : ( - } - loadMore={fetchNextPage} - pageStart={0} - > - - - )} -
- ); -}; - -export default EarnHistory; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItem/EarnHistoryItem.module.scss b/client/src/components/Earn/EarnHistory/EarnHistoryItem/EarnHistoryItem.module.scss deleted file mode 100644 index 44d5a79741..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItem/EarnHistoryItem.module.scss +++ /dev/null @@ -1,41 +0,0 @@ -.box { - display: flex; - position: relative; - border-radius: 0; - padding: 0.8rem 0; -} - -.child { - padding: 1.3rem $historyHorizontalPadding; - border-radius: $border-radius-06; - - &:hover { - background: $color-octant-grey6; - } -} - -.titleAndSubtitle { - display: flex; - flex: 1; - flex-direction: column; - align-items: flex-start; -} - -.title { - font-size: $font-size-14; -} - -.separator { - width: calc(100% - $historyHorizontalPadding * 2); - height: 0.1rem; - margin-left: $historyHorizontalPadding; - background: $color-octant-grey8; -} - -.patronDonationTimestamp { - color: $color-octant-grey5; - font-size: $font-size-10; - font-weight: $font-weight-semibold; - margin-top: 0.2rem; - line-height: 1.5rem; -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/EarnHistoryItemDateAndTime.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/EarnHistoryItemDateAndTime.tsx deleted file mode 100644 index a76a811502..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/EarnHistoryItemDateAndTime.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React, { FC } from 'react'; - -import styles from './EarnHistoryItemDateAndTime.module.scss'; -import EarnHistoryItemDateAndTimeProps from './types'; -import { getHistoryItemDateAndTime } from './utils'; - -const EarnHistoryItemDateAndTime: FC = ({ timestamp }) => ( -
{getHistoryItemDateAndTime(timestamp)}
-); - -export default EarnHistoryItemDateAndTime; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/index.tsx deleted file mode 100644 index ff21a46282..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItemDateAndTime'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/types.ts deleted file mode 100644 index 4b3e75c970..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDateAndTime/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnHistoryItemDateAndTimeProps { - timestamp: string; -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetails.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetails.tsx deleted file mode 100644 index b368f0ce2a..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetails.tsx +++ /dev/null @@ -1,15 +0,0 @@ -import React, { FC } from 'react'; - -import EarnHistoryItemDetailsAllocation from './EarnHistoryItemDetailsAllocation'; -import EarnHistoryItemDetailsAllocationProps from './EarnHistoryItemDetailsAllocation/types'; -import EarnHistoryItemDetailsRest from './EarnHistoryItemDetailsRest'; -import EarnHistoryItemDetailsModalProps from './types'; - -const EarnHistoryItemDetails: FC = props => - props.type === 'allocation' ? ( - - ) : ( - - ); - -export default EarnHistoryItemDetails; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/index.tsx deleted file mode 100644 index 0434055672..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItemDetailsAllocation'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/types.ts deleted file mode 100644 index a3d57d3ee4..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsAllocation/types.ts +++ /dev/null @@ -1,8 +0,0 @@ -import HistoryItemDetailsProps from 'components/Earn/EarnHistory/EarnHistoryItemDetails/types'; -import { AllocationEventTypeParsed } from 'hooks/queries/useHistory'; - -type HistoryItemDetailsAllocationProps = Omit & { - eventData: AllocationEventTypeParsed & { amount: bigint }; -}; - -export default HistoryItemDetailsAllocationProps; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/index.tsx deleted file mode 100644 index b717fcdd7f..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItemDetailsRest'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/types.ts deleted file mode 100644 index 810fe51082..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/EarnHistoryItemDetailsRest/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import HistoryItemDetailsProps from 'components/Earn/EarnHistory/EarnHistoryItemDetails/types'; - -type HistoryItemDetailsRestProps = HistoryItemDetailsProps; - -export default HistoryItemDetailsRestProps; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/index.tsx deleted file mode 100644 index f9763b32cb..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItemDetails'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/types.ts deleted file mode 100644 index 4f52d52c5a..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetails/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import EarnHistoryItemProps from 'components/Earn/EarnHistory/EarnHistoryItem/types'; - -type EarnHistoryItemDetailsProps = Omit; - -export default EarnHistoryItemDetailsProps; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/index.tsx deleted file mode 100644 index a4668c4897..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItemDetailsModal'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/types.ts deleted file mode 100644 index 31e7f15111..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItemDetailsModal/types.ts +++ /dev/null @@ -1,6 +0,0 @@ -import HistoryItemProps from 'components/Earn/EarnHistory/EarnHistoryItem/types'; -import ModalProps from 'components/ui/Modal/types'; - -export default interface HistoryItemDetailsModalProps extends Omit { - modalProps: Omit; -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.module.scss b/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.module.scss deleted file mode 100644 index bb61975271..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.module.scss +++ /dev/null @@ -1,10 +0,0 @@ -.emptyHistoryInfo { - height: 7.2rem; - padding: 0 $historyHorizontalPadding; - display: flex; - align-items: center; - color: $color-octant-grey5; - font-size: $font-size-14; - font-weight: $font-weight-semibold; - width: 100%; -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.tsx deleted file mode 100644 index 191f421ecc..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryList/EarnHistoryList.tsx +++ /dev/null @@ -1,26 +0,0 @@ -import React, { FC, Fragment } from 'react'; -import { useTranslation } from 'react-i18next'; - -import EarnHistoryItem from 'components/Earn/EarnHistory/EarnHistoryItem'; - -import styles from './EarnHistoryList.module.scss'; -import EarnHistoryListProps from './types'; - -const EarnHistoryList: FC = ({ history }) => { - const { t } = useTranslation('translation', { keyPrefix: 'components.dedicated.historyList' }); - - if (!history?.length) { - return
{t('emptyHistory')}
; - } - - return ( - - {history.map((element, index) => ( - // eslint-disable-next-line react/no-array-index-key - - ))} - - ); -}; - -export default EarnHistoryList; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryList/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryList/index.tsx deleted file mode 100644 index 30ab58f35f..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryList/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryList'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryList/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryList/types.ts deleted file mode 100644 index 7b2d5ec9bf..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryList/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -import EarnHistoryItemProps from 'components/Earn/EarnHistory/EarnHistoryItem/types'; - -export default interface EarnHistoryListProps { - history: Omit[] | undefined; -} diff --git a/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/index.tsx deleted file mode 100644 index f135d4aeab..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistorySkeleton'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/index.tsx b/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/index.tsx deleted file mode 100644 index 89d0ec7f40..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryTransactionLabel'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/types.ts b/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/types.ts deleted file mode 100644 index 48ecf3a83a..0000000000 --- a/client/src/components/Earn/EarnHistory/EarnHistoryTransactionLabel/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default interface EarnHistoryTransactionLabelProps { - isFinalized: boolean; - isMultisig?: boolean; -} diff --git a/client/src/components/Earn/EarnHistory/types.ts b/client/src/components/Earn/EarnHistory/types.ts deleted file mode 100644 index 36f13f4510..0000000000 --- a/client/src/components/Earn/EarnHistory/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnHistoryProps { - className?: string; -} diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.module.scss b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.module.scss deleted file mode 100644 index cd51f05d94..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculator.module.scss +++ /dev/null @@ -1,3 +0,0 @@ -.glmInput { - margin-bottom: 1.4rem; -} diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEpochDaysSelector/index.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEpochDaysSelector/index.tsx deleted file mode 100644 index 8a5a8975ad..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEpochDaysSelector/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnRewardsCalculatorEpochDaysSelector'; diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.module.scss b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.module.scss deleted file mode 100644 index 9fe43db3c0..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.module.scss +++ /dev/null @@ -1,56 +0,0 @@ -.root { - width: 100%; - - .estimates { - border-radius: $border-radius-04; - border: 1px solid $color-octant-grey1; - background: $color-octant-grey8; - } - - .estimatesLabel { - color: $color-octant-grey5; - font-size: $font-size-10; - font-weight: $font-weight-bold; - line-height: normal; - margin-bottom: 0.6rem; - text-align: left; - width: 100%; - } - - .row { - display: flex; - padding: 1.2rem 1.6rem; - justify-content: space-between; - position: relative; - - &:first-child { - &::after { - position: absolute; - bottom: 0; - left: 1.6rem; - content: ''; - width: calc(100% - 3.2rem); - height: 0.1rem; - background: $color-octant-grey1; - } - } - - .label, - .value { - display: flex; - align-items: center; - height: 2.4rem; - color: $color-octant-dark; - font-size: $font-size-14; - font-weight: $font-weight-bold; - } - - .value { - &.showSkeleton { - @include skeleton($color-octant-grey1, $color-octant-grey12); - width: 10.4rem; - height: 2.4rem; - } - } - } -} diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.tsx deleted file mode 100644 index 0525ae5229..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/EarnRewardsCalculatorEstimates.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import cx from 'classnames'; -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; - -import styles from './EarnRewardsCalculatorEstimates.module.scss'; -import { EarnRewardsCalculatorEstimatesProps } from './types'; - -const EarnRewardsCalculatorEstimates: FC = ({ - estimatedRewards, - matchFunding, - isLoading, -}) => { - const { i18n, t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.rewardsCalculator', - }); - - const dataTest = 'EarnRewardsCalculatorEstimates'; - - return ( -
-
- {t('estimates')} -
-
-
-
- {i18n.t('common.rewards', { rewards: '' })} -
-
- {estimatedRewards ? estimatedRewards.primary : ''} -
-
-
-
- {i18n.t('common.matchFunding')} -
-
- {matchFunding ? matchFunding.primary : ''} -
-
-
-
- ); -}; - -export default EarnRewardsCalculatorEstimates; diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/index.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/index.tsx deleted file mode 100644 index c66017dc17..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorEstimates/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnRewardsCalculatorEstimates'; diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.module.scss b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.module.scss deleted file mode 100644 index 6cbabb095f..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.module.scss +++ /dev/null @@ -1,63 +0,0 @@ -.root { - width: 100%; - - .daysSelectorLabel, - .estimatesLabel { - color: $color-octant-grey5; - font-size: $font-size-10; - font-weight: $font-weight-bold; - margin-bottom: 0.6rem; - text-align: left; - width: 100%; - } - - .daysSelector { - margin-bottom: 1rem; - display: flex; - align-items: center; - justify-content: space-between; - width: 100%; - height: 4.8rem; - background: $color-white; - border-radius: $border-radius-04; - border: 0.1rem solid $color-octant-grey1; - padding: 0.4rem 1.6rem 0.4rem 0.6rem; - - .daysWrapper { - display: flex; - flex: 1; - } - - .day { - position: relative; - cursor: pointer; - color: $color-octant-grey2; - font-weight: $font-weight-bold; - display: flex; - align-items: center; - justify-content: center; - flex: 1; - height: 3.8rem; - - .dayLabel { - z-index: $z-index-2; - } - - &.isSelected { - color: $color-octant-dark; - transition: all $transition-time-4; - } - - .selectedItemBackground { - position: absolute; - z-index: $z-index-1; - width: 100%; - height: 100%; - top: 0; - left: 0; - background-color: $color-octant-grey1; - border-radius: $border-radius-04; - } - } - } -} diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.tsx deleted file mode 100644 index 725a5ff1f1..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/EarnRewardsCalculatorUqSelector.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import cx from 'classnames'; -import { motion } from 'framer-motion'; -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; - -import styles from './EarnRewardsCalculatorUqSelector.module.scss'; -import EarnRewardsCalculatorUqSelectorProps from './types'; - -const EarnRewardsCalculatorUqSelector: FC = ({ - isUqScoreOver20, - onChange, -}) => { - const { t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.rewardsCalculator.uqSelector', - }); - - const selectorOptions = [true, false]; // isUqScoreOver20 - - const dataTest = 'EarnRewardsCalculatorUqSelector'; - - return ( -
-
- {t('header')} -
-
-
- {/* eslint-disable-next-line @typescript-eslint/naming-convention */} - {selectorOptions.map((option, index) => ( -
onChange(option)} - > - - {option ? t('isUqScoreOver20_true') : t('isUqScoreOver20_false')} - - {isUqScoreOver20 === option ? ( - - ) : null} -
- ))} -
-
-
- ); -}; - -export default EarnRewardsCalculatorUqSelector; diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/index.tsx b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/index.tsx deleted file mode 100644 index c15db5a662..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnRewardsCalculatorUqSelector'; diff --git a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/types.ts b/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/types.ts deleted file mode 100644 index 5b4d67db72..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/EarnRewardsCalculatorUqSelector/types.ts +++ /dev/null @@ -1,4 +0,0 @@ -export default interface EarnRewardsCalculatorUqSelectorProps { - isUqScoreOver20: boolean; - onChange: (isUqScoreOver20: boolean) => void; -} diff --git a/client/src/components/Earn/EarnRewardsCalculator/index.tsx b/client/src/components/Earn/EarnRewardsCalculator/index.tsx deleted file mode 100644 index e80032b867..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnRewardsCalculator'; diff --git a/client/src/components/Earn/EarnRewardsCalculator/types.ts b/client/src/components/Earn/EarnRewardsCalculator/types.ts deleted file mode 100644 index e6173bd363..0000000000 --- a/client/src/components/Earn/EarnRewardsCalculator/types.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type FormFields = { - isUqScoreOver20: boolean; - numberOfEpochs: number; - valueCrypto?: string; -}; diff --git a/client/src/components/Earn/EarnTipTiles/EarnTipTiles.module.scss b/client/src/components/Earn/EarnTipTiles/EarnTipTiles.module.scss deleted file mode 100644 index ba0bfc9c8c..0000000000 --- a/client/src/components/Earn/EarnTipTiles/EarnTipTiles.module.scss +++ /dev/null @@ -1,27 +0,0 @@ -.lockGlmImage { - height: 10.5rem; - - @media #{$desktop-up} { - height: 11.7rem; - } -} - -.connectWalletImage { - @include tipTileConnectWalletImage(); -} - -.withdrawImage { - height: 7.8rem; - - @media #{$desktop-up} { - height: 10.4rem; - } -} - -.allocateYourRewardsImage { - height: 7.5rem; - - @media #{$desktop-up} { - height: 10.4rem; - } -} diff --git a/client/src/components/Earn/EarnTipTiles/EarnTipTiles.tsx b/client/src/components/Earn/EarnTipTiles/EarnTipTiles.tsx deleted file mode 100644 index b8404e488a..0000000000 --- a/client/src/components/Earn/EarnTipTiles/EarnTipTiles.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import { differenceInCalendarDays } from 'date-fns'; -import React, { Fragment, ReactElement } from 'react'; -import { useTranslation } from 'react-i18next'; -import { useAccount } from 'wagmi'; - -import TipTile from 'components/shared/TipTile'; -import useEpochAndAllocationTimestamps from 'hooks/helpers/useEpochAndAllocationTimestamps'; -import useMediaQuery from 'hooks/helpers/useMediaQuery'; -import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; -import useDepositValue from 'hooks/queries/useDepositValue'; -import useIndividualReward from 'hooks/queries/useIndividualReward'; -import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; -import useUserAllocations from 'hooks/queries/useUserAllocations'; -import useWithdrawals from 'hooks/queries/useWithdrawals'; -import useTipsStore from 'store/tips/store'; -import getIsPreLaunch from 'utils/getIsPreLaunch'; - -import styles from './EarnTipTiles.module.scss'; - -const EarnTipTiles = (): ReactElement => { - const { t } = useTranslation('translation', { - keyPrefix: 'views.earn.tips', - }); - const { isConnected } = useAccount(); - const { isDesktop } = useMediaQuery(); - const { data: withdrawals } = useWithdrawals(); - const { data: currentEpoch } = useCurrentEpoch(); - const isPreLaunch = getIsPreLaunch(currentEpoch); - const { data: depositsValue, isFetching: isFetchingDepositsValue } = useDepositValue(); - const { data: individualReward } = useIndividualReward(); - const { data: userAllocations } = useUserAllocations(); - const { timeCurrentAllocationEnd } = useEpochAndAllocationTimestamps(); - const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); - - const { - wasWithdrawAlreadyClosed, - setWasWithdrawAlreadyClosed, - wasConnectWalletAlreadyClosed, - setWasConnectWalletAlreadyClosed, - wasLockGLMAlreadyClosed, - setWasLockGLMAlreadyClosed, - wasAllocateRewardsAlreadyClosed, - setWasAllocateRewardsAlreadyClosed, - } = useTipsStore(state => ({ - setWasAllocateRewardsAlreadyClosed: state.setWasAllocateRewardsAlreadyClosed, - setWasConnectWalletAlreadyClosed: state.setWasConnectWalletAlreadyClosed, - setWasLockGLMAlreadyClosed: state.setWasLockGLMAlreadyClosed, - setWasWithdrawAlreadyClosed: state.setWasWithdrawAlreadyClosed, - wasAllocateRewardsAlreadyClosed: state.data.wasAllocateRewardsAlreadyClosed, - wasConnectWalletAlreadyClosed: state.data.wasConnectWalletAlreadyClosed, - wasLockGLMAlreadyClosed: state.data.wasLockGLMAlreadyClosed, - wasWithdrawAlreadyClosed: state.data.wasWithdrawAlreadyClosed, - })); - - const isLockGlmTipVisible = - !isFetchingDepositsValue && - (!depositsValue || (!!depositsValue && depositsValue === 0n)) && - isConnected && - !wasLockGLMAlreadyClosed; - const isConnectWalletTipVisible = !isPreLaunch && !isConnected && !wasConnectWalletAlreadyClosed; - const isWithdrawTipVisible = - !!currentEpoch && - currentEpoch > 1 && - !!withdrawals && - withdrawals.sums.available !== 0n && - !wasWithdrawAlreadyClosed; - - const isAllocateRewardsTipVisible = - (!wasAllocateRewardsAlreadyClosed && - isDecisionWindowOpen && - !!(individualReward && individualReward !== 0n) && - !userAllocations?.hasUserAlreadyDoneAllocation && - differenceInCalendarDays(new Date(), timeCurrentAllocationEnd!) <= 2) ?? - false; - - return ( - - setWasLockGLMAlreadyClosed(true)} - text={t(isDesktop ? 'lockGlm.text.desktop' : 'lockGlm.text.mobile')} - title={t('lockGlm.title')} - /> - setWasConnectWalletAlreadyClosed(true)} - text={t(isDesktop ? 'connectWallet.text.desktop' : 'connectWallet.text.mobile')} - title={t(isDesktop ? 'connectWallet.title.desktop' : 'connectWallet.title.mobile')} - /> - setWasWithdrawAlreadyClosed(true)} - text={t(isDesktop ? 'withdrawEth.text.desktop' : 'withdrawEth.text.mobile')} - title={t('withdrawEth.title')} - /> - setWasAllocateRewardsAlreadyClosed(true)} - text={t(isDesktop ? 'allocateYourRewards.text.desktop' : 'allocateYourRewards.text.mobile')} - title={t('allocateYourRewards.title')} - /> - - ); -}; - -export default EarnTipTiles; diff --git a/client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.module.scss b/client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.module.scss deleted file mode 100644 index 067ce0ebaf..0000000000 --- a/client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.module.scss +++ /dev/null @@ -1,20 +0,0 @@ -.root { - width: 100%; - height: 100%; -} - -.inputs { - display: flex; - justify-content: space-between; - align-items: flex-end; - width: 100%; -} - -.input { - @include flexBasisGutter(2, 1.6rem); -} - -.button { - width: 100%; - margin: 2.4rem 0; -} diff --git a/client/src/components/Earn/EarnWithdrawEth/index.tsx b/client/src/components/Earn/EarnWithdrawEth/index.tsx deleted file mode 100644 index 0939f1d9b8..0000000000 --- a/client/src/components/Earn/EarnWithdrawEth/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './EarnWithdrawEth'; diff --git a/client/src/components/Earn/EarnWithdrawEth/types.ts b/client/src/components/Earn/EarnWithdrawEth/types.ts deleted file mode 100644 index e332c4c258..0000000000 --- a/client/src/components/Earn/EarnWithdrawEth/types.ts +++ /dev/null @@ -1,3 +0,0 @@ -export default interface EarnWithdrawEthProps { - onCloseModal: () => void; -} diff --git a/client/src/components/Earn/ModalEarnGlmLock/index.tsx b/client/src/components/Earn/ModalEarnGlmLock/index.tsx deleted file mode 100644 index 252540a7ba..0000000000 --- a/client/src/components/Earn/ModalEarnGlmLock/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './ModalEarnGlmLock'; diff --git a/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.module.scss b/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.module.scss deleted file mode 100644 index f73dec9c84..0000000000 --- a/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.module.scss +++ /dev/null @@ -1,21 +0,0 @@ -.root { - .header { - display: flex; - align-items: center; - overflow: visible; - } -} - -.tooltip { - margin-left: 0.8rem; - position: relative; - - .tooltipContainer { - top: 3.6rem; - left: -16.7rem; - } -} - -.tooltipWrapper { - position: relative; -} diff --git a/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.tsx b/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.tsx deleted file mode 100644 index aa83945606..0000000000 --- a/client/src/components/Earn/ModalEarnRewardsCalculator/ModalEarnRewardsCalculator.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; - -import EarnRewardsCalculator from 'components/Earn/EarnRewardsCalculator'; -import Modal from 'components/ui/Modal'; -import Svg from 'components/ui/Svg'; -import Tooltip from 'components/ui/Tooltip'; -import { questionMark } from 'svg/misc'; - -import styles from './ModalEarnRewardsCalculator.module.scss'; -import ModalEarnRewardsCalculatorProps from './types'; - -const ModalEarnRewardsCalculator: FC = ({ modalProps }) => { - const { i18n, t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.rewardsCalculator', - }); - - return ( - -
{i18n.t('common.estimateRewards')}
- - - - - } - headerClassName={styles.header} - isOpen={modalProps.isOpen} - onClosePanel={modalProps.onClosePanel} - > - -
- ); -}; - -export default ModalEarnRewardsCalculator; diff --git a/client/src/components/Earn/ModalEarnRewardsCalculator/index.tsx b/client/src/components/Earn/ModalEarnRewardsCalculator/index.tsx deleted file mode 100644 index 975cda7194..0000000000 --- a/client/src/components/Earn/ModalEarnRewardsCalculator/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './ModalEarnRewardsCalculator'; diff --git a/client/src/components/Earn/ModalEarnWithdrawEth/ModalEarnWithdrawEth.tsx b/client/src/components/Earn/ModalEarnWithdrawEth/ModalEarnWithdrawEth.tsx deleted file mode 100644 index 1004f04d8d..0000000000 --- a/client/src/components/Earn/ModalEarnWithdrawEth/ModalEarnWithdrawEth.tsx +++ /dev/null @@ -1,21 +0,0 @@ -import React, { FC } from 'react'; -import { useTranslation } from 'react-i18next'; - -import EarnWithdrawEth from 'components/Earn/EarnWithdrawEth'; -import Modal from 'components/ui/Modal'; - -import ModalEarnWithdrawingProps from './types'; - -const ModalEarnWithdrawEth: FC = ({ modalProps }) => { - const { t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.modalWithdrawEth', - }); - - return ( - - - - ); -}; - -export default ModalEarnWithdrawEth; diff --git a/client/src/components/Earn/ModalEarnWithdrawEth/index.tsx b/client/src/components/Earn/ModalEarnWithdrawEth/index.tsx deleted file mode 100644 index 1744f844c5..0000000000 --- a/client/src/components/Earn/ModalEarnWithdrawEth/index.tsx +++ /dev/null @@ -1,2 +0,0 @@ -// eslint-disable-next-line no-restricted-exports -export { default } from './ModalEarnWithdrawEth'; diff --git a/client/src/components/Home/HomeGrid/HomeGrid.module.scss b/client/src/components/Home/HomeGrid/HomeGrid.module.scss new file mode 100644 index 0000000000..934ddad086 --- /dev/null +++ b/client/src/components/Home/HomeGrid/HomeGrid.module.scss @@ -0,0 +1,105 @@ +.gridTile { + height: 32.8rem; +} + +.transactions { + @media #{$large-desktop-up} { + order: 2; + } + + &.isPatronMode, + &.isProjectAdminMode { + order: 0; + } +} + +.rewardsEstimator { + @media #{$large-desktop-up} { + order: 2; + } + + &.isPatronMode, + &.isProjectAdminMode { + order: 0; + } +} + +.videoBar { + position: relative; + order: 1; + grid-column: span 1; + + @media #{$tablet-up} { + grid-column: span 2; + } + + @media #{$desktop-up} { + grid-column: span 3; + } + + @media #{$large-desktop-up} { + grid-column: span 4; + } + + &.isPatronMode { + @media #{$tablet-up} { + grid-column: span 2; + } + + @media #{$large-desktop-up} { + grid-column: span 4; + } + } + + &.isProjectAdminMode { + order: 1; + grid-column: span 1; + + @media #{$desktop-up} { + grid-column: span 3; + } + + @media #{$large-desktop-up} { + grid-column: span 1; + } + } +} +.epochResults { + order: 2; + + @media #{$tablet-up} { + grid-column: span 2; + } + + @media #{$desktop-up} { + grid-column: span 3; + + &.isPatronMode:not(.withVideoBar) { + order: 0; + grid-column: span 2; + } + } + + @media #{$large-desktop-up} { + grid-column: span 2; + + &.isProjectAdminMode, + &.isPatronMode, + &.isPatronMode:not(.withVideoBar) { + order: 2; + grid-column: span 4; + } + } +} + +.divider1 { + @media #{$tablet-up} { + order: 1; + } +} + +.divider2 { + @media #{$tablet-up} { + order: 2; + } +} diff --git a/client/src/components/Home/HomeGrid/HomeGrid.tsx b/client/src/components/Home/HomeGrid/HomeGrid.tsx new file mode 100644 index 0000000000..a0cc5d2805 --- /dev/null +++ b/client/src/components/Home/HomeGrid/HomeGrid.tsx @@ -0,0 +1,88 @@ +import cx from 'classnames'; +import React, { memo, ReactNode } from 'react'; + +import HomeGridCurrentGlmLock from 'components/Home/HomeGridCurrentGlmLock'; +import HomeGridDivider from 'components/Home/HomeGridDivider'; +import HomeGridDonations from 'components/Home/HomeGridDonations'; +import HomeGridEpochResults from 'components/Home/HomeGridEpochResults'; +import HomeGridPersonalAllocation from 'components/Home/HomeGridPersonalAllocation'; +import HomeGridRewardsEstimator from 'components/Home/HomeGridRewardsEstimator'; +import HomeGridTransactions from 'components/Home/HomeGridTransactions'; +import HomeGridUQScore from 'components/Home/HomeGridUQScore'; +import HomeGridVideoBar from 'components/Home/HomeGridVideoBar'; +import Grid from 'components/shared/Grid'; +import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useIsPatronMode from 'hooks/queries/useIsPatronMode'; +import useSettingsStore from 'store/settings/store'; + +import styles from './HomeGrid.module.scss'; + +const HomeGrid = (): ReactNode => { + const { isLargeDesktop, isDesktop, isMobile, isTablet } = useMediaQuery(); + const isProjectAdminMode = useIsProjectAdminMode(); + const { data: isPatronMode } = useIsPatronMode(); + + const { showHelpVideos } = useSettingsStore(state => ({ + showHelpVideos: state.data.showHelpVideos, + })); + + const showDivider1 = + showHelpVideos && + ((isProjectAdminMode && isDesktop && !isLargeDesktop) || + (!isProjectAdminMode && !isPatronMode && !isMobile) || + (isPatronMode && (isLargeDesktop || isTablet))); + + const showDivider2 = + (!isPatronMode && !isMobile) || + (isPatronMode && showHelpVideos && !isMobile) || + (isPatronMode && !showHelpVideos && (isLargeDesktop || isTablet)); + + return ( + + {!isProjectAdminMode && } + {!isProjectAdminMode && !isPatronMode && } + {!isPatronMode && } + {!isProjectAdminMode && } + {showDivider1 && } + {showHelpVideos && ( + + )} + {showDivider2 && } + + + + + ); +}; + +export default memo(HomeGrid); diff --git a/client/src/components/Earn/EarnGlmLock/index.tsx b/client/src/components/Home/HomeGrid/index.tsx similarity index 54% rename from client/src/components/Earn/EarnGlmLock/index.tsx rename to client/src/components/Home/HomeGrid/index.tsx index 51932492ba..5bde98c328 100644 --- a/client/src/components/Earn/EarnGlmLock/index.tsx +++ b/client/src/components/Home/HomeGrid/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLock'; +export { default } from './HomeGrid'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.module.scss new file mode 100644 index 0000000000..7b6adc1d17 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.module.scss @@ -0,0 +1,22 @@ +.root { + padding: 1.4rem 2.4rem 2.4rem; + + @media #{$tablet-up} { + padding-top: 0.8rem; + } + + .divider { + margin-top: 2rem; + width: 100%; + height: 0.1rem; + background-color: $color-octant-grey1; + } + + .lockGlmButton { + margin-top: 2.4rem; + width: 100%; + } + .effective { + min-height: 6.4rem; + } +} diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx new file mode 100644 index 0000000000..5d3210569f --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/HomeGridCurrentGlmLock.tsx @@ -0,0 +1,110 @@ +import _first from 'lodash/first'; +import React, { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAccount } from 'wagmi'; + +import ModalLockGlm from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm'; +import GridTile from 'components/shared/Grid/GridTile'; +import Sections from 'components/ui/BoxRounded/Sections/Sections'; +import Button from 'components/ui/Button'; +import DoubleValue from 'components/ui/DoubleValue'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useDepositValue from 'hooks/queries/useDepositValue'; +import useEstimatedEffectiveDeposit from 'hooks/queries/useEstimatedEffectiveDeposit'; +import useTransactionLocalStore from 'store/transactionLocal/store'; +import getIsPreLaunch from 'utils/getIsPreLaunch'; + +import styles from './HomeGridCurrentGlmLock.module.scss'; +import HomeGridCurrentGlmLockProps from './types'; + +const HomeGridCurrentGlmLock: FC = ({ className }) => { + const { isConnected } = useAccount(); + const { i18n, t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridCurrentGlmLock', + }); + const { data: currentEpoch } = useCurrentEpoch(); + const { isMobile } = useMediaQuery(); + const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( + state => ({ + isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, + transactionsPending: state.data.transactionsPending, + }), + ); + + const [isModalLockGlmOpen, setIsModalLockGlmOpen] = useState(false); + const { data: estimatedEffectiveDeposit, isFetching: isFetchingEstimatedEffectiveDeposit } = + useEstimatedEffectiveDeposit(); + const { data: depositsValue, isFetching: isFetchingDepositValue } = useDepositValue(); + + const isPreLaunch = getIsPreLaunch(currentEpoch); + + return ( + <> + +
+ +
+ + +
+ + setIsModalLockGlmOpen(false), + }} + /> + + ); +}; + +export default HomeGridCurrentGlmLock; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLock.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/LockGlm.module.scss similarity index 100% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLock.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/LockGlm.module.scss diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/LockGlm.tsx similarity index 90% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/LockGlm.tsx index 3a966b3735..283cbce4ad 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLock.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/LockGlm.tsx @@ -4,10 +4,10 @@ import { useTranslation } from 'react-i18next'; import { useAccount, useWalletClient, usePublicClient, useWaitForTransactionReceipt } from 'wagmi'; import { apiGetSafeTransactions } from 'api/calls/safeTransactions'; -import EarnGlmLockBudget from 'components/Earn/EarnGlmLock/EarnGlmLockBudget'; -import EarnGlmLockNotification from 'components/Earn/EarnGlmLock/EarnGlmLockNotification'; -import EarnGlmLockStepper from 'components/Earn/EarnGlmLock/EarnGlmLockStepper'; -import EarnGlmLockTabs from 'components/Earn/EarnGlmLock/EarnGlmLockTabs'; +import LockGlmBudget from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget'; +import LockGlmNotification from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification'; +import LockGlmStepper from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper'; +import LockGlmTabs from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs'; import networkConfig from 'constants/networkConfig'; import env from 'env'; import { writeContractERC20 } from 'hooks/contracts/writeContracts'; @@ -26,11 +26,11 @@ import toastService from 'services/toastService'; import useTransactionLocalStore from 'store/transactionLocal/store'; import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; -import styles from './EarnGlmLock.module.scss'; -import EarnGlmLockProps, { Step, OnReset } from './types'; +import styles from './LockGlm.module.scss'; +import LockGlmProps, { Step, OnReset } from './types'; import { formInitialValues, validationSchema } from './utils'; -const EarnGlmLock: FC = ({ currentMode, onCurrentModeChange, onCloseModal }) => { +const LockGlm: FC = ({ currentMode, onCurrentModeChange, onCloseModal }) => { const { i18n } = useTranslation(); const { address } = useAccount(); const publicClient = usePublicClient({ chainId: networkConfig.id }); @@ -83,7 +83,7 @@ const EarnGlmLock: FC = ({ currentMode, onCurrentModeChange, o /** * When input is focused isCryptoOrFiatInputFocused is true. * Clicking "use max" blurs inputs, setting isCryptoOrFiatInputFocused to false. - * EarnGlmLockTabs onMax sets the focus back on inputs, triggering isCryptoOrFiatInputFocused to true. + * LockGlmTabs onMax sets the focus back on inputs, triggering isCryptoOrFiatInputFocused to true. * * Between second and third update flickering can occur, when focus is already set to input, * but state didn't update yet. @@ -211,11 +211,11 @@ const EarnGlmLock: FC = ({ currentMode, onCurrentModeChange, o {props => (
{isDesktop && ( - + )} {(step === 2 && currentMode === 'lock' && isApprovalKnown && !isLockingApproved) || step === 3 ? ( - = ({ currentMode, onCurrentModeChange, o type={step === 3 ? 'success' : 'info'} /> ) : ( - + )} - = ({ currentMode, onCurrentModeChange, o ); }; -export default EarnGlmLock; +export default LockGlm; diff --git a/client/src/components/shared/TipTile/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/index.tsx similarity index 57% rename from client/src/components/shared/TipTile/index.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/index.tsx index 20b8333882..90241ca3cc 100644 --- a/client/src/components/shared/TipTile/index.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './TipTile'; +export { default } from './LockGlm'; diff --git a/client/src/components/Earn/EarnGlmLock/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types.ts similarity index 91% rename from client/src/components/Earn/EarnGlmLock/types.ts rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types.ts index 29497d8612..bd8d5fe2d2 100644 --- a/client/src/components/Earn/EarnGlmLock/types.ts +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types.ts @@ -17,7 +17,7 @@ export type OnReset = ({ setFieldValue?: FormikHelpers['setFieldValue']; }) => void; -export default interface EarnGlmLockProps { +export default interface LockGlmProps { currentMode: CurrentMode; onCloseModal: () => void; onCurrentModeChange: (currentMode: CurrentMode) => void; diff --git a/client/src/components/Earn/EarnGlmLock/utils.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/utils.ts similarity index 100% rename from client/src/components/Earn/EarnGlmLock/utils.ts rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/utils.ts diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/EarnGlmLockBudget.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/LockGlmBudget.module.scss similarity index 100% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockBudget/EarnGlmLockBudget.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/LockGlmBudget.module.scss diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/LockGlmBudget.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/LockGlmBudget.tsx new file mode 100644 index 0000000000..b5d65bbb09 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/LockGlmBudget.tsx @@ -0,0 +1,26 @@ +import { useFormikContext } from 'formik'; +import React, { FC } from 'react'; + +import { FormFields } from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; +import LockGlmBudgetBox from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox'; + +import styles from './LockGlmBudget.module.scss'; +import LockGlmBudgetProps from './types'; + +const LockGlmBudget: FC = ({ isVisible }) => { + const { errors } = useFormikContext(); + + if (!isVisible) { + return null; + } + + return ( + + ); +}; + +export default LockGlmBudget; diff --git a/client/src/components/Project/ProjectDonors/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/index.tsx similarity index 53% rename from client/src/components/Project/ProjectDonors/index.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/index.tsx index c37620f686..52502f923e 100644 --- a/client/src/components/Project/ProjectDonors/index.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './ProjectDonors'; +export { default } from './LockGlmBudget'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/types.ts new file mode 100644 index 0000000000..34f7b64b31 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudget/types.ts @@ -0,0 +1,3 @@ +export default interface LockGlmBudgetProps { + isVisible: boolean; +} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss similarity index 63% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss index 8762572265..aafbfc59d3 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.module.scss +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.module.scss @@ -1,29 +1,3 @@ -.transactionHash { - display: flex; - justify-content: center; - margin: 1rem 0 0 0; -} - -.availableFunds { - height: 3.2rem; - display: flex; - align-items: flex-end; - border-top: 0.1rem solid $color-white; - margin-top: 1.6rem; - - .value { - color: $color-black; - - &.isError { - color: $color-octant-orange; - } - } -} - -.button { - padding: 0; -} - .budgetRow { position: relative; height: 5.6rem; @@ -50,7 +24,6 @@ width: 10rem; } - .budgetLabel { color: $color-octant-grey5; } diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx similarity index 81% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx index 37733eaf77..1713e0581d 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/EarnGlmLockBudgetBox.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/LockGlmBudgetBox.tsx @@ -7,10 +7,10 @@ import useAvailableFundsGlm from 'hooks/helpers/useAvailableFundsGlm'; import useDepositValue from 'hooks/queries/useDepositValue'; import getFormattedGlmValue from 'utils/getFormattedGlmValue'; -import styles from './EarnGlmLockBudgetBox.module.scss'; -import EarnGlmLockBudgetBoxProps from './types'; +import styles from './LockGlmBudgetBox.module.scss'; +import LockGlmBudgetBoxProps from './types'; -const EarnGlmLockBudgetBox: FC = ({ +const LockGlmBudgetBox: FC = ({ className, isWalletBalanceError, isCurrentlyLockedError, @@ -19,7 +19,7 @@ const EarnGlmLockBudgetBox: FC = ({ const { data: availableFundsGlm, isFetched: isFetchedAvailableFundsGlm } = useAvailableFundsGlm(); const { t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.budgetBox', + keyPrefix: 'components.home.homeGridCurrentGlmLock.modalLockGlm.lockGlmBudgetBox', }); const depositsValueString = useMemo( @@ -35,7 +35,7 @@ const EarnGlmLockBudgetBox: FC = ({ = ({ ) : (
{depositsValueString}
@@ -60,7 +60,7 @@ const EarnGlmLockBudgetBox: FC = ({ ) : (
{availableFundsGlmString}
@@ -70,4 +70,4 @@ const EarnGlmLockBudgetBox: FC = ({ ); }; -export default EarnGlmLockBudgetBox; +export default LockGlmBudgetBox; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/index.tsx new file mode 100644 index 0000000000..479b4382ca --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './LockGlmBudgetBox'; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/types.ts similarity index 63% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/types.ts rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/types.ts index 74d224f84c..182be764b7 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/types.ts +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmBudgetBox/types.ts @@ -1,4 +1,4 @@ -export default interface EarnGlmLockBudgetBoxProps { +export default interface LockGlmBudgetBoxProps { className?: string; isCurrentlyLockedError?: boolean; isWalletBalanceError?: boolean; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.module.scss similarity index 65% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.module.scss index 8c77ef5984..5199cae37c 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.module.scss +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.module.scss @@ -20,16 +20,5 @@ .text { color: $color-octant-grey5; text-align: left; - - .link { - font-size: $font-size-12; - min-height: 2rem; - font-weight: $font-weight-semibold; - - &:hover { - cursor: pointer; - transform: none; - } - } } } diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.tsx similarity index 63% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.tsx index e32df3f928..4e857801d6 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockNotification/EarnGlmLockNotification.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/LockGlmNotification.tsx @@ -2,30 +2,15 @@ import React, { FC, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; import BoxRounded from 'components/ui/BoxRounded'; -import Button from 'components/ui/Button'; import Svg from 'components/ui/Svg'; -import networkConfig from 'constants/networkConfig'; -import { arrowTopRight, checkMark, notificationIconWarning } from 'svg/misc'; +import { checkMark, notificationIconWarning } from 'svg/misc'; -import styles from './EarnGlmLockNotification.module.scss'; -import EarnGlmLockNotificationProps from './types'; +import styles from './LockGlmNotification.module.scss'; +import LockGlmNotificationProps from './types'; -const ButtonLinkWithIcon: FC<{ children?: React.ReactNode; transactionHash: string }> = ({ - children, - transactionHash, -}) => { - return ( - - ); -}; +import LockGlmNotificationLinkButton from '../LockGlmNotificationLinkButton'; -const EarnGlmLockNotification: FC = ({ +const LockGlmNotification: FC = ({ className, isLockingApproved, type, @@ -68,11 +53,7 @@ const EarnGlmLockNotification: FC = ({ isVertical >
- +
{label &&
{label}
} {text && ( @@ -80,7 +61,7 @@ const EarnGlmLockNotification: FC = ({ ] + ? [] : undefined } i18nKey={text} @@ -93,4 +74,4 @@ const EarnGlmLockNotification: FC = ({ ); }; -export default EarnGlmLockNotification; +export default LockGlmNotification; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/index.tsx new file mode 100644 index 0000000000..c72b658630 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './LockGlmNotification'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/types.ts new file mode 100644 index 0000000000..bd4f878d22 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotification/types.ts @@ -0,0 +1,9 @@ +import { CurrentMode } from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; + +export default interface LockGlmNotificationProps { + className?: string; + currentMode: CurrentMode; + isLockingApproved: boolean; + transactionHash?: string; + type: 'success' | 'info'; +} diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.module.scss new file mode 100644 index 0000000000..c430af7485 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.module.scss @@ -0,0 +1,10 @@ +.root { + font-size: $font-size-12; + min-height: 2rem; + font-weight: $font-weight-semibold; + + &:hover { + cursor: pointer; + transform: none; + } +} diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.tsx new file mode 100644 index 0000000000..752f2b2ab3 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/LockGlmNotificationLinkButton.tsx @@ -0,0 +1,26 @@ +import React, { FC, memo } from 'react'; + +import Button from 'components/ui/Button'; +import Svg from 'components/ui/Svg'; +import networkConfig from 'constants/networkConfig'; +import { arrowTopRight } from 'svg/misc'; + +import styles from './LockGlmNotificationLinkButton.module.scss'; +import LockGlmNotificationLinkButtonProps from './types'; + +const LockGlmNotificationLinkButton: FC = ({ + children, + transactionHash, +}) => { + return ( + + ); +}; + +export default memo(LockGlmNotificationLinkButton); diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/index.tsx new file mode 100644 index 0000000000..8941cd49be --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './LockGlmNotificationLinkButton'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/types.ts new file mode 100644 index 0000000000..c96ea3baec --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmNotificationLinkButton/types.ts @@ -0,0 +1,6 @@ +import { ReactNode } from 'react'; + +export default interface LockGlmNotificationLinkButtonProps { + children?: ReactNode; + transactionHash: string; +} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/EarnGlmLockStepper.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/LockGlmStepper.tsx similarity index 76% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/EarnGlmLockStepper.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/LockGlmStepper.tsx index e2c30eb9dd..8efd0b967b 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/EarnGlmLockStepper.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/LockGlmStepper.tsx @@ -2,13 +2,13 @@ import { useFormikContext } from 'formik'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import { FormFields } from 'components/Earn/EarnGlmLock/types'; +import { FormFields } from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; import BoxRounded from 'components/ui/BoxRounded'; import ProgressStepper from 'components/ui/ProgressStepper'; -import EarnGlmLockStepperProps from './types'; +import LockGlmStepperProps from './types'; -const EarnGlmLockStepper: FC = ({ currentMode, step, className }) => { +const LockGlmStepper: FC = ({ currentMode, step, className }) => { const { t, i18n } = useTranslation('translation', { keyPrefix: 'components.dedicated.glmLock', }); @@ -29,4 +29,4 @@ const EarnGlmLockStepper: FC = ({ currentMode, step, cl ); }; -export default EarnGlmLockStepper; +export default LockGlmStepper; diff --git a/client/src/components/Earn/EarnBoxGlmLock/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/index.tsx similarity index 53% rename from client/src/components/Earn/EarnBoxGlmLock/index.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/index.tsx index ced474dc20..c34128c49f 100644 --- a/client/src/components/Earn/EarnBoxGlmLock/index.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnBoxGlmLock'; +export { default } from './LockGlmStepper'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/types.ts new file mode 100644 index 0000000000..6c5ded6a40 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmStepper/types.ts @@ -0,0 +1,7 @@ +import { CurrentMode } from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; + +export default interface LockGlmStepperProps { + className?: string; + currentMode: CurrentMode; + step: 1 | 2 | 3; +} diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/EarnGlmLockTabs.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/LockGlmTabs.module.scss similarity index 100% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/EarnGlmLockTabs.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/LockGlmTabs.module.scss diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/EarnGlmLockTabs.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/LockGlmTabs.tsx similarity index 92% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/EarnGlmLockTabs.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/LockGlmTabs.tsx index 02b418d638..dc6615d128 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/EarnGlmLockTabs.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/LockGlmTabs.tsx @@ -3,8 +3,8 @@ import { useFormikContext } from 'formik'; import React, { FC, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; -import EarnGlmLockTabsInputs from 'components/Earn/EarnGlmLock/EarnGlmLockTabsInputs'; -import { FormFields } from 'components/Earn/EarnGlmLock/types'; +import { FormFields } from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; +import LockGlmTabsInputs from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs'; import BoxRounded from 'components/ui/BoxRounded'; import Button from 'components/ui/Button'; import ButtonProps from 'components/ui/Button/types'; @@ -14,10 +14,10 @@ import { formatUnitsBigInt } from 'utils/formatUnitsBigInt'; import getFormattedGlmValue from 'utils/getFormattedGlmValue'; import { parseUnitsBigInt } from 'utils/parseUnitsBigInt'; -import styles from './EarnGlmLockTabs.module.scss'; -import EarnGlmLockTabsProps from './types'; +import styles from './LockGlmTabs.module.scss'; +import LockGlmTabsProps from './types'; -const EarnGlmLockTabs: FC = ({ +const LockGlmTabs: FC = ({ buttonUseMaxRef, className, currentMode, @@ -88,7 +88,7 @@ const EarnGlmLockTabs: FC = ({ return ( = ({ > {t('glmLockTabs.useMax')} - = ({ ); }; -export default EarnGlmLockTabs; +export default LockGlmTabs; diff --git a/client/src/components/Earn/EarnTipTiles/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/index.tsx similarity index 54% rename from client/src/components/Earn/EarnTipTiles/index.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/index.tsx index 6211f7158a..62e75a195e 100644 --- a/client/src/components/Earn/EarnTipTiles/index.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnTipTiles'; +export { default } from './LockGlmTabs'; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/types.ts similarity index 72% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/types.ts rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/types.ts index 988071327b..3f1f959ff4 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/types.ts +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabs/types.ts @@ -1,9 +1,13 @@ import { FormikHelpers } from 'formik'; import { RefObject } from 'react'; -import { FormFields, CurrentMode, OnReset } from 'components/Earn/EarnGlmLock/types'; +import { + FormFields, + CurrentMode, + OnReset, +} from 'components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlm/types'; -export default interface EarnGlmLockTabsProps { +export default interface LockGlmTabsProps { buttonUseMaxRef: RefObject; className?: string; currentMode: CurrentMode; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/EarnGlmLockTabsInputs.module.scss b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs/LockGlmTabsInputs.module.scss similarity index 100% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/EarnGlmLockTabsInputs.module.scss rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs/LockGlmTabsInputs.module.scss diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/EarnGlmLockTabsInputs.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs/LockGlmTabsInputs.tsx similarity index 95% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/EarnGlmLockTabsInputs.tsx rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs/LockGlmTabsInputs.tsx index b0824eb32f..0adace6884 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabsInputs/EarnGlmLockTabsInputs.tsx +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/LockGlmTabsInputs/LockGlmTabsInputs.tsx @@ -10,10 +10,10 @@ import { floatNumberWithUpTo2DecimalPlaces, } from 'utils/regExp'; -import styles from './EarnGlmLockTabsInputs.module.scss'; -import EarnGlmLockTabsInputsProps from './types'; +import styles from './LockGlmTabsInputs.module.scss'; +import LockGlmTabsInputsProps from './types'; -const EarnGlmLockTabsInputs = forwardRef( +const LockGlmTabsInputs = forwardRef( ( { areInputsDisabled, @@ -146,4 +146,4 @@ const EarnGlmLockTabsInputs = forwardRef = ({ modalProps }) => { +const ModalLockGlm: FC = ({ modalProps }) => { const { t, i18n } = useTranslation('translation', { - keyPrefix: 'components.dedicated.modalGlmLock', + keyPrefix: 'components.home.homeGridCurrentGlmLock.modalLockGlm', }); const [currentMode, setCurrentMode] = useState('lock'); @@ -17,7 +17,7 @@ const ModalEarnGlmLock: FC = ({ modalProps }) => { return ( - = ({ modalProps }) => { ); }; -export default ModalEarnGlmLock; +export default ModalLockGlm; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/index.tsx new file mode 100644 index 0000000000..fafd32c588 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ModalLockGlm'; diff --git a/client/src/components/Earn/ModalEarnGlmLock/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/types.ts similarity index 69% rename from client/src/components/Earn/ModalEarnGlmLock/types.ts rename to client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/types.ts index a282b96bad..b30ac68564 100644 --- a/client/src/components/Earn/ModalEarnGlmLock/types.ts +++ b/client/src/components/Home/HomeGridCurrentGlmLock/ModalLockGlm/types.ts @@ -1,5 +1,5 @@ import ModalProps from 'components/ui/Modal/types'; -export default interface ModalEarnGlmLockProps { +export default interface ModalLockGlmProps { modalProps: Omit; } diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/index.tsx b/client/src/components/Home/HomeGridCurrentGlmLock/index.tsx new file mode 100644 index 0000000000..6a972ebcc8 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './HomeGridCurrentGlmLock'; diff --git a/client/src/components/Home/HomeGridCurrentGlmLock/types.ts b/client/src/components/Home/HomeGridCurrentGlmLock/types.ts new file mode 100644 index 0000000000..da4457ce45 --- /dev/null +++ b/client/src/components/Home/HomeGridCurrentGlmLock/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridCurrentGlmLockProps { + className?: string; +} diff --git a/client/src/components/Home/HomeGridDivider/HomeGridDivider.module.scss b/client/src/components/Home/HomeGridDivider/HomeGridDivider.module.scss new file mode 100644 index 0000000000..568cbd4dbf --- /dev/null +++ b/client/src/components/Home/HomeGridDivider/HomeGridDivider.module.scss @@ -0,0 +1,19 @@ +.root { + height: 0.1rem; + background: $color-octant-grey1; + width: 100%; + margin: 4rem 0; + grid-column: span 1; + + @media #{$tablet-up} { + grid-column: span 2; + } + + @media #{$desktop-up} { + grid-column: span 3; + } + + @media #{$large-desktop-up} { + grid-column: span 4; + } +} diff --git a/client/src/components/Home/HomeGridDivider/HomeGridDivider.tsx b/client/src/components/Home/HomeGridDivider/HomeGridDivider.tsx new file mode 100644 index 0000000000..0913072efb --- /dev/null +++ b/client/src/components/Home/HomeGridDivider/HomeGridDivider.tsx @@ -0,0 +1,11 @@ +import cx from 'classnames'; +import React, { FC, memo } from 'react'; + +import styles from './HomeGridDivider.module.scss'; +import HomeGridDividerProps from './types'; + +const HomeGridDivider: FC = ({ className }) => { + return
; +}; + +export default memo(HomeGridDivider); diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/index.tsx b/client/src/components/Home/HomeGridDivider/index.tsx similarity index 52% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/index.tsx rename to client/src/components/Home/HomeGridDivider/index.tsx index 3df89b0dc9..717c940407 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockTabs/index.tsx +++ b/client/src/components/Home/HomeGridDivider/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLockTabs'; +export { default } from './HomeGridDivider'; diff --git a/client/src/components/Home/HomeGridDivider/types.ts b/client/src/components/Home/HomeGridDivider/types.ts new file mode 100644 index 0000000000..63e61cdd25 --- /dev/null +++ b/client/src/components/Home/HomeGridDivider/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridDividerProps { + className?: string; +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.module.scss b/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.module.scss new file mode 100644 index 0000000000..bdca81f093 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.module.scss @@ -0,0 +1,9 @@ +.root { + position: relative; + + .donationsList { + max-height: 100%; + overflow: auto; + margin-right: 0.8rem; + } +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.tsx b/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.tsx new file mode 100644 index 0000000000..f4a7917890 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsList/DonationsList.tsx @@ -0,0 +1,50 @@ +import React, { FC } from 'react'; + +import DonationsListItem from 'components/Home/HomeGridDonations/DonationsListItem'; +import DonationsListSkeletonItem from 'components/Home/HomeGridDonations/DonationsListSkeletonItem'; +import useGetValuesToDisplay from 'hooks/helpers/useGetValuesToDisplay'; + +import styles from './DonationsList.module.scss'; +import DonationsListProps from './types'; + +const DonationsList: FC = ({ + donations, + isLoading, + numberOfSkeletons, + dataTest = 'DonationsList', +}) => { + const getValuesToDisplay = useGetValuesToDisplay(); + + return ( +
+
+ {isLoading + ? Array.from(Array(numberOfSkeletons)).map((_, idx) => ( + // eslint-disable-next-line react/no-array-index-key + + )) + : donations.map(donation => ( + + ))} +
+
+ ); +}; + +export default DonationsList; diff --git a/client/src/components/Home/HomeGridDonations/DonationsList/index.tsx b/client/src/components/Home/HomeGridDonations/DonationsList/index.tsx new file mode 100644 index 0000000000..66ce92c89e --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsList/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './DonationsList'; diff --git a/client/src/components/Home/HomeGridDonations/DonationsList/types.ts b/client/src/components/Home/HomeGridDonations/DonationsList/types.ts new file mode 100644 index 0000000000..1a0f5fc634 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsList/types.ts @@ -0,0 +1,8 @@ +import { ResponseItem } from 'hooks/helpers/useUserAllocationsAllEpochs'; + +export default interface DonationsListProps { + dataTest?: string; + donations: ResponseItem['elements']; + isLoading: boolean; + numberOfSkeletons: number; +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.module.scss b/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.module.scss new file mode 100644 index 0000000000..dbc6f9f717 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.module.scss @@ -0,0 +1,37 @@ +.root { + display: flex; + align-items: center; + justify-content: space-between; + height: 5.6rem; + + &:not(:last-child) { + border-bottom: 0.1rem solid $color-octant-grey3; + } + + .image { + height: 2.4rem; + width: 2.4rem; + border-radius: 100%; + margin-right: 1.6rem; + } + + .name, + .value { + font-size: $font-size-12; + font-weight: $font-weight-bold; + color: $color-octant-dark; + } + + .name { + width: 100%; + text-align: left; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; + } + + .value { + margin-left: 1rem; + flex-shrink: 0; + } +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.tsx b/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.tsx new file mode 100644 index 0000000000..0a48c005d7 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListItem/DonationsListItem.tsx @@ -0,0 +1,39 @@ +import React, { FC, memo } from 'react'; + +import Img from 'components/ui/Img/Img'; +import env from 'env'; +import useProjectsIpfs from 'hooks/queries/useProjectsIpfs'; + +import styles from './DonationsListItem.module.scss'; +import DonationsListItemProps from './types'; + +const DonationsListItem: FC = ({ + address, + epoch, + value, + dataTest = 'DonationsListItem', +}) => { + const { ipfsGateways } = env; + const { data: projectsIpfs } = useProjectsIpfs([address], epoch); + + const image = projectsIpfs.at(0)?.profileImageSmall; + const name = projectsIpfs.at(0)?.name; + + return ( +
+ project logo `${element}${image}`)} + /> +
+ {name} +
+
+ {value} +
+
+ ); +}; + +export default memo(DonationsListItem); diff --git a/client/src/components/Home/HomeGridDonations/DonationsListItem/index.tsx b/client/src/components/Home/HomeGridDonations/DonationsListItem/index.tsx new file mode 100644 index 0000000000..7ec3ac8698 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListItem/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './DonationsListItem'; diff --git a/client/src/components/Home/HomeGridDonations/DonationsListItem/types.ts b/client/src/components/Home/HomeGridDonations/DonationsListItem/types.ts new file mode 100644 index 0000000000..e346209a81 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListItem/types.ts @@ -0,0 +1,6 @@ +export default interface DonationsListItemProps { + address: string; + dataTest?: string; + epoch?: number; + value: string; +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.module.scss b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.module.scss new file mode 100644 index 0000000000..be193215bf --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.module.scss @@ -0,0 +1,31 @@ +.root { + display: flex; + align-items: center; + justify-content: space-between; + height: 5.6rem; + + &:not(:last-child) { + border-bottom: 0.1rem solid $color-octant-grey3; + } + + .image { + @include skeleton(); + height: 2.4rem; + width: 2.4rem; + border-radius: 100%; + margin-right: 1.6rem; + } + + .name { + @include skeleton(); + height: 1.5rem; + width: 18rem; + } + + .value { + @include skeleton(); + height: 1.5rem; + width: 4rem; + margin-left: auto; + } +} diff --git a/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.tsx b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.tsx new file mode 100644 index 0000000000..bb623e4ffd --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/DonationsListSkeletonItem.tsx @@ -0,0 +1,15 @@ +import React, { ReactElement, memo } from 'react'; + +import styles from './DonationsListSkeletonItem.module.scss'; + +const DonationsListSkeletonItem = (): ReactElement => { + return ( +
+
+
+
+
+ ); +}; + +export default memo(DonationsListSkeletonItem); diff --git a/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/index.tsx b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/index.tsx new file mode 100644 index 0000000000..e6039ffa45 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/DonationsListSkeletonItem/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './DonationsListSkeletonItem'; diff --git a/client/src/components/Home/HomeGridDonations/HomeGridDonations.module.scss b/client/src/components/Home/HomeGridDonations/HomeGridDonations.module.scss new file mode 100644 index 0000000000..3ad82331e5 --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/HomeGridDonations.module.scss @@ -0,0 +1,51 @@ +.root { + padding: 0rem 2.4rem 2.4rem; + overflow: auto; +} + +.titleWrapper { + display: flex; + + .numberOfAllocations { + display: flex; + align-items: center; + justify-content: center; + color: $color-white; + background-color: $color-octant-dark; + font-size: $font-size-08; + font-weight: $font-weight-superbold; + border-radius: 50%; + width: 1.6rem; + height: 1.6rem; + margin-left: 1rem; + } +} + +.noDonationsYet { + height: 100%; + width: 100%; + + .noDonationsYetImage { + width: 12.9rem; + margin-top: 0.6rem; + } + + .noDonationsYetLabel { + line-height: 2rem; + margin-top: 4.6rem; + font-size: $font-size-14; + font-weight: $font-weight-bold; + color: $color-octant-grey5; + } +} + +.editButton { + margin-left: auto; + width: 6rem; + padding: 0; + min-height: 3.2rem; +} + +.projectsLink { + font-size: $font-size-14; +} diff --git a/client/src/components/Home/HomeGridDonations/HomeGridDonations.tsx b/client/src/components/Home/HomeGridDonations/HomeGridDonations.tsx new file mode 100644 index 0000000000..4eefa3406f --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/HomeGridDonations.tsx @@ -0,0 +1,112 @@ +import React, { FC } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { useAccount } from 'wagmi'; + +import DonationsList from 'components/Home/HomeGridDonations/DonationsList'; +import GridTile from 'components/shared/Grid/GridTile'; +import Button from 'components/ui/Button'; +import Img from 'components/ui/Img'; +import useUserAllocationsAllEpochs from 'hooks/helpers/useUserAllocationsAllEpochs'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useUserAllocations from 'hooks/queries/useUserAllocations'; +import { ROOT_ROUTES } from 'routes/RootRoutes/routes'; +import useAllocationsStore from 'store/allocations/store'; +import useLayoutStore from 'store/layout/store'; + +import styles from './HomeGridDonations.module.scss'; +import HomeGridDonationsProps from './types'; +import { getReducedUserAllocationsAllEpochs } from './utils'; + +const HomeGridDonations: FC = ({ className }) => { + const { i18n, t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridDonations', + }); + const { isConnected } = useAccount(); + const { data: userAllocationsAllEpochs, isFetching: isFetchingUserAllocationsAllEpochs } = + useUserAllocationsAllEpochs(); + const reducedUserAllocationsAllEpochs = + getReducedUserAllocationsAllEpochs(userAllocationsAllEpochs); + const { data: userAllocations, isFetching: isFetchingUserAllocations } = useUserAllocations(); + const { data: currentEpoch } = useCurrentEpoch(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const { isAllocationDrawerOpen, setIsAllocationDrawerOpen } = useLayoutStore(state => ({ + isAllocationDrawerOpen: state.data.isAllocationDrawerOpen, + setIsAllocationDrawerOpen: state.setIsAllocationDrawerOpen, + })); + + const { setCurrentView } = useAllocationsStore(state => ({ + setCurrentView: state.setCurrentView, + })); + + const areAllocationsEmpty = + !isConnected || + (isDecisionWindowOpen + ? !isFetchingUserAllocations && userAllocations?.elements.length === 0 + : !isFetchingUserAllocationsAllEpochs && reducedUserAllocationsAllEpochs?.length === 0); + + return ( + + {!isDecisionWindowOpen && !areAllocationsEmpty + ? t('donationHistory') + : i18n.t('common.donations')} + {isDecisionWindowOpen && userAllocations?.elements !== undefined && ( +
{userAllocations?.elements?.length}
+ )} +
+ } + titleSuffix={ + isDecisionWindowOpen ? ( + + ) : null + } + > +
+ {areAllocationsEmpty ? ( +
+ +
+ , + ]} + i18nKey={`components.home.homeGridDonations.${isDecisionWindowOpen ? 'noDonationsYetAWOpen' : 'noDonationsYet'}`} + /> +
+
+ ) : ( + ({ ...a, epoch: currentEpoch! - 1 })) + : reducedUserAllocationsAllEpochs + } + isLoading={ + isDecisionWindowOpen ? isFetchingUserAllocations : isFetchingUserAllocationsAllEpochs + } + numberOfSkeletons={4} + /> + )} +
+ + ); +}; + +export default HomeGridDonations; diff --git a/client/src/components/Home/HomeGridDonations/index.tsx b/client/src/components/Home/HomeGridDonations/index.tsx new file mode 100644 index 0000000000..0a66ad2aec --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './HomeGridDonations'; diff --git a/client/src/components/Home/HomeGridDonations/types.ts b/client/src/components/Home/HomeGridDonations/types.ts new file mode 100644 index 0000000000..bb192c9fba --- /dev/null +++ b/client/src/components/Home/HomeGridDonations/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridDonationsProps { + className?: string; +} diff --git a/client/src/components/Metrics/MetricsPersonal/MetricsPersonalGridAllocations/utils.ts b/client/src/components/Home/HomeGridDonations/utils.ts similarity index 100% rename from client/src/components/Metrics/MetricsPersonal/MetricsPersonalGridAllocations/utils.ts rename to client/src/components/Home/HomeGridDonations/utils.ts diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.module.scss b/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.module.scss new file mode 100644 index 0000000000..d0c86b894c --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.module.scss @@ -0,0 +1,40 @@ +.root { + display: flex; + flex-direction: column; + flex: 1; + user-select: none; + -webkit-user-select: none; + + .image { + height: 11.1rem; + margin: 0 auto 0; + opacity: 0.5; + } + + .graphContainer { + margin-bottom: 0.8rem; + flex: 1; + display: flex; + padding: 6rem 2rem 0; + margin: -3.2rem 0 0; + overflow-y: hidden; + overflow-x: auto; + + &.isLoading { + margin: 0; + padding-top: 2.4rem; + } + + .barsContainer { + display: flex; + min-width: 100%; + } + } + + .details { + width: 100%; + height: 3.2rem; + border-radius: $border-radius-08; + background-color: $color-octant-grey6; + } +} diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.tsx new file mode 100644 index 0000000000..2108f1d253 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResults/EpochResults.tsx @@ -0,0 +1,236 @@ +import cx from 'classnames'; +import { maxBy } from 'lodash'; +import React, { FC, useEffect, useRef, useState } from 'react'; + +import EpochResultsBar from 'components/Home/HomeGridEpochResults/EpochResultsBar'; +import EpochResultsDetails from 'components/Home/HomeGridEpochResults/EpochResultsDetails'; +import Img from 'components/ui/Img'; +import { EPOCH_RESULTS_BAR_ID } from 'constants/domElementsIds'; +import env from 'env'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import { ProjectIpfsWithRewards } from 'hooks/queries/useProjectsIpfsWithRewards'; + +import styles from './EpochResults.module.scss'; +import EpochResultsProps from './types'; + +const EpochResults: FC = ({ + projects, + isLoading, + epoch, + highlightedBarAddress, + setHighlightedBarAddress, +}) => { + const [startDraggingPageX, setStartDraggingPageX] = useState(null); + const [lastScrollLeft, setLastScrollLeft] = useState(0); + const [isScrollable, setIsScrollable] = useState(false); + const [isDragging, setIsDragging] = useState(false); + const [scrollDirection, setScrollDirection] = useState<'right' | 'left'>('right'); + const { ipfsGateways } = env; + const { isMobile, isTablet, isDesktop } = useMediaQuery(); + const graphContainerRef = useRef(null); + + const details = projects.find(d => d.address === highlightedBarAddress); + + const getMaxValue = (): bigint => { + const { matchedRewards, donations } = maxBy(projects, d => { + if (d.donations > d.matchedRewards) { + return d.donations; + } + return d.matchedRewards; + }) as ProjectIpfsWithRewards; + return matchedRewards > donations ? matchedRewards : donations; + }; + + const getBarHeightPercentage = (value: bigint) => { + const maxValue = getMaxValue(); + if (!maxValue || !value) { + return 0; + } + return (Number(value) / Number(maxValue)) * 100; + }; + + const onMouseDown = (pageX: number) => { + if (!isScrollable) { + return; + } + setStartDraggingPageX(pageX); + }; + + const onMouseMove = (pageX: number) => { + if (!isScrollable || startDraggingPageX === null || !graphContainerRef.current) { + return; + } + setIsDragging(true); + const el = graphContainerRef.current; + const maxScrollLeft = el.scrollWidth - el.clientWidth; + + const difference = pageX + lastScrollLeft - startDraggingPageX; + + if (difference > 0 && lastScrollLeft <= maxScrollLeft) { + el.scroll({ left: difference }); + } + + setScrollDirection(el.scrollLeft === maxScrollLeft ? 'left' : 'right'); + }; + + useEffect(() => { + if (isLoading || !graphContainerRef.current) { + return; + } + const el = graphContainerRef.current; + const isScrollableNext = el.scrollWidth > el.clientWidth; + setIsScrollable(isScrollableNext); + + if (!isScrollableNext) { + return; + } + const mouseUpTouchEndListener = () => { + setIsDragging(false); + setStartDraggingPageX(null); + setLastScrollLeft(graphContainerRef.current?.scrollLeft || 0); + }; + + document.addEventListener('mouseup', mouseUpTouchEndListener); + document.addEventListener('touchend', mouseUpTouchEndListener); + + return () => { + document.addEventListener('mouseup', mouseUpTouchEndListener); + document.removeEventListener('touchend', mouseUpTouchEndListener); + }; + }, [isLoading, isMobile, isTablet, isDesktop]); + + useEffect(() => { + if (isLoading && projects.length) { + return; + } + + const clickListener = e => { + if ( + e.target?.id === EPOCH_RESULTS_BAR_ID || + e.target.parentElement?.id === EPOCH_RESULTS_BAR_ID || + !graphContainerRef.current || + highlightedBarAddress === null || + !isScrollable + ) { + return; + } + + if (isMobile) { + if (e.detail === 2) { + setHighlightedBarAddress(null); + return; + } + + const { left, right } = graphContainerRef.current.getBoundingClientRect(); + const highlightedProjectIdx = projects.findIndex( + project => project.address === highlightedBarAddress, + ); + + if (e.pageX < left && highlightedProjectIdx > 0) { + setHighlightedBarAddress(projects[highlightedProjectIdx - 1].address); + return; + } + + if (e.pageX > right && highlightedProjectIdx < projects.length - 1) { + setHighlightedBarAddress(projects[highlightedProjectIdx + 1].address); + } + return; + } + + setHighlightedBarAddress(null); + }; + + document.addEventListener('click', clickListener); + + return () => { + document.removeEventListener('click', clickListener); + }; + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading, isMobile, highlightedBarAddress, isScrollable]); + + useEffect(() => { + if (isLoading || !projects.length || !graphContainerRef.current) { + return; + } + + const { width } = graphContainerRef.current.getBoundingClientRect(); + + const barWidth = isDesktop ? 16 : 8; + const barWidthWithMarginLeft = barWidth + (isDesktop ? 18 : 14); + const numberOfVisibleBars = Math.floor((width - barWidth) / barWidthWithMarginLeft); + + const idxOfActiveBarToHighlight = Math.round(numberOfVisibleBars / 2) - 1; + + const projectAddressToHighlight = + projects[ + idxOfActiveBarToHighlight > projects.length - 1 + ? projects.length - 1 + : idxOfActiveBarToHighlight + ]?.address; + + setHighlightedBarAddress(projectAddressToHighlight); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + return ( +
+
setHighlightedBarAddress(null)} + onMouseDown={e => onMouseDown(e.pageX)} + onMouseMove={e => onMouseMove(e.pageX)} + onScroll={() => { + if (!isScrollable || !graphContainerRef.current || isDragging || isMobile) { + return; + } + const el = graphContainerRef.current; + const maxScrollLeft = el.scrollWidth - el.clientWidth; + setLastScrollLeft(el.scrollLeft); + setScrollDirection(el.scrollLeft === maxScrollLeft ? 'left' : 'right'); + }} + onTouchMove={e => { + if (!isScrollable) { + return; + } + e.stopPropagation(); + setIsDragging(true); + }} + > + {isLoading ? ( + + ) : ( +
+ {projects.map(({ address, matchedRewards, donations, profileImageSmall }) => ( + `${element}${profileImageSmall}`)} + isDragging={isDragging} + isHighlighted={!!(highlightedBarAddress && highlightedBarAddress === address)} + setHighlightedBarAddress={setHighlightedBarAddress} + topBarHeightPercentage={getBarHeightPercentage(matchedRewards)} + /> + ))} +
+ )} +
+ +
+ ); +}; + +export default EpochResults; diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResults/index.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResults/index.tsx new file mode 100644 index 0000000000..f409db3473 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResults/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './EpochResults'; diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResults/types.ts b/client/src/components/Home/HomeGridEpochResults/EpochResults/types.ts new file mode 100644 index 0000000000..85eed6f7f4 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResults/types.ts @@ -0,0 +1,10 @@ +import { ProjectIpfsWithRewards } from 'hooks/queries/useProjectsIpfsWithRewards'; + +export default interface EpochResultsProps { + className?: string; + epoch: number; + highlightedBarAddress: null | string; + isLoading?: boolean; + projects: ProjectIpfsWithRewards[]; + setHighlightedBarAddress: (address: null | string) => void; +} diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.module.scss b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.module.scss new file mode 100644 index 0000000000..67b91cf854 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.module.scss @@ -0,0 +1,75 @@ +.root { + height: 100%; + min-width: 0.8rem; + position: relative; + flex-grow: 1; + + &.hasValue { + cursor: pointer; + } + + &:not(:first-child) { + margin-left: 1.4rem; + } + + @media #{$desktop-up} { + width: 1.6rem; + &:not(:first-child) { + margin-left: 1.8rem; + } + } + + @media #{$large-desktop-up} { + width: 0.8rem; + } + + .topBar { + background-color: $color-octant-orange2; + border-radius: $border-radius-08 $border-radius-08 0 0; + bottom: 0; + left: 0; + width: 100%; + position: absolute; + min-height: 6%; + } + + .bottomBar { + background-color: $color-octant-green; + border-radius: $border-radius-08 $border-radius-08 0 0; + bottom: 0; + left: 0; + width: 100%; + position: absolute; + z-index: $z-index-2; + min-height: 2%; + } + + .projectLogo { + position: absolute; + right: 50%; + transform: translate(50%); + display: flex; + align-items: center; + justify-content: center; + height: 3.2rem; + width: 3.2rem; + border-radius: 100%; + background-color: $color-white; + box-shadow: 0 0 2.5rem 0 $color-black-20; + z-index: $z-index-3; + + .projectLogoImg { + height: 2.4rem; + width: 2.4rem; + border-radius: 100%; + } + + .triangle { + position: absolute; + bottom: -0.7rem; + border-top: 0.8rem solid $color-white; + border-left: 0.4rem solid transparent; + border-right: 0.4rem solid transparent; + } + } +} diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.tsx new file mode 100644 index 0000000000..8152ea0c2a --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/EpochResultsBar.tsx @@ -0,0 +1,104 @@ +import cx from 'classnames'; +import { animate, AnimatePresence, motion, useInView } from 'framer-motion'; +import React, { FC, useEffect, useRef } from 'react'; + +import Img from 'components/ui/Img'; +import { EPOCH_RESULTS_BAR_ID } from 'constants/domElementsIds'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import { ROOT_ROUTES } from 'routes/RootRoutes/routes'; + +import styles from './EpochResultsBar.module.scss'; +import EpochResultsBarProps from './types'; + +const EpochResultsBar: FC = ({ + address, + topBarHeightPercentage, + bottomBarHeightPercentage, + setHighlightedBarAddress, + isHighlighted, + imageSources, + epoch, + isDragging, +}) => { + const topBarRef = useRef(null); + const bottomBarRef = useRef(null); + const ref = useRef(null); + const { isMobile } = useMediaQuery(); + + const isInView = useInView(ref); + + useEffect(() => { + if (!isInView) { + return; + } + const a = animate(topBarRef.current, { height: `${topBarHeightPercentage}%` }); + return () => { + a.cancel(); + }; + }, [topBarHeightPercentage, isInView]); + + useEffect(() => { + if (!isInView) { + return; + } + + const a = animate(bottomBarRef.current, { height: `${bottomBarHeightPercentage}%` }); + return () => { + a.cancel(); + }; + }, [bottomBarHeightPercentage, isInView]); + + return ( + { + e.stopPropagation(); + if (isDragging) { + return; + } + if (isMobile) { + setHighlightedBarAddress(address); + return; + } + window.open(`${ROOT_ROUTES.project.absolute}/${epoch}/${address}`); + }} + onMouseOver={() => { + if (isDragging || isMobile) { + return; + } + setHighlightedBarAddress(address); + }} + > + + {isHighlighted && ( + bottomBarHeightPercentage ? topBarHeightPercentage : bottomBarHeightPercentage}% + 1rem)`, + opacity: 0, + scale: 0.5, + x: '50%', + }} + > + +
+ + )} + + + + + ); +}; + +export default EpochResultsBar; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItem/index.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/index.tsx similarity index 52% rename from client/src/components/Earn/EarnHistory/EarnHistoryItem/index.tsx rename to client/src/components/Home/HomeGridEpochResults/EpochResultsBar/index.tsx index fa37985d9d..829caeb836 100644 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItem/index.tsx +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnHistoryItem'; +export { default } from './EpochResultsBar'; diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/types.ts b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/types.ts new file mode 100644 index 0000000000..0b7e32e8e6 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsBar/types.ts @@ -0,0 +1,10 @@ +export default interface EpochResultsBarProps { + address: string; + bottomBarHeightPercentage: number; + epoch: number; + imageSources: string[]; + isDragging: boolean; + isHighlighted: boolean; + setHighlightedBarAddress: (address: string | null) => void; + topBarHeightPercentage: number; +} diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.module.scss b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.module.scss new file mode 100644 index 0000000000..487703f0d2 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.module.scss @@ -0,0 +1,79 @@ +.root { + display: flex; + font-size: $font-size-12; + height: 3.2rem; + align-items: center; + padding: 0 0.8rem 0rem 1.6rem; + background-color: $color-octant-grey6; + border-radius: $border-radius-08; + + .scrollInfo { + width: 100%; + display: flex; + align-items: center; + justify-content: center; + color: $color-octant-grey5; + font-weight: $font-weight-semibold; + font-size: $font-size-12; + + .scrollInfoText { + margin: 0 0.2rem; + } + } + + .loading { + color: $color-octant-grey5; + font-weight: $font-weight-bold; + } + + .projectName { + color: $color-octant-dark; + font-weight: $font-weight-bold; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + text-align: left; + margin-right: auto; + } + + .donations { + color: $color-octant-green; + } + + .matching { + color: $color-octant-orange2; + } + + .total { + color: $color-octant-dark; + } + + .donations, + .matching, + .total { + white-space: nowrap; + font-weight: $font-weight-semibold; + margin-left: 0.8rem; + + @media #{$tablet-up} { + margin-left: 1.6rem; + } + } + + .link { + white-space: nowrap; + color: $color-octant-grey5; + font-weight: $font-weight-semibold; + padding: 0 1.6rem; + + path { + stroke: $color-octant-grey5; + } + + &:hover { + path { + stroke: $color-octant-grey5; + } + } + } +} diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.tsx new file mode 100644 index 0000000000..3192b3b334 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/EpochResultsDetails.tsx @@ -0,0 +1,205 @@ +/* eslint-disable jsx-a11y/mouse-events-have-key-events */ +import React, { FC, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Button from 'components/ui/Button'; +import useGetValuesToDisplay, { + GetValuesToDisplayProps, +} from 'hooks/helpers/useGetValuesToDisplay'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; + +import styles from './EpochResultsDetails.module.scss'; +import EpochResultsDetailsProps from './types'; + +const EpochResultsDetails: FC = ({ + details, + isLoading, + isScrollable, + scrollDirection, + isDragging, + onMouseMove, + onMouseDown, +}) => { + const { i18n, t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridEpochResults', + }); + const ref = useRef(null); + const { isMobile } = useMediaQuery(); + const getValuesToDisplay = useGetValuesToDisplay(); + const [dots, setDots] = useState(0); + const [showScrollInfo, setShowScrollInfo] = useState(false); + + const getValuesToDisplayCommonProps: GetValuesToDisplayProps = { + cryptoCurrency: 'ethereum', + getFormattedEthValueProps: { + maxNumberOfDigitsToShow: 5, + shouldIgnoreGwei: true, + shouldIgnoreWei: true, + showShortFormat: isMobile, + }, + showCryptoSuffix: true, + }; + + const donationsToDisplay = details + ? getValuesToDisplay({ + ...getValuesToDisplayCommonProps, + valueCrypto: details.donations, + }).primary + : null; + + const matchingToDisplay = details + ? getValuesToDisplay({ + ...getValuesToDisplayCommonProps, + valueCrypto: details.matchedRewards, + }).primary + : null; + + const totalToDisplay = details + ? getValuesToDisplay({ + ...getValuesToDisplayCommonProps, + valueCrypto: details.totalValueOfAllocations, + }).primary + : null; + + const isScrollInfoVisible = isDragging || showScrollInfo; + + useEffect(() => { + if (!isLoading) { + return; + } + const id = setInterval( + () => + setDots(prev => { + if (prev === 3) { + return 0; + } + return prev + 1; + }), + 300, + ); + return () => { + clearInterval(id); + setDots(0); + }; + }, [isLoading]); + + useEffect(() => { + if (!isMobile) { + return; + } + + const listener = e => { + if (ref.current && ref.current.contains(e.target)) { + return; + } + setShowScrollInfo(false); + }; + + document.addEventListener('click', listener); + + return () => document.removeEventListener('click', listener); + }, [isMobile]); + + return ( +
{ + if (!isScrollable) { + return; + } + onMouseDown(e.pageX); + }} + onMouseLeave={e => { + e.stopPropagation(); + if (!isScrollable) { + return; + } + setShowScrollInfo(false); + }} + onMouseMove={e => { + if (!isScrollable) { + return; + } + onMouseMove(e.pageX); + }} + onMouseOver={e => { + e.stopPropagation(); + if (!isScrollable) { + return; + } + setShowScrollInfo(true); + }} + onTouchEnd={() => { + if (!isScrollable) { + return; + } + setShowScrollInfo(false); + }} + onTouchMove={e => { + if (!isScrollable) { + return; + } + e.stopPropagation(); + onMouseMove(e.changedTouches[0].pageX); + }} + onTouchStart={e => { + if (!isScrollable) { + return; + } + setShowScrollInfo(true); + onMouseDown(e.changedTouches[0].pageX); + }} + > + {isLoading && ( +
+ {t('loadingChartData')} + {[...Array(dots).keys()].map(key => ( + . + ))} +
+ )} + {isScrollInfoVisible && ( +
+ {scrollDirection === 'left' && '←'} + + {isMobile ? t('scrollInfoMobile') : t('scrollInfo')} + + {scrollDirection === 'right' && '→'} +
+ )} + {!isScrollInfoVisible && details && ( + <> +
{details.name}
+
+ {isMobile ? t('donationsShort') : i18n.t('common.donations')} + {isMobile ? '' : ' '} + {donationsToDisplay} +
+
+ {isMobile ? t('matchingShort') : i18n.t('common.matching')} + {isMobile ? '' : ' '} + {matchingToDisplay} +
+
+ {isMobile ? t('totalShort') : i18n.t('common.total')} + {isMobile ? '' : ' '} + {totalToDisplay} +
+ {!isMobile && ( + + )} + + )} +
+ ); +}; + +export default EpochResultsDetails; diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/index.tsx b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/index.tsx new file mode 100644 index 0000000000..77c87aedaa --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './EpochResultsDetails'; diff --git a/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/types.ts b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/types.ts new file mode 100644 index 0000000000..9771c486b1 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/EpochResultsDetails/types.ts @@ -0,0 +1,12 @@ +import { ProjectIpfsWithRewards } from 'hooks/queries/useProjectsIpfsWithRewards'; + +export default interface EpochResultsDetailsProps { + details?: ProjectIpfsWithRewards; + epoch: number; + isDragging: boolean; + isLoading?: boolean; + isScrollable: boolean; + onMouseDown: (pageX: number) => void; + onMouseMove: (pageX: number) => void; + scrollDirection: 'right' | 'left'; +} diff --git a/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.module.scss b/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.module.scss new file mode 100644 index 0000000000..44b30327bd --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.module.scss @@ -0,0 +1,46 @@ +.root { + padding: 0 0.8rem 0.8rem; + display: flex; + flex-direction: column; + flex: 1; +} + +.arrowsWrapper { + user-select: none; + display: flex; + margin-left: auto; + + .arrow { + cursor: pointer; + width: 3.2rem; + height: 3.2rem; + background: $color-octant-grey8; + border-radius: $border-radius-10; + transition: all $transition-time-5; + display: flex; + align-items: center; + justify-content: center; + + &.leftArrow { + svg { + transform: rotate(180deg); + } + } + + &.isDisabled { + background: $color-octant-grey6; + + svg path { + fill: $color-octant-grey5; + } + } + + &:not(.isDisabled):hover { + background: $color-octant-grey1; + } + + &:first-child { + margin-right: 1.6rem; + } + } +} diff --git a/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.tsx b/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.tsx new file mode 100644 index 0000000000..3e27b8fa86 --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/HomeGridEpochResults.tsx @@ -0,0 +1,78 @@ +import React, { FC, useEffect, useRef, useState } from 'react'; +import { useTranslation } from 'react-i18next'; + +import GridTile from 'components/shared/Grid/GridTile'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useProjectsIpfsWithRewards, { + ProjectIpfsWithRewards, +} from 'hooks/queries/useProjectsIpfsWithRewards'; + +import EpochResults from './EpochResults'; +import styles from './HomeGridEpochResults.module.scss'; +import HomeGridEpochResultsProps from './types'; + +const HomeGridEpochResults: FC = ({ className }) => { + const initalLoadingRef = useRef(true); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const { data: currentEpoch } = useCurrentEpoch(); + const [epoch, setEpoch] = useState(currentEpoch! - 1); + const [highlightedBarAddress, setHighlightedBarAddress] = useState(null); + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridEpochResults', + }); + const { data: projectsIpfsWithRewards, isFetching: isFetchingProjectsIpfsWithRewards } = + useProjectsIpfsWithRewards( + isDecisionWindowOpen && epoch === currentEpoch! - 1 ? undefined : epoch, + ); + const projects = projectsIpfsWithRewards.reduce((acc, curr, idx) => { + if (!curr.totalValueOfAllocations) { + return acc; + } + acc[idx % 2 === 0 ? 'push' : 'unshift'](curr); + return acc; + }, [] as ProjectIpfsWithRewards[]); + + const isAnyProjectDonated = projects.some(({ donations }) => donations > 0n); + + const isLoading = isFetchingProjectsIpfsWithRewards && !isAnyProjectDonated; + + useEffect(() => { + if (!isDecisionWindowOpen || isLoading || epoch !== currentEpoch! - 1 || isAnyProjectDonated) { + return; + } + + setEpoch(prev => prev - 1); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isLoading]); + + useEffect(() => { + if ((initalLoadingRef.current && isLoading) || !initalLoadingRef.current) { + return; + } + + initalLoadingRef.current = false; + }, [isLoading]); + + return ( + setHighlightedBarAddress(null)} + title={t(isDecisionWindowOpen && epoch === currentEpoch! - 1 ? 'epochLive' : 'epochResults', { + epoch, + })} + > +
+ +
+
+ ); +}; + +export default HomeGridEpochResults; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/index.tsx b/client/src/components/Home/HomeGridEpochResults/index.tsx similarity index 50% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/index.tsx rename to client/src/components/Home/HomeGridEpochResults/index.tsx index 07c38db419..ef38dc65b4 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockBudgetBox/index.tsx +++ b/client/src/components/Home/HomeGridEpochResults/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLockBudgetBox'; +export { default } from './HomeGridEpochResults'; diff --git a/client/src/components/Home/HomeGridEpochResults/types.ts b/client/src/components/Home/HomeGridEpochResults/types.ts new file mode 100644 index 0000000000..b1b0054ced --- /dev/null +++ b/client/src/components/Home/HomeGridEpochResults/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridEpochResultsProps { + className?: string; +} diff --git a/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.module.scss b/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.module.scss new file mode 100644 index 0000000000..84fd886751 --- /dev/null +++ b/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.module.scss @@ -0,0 +1,23 @@ +.root { + padding: 1.4rem 2.4rem 2.4rem; + + @media #{$tablet-up} { + padding-top: 0.8rem; + } + + .divider { + margin-top: 2rem; + width: 100%; + height: 0.1rem; + background-color: $color-octant-grey1; + } + + .withdrawEthButton { + margin-top: 2.4rem; + width: 100%; + } + + .pending { + min-height: 6.4rem; + } +} diff --git a/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.tsx b/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.tsx new file mode 100644 index 0000000000..abccb37821 --- /dev/null +++ b/client/src/components/Home/HomeGridPersonalAllocation/HomeGridPersonalAllocation.tsx @@ -0,0 +1,138 @@ +import { format } from 'date-fns'; +import _first from 'lodash/first'; +import React, { FC, useState } from 'react'; +import { useTranslation } from 'react-i18next'; +import { useAccount } from 'wagmi'; + +import ModalWithdrawEth from 'components/Home/HomeGridPersonalAllocation/ModalWithdrawEth'; +import GridTile from 'components/shared/Grid/GridTile'; +import Sections from 'components/ui/BoxRounded/Sections/Sections'; +import Button from 'components/ui/Button'; +import DoubleValue from 'components/ui/DoubleValue'; +import useEpochAndAllocationTimestamps from 'hooks/helpers/useEpochAndAllocationTimestamps'; +import useIsProjectAdminMode from 'hooks/helpers/useIsProjectAdminMode'; +import useMediaQuery from 'hooks/helpers/useMediaQuery'; +import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; +import useCurrentEpochProps from 'hooks/queries/useCurrentEpochProps'; +import useIsDecisionWindowOpen from 'hooks/queries/useIsDecisionWindowOpen'; +import useWithdrawals from 'hooks/queries/useWithdrawals'; +import useTransactionLocalStore from 'store/transactionLocal/store'; +import getIsPreLaunch from 'utils/getIsPreLaunch'; + +import styles from './HomeGridPersonalAllocation.module.scss'; +import HomeGridPersonalAllocationProps from './types'; + +const HomeGridPersonalAllocation: FC = ({ className }) => { + const { isConnected } = useAccount(); + const { isMobile } = useMediaQuery(); + const { i18n, t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridPersonalAllocation', + }); + const [isModalWithdrawEthOpen, setIsModalWithdrawEthOpen] = useState(false); + const { data: currentEpoch } = useCurrentEpoch(); + const { data: isDecisionWindowOpen } = useIsDecisionWindowOpen(); + const { timeCurrentEpochStart, timeCurrentAllocationEnd } = useEpochAndAllocationTimestamps(); + const { data: currentEpochProps } = useCurrentEpochProps(); + const { data: withdrawals, isFetching: isFetchingWithdrawals } = useWithdrawals(); + const { isAppWaitingForTransactionToBeIndexed, transactionsPending } = useTransactionLocalStore( + state => ({ + isAppWaitingForTransactionToBeIndexed: state.data.isAppWaitingForTransactionToBeIndexed, + transactionsPending: state.data.transactionsPending, + }), + ); + + const isPreLaunch = getIsPreLaunch(currentEpoch); + const isProjectAdminMode = useIsProjectAdminMode(); + + return ( + <> + +
+ + +
+ +
{t('pendingFundsAvailableAfter')}
+
+ {/* TODO OCT-1041 fetch next epoch props instead of assuming the same length */} + {currentEpochProps && timeCurrentEpochStart && timeCurrentAllocationEnd + ? format( + new Date( + isDecisionWindowOpen + ? timeCurrentAllocationEnd + : // When AW is closed, it's when the last AW closed. + timeCurrentEpochStart + currentEpochProps.decisionWindow, + ), + 'haaa z, d LLLL', + ) + : ''} +
+
+ ), + }, + }, + ]} + variant="standard" + /> + +
+
+ setIsModalWithdrawEthOpen(false), + }} + /> + + ); +}; + +export default HomeGridPersonalAllocation; diff --git a/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/ModalWithdrawEth.tsx b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/ModalWithdrawEth.tsx new file mode 100644 index 0000000000..eba45daf2e --- /dev/null +++ b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/ModalWithdrawEth.tsx @@ -0,0 +1,21 @@ +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import WithdrawEth from 'components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth'; +import Modal from 'components/ui/Modal'; + +import ModalWithdrawEthProps from './types'; + +const ModalWithdrawEth: FC = ({ modalProps }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridPersonalAllocation.modalWithdrawEth', + }); + + return ( + + + + ); +}; + +export default ModalWithdrawEth; diff --git a/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.module.scss b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.module.scss new file mode 100644 index 0000000000..3bfdb2167a --- /dev/null +++ b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.module.scss @@ -0,0 +1,9 @@ +.root { + width: 100%; + height: 100%; +} + +.button { + width: 100%; + margin: 2.4rem 0; +} diff --git a/client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.tsx b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.tsx similarity index 89% rename from client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.tsx rename to client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.tsx index 9f916abb7a..f489c977d1 100644 --- a/client/src/components/Earn/EarnWithdrawEth/EarnWithdrawEth.tsx +++ b/client/src/components/Home/HomeGridPersonalAllocation/ModalWithdrawEth/WithdrawEth/WithdrawEth.tsx @@ -10,12 +10,12 @@ import useWithdrawEth, { BatchWithdrawRequest } from 'hooks/mutations/useWithdra import useWithdrawals from 'hooks/queries/useWithdrawals'; import useTransactionLocalStore from 'store/transactionLocal/store'; -import styles from './EarnWithdrawEth.module.scss'; -import EarnWithdrawEthProps from './types'; +import WithdrawEthProps from './types'; +import styles from './WithdrawEth.module.scss'; -const EarnWithdrawEth: FC = ({ onCloseModal }) => { +const WithdrawEth: FC = ({ onCloseModal }) => { const { i18n, t } = useTranslation('translation', { - keyPrefix: 'components.dedicated.withdrawEth', + keyPrefix: 'components.home.homeGridPersonalAllocation.modalWithdrawEth', }); const { data: feeData, isFetching: isFetchingFeeData } = useFeeData(); const { isAppWaitingForTransactionToBeIndexed, addTransactionPending } = useTransactionLocalStore( @@ -59,7 +59,7 @@ const EarnWithdrawEth: FC = ({ onCloseModal }) => { showCryptoSuffix: true, valueCrypto: withdrawals?.sums.available, }, - label: t('amount'), + label: i18n.t('common.amount'), }, { doubleValueProps: { @@ -74,7 +74,7 @@ const EarnWithdrawEth: FC = ({ onCloseModal }) => { return (
- +
) : ( type !== 'allocation' && ( - + ) )}
@@ -88,9 +88,8 @@ const EarnHistoryItem: FC = ({ isLast, ...rest }) => { variant="tiny" /> - {!isLast &&
} - setIsModalOpen(false), @@ -100,4 +99,4 @@ const EarnHistoryItem: FC = ({ isLast, ...rest }) => { ); }; -export default memo(EarnHistoryItem); +export default memo(TransactionsListItem); diff --git a/client/src/components/Home/HomeGridTransactions/TransactionsListItem/index.tsx b/client/src/components/Home/HomeGridTransactions/TransactionsListItem/index.tsx new file mode 100644 index 0000000000..103b277ac4 --- /dev/null +++ b/client/src/components/Home/HomeGridTransactions/TransactionsListItem/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './TransactionsListItem'; diff --git a/client/src/components/Earn/EarnHistory/EarnHistoryItem/types.ts b/client/src/components/Home/HomeGridTransactions/TransactionsListItem/types.ts similarity index 75% rename from client/src/components/Earn/EarnHistory/EarnHistoryItem/types.ts rename to client/src/components/Home/HomeGridTransactions/TransactionsListItem/types.ts index 1d6312c52f..012f045937 100644 --- a/client/src/components/Earn/EarnHistory/EarnHistoryItem/types.ts +++ b/client/src/components/Home/HomeGridTransactions/TransactionsListItem/types.ts @@ -1,13 +1,12 @@ import { HistoryElement, EventData } from 'hooks/queries/useHistory'; import { TransactionPending } from 'store/transactionLocal/types'; -type EarnHistoryItemProps = Omit & { +type TransactionsListItemProps = Omit & { eventData: Partial & { amount: bigint }; } & { isFinalized?: TransactionPending['isFinalized']; - isLast: boolean; isMultisig?: boolean; isWaitingForTransactionInitialized?: TransactionPending['isWaitingForTransactionInitialized']; }; -export default EarnHistoryItemProps; +export default TransactionsListItemProps; diff --git a/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/EarnHistorySkeleton.module.scss b/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/TransactionsSkeleton.module.scss similarity index 100% rename from client/src/components/Earn/EarnHistory/EarnHistorySkeleton/EarnHistorySkeleton.module.scss rename to client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/TransactionsSkeleton.module.scss diff --git a/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/EarnHistorySkeleton.tsx b/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/TransactionsSkeleton.tsx similarity index 73% rename from client/src/components/Earn/EarnHistory/EarnHistorySkeleton/EarnHistorySkeleton.tsx rename to client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/TransactionsSkeleton.tsx index 83456c4b48..e8375e2e91 100644 --- a/client/src/components/Earn/EarnHistory/EarnHistorySkeleton/EarnHistorySkeleton.tsx +++ b/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/TransactionsSkeleton.tsx @@ -1,9 +1,9 @@ import cx from 'classnames'; import React, { ReactElement } from 'react'; -import styles from './EarnHistorySkeleton.module.scss'; +import styles from './TransactionsSkeleton.module.scss'; -const EarnHistorySkeleton = (): ReactElement => { +const TransactionsSkeleton = (): ReactElement => { return (
@@ -18,4 +18,4 @@ const EarnHistorySkeleton = (): ReactElement => { ); }; -export default EarnHistorySkeleton; +export default TransactionsSkeleton; diff --git a/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/index.tsx b/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/index.tsx new file mode 100644 index 0000000000..ba5cbd5467 --- /dev/null +++ b/client/src/components/Home/HomeGridTransactions/TransactionsSkeleton/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './TransactionsSkeleton'; diff --git a/client/src/components/Home/HomeGridTransactions/index.tsx b/client/src/components/Home/HomeGridTransactions/index.tsx new file mode 100644 index 0000000000..fa3e746f54 --- /dev/null +++ b/client/src/components/Home/HomeGridTransactions/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './HomeGridTransactions'; diff --git a/client/src/components/Home/HomeGridTransactions/types.ts b/client/src/components/Home/HomeGridTransactions/types.ts new file mode 100644 index 0000000000..4cbc3cbfba --- /dev/null +++ b/client/src/components/Home/HomeGridTransactions/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridTransactionsProps { + className?: string; +} diff --git a/client/src/components/Settings/SettingsAddressScore/SettingsAddressScore.module.scss b/client/src/components/Home/HomeGridUQScore/AddressScore/AddressScore.module.scss similarity index 100% rename from client/src/components/Settings/SettingsAddressScore/SettingsAddressScore.module.scss rename to client/src/components/Home/HomeGridUQScore/AddressScore/AddressScore.module.scss diff --git a/client/src/components/Settings/SettingsAddressScore/SettingsAddressScore.tsx b/client/src/components/Home/HomeGridUQScore/AddressScore/AddressScore.tsx similarity index 89% rename from client/src/components/Settings/SettingsAddressScore/SettingsAddressScore.tsx rename to client/src/components/Home/HomeGridUQScore/AddressScore/AddressScore.tsx index ca9ef29e18..654d18ca73 100644 --- a/client/src/components/Settings/SettingsAddressScore/SettingsAddressScore.tsx +++ b/client/src/components/Home/HomeGridUQScore/AddressScore/AddressScore.tsx @@ -10,10 +10,10 @@ import Svg from 'components/ui/Svg'; import { checkMark } from 'svg/misc'; import truncateEthAddress from 'utils/truncateEthAddress'; -import styles from './SettingsAddressScore.module.scss'; -import SettingsAddressScoreProps from './types'; +import styles from './AddressScore.module.scss'; +import AddressScoreProps from './types'; -const SettingsAddressScore: FC = ({ +const AddressScore: FC = ({ address, badge, score, @@ -26,7 +26,9 @@ const SettingsAddressScore: FC = ({ mode, showActiveDot, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore.addressScore', + }); const { address: activeAddress } = useAccount(); const isActive = activeAddress === address; @@ -92,4 +94,4 @@ const SettingsAddressScore: FC = ({ ); }; -export default memo(SettingsAddressScore); +export default memo(AddressScore); diff --git a/client/src/components/Home/HomeGridUQScore/AddressScore/index.tsx b/client/src/components/Home/HomeGridUQScore/AddressScore/index.tsx new file mode 100644 index 0000000000..772b0e39ed --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/AddressScore/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './AddressScore'; diff --git a/client/src/components/Settings/SettingsAddressScore/types.ts b/client/src/components/Home/HomeGridUQScore/AddressScore/types.ts similarity index 85% rename from client/src/components/Settings/SettingsAddressScore/types.ts rename to client/src/components/Home/HomeGridUQScore/AddressScore/types.ts index dfaace9d8b..807f77fa20 100644 --- a/client/src/components/Settings/SettingsAddressScore/types.ts +++ b/client/src/components/Home/HomeGridUQScore/AddressScore/types.ts @@ -1,4 +1,4 @@ -export default interface SettingsAddressScoreProps { +export default interface AddressScoreProps { address: string; areBottomCornersRounded?: boolean; badge: 'primary' | 'secondary'; diff --git a/client/src/components/Home/HomeGridUQScore/HomeGridUQScore.module.scss b/client/src/components/Home/HomeGridUQScore/HomeGridUQScore.module.scss new file mode 100644 index 0000000000..6eb6d3d628 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/HomeGridUQScore.module.scss @@ -0,0 +1,35 @@ +.root { + padding: 0 2.4rem 2.4rem; + + .furtherActions { + height: 4.8rem; + justify-content: left; + font-size: 1rem; + border-bottom: 0.1rem solid $color-octant-grey3; + margin-bottom: 2.4rem; + padding: 0; + } + + .buttonsWrapper { + width: 100%; + display: flex; + justify-content: space-between; + + .button { + flex: 1; + + &:first-child { + margin-right: 1.2rem; + } + } + } +} + +.titleSuffix { + margin-left: auto; + cursor: pointer; + color: $color-octant-green; + font-size: $font-size-10; + font-weight: $font-weight-bold; + min-height: 3.2rem; +} diff --git a/client/src/components/Settings/SettingsUniquenessScoreBox/SettingsUniquenessScoreBox.tsx b/client/src/components/Home/HomeGridUQScore/HomeGridUQScore.tsx similarity index 65% rename from client/src/components/Settings/SettingsUniquenessScoreBox/SettingsUniquenessScoreBox.tsx rename to client/src/components/Home/HomeGridUQScore/HomeGridUQScore.tsx index 61118f8b5f..649b95a325 100644 --- a/client/src/components/Settings/SettingsUniquenessScoreBox/SettingsUniquenessScoreBox.tsx +++ b/client/src/components/Home/HomeGridUQScore/HomeGridUQScore.tsx @@ -1,18 +1,21 @@ import { watchAccount } from '@wagmi/core'; import uniq from 'lodash/uniq'; -import React, { ReactNode, useEffect, useMemo, useState } from 'react'; +import React, { FC, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useAccount, useConnectors } from 'wagmi'; import { wagmiConfig } from 'api/clients/client-wagmi'; -import ModalSettingsCalculatingUQScore from 'components/Settings/ModalSettingsCalculatingUQScore'; -import ModalSettingsCalculatingYourUniqueness from 'components/Settings/ModalSettingsCalculatingYourUniqueness'; -import ModalSettingsRecalculatingScore from 'components/Settings/ModalSettingsRecalculatingScore'; -import SettingsUniquenessScoreAddresses from 'components/Settings/SettingsUniquenessScoreAddresses'; -import BoxRounded from 'components/ui/BoxRounded'; +import HomeGridUQScoreAddresses from 'components/Home/HomeGridUQScore/HomeGridUQScoreAddresses'; +import ModalCalculatingUQScore from 'components/Home/HomeGridUQScore/ModalCalculatingUQScore'; +import ModalCalculatingYourUniqueness from 'components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness'; +import ModalRecalculatingScore from 'components/Home/HomeGridUQScore/ModalRecalculatingScore'; +import GridTile from 'components/shared/Grid/GridTile'; import Button from 'components/ui/Button'; -import { DELEGATION_MIN_SCORE } from 'constants/delegation'; -import { GITCOIN_PASSPORT_CUSTOM_OCTANT_DASHBOARD } from 'constants/urls'; +import { UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1 } from 'constants/uq'; +import { + GITCOIN_PASSPORT_CUSTOM_OCTANT_DASHBOARD, + TIME_OUT_LIST_DISPUTE_FORM, +} from 'constants/urls'; import useCheckDelegation from 'hooks/mutations/useCheckDelegation'; import useRefreshAntisybilStatus from 'hooks/mutations/useRefreshAntisybilStatus'; import useAntisybilStatusScore from 'hooks/queries/useAntisybilStatusScore'; @@ -20,13 +23,16 @@ import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useUqScore from 'hooks/queries/useUqScore'; import useUserTOS from 'hooks/queries/useUserTOS'; import toastService from 'services/toastService'; -import useSettingsStore from 'store/settings/store'; +import useDelegationStore from 'store/delegation/store'; -import styles from './SettingsUniquenessScoreBox.module.scss'; +import styles from './HomeGridUQScore.module.scss'; +import HomeGridUQScoreProps from './types'; -const SettingsUniquenessScoreBox = (): ReactNode => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); - const { address } = useAccount(); +const HomeGridUQScore: FC = ({ className }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore', + }); + const { address, isConnected } = useAccount(); const connectors = useConnectors(); const { data: currentEpoch } = useCurrentEpoch(); const { data: isUserTOSAccepted } = useUserTOS(); @@ -52,7 +58,7 @@ const SettingsUniquenessScoreBox = (): ReactNode => { setDelegationSecondaryAddress, setIsDelegationCalculatingUQScoreModalOpen, setIsDelegationCompleted, - } = useSettingsStore(state => ({ + } = useDelegationStore(state => ({ delegationPrimaryAddress: state.data.delegationPrimaryAddress, delegationSecondaryAddress: state.data.delegationSecondaryAddress, isDelegationCalculatingUQScoreModalOpen: state.data.isDelegationCalculatingUQScoreModalOpen, @@ -75,22 +81,6 @@ const SettingsUniquenessScoreBox = (): ReactNode => { : primaryAddressScore === null || primaryAddressScore !== address, ); - const isRecalculateButtonDisabled = - isDelegationCompleted || - isFetchingScore || - isFetchingUqScore || - delegationSecondaryAddress === '0x???'; - - const isDelegateButtonDisabled = - isDelegationCompleted || - isFetchingScore || - primaryAddressScore === null || - primaryAddressScore === undefined || - primaryAddressScore >= 20 || - isFetchingUqScore || - uqScore === 100n || - delegationSecondaryAddress === '0x???'; - const { mutateAsync: checkDelegationMutation } = useCheckDelegation(); const { mutateAsync: refreshAntisybilStatus, @@ -111,7 +101,7 @@ const SettingsUniquenessScoreBox = (): ReactNode => { }, ); - const modalSettingsCalculatingUQScoreProps = useMemo(() => { + const modalCalculatingUQScoreProps = useMemo(() => { return { isOpen: isDelegationCalculatingUQScoreModalOpen, onClosePanel: () => setIsDelegationCalculatingUQScoreModalOpen(false), @@ -119,7 +109,7 @@ const SettingsUniquenessScoreBox = (): ReactNode => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isDelegationCalculatingUQScoreModalOpen]); - const modalSettingsRecalculatingScoreProps = useMemo(() => { + const modalRecalculatingScoreProps = useMemo(() => { return { isOpen: isRecalculatingScoreModalOpen, onClosePanel: () => setIsRecalculatingScoreModalOpen(false), @@ -127,7 +117,7 @@ const SettingsUniquenessScoreBox = (): ReactNode => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isRecalculatingScoreModalOpen]); - const modalSettingsCalculatingYourUniquenessProps = useMemo(() => { + const modalCalculatingYourUniquenessProps = useMemo(() => { return { isOpen: isCalculatingYourUniquenessModalOpen, onClosePanel: () => setIsCalculatingYourUniquenessModalOpen(false), @@ -179,16 +169,16 @@ const SettingsUniquenessScoreBox = (): ReactNode => { return; } if (isDelegationCompleted) { - setSecondaryAddressScore(antisybilStatusScore); + setSecondaryAddressScore(antisybilStatusScore?.score); } else { if (refreshAntisybilStatusError) { setDelegationPrimaryAddress(address); setDelegationSecondaryAddress('0x???'); } setPrimaryAddressScore( - antisybilStatusScore < DELEGATION_MIN_SCORE && uqScore === 100n - ? DELEGATION_MIN_SCORE - : antisybilStatusScore, + antisybilStatusScore?.score < UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1 && uqScore === 100n + ? UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1 + : antisybilStatusScore?.score, ); } setIsFetchingScore(false); @@ -230,64 +220,91 @@ const SettingsUniquenessScoreBox = (): ReactNode => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [isUserTOSAccepted, address]); + const isRecalculateButtonDisabled = + !isConnected || + isDelegationCompleted || + isFetchingScore || + isFetchingUqScore || + delegationSecondaryAddress === '0x???' || + antisybilStatusScore?.isOnTimeOutList; + + const isDelegateButtonDisabled = + !isConnected || + isDelegationCompleted || + isFetchingScore || + primaryAddressScore === null || + primaryAddressScore === undefined || + primaryAddressScore >= 20 || + isFetchingUqScore || + uqScore === 100n || + delegationSecondaryAddress === '0x???' || + antisybilStatusScore?.isOnTimeOutList; + return ( - setIsCalculatingYourUniquenessModalOpen(true)} - > - {t('whatIsThis')} -
- } - > - <> - - + <> + { - setIsDelegationInProgress(true); - setDelegationPrimaryAddress(address); - setIsDelegationConnectModalOpen(true); - }} - > - {t('delegate')} - + className={styles.titleSuffix} + isButtonScalingUpOnHover={false} + label={t('whatIsThis')} + onClick={() => setIsCalculatingYourUniquenessModalOpen(true)} + variant="link3" + /> + } + > +
+ + {antisybilStatusScore?.isOnTimeOutList ? ( + + +
- - - - - + + + + + ); }; -export default SettingsUniquenessScoreBox; +export default HomeGridUQScore; diff --git a/client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.module.scss b/client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.module.scss similarity index 71% rename from client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.module.scss rename to client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.module.scss index 05ba83fcad..b2d5a6f105 100644 --- a/client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.module.scss +++ b/client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.module.scss @@ -1,12 +1,40 @@ .root { + position: relative; width: 100%; - height: 7.2rem; + height: 10.4rem; padding: 0 2.4rem; display: flex; align-items: center; border-radius: $border-radius-16; background-color: $color-octant-grey3; - margin: 1.6rem 0 1.4rem; + + &.noWalletConnected { + .octantLogo { + path { + fill: $color-octant-grey2; + } + } + + .addresses .addressesGroup { + max-width: 13.2rem; + opacity: 0.5; + } + + .score { + color: $color-octant-grey5; + } + } + + &.isOnTimeOutList { + background: $color-octant-orange6; + } + + .isOnTimeOutListLabel { + position: absolute; + top: 1.6rem; + left: 2.4rem; + text-transform: uppercase; + } .avatarsGroup { position: relative; @@ -51,12 +79,12 @@ font-weight: $font-weight-bold; letter-spacing: 0.03rem; text-transform: uppercase; + text-align: left; } } .score { - font-size: $font-size-24; - font-weight: $font-weight-bold; + @include fontBig($font-size-32); color: $color-octant-dark; margin-left: auto; diff --git a/client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.tsx b/client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.tsx similarity index 62% rename from client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.tsx rename to client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.tsx index 5c9ee98235..36f7874897 100644 --- a/client/src/components/Settings/SettingsUniquenessScoreAddresses/SettingsUniquenessScoreAddresses.tsx +++ b/client/src/components/Home/HomeGridUQScore/HomeGridUQScoreAddresses/HomeGridUQScoreAddresses.tsx @@ -6,20 +6,24 @@ import { useAccount } from 'wagmi'; import Identicon from 'components/ui/Identicon'; import Svg from 'components/ui/Svg'; -import useSettingsStore from 'store/settings/store'; +import TinyLabel from 'components/ui/TinyLabel'; +import useDelegationStore from 'store/delegation/store'; import { octant } from 'svg/logo'; import truncateEthAddress from 'utils/truncateEthAddress'; -import styles from './SettingsUniquenessScoreAddresses.module.scss'; -import SettingsUniquenessScoreAddressesProps from './types'; +import styles from './HomeGridUQScoreAddresses.module.scss'; +import HomeGridUQScoreAddressesProps from './types'; -const SettingsUniquenessScoreAddresses: FC = ({ +const HomeGridUQScoreAddresses: FC = ({ isFetchingScore, + isOnTimeOutList, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore', + }); const ref = useRef(null); - const { address: accountAddress } = useAccount(); + const { address: accountAddress, isConnected } = useAccount(); const { isDelegationCompleted, @@ -27,7 +31,7 @@ const SettingsUniquenessScoreAddresses: FC ({ + } = useDelegationStore(state => ({ delegationPrimaryAddress: state.data.delegationPrimaryAddress, delegationSecondaryAddress: state.data.delegationSecondaryAddress, isDelegationCompleted: state.data.isDelegationCompleted, @@ -43,20 +47,25 @@ const SettingsUniquenessScoreAddresses: FC 1; const addressesToShow = useMemo(() => { + if (!isConnected) { + return t('noWalletConnected'); + } if (showMoreThanOneAddress) { return addresses.map(address => address.slice(0, 5)).join(', '); } return truncateEthAddress(addresses.at(0) || ''); - }, [showMoreThanOneAddress, addresses]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [showMoreThanOneAddress, addresses, isConnected]); useEffect(() => { - if (isFetchingScore || !ref?.current) { + if (isFetchingScore || !ref?.current || !isConnected) { return; } const controls = animate( @@ -74,15 +83,35 @@ const SettingsUniquenessScoreAddresses: FC controls.complete(); - }, [isFetchingScore, isDelegationCompleted, secondaryAddressScore, primaryAddressScore]); + }, [ + isFetchingScore, + isDelegationCompleted, + secondaryAddressScore, + primaryAddressScore, + isConnected, + ]); return ( -
+
+ {isOnTimeOutList && ( + + )}
- {addresses.map(address => ( -
- {address === '0x???' ? ( - + {addresses?.map((address, index) => ( +
+ {!isConnected || address === '0x???' ? ( + ) : ( )} @@ -102,4 +131,4 @@ const SettingsUniquenessScoreAddresses: FC = ({ - setShowCloseButton, -}) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); +const CalculatingUQScore: FC = ({ setShowCloseButton }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore.modalCalculatingUQScore', + }); const { address } = useAccount(); const { data: isUserTOSAccepted } = useUserTOS(); @@ -41,7 +41,7 @@ const SettingsCalculatingUQScore: FC = ({ setCalculatingUQScoreMode, setIsDelegationCompleted, setSecondaryAddressScore, - } = useSettingsStore(state => ({ + } = useDelegationStore(state => ({ calculatingUQScoreMode: state.data.calculatingUQScoreMode, delegationPrimaryAddress: state.data.delegationPrimaryAddress, delegationSecondaryAddress: state.data.delegationSecondaryAddress, @@ -69,13 +69,13 @@ const SettingsCalculatingUQScore: FC = ({ const showLowScoreInfo = isScoreHighlighted && secondaryAddressAntisybilStatusScore !== undefined && - secondaryAddressAntisybilStatusScore < DELEGATION_MIN_SCORE; + secondaryAddressAntisybilStatusScore.score < UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1; const scoreHighlight = useMemo(() => { if (!isScoreHighlighted || secondaryAddressAntisybilStatusScore === undefined) { return undefined; } - if (secondaryAddressAntisybilStatusScore < DELEGATION_MIN_SCORE) { + if (secondaryAddressAntisybilStatusScore.score < UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1) { return 'red'; } return 'black'; @@ -146,12 +146,12 @@ const SettingsCalculatingUQScore: FC = ({ setLastDoneStep(1); setTimeout(() => { setLastDoneStep(2); - if (secondaryAddressAntisybilStatusScore < DELEGATION_MIN_SCORE) { + if (secondaryAddressAntisybilStatusScore.score < UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1) { setShowCloseButton(true); setIsDelegationInProgress(false); return; } - setSecondaryAddressScore(secondaryAddressAntisybilStatusScore); + setSecondaryAddressScore(secondaryAddressAntisybilStatusScore.score); setCalculatingUQScoreMode('sign'); }, 2500); }, 2500); @@ -160,7 +160,7 @@ const SettingsCalculatingUQScore: FC = ({ return ( - = ({ score={primaryAddressScore ?? 0} showActiveDot={calculatingUQScoreMode === 'sign'} /> - = ({ } mode={calculatingUQScoreMode} onSignMessage={() => signMessageAndDelegate(true)} - score={secondaryAddressAntisybilStatusScore ?? 0} + score={secondaryAddressAntisybilStatusScore?.score ?? 0} scoreHighlight={scoreHighlight} showActiveDot={calculatingUQScoreMode === 'sign'} /> {calculatingUQScoreMode === 'score' && ( - + )} {showLowScoreInfo && ( = ({ ); }; -export default SettingsCalculatingUQScore; +export default CalculatingUQScore; diff --git a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/index.tsx b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore/index.tsx similarity index 51% rename from client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/index.tsx rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore/index.tsx index 002295c96d..7a7fa10116 100644 --- a/client/src/components/Earn/EarnGlmLock/EarnGlmLockStepper/index.tsx +++ b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore/index.tsx @@ -1,2 +1,2 @@ // eslint-disable-next-line no-restricted-exports -export { default } from './EarnGlmLockStepper'; +export { default } from './CalculatingUQScore'; diff --git a/client/src/components/Settings/SettingsCalculatingUQScore/types.ts b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore/types.ts similarity index 100% rename from client/src/components/Settings/SettingsCalculatingUQScore/types.ts rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore/types.ts diff --git a/client/src/components/Settings/ModalSettingsCalculatingUQScore/ModalSettingsCalculatingUQScore.module.scss b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/ModalCalculatingUQScore.module.scss similarity index 100% rename from client/src/components/Settings/ModalSettingsCalculatingUQScore/ModalSettingsCalculatingUQScore.module.scss rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/ModalCalculatingUQScore.module.scss diff --git a/client/src/components/Settings/ModalSettingsCalculatingUQScore/ModalSettingsCalculatingUQScore.tsx b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/ModalCalculatingUQScore.tsx similarity index 61% rename from client/src/components/Settings/ModalSettingsCalculatingUQScore/ModalSettingsCalculatingUQScore.tsx rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/ModalCalculatingUQScore.tsx index a2048c5968..227295311c 100644 --- a/client/src/components/Settings/ModalSettingsCalculatingUQScore/ModalSettingsCalculatingUQScore.tsx +++ b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/ModalCalculatingUQScore.tsx @@ -1,20 +1,20 @@ import React, { FC, useState } from 'react'; import { useTranslation } from 'react-i18next'; -import SettingsCalculatingUQScore from 'components/Settings/SettingsCalculatingUQScore'; +import CalculatingUQScore from 'components/Home/HomeGridUQScore/ModalCalculatingUQScore/CalculatingUQScore'; import Button from 'components/ui/Button'; import Modal from 'components/ui/Modal'; -import useSettingsStore from 'store/settings/store'; +import useDelegationStore from 'store/delegation/store'; -import styles from './ModalSettingsCalculatingUQScore.module.scss'; -import ModalSettingsCalculatingUQScoreProps from './types'; +import styles from './ModalCalculatingUQScore.module.scss'; +import ModalCalculatingUQScoreProps from './types'; -const ModalSettingsCalculatingUQScore: FC = ({ - modalProps, -}) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); +const ModalCalculatingUQScore: FC = ({ modalProps }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore.modalCalculatingUQScore', + }); - const { calculatingUQScoreMode, setIsDelegationConnectModalOpen } = useSettingsStore(state => ({ + const { calculatingUQScoreMode, setIsDelegationConnectModalOpen } = useDelegationStore(state => ({ calculatingUQScoreMode: state.data.calculatingUQScoreMode, setIsDelegationConnectModalOpen: state.setIsDelegationConnectModalOpen, })); @@ -24,7 +24,7 @@ const ModalSettingsCalculatingUQScore: FC return ( @@ -45,9 +45,9 @@ const ModalSettingsCalculatingUQScore: FC showCloseButton={showCloseButton} {...modalProps} > - + ); }; -export default ModalSettingsCalculatingUQScore; +export default ModalCalculatingUQScore; diff --git a/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/index.tsx b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/index.tsx new file mode 100644 index 0000000000..1ecc801c97 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ModalCalculatingUQScore'; diff --git a/client/src/components/Settings/ModalSettingsCalculatingUQScore/types.ts b/client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/types.ts similarity index 100% rename from client/src/components/Settings/ModalSettingsCalculatingUQScore/types.ts rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingUQScore/types.ts diff --git a/client/src/components/Settings/ModalSettingsCalculatingYourUniqueness/ModalSettingsCalculatingYourUniqueness.module.scss b/client/src/components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness/ModalCalculatingYourUniqueness.module.scss similarity index 100% rename from client/src/components/Settings/ModalSettingsCalculatingYourUniqueness/ModalSettingsCalculatingYourUniqueness.module.scss rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness/ModalCalculatingYourUniqueness.module.scss diff --git a/client/src/components/Settings/ModalSettingsCalculatingYourUniqueness/ModalSettingsCalculatingYourUniqueness.tsx b/client/src/components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness/ModalCalculatingYourUniqueness.tsx similarity index 73% rename from client/src/components/Settings/ModalSettingsCalculatingYourUniqueness/ModalSettingsCalculatingYourUniqueness.tsx rename to client/src/components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness/ModalCalculatingYourUniqueness.tsx index 58a900748b..95ad8f9a00 100644 --- a/client/src/components/Settings/ModalSettingsCalculatingYourUniqueness/ModalSettingsCalculatingYourUniqueness.tsx +++ b/client/src/components/Home/HomeGridUQScore/ModalCalculatingYourUniqueness/ModalCalculatingYourUniqueness.tsx @@ -13,18 +13,21 @@ import { } from 'constants/urls'; import useModalStepperNavigation from 'hooks/helpers/useModalStepperNavigation'; -import styles from './ModalSettingsCalculatingYourUniqueness.module.scss'; -import ModalSettingsCalculatingYourUniquenessProps from './types'; +import styles from './ModalCalculatingYourUniqueness.module.scss'; +import ModalCalculatingYourUniquenessProps from './types'; -const ModalSettingsCalculatingYourUniqueness: FC = ({ +const ModalCalculatingYourUniqueness: FC = ({ modalProps, }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); + const translationKeyPrefix = 'components.home.homeGridUQScore.modalCalculatingYourUniqueness'; + const { t } = useTranslation('translation', { + keyPrefix: translationKeyPrefix, + }); const steps = [ ]} - i18nKey="views.settings.calculatingYourUniquenessStep1" + i18nKey={`${translationKeyPrefix}.calculatingYourUniquenessStep1`} />, , ]} - i18nKey="views.settings.calculatingYourUniquenessStep2" + i18nKey={`${translationKeyPrefix}.calculatingYourUniquenessStep2`} />, , ]} - i18nKey="views.settings.calculatingYourUniquenessStep3" + i18nKey={`${translationKeyPrefix}.calculatingYourUniquenessStep3`} />, ]; @@ -55,7 +58,7 @@ const ModalSettingsCalculatingYourUniqueness: FC @@ -75,7 +78,7 @@ const ModalSettingsCalculatingYourUniqueness: FC { if (stepIndex === currentStepIndex && stepIndex !== steps.length - 1) { @@ -89,4 +92,4 @@ const ModalSettingsCalculatingYourUniqueness: FC; } diff --git a/client/src/components/Settings/ModalSettingsRecalculatingScore/ModalSettingsRecalculatingScore.module.scss b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/ModalRecalculatingScore.module.scss similarity index 100% rename from client/src/components/Settings/ModalSettingsRecalculatingScore/ModalSettingsRecalculatingScore.module.scss rename to client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/ModalRecalculatingScore.module.scss diff --git a/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/ModalRecalculatingScore.tsx b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/ModalRecalculatingScore.tsx new file mode 100644 index 0000000000..38d8b676b2 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/ModalRecalculatingScore.tsx @@ -0,0 +1,27 @@ +import React, { FC, memo } from 'react'; +import { useTranslation } from 'react-i18next'; + +import RecalculatingScore from 'components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore'; +import Modal from 'components/ui/Modal'; + +import styles from './ModalRecalculatingScore.module.scss'; +import ModalRecalculatingScoreProps from './types'; + +const ModalRecalculatingScore: FC = ({ modalProps }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore.modalRecalculatingScore', + }); + + return ( + + + + ); +}; + +export default memo(ModalRecalculatingScore); diff --git a/client/src/components/Settings/SettingsRecalculatingScore/SettingsRecalculatingScore.tsx b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/RecalculatingScore.tsx similarity index 81% rename from client/src/components/Settings/SettingsRecalculatingScore/SettingsRecalculatingScore.tsx rename to client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/RecalculatingScore.tsx index e09a3339ff..63eb59dc3d 100644 --- a/client/src/components/Settings/SettingsRecalculatingScore/SettingsRecalculatingScore.tsx +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/RecalculatingScore.tsx @@ -1,19 +1,19 @@ import React, { FC, useEffect, useMemo, useState } from 'react'; import { useAccount } from 'wagmi'; -import SettingsAddressScore from 'components/Settings/SettingsAddressScore'; -import SettingsProgressPath from 'components/Settings/SettingsProgressPath'; -import { DELEGATION_MIN_SCORE } from 'constants/delegation'; +import AddressScore from 'components/Home/HomeGridUQScore/AddressScore'; +import ProgressPath from 'components/Home/HomeGridUQScore/ProgressPath'; +import { UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1 } from 'constants/uq'; import useRefreshAntisybilStatus from 'hooks/mutations/useRefreshAntisybilStatus'; import useAntisybilStatusScore from 'hooks/queries/useAntisybilStatusScore'; import useCurrentEpoch from 'hooks/queries/useCurrentEpoch'; import useUqScore from 'hooks/queries/useUqScore'; import useUserTOS from 'hooks/queries/useUserTOS'; -import useSettingsStore from 'store/settings/store'; +import useDelegationStore from 'store/delegation/store'; -import SettingsRecalculatingScoreProps from './types'; +import RecalculatingScoreProps from './types'; -const SettingsRecalculatingScore: FC = ({ onLastStepDone }) => { +const RecalculatingScore: FC = ({ onLastStepDone }) => { const { data: currentEpoch } = useCurrentEpoch(); const { address } = useAccount(); const { data: isUserTOSAccepted } = useUserTOS(); @@ -25,7 +25,7 @@ const SettingsRecalculatingScore: FC = ({ onLas setSecondaryAddressScore, isDelegationCompleted, delegationSecondaryAddress, - } = useSettingsStore(state => ({ + } = useDelegationStore(state => ({ delegationSecondaryAddress: state.data.delegationSecondaryAddress, isDelegationCompleted: state.data.isDelegationCompleted, setPrimaryAddressScore: state.setPrimaryAddressScore, @@ -56,10 +56,14 @@ const SettingsRecalculatingScore: FC = ({ onLas ) { return 0; } - if (!isDelegationCompleted && antisybilStatusScore < DELEGATION_MIN_SCORE && uqScore === 100n) { - return DELEGATION_MIN_SCORE; + if ( + !isDelegationCompleted && + antisybilStatusScore.score < UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1 && + uqScore === 100n + ) { + return UQ_SCORE_THRESHOLD_FOR_LEVERAGE_1; } - return antisybilStatusScore; + return antisybilStatusScore.score; }, [antisybilStatusScore, uqScore, lastDoneStep, isDelegationCompleted, isErrorUqScore]); const scoreHighlight = lastDoneStep && lastDoneStep >= 1 ? 'black' : undefined; @@ -110,7 +114,7 @@ const SettingsRecalculatingScore: FC = ({ onLas return ( <> - = ({ onLas score={calculatedUqScore} scoreHighlight={scoreHighlight} /> - + ); }; -export default SettingsRecalculatingScore; +export default RecalculatingScore; diff --git a/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/index.tsx b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/index.tsx new file mode 100644 index 0000000000..05b1f8c13d --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './RecalculatingScore'; diff --git a/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/types.ts b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/types.ts new file mode 100644 index 0000000000..64f5c89c10 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/RecalculatingScore/types.ts @@ -0,0 +1,3 @@ +export default interface RecalculatingScoreProps { + onLastStepDone: () => void; +} diff --git a/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/index.tsx b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/index.tsx new file mode 100644 index 0000000000..b5db14a56d --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ModalRecalculatingScore'; diff --git a/client/src/components/Earn/ModalEarnRewardsCalculator/types.ts b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/types.ts similarity index 65% rename from client/src/components/Earn/ModalEarnRewardsCalculator/types.ts rename to client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/types.ts index 04220a105c..7615d40575 100644 --- a/client/src/components/Earn/ModalEarnRewardsCalculator/types.ts +++ b/client/src/components/Home/HomeGridUQScore/ModalRecalculatingScore/types.ts @@ -1,5 +1,5 @@ import ModalProps from 'components/ui/Modal/types'; -export default interface ModalEarnRewardsCalculatorProps { +export default interface ModalRecalculatingScoreProps { modalProps: Omit; } diff --git a/client/src/components/Settings/SettingsProgressPath/SettingsProgressPath.module.scss b/client/src/components/Home/HomeGridUQScore/ProgressPath/ProgressPath.module.scss similarity index 100% rename from client/src/components/Settings/SettingsProgressPath/SettingsProgressPath.module.scss rename to client/src/components/Home/HomeGridUQScore/ProgressPath/ProgressPath.module.scss diff --git a/client/src/components/Settings/SettingsProgressPath/SettingsProgressPath.tsx b/client/src/components/Home/HomeGridUQScore/ProgressPath/ProgressPath.tsx similarity index 84% rename from client/src/components/Settings/SettingsProgressPath/SettingsProgressPath.tsx rename to client/src/components/Home/HomeGridUQScore/ProgressPath/ProgressPath.tsx index 0dd0001970..85edc6c1d3 100644 --- a/client/src/components/Settings/SettingsProgressPath/SettingsProgressPath.tsx +++ b/client/src/components/Home/HomeGridUQScore/ProgressPath/ProgressPath.tsx @@ -3,11 +3,13 @@ import { motion } from 'framer-motion'; import React, { FC } from 'react'; import { useTranslation } from 'react-i18next'; -import styles from './SettingsProgressPath.module.scss'; -import SettingsProgressPathProps from './types'; +import styles from './ProgressPath.module.scss'; +import ProgressPathProps from './types'; -const SettingsProgressPath: FC = ({ lastDoneStep }) => { - const { t } = useTranslation('translation', { keyPrefix: 'views.settings' }); +const ProgressPath: FC = ({ lastDoneStep }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridUQScore.progressPath', + }); const steps = [t('checkingPassportScore'), t('checkingAllowlist'), t('finished')]; return ( @@ -54,4 +56,4 @@ const SettingsProgressPath: FC = ({ lastDoneStep }) = ); }; -export default SettingsProgressPath; +export default ProgressPath; diff --git a/client/src/components/Home/HomeGridUQScore/ProgressPath/index.tsx b/client/src/components/Home/HomeGridUQScore/ProgressPath/index.tsx new file mode 100644 index 0000000000..204ba1a586 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ProgressPath/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './ProgressPath'; diff --git a/client/src/components/Home/HomeGridUQScore/ProgressPath/types.ts b/client/src/components/Home/HomeGridUQScore/ProgressPath/types.ts new file mode 100644 index 0000000000..f4980ed0bc --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/ProgressPath/types.ts @@ -0,0 +1,3 @@ +export default interface ProgressPathProps { + lastDoneStep: null | 0 | 1 | 2; +} diff --git a/client/src/components/Home/HomeGridUQScore/index.tsx b/client/src/components/Home/HomeGridUQScore/index.tsx new file mode 100644 index 0000000000..c98a2ffb08 --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/index.tsx @@ -0,0 +1,2 @@ +// eslint-disable-next-line no-restricted-exports +export { default } from './HomeGridUQScore'; diff --git a/client/src/components/Home/HomeGridUQScore/types.ts b/client/src/components/Home/HomeGridUQScore/types.ts new file mode 100644 index 0000000000..369ea383ce --- /dev/null +++ b/client/src/components/Home/HomeGridUQScore/types.ts @@ -0,0 +1,3 @@ +export default interface HomeGridUQScoreProps { + className?: string; +} diff --git a/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.module.scss b/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.module.scss new file mode 100644 index 0000000000..9d92746e0f --- /dev/null +++ b/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.module.scss @@ -0,0 +1,20 @@ +.videosWrapper { + display: flex; + align-items: center; + flex: 1; + min-height: 0; + padding: 0 2.4rem; + gap: 2.4rem; +} + +.constraintsWrapper { + display: flex; + overflow: hidden; +} + +.buttonClose { + position: absolute; + top: 1.6rem; + right: 1.6rem; + background-color: $color-octant-grey6; +} diff --git a/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.tsx b/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.tsx new file mode 100644 index 0000000000..886409c0aa --- /dev/null +++ b/client/src/components/Home/HomeGridVideoBar/HomeGridVideoBar.tsx @@ -0,0 +1,56 @@ +import { motion } from 'framer-motion'; +import React, { FC, useRef } from 'react'; +import { useTranslation } from 'react-i18next'; + +import VideoTile from 'components/Home/HomeGridVideoBar/VideoTile'; +import GridTile from 'components/shared/Grid/GridTile'; +import Button from 'components/ui/Button'; +import Svg from 'components/ui/Svg'; +import useVimeoVideos from 'hooks/queries/useVimeoVideos'; +import useSettingsStore from 'store/settings/store'; +import { cross } from 'svg/misc'; + +import styles from './HomeGridVideoBar.module.scss'; +import HomeGridVideoBarProps from './types'; + +const HomeGridVideoBar: FC = ({ className }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'components.home.homeGridVideoBar', + }); + const { data } = useVimeoVideos(); + const constraintsRef = useRef(null); + + const { setShowHelpVideos } = useSettingsStore(state => ({ + setShowHelpVideos: state.setShowHelpVideos, + })); + + return ( + } + onClick={() => setShowHelpVideos(false)} + variant="iconOnly" + /> + } + > +
+ + {data?.map(({ name, player_embed_url, user }) => ( + + ))} + +
+
+ ); +}; + +export default HomeGridVideoBar; diff --git a/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.module.scss b/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.module.scss new file mode 100644 index 0000000000..b3c92bd6b7 --- /dev/null +++ b/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.module.scss @@ -0,0 +1,62 @@ +.root { + position: relative; + width: 28rem; + transition: opacity $transition-time-1; + opacity: 0.5; + + &.isInView { + opacity: 1; + } + + .video, + .videoOverlay { + height: 16.8rem; + width: 100%; + border: none; + border-radius: $border-radius-08 $border-radius-08 0 0; + } + + .videoOverlay { + top: 0; + position: absolute; + background-color: $color-octant-grey2; + opacity: 0.3; + + &.isVideoPlaying { + cursor: pointer; + opacity: 0; + } + } + + .playButton { + position: absolute; + top: 6.7rem; + left: 50%; + transform: translate(-50%); + cursor: pointer; + } + + .info { + width: 100%; + background: $color-octant-grey8; + font-weight: $font-weight-bold; + line-height: 1.8rem; + padding: 1rem 2.4rem; + margin-top: -0.4rem; + border-radius: 0 0 $border-radius-08 $border-radius-08; + text-align: left; + + .title { + font-size: $font-size-14; + color: $color-octant-dark; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + .subtitle { + font-size: $font-size-12; + color: $color-octant-grey5; + } + } +} diff --git a/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.tsx b/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.tsx new file mode 100644 index 0000000000..6ac8159fc2 --- /dev/null +++ b/client/src/components/Home/HomeGridVideoBar/VideoTile/VideoTile.tsx @@ -0,0 +1,50 @@ +import Player from '@vimeo/player'; +import cx from 'classnames'; +import { useInView } from 'framer-motion'; +import React, { FC, useRef } from 'react'; + +import VideoTileProps from './types'; +import styles from './VideoTile.module.scss'; + +const VideoTile: FC = ({ title, author, url }) => { + const ref = useRef(null); + const playerRef = useRef(); + const isInView = useInView(ref, { amount: 'all' }); + + const urlWithOptions = `${url}&dnt=true&muted=true`; + + return ( +
+