From 07f66d05efc64629afc793580fa459c7d2388014 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andrzej=20Zi=C3=B3=C5=82ek?= Date: Thu, 3 Aug 2023 11:57:08 +0000 Subject: [PATCH] FEAT: Epoch 0 GLM claiming --- backend/.env.template | 5 + backend/README.md | 8 +- backend/app/contracts/erc20.py | 19 + backend/app/controllers/glm_claim.py | 33 + backend/app/core/claims.py | 7 + backend/app/core/glm.py | 21 + backend/app/core/user.py | 1 + backend/app/crypto/eip712.py | 20 + backend/app/database/__init__.py | 1 + backend/app/database/claims.py | 39 + backend/app/database/models.py | 10 +- backend/app/database/user.py | 10 +- backend/app/exceptions.py | 16 + backend/app/extensions.py | 1 - backend/app/infrastructure/apscheduler.py | 11 + backend/app/infrastructure/routes/__init__.py | 1 + .../app/infrastructure/routes/allocations.py | 92 +-- backend/app/infrastructure/routes/deposits.py | 51 +- backend/app/infrastructure/routes/docs.py | 29 +- .../app/infrastructure/routes/glm_claim.py | 93 +++ backend/app/infrastructure/routes/rewards.py | 192 +++-- .../app/infrastructure/routes/snapshots.py | 59 +- .../app/infrastructure/routes/withdrawals.py | 40 +- backend/app/settings.py | 10 + .../{306a56af8932_.py => 16ed3f1d5233_.py} | 10 +- backend/migrations/versions/bd79c598c867_.py | 52 ++ ..._add_addresses_eligible_for_epoch_zero_.py | 685 ++++++++++++++++++ backend/migrations/versions/f9b48943e363_.py | 44 ++ backend/test_html/glm-claim.html | 87 +++ .../index.html => test_html/websocket.html} | 0 backend/tests/database/__init__.py | 0 ..._allocations.py => test_allocations_db.py} | 0 backend/tests/test_claims_db.py | 32 + backend/tests/test_eip712.py | 18 +- backend/tests/test_glm_claim.py | 70 ++ ci/argocd/templates/octant-application.yaml | 6 + client/nix/sources.json | 14 - client/nix/sources.nix | 176 ----- client/shell.nix | 17 - client/src/api/calls/glmClaim.ts | 18 + client/src/api/errorMessages/index.ts | 10 +- client/src/api/errorMessages/types.ts | 4 + client/src/api/queryKeys/index.ts | 2 + client/src/api/queryKeys/types.ts | 2 + .../ButtonClaimGlm/ButtonGlmClaim.tsx | 25 + .../dedicated/ButtonClaimGlm/types.ts | 6 + .../ModalOnboarding.module.scss | 17 + .../ModalOnboarding/ModalOnboarding.tsx | 77 +- .../dedicated/ModalOnboarding/utils.tsx | 54 ++ client/src/hooks/events/useAllocate.ts | 3 +- client/src/hooks/mutations/useGlmClaim.ts | 39 + client/src/hooks/queries/useGlmClaimCheck.ts | 32 + client/src/locales/en/translation.json | 9 + client/src/utils/getNumberWithSpaces.ts | 4 +- client/synpress.config.ts | 1 + flake.lock | 61 ++ flake.nix | 39 + nix/sources.json | 26 - nix/sources.nix | 176 ----- subgraph/entrypoint.sh | 4 +- subgraph/src/epochs.ts | 35 - subgraph/src/vault.ts | 17 - subgraph/subgraph.yaml | 42 +- subgraph/tests/epochs.test.ts | 57 -- subgraph/tests/utils.ts | 18 - subgraph/tests/vault.test.ts | 62 -- 66 files changed, 1911 insertions(+), 909 deletions(-) create mode 100644 backend/app/controllers/glm_claim.py create mode 100644 backend/app/core/claims.py create mode 100644 backend/app/database/claims.py create mode 100644 backend/app/infrastructure/routes/glm_claim.py rename backend/migrations/versions/{306a56af8932_.py => 16ed3f1d5233_.py} (85%) create mode 100644 backend/migrations/versions/bd79c598c867_.py create mode 100644 backend/migrations/versions/c4b0243c24d6_add_addresses_eligible_for_epoch_zero_.py create mode 100644 backend/migrations/versions/f9b48943e363_.py create mode 100644 backend/test_html/glm-claim.html rename backend/{websocket_test/index.html => test_html/websocket.html} (100%) delete mode 100644 backend/tests/database/__init__.py rename backend/tests/{database/test_allocations.py => test_allocations_db.py} (100%) create mode 100644 backend/tests/test_claims_db.py create mode 100644 backend/tests/test_glm_claim.py delete mode 100644 client/nix/sources.json delete mode 100644 client/nix/sources.nix delete mode 100644 client/shell.nix create mode 100644 client/src/api/calls/glmClaim.ts create mode 100644 client/src/components/dedicated/ButtonClaimGlm/ButtonGlmClaim.tsx create mode 100644 client/src/components/dedicated/ButtonClaimGlm/types.ts create mode 100644 client/src/components/dedicated/ModalOnboarding/utils.tsx create mode 100644 client/src/hooks/mutations/useGlmClaim.ts create mode 100644 client/src/hooks/queries/useGlmClaimCheck.ts create mode 100644 flake.lock create mode 100644 flake.nix delete mode 100644 nix/sources.json delete mode 100644 nix/sources.nix delete mode 100644 subgraph/src/epochs.ts delete mode 100644 subgraph/src/vault.ts delete mode 100644 subgraph/tests/epochs.test.ts delete mode 100644 subgraph/tests/vault.test.ts diff --git a/backend/.env.template b/backend/.env.template index 9fca235003..b78447d308 100644 --- a/backend/.env.template +++ b/backend/.env.template @@ -22,3 +22,8 @@ SCHEDULER_ENABLED= SCHEDULER_API_ENABLED= TESTNET_MULTISIG_PRIVATE_KEY= VAULT_CONFIRM_WITHDRAWALS_ENABLED= + +GLM_CLAIM_ENABLED= +GLM_SENDER_ADDRESS= +GLM_SENDER_PRIVATE_KEY= +GLM_SENDER_NONCE= diff --git a/backend/README.md b/backend/README.md index 825a0258c5..c85d14d5f8 100644 --- a/backend/README.md +++ b/backend/README.md @@ -31,7 +31,8 @@ poetry install --no-interaction --no-ansi -v --with prod --without dev poetry shell ``` -Run the following commands to create your app's +Run the following commands to create your a +pp's database tables and perform the initial migration ```bash @@ -96,3 +97,8 @@ http://localhost:5000/docs/chain-info - `TESTNET_MULTISIG_PRIVATE_KEY` - Multisig private key, which is allowed to send transactions to `Vault.sol`. Needed for automatic withdrawals confirmations for test purposes. - `VAULT_CONFIRM_WITHDRAWALS_ENABLED` - Confirming withdrawals requires sending a merkle root to `Vault.sol`. For test purposes you can enable sending this value automatically by setting this var to `True`. In order for it to work scheduler must be enabled as well + +- `GLM_CLAIM_ENABLED` - Set it to `True` if you want to enable GLM claiming feature. It requires also to enable scheduler. +- `GLM_SENDER_ADDRESS` - Address, from which GLMs will be sent. +- `GLM_SENDER_PRIVATE_KEY` - Private key corresponding to `GLM_SENDER_ADDRESS` +- `GLM_SENDER_NONCE` - Current nonce of the `GLM_SENDER_ADDRESS` diff --git a/backend/app/contracts/erc20.py b/backend/app/contracts/erc20.py index 2c0cb73de4..6978648f0c 100644 --- a/backend/app/contracts/erc20.py +++ b/backend/app/contracts/erc20.py @@ -18,6 +18,16 @@ "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", + }, ] @@ -32,6 +42,15 @@ def balance_of(self, address): checksum_address = to_checksum_address(address) return self.contract.functions.balanceOf(checksum_address).call() + def transfer(self, to_address, nonce): + transaction = self.contract.functions.transfer( + to_address, config.GLM_WITHDRAWAL_AMOUNT + ).build_transaction({"from": config.GLM_SENDER_ADDRESS, "nonce": nonce}) + signed_tx = w3.eth.account.sign_transaction( + transaction, config.GLM_SENDER_PRIVATE_KEY + ) + return w3.eth.send_raw_transaction(signed_tx.rawTransaction) + glm = ERC20(config.GLM_CONTRACT_ADDRESS) gnt = ERC20(config.GNT_CONTRACT_ADDRESS) diff --git a/backend/app/controllers/glm_claim.py b/backend/app/controllers/glm_claim.py new file mode 100644 index 0000000000..f7f3ff610a --- /dev/null +++ b/backend/app/controllers/glm_claim.py @@ -0,0 +1,33 @@ +from flask import current_app as app + +from app import database +from app.core import claims as claims_core +from app.crypto.eip712 import recover_address, build_claim_glm_eip712_data +from app.database.models import EpochZeroClaim +from app.exceptions import GlmClaimed, NotEligibleToClaimGLM +from app.extensions import db + + +def claim(signature: str): + eip712_data = build_claim_glm_eip712_data() + user_address = recover_address(eip712_data, signature) + nonce = claims_core.get_next_claim_nonce() + app.logger.info(f"User: {user_address} is claiming GLMs with a nonce: {nonce}") + + user_claim = check(user_address) + + user_claim.claimed = True + user_claim.claim_nonce = nonce + db.session.commit() + + +def check(address: str) -> EpochZeroClaim: + user_claim = database.claims.get_by_address(address) + + if user_claim is None: + raise NotEligibleToClaimGLM(address) + + if user_claim.claimed: + raise GlmClaimed(address) + + return user_claim diff --git a/backend/app/core/claims.py b/backend/app/core/claims.py new file mode 100644 index 0000000000..c0133c1672 --- /dev/null +++ b/backend/app/core/claims.py @@ -0,0 +1,7 @@ +from app import database +from app.settings import config + + +def get_next_claim_nonce() -> int: + last_nonce = database.claims.get_highest_claim_nonce() + return config.GLM_SENDER_NONCE if last_nonce is None else last_nonce + 1 diff --git a/backend/app/core/glm.py b/backend/app/core/glm.py index 36b4456d0a..54b9ef1ae9 100644 --- a/backend/app/core/glm.py +++ b/backend/app/core/glm.py @@ -1,5 +1,10 @@ +from app import database from app.constants import BURN_ADDRESS from app.contracts.erc20 import glm, gnt +from flask import current_app as app + +from app.crypto.account import Account +from app.settings import config def get_current_glm_supply() -> int: @@ -9,3 +14,19 @@ def get_current_glm_supply() -> int: - glm.balance_of(BURN_ADDRESS) - gnt.balance_of(BURN_ADDRESS) ) + + +def transfer_claimed(): + account = Account.from_key(config.GLM_SENDER_PRIVATE_KEY) + claims = database.claims.get_by_claimed_true_and_nonce_gte(account.nonce) + for claim in claims: + app.logger.debug(f"Transferring GLM to user: {claim.address}") + try: + tx_hash = glm.transfer(claim.address, claim.claim_nonce) + app.logger.info( + f"GLM transferred to user: {claim.address}, tx: {tx_hash.hex()}" + ) + except Exception as e: + if "nonce too low" in str(e).lower(): + continue + app.logger.error(f"Cannot transfer GLM: {e}") diff --git a/backend/app/core/user.py b/backend/app/core/user.py index de931cf2a6..8944e7d42d 100644 --- a/backend/app/core/user.py +++ b/backend/app/core/user.py @@ -3,6 +3,7 @@ from app import database from app.core.common import AccountFunds +from app.settings import config from app.crypto.terms_and_conditions_consent import verify_signed_message from app.exceptions import DuplicateConsent, InvalidSignature diff --git a/backend/app/crypto/eip712.py b/backend/app/crypto/eip712.py index d91f2a66ca..7724282e12 100644 --- a/backend/app/crypto/eip712.py +++ b/backend/app/crypto/eip712.py @@ -44,6 +44,26 @@ def build_allocations_eip712_data(message: dict) -> dict: } +def build_claim_glm_eip712_data() -> dict: + claim_glm_types = { + "EIP712Domain": [ + {"name": "name", "type": "string"}, + {"name": "version", "type": "string"}, + {"name": "chainId", "type": "uint256"}, + ], + "ClaimGLMPayload": [ + {"name": "msg", "type": "string"}, + ], + } + + return { + "types": claim_glm_types, + "domain": domain, + "primaryType": "ClaimGLMPayload", + "message": {"msg": f"Claim {int(config.GLM_WITHDRAWAL_AMOUNT / 1e18)} GLMs"}, + } + + def sign(account: Union[Account, LocalAccount], data: dict) -> str: """ Signs the provided message with w3.eth.account following EIP-712 structure diff --git a/backend/app/database/__init__.py b/backend/app/database/__init__.py index 751547636b..b58e8a45ff 100644 --- a/backend/app/database/__init__.py +++ b/backend/app/database/__init__.py @@ -5,5 +5,6 @@ finalized_epoch_snapshot, deposits, rewards, + claims, user_consents, ) diff --git a/backend/app/database/claims.py b/backend/app/database/claims.py new file mode 100644 index 0000000000..e68cdc768e --- /dev/null +++ b/backend/app/database/claims.py @@ -0,0 +1,39 @@ +from typing import List + +from eth_utils import to_checksum_address +from sqlalchemy import func + +from app.database.models import EpochZeroClaim +from app.extensions import db + + +def get_all() -> List[EpochZeroClaim]: + return EpochZeroClaim.query.all() + + +def get_by_address(user_address: str) -> EpochZeroClaim: + return EpochZeroClaim.query.filter_by( + address=to_checksum_address(user_address) + ).first() + + +def add_claim(user_address: str) -> EpochZeroClaim: + claim = EpochZeroClaim(address=to_checksum_address(user_address)) + db.session.add(claim) + + return claim + + +def get_by_claimed_true_and_nonce_gte(nonce: int = 0) -> List[EpochZeroClaim]: + return ( + EpochZeroClaim.query.filter( + EpochZeroClaim.claimed == True, + EpochZeroClaim.claim_nonce >= nonce, + ) + .order_by(EpochZeroClaim.claim_nonce.asc()) + .all() + ) + + +def get_highest_claim_nonce() -> int: + return db.session.query(func.max(EpochZeroClaim.claim_nonce)).scalar() diff --git a/backend/app/database/models.py b/backend/app/database/models.py index aefb3e7f13..058a57046a 100644 --- a/backend/app/database/models.py +++ b/backend/app/database/models.py @@ -18,7 +18,7 @@ class User(BaseModel): id = Column(db.Integer, primary_key=True) address = Column(db.String(42), unique=True, nullable=False) - nonce = Column(db.Integer, nullable=False, default=0) + allocation_nonce = Column(db.Integer, nullable=False, default=0) class UserConsents(BaseModel): @@ -82,3 +82,11 @@ class Reward(BaseModel): epoch = Column(db.Integer, nullable=False) address = Column(db.String(42), nullable=False) amount = Column(db.String, nullable=False) + + +class EpochZeroClaim(BaseModel): + __tablename__ = "epoch_zero_claims" + + address = Column(db.String(42), primary_key=True, nullable=False) + claimed = Column(db.Boolean, default=False) + claim_nonce = db.Column(db.Integer(), unique=True, nullable=True) diff --git a/backend/app/database/user.py b/backend/app/database/user.py index d83ffc0e96..ac1ab6d8be 100644 --- a/backend/app/database/user.py +++ b/backend/app/database/user.py @@ -1,25 +1,27 @@ +from typing import List + from eth_utils import to_checksum_address from app.database.models import User from app.extensions import db -def get_all(): +def get_all() -> List[User]: return User.query.all() -def get_by_address(user_address): +def get_by_address(user_address: str) -> User: return User.query.filter_by(address=to_checksum_address(user_address)).first() -def add_user(user_address): +def add_user(user_address: str) -> User: user = User(address=to_checksum_address(user_address)) db.session.add(user) return user -def get_or_add_user(user_address): +def get_or_add_user(user_address: str) -> User: user = get_by_address(user_address) if not user: user = add_user(user_address) diff --git a/backend/app/exceptions.py b/backend/app/exceptions.py index 10aabe9489..cd03dead56 100644 --- a/backend/app/exceptions.py +++ b/backend/app/exceptions.py @@ -101,6 +101,22 @@ def __init__(self, address: str): super().__init__(self.description.format(address), self.code) +class NotEligibleToClaimGLM(OctantException): + code = 403 + description = "User with address: {} is not eligible to claim GLMs." + + def __init__(self, user_address: str): + super().__init__(self.description.format(user_address), self.code) + + +class GlmClaimed(OctantException): + code = 400 + description = "User with address: {} has already claimed GLMs." + + def __init__(self, user_address: str): + super().__init__(self.description.format(user_address), self.code) + + class InvalidSignature(OctantException): code = 400 description = "Given signature {} is invalid or does not belong to {}" diff --git a/backend/app/extensions.py b/backend/app/extensions.py index e3c8d44dff..cca3e15639 100644 --- a/backend/app/extensions.py +++ b/backend/app/extensions.py @@ -22,7 +22,6 @@ cors = CORS() scheduler = APScheduler() - # Other extensions graphql_client = Client() w3 = Web3() diff --git a/backend/app/infrastructure/apscheduler.py b/backend/app/infrastructure/apscheduler.py index f2f4c4758e..dc18273c6e 100644 --- a/backend/app/infrastructure/apscheduler.py +++ b/backend/app/infrastructure/apscheduler.py @@ -2,6 +2,7 @@ from app.core import vault from app.extensions import scheduler +from app.core import glm @scheduler.task( @@ -16,3 +17,13 @@ def vault_confirm_withdrawals(): if app.config["VAULT_CONFIRM_WITHDRAWALS_ENABLED"]: app.logger.debug("Confirming withdrawals in Vault contract...") vault.confirm_withdrawals() + + +@scheduler.task( + "interval", id="glm-transfer", seconds=15, misfire_grace_time=900, max_instances=1 +) +def glm_transfer(): + with scheduler.app.app_context(): + if app.config["GLM_CLAIM_ENABLED"]: + app.logger.debug("Transferring claimed GLMs...") + glm.transfer_claimed() diff --git a/backend/app/infrastructure/routes/__init__.py b/backend/app/infrastructure/routes/__init__.py index efe9ca8f26..fa13a5c361 100644 --- a/backend/app/infrastructure/routes/__init__.py +++ b/backend/app/infrastructure/routes/__init__.py @@ -11,6 +11,7 @@ deposits, withdrawals, allocations, + glm_claim, epochs, user, ) diff --git a/backend/app/infrastructure/routes/allocations.py b/backend/app/infrastructure/routes/allocations.py index 55dacdb642..a0672c58e2 100644 --- a/backend/app/infrastructure/routes/allocations.py +++ b/backend/app/infrastructure/routes/allocations.py @@ -4,11 +4,11 @@ from app.controllers import allocations from app.extensions import api +from app.settings import config ns = Namespace("allocations", description="Octant allocations") api.add_namespace(ns) - user_allocations_model = api.model( "UserAllocations", { @@ -32,7 +32,6 @@ }, ) - proposal_donors_model = api.model( "ProposalDonors", { @@ -47,50 +46,51 @@ }, ) +if config.EPOCH_2_FEATURES_ENABLED: -@ns.route("/user//epoch/") -@ns.doc( - description="Returns user's latest allocation in a particular epoch", - params={ - "user_address": "User ethereum address in hexadecimal format (case-insensitive, prefixed with 0x)", - "epoch": "Epoch number", - }, -) -class UserAllocations(Resource): - @ns.marshal_with(user_allocations_model) - @ns.response(200, "User allocations successfully retrieved") - def get(self, user_address: str, epoch: int): - return [ - dataclasses.asdict(w) - for w in allocations.get_all_by_user_and_epoch(user_address, epoch) - ] + @ns.route("/user//epoch/") + @ns.doc( + description="Returns user's latest allocation in a particular epoch", + params={ + "user_address": "User ethereum address in hexadecimal format (case-insensitive, prefixed with 0x)", + "epoch": "Epoch number", + }, + ) + class UserAllocations(Resource): + @ns.marshal_with(user_allocations_model) + @ns.response(200, "User allocations successfully retrieved") + def get(self, user_address: str, epoch: int): + return [ + dataclasses.asdict(w) + for w in allocations.get_all_by_user_and_epoch(user_address, epoch) + ] + @ns.route("/users/sum") + @ns.doc( + description="Returns user's allocations sum", + ) + class UserAllocationsSum(Resource): + @ns.marshal_with(user_allocations_sum_model) + @ns.response(200, "User allocations sum successfully retrieved") + def get(self): + allocations_sum = allocations.get_sum_by_epoch() + return {"amount": str(allocations_sum)} -@ns.route("/users/sum") -@ns.doc( - description="Returns user's allocations sum", -) -class UserAllocationsSum(Resource): - @ns.marshal_with(user_allocations_sum_model) - @ns.response(200, "User allocations sum successfully retrieved") - def get(self): - allocations_sum = allocations.get_sum_by_epoch() - return {"amount": str(allocations_sum)} - - -@ns.route("/proposal//epoch/") -@ns.doc( - description="Returns list of donors for given proposal in particular epoch", - params={ - "proposal_address": "Proposal ethereum address in hexadecimal format (case-insensitive, prefixed with 0x)", - "epoch": "Epoch number", - }, -) -class ProposalDonors(Resource): - @ns.marshal_with(proposal_donors_model) - @ns.response(200, "Returns list of proposal donors") - def get(self, proposal_address: str, epoch: int): - return [ - dataclasses.asdict(w) - for w in allocations.get_all_by_proposal_and_epoch(proposal_address, epoch) - ] + @ns.route("/proposal//epoch/") + @ns.doc( + description="Returns list of donors for given proposal in particular epoch", + params={ + "proposal_address": "Proposal ethereum address in hexadecimal format (case-insensitive, prefixed with 0x)", + "epoch": "Epoch number", + }, + ) + class ProposalDonors(Resource): + @ns.marshal_with(proposal_donors_model) + @ns.response(200, "Returns list of proposal donors") + def get(self, proposal_address: str, epoch: int): + return [ + dataclasses.asdict(w) + for w in allocations.get_all_by_proposal_and_epoch( + proposal_address, epoch + ) + ] diff --git a/backend/app/infrastructure/routes/deposits.py b/backend/app/infrastructure/routes/deposits.py index a968eaf1a0..3e05b0f939 100644 --- a/backend/app/infrastructure/routes/deposits.py +++ b/backend/app/infrastructure/routes/deposits.py @@ -4,6 +4,7 @@ from app.database import pending_epoch_snapshot import app.controllers.deposits as deposits_controller from app.extensions import api +from app.settings import config ns = Namespace("deposits", description="Octant deposits") api.add_namespace(ns) @@ -35,33 +36,33 @@ }, ) +if config.EPOCH_2_FEATURES_ENABLED: -@ns.route("//total_effective") -@ns.doc( - description="Returns value of total effective deposits made by the end of an epoch. Latest data and data for any given point in time from the past is available in the Subgraph.", - params={"epoch": "Epoch number"}, -) -class TotalEffectiveDeposit(Resource): - @ns.marshal_with(total_effective_model) - @ns.response(200, "Epoch total effective deposit successfully retrieved") - def get(self, epoch): - total_effective_deposit = pending_epoch_snapshot.get_by_epoch_num( - epoch - ).total_effective_deposit - return {"totalEffective": total_effective_deposit} - + @ns.route("//total_effective") + @ns.doc( + description="Returns value of total effective deposits made by the end of an epoch. Latest data and data for any given point in time from the past is available in the Subgraph.", + params={"epoch": "Epoch number"}, + ) + class TotalEffectiveDeposit(Resource): + @ns.marshal_with(total_effective_model) + @ns.response(200, "Epoch total effective deposit successfully retrieved") + def get(self, epoch): + total_effective_deposit = pending_epoch_snapshot.get_by_epoch_num( + epoch + ).total_effective_deposit + return {"totalEffective": total_effective_deposit} -@ns.route("//locked_ratio") -@ns.doc( - description="Returns locked ratio of total effective deposits made by the end of an epoch. Latest data and data for any given point in time from the past is available in the Subgraph.", - params={"epoch": "Epoch number"}, -) -class LockedRatio(Resource): - @ns.marshal_with(locked_ratio_model) - @ns.response(200, "Epoch locked ratio successfully retrieved") - def get(self, epoch): - locked_ratio = pending_epoch_snapshot.get_by_epoch_num(epoch).locked_ratio - return {"lockedRatio": locked_ratio} + @ns.route("//locked_ratio") + @ns.doc( + description="Returns locked ratio of total effective deposits made by the end of an epoch. Latest data and data for any given point in time from the past is available in the Subgraph.", + params={"epoch": "Epoch number"}, + ) + class LockedRatio(Resource): + @ns.marshal_with(locked_ratio_model) + @ns.response(200, "Epoch locked ratio successfully retrieved") + def get(self, epoch): + locked_ratio = pending_epoch_snapshot.get_by_epoch_num(epoch).locked_ratio + return {"lockedRatio": locked_ratio} @ns.route("/users//") diff --git a/backend/app/infrastructure/routes/docs.py b/backend/app/infrastructure/routes/docs.py index 0e6fafe6f0..a071353fe1 100644 --- a/backend/app/infrastructure/routes/docs.py +++ b/backend/app/infrastructure/routes/docs.py @@ -39,21 +39,24 @@ ) -@ns.route("/websockets-api") -@ns.doc(description="The documentation for websockets can be found under this path") -class WebsocketsDocs(Resource): - def get(self): - headers = {"Content-Type": "text/html"} - return make_response(render_template("websockets-api-docs.html"), 200, headers) +if config.EPOCH_2_FEATURES_ENABLED: + @ns.route("/websockets-api") + @ns.doc(description="The documentation for websockets can be found under this path") + class WebsocketsDocs(Resource): + def get(self): + headers = {"Content-Type": "text/html"} + return make_response( + render_template("websockets-api-docs.html"), 200, headers + ) -@ns.route("/websockets-api.yaml") -class WebsocketsDocsYaml(Resource): - def get(self): - docs_folder = f"{settings.config.PROJECT_ROOT}/docs" - return send_from_directory( - docs_folder, "websockets-api.yaml", mimetype="text/plain" - ) + @ns.route("/websockets-api.yaml") + class WebsocketsDocsYaml(Resource): + def get(self): + docs_folder = f"{settings.config.PROJECT_ROOT}/docs" + return send_from_directory( + docs_folder, "websockets-api.yaml", mimetype="text/plain" + ) @ns.route("/chain-info") diff --git a/backend/app/infrastructure/routes/glm_claim.py b/backend/app/infrastructure/routes/glm_claim.py new file mode 100644 index 0000000000..851e74e0ca --- /dev/null +++ b/backend/app/infrastructure/routes/glm_claim.py @@ -0,0 +1,93 @@ +from flask_restx import Resource, Namespace, fields + +from app.controllers import glm_claim +from app.exceptions import GlmClaimed +from app.extensions import api +from app.settings import config + +ns = Namespace("glm", description="Operations related to GLM smart contract") +api.add_namespace(ns) + +claim_glm_request = ns.model( + "ClaimGLMRequest", + { + "signature": fields.String( + required=True, + description='EIP-712 signature of a payload with the following message: {"msg": "Claim \ GLMs"} as a hexadecimal string', + ) + }, +) + +check_claim_model = api.model( + "CheckClaim", + { + "address": fields.String( + required=True, + description="Address of the user", + ), + "claimable": fields.String( + required=True, + description="Amount of GLMs that can be claimed, in WEI", + ), + }, +) + + +@ns.route("/claim") +@ns.doc( + description="Claim GLMs from epoch 0. Only eligible accounts are able to claim." +) +@ns.response(201, "GLMs claimed successfully") +@ns.response( + 404, + "User address not found in db - user is not eligible to claim GLMs", +) +@ns.response( + 403, + "Claiming GLMs is disabled", +) +@ns.response( + 400, + "GLMs have been already claimed", +) +class Claim(Resource): + @ns.expect(claim_glm_request) + def post(self): + if not config.GLM_CLAIM_ENABLED: + return {"message": "GLM claiming is disabled"}, 403 + glm_claim.claim(ns.payload["signature"]) + return {}, 201 + + +@ns.route("/claim//check") +@ns.doc( + description="Check if account is eligible are able to claim GLMs from epoch 0. Return number of GLMs in wei" +) +@ns.response(200, "Account is eligible to claim GLMs") +@ns.response( + 404, + "User address not found in db - user is not eligible to claim GLMs", +) +@ns.response( + 403, + "Claiming GLMs is disabled", +) +@ns.response( + 400, + "GLMs have been already claimed", +) +class CheckClaim(Resource): + @ns.marshal_with(check_claim_model) + def get(self, user_address: str): + if not config.GLM_CLAIM_ENABLED: + return {"message": "GLM claiming is disabled"}, 403 + claimable = str(config.GLM_WITHDRAWAL_AMOUNT) + try: + glm_claim.check(user_address) + except GlmClaimed: + claimable = "0" + + return { + "claimable": claimable, + "address": user_address, + }, 200 diff --git a/backend/app/infrastructure/routes/rewards.py b/backend/app/infrastructure/routes/rewards.py index fab59f7aa3..46b6fe4bae 100644 --- a/backend/app/infrastructure/routes/rewards.py +++ b/backend/app/infrastructure/routes/rewards.py @@ -6,6 +6,7 @@ from app.controllers import rewards from app.core import user from app.extensions import api +from app.settings import config ns = Namespace("rewards", description="Octant rewards") api.add_namespace(ns) @@ -98,100 +99,97 @@ }, ) - -@ns.route("/budget//epoch/") -@ns.doc( - description="Returns user's rewards budget available to allocate for given epoch", - params={ - "user_address": "User ethereum address in hexadecimal format (case-insensitive, prefixed " - "with 0x)", - "epoch": "Epoch number", - }, -) -class UserBudget(Resource): - @ns.marshal_with(user_budget_model) - @ns.response(200, "Budget successfully retrieved") - def get(self, user_address, epoch): - current_app.logger.info( - f"Getting budget for user: {user_address} in epoch {epoch}" - ) - budget = user.get_budget(user_address, epoch) - return {"budget": budget} - - -@ns.route("/threshold/") -@ns.doc( - description="Returns allocation threshold for the projects to be eligible for rewards", - params={ - "epoch": "Epoch number", - }, -) -@ns.response(200, "Returns allocation threshold value as uint256") -class Threshold(Resource): - @ns.marshal_with(threshold_model) - @ns.response(200, "Threshold successfully retrieved") - def get(self, epoch): - current_app.logger.info(f"Requested threshold for epoch: {epoch}") - threshold = rewards.get_allocation_threshold(epoch) - return {"threshold": threshold} - - -@ns.doc( - description="Returns proposals with matched rewards for a given epoch", - params={ - "epoch": "Epoch number", - }, -) -@ns.response( - 200, - "", -) -@ns.response( - 400, - "Invalid epoch number given. Has the allocation window " - "for the given epoch started already?", -) -@ns.route("/proposals/epoch/") -class Proposals(Resource): - @ns.marshal_with(proposal_model) - def get(self, epoch): - current_app.logger.info(f"Requested proposal rewards for: {epoch}") - return [ - dataclasses.asdict(proposal) - for proposal in rewards.get_proposals_rewards(epoch) - ] - - -@ns.doc( - description="Returns total of allocated and budget for matched rewards for a given epoch", - params={ - "epoch": "Epoch number", - }, -) -@ns.response(200, "") -@ns.route("/budget/epoch/") -class Budget(Resource): - @ns.marshal_with(budget_model) - def get(self, epoch): - budget = rewards.get_rewards_budget(epoch) - return dataclasses.asdict(budget) - - -@ns.doc( - description="Returns merkle tree leaves with rewards for a given epoch", - params={ - "epoch": "Epoch number", - }, -) -@ns.response( - 200, - "", -) -@ns.response(400, "Invalid epoch number given") -@ns.route("/merkle_tree/") -class RewardsMerkleTree(Resource): - @ns.marshal_with(epoch_rewards_merkle_tree_model) - def get(self, epoch: int): - current_app.logger.info(f"Requested merkle tree leaves for: {epoch}") - merkle_tree_leaves = rewards.get_rewards_merkle_tree(epoch) - return merkle_tree_leaves.to_dict() +if config.EPOCH_2_FEATURES_ENABLED: + + @ns.route("/budget//epoch/") + @ns.doc( + description="Returns user's rewards budget available to allocate for given epoch", + params={ + "user_address": "User ethereum address in hexadecimal format (case-insensitive, prefixed " + "with 0x)", + "epoch": "Epoch number", + }, + ) + class UserBudget(Resource): + @ns.marshal_with(user_budget_model) + @ns.response(200, "Budget successfully retrieved") + def get(self, user_address, epoch): + current_app.logger.info( + f"Getting budget for user: {user_address} in epoch {epoch}" + ) + budget = user.get_budget(user_address, epoch) + return {"budget": budget} + + @ns.route("/threshold/") + @ns.doc( + description="Returns allocation threshold for the projects to be eligible for rewards", + params={ + "epoch": "Epoch number", + }, + ) + @ns.response(200, "Returns allocation threshold value as uint256") + class Threshold(Resource): + @ns.marshal_with(threshold_model) + @ns.response(200, "Threshold successfully retrieved") + def get(self, epoch): + current_app.logger.info(f"Requested threshold for epoch: {epoch}") + threshold = rewards.get_allocation_threshold(epoch) + return {"threshold": threshold} + + @ns.doc( + description="Returns proposals with matched rewards for a given epoch", + params={ + "epoch": "Epoch number", + }, + ) + @ns.response( + 200, + "", + ) + @ns.response( + 400, + "Invalid epoch number given. Has the allocation window " + "for the given epoch started already?", + ) + @ns.route("/proposals/epoch/") + class Proposals(Resource): + @ns.marshal_with(proposal_model) + def get(self, epoch): + current_app.logger.info(f"Requested proposal rewards for: {epoch}") + return [ + dataclasses.asdict(proposal) + for proposal in rewards.get_proposals_rewards(epoch) + ] + + @ns.doc( + description="Returns total of allocated and budget for matched rewards for a given epoch", + params={ + "epoch": "Epoch number", + }, + ) + @ns.response(200, "") + @ns.route("/budget/epoch/") + class Budget(Resource): + @ns.marshal_with(budget_model) + def get(self, epoch): + budget = rewards.get_rewards_budget(epoch) + return dataclasses.asdict(budget) + + @ns.doc( + description="Returns merkle tree leaves with rewards for a given epoch", + params={ + "epoch": "Epoch number", + }, + ) + @ns.response( + 200, + "", + ) + @ns.response(400, "Invalid epoch number given") + @ns.route("/merkle_tree/") + class RewardsMerkleTree(Resource): + @ns.marshal_with(epoch_rewards_merkle_tree_model) + def get(self, epoch: int): + current_app.logger.info(f"Requested merkle tree leaves for: {epoch}") + merkle_tree_leaves = rewards.get_rewards_merkle_tree(epoch) + return merkle_tree_leaves.to_dict() diff --git a/backend/app/infrastructure/routes/snapshots.py b/backend/app/infrastructure/routes/snapshots.py index 2a3296e4e7..87a1508989 100644 --- a/backend/app/infrastructure/routes/snapshots.py +++ b/backend/app/infrastructure/routes/snapshots.py @@ -5,6 +5,7 @@ from app.controllers import snapshots from app.extensions import api +from app.settings import config ns = Namespace("snapshots", description="Database snapshots") api.add_namespace(ns) @@ -27,36 +28,38 @@ }, ) +if config.EPOCH_2_FEATURES_ENABLED: -@ns.route("/pending") -@ns.doc( - description="Take a database snapshot of the recently completed epoch. \ - This endpoint should be executed at the beginning of an epoch to activate \ - a decision window." -) -@ns.response( - 200, "Snapshot could not be created due to an existing snapshot for previous epoch" -) -@ns.response(201, "Snapshot created successfully") -class PendingEpochSnapshot(Resource): - def post(self): - epoch = snapshots.snapshot_pending_epoch() - return ({"epoch": epoch}, 201) if epoch is not None else Response() - + @ns.route("/pending") + @ns.doc( + description="Take a database snapshot of the recently completed epoch. \ + This endpoint should be executed at the beginning of an epoch to activate \ + a decision window." + ) + @ns.response( + 200, + "Snapshot could not be created due to an existing snapshot for previous epoch", + ) + @ns.response(201, "Snapshot created successfully") + class PendingEpochSnapshot(Resource): + def post(self): + epoch = snapshots.snapshot_pending_epoch() + return ({"epoch": epoch}, 201) if epoch is not None else Response() -@ns.route("/finalized") -@ns.doc( - description="Take a database snapshot of the recently completed allocations. \ - This endpoint should be executed at the end of the decision window" -) -@ns.response( - 200, "Snapshot could not be created due to an existing snapshot for previous epoch" -) -@ns.response(201, "Snapshot created successfully") -class FinalizedEpochSnapshot(Resource): - def post(self): - epoch = snapshots.snapshot_finalized_epoch() - return ({"epoch": epoch}, 201) if epoch is not None else Response() + @ns.route("/finalized") + @ns.doc( + description="Take a database snapshot of the recently completed allocations. \ + This endpoint should be executed at the end of the decision window" + ) + @ns.response( + 200, + "Snapshot could not be created due to an existing snapshot for previous epoch", + ) + @ns.response(201, "Snapshot created successfully") + class FinalizedEpochSnapshot(Resource): + def post(self): + epoch = snapshots.snapshot_finalized_epoch() + return ({"epoch": epoch}, 201) if epoch is not None else Response() @ns.route("/status/") diff --git a/backend/app/infrastructure/routes/withdrawals.py b/backend/app/infrastructure/routes/withdrawals.py index bff83c0f78..d83c359e72 100644 --- a/backend/app/infrastructure/routes/withdrawals.py +++ b/backend/app/infrastructure/routes/withdrawals.py @@ -5,6 +5,7 @@ from app.controllers import withdrawals from app.extensions import api +from app.settings import config ns = Namespace("withdrawals", description="Octant withdrawals") api.add_namespace(ns) @@ -28,22 +29,25 @@ }, ) +if config.EPOCH_2_FEATURES_ENABLED: -@ns.doc( - description="Returns a list containing amount and merkle proofs for all not claimed epochs", - params={ - "address": "User or proposal address", - }, -) -@ns.response( - 200, - "", -) -@ns.route("/") -class Withdrawals(Resource): - @ns.marshal_with(withdrawable_rewards_model) - def get(self, address): - current_app.logger.info(f"Requested withdrawable eth for address: {address}") - return [ - dataclasses.asdict(w) for w in withdrawals.get_withdrawable_eth(address) - ] + @ns.doc( + description="Returns a list containing amount and merkle proofs for all not claimed epochs", + params={ + "address": "User or proposal address", + }, + ) + @ns.response( + 200, + "", + ) + @ns.route("/") + class Withdrawals(Resource): + @ns.marshal_with(withdrawable_rewards_model) + def get(self, address): + current_app.logger.info( + f"Requested withdrawable eth for address: {address}" + ) + return [ + dataclasses.asdict(w) for w in withdrawals.get_withdrawable_eth(address) + ] diff --git a/backend/app/settings.py b/backend/app/settings.py index 92c009dcbe..5a50a57c3d 100644 --- a/backend/app/settings.py +++ b/backend/app/settings.py @@ -23,6 +23,7 @@ class Config(object): SUBGRAPH_ENDPOINT = os.getenv("SUBGRAPH_ENDPOINT") WEB3_PROVIDER = Web3.HTTPProvider(os.getenv("ETH_RPC_PROVIDER_URL")) SCHEDULER_ENABLED = _parse_bool(os.getenv("SCHEDULER_ENABLED")) + EPOCH_2_FEATURES_ENABLED = _parse_bool(os.getenv("EPOCH_2_FEATURES_ENABLED")) # Epoch ending dates EPOCH_0_END = int(os.getenv("EPOCH_0_END", 1690848000)) @@ -51,6 +52,15 @@ class Config(object): os.getenv("VAULT_CONFIRM_WITHDRAWALS_ENABLED") ) + # GLM claiming + GLM_CLAIM_ENABLED = _parse_bool(os.getenv("GLM_CLAIM_ENABLED")) + GLM_WITHDRAWAL_AMOUNT = int( + os.getenv("GLM_WITHDRAWAL_AMOUNT", 1000_000000000_000000000) + ) + GLM_SENDER_ADDRESS = os.getenv("GLM_SENDER_ADDRESS") + GLM_SENDER_PRIVATE_KEY = os.getenv("GLM_SENDER_PRIVATE_KEY") + GLM_SENDER_NONCE = int(os.getenv("GLM_SENDER_NONCE", 0)) + class ProdConfig(Config): """Production configuration.""" diff --git a/backend/migrations/versions/306a56af8932_.py b/backend/migrations/versions/16ed3f1d5233_.py similarity index 85% rename from backend/migrations/versions/306a56af8932_.py rename to backend/migrations/versions/16ed3f1d5233_.py index a8d0f95569..00580c5823 100644 --- a/backend/migrations/versions/306a56af8932_.py +++ b/backend/migrations/versions/16ed3f1d5233_.py @@ -1,8 +1,8 @@ """empty message -Revision ID: 306a56af8932 -Revises: a49d3e62bda9 -Create Date: 2023-07-27 09:04:54.184654 +Revision ID: 16ed3f1d5233 +Revises: bd79c598c867 +Create Date: 2023-07-31 13:25:54.696247 """ from alembic import op @@ -10,8 +10,8 @@ # revision identifiers, used by Alembic. -revision = "306a56af8932" -down_revision = "a49d3e62bda9" +revision = "16ed3f1d5233" +down_revision = "bd79c598c867" branch_labels = None depends_on = None diff --git a/backend/migrations/versions/bd79c598c867_.py b/backend/migrations/versions/bd79c598c867_.py new file mode 100644 index 0000000000..014feac241 --- /dev/null +++ b/backend/migrations/versions/bd79c598c867_.py @@ -0,0 +1,52 @@ +"""empty message + +Revision ID: bd79c598c867 +Revises: f9b48943e363 +Create Date: 2023-07-11 13:57:06.271299 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "bd79c598c867" +down_revision = "f9b48943e363" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "epoch_zero_claims", + sa.Column("address", sa.String(length=42), nullable=False), + sa.Column("claimed", sa.Boolean(), nullable=True), + sa.Column("claim_nonce", sa.Integer(), nullable=True), + sa.Column("created_at", sa.TIMESTAMP(), nullable=True), + sa.PrimaryKeyConstraint("address"), + sa.UniqueConstraint("claim_nonce"), + ) + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.drop_constraint( + "glm_withdrawal_nonce_unique_constraint", type_="unique" + ) + batch_op.drop_column("glm_withdrawn") + batch_op.drop_column("glm_withdrawal_nonce") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column( + sa.Column("glm_withdrawal_nonce", sa.INTEGER(), nullable=True) + ) + batch_op.add_column(sa.Column("glm_withdrawn", sa.BOOLEAN(), nullable=True)) + batch_op.create_unique_constraint( + "glm_withdrawal_nonce_unique_constraint", ["glm_withdrawal_nonce"] + ) + + op.drop_table("epoch_zero_claims") + # ### end Alembic commands ### diff --git a/backend/migrations/versions/c4b0243c24d6_add_addresses_eligible_for_epoch_zero_.py b/backend/migrations/versions/c4b0243c24d6_add_addresses_eligible_for_epoch_zero_.py new file mode 100644 index 0000000000..e2b525dbb5 --- /dev/null +++ b/backend/migrations/versions/c4b0243c24d6_add_addresses_eligible_for_epoch_zero_.py @@ -0,0 +1,685 @@ +"""add addresses eligible for epoch zero GLM claim + +Revision ID: c4b0243c24d6 +Revises: 16ed3f1d5233 +Create Date: 2023-08-02 17:55:40.476945 + +""" +from alembic import op + +# revision identifiers, used by Alembic. +revision = 'c4b0243c24d6' +down_revision = '16ed3f1d5233' +branch_labels = None +depends_on = None + +def upgrade(): + op.execute(sqltext()) + + +def downgrade(): + op.execute('DELETE FROM epoch_zero_claims') + + +def sqltext(): + return """ +insert into epoch_zero_claims (address) values +('0x5754aC842D6eaF6a4E29101D46ac25D7C567311E'), +('0x76273DCC41356e5f0c49bB68e525175DC7e83417'), +('0xEDe4f636Bc9ff333b01C26ba8a37D13B82E40214'), +('0x14D92832265eeAFDEF9e526356FEfc90105966c3'), +('0xCE8D52c38d74B77a0aA361c48Fdce6b220A3370e'), +('0xf6B6F07862A02C85628B3A9688beae07fEA9C863'), +('0xcCE9A28b570946123f392Cf1DbfA6D2D5e636a1f'), +('0x3662E728316e5fb2B7B47Dda01c883937BE39Ba6'), +('0x2B13D52dFd33E2eBd13232866fDf96088e77d596'), +('0x4dD6720D2Bb8721A46bdF9a528704164578E03B9'), +('0x60566cb8D8dfb84d0B25A7cd3daE7C1eaDfbDb5F'), +('0x015122A625b45f68E6D795C0Ab99fC7107e4c3B9'), +('0x37341cbb14c5F128A70B149726ad8B2CE6F4C793'), +('0x5FfD23B1B0350debB17A2cB668929aC5f76d0E18'), +('0xa7530577983e2A1bcbd094EdEe034586efC65eC9'), +('0xed8DB37778804A913670d9367aAf4F043AAd938b'), +('0xdd0206010CA82fF22303b58863b3a6f3006C86C4'), +('0x4eFFbA37E8C7e34Fc4241995b3380F2B9033C99f'), +('0xF0193dF47411EAf686dDBd99276572210838BCF3'), +('0x6fE74660D9CCAd62F5CE15657E95453f1c54B6fb'), +('0xDa830d2D83A57Cea255bCfD0Cf89C3e94Abde0FD'), +('0xE04885c3f1419C6E8495C33bDCf5F8387cd88846'), +('0x035E8A0A57f24FD10D447c6cE44524513dd6e09C'), +('0x978eB534b26CB8749D352a2C94EC21e659e4248d'), +('0xd1a5b91957530E1B3e9cfac1543467C60c352F69'), +('0xbd5eb58790C4C8FE959f8Ea9d736DB3619A59300'), +('0xc82a75D564521306e7Ee9eBD530a459292c45Ae7'), +('0x2fEe53687906bA239602eD42CCEee2aC8A4ADa36'), +('0xAFE2b51592b89095A4cFb18da2B5914b528f4c01'), +('0x42B0A3c7F94A9ee4AC3E2242Ec8031f45DF7F589'), +('0x95E301C5AA86F1c9c5c8a04fCcFF916de7cb6Ea3'), +('0x8c57D1Fab10D4B78F49ef5C6ABaEB76a63710f61'), +('0xF72131e357295dDBED1C478096Bb81D90eFf0c19'), +('0x5d1F4F1E99a9d0866Be15d7300b9ac2970E232D4'), +('0x489D69939b4Aca357f630055e351A81F1Fbb7Fb6'), +('0x5d9fbd984B9CeC714a4B14c38Ea83bBC82d06d69'), +('0x3d02003EF41589Be90da67B4b7DaCfC76eed71De'), +('0xCd0D4CDb238Eec15Fcf4ff9d13d5a59051E507D7'), +('0x9f45Fa7845889a3076788e4849b0F5173Cc6a76f'), +('0x432C53218A11bEd08d238Cf84ff547CE4fe933ab'), +('0x9d17Bb55b57b31329CF01aa7017948e398B277bc'), +('0xf6261D145Ec7676DC0E55424B679403F1Ca89640'), +('0xC46c67Bb7E84490D7EbdD0b8ecDaca68Cf3823F4'), +('0xa8EB650d195B8c271d16Bdd80DFa0dC54A0FC27A'), +('0x14d1a955e2467ee8D04d747EeD647c7966E6AC06'), +('0x28ff8e457feF9870B9d1529FE68Fbb95C3181f64'), +('0xf9e1D1e9F22c96752356AdFd377231528c7E851E'), +('0x3b6CCE32ba37fA5689Fab313371Dcf044ECeF536'), +('0xaCE1f1c6c5c89AE3Fc3209ff92e7120fb74445aA'), +('0x0D20dea491abFb7e374877c6E9629161d3Ae4ef4'), +('0x03cddB901b84f7E4D49350FEdF0B764aB6fcfe73'), +('0x78E87757861185Ec5e8C0EF6BF0C69Fa7832df6C'), +('0x69EEae3A39d5365A93081A3D95eE473Ba317Ad81'), +('0x6C9258895FFBE2178b3EdEfE09AF304a1e99bF2F'), +('0x8ef7E5B9755B998c323A445BEB5465067c615785'), +('0x4853F81413eEFf3c53BaEf6e9EfE7383AC6A24Fd'), +('0xbeC48f1cCf82d8e4C983Ee00Ac2eC6B03B81d710'), +('0xCb1b704cE494B73E2F9f67f876A464b507D2c97a'), +('0x3bE70936FCf9D3ECaaD5fb41d24613d33EFe3D2A'), +('0xF04aEBaa5c9e6ab7f2Ae303f76504E00e7ed43cF'), +('0xDf06A761D286C0ae835173236E1f12f1379E4977'), +('0x6C31212a23040998E1D1c157ACe3982aBDBE3154'), +('0x731A2e51ebfAeBacF8477E992CDEB1E8eacf519C'), +('0x25744247A760Dd8c73Eaa81307E1ff827E8BcFa3'), +('0x6C965b656C450259a6D4d95A2E68Fb4319EecBc0'), +('0x0A251dF99A88A20a93876205Fb7f5Faf2E85A481'), +('0x8E097Cf15581e344687127E6247a53663C0e4f71'), +('0x826976d7C600d45FB8287CA1d7c76FC8eb732030'), +('0x25FA68A4c340202737EDBC67fD1a2Ec8DE872dB6'), +('0x172DBab6f5E62A1FE7E2bA5eA1624ADB33e0aa14'), +('0x224aBa5D489675a7bD3CE07786FAda466b46FA0F'), +('0x0442A9aBbc93058a873c371F21CC366338254A88'), +('0x8dA48e5846c06B558970ACd42EDc7Da8799481E4'), +('0xA3aD5CFb4FF4B68e37A338Da200BA441C1850B5b'), +('0xaf246445e50649Bcf89C277dfA0d545a7001b4Ab'), +('0x38a03958C22065E13397B19c817E74e9477D5984'), +('0xf13e477365B0FAa64130DA2FF663aAb20d32d929'), +('0x2AC6A3561a43f06d62602eF9728C2B9eEc393326'), +('0xDd31dB93082a3A71b98D37ba26230f8734Bd63C3'), +('0xb61f64f6F4A76459f2684056deBdf447d8629d32'), +('0x1c0AcCc24e1549125b5b3c14D999D3a496Afbdb1'), +('0xb3313b023e68CdA95d7B625200e1B0fE6335A0c2'), +('0x8FDA1Daa6a674C1726d1896E3054B9a82d123F12'), +('0x0194325BF525Be0D4fBB0856894cEd74Da3B8356'), +('0x9e602c1920443F01Cb100a57A7F894df8Eb42f66'), +('0x3B981fA5dD50237dAb6F96A417A6690B6f20FcC4'), +('0xCA72c93172BA6EfF168E59e7F17C3C7A8FeA9B62'), +('0x9924285ff2207D6e36642B6832A515A6a3aedCAB'), +('0x144c4E5027B69f7798B2B162D924BcAE5c149f15'), +('0x7d547666209755FB833f9B37EebEa38eBF513Abb'), +('0xAD7A185b2456d5AFD85838A50C7d8aCE3aB2f871'), +('0xA8F0048A0d1A04663Ca5010d0bEaC5BCAEeA0eef'), +('0xdDAb0F77a888c97fE8c4297c5dC00c35F41ea7fb'), +('0xF3Ad97364bcCC3eA0582Ede58C363888f8C4ec85'), +('0x1Fcdb5A21145479b2F67c5e1F001Aa8949b341D5'), +('0x841C11b14c428dd591093348B8Afa2652C863988'), +('0xA4369e39e3ED13593Adb0142A1ea5d08AbdF99C4'), +('0x7e4Cab656162C16a2326c7B008C9CEE2f978af5C'), +('0x5d47e5D242a8F66a6286b0a2353868875F5d6068'), +('0x3A497a2a8B35b934Bf271938502Ca24E444E689E'), +('0xEd36bf0b2b17768E782Db2ece6A327055b2f3e9C'), +('0x0418Eae48E1eA7f45f8AD1213d3a7c509010a1F5'), +('0x649165b44A9ae50eAAde055E9ecD88134C8cCD62'), +('0x1A8069522109Cc00AAA6701a29Cc076f71A68C8B'), +('0xE77A8fB60De5F73c610997976E619E4D51d09C98'), +('0x46fF24b3672F8600f4a6B8FcD70Cb50f1FDCB782'), +('0xf45B0fFA221f86f4724237d3D1E1bFaf33670877'), +('0xa279169D570635b8C9D59AeF9Ed0d0E9Ae6EDf34'), +('0xd2BD2d91aE8282Db3dDc6a9ef78420D8A5D14a5C'), +('0xCd86A860FDA7870862dDF1bf4961ec3F920f7ce3'), +('0xb62E762Af637b49Eb4870BCe8fE21bffF189e495'), +('0x9f729294b308f79243285348A7Be3f58ae5ED31A'), +('0xE77ad9c5af60332D24E5531B51A6B7f61D0B8703'), +('0xB22981bA3FE1De2325935c91a3B717168fB86714'), +('0xbA36FbA9BaAB8872Fa0F239fE2c63f09De95B18F'), +('0x3bEcE46eA4cb308b1B8D8f50cB1728D6A4a20808'), +('0x315bd14b67b0cd18dE87F1FBce533723c42b0DAE'), +('0x103D179298a20DD337e911b1e092Fc3B9D54bf61'), +('0x9194b1713A8E07Bc47B90Fb7937E28e09b6cd100'), +('0x47C2149AC0a70b74DD6f5538BF112ADeA54eee93'), +('0x9d406203a93630dD017471D83fef98193e0d61F5'), +('0xc42c77b6B2A2B220b9502F357bBf51334Db3C93f'), +('0x4506568E4733F3062CfA9cdb952Ba7904Fc7d8bb'), +('0x8D247f4Fbbe81429d3D164a5c9Ae0063210edBdC'), +('0xB7BaBe35CE543e2Cf2F615CB1c792a2025feb572'), +('0xcf79C7EaEC5BDC1A9e32D099C5D6BdF67E4cF6e8'), +('0xA73064A00512aa5b6B9119cB4Eb3Fe3229269993'), +('0xbC1500dAafF721f626Ef1a4F1a8C4e333648c5A4'), +('0xc1B5F393b64ff167447f87e6a443d9840f9d0604'), +('0xC6E18DE9930a1B90F339e09c574a4123c3EAcE83'), +('0xdef6174361100a3935b589bf3A6f0C0Bf8272Ea9'), +('0xAC0Daf3eeF18A76B4fd0865864186EB72b683e29'), +('0x23ee51e614cBF138e4cAbA9EC5ed4fF7D27A8596'), +('0x7fC80faD32Ec41fd5CfcC14EeE9C31953b6B4a8B'), +('0x69155E7cA2E688ccDc247f6c4DDF374b3ae77Bd6'), +('0xC6bb80aC6fE401772D1FDeeD497da201241A5bF4'), +('0x841AD0AbAb2D33520ca236A2F5D8b038adDc12BA'), +('0x67243d6c3c3bDc2F59D2f74ba1949a02973a529d'), +('0xbd56EFc637f8Cb7133e304b3f929dF9A6fa35468'), +('0xA21000E7A5A2A2Bd9329428A859f9d7dcE0f0961'), +('0xABF28f8D9adFB2255F4a059e37d3BcE9104969dB'), +('0x14dDf441721297b97a4D5aA3304fb845Bc35Ec56'), +('0x175E0ED70627644c35F310288fC8d6061f75ac76'), +('0xc608df70777C89870E629354B25571f33cB82D56'), +('0x32cEfb2dC869BBfe636f7547CDa43f561Bf88d5A'), +('0x5b655EDa7D101f98934392Cc3610BcB25b633789'), +('0x59072B3a3287F4a75cadfb36D671A2f0d1959B09'), +('0x399e0Ae23663F27181Ebb4e66Ec504b3AAB25541'), +('0x976FACf30726e2F60082f98fCffeE1486C394810'), +('0x212647c56BA10ee429a838bc567dFb03A8D054Ba'), +('0x5f3371793285920351344a1EaaAA48d45e600652'), +('0x04c0cD38B8c203b14ef2b7B8d736D69B938AFF71'), +('0xDAcfA9601a556a6630194ffAb7EEE60866072cce'), +('0xcc3c930cc2114A29106F90c10c8E32D2d770F79b'), +('0x835918a3fBDf946364a9aee3114173865b712663'), +('0xDA47bdcC48f26FB4709f90316341D9104cB1fb89'), +('0xF11C19Ae9390392763Ed8E4c51BA4055e7C7C6f3'), +('0xFB40932271Fc9Db9DbF048E80697E2Da4AA57250'), +('0xE36BD8C15a83b89E2E49806d7312846069755C63'), +('0x94fcd7f03aeF7A2089301A183ed6e26b8cC0de47'), +('0x94824d798C4d2fd2e093EE4e0084d2f1F75F7554'), +('0x4DD05e12d0244575C77c31C24F0E273610C085D9'), +('0x183bDB344A07Ee3D27f07AC4799A56E4A2fE5439'), +('0x583bBaDA56bb535BCBb31877A620A6ff2A25CeA5'), +('0x82c0BD9c20379ae7d08Bd74BD7Afb2a18c6dBd43'), +('0xDfd77a848618b3428C79790b5222b85c2adfcfe5'), +('0x0D2349e291A9e0Bb755bFFA4097E6D378A72330B'), +('0xA906c85B7e809b79c5e69d485693B44d65B1B252'), +('0xD442DeB96B90E3B3135653705c3ef1FeefB346BE'), +('0xEB40A065854bd90126A4E697aeA0976BA51b2eE7'), +('0xaC4361f56c82Ed59D533d45129F407015D84702a'), +('0xc4F91CDd498A30f8ed1dEa3883Cba314a7A2a022'), +('0xc15677AbA8826e78266ff32d3b4bb4bcC8CD9b19'), +('0xFef75B27d4Ae3D5228bCc2912f9CdCEAfe5f82E3'), +('0x051010142A0B9de7F0Fd8fb31d085407287F6381'), +('0x10C292a9B4b0D085e71590B67F99408a38F3e40a'), +('0x03894d4ac41Dd6C4c2f524eD4417C90fA46972c6'), +('0x40f9bf922c23c43acdad71Ab4425280C0ffBD697'), +('0x0F46540678c7e6D2eF983B382CC07Fa815AB148c'), +('0x48b576c87e788a762D6f95A456f2A39113b46950'), +('0x38f80f8f76B1C44B2beeFB63bb561F570fb6ddB6'), +('0x9AE494FBAc34682c122A1D4e3D6ae1Eb5404A469'), +('0x639749b7b08aEe65039c21d8a411103C6ceBEBF0'), +('0x27259b0F4209e76f8C6Cf27106C9FF83BdC2E831'), +('0x76E4f864bbEb60Bee66Ff5BbcD32dEcAF7FBDE71'), +('0xaCf4C2950107eF9b1C37faA1F9a866C8F0da88b9'), +('0xd61FA7454b484cbAA69a7f7c04b3610FbcF2514B'), +('0x7b4b79C7Adb4a7fDe8bB3b48b3f133448ce4DdD5'), +('0x5B1899D88b4Ff0Cf5A34651e7CE7164398211C66'), +('0x61987699055394c65355F2797D3e4e589f7FaBf4'), +('0x92eECA199466877a56EeA7B939b54717F6acbC2E'), +('0x4dDE7D4dAafFA88DE922b99fa0890Ff6872cDF59'), +('0xFD7bD29E1050932829c1FC080eA42D7394C42847'), +('0xA270f5649A42feDfE66ddb3b0b50bebAe1e3DDB0'), +('0xF3E16a28b09bE3836e66f091613c230128141Fc1'), +('0xEFEdaf9c07E6eB56BB8F82f30018e4461B1c5F4c'), +('0xc68e8827FC1Dd3b2bc94Ca8b0228268f16dd6cf6'), +('0x40Db8365d1252bcb06598927698238a99D39228E'), +('0x2f75fFe98046E4a55Fa79daf97B2730d7e707186'), +('0xaa79B87DC8B046A5E4f7D03F1562D7fe5BF98737'), +('0xF1659A2FD5007192314F9676e6a4a39FD1202160'), +('0x973375b099943cDdFd390022CeA90D4F1d0c493c'), +('0x10a84b835C5df26f2A380B3E00bCC84A66cD2d34'), +('0xC8Ddd59c496D04C4C060Ab5038d03d661DDC2617'), +('0xB986A0263b045b45765DAD38a000334c1e10B6D7'), +('0x2615214F8200B526a7B1eACe03971F2672B48CF2'), +('0xF95D9549b3Ab9470d306a6413Aa45082e8B66043'), +('0x45A7B69A522C5ecFF77aD3540b901Af59AF12f3d'), +('0xd454ED303748Bb5a433388F9508433ba5d507030'), +('0xd364055a2B9Df6ca96970CBD6827991d741Ee66A'), +('0xF1bb436c29E46B1987bC825879ffc9c34Ab97f99'), +('0x5725a458b319d73B8Ec84c47de80620E7B191B0C'), +('0xA231a5Ae629a0F15e1C1EB3FfBf813589e206926'), +('0xb7025E51a5DCcA0cb1C03f66fE31F5dCFfd599aa'), +('0x89ED5d9F29789c0C55F415bbCd2bA259882307C0'), +('0x5C0e1377edF9A8E4f0430118AE5381024186F12D'), +('0x4A5da2a1D3258dF8FFb431Cf0110FE9b98ADeEbf'), +('0x4520cD8BC085B962eF8c0ec696ac9D3Ef1d8bf55'), +('0x765a16ca391A6b9249cfA65bf2D14C38722198e3'), +('0x33878e070db7f70D2953Fe0278Cd32aDf8104572'), +('0x98dc06b6809984A490e3c8a8A3e262aE6Ed16be9'), +('0x051c6827F00f16Ba8F6dD9802dAC2FB453F4C535'), +('0x6733c60E6E02f9C8FA221Db1aeA018d80D949861'), +('0x839395e20bbB182fa440d08F850E6c7A8f6F0780'), +('0x8F48094a12c8F99d616AE8F3305D5eC73cBAA6b6'), +('0x46331769A8a01947E07C310704893076a3409d36'), +('0xB4dC4C7460c58E7652Cd615675a1F707EbB67E9a'), +('0xe3F4F3aD70C1190EC480554bbc3Ed30285aE0610'), +('0x924477d36aa64529D31E2f27EFa40E093c7eC001'), +('0x1B2C142ae4B9c72d2B8957079563d171B7F72892'), +('0x514A9771Af8Afe71057666b680238dFBeA578d65'), +('0x180e5D71569106DB9F27DD04780850fbf883F7Ec'), +('0x0786A24145Fef2c60A38237e8671332899cE7C1F'), +('0x000000064730640b7d670408d74280924883064F'), +('0x33f6EE932cEa603Fafd6854827259bE172C91Da4'), +('0xE4DDCC1996B326e2Bb5BD364237fd12e9d713201'), +('0x4eb7E921393c3DeBAced3378bb458cdCF9f1AC15'), +('0x548039186Fb60952Df4c8aFbe7F22B35c3dDFeCb'), +('0x19CD3605D3c490B8E6a1538aCcb45D2a506DDE7c'), +('0x2bC12061C8912505978472C21d4a23dB43AF62aA'), +('0x42EeD31a8BB26099364b301ae087ACFB037Ba864'), +('0x4bfb2c232F70c83136a3F206cd26Df2A0B605cEC'), +('0x4a7EF87fc299b5102C35DA4ACd6e5F253b8F078B'), +('0xC3d43f8E008ccFf959101b8d0d26562e84d2B204'), +('0x12846A6Da4e729a6B5c33d32F93bf52EE3CAe8C3'), +('0x9120FfD5d04ca4B26AaBCe611989A8F026dc099a'), +('0x79D771d57f3596c77904f539904e2808d67815d6'), +('0x73Abb4291E37Ba0f1EbEA2661Fc015a8245f7508'), +('0x6b759Bf480407D19c8903c16023c706868c29a2A'), +('0x4455B1DA5f058167cC05800efB5A7055AE33fa0A'), +('0xd98aD1Fd4aa0E1c876d91968D1385aa9E1Aa98df'), +('0xBC2E26deD32A96911B65EE2283a1E30F077BbC59'), +('0x07506a5F48D71fDB34D3900fB086D43EF1B58FF9'), +('0xC2812325caD4C4C782CbbC1164e9373371D31dB2'), +('0x104AF6c440D274a4abCb353c0Ab85DC2f911e074'), +('0x43e65F8116174d1c38Eed3545d21fC37ea34B565'), +('0x8B147C4dB4e2616C0bAcfb29F5C61589BC0dE4dC'), +('0x55F5601357f6e0B10a3386914c93916c6C9A368A'), +('0xCa144Bc972E07ac38c6d68542C291cfDCEF2b31e'), +('0xCC3d7F9fE6946979215A901BbA385a88FdabBBf4'), +('0xa80B9CBA10e137CF1279128523A98430B0f30733'), +('0x890a0047f8D573347872cB6C019F86552f2367d6'), +('0xCA119907Cfd60C35E8EEDCf99B1dB01eF8c179AA'), +('0x8eb97C7dBC3916b1C19768F02DA61555f44D51bE'), +('0xdC06D8EaA3dC6C68a447849EDF0897E469386E67'), +('0x8d9CF2AdCf647D78809077E35870A2E3F50Bb113'), +('0x22bAac1E95efC010E35D5eD643BB16c9dB254a11'), +('0x8e7D20638947132B0e6E1aFdE2da1B103aFF9280'), +('0xB1E07a72eCAF19c806133b155Bdf8D8Dd8608A24'), +('0x56De6e445d3181ca47825fAd343313abcA06D685'), +('0x121DD42384db6ad04851F7ac1E81bCd8Bbb1876E'), +('0xF007AF3748AD93B4F045306a3b09E07F0191929A'), +('0x71B70232BA6228E2E378CcE858C54cb01fd04171'), +('0xE3CB44d6D165225053F3625f13f9A729646929c7'), +('0xaB69562F582FBB458828558dF66f0d99BDcEDaa7'), +('0x752a1ec2358082D1e648e4FdaCcc41d321d9Ff6B'), +('0x4bd5487aC9e2B4f6D662b355B8302aEe83827170'), +('0x9bACee870643B1BBF7e2556aE56ec06e07912d46'), +('0x7E52082fd8ca9025E7E2C4Da3fc7C4FeF9Fd9706'), +('0xebE45812659d8A4Fcc0703D478ABA0ba56E49D9c'), +('0x719028736f10164c838Ef129936779eD739312f2'), +('0x002B5dfB3C71E1dC97A2e5A0A7f69F3e7b83F269'), +('0x55bA9c90c37e3206570AC9dc872c0f053d155F77'), +('0xf404A6Eca17E1b2BFA19722991afc3C6538E58bB'), +('0x3672318d2cC4300E1276ea0adf0B9FE0c9C02e19'), +('0xf5819cC26F0481c9b86294B4c24027518a04BD5B'), +('0xF123e9b47aa50265d01cb0b69B2B027Ab8e5470e'), +('0x2456bd7F6baF756276A1bab548988B8089f2E8B4'), +('0xCc2005844AeABbd4d663f7C45C9bd1369eb062b3'), +('0x901B1d227E6DCf9217BF613f7345758fc6e18Ac3'), +('0xD1e95D1B1eFd0Eb23B1F1223b05D5a0130b2f032'), +('0x30c98Ab8FB66212634bF284F3C13b1e1fe61B3CE'), +('0x3a43e3aBDCE877ea7db72F5ba8cdc8520E830e4E'), +('0xBAab83De8DbA764bF02a530cad33555bD23eba22'), +('0xb2b9300475aF157676C44eE64d39a5eB3C294DbD'), +('0x7b1E8B4C96982D828eC3d99bB4D5951Ac62D94a5'), +('0x76E059C6FF6bf9FFFD5f33AFdf4AB2FD511C9DF4'), +('0xDD4B4d44BfBa72eF6cddc4048ed2B678105Dc008'), +('0x3f18b8b7ea6a6F6715dE3b75434d45051B495B44'), +('0x3F9D06d42cB2B04ADa2D9A8cdFBF63c5206d7deb'), +('0xB26B5137BE5A7bB991Fa63A36a8EaA3daF394B7b'), +('0xD2602C7bDFC9F413974e944280BbFae275d1B1b6'), +('0x4F3212bbE661995Dd6E6579b51C7C437e022b75A'), +('0xDE1d2cF646BFb8f3148364B9419cEfc0f376226a'), +('0xc191a29203a83eec8e846c26340f828C68835715'), +('0x21606eE18fc1c9B398c25f56e98E6035ab434299'), +('0x76De3a7204E62f15FF6B106bCef8372d10045fEb'), +('0xe4Af9eCFC747fD6BEcF41D35C6E59f326e9bA28E'), +('0x2df292AF809Fd693D94C7D17E36BE352e15Bb98a'), +('0xE76f1230c61160206306CB55e524d5843bAD697a'), +('0x57CaF918a5982D91658c5819393840708FC351f3'), +('0x24749AedF18208aB74A8110E07E820286Bb5acf8'), +('0x6CFa99B2352163D70bd52de24CdBf553374B9335'), +('0xaF7610578F54c7De7563655AaF461E2CbeCB08C6'), +('0x2c3E79D3DCE90FB0886C89Ec602E61757E589a94'), +('0xfe4a7CB9C11F090b74811e8620F15CDf806b647b'), +('0x6c3F373Baec5D2d0Fb3C82C4f3Db5E48873ae363'), +('0x4D710A7A46A221B3361bCa2E524af4C1BDe44BaE'), +('0x20cb97EbC067bEfc988C5E2FBB198cd9E2c40c93'), +('0xCb36F8580A36788A48518dEC95Ea458357E64E30'), +('0x7bC4dC490903e046aEC4303f03599dD6fd06851a'), +('0x0000014ffa35ED6Ad0D5c9c079c908716c113EB8'), +('0xDA24aA83ef7c1DaAad3e40c8De2e4737908BF636'), +('0xF5b3B17D2c2118E2ffff5a758af3D4805B8a6Fa1'), +('0x52242bc136855a96E44ACfe79c2F6AF3BaE24B7c'), +('0xEb263241eB948Cc0eB53A58bf743289D074F474F'), +('0x34C226Bb299999d4D3Fb50237763240ea68F3220'), +('0x0c2Ab8F849838BA353ab82a147042565A1b972Fd'), +('0xFeB3E0f50107f6cfB2EC8C2bC8287f2707E0E2Ea'), +('0xe173fAdf2ce1340f7d199423dC4111012806Aa8f'), +('0xE65Bc9d90619e0F98Fdb6BDa851Aaa19e054355a'), +('0x387fe86D6AbfdC8C2C3a01feF3E6F8019CbFfDF2'), +('0x014607F2d6477bADD9d74bF2c5D6356e29a9b957'), +('0x2A0B6e68b2b282a80e4f4c4Ae24DB79792ea5fc4'), +('0xB68da7fbF71383Afab240839287878539cFFf20b'), +('0x5d45dADA1360A8d6Fab78204E60976d775d6883F'), +('0xf21e38ac177B48fDE02dB7F2CA97466AE8Eae87D'), +('0x6C5Ecf9269DC1bB81980B5112b6d32D476cB4B9D'), +('0xABb66648ABA46e17279B58b443FacA6b447e19ED'), +('0x6ae8EA3D4027DFbfdBC9B2e92F2c3D24997d1faa'), +('0x3444CFDafA8d986760cD07d5f8DBf9D2D113fF67'), +('0xB49c63E552e330A66FF882Ce38406CAC9fF30D36'), +('0x7993F18C91A9f68593d308C5846f380A2a374F46'), +('0xC1546aEfaC4081f343C54bEe1764fd18136ECD0E'), +('0x6482E486832c345363C36b130D700185Fe781771'), +('0xA95261d06CDe28A8e491aCAAA7687673bF08FF0b'), +('0xFe7ec4d52e68f1aD99A3227F2084E93c6F9a5C95'), +('0x8c9D6173CD7a77dd71E1A4E67c5CFEdA424e6242'), +('0x46B830bf2c258f5A7Db64EB9A96706a91132715e'), +('0x9e9865d26131d76186aF09f29e71f2fF12C385e1'), +('0x64E1A38CbdcEC7753e1242F0a23f6A6d6C9f4Fa3'), +('0xDB54c49C6fEb76c620C8F7A9a71A982050913dF2'), +('0x673176Bdf8431153dcfdbeb69afe3308E23dc4ed'), +('0x4265fb52E31B1eAB0193F36E9c3480deca79F9c6'), +('0x542E4dc1D3e7314CcBE93568baa428b01eb90FBC'), +('0xf9Be154c91BbfC898B1e683ACa683973D7C525Ef'), +('0xD8AbBB00661C9db59F22EedA7950d4cBb3EEdb57'), +('0x3b2f8C00cc8593d9cBD1ca644C44203e817E2C71'), +('0xD8b00122B6CC0d44Fb3198fB675635272d516B7F'), +('0xc96b54890f172986F8EdC7AEC6E8C09a53760526'), +('0x6c209c19deEa3809AA4cd3Dc2039E9Af73D4Fd44'), +('0xC726A77353DBc81fc506D71fa6752B8A44b4736d'), +('0xAec19D15F1B6b737c8f374FD90D1F19Ca8FCbDEB'), +('0x4831DdB6502ca45dbEEDf58B47292061Cdb6050B'), +('0x37f749Ae2E7A28EFE14beD6ccCA5272527E43112'), +('0x8498843f6D9046f7b59482978E152D61869203bC'), +('0x99F83eC57Fe1e09da9a20Efc00156b71826E11C6'), +('0xe308eE20eE2528830f53364E84B47e0ed08d4efA'), +('0xf816433683ad38146AF38671cFE07eeF6e79662F'), +('0x8f522E9Df8E459f3806f881516bfcCd7e5D6DE7c'), +('0x16d9d13bFDee5012b5CE81a2e5E621a3362b94b9'), +('0x597dC4159a4b85c086c3C679a0B6c8Fe2836886F'), +('0x282d656A9d95c64522F6Bc1a42EE759DE81e8dc0'), +('0x40DE3299Bd8a10D8Ac3f32C1A55DE40640cF9B75'), +('0x01ACB9d2Ad37E2e452dB62D80F472B6D39E2Cd6D'), +('0x0743542070891051861f8D0a4550f97B43B0B89a'), +('0xefD763902F9e303c24890D4E9f3Fc615f0aFEBe1'), +('0x0CF30daf2Fb962Ed1d5D19C97F5f6651F3b691c1'), +('0x3b78637E124f302cA73257Ea6bABC705ac0208dc'), +('0xdfBaeeF21396BF205D4B7D23345155489072Cf9B'), +('0xCBA711BEF21496Cfd66323d9AEA8C8EFd0F43e9d'), +('0x43feCdBEba28503c303360672c5f05825f306785'), +('0xA25207Bb8f8EC2423E2ddf2686A0CD2048352f3E'), +('0x3AD2EC0c534266Cbc4F789db1BD664Bd9F3FFC99'), +('0x18a3a09e417d9d101bd3C28d0130b28492676bec'), +('0xC90BFB542E4d570f4E9Ff83D0d6766B2eD3a557F'), +('0xd0ef3a2Ae7Fb496C0897400f1be46693B6183B0a'), +('0x25CC275CFE3Cce1700E816e00d4CD1f60872038A'), +('0xA7851139F56f366C507DC889D2Be39063F14F06A'), +('0xb71bF0709341Ad34F729C0D1d1C53e46D9a98E83'), +('0xf1a4818e86265230a845a30b78bcC311779a1784'), +('0x1E8eE48D0621289297693fC98914DA2EfDcE1477'), +('0xAa01DeC5307CF17F20881A3286dcaA062578cea7'), +('0x30043aAbBCeBbD887437Ec4F0Cfe6d4c0eB5CC64'), +('0x854711F82e1DF2dc0278f623e9C66A2DBeF5389A'), +('0x181235Fb8233aDFFDcd878854e7033ef66e3AEAc'), +('0x82073f802547fEeEc0fd49719a3D7697fB66076a'), +('0x073a360C372FD51Bd6E56B4a4d73790fDAec4641'), +('0xAfA3a2528E8baAd576a83ffC52dB9f100dEbe307'), +('0x7b3012531aaEd7b48bc9BfbC1cd7E7f20B1BB5b8'), +('0x2f51E78ff8aeC6A941C4CEeeb26B4A1f03737c50'), +('0xC70D7342332eCF3c26F991FdBC85A8b8AB1A48bA'), +('0x2b5d8c0B9FB17e3c3B56FEE12212c8e40Ce4F1F2'), +('0xEACEaC3fE9B210724D19F151ffdba42C072333f6'), +('0x8c79A3252D1ca011dd1be1D8D9dDBdAA9f445D14'), +('0x4E291277d3FE2264753cb745C4CabEa3b5e06D06'), +('0xB4b98C463D55F0Bf02454a781A8A8897f56D8c31'), +('0x729DB9852eB2896EA9888EFc32aBa14DFF187D7b'), +('0x0D6d769301e69eA908702103C04355FB2d338af0'), +('0xB7b1Ba671434E10DF9DAeF5D24054939c1bF79d1'), +('0xe56A0Cc5A73B60C208aBA49572838ee26947F766'), +('0x11CA8fd9000d858491f1C2eEa0DAccA8688b19d0'), +('0xa7a7EFFdB2af2e8b68bffb9f96E883e71b411054'), +('0xbE35E4B528C1BCf91f55e270315A60D101968BFF'), +('0xA9c96d71e9C2d499f424b2A490006e8D92225222'), +('0xC28D2fDFE6d5a482d32f855457Bb5F8cAcdB32b1'), +('0xa305B293e44A82f3Cd489b5fB26084647bb5D8ae'), +('0x58d7d9c971A613117E493062bEC1A6A5484f2780'), +('0xE83B9A1B9056B21a01b85162E77AD76a42A1c64B'), +('0x0f792e55668AD78476d4B563E6EB1228D636a71e'), +('0x16f3f2f0ba34973937A1ebb989a295Ca106b67C7'), +('0xFC967DE4e029fdcD16B418DaC2147d282C93085b'), +('0x6a5c65f6d738Fc91585A64AAB73Acc59a94fE431'), +('0x0B3BD83E857997b370FaDC8504fB712244F6786C'), +('0x1E6127D4716c3AC3b3a2B005631c1C541C13D120'), +('0x453694162311a97C7852841368d555096500f7bC'), +('0xBB5935dAaFbacAE82c8D2CA8377F16073D70061a'), +('0x4e9A05226993F094A56A3472C8c816F2599423A6'), +('0xba84B5cA750b33DfAdDBFdD1B7C6887885a34977'), +('0x801a6d6dBC1e40466E131aA21D951629A9efAB4e'), +('0x4892139De0e73141438D9E55D593171C0Cc6B143'), +('0xA8cadC2268B01395f8573682fb9DD00Bd582E8A0'), +('0x297Aa50D0557c865F6C9B0AA0a91f41C26E55eE6'), +('0x9Ff46343d0b652D6e766F85f9aE91653869349a5'), +('0x4343182e56b1AC756Ef2fC81466C63C2bc275404'), +('0x2cCbbC4c10F5d807FDd447219B57D0b883a28DC8'), +('0x1bBeAc736875c5043486A8a4374E6B5616eC8883'), +('0x049C9dB54a3d21652546958977318966114B373B'), +('0x195F7Ed714986eF27463a82D017B47Bf8B2A6A35'), +('0x9FA62342f6F6e494EC4fc42822611d0395faab8b'), +('0xfbc8f2951d5255dcad5938557CfD505d8B9513CF'), +('0xBf3D49007f31D2b79635fb7372F435Fb9A0C4900'), +('0xEA809D3fb969d1D4de90c022C34B075B1Fa5eC50'), +('0x843Ea4D5b8B34e07A63d256785A1c9560F3Ea2Bc'), +('0x934775cE4466a38226cB593895a7fDA112c9B1e0'), +('0x5D8aEed55130626a473eb75Bf1301ce3199c85b4'), +('0x95add3DfEF3AE0A832607Dc71C4A9C6A6C2D7Eb7'), +('0x770569f85346B971114e11E4Bb5F7aC776673469'), +('0xd134fa8d5e0Bd7Fe6f080914330967eC035517f9'), +('0x1200f37ec1FB08223B8B692977bd5a475440e850'), +('0x78117085818a049BA5Ca8BFD9e8F75c849745F85'), +('0xe43d836c07e8159A114Fb8f06c1851468655Dbd0'), +('0xbDa7c5B8a747487B475468D556dCb744b4f7D240'), +('0x9774C35E38CCaF1F1604cB8062C934510375b539'), +('0x992064C3065422dC7f3139645Ff9f31EE0775a83'), +('0x7FD120639464401D5d7d13a9C22276169Fa553e3'), +('0xDE79c95d3C3B501770efeb871176d9E9cDD10E81'), +('0xdE19A6CE83cC934e5D4c4573f0f026C02c984FB2'), +('0x55Bc90139259Ec9Ab21d89be918D7b94e0FAffb5'), +('0xc93D7cC62A21488396CDD0Fe5E4fD1B721069303'), +('0xcd11c24CCd74428E6bE992e78E03Fa4A1cbD4D54'), +('0xB06D62011c26CF0AeDe7cdc6c412bD367f1AA18d'), +('0xF7D9a2902Db9829A2C51004253a3A3cfE4D1A4Cc'), +('0x7fBb70dA8c4Ee8ECf07a39f5E2B866D9b3de7238'), +('0x0A4D7d043107108ec348a19a60219398e7b4fb4D'), +('0x930e299b84453732797C14fed9c0830bB8946dEA'), +('0xE78D59CF340665BF7157cb600b44f3032F4c27f6'), +('0xB227220dd3479c634bacF59e99F39925628478aB'), +('0x3FC8bE2b93C54eAF6CDB5Ee47420E5B3c0434975'), +('0x5C127CcDD428AC57A1ffF2ec4BC1cD00dbBC9Eed'), +('0x744c6Eed427aF293b0106B46700fdDD3C9f62Eef'), +('0xDd03d2434C02c6BfFb097b7130528F9568b6C70d'), +('0x743Ec55fc166D24D2FD0211fb6Ce53926D0Ff3b1'), +('0x97C12EFA574923E3ee445370d2dE432332857110'), +('0xB69951a0642b55CD5731535ed5B290Fa49D3454A'), +('0xBA56878729540404dE2aa14561b451aE2350744a'), +('0xa85cdd5478B7E525a808eF9707c3e33432cE1e7F'), +('0x315E1A5C796B734585c27E615b774441372Fbd08'), +('0x1d921dff757610FbDB0073479e12c0a07d382677'), +('0x46cda5EB47B88cafdDBE2dFE53285b1DA1D860EB'), +('0x9453efe4177bfDc244CA70627C8948880c5416F3'), +('0xaf6aDB966A6DB512FAc9606c9116aA42Efdd534b'), +('0x92db0Aa43ACc43B8B685a0f46F5193bf87462dF2'), +('0xB5dD7D81bcfF4640D5f8AEEC8162E5e1418Aa5BA'), +('0xD285a581e4129605C13ca6D32Be09d10A3579A1D'), +('0xfad129eF592408085D3B1D8D61d6C676A9dfc49f'), +('0x1144a84e81349cF347c3BB75237741F2936795A5'), +('0x8e30Dc2AEF957B1F7dd67B1b7bC651fFe7E17a06'), +('0x66b8390157e5afc8f42A84F34a1a12e02220FC37'), +('0xB02aB3A052cc614460DB46d32156cb0C41478c3e'), +('0xe643905624879b401672802036043ce2C954268A'), +('0x0F76C80d5C1e0c5f118E56AC1195B6B6A8b50C8a'), +('0x791a0f13CD2AD330a9f3DbA4cf83DD606E0f0023'), +('0x8260EB7b04B1f4fAcf78ed90D4254FC97f9ba6A1'), +('0x2CBB536A8d80974F3Cc91Dbb8c6a1B2a5659108C'), +('0x140A50799d54e9059195B1f01aB3EC26106B161e'), +('0xa38005879dAFf0c35cA8C726764E020Fd07Ce16e'), +('0xe9F24Ad877C0B3B63ed4ecfcCA21f50dF7e6c4d1'), +('0x36ad9923ca4b24794507D3b5fCaB50f8ae616E6d'), +('0x9B7A33687E43A0C458900237Ce154823a9B473B3'), +('0x2655fc09Ce0336E2bA6143AfA4a4Ac840D98e424'), +('0xAbDBe9A32fdf17e034914641106B46C65D79921B'), +('0x8F48282e50B0210bd7c7DD69C54205E98b9Ef5D9'), +('0x51Cc88bf2BEe4BB13BDFE9f6735d8639Ab3c9cFA'), +('0x1c1575A69a79b2D7E853e1D4e2E07979a33D4899'), +('0x446966833acB28C9A6fc7b6eB463B70815A3B182'), +('0x5a979491D8AEeFf81846312B813e4A77779df1f9'), +('0x705565c4413C384721A70a0A6a646eD43c6ad7fC'), +('0x1b8Da75295cF01bf23B66db1df19fd033Ef9BD86'), +('0x7480014C77b15ACa1054aF7Cbb0888833326076c'), +('0x08F2AEDa703348EBAaDfa84723126F0927FFCE62'), +('0x047d26f4eeBA32DbbaD29078Abe0974Faa037266'), +('0xb57d7e4cF73E0e6383A87F23104E4f4C8B317376'), +('0x673bb40E274786dc58Fd19fbbf9FFA1f903a2FD8'), +('0xB6989F472Bef8931e6Ca882b1f875539b7D5DA19'), +('0x81EbE8Ee7b51741fD5DaD31F6987E626A9bb8111'), +('0x8FaE81bb674c89cCDE35a386587333D074b57786'), +('0x1e917081e0f2D7AD58E732882EbC51e457Df616c'), +('0x70c9CCAC86d42b8bE0bbb26680cdAB1A8E0F8b82'), +('0xc5E9eD18Eb959fa98002d219A0d8adc7781a1d76'), +('0x88e137bd3C1d8E94162B48034b221335f7aCe9ff'), +('0xD923dE646D3f0BB9Eb7f9169f259C79C3edE2188'), +('0x5D6Ddbb7252681C0203e375284F202cF1Ede8eee'), +('0x00409fC839a2Ec2e6d12305423d37Cd011279C09'), +('0xf614ddC260c8E72efd5aeC92E3A4e3Ecc4C2A116'), +('0x954F716e6de059360d278B773138f8e046696721'), +('0x9D4f5De4367870a599b086FF2D7789544a85B6D7'), +('0x6D97d65aDfF6771b31671443a6b9512104312d3D'), +('0xc9e60767BE3C0e812301ab2F38237EC52B56a009'), +('0x47C1E0fc21b11B2D8c5c6A8702D066b42b0B4693'), +('0xd40317DB258590b61b7E94D7678F597199c65498'), +('0x90E5a0Eedc5e89A1Ada9f1C9267bd64d939A6aFd'), +('0xb64fcC38A590Ad62b1Ca45a397C717dE806a9B02'), +('0x66633Cc6B84CD127A0bb84864c1a4eA0172469A6'), +('0xB352bB4E2A4f27683435f153A259f1B207218b1b'), +('0x85BEad65c61dB8cF230b3ec30552B8b3E6388570'), +('0xC728DEa8B2972E6e07493BE8DC2F0314F7dC3E98'), +('0x87f1C862C166b0CEb79da7ad8d0864d53468D076'), +('0x7eE789B7E6FA20eab7ECBcE44626AfA7f58a94b7'), +('0x5D28FE1e9F895464aab52287d85Ebff32B351674'), +('0x4318cC449b1cbE6d64dd82E16abE58C79E076C2B'), +('0xe64113140960528f6AF928d7cA4f45d192286a7a'), +('0xf85c22c0D429f7a43F8d36fDDe308b423E091E7E'), +('0x02766E3d25BDA6badcD34d7BCEA5bF054990444A'), +('0xa1403A5806e4e3EF8BcEc00B9755E0c8374b7447'), +('0x884Ff907D5fB8BAe239B64AA8aD18bA3f8196038'), +('0x2c2EEeFb2201bE2179B089e0d4077DDc3066242C'), +('0x0d193c250583b704CBFA715e05AC8741Af63f906'), +('0x9fC37cDf1Abd391B17E72786644A8DA92143A0Ea'), +('0x3fB2F8276Ec5FdEa3a0FB1bE23e9E8463e266778'), +('0x9600e2eE6377DAdad7299B120026661c336A5e6d'), +('0x86e6b55bB954e1cfAb567f9582E0fa580bb0290d'), +('0x809C9f8dd8CA93A41c3adca4972Fa234C28F7714'), +('0xBC514De784B49dC43282F8340EbC3aa7B86Da12A'), +('0xA515F7fB260095eebC860425493b8761B4FC9abd'), +('0xe7B9C112aC2EC9570521b69975613b6663593FFb'), +('0x38Dfd788DB4CFaB7b9Da57b06CfC06C6E0f33C3b'), +('0x0D89421D6eec0A4385F95f410732186A2Ab45077'), +('0xff75E131c711e4310C045317779d39B3B4f718C4'), +('0x00d18ca9782bE1CaEF611017c2Fbc1a39779A57C'), +('0x3F20a6fE04b3A4b37bBBBea826ae23be56Baed74'), +('0x92b52e6441A9e2E03d080E951249d93dF222f855'), +('0xe52C39327FF7576bAEc3DBFeF0787bd62dB6d726'), +('0x67649aC58a3DCE17f7b3Bc19147b8b7a89ddABcB'), +('0xf5AB6B4a8d578807491ef59cE855982990932617'), +('0x9268c75FB8f147d96ae67f2cd5b1A1329367adf5'), +('0x187089B33E5812310Ed32A57F53B3fAD0383a19D'), +('0xa8258ED271BB9be9d7E16c5818E45eF6F2577d92'), +('0x806C3d1e2c0b0c1556B299c4B9d6aE85809DeaB7'), +('0x07588353130f93056c7d32250a217ADA1C5AE022'), +('0x85553C73886977397c0eB6231Ac95f86015EcE9a'), +('0x3E68FCA5b47bF0510DEBB0fa95d51c54B032A1C8'), +('0x5AC583Feb2b1f288C0A51d6Cdca2e8c814BFE93B'), +('0x2Ea846Dc38C6b6451909F1E7ff2bF613a96DC1F3'), +('0xB950e0E108546743Af96eB493D4FF2AbC63816dB'), +('0xE2D6AFF297b41881c1aEA9599F68AEDFAB38C651'), +('0x17A8eaF9241a220E1ebDA22A623cb058530cb0aB'), +('0xafA3E6E29D99337b166b83fB24bA17b19764B49D'), +('0x07Fa3Db26C2606DAC6e619540eeb28Baa3de30E5'), +('0x35853Ec4C48F5d10dd1a6261113a6253287C5f98'), +('0x12EF2cD4231D5CF655a6CDD4Ac1524FFaa439c17'), +('0x2eaEdf29987F410cc14FD1745F7C0f02695085B4'), +('0x0CA505e54119196EC489c373DE0623DB70959b51'), +('0x8F6Ccc53144fbF46eF7F4f335eD4F1455BaaE848'), +('0x276E69CdD336001afEF07075859A93078496C3c1'), +('0x65ad2BF7E09af2597C140dF6386a3003d0F5f8Ee'), +('0x0d2D685b3EAA41d9DB61e2D34CD8790E11bc96f4'), +('0xAEc2aFD87fE3410e34d94F3886651E44c4ABe5B2'), +('0xE945E42743AA9b44af588142cEba4Bd45429c6f8'), +('0xfFc02aEE7b4B21e17F378A6ae4283a665CA2Bf42'), +('0x43e2ec837a789Fb39f13098BeF24c72C27c28806'), +('0xb6d17997Af674EF938BB3ead45bcbb54284A9a9F'), +('0xe96056A9936C58e89D1703cF6bD97F134341EE44'), +('0x7554f10Da3Ed7128300577e55abCd8F8835BCee4'), +('0xEDD425359FB15e894c639B6A74112954486146B9'), +('0xa81dC28C807939AA792952c7e0c0A5A7Ac6A0649'), +('0xe0144FA05A0d32B5B1De10CcEe7211616B3E3EF0'), +('0x0e9F1488F885Ef329bA2aEDA7004500f434c4582'), +('0x1fB86E4680A0b0dF128e39c40db91be7526052ED'), +('0x8289432ACD5EB0214B1C2526A5EDB480Aa06A9ab'), +('0x2c8467DA8B7B3D0a7e3886F6Cb6697c49571Ff66'), +('0x9885a84930E3C8c865A1D3dEBDC204F990BaB7C3'), +('0xA697AEdF5af55fd7a3D75122fAa8fFC57e84261C'), +('0xB1dE969883b1FdD90a43fF475A5171a3CfEfe76d'), +('0x40Dc654af5cE40C122ffDC679fa8E8ca8b91556A'), +('0xb08F95dbC639621DbAf48A472AE8Fce0f6f56a6e'), +('0x0E07Ecb7eA74B53f41A3C82B9fF7323512866cE8'), +('0xf592eE0E3a20Ddd65882E0fE6bFBB4B465A98Ae4'), +('0xB1FbaD53FD06082d884c90dbd66c68613453Bf20'), +('0x136d54E730ad568cf49f8d7b8D36E1b079f37e1E'), +('0xAE419a2Ec9E77374B03479293CB4a509A6117825'), +('0x631088Af5A770Bee50FFA7dd5DC18994616DC1fF'), +('0x32bA2323F97d7E031586A34944d11ff6b57D10c9'), +('0x8135d8CCd2AcBE575Ebf1827349185df7185Cf97'), +('0x7fb43C99a26a9EA8ba841d58390BF1C2996EDFB0'), +('0x5cbB6ad79008908aA125667D1300558D9253B589'), +('0x0Ff902dad0E74C569a2f0ee302114940150b16Fe'), +('0xfB2e9D1C95dcd5665eE230352d9973263f822F8a'), +('0xd26b76e50f6510cdD4bf45d59279705f36946d23'), +('0x10F5c0F9431C3161c73a32969434F1dDF9F0dF78'), +('0x84c2B621D2CFB14EfC785c8E853cdc714503152D'), +('0x301605C95acbED7A1fD9C2c0DeEe964e2AFBd0C3'), +('0xf93F0b770784602fC3079eb1D2fB1Ff488Bb02B0'), +('0x2dd2036C9Db2ADA2739509AF0047c00C8b9291EF'), +('0x1d9302c112514D536340098e6f13d63E02051Ef8'), +('0x616caD18642F45d3fa5FCaaD0a2d81764A9cBa84'), +('0xC18ded48642B66CD917951731621C859D3e7770B'), +('0x1e90474D2E83e7B7dD45553156Beb316845E66A4'), +('0x3769092DBfa6eb34434fB5B7cf0eB06E710728F3'), +('0xEBCd250474C27cBaD3C56f3F34e08F97b370AC2d'), +('0x1d4F6aFC0E305048aE0A9bE9F76b4b89d4a360A5'), +('0xC1853582CC52Ef41876ff177Be8aF60566Dc80e0'), +('0xcba08346895f93331caEa633efB83a9906Bd219C'), +('0xF41b98a4C32bB61468C8001E0C69Ef64ce6DEa57'), +('0xCf7C21DeD40f2Df85A564207A89b3379780d9CE3'), +('0x01Bc28E036b6e75247Fe8F49f0a8b9410b19d851'), +('0xe6db79364A2bb898e51e0b2350f6e00901f52773'), +('0xec8CAaf76d3cF02DF2E0A21C361C18A4b5401D5a'), +('0x2B888954421b424C5D3D9Ce9bB67c9bD47537d12'), +('0xB9573982875b83aaDc1296726E2ae77D13D9B98F'), +('0x1e55C85801a2C4F0beC57c84742a8eF3d72dE57B'), +('0xEB0C4F040FF0e2278bB2c14B7CC9c357467C83e3'), +('0x0419395ef65947B74aD9cCd1A9753251e72E411B'), +('0x6B35DFcED26650A950D17711beb21a35E2D70628'), +('0x7Eb84E42059F0D44269C50f4D3A280Fd307a6824'), +('0xb150c9bEd10a8C62997d58a81c4e1fA75160643e'), +('0x47ea9e812f39bc26F95Cbf1fc36230558492133d'), +('0xb2a3b5B9d2C0f07cBA328b58737147cfc172EB9f'), +('0xCe57ebEd9aC38402DcAA44f65a1c9b04e26b8283'), +('0x112a92c776542912A55350E64498Da9dfC7CA5c4'), +('0x90C32e6B29794Fd7f5BbA2BBEE74e924078B3f9b'), +('0x8a8fd287f9C0D69c8D749b32e10a1e0A96dedCf3'), +('0xfBDDB719cC7c795a1D9b7EA7aC11494A19b3231F'), +('0x57fb3f4b027fbaDbd8d20Eb5E48feb1e2b02DF30'), +('0x6685dd9cb58bA8d27f5e2E9eB54A0Fe301c8F78C'), +('0xe8568e4F3De23A9A213CC22C485CB1A74eD5935d'), +('0x968a0e5603c5D4dbF24cbd7df562921d158aD19C'), +('0xa96a437eFb71bAF50A59027C340FA3362ef703F7') + """ diff --git a/backend/migrations/versions/f9b48943e363_.py b/backend/migrations/versions/f9b48943e363_.py new file mode 100644 index 0000000000..d687292ab3 --- /dev/null +++ b/backend/migrations/versions/f9b48943e363_.py @@ -0,0 +1,44 @@ +"""empty message + +Revision ID: f9b48943e363 +Revises: a49d3e62bda9 +Create Date: 2023-07-04 10:15:37.497160 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "f9b48943e363" +down_revision = "a49d3e62bda9" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column(sa.Column("allocation_nonce", sa.Integer(), nullable=False)) + batch_op.add_column(sa.Column("glm_withdrawn", sa.Boolean(), nullable=True)) + batch_op.add_column( + sa.Column("glm_withdrawal_nonce", sa.Integer(), nullable=True) + ) + batch_op.create_unique_constraint( + "glm_withdrawal_nonce_unique_constraint", ["glm_withdrawal_nonce"] + ) + batch_op.drop_column("nonce") + + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + with op.batch_alter_table("users", schema=None) as batch_op: + batch_op.add_column(sa.Column("nonce", sa.INTEGER(), nullable=False)) + batch_op.drop_constraint(None, type_="unique") + batch_op.drop_column("glm_withdrawal_nonce") + batch_op.drop_column("glm_withdrawn") + batch_op.drop_column("allocation_nonce") + + # ### end Alembic commands ### diff --git a/backend/test_html/glm-claim.html b/backend/test_html/glm-claim.html new file mode 100644 index 0000000000..0cb3b8aa63 --- /dev/null +++ b/backend/test_html/glm-claim.html @@ -0,0 +1,87 @@ + + + + + + + + GLM Claim Octant Client + + + +
+
+
+
+ + + + + + diff --git a/backend/websocket_test/index.html b/backend/test_html/websocket.html similarity index 100% rename from backend/websocket_test/index.html rename to backend/test_html/websocket.html diff --git a/backend/tests/database/__init__.py b/backend/tests/database/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/backend/tests/database/test_allocations.py b/backend/tests/test_allocations_db.py similarity index 100% rename from backend/tests/database/test_allocations.py rename to backend/tests/test_allocations_db.py diff --git a/backend/tests/test_claims_db.py b/backend/tests/test_claims_db.py new file mode 100644 index 0000000000..275ccc274c --- /dev/null +++ b/backend/tests/test_claims_db.py @@ -0,0 +1,32 @@ +from app import database, db + + +def test_get_claims_by_glm_withdrawn_and_nonce_get(app, user_accounts): + user1 = database.claims.add_claim(user_accounts[0].address) + user1.claimed = True + user1.claim_nonce = 1 + + user2 = database.claims.add_claim(user_accounts[1].address) + user2.claimed = True + user2.claim_nonce = 2 + + database.claims.add_claim(user_accounts[2].address) + + db.session.commit() + + claims = database.claims.get_by_claimed_true_and_nonce_gte() + assert len(claims) == 2 + assert claims[0].address == user1.address + assert claims[1].address == user2.address + + claims = database.claims.get_by_claimed_true_and_nonce_gte(1) + assert len(claims) == 2 + assert claims[0].address == user1.address + assert claims[1].address == user2.address + + claims = database.claims.get_by_claimed_true_and_nonce_gte(2) + assert len(claims) == 1 + assert claims[0].address == user2.address + + claims = database.claims.get_by_claimed_true_and_nonce_gte(3) + assert len(claims) == 0 diff --git a/backend/tests/test_eip712.py b/backend/tests/test_eip712.py index b5c8573874..299a931fc4 100644 --- a/backend/tests/test_eip712.py +++ b/backend/tests/test_eip712.py @@ -1,4 +1,9 @@ -from app.crypto.eip712 import sign, recover_address, build_allocations_eip712_data +from app.crypto.eip712 import ( + sign, + recover_address, + build_allocations_eip712_data, + build_claim_glm_eip712_data, +) def test_sign_and_recover_address_from_allocations(user_accounts): @@ -50,3 +55,14 @@ def test_fails_when_data_to_recover_has_changed(user_accounts): address = recover_address(eip712_data, signature) assert address != account.address + + +def test_sign_and_recover_address_for_glm_claim(user_accounts): + account = user_accounts[0] + + eip712_data = build_claim_glm_eip712_data() + signature = sign(account, eip712_data) + + address = recover_address(eip712_data, signature) + + assert address == account.address diff --git a/backend/tests/test_glm_claim.py b/backend/tests/test_glm_claim.py new file mode 100644 index 0000000000..9f597646c3 --- /dev/null +++ b/backend/tests/test_glm_claim.py @@ -0,0 +1,70 @@ +import pytest + +from app import database, db, exceptions +from app.controllers.glm_claim import claim, check + + +def test_users_can_check_eligibility(app, user_accounts): + account1 = user_accounts[0] + + with pytest.raises(exceptions.NotEligibleToClaimGLM): + check(account1.address) + + database.claims.add_claim(account1.address) + db.session.commit() + + assert check(account1.address) + + account1_signature = "0xe15f64350e7cda9145144f7540775cf875cf19c323ccb17b79846eb6d6da98aa47299965cdfc16a81a378ef7bfd89752104188a2cbadb62ff447424be617854c1b" + claim(account1_signature) + + with pytest.raises(exceptions.GlmClaimed): + check(account1.address) + + +@pytest.mark.skip(reason="https://linear.app/golemfoundation/issue/OCT-612/use-testconfig-in-tests") +def test_users_can_claim_glms(app, user_accounts): + account1 = user_accounts[0] + account2 = user_accounts[1] + database.claims.add_claim(account1.address) + database.claims.add_claim(account2.address) + db.session.commit() + account1_signature = "0xe15f64350e7cda9145144f7540775cf875cf19c323ccb17b79846eb6d6da98aa47299965cdfc16a81a378ef7bfd89752104188a2cbadb62ff447424be617854c1b" + account2_signature = "0xbc0da5deb63cc8c1c4eb62a3fd48d84cc967955f01caa8e90f8a4bb5ea787ab2539001f4f9067a38974e55c6995bbd7522614d4171bac8e0e07566f1976a3c3e1b" + + claim(account1_signature) + claim(account2_signature) + + user1_after_claim = database.claims.get_by_address(account1.address) + assert user1_after_claim.claim_nonce == 9 + assert user1_after_claim.claimed is True + + user2_after_claim = database.claims.get_by_address(account2.address) + assert user2_after_claim.claim_nonce == 10 + assert user2_after_claim.claimed is True + + +@pytest.mark.skip(reason="https://linear.app/golemfoundation/issue/OCT-612/use-testconfig-in-tests") +def test_user_cannot_claim_twice(app, user_accounts): + account1 = user_accounts[0] + database.claims.add_claim(account1.address) + db.session.commit() + account1_signature = "0xe15f64350e7cda9145144f7540775cf875cf19c323ccb17b79846eb6d6da98aa47299965cdfc16a81a378ef7bfd89752104188a2cbadb62ff447424be617854c1b" + + claim(account1_signature) + + # Call claim second time, expect GlmWithdrawn exception + with pytest.raises(exceptions.GlmClaimed): + claim(account1_signature) + + user1_after_claim = database.claims.get_by_address(account1.address) + assert user1_after_claim.claim_nonce == 9 + assert user1_after_claim.claimed is True + + +def test_not_eligible_user_cannot_claim(app, user_accounts): + account1_signature = "0xe15f64350e7cda9145144f7540775cf875cf19c323ccb17b79846eb6d6da98aa47299965cdfc16a81a378ef7bfd89752104188a2cbadb62ff447424be617854c1b" + + # User is not present in db, expect NotEligibleToClaimGLM exception + with pytest.raises(exceptions.NotEligibleToClaimGLM): + claim(account1_signature) diff --git a/ci/argocd/templates/octant-application.yaml b/ci/argocd/templates/octant-application.yaml index 7b96a64699..d9cc748432 100644 --- a/ci/argocd/templates/octant-application.yaml +++ b/ci/argocd/templates/octant-application.yaml @@ -34,11 +34,17 @@ spec: value: "$EPOCH_0_END" - name: "webClient.alchemyId" value: "$VITE_ALCHEMY_ID" + - name: "backendServer.glmClaim.senderNonce" + value: "$GLM_SENDER_NONCE" # Hardcoded in this file - name: "contracts.addresses.multisigAddress" value: "$MULTISIG_ADDRESS" - name: "backendServer.schedulerEnabled" value: "True" + - name: "backendServer.glmClaim.enabled" + value: "True" + - name: "backendServer.snapshotter.enabled" + value: "False" - name: "contracts.deploy" value: "false" # From Deploy contracts job diff --git a/client/nix/sources.json b/client/nix/sources.json deleted file mode 100644 index 99fdf45f4d..0000000000 --- a/client/nix/sources.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "nixpkgs": { - "branch": "master", - "description": "Nix Packages collection", - "homepage": "", - "owner": "NixOS", - "repo": "nixpkgs", - "rev": "738fe494da28777ddeb2612c70a5dc909958df4b", - "sha256": "1im58ql0ilvk79pn349f1yzr4z6mycag40vlym78q1p9xlag2fkn", - "type": "tarball", - "url": "https://github.com/NixOS/nixpkgs/archive/738fe494da28777ddeb2612c70a5dc909958df4b.tar.gz", - "url_template": "https://github.com///archive/.tar.gz" - } -} diff --git a/client/nix/sources.nix b/client/nix/sources.nix deleted file mode 100644 index 41af0c6823..0000000000 --- a/client/nix/sources.nix +++ /dev/null @@ -1,176 +0,0 @@ -# This file has been generated by Niv. - -let - - # - # The fetchers. fetch_ fetches specs of type . - # - - fetch_file = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchurl { inherit (spec) url sha256; name = name'; } - else - pkgs.fetchurl { inherit (spec) url sha256; name = name'; }; - - fetch_tarball = pkgs: name: spec: - let - name' = sanitizeName name + "-src"; - in - if spec.builtin or true then - builtins_fetchTarball { name = name'; inherit (spec) url sha256; } - else - pkgs.fetchzip { name = name'; inherit (spec) url sha256; }; - - fetch_git = name: spec: - let - ref = - if spec ? ref then spec.ref else - if spec ? branch then "refs/heads/${spec.branch}" else - if spec ? tag then "refs/tags/${spec.tag}" else - abort "In git source '${name}': Please specify `ref`, `tag` or `branch`!"; - submodules = if spec ? submodules then spec.submodules else false; - in - builtins.fetchGit { url = spec.repo; inherit (spec) rev; inherit ref; } - // (if builtins.compareVersions builtins.nixVersion "2.4" >= 0 then { inherit submodules; } else {}); - - fetch_local = spec: spec.path; - - fetch_builtin-tarball = name: throw - ''[${name}] The niv type "builtin-tarball" is deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=tarball -a builtin=true''; - - fetch_builtin-url = name: throw - ''[${name}] The niv type "builtin-url" will soon be deprecated. You should instead use `builtin = true`. - $ niv modify ${name} -a type=file -a builtin=true''; - - # - # Various helpers - # - - # https://github.com/NixOS/nixpkgs/pull/83241/files#diff-c6f540a4f3bfa4b0e8b6bafd4cd54e8bR695 - sanitizeName = name: - ( - concatMapStrings (s: if builtins.isList s then "-" else s) - ( - builtins.split "[^[:alnum:]+._?=-]+" - ((x: builtins.elemAt (builtins.match "\\.*(.*)" x) 0) name) - ) - ); - - # The set of packages used when specs are fetched using non-builtins. - mkPkgs = sources: system: - let - sourcesNixpkgs = - import (builtins_fetchTarball { inherit (sources.nixpkgs) url sha256; }) { inherit system; }; - hasNixpkgsPath = builtins.any (x: x.prefix == "nixpkgs") builtins.nixPath; - hasThisAsNixpkgsPath = == ./.; - in - if builtins.hasAttr "nixpkgs" sources - then sourcesNixpkgs - else if hasNixpkgsPath && ! hasThisAsNixpkgsPath then - import {} - else - abort - '' - Please specify either (through -I or NIX_PATH=nixpkgs=...) or - add a package called "nixpkgs" to your sources.json. - ''; - - # The actual fetching function. - fetch = pkgs: name: spec: - - if ! builtins.hasAttr "type" spec then - abort "ERROR: niv spec ${name} does not have a 'type' attribute" - else if spec.type == "file" then fetch_file pkgs name spec - else if spec.type == "tarball" then fetch_tarball pkgs name spec - else if spec.type == "git" then fetch_git name spec - else if spec.type == "local" then fetch_local spec - else if spec.type == "builtin-tarball" then fetch_builtin-tarball name - else if spec.type == "builtin-url" then fetch_builtin-url name - else - abort "ERROR: niv spec ${name} has unknown type ${builtins.toJSON spec.type}"; - - # If the environment variable NIV_OVERRIDE_${name} is set, then use - # the path directly as opposed to the fetched source. - replace = name: drv: - let - saneName = stringAsChars (c: if isNull (builtins.match "[a-zA-Z0-9]" c) then "_" else c) name; - ersatz = builtins.getEnv "NIV_OVERRIDE_${saneName}"; - in - if ersatz == "" then drv else - # this turns the string into an actual Nix path (for both absolute and - # relative paths) - if builtins.substring 0 1 ersatz == "/" then /. + ersatz else /. + builtins.getEnv "PWD" + "/${ersatz}"; - - # Ports of functions for older nix versions - - # a Nix version of mapAttrs if the built-in doesn't exist - mapAttrs = builtins.mapAttrs or ( - f: set: with builtins; - listToAttrs (map (attr: { name = attr; value = f attr set.${attr}; }) (attrNames set)) - ); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/lists.nix#L295 - range = first: last: if first > last then [] else builtins.genList (n: first + n) (last - first + 1); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L257 - stringToCharacters = s: map (p: builtins.substring p 1 s) (range 0 (builtins.stringLength s - 1)); - - # https://github.com/NixOS/nixpkgs/blob/0258808f5744ca980b9a1f24fe0b1e6f0fecee9c/lib/strings.nix#L269 - stringAsChars = f: s: concatStrings (map f (stringToCharacters s)); - concatMapStrings = f: list: concatStrings (map f list); - concatStrings = builtins.concatStringsSep ""; - - # https://github.com/NixOS/nixpkgs/blob/8a9f58a375c401b96da862d969f66429def1d118/lib/attrsets.nix#L331 - optionalAttrs = cond: as: if cond then as else {}; - - # fetchTarball version that is compatible between all the versions of Nix - builtins_fetchTarball = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchTarball; - in - if lessThan nixVersion "1.12" then - fetchTarball ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchTarball attrs; - - # fetchurl version that is compatible between all the versions of Nix - builtins_fetchurl = { url, name ? null, sha256 }@attrs: - let - inherit (builtins) lessThan nixVersion fetchurl; - in - if lessThan nixVersion "1.12" then - fetchurl ({ inherit url; } // (optionalAttrs (!isNull name) { inherit name; })) - else - fetchurl attrs; - - # Create the final "sources" from the config - mkSources = config: - mapAttrs ( - name: spec: - if builtins.hasAttr "outPath" spec - then abort - "The values in sources.json should not have an 'outPath' attribute" - else - spec // { outPath = replace name (fetch config.pkgs name spec); } - ) config.sources; - - # The "config" used by the fetchers - mkConfig = - { sourcesFile ? if builtins.pathExists ./sources.json then ./sources.json else null - , sources ? if isNull sourcesFile then {} else builtins.fromJSON (builtins.readFile sourcesFile) - , system ? builtins.currentSystem - , pkgs ? mkPkgs sources system - }: rec { - # The sources, i.e. the attribute set of spec name to spec - inherit sources; - - # The "pkgs" (evaluated nixpkgs) to use for e.g. non-builtin fetchers - inherit pkgs; - }; - -in -mkSources (mkConfig {}) // { __functor = _: settings: mkSources (mkConfig settings); } diff --git a/client/shell.nix b/client/shell.nix deleted file mode 100644 index 72d16fbbb8..0000000000 --- a/client/shell.nix +++ /dev/null @@ -1,17 +0,0 @@ -# shell.nix -let - sources = import ./nix/sources.nix; - pkgs = import sources.nixpkgs {}; - yarn16 = pkgs.yarn.overrideAttrs (finalAttrs: previousAttrs: { - buildInputs = [ pkgs.nodejs-16_x ]; - }); -in - -pkgs.mkShell { - buildInputs = [ - pkgs.nodejs-16_x - pkgs.git - pkgs.ripgrep - yarn16 - ]; -} diff --git a/client/src/api/calls/glmClaim.ts b/client/src/api/calls/glmClaim.ts new file mode 100644 index 0000000000..ce28bce10e --- /dev/null +++ b/client/src/api/calls/glmClaim.ts @@ -0,0 +1,18 @@ +import env from 'env'; +import apiService from 'services/apiService'; + +export function apiPostGlmClaim(signature: string): Promise { + return apiService.post(`${env.serverEndpoint}glm/claim`, { signature }).then(({ data }) => data); +} + +export type GetGlmClaimCheckResponse = { + address: string; + // Number of GLMs in wei + claimable: string; +}; + +export function apiGetGlmClaimCheck(userAddress: string): Promise { + return apiService + .get(`${env.serverEndpoint}glm/claim/${userAddress}/check`) + .then(({ data }) => data); +} diff --git a/client/src/api/errorMessages/index.ts b/client/src/api/errorMessages/index.ts index d9af791f41..504dd2f2f7 100644 --- a/client/src/api/errorMessages/index.ts +++ b/client/src/api/errorMessages/index.ts @@ -1,11 +1,13 @@ /* eslint-disable @typescript-eslint/naming-convention */ import { Query } from '@tanstack/react-query'; -import { ROOTS } from 'api/queryKeys'; +import { ROOTS, QUERY_KEYS } from 'api/queryKeys'; import i18n from 'i18n'; import triggerToast from 'utils/triggerToast'; -import { QueryMutationError, QueryMutationErrorConfig } from './types'; +import { QueryMutationError, QueryMutationErrorConfig, IgnoredQueries} from './types'; + +const IGNORED_QUERIES: IgnoredQueries = [ROOTS.cryptoValues, QUERY_KEYS.glmClaimCheck[0]]; const errors: QueryMutationErrorConfig = { 'HN:Allocations/allocate-above-rewards-budget': { @@ -51,8 +53,8 @@ function getError(reason: string): QueryMutationError { export function handleError(reason: string, query?: Query | unknown): string | undefined { // @ts-expect-error mutations do not have queryKey field, they are pure value and are unknown. - if (query && query.queryKey?.find(element => element === ROOTS.cryptoValues)) { - // Graceful failure, no notification, no error. Inline info shown in places for values. + if (query && query.queryKey?.find(element => IGNORED_QUERIES.includes(element))) { + // No notification. Either graceful failure, or local handling. return; } diff --git a/client/src/api/errorMessages/types.ts b/client/src/api/errorMessages/types.ts index 0cd7d1d763..2431cb970f 100644 --- a/client/src/api/errorMessages/types.ts +++ b/client/src/api/errorMessages/types.ts @@ -1,3 +1,5 @@ +import { QueryKeys, Root } from 'api/queryKeys/types'; + export type QueryMutationError = { message: string; type: 'inline' | 'toast'; @@ -6,3 +8,5 @@ export type QueryMutationError = { export type QueryMutationErrorConfig = { [key: string]: QueryMutationError; }; + +export type IgnoredQueries = [Root['cryptoValues'], QueryKeys['glmClaimCheck'][0]]; diff --git a/client/src/api/queryKeys/index.ts b/client/src/api/queryKeys/index.ts index ad735dc36a..fff8cd65f1 100644 --- a/client/src/api/queryKeys/index.ts +++ b/client/src/api/queryKeys/index.ts @@ -16,7 +16,9 @@ export const QUERY_KEYS: QueryKeys = { currentEpochProps: ['currentEpochProps'], depositAtGivenEpoch: epochNumber => [ROOTS.depositAt, epochNumber.toString()], depositsValue: ['depositsValue'], + glmClaimCheck: ['glmClaimCheck'], history: ['history'], + individualProposalRewards: ['individualProposalRewards'], individualReward: ['individualReward'], isDecisionWindowOpen: ['isDecisionWindowOpen'], lockedSummaryLatest: ['lockedSummaryLatest'], diff --git a/client/src/api/queryKeys/types.ts b/client/src/api/queryKeys/types.ts index 33ead6af3e..09ae31a77f 100644 --- a/client/src/api/queryKeys/types.ts +++ b/client/src/api/queryKeys/types.ts @@ -18,7 +18,9 @@ export type QueryKeys = { currentEpochProps: ['currentEpochProps']; depositAtGivenEpoch: (epochNumber: number) => [Root['depositAt'], string]; depositsValue: ['depositsValue']; + glmClaimCheck: ['glmClaimCheck']; history: ['history']; + individualProposalRewards: ['individualProposalRewards']; individualReward: ['individualReward']; isDecisionWindowOpen: ['isDecisionWindowOpen']; lockedSummaryLatest: ['lockedSummaryLatest']; diff --git a/client/src/components/dedicated/ButtonClaimGlm/ButtonGlmClaim.tsx b/client/src/components/dedicated/ButtonClaimGlm/ButtonGlmClaim.tsx new file mode 100644 index 0000000000..fef5a3da4e --- /dev/null +++ b/client/src/components/dedicated/ButtonClaimGlm/ButtonGlmClaim.tsx @@ -0,0 +1,25 @@ +import React, { FC } from 'react'; +import { useTranslation } from 'react-i18next'; + +import Button from 'components/core/Button/Button'; + +import ButtonGlmClaim from './types'; + +const ButtonClaimGlm: FC = ({ className, glmClaimMutation }) => { + const { t } = useTranslation('translation', { + keyPrefix: 'views.onboarding.steps.claimGlm.buttonLabel', + }); + + return ( +