From 8e27a4bb2ff60f164f0afb4996a0e6d747141ca9 Mon Sep 17 00:00:00 2001 From: CryptoRush <98655210+leric7@users.noreply.github.com> Date: Wed, 30 Aug 2023 21:09:27 +0800 Subject: [PATCH] feat: Getter functions of SDK (#833) * add data model for statistics data * add getter function for escrow and statistics * update gql queries for typescript and python sdk * fix storage client related issues --- .../src/modules/webhook/webhook.service.ts | 2 +- .../sdk/python/human-protocol-sdk/Makefile | 3 + .../sdk/python/human-protocol-sdk/example.py | 64 ++++ .../human_protocol_sdk/constants.py | 16 +- .../human_protocol_sdk/encryption.py | 9 +- .../human_protocol_sdk/escrow.py | 76 ++--- .../human_protocol_sdk/gql/escrow.py | 58 ++-- .../human_protocol_sdk/gql/hmtoken.py | 17 + .../human_protocol_sdk/gql/reward.py | 4 +- .../human_protocol_sdk/gql/statistics.py | 93 +++++ .../human_protocol_sdk/kvstore.py | 6 +- .../human_protocol_sdk/staking.py | 18 +- .../human_protocol_sdk/statistics.py | 287 ++++++++++++++++ .../human_protocol_sdk/storage.py | 2 +- .../test/human_protocol_sdk/test_escrow.py | 119 +++---- .../test/human_protocol_sdk/test_kvstore.py | 11 + .../test/human_protocol_sdk/test_staking.py | 11 + .../human_protocol_sdk/test_statistics.py | 320 ++++++++++++++++++ .../human-protocol-sdk/example/escrow.ts | 29 ++ .../human-protocol-sdk/example/statistics.ts | 104 ++++++ .../human-protocol-sdk/src/constants.ts | 16 +- .../human-protocol-sdk/src/escrow.ts | 48 +-- .../src/graphql/queries/escrow.ts | 56 +-- .../src/graphql/queries/hmtoken.ts | 17 + .../src/graphql/queries/index.ts | 2 + .../src/graphql/queries/statistics.ts | 88 +++++ .../human-protocol-sdk/src/graphql/types.ts | 105 ++++++ .../human-protocol-sdk/src/index.ts | 9 +- .../human-protocol-sdk/src/interfaces.ts | 19 +- .../human-protocol-sdk/src/statistics.ts | 196 +++++++++++ .../human-protocol-sdk/test/escrow.test.ts | 126 ++++--- .../test/statistics.test.ts | 302 +++++++++++++++++ 32 files changed, 1948 insertions(+), 285 deletions(-) create mode 100644 packages/sdk/python/human-protocol-sdk/example.py create mode 100644 packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/hmtoken.py create mode 100644 packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/statistics.py create mode 100644 packages/sdk/python/human-protocol-sdk/human_protocol_sdk/statistics.py create mode 100644 packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_statistics.py create mode 100644 packages/sdk/typescript/human-protocol-sdk/example/escrow.ts create mode 100644 packages/sdk/typescript/human-protocol-sdk/example/statistics.ts create mode 100644 packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/hmtoken.ts create mode 100644 packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/statistics.ts create mode 100644 packages/sdk/typescript/human-protocol-sdk/src/statistics.ts create mode 100644 packages/sdk/typescript/human-protocol-sdk/test/statistics.test.ts diff --git a/packages/apps/reputation-oracle/server/src/modules/webhook/webhook.service.ts b/packages/apps/reputation-oracle/server/src/modules/webhook/webhook.service.ts index cbe62370d5..3907a4efd7 100644 --- a/packages/apps/reputation-oracle/server/src/modules/webhook/webhook.service.ts +++ b/packages/apps/reputation-oracle/server/src/modules/webhook/webhook.service.ts @@ -66,8 +66,8 @@ export class WebhookService { this.bucket = this.configService.get(ConfigNames.S3_BUCKET)!; this.storageClient = new StorageClient( - storageCredentials, this.storageParams, + storageCredentials, ); } diff --git a/packages/sdk/python/human-protocol-sdk/Makefile b/packages/sdk/python/human-protocol-sdk/Makefile index 01e80b4868..d84eb47dac 100644 --- a/packages/sdk/python/human-protocol-sdk/Makefile +++ b/packages/sdk/python/human-protocol-sdk/Makefile @@ -38,3 +38,6 @@ build-package: publish-package: make build-package twine upload dist/* --skip-existing + +run-example: + pipenv run python3 example.py diff --git a/packages/sdk/python/human-protocol-sdk/example.py b/packages/sdk/python/human-protocol-sdk/example.py new file mode 100644 index 0000000000..f1596a78be --- /dev/null +++ b/packages/sdk/python/human-protocol-sdk/example.py @@ -0,0 +1,64 @@ +import datetime +from web3 import Web3 + +from human_protocol_sdk.escrow import EscrowClient, EscrowFilter, Status +from human_protocol_sdk.statistics import StatisticsClient, StatisticsParam + +if __name__ == "__main__": + alchemy_url = ( + "https://polygon-mumbai.g.alchemy.com/v2/lnog1fIT7pvL4_o3lkcosQ7PL08ed3nX" + ) + w3 = Web3(Web3.HTTPProvider(alchemy_url)) + + escrow_client = EscrowClient(w3) + + print( + escrow_client.get_escrows( + EscrowFilter( + status=Status.Pending, + date_from=datetime.datetime(2023, 5, 8), + date_to=datetime.datetime(2023, 6, 8), + ) + ) + ) + + statistics_client = StatisticsClient(w3) + + print(statistics_client.get_escrow_statistics()) + print( + statistics_client.get_escrow_statistics( + StatisticsParam( + date_from=datetime.datetime(2023, 5, 8), + date_to=datetime.datetime(2023, 6, 8), + ) + ) + ) + + print(statistics_client.get_worker_statistics()) + print( + statistics_client.get_worker_statistics( + StatisticsParam( + date_from=datetime.datetime(2023, 5, 8), + date_to=datetime.datetime(2023, 6, 8), + ) + ) + ) + + print(statistics_client.get_payment_statistics()) + print( + statistics_client.get_payment_statistics( + StatisticsParam( + date_from=datetime.datetime(2023, 5, 8), + date_to=datetime.datetime(2023, 6, 8), + ) + ) + ) + print(statistics_client.get_hmt_statistics()) + print( + statistics_client.get_hmt_statistics( + StatisticsParam( + date_from=datetime.datetime(2023, 5, 8), + date_to=datetime.datetime(2023, 6, 8), + ) + ) + ) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py index 7ea73c6c87..4b390ede8f 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/constants.py @@ -24,7 +24,7 @@ class ChainId(Enum): ChainId.MAINNET: { "title": "Ethereum", "scan_url": "https://etherscan.io", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2", "hmt_address": "0xd1ba9BAC957322D6e8c07a160a3A8dA11A0d2867", "factory_address": "0xD9c75a1Aa4237BB72a41E5E26bd8384f10c1f55a", "staking_address": "0x05398211bA2046E296fBc9a9D3EB49e3F15C3123", @@ -36,7 +36,7 @@ class ChainId(Enum): ChainId.GOERLI: { "title": "Ethereum Goerli", "scan_url": "https://goerli.etherscan.io", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/goerli-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/goerli-v2", "hmt_address": "0xd3A31D57FDD790725d0F6B78095F62E8CD4ab317", "factory_address": "0x87469B4f2Fcf37cBd34E54244c0BD4Fa0603664c", "staking_address": "0xf46B45Df3d956369726d8Bd93Ba33963Ab692920", @@ -48,7 +48,7 @@ class ChainId(Enum): ChainId.BSC_MAINNET: { "title": "Binance Smart Chain", "scan_url": "https://bscscan.com", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/bsc-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/bsc-v2", "hmt_address": "0x711Fd6ab6d65A98904522d4e3586F492B989c527", "factory_address": "0x92FD968AcBd521c232f5fB8c33b342923cC72714", "staking_address": "0xdFbB79dC35a3A53741be54a2C9b587d6BafAbd1C", @@ -60,7 +60,7 @@ class ChainId(Enum): ChainId.BSC_TESTNET: { "title": "Binance Smart Chain (Testnet)", "scan_url": "https://testnet.bscscan.com", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest-v2", "hmt_address": "0xE3D74BBFa45B4bCa69FF28891fBE392f4B4d4e4d", "factory_address": "0x2bfA592DBDaF434DDcbb893B1916120d181DAD18", "staking_address": "0x5517fE916Fe9F8dB15B0DDc76ebDf0BdDCd4ed18", @@ -72,7 +72,7 @@ class ChainId(Enum): ChainId.POLYGON: { "title": "Polygon", "scan_url": "https://polygonscan.com", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/polygon-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/polygon-v2", "hmt_address": "0xc748B2A084F8eFc47E086ccdDD9b7e67aEb571BF", "factory_address": "0xBDBfD2cC708199C5640C6ECdf3B0F4A4C67AdfcB", "staking_address": "0xcbAd56bE3f504E98bd70875823d3CC0242B7bB29", @@ -84,7 +84,7 @@ class ChainId(Enum): ChainId.POLYGON_MUMBAI: { "title": "Polygon Mumbai", "scan_url": "https://mumbai.polygonscan.com", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/mumbai-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/mumbai-v2", "hmt_address": "0x0376D26246Eb35FF4F9924cF13E6C05fd0bD7Fb4", "factory_address": "0xA8D927C4DA17A6b71675d2D49dFda4E9eBE58f2d", "staking_address": "0x7Fd3dF914E7b6Bd96B4c744Df32183b51368Bfac", @@ -96,7 +96,7 @@ class ChainId(Enum): ChainId.MOONBEAM: { "title": "Moonbeam", "scan_url": "https://moonbeam.moonscan.io", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/moonbeam-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/moonbeam-v2", "hmt_address": "0x3b25BC1dC591D24d60560d0135D6750A561D4764", "factory_address": "0xD9c75a1Aa4237BB72a41E5E26bd8384f10c1f55a", "staking_address": "0x05398211bA2046E296fBc9a9D3EB49e3F15C3123", @@ -108,7 +108,7 @@ class ChainId(Enum): ChainId.MOONBASE_ALPHA: { "title": "Moonbase Alpha", "scan_url": "https://moonbase.moonscan.io/", - "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/moonbase-alpha-v1", + "subgraph_url": "https://api.thegraph.com/subgraphs/name/humanprotocol/moonbase-alpha-v2", "hmt_address": "0x2dd72db2bBA65cE663e476bA8b84A1aAF802A8e3", "factory_address": "0x5e622FF522D81aa426f082bDD95210BC25fCA7Ed", "staking_address": "0xBFC7009F3371F93F3B54DdC8caCd02914a37495c", diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/encryption.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/encryption.py index c0e6085e39..bf673785aa 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/encryption.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/encryption.py @@ -1,11 +1,6 @@ from typing import Optional, List -from pgpy import PGPKey, PGPMessage, PGPUID -from pgpy.constants import ( - SymmetricKeyAlgorithm, - HashAlgorithm, - KeyFlags, - PubKeyAlgorithm, -) +from pgpy import PGPKey, PGPMessage +from pgpy.constants import SymmetricKeyAlgorithm from pgpy.errors import PGPError diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/escrow.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/escrow.py index 45017fb996..b7b7c8a7ec 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/escrow.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/escrow.py @@ -7,10 +7,6 @@ from typing import List, Optional from human_protocol_sdk.constants import NETWORKS, ChainId, Role, Status -from human_protocol_sdk.gql.escrow import ( - get_escrows_by_launcher_query, - get_filtered_escrows_query, -) from human_protocol_sdk.utils import ( get_data_from_subgraph, get_escrow_interface, @@ -96,7 +92,9 @@ class EscrowFilter: def __init__( self, - launcher_address: Optional[str] = None, + launcher: Optional[str] = None, + reputation_oracle: Optional[str] = None, + recording_oracle: Optional[str] = None, status: Optional[Status] = None, date_from: Optional[datetime.datetime] = None, date_to: Optional[datetime.datetime] = None, @@ -105,23 +103,30 @@ def __init__( Initializes a EscrowFilter instance. Args: - launcher_address (Optional[str]): Launcher ddress + launcher (Optional[str]): Launcher address + reputation_oracle (Optional[str]): Reputation oracle address + recording_oracle (Optional[str]): Recording oracle address status (Optional[Status]): Escrow status - date_from (Optional[date]): Created from date - date_to (Optional[date]): Created to date + date_from (Optional[datetime.datetime]): Created from date + date_to (Optional[datetime.datetime]): Created to date """ - if not launcher_address and not status and not date_from and not date_to: - raise EscrowClientError( - "EscrowFilter class must have at least one parameter" - ) - if launcher_address and not Web3.is_address(launcher_address): - raise EscrowClientError(f"Invalid address: {launcher_address}") + if launcher and not Web3.is_address(launcher): + raise EscrowClientError(f"Invalid address: {launcher}") + + if reputation_oracle and not Web3.is_address(reputation_oracle): + raise EscrowClientError(f"Invalid address: {reputation_oracle}") + + if recording_oracle and not Web3.is_address(recording_oracle): + raise EscrowClientError(f"Invalid address: {recording_oracle}") + if date_from and date_to and date_from > date_to: raise EscrowClientError( f"Invalid dates: {date_from} must be earlier than {date_to}" ) - self.launcher_address = launcher_address + self.launcher = launcher + self.reputation_oracle = reputation_oracle + self.recording_oracle = recording_oracle self.status = status self.date_from = date_from self.date_to = date_to @@ -145,12 +150,16 @@ def __init__(self, web3: Web3): if not self.w3.middleware_onion.get("geth_poa"): self.w3.middleware_onion.inject(geth_poa_middleware, "geth_poa", layer=0) + chain_id = None # Load network configuration based on chainId try: chain_id = self.w3.eth.chain_id self.network = NETWORKS[ChainId(chain_id)] except: - raise EscrowClientError(f"Invalid ChainId: {chain_id}") + if chain_id is not None: + raise EscrowClientError(f"Invalid ChainId: {chain_id}") + else: + raise EscrowClientError(f"Invalid Web3 Instance") # Initialize contract instances factory_interface = get_factory_interface() @@ -589,26 +598,7 @@ def get_status(self, escrow_address: str) -> Status: self._get_escrow_contract(escrow_address).functions.status().call() ) - def get_launched_escrows(self, launcher_address: str) -> List[dict]: - """Get escrows addresses created by a job launcher. - - Args: - launcher_address (str): Address of the launcher - - Returns: - List[dict]: List of escrows - """ - - escrows_data = get_data_from_subgraph( - self.network["subgraph_url"], - query=get_escrows_by_launcher_query, - params={"launcherAddress": launcher_address}, - ) - escrows = escrows_data["data"]["escrows"] - - return escrows - - def get_escrows_filtered(self, filter: EscrowFilter) -> List[dict]: + def get_escrows(self, filter: EscrowFilter = EscrowFilter()) -> List[dict]: """Get an array of escrow addresses based on the specified filter parameters. Args: @@ -617,19 +607,25 @@ def get_escrows_filtered(self, filter: EscrowFilter) -> List[dict]: Returns: List[dict]: List of escrows """ + from human_protocol_sdk.gql.escrow import ( + get_escrows_query, + ) + escrows_data = get_data_from_subgraph( self.network["subgraph_url"], - query=get_filtered_escrows_query, + query=get_escrows_query(filter), params={ - "launcherAddress": filter.launcher_address, + "launcher": filter.launcher, + "reputationOracle": filter.reputation_oracle, + "recordingOracle": filter.recording_oracle, "status": filter.status.name if filter.status else None, "from": int(filter.date_from.timestamp()) if filter.date_from else None, "to": int(filter.date_to.timestamp()) if filter.date_to else None, }, ) - launched_escrows = escrows_data["data"]["escrows"] + escrows = escrows_data["data"]["escrows"] - return launched_escrows + return escrows def get_recording_oracle_address(self, escrow_address: str) -> str: """Gets the recording oracle address of the escrow. diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py index 776b80e9f6..6f0986d1b5 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/escrow.py @@ -1,5 +1,7 @@ +from human_protocol_sdk.escrow import EscrowFilter + escrow_fragment = """ -fragment EscrowFields on Escrow {{ +fragment EscrowFields on Escrow { address amountPaid balance @@ -18,39 +20,45 @@ status token totalFundedAmount -}} + createdAt +} """ -get_escrows_by_launcher_query = """ -query GetEscrowByLauncher($launcherAddress: String!) {{ - escrows(where: {{ launcher: $launcherAddress }}) {{ - ...EscrowFields - }} -}} -{escrow_fragment} -""".format( - escrow_fragment=escrow_fragment -) -get_filtered_escrows_query = """ -query GetFilteredEscrows( - $launcherAddress: String - $status: EscrowStatus +def get_escrows_query(filter: EscrowFilter): + return """ +query GetEscrows( + $launcher: String + $reputationOracle: String + $recordingOracle: String + $status: String $from: Int $to: Int ) {{ escrows( - where: {{ - launcher: $launcherAddress - status: $status - createdAt_gte: $from - createdAt_lte: $to - }} + where: {{ + {launcher_clause} + {reputation_oracle_clause} + {recording_oracle_clause} + {status_clause} + {from_clause} + {to_clause} + }} ) {{ - ...EscrowFields + ...EscrowFields }} }} {escrow_fragment} """.format( - escrow_fragment=escrow_fragment -) + escrow_fragment=escrow_fragment, + launcher_clause="launcher: $launcher" if filter.launcher else "", + reputation_oracle_clause="reputationOracle: $reputationOracle" + if filter.reputation_oracle + else "", + recording_oracle_clause="recordingOracle: $recordingOracle" + if filter.recording_oracle + else "", + status_clause="status: $status" if filter.status else "", + from_clause="createdAt_gte: $from" if filter.date_from else "", + to_clause="createdAt_lte: $to" if filter.date_from else "", + ) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/hmtoken.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/hmtoken.py new file mode 100644 index 0000000000..a9bd32c28e --- /dev/null +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/hmtoken.py @@ -0,0 +1,17 @@ +holder_fragment = """ +fragment HolderFields on Holder { + address + balance +} +""" + +get_holders_query = """ +query GetHolders {{ + holders {{ + ...HolderFields + }} +}} +{holder_fragment} +""".format( + holder_fragment=holder_fragment +) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py index 3c3378b384..3f43bbff9e 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/reward.py @@ -1,10 +1,10 @@ reward_added_event_fragment = """ -fragment RewardAddedEventFields on RewardAddedEvent {{ +fragment RewardAddedEventFields on RewardAddedEvent { escrowAddress staker slasher amount -}} +} """ get_reward_added_events_query = """ diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/statistics.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/statistics.py new file mode 100644 index 0000000000..41af029390 --- /dev/null +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/gql/statistics.py @@ -0,0 +1,93 @@ +from human_protocol_sdk.statistics import StatisticsParam + +hmtoken_statistics_fragment = """ +fragment HMTokenStatisticsFields on HMTokenStatistics { + totalTransferEventCount + totalBulkTransferEventCount + totalApprovalEventCount + totalBulkApprovalEventCount + totalValueTransfered + holders +} +""" + +escrow_statistics_fragment = """ +fragment EscrowStatisticsFields on EscrowStatistics { + fundEventCount + setupEventCount + storeResultsEventCount + bulkPayoutEventCount + pendingStatusEventCount + cancelledStatusEventCount + partialStatusEventCount + paidStatusEventCount + completedStatusEventCount + totalEventCount + totalEscrowCount +} +""" + +event_day_data_fragment = """ +fragment EventDayDataFields on EventDayData { + timestamp + dailyFundEventCount + dailySetupEventCount + dailyStoreResultsEventCount + dailyBulkPayoutEventCount + dailyPendingStatusEventCount + dailyCancelledStatusEventCount + dailyPartialStatusEventCount + dailyPaidStatusEventCount + dailyCompletedStatusEventCount + dailyTotalEventCount + dailyEscrowCount + dailyWorkerCount + dailyPayoutCount + dailyPayoutAmount + dailyHMTTransferCount + dailyHMTTransferAmount +} +""" + +get_hmtoken_statistics_query = """ +query GetHMTokenStatistics {{ + hmtokenStatistics(id: "hmt-statistics-id") {{ + ...HMTokenStatisticsFields + }} +}} +{hmtoken_statistics_fragment} +""".format( + hmtoken_statistics_fragment=hmtoken_statistics_fragment +) + +get_escrow_statistics_query = """ +query GetEscrowStatistics {{ + escrowStatistics(id: "escrow-statistics-id") {{ + ...EscrowStatisticsFields + }} +}} +{escrow_statistics_fragment} +""".format( + escrow_statistics_fragment=escrow_statistics_fragment +) + + +def get_event_day_data_query(param: StatisticsParam): + return """ +query GetEscrowDayData($from: Int, $to: Int) {{ + eventDayDatas( + where: {{ + {from_clause} + {to_clause} + + }} + ) {{ + ...EventDayDataFields + }} +}} +{event_day_data_fragment} +""".format( + event_day_data_fragment=event_day_data_fragment, + from_clause="timestamp_gte: $from" if param.date_from else "", + to_clause="timestamp_lte: $to" if param.date_to else "", + ) diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/kvstore.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/kvstore.py index ac7ce7eeab..d51de57c15 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/kvstore.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/kvstore.py @@ -41,12 +41,16 @@ def __init__(self, web3: Web3): if not self.w3.middleware_onion.get("geth_poa"): self.w3.middleware_onion.inject(geth_poa_middleware, "geth_poa", layer=0) + chain_id = None # Load network configuration based on chainId try: chain_id = self.w3.eth.chain_id self.network = NETWORKS[ChainId(chain_id)] except: - raise KVStoreClientError(f"Invalid ChainId: {chain_id}") + if chain_id is not None: + raise KVStoreClientError(f"Invalid ChainId: {chain_id}") + else: + raise KVStoreClientError(f"Invalid Web3 Instance") # Initialize contract instances kvstore_interface = get_kvstore_interface() diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/staking.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/staking.py index 1811946469..16a5d534c5 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/staking.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/staking.py @@ -27,8 +27,6 @@ class StakingClientError(Exception): - """Raises when some error happens when interacting with staking.""" - """ Raises when some error happens when interacting with staking. """ @@ -37,14 +35,8 @@ class StakingClientError(Exception): class StakingClient: - """A class used to manage staking, and allocation on the HUMAN network. - - Args: - staking_addr (str): The address of staking contract - - Attributes: - - + """ + A class used to manage staking, and allocation on the HUMAN network. """ def __init__(self, w3: Web3): @@ -59,12 +51,16 @@ def __init__(self, w3: Web3): if not self.w3.middleware_onion.get("geth_poa"): self.w3.middleware_onion.inject(geth_poa_middleware, "geth_poa", layer=0) + chain_id = None # Load network configuration based on chain_id try: chain_id = self.w3.eth.chain_id self.network = NETWORKS[ChainId(chain_id)] except: - raise StakingClientError(f"Invalid ChainId: {chain_id}") + if chain_id is not None: + raise StakingClientError(f"Invalid ChainId: {chain_id}") + else: + raise StakingClientError(f"Invalid Web3 Instance") if not self.network: raise StakingClientError("Empty network configuration") diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/statistics.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/statistics.py new file mode 100644 index 0000000000..ee91b60df3 --- /dev/null +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/statistics.py @@ -0,0 +1,287 @@ +#!/usr/bin/env python3 + +import datetime +import logging +import os + +from typing import Optional + +from web3 import Web3 + +from human_protocol_sdk.constants import ChainId, NETWORKS +from human_protocol_sdk.gql.hmtoken import get_holders_query + +from human_protocol_sdk.utils import ( + get_data_from_subgraph, +) + +GAS_LIMIT = int(os.getenv("GAS_LIMIT", 4712388)) + +LOG = logging.getLogger("human_protocol_sdk.statistics") + + +class StatisticsClientError(Exception): + """ + Raises when some error happens when getting data from subgraph. + """ + + pass + + +class StatisticsParam: + """ + A class used to specify statistics params. + """ + + def __init__( + self, + date_from: Optional[datetime.datetime] = None, + date_to: Optional[datetime.datetime] = None, + ): + """ + Initializes a StatisticsParam instance. + + Args: + date_from (Optional[datetime.datetime]): Statistical data from date + date_to (Optional[datetime.datetime]): Statistical data to date + """ + + self.date_from = date_from + self.date_to = date_to + + +class StatisticsClient: + """ + A client used to get statistical data. + """ + + def __init__(self, w3: Web3): + """Initializes a Staking instance + + Args: + + """ + + # Initialize web3 instance + self.w3 = w3 + + chain_id = None + # Load network configuration based on chain_id + try: + chain_id = self.w3.eth.chain_id + self.network = NETWORKS[ChainId(chain_id)] + except: + if chain_id is not None: + raise StatisticsClientError(f"Invalid ChainId: {chain_id}") + else: + raise StatisticsClientError(f"Invalid Web3 Instance") + + if not self.network: + raise StatisticsClientError("Empty network configuration") + + def get_escrow_statistics(self, param: StatisticsParam = StatisticsParam()) -> dict: + """Get escrow statistics data for the given date range. + + Args: + param (StatisticsParam): Object containing the date range + + Returns: + dict: Escrow statistics data + """ + from human_protocol_sdk.gql.statistics import ( + get_event_day_data_query, + get_escrow_statistics_query, + ) + + escrow_statistics_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_escrow_statistics_query, + ) + escrow_statistics = escrow_statistics_data["data"]["escrowStatistics"] + + event_day_datas_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_event_day_data_query(param), + params={ + "from": int(param.date_from.timestamp()) if param.date_from else None, + "to": int(param.date_to.timestamp()) if param.date_to else None, + }, + ) + event_day_datas = event_day_datas_data["data"]["eventDayDatas"] + + return { + "total_escrows": int(escrow_statistics["totalEscrowCount"]), + "daily_escrows_data": [ + { + "timestamp": datetime.datetime.fromtimestamp( + int(event_day_data["timestamp"]) + ), + "escrows_total": int(event_day_data["dailyEscrowCount"]), + "escrows_pending": int( + event_day_data["dailyPendingStatusEventCount"] + ), + "escrows_solved": int( + event_day_data["dailyCompletedStatusEventCount"] + ), + "escrows_paid": int(event_day_data["dailyPaidStatusEventCount"]), + "escrows_cancelled": int( + event_day_data["dailyCancelledStatusEventCount"] + ), + } + for event_day_data in event_day_datas + ], + } + + def get_worker_statistics(self, param: StatisticsParam = StatisticsParam()) -> dict: + """Get worker statistics data for the given date range. + + Args: + param (StatisticsParam): Object containing the date range + + Returns: + dict: Worker statistics data + """ + + from human_protocol_sdk.gql.statistics import ( + get_event_day_data_query, + ) + + event_day_datas_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_event_day_data_query(param), + params={ + "from": int(param.date_from.timestamp()) if param.date_from else None, + "to": int(param.date_to.timestamp()) if param.date_to else None, + }, + ) + event_day_datas = event_day_datas_data["data"]["eventDayDatas"] + + return { + "daily_workers_data": [ + { + "timestamp": datetime.datetime.fromtimestamp( + int(event_day_data["timestamp"]) + ), + "active_workers": int(event_day_data["dailyWorkerCount"]), + "average_jobs_solved": int( + event_day_data["dailyBulkPayoutEventCount"] + ) + / int(event_day_data["dailyWorkerCount"]) + if event_day_data["dailyWorkerCount"] != "0" + else 0, + } + for event_day_data in event_day_datas + ], + } + + def get_payment_statistics( + self, param: StatisticsParam = StatisticsParam() + ) -> dict: + """Get payment statistics data for the given date range. + + Args: + param (StatisticsParam): Object containing the date range + + Returns: + dict: Payment statistics data + + """ + + from human_protocol_sdk.gql.statistics import ( + get_event_day_data_query, + ) + + event_day_datas_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_event_day_data_query(param), + params={ + "from": int(param.date_from.timestamp()) if param.date_from else None, + "to": int(param.date_to.timestamp()) if param.date_to else None, + }, + ) + event_day_datas = event_day_datas_data["data"]["eventDayDatas"] + + return { + "daily_payments_data": [ + { + "timestamp": datetime.datetime.fromtimestamp( + int(event_day_data["timestamp"]) + ), + "total_amount_paid": int(event_day_data["dailyPayoutAmount"]), + "total_count": int(event_day_data["dailyPayoutCount"]), + "average_amount_per_job": int(event_day_data["dailyPayoutAmount"]) + / int(event_day_data["dailyBulkPayoutEventCount"]) + if event_day_data["dailyBulkPayoutEventCount"] != "0" + else 0, + "average_amount_per_worker": int( + event_day_data["dailyPayoutAmount"] + ) + / int(event_day_data["dailyWorkerCount"]) + if event_day_data["dailyWorkerCount"] != "0" + else 0, + } + for event_day_data in event_day_datas + ], + } + + def get_hmt_statistics(self, param: StatisticsParam = StatisticsParam()) -> dict: + """Get HMT statistics data for the given date range. + + Args: + param (StatisticsParam): Object containing the date range + + Returns: + dict: HMT statistics data + """ + from human_protocol_sdk.gql.statistics import ( + get_event_day_data_query, + get_hmtoken_statistics_query, + ) + + hmtoken_statistics_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_hmtoken_statistics_query, + ) + hmtoken_statistics = hmtoken_statistics_data["data"]["hmtokenStatistics"] + + holders_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_holders_query, + ) + holders = holders_data["data"]["holders"] + + event_day_datas_data = get_data_from_subgraph( + self.network["subgraph_url"], + query=get_event_day_data_query(param), + params={ + "from": int(param.date_from.timestamp()) if param.date_from else None, + "to": int(param.date_to.timestamp()) if param.date_to else None, + }, + ) + event_day_datas = event_day_datas_data["data"]["eventDayDatas"] + + return { + "total_transfer_amount": int(hmtoken_statistics["totalValueTransfered"]), + "total_holders": int(hmtoken_statistics["holders"]), + "holders": [ + { + "address": holder["address"], + "balance": int(holder["balance"]), + } + for holder in holders + ], + "daily_hmt_data": [ + { + "timestamp": datetime.datetime.fromtimestamp( + int(event_day_data["timestamp"]) + ), + "total_transaction_amount": int( + event_day_data["dailyHMTTransferAmount"] + ), + "total_transaction_count": int( + event_day_data["dailyHMTTransferCount"] + ), + } + for event_day_data in event_day_datas + ], + } diff --git a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/storage.py b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/storage.py index 7fddeb981d..f291681fd3 100644 --- a/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/storage.py +++ b/packages/sdk/python/human-protocol-sdk/human_protocol_sdk/storage.py @@ -216,7 +216,7 @@ def upload_files(self, files: List[dict], bucket: str) -> List[dict]: bucket_name=bucket, object_name=key ) except Exception as e: - if e.code == "NoSuchKey": + if hasattr(e, "code") and str(e.code) == "NoSuchKey": # file does not exist in bucket, so upload it pass else: diff --git a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_escrow.py b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_escrow.py index 5f3e00a73f..edae325049 100644 --- a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_escrow.py +++ b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_escrow.py @@ -6,8 +6,7 @@ from human_protocol_sdk.constants import NETWORKS, ChainId, Status from human_protocol_sdk.gql.escrow import ( - get_escrows_by_launcher_query, - get_filtered_escrows_query, + get_escrows_query, ) from human_protocol_sdk.escrow import ( EscrowClient, @@ -62,6 +61,17 @@ def test_init_with_invalid_chain_id(self): EscrowClient(w3) self.assertEqual(f"Invalid ChainId: {mock_chain_id}", str(cm.exception)) + def test_init_with_invalid_web3(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = None + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + with self.assertRaises(EscrowClientError) as cm: + EscrowClient(w3) + self.assertEqual(f"Invalid Web3 Instance", str(cm.exception)) + def test_escrow_config_valid_params(self): recording_oracle_address = "0x1234567890123456789012345678901234567890" reputation_oracle_address = "0x1234567890123456789012345678901234567890" @@ -1704,97 +1714,42 @@ def test_get_status_invalid_escrow(self): "Escrow address is not provided by the factory", str(cm.exception) ) - def test_get_launched_escrows(self): - requester_address = "0x1234567890123456789012345678901234567890" - mock_function = MagicMock() - with patch("human_protocol_sdk.escrow.get_data_from_subgraph") as mock_function: - mock_escrow_1 = ( - { - "id": "0x1234567890123456789012345678901234567891", - "address": "0x1234567890123456789012345678901234567891", - "amountPaid": "1000000000000000000", - "balance": "1000000000000000000", - "count": "1", - "factoryAddress": "0x1234567890123456789012345678901234567890", - "finalResultsUrl": "https://example.com", - "intermediateResultsUrl": "https://example.com", - "launcher": "0x1234567890123456789012345678901234567891", - "manifestHash": "0x1234567890123456789012345678901234567891", - "manifestUrl": "https://example.com", - "recordingOracle": "0x1234567890123456789012345678901234567891", - "recordingOracleFee": "1000000000000000000", - "reputationOracle": "0x1234567890123456789012345678901234567891", - "reputationOracleFee": "1000000000000000000", - "status": "Pending", - "token": "0x1234567890123456789012345678901234567891", - "totalFundedAmount": "1000000000000000000", - }, - ) - mock_escrow_2 = ( - { - "id": "0x1234567890123456789012345678901234567892", - "address": "0x1234567890123456789012345678901234567892", - "amountPaid": "1000000000000000000", - "balance": "1000000000000000000", - "count": "1", - "factoryAddress": "0x1234567890123456789012345678901234567890", - "finalResultsUrl": "https://example.com", - "intermediateResultsUrl": "https://example.com", - "launcher": "0x1234567890123456789012345678901234567892", - "manifestHash": "0x1234567890123456789012345678901234567892", - "manifestUrl": "https://example.com", - "recordingOracle": "0x1234567890123456789012345678901234567892", - "recordingOracleFee": "1000000000000000000", - "reputationOracle": "0x1234567890123456789012345678901234567892", - "reputationOracleFee": "1000000000000000000", - "status": "Pending", - "token": "0x1234567890123456789012345678901234567892", - "totalFundedAmount": "1000000000000000000", - }, - ) - - mock_function.return_value = { - "data": {"escrows": [mock_escrow_1, mock_escrow_2]} - } - escrows = self.escrow.get_launched_escrows(requester_address) - - mock_function.assert_called_once_with( - "subgraph_url", - query=get_escrows_by_launcher_query, - params={"launcherAddress": requester_address}, - ) - - self.assertEqual(len(escrows), 2) - self.assertEqual(escrows[0], mock_escrow_1) - self.assertEqual(escrows[1], mock_escrow_2) - def test_escrow_filter_valid_params(self): - launcher_address = "0x1234567890123456789012345678901234567891" + launcher = "0x1234567890123456789012345678901234567891" + reputation_oracle = "0x1234567890123456789012345678901234567891" + recording_oracle = "0x1234567890123456789012345678901234567891" date_from = datetime.fromtimestamp(1683811973) date_to = datetime.fromtimestamp(1683812007) escrow_filter = EscrowFilter( - launcher_address=launcher_address, + launcher=launcher, + reputation_oracle=reputation_oracle, + recording_oracle=recording_oracle, status=Status.Pending, date_from=date_from, date_to=date_to, ) - self.assertEqual(escrow_filter.launcher_address, launcher_address) + self.assertEqual(escrow_filter.launcher, launcher) + self.assertEqual(escrow_filter.reputation_oracle, reputation_oracle) + self.assertEqual(escrow_filter.recording_oracle, recording_oracle) self.assertEqual(escrow_filter.status, Status.Pending) self.assertEqual(escrow_filter.date_from, date_from) self.assertEqual(escrow_filter.date_to, date_to) - def test_escrow_filter_invalid_address(self): + def test_escrow_filter_invalid_address_launcher(self): with self.assertRaises(EscrowClientError) as cm: - EscrowFilter(launcher_address="invalid_address") + EscrowFilter(launcher="invalid_address") self.assertEqual("Invalid address: invalid_address", str(cm.exception)) - def test_escrow_filter_no_parameters(self): + def test_escrow_filter_invalid_address_reputation_oracle(self): with self.assertRaises(EscrowClientError) as cm: - EscrowFilter() - self.assertEqual( - "EscrowFilter class must have at least one parameter", str(cm.exception) - ) + EscrowFilter(reputation_oracle="invalid_address") + self.assertEqual("Invalid address: invalid_address", str(cm.exception)) + + def test_escrow_filter_invalid_address_recording_oracle(self): + with self.assertRaises(EscrowClientError) as cm: + EscrowFilter(recording_oracle="invalid_address") + self.assertEqual("Invalid address: invalid_address", str(cm.exception)) def test_escrow_filter_invalid_dates(self): with self.assertRaises(EscrowClientError) as cm: @@ -1807,9 +1762,9 @@ def test_escrow_filter_invalid_dates(self): str(cm.exception), ) - def test_get_filtered_escrows(self): + def test_get_escrows(self): filter = EscrowFilter( - launcher_address="0x1234567890123456789012345678901234567891", + launcher="0x1234567890123456789012345678901234567891", status=Status.Pending, date_from=datetime.fromtimestamp(1683811973), date_to=datetime.fromtimestamp(1683812007), @@ -1868,13 +1823,15 @@ def test_get_filtered_escrows(self): ] } } - filtered = self.escrow.get_escrows_filtered(filter) + filtered = self.escrow.get_escrows(filter) mock_function.assert_called_once_with( "subgraph_url", - query=get_filtered_escrows_query, + query=get_escrows_query(filter), params={ - "launcherAddress": "0x1234567890123456789012345678901234567891", + "launcher": "0x1234567890123456789012345678901234567891", + "reputationOracle": None, + "recordingOracle": None, "status": "Pending", "from": 1683811973, "to": 1683812007, diff --git a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_kvstore.py b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_kvstore.py index ba4965b944..049844544e 100644 --- a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_kvstore.py +++ b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_kvstore.py @@ -53,6 +53,17 @@ def test_init_with_invalid_chain_id(self): KVStoreClient(w3) self.assertEqual("Invalid ChainId: 9999", str(cm.exception)) + def test_init_with_invalid_web3(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = None + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + with self.assertRaises(KVStoreClientError) as cm: + KVStoreClient(w3) + self.assertEqual(f"Invalid Web3 Instance", str(cm.exception)) + def test_set(self): mock_function = MagicMock() self.kvstore.kvstore_contract.functions.set = mock_function diff --git a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_staking.py b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_staking.py index 35e8773780..38d55cde28 100644 --- a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_staking.py +++ b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_staking.py @@ -60,6 +60,17 @@ def test_init_with_invalid_chain_id(self): StakingClient(w3) self.assertEqual("Invalid ChainId: 9999", str(cm.exception)) + def test_init_with_invalid_web3(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = None + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + with self.assertRaises(StakingClientError) as cm: + StakingClient(w3) + self.assertEqual(f"Invalid Web3 Instance", str(cm.exception)) + def test_approve_stake(self): mock_function = MagicMock() self.staking_client.hmtoken_contract.functions.approve = mock_function diff --git a/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_statistics.py b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_statistics.py new file mode 100644 index 0000000000..c82689772c --- /dev/null +++ b/packages/sdk/python/human-protocol-sdk/test/human_protocol_sdk/test_statistics.py @@ -0,0 +1,320 @@ +import unittest +from datetime import datetime +from unittest.mock import MagicMock, PropertyMock, patch + +from human_protocol_sdk.constants import NETWORKS, ChainId, Status +from human_protocol_sdk.gql.hmtoken import get_holders_query +from human_protocol_sdk.gql.statistics import ( + get_event_day_data_query, + get_escrow_statistics_query, + get_hmtoken_statistics_query, +) +from human_protocol_sdk.statistics import ( + StatisticsClient, + StatisticsClientError, + StatisticsParam, +) +from web3 import Web3 +from web3.middleware import construct_sign_and_send_raw_middleware +from web3.providers.rpc import HTTPProvider + + +class StatisticsTestCase(unittest.TestCase): + def setUp(self): + self.mock_provider = MagicMock(spec=HTTPProvider) + self.w3 = Web3(self.mock_provider) + + self.mock_chain_id = ChainId.LOCALHOST.value + type(self.w3.eth).chain_id = PropertyMock(return_value=self.mock_chain_id) + + self.statistics = StatisticsClient(self.w3) + + def test_init_with_valid_inputs(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = ChainId.LOCALHOST.value + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + statistics = StatisticsClient(w3) + + self.assertEqual(statistics.w3, w3) + self.assertEqual(statistics.network, NETWORKS[ChainId(mock_chain_id)]) + + def test_init_with_invalid_chain_id(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = 9999 + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + with self.assertRaises(StatisticsClientError) as cm: + StatisticsClient(w3) + self.assertEqual(f"Invalid ChainId: {mock_chain_id}", str(cm.exception)) + + def test_init_with_invalid_web3(self): + mock_provider = MagicMock(spec=HTTPProvider) + w3 = Web3(mock_provider) + + mock_chain_id = None + type(w3.eth).chain_id = PropertyMock(return_value=mock_chain_id) + + with self.assertRaises(StatisticsClientError) as cm: + StatisticsClient(w3) + self.assertEqual(f"Invalid Web3 Instance", str(cm.exception)) + + def test_get_escrow_statistics(self): + param = StatisticsParam( + date_from=datetime.fromtimestamp(1683811973), + date_to=datetime.fromtimestamp(1683812007), + ) + mock_function = MagicMock() + + with patch( + "human_protocol_sdk.statistics.get_data_from_subgraph" + ) as mock_function: + mock_function.side_effect = [ + { + "data": { + "escrowStatistics": { + "totalEscrowCount": "1", + }, + } + }, + { + "data": { + "eventDayDatas": [ + { + "timestamp": 1, + "dailyEscrowCount": "1", + "dailyPendingStatusEventCount": "1", + "dailyCancelledStatusEventCount": "1", + "dailyPartialStatusEventCount": "1", + "dailyPaidStatusEventCount": "1", + "dailyCompletedStatusEventCount": "1", + }, + ], + } + }, + ] + + escrow_statistics = self.statistics.get_escrow_statistics(param) + + mock_function.assert_any_call( + "subgraph_url", + query=get_escrow_statistics_query, + ) + + mock_function.assert_any_call( + "subgraph_url", + query=get_event_day_data_query(param), + params={ + "from": 1683811973, + "to": 1683812007, + }, + ) + + self.assertEqual( + escrow_statistics, + { + "total_escrows": 1, + "daily_escrows_data": [ + { + "timestamp": datetime.fromtimestamp(1), + "escrows_total": 1, + "escrows_pending": 1, + "escrows_solved": 1, + "escrows_paid": 1, + "escrows_cancelled": 1, + }, + ], + }, + ) + + def test_get_worker_statistics(self): + param = StatisticsParam( + date_from=datetime.fromtimestamp(1683811973), + date_to=datetime.fromtimestamp(1683812007), + ) + mock_function = MagicMock() + + with patch( + "human_protocol_sdk.statistics.get_data_from_subgraph" + ) as mock_function: + mock_function.side_effect = [ + { + "data": { + "eventDayDatas": [ + { + "timestamp": 1, + "dailyBulkPayoutEventCount": "1", + "dailyWorkerCount": "2", + }, + ], + } + }, + ] + + worker_statistics = self.statistics.get_worker_statistics(param) + + mock_function.assert_any_call( + "subgraph_url", + query=get_event_day_data_query(param), + params={ + "from": 1683811973, + "to": 1683812007, + }, + ) + + self.assertEqual( + worker_statistics, + { + "daily_workers_data": [ + { + "timestamp": datetime.fromtimestamp(1), + "active_workers": 2, + "average_jobs_solved": 0.5, + }, + ], + }, + ) + + def test_get_payment_statistics(self): + param = StatisticsParam( + date_from=datetime.fromtimestamp(1683811973), + date_to=datetime.fromtimestamp(1683812007), + ) + mock_function = MagicMock() + + with patch( + "human_protocol_sdk.statistics.get_data_from_subgraph" + ) as mock_function: + mock_function.side_effect = [ + { + "data": { + "eventDayDatas": [ + { + "timestamp": 1, + "dailyPayoutCount": "4", + "dailyPayoutAmount": "100", + "dailyWorkerCount": "4", + "dailyBulkPayoutEventCount": "2", + }, + ], + } + }, + ] + + payment_statistics = self.statistics.get_payment_statistics(param) + + mock_function.assert_any_call( + "subgraph_url", + query=get_event_day_data_query(param), + params={ + "from": 1683811973, + "to": 1683812007, + }, + ) + + self.assertEqual( + payment_statistics, + { + "daily_payments_data": [ + { + "timestamp": datetime.fromtimestamp(1), + "total_amount_paid": 100, + "total_count": 4, + "average_amount_per_job": 50, + "average_amount_per_worker": 25, + }, + ], + }, + ) + + def test_get_hmt_statistics(self): + param = StatisticsParam( + date_from=datetime.fromtimestamp(1683811973), + date_to=datetime.fromtimestamp(1683812007), + ) + mock_function = MagicMock() + + with patch( + "human_protocol_sdk.statistics.get_data_from_subgraph" + ) as mock_function: + mock_function.side_effect = [ + { + "data": { + "hmtokenStatistics": { + "totalValueTransfered": "100", + "holders": "2", + }, + } + }, + { + "data": { + "holders": [ + { + "address": "0x123", + "balance": "10", + }, + ], + } + }, + { + "data": { + "eventDayDatas": [ + { + "timestamp": 1, + "dailyHMTTransferCount": "4", + "dailyHMTTransferAmount": "100", + }, + ], + } + }, + ] + + hmt_statistics = self.statistics.get_hmt_statistics(param) + + mock_function.assert_any_call( + "subgraph_url", + query=get_hmtoken_statistics_query, + ) + + mock_function.assert_any_call( + "subgraph_url", + query=get_holders_query, + ) + + mock_function.assert_any_call( + "subgraph_url", + query=get_event_day_data_query(param), + params={ + "from": 1683811973, + "to": 1683812007, + }, + ) + + self.assertEqual( + hmt_statistics, + { + "total_transfer_amount": 100, + "total_holders": 2, + "holders": [ + { + "address": "0x123", + "balance": 10, + }, + ], + "daily_hmt_data": [ + { + "timestamp": datetime.fromtimestamp(1), + "total_transaction_amount": 100, + "total_transaction_count": 4, + }, + ], + }, + ) + + +if __name__ == "__main__": + unittest.main(exit=True) diff --git a/packages/sdk/typescript/human-protocol-sdk/example/escrow.ts b/packages/sdk/typescript/human-protocol-sdk/example/escrow.ts new file mode 100644 index 0000000000..3a08c7ed90 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/example/escrow.ts @@ -0,0 +1,29 @@ +/* eslint-disable no-console */ +import { providers } from 'ethers'; +import { EscrowClient } from '../src/escrow'; +import { NETWORKS } from '../src/constants'; +import { ChainId } from '../src/enums'; + +export const getEscrows = async () => { + if (!NETWORKS[ChainId.POLYGON_MUMBAI]) { + return; + } + + const jsonRPCProvider = new providers.JsonRpcProvider(); + const escrowClient = new EscrowClient( + jsonRPCProvider, + NETWORKS[ChainId.POLYGON_MUMBAI] + ); + + const escrows = await escrowClient.getEscrows({ + status: 'Pending', + from: new Date(2023, 4, 8), + to: new Date(2023, 5, 8), + }); + + console.log('Pending escrows:', escrows); +}; + +(async () => { + getEscrows(); +})(); diff --git a/packages/sdk/typescript/human-protocol-sdk/example/statistics.ts b/packages/sdk/typescript/human-protocol-sdk/example/statistics.ts new file mode 100644 index 0000000000..0950c92f0f --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/example/statistics.ts @@ -0,0 +1,104 @@ +/* eslint-disable no-console */ +import { StatisticsClient } from '../src/statistics'; +import { NETWORKS } from '../src/constants'; +import { ChainId } from '../src/enums'; + +export const getStatistics = async () => { + if (!NETWORKS[ChainId.POLYGON_MUMBAI]) { + return; + } + + const statisticsClient = new StatisticsClient( + NETWORKS[ChainId.POLYGON_MUMBAI] + ); + + console.log( + 'Escrow statistics:', + await statisticsClient.getEscrowStatistics() + ); + + console.log( + 'Escrow statistics from 5/8 - 6/8:', + await statisticsClient.getEscrowStatistics({ + from: new Date(2023, 4, 8), + to: new Date(2023, 5, 8), + }) + ); + + console.log( + 'Worker statistics:', + await statisticsClient.getWorkerStatistics() + ); + + console.log( + 'Worker statistics from 5/8 - 6/8:', + await statisticsClient.getWorkerStatistics({ + from: new Date(2023, 4, 8), + to: new Date(2023, 5, 8), + }) + ); + + console.log( + 'Payment statistics:', + (await statisticsClient.getPaymentStatistics()).dailyPaymentsData.map( + (p) => ({ + ...p, + totalAmountPaid: p.totalAmountPaid.toString(), + averageAmountPerJob: p.averageAmountPerJob.toString(), + averageAmountPerWorker: p.averageAmountPerWorker.toString(), + }) + ) + ); + + console.log( + 'Payment statistics from 5/8 - 6/8:', + ( + await statisticsClient.getPaymentStatistics({ + from: new Date(2023, 4, 8), + to: new Date(2023, 5, 8), + }) + ).dailyPaymentsData.map((p) => ({ + ...p, + totalAmountPaid: p.totalAmountPaid.toString(), + averageAmountPerJob: p.averageAmountPerJob.toString(), + averageAmountPerWorker: p.averageAmountPerWorker.toString(), + })) + ); + + const hmtStatistics = await statisticsClient.getHMTStatistics(); + + console.log('HMT statistics:', { + ...hmtStatistics, + totalTransferAmount: hmtStatistics.totalTransferAmount.toString(), + holders: hmtStatistics.holders.map((h) => ({ + ...h, + balance: h.balance.toString(), + })), + dailyHMTData: hmtStatistics.dailyHMTData.map((d) => ({ + ...d, + totalTransactionAmount: d.totalTransactionAmount.toString(), + })), + }); + + const hmtStatisticsRange = await statisticsClient.getHMTStatistics({ + from: new Date(2023, 4, 8), + to: new Date(2023, 5, 8), + }); + + console.log('HMT statistics from 5/8 - 6/8:', { + ...hmtStatisticsRange, + totalTransferAmount: hmtStatisticsRange.totalTransferAmount.toString(), + holders: hmtStatisticsRange.holders.map((h) => ({ + ...h, + balance: h.balance.toString(), + })), + dailyHMTData: hmtStatisticsRange.dailyHMTData.map((d) => ({ + ...d, + totalTransactionAmount: d.totalTransactionAmount.toString(), + })), + }); +}; + +(async () => { + getStatistics(); +})(); diff --git a/packages/sdk/typescript/human-protocol-sdk/src/constants.ts b/packages/sdk/typescript/human-protocol-sdk/src/constants.ts index a40e07384a..3eb75f0f02 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/constants.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/constants.ts @@ -61,7 +61,7 @@ export const NETWORKS: { rewardPoolAddress: '0x4A5963Dd6792692e9147EdC7659936b96251917a', kvstoreAddress: '0x70671167176C4934204B1C7e97F5e86695857ef2', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', oldSubgraphUrl: '', oldFactoryAddress: '', }, @@ -88,7 +88,7 @@ export const NETWORKS: { rewardPoolAddress: '0x0376D26246Eb35FF4F9924cF13E6C05fd0bD7Fb4', kvstoreAddress: '0xc9Fe39c4b6e1d7A2991355Af159956982DADf842', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/goerli-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/goerli-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/goerli', oldFactoryAddress: '0xaAe6a2646C1F88763E62e0cD08aD050Ea66AC46F', @@ -102,7 +102,7 @@ export const NETWORKS: { stakingAddress: '0xdFbB79dC35a3A53741be54a2C9b587d6BafAbd1C', rewardPoolAddress: '0xf376443BCc6d4d4D63eeC086bc4A9E4a83878e0e', kvstoreAddress: '0x2B95bEcb6EBC4589f64CB000dFCF716b4aeF8aA6', - subgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsc-v1', + subgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsc-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsc', oldFactoryAddress: '0xc88bC422cAAb2ac8812de03176402dbcA09533f4', }, @@ -116,7 +116,7 @@ export const NETWORKS: { rewardPoolAddress: '0xB0A0500103eCEc431b73F6BAd923F0a2774E6e29', kvstoreAddress: '0x3aD4B091E054f192a822D1406f4535eAd38580e4', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/bsctest', oldFactoryAddress: '0xaae6a2646c1f88763e62e0cd08ad050ea66ac46f', @@ -131,7 +131,7 @@ export const NETWORKS: { rewardPoolAddress: '0xa8e32d777a3839440cc7c24D591A64B9481753B3', kvstoreAddress: '0x35Cf4beBD58F9C8D75B9eA2599479b6C173d406F', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/polygon-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/polygon-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/polygon', oldFactoryAddress: '0x45eBc3eAE6DA485097054ae10BA1A0f8e8c7f794', @@ -146,7 +146,7 @@ export const NETWORKS: { rewardPoolAddress: '0xf0145eD99AC3c4f877aDa7dA4D1E059ec9116BAE', kvstoreAddress: '0xD7F61E812e139a5a02eDae9Dfec146E1b8eA3807', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/mumbai-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mumbai-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/mumbai', oldFactoryAddress: '0x558cd800f9F0B02f3B149667bDe003284c867E94', @@ -161,7 +161,7 @@ export const NETWORKS: { rewardPoolAddress: '0x4A5963Dd6792692e9147EdC7659936b96251917a', kvstoreAddress: '0x70671167176C4934204B1C7e97F5e86695857ef2', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/moonbeam-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/moonbeam-v2', oldSubgraphUrl: 'https://api.thegraph.com/subgraphs/name/humanprotocol/moonbeam', oldFactoryAddress: '0x98108c28B7767a52BE38B4860832dd4e11A7ecad', @@ -176,7 +176,7 @@ export const NETWORKS: { rewardPoolAddress: '0xf46B45Df3d956369726d8Bd93Ba33963Ab692920', kvstoreAddress: '0xE3D74BBFa45B4bCa69FF28891fBE392f4B4d4e4d', subgraphUrl: - 'https://api.thegraph.com/subgraphs/name/humanprotocol/moonbase-alpha-v1', + 'https://api.thegraph.com/subgraphs/name/humanprotocol/moonbase-alpha-v2', oldSubgraphUrl: '', oldFactoryAddress: '', }, diff --git a/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts b/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts index c2f28bf807..bea3636a01 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/escrow.ts @@ -39,11 +39,7 @@ import { import { IEscrowConfig, IEscrowsFilter } from './interfaces'; import { EscrowStatus, NetworkData } from './types'; import { isValidUrl, throwError } from './utils'; -import { - EscrowData, - GET_ESCROWS_BY_LAUNCHER_QUERY, - GET_FILTERED_ESCROWS_QUERY, -} from './graphql'; +import { EscrowData, GET_ESCROWS_QUERY } from './graphql'; export class EscrowClient { private escrowFactoryContract: EscrowFactory; @@ -721,41 +717,27 @@ export class EscrowClient { } /** - * Returns the escrow addresses created by a job requester. + * Returns the list of escrows for given filter * - * @param {IEscrowsFilter} requesterAddress - Address of the requester. - * @returns {Promise} + * @param {IEscrowsFilter} filter - Filter parameters. + * @returns {Promise} * @throws {Error} - An error object if an error occurred. */ - async getLaunchedEscrows(launcherAddress: string): Promise { - if (!ethers.utils.isAddress(launcherAddress)) { + async getEscrows(filter: IEscrowsFilter = {}): Promise { + if (filter.launcher && !ethers.utils.isAddress(filter.launcher)) { throw ErrorInvalidAddress; } - try { - const { escrows } = await gqlFetch<{ escrows: EscrowData[] }>( - this.network.subgraphUrl, - GET_ESCROWS_BY_LAUNCHER_QUERY, - { launcherAddress } - ); - - return escrows; - } catch (e: any) { - return throwError(e); + if ( + filter.recordingOracle && + !ethers.utils.isAddress(filter.recordingOracle) + ) { + throw ErrorInvalidAddress; } - } - /** - * Returns the escrow addresses based on a specified filter. - * - * @param {IEscrowsFilter} filter - Filter parameters. - * @returns {Promise} - * @throws {Error} - An error object if an error occurred. - */ - async getEscrowsFiltered(filter: IEscrowsFilter): Promise { if ( - filter?.launcherAddress && - !ethers.utils.isAddress(filter?.launcherAddress) + filter.reputationOracle && + !ethers.utils.isAddress(filter.reputationOracle) ) { throw ErrorInvalidAddress; } @@ -763,9 +745,11 @@ export class EscrowClient { try { const { escrows } = await gqlFetch<{ escrows: EscrowData[] }>( this.network.subgraphUrl, - GET_FILTERED_ESCROWS_QUERY, + GET_ESCROWS_QUERY(filter), { ...filter, + from: filter.from ? +filter.from.getTime() / 1000 : undefined, + to: filter.to ? +filter.to.getTime() / 1000 : undefined, } ); diff --git a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/escrow.ts b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/escrow.ts index 8086f99e95..433e458e4c 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/escrow.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/escrow.ts @@ -1,4 +1,5 @@ import gql from 'graphql-tag'; +import { IEscrowsFilter } from '../../interfaces'; const ESCROW_FRAGMENT = gql` fragment EscrowFields on Escrow { @@ -20,35 +21,40 @@ const ESCROW_FRAGMENT = gql` status token totalFundedAmount + createdAt } `; -export const GET_ESCROWS_BY_LAUNCHER_QUERY = gql` - query GetEscrowByLauncher($launcherAddress: String!) { - escrows(where: { launcher: $launcherAddress }) { - ...EscrowFields +export const GET_ESCROWS_QUERY = (filter: IEscrowsFilter) => { + const { launcher, reputationOracle, recordingOracle, status, from, to } = + filter; + + const WHERE_CLAUSE = ` + where: { + ${launcher ? `launcher: $launcher` : ''} + ${reputationOracle ? `reputationOracle: $reputationOracle` : ''} + ${recordingOracle ? `recordingOracle: $recordingOracle` : ''} + ${status ? `status: $status` : ''} + ${from ? `createdAt_gte: $from` : ''} + ${to ? `createdAt_lte: $to` : ''} } - } - ${ESCROW_FRAGMENT} -`; + `; -export const GET_FILTERED_ESCROWS_QUERY = gql` - query GetFilteredEscrows( - $launcherAddress: String - $status: EscrowStatus - $from: Int - $to: Int - ) { - escrows( - where: { - launcher: $launcherAddress - status: $status - createdAt_gte: $from - createdAt_lte: $to - } + return gql` + query getEscrows( + $launcher: String + $reputationOracle: String + $recordingOracle: String + $status: String + $from: Int + $to: Int ) { - ...EscrowFields + escrows( + ${WHERE_CLAUSE} + ) { + ...EscrowFields + } } - } - ${ESCROW_FRAGMENT} -`; + ${ESCROW_FRAGMENT} + `; +}; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/hmtoken.ts b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/hmtoken.ts new file mode 100644 index 0000000000..11b5758092 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/hmtoken.ts @@ -0,0 +1,17 @@ +import gql from 'graphql-tag'; + +const HOLDER_FRAGMENT = gql` + fragment HolderFields on Holder { + address + balance + } +`; + +export const GET_HOLDERS_QUERY = gql` + query GetHolders { + holders { + ...HolderFields + } + } + ${HOLDER_FRAGMENT} +`; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/index.ts index 13d7bd1aea..3b6d92eca4 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/index.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/index.ts @@ -1,2 +1,4 @@ export * from './escrow'; +export * from './hmtoken'; export * from './reward'; +export * from './statistics'; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/statistics.ts b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/statistics.ts new file mode 100644 index 0000000000..32d9306ba5 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/graphql/queries/statistics.ts @@ -0,0 +1,88 @@ +import gql from 'graphql-tag'; +import { IStatisticsParams } from '../../interfaces'; + +const HMTOKEN_STATISTICS_FRAGMENT = gql` + fragment HMTokenStatisticsFields on HMTokenStatistics { + totalTransferEventCount + totalBulkTransferEventCount + totalApprovalEventCount + totalBulkApprovalEventCount + totalValueTransfered + holders + } +`; + +const ESCROW_STATISTICS_FRAGMENT = gql` + fragment EscrowStatisticsFields on EscrowStatistics { + fundEventCount + setupEventCount + storeResultsEventCount + bulkPayoutEventCount + pendingStatusEventCount + cancelledStatusEventCount + partialStatusEventCount + paidStatusEventCount + completedStatusEventCount + totalEventCount + totalEscrowCount + } +`; + +const EVENT_DAY_DATA_FRAGMENT = gql` + fragment EventDayDataFields on EventDayData { + timestamp + dailyFundEventCount + dailySetupEventCount + dailyStoreResultsEventCount + dailyBulkPayoutEventCount + dailyPendingStatusEventCount + dailyCancelledStatusEventCount + dailyPartialStatusEventCount + dailyPaidStatusEventCount + dailyCompletedStatusEventCount + dailyTotalEventCount + dailyEscrowCount + dailyWorkerCount + dailyPayoutCount + dailyPayoutAmount + dailyHMTTransferCount + dailyHMTTransferAmount + } +`; + +export const GET_HMTOKEN_STATISTICS_QUERY = gql` + query GetHMTokenStatistics { + hmtokenStatistics(id: "hmt-statistics-id") { + ...HMTokenStatisticsFields + } + } + ${HMTOKEN_STATISTICS_FRAGMENT} +`; + +export const GET_ESCROW_STATISTICS_QUERY = gql` + query GetEscrowStatistics { + escrowStatistics(id: "escrow-statistics-id") { + ...EscrowStatisticsFields + } + } + ${ESCROW_STATISTICS_FRAGMENT} +`; + +export const GET_EVENT_DAY_DATA_QUERY = (params: IStatisticsParams) => { + const { from, to } = params; + const WHERE_CLAUSE = ` + where: { + ${from !== undefined ? `timestamp_gte: $from` : ''} + ${to !== undefined ? `timestamp_lte: $to` : ''} + } + `; + + return gql` + query GetEscrowDayData($from: Int, $to: Int) { + eventDayDatas(${WHERE_CLAUSE}) { + ...EventDayDataFields + } + } + ${EVENT_DAY_DATA_FRAGMENT} + `; +}; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/graphql/types.ts b/packages/sdk/typescript/human-protocol-sdk/src/graphql/types.ts index b10eb18f18..abacfe9faa 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/graphql/types.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/graphql/types.ts @@ -1,3 +1,5 @@ +import { BigNumber } from 'ethers'; + export type EscrowData = { id: string; address: string; @@ -17,6 +19,50 @@ export type EscrowData = { status: string; token: string; totalFundedAmount: string; + createdAt: string; +}; + +export type HMTStatisticsData = { + totalTransferEventCount: string; + totalBulkTransferEventCount: string; + totalApprovalEventCount: string; + totalBulkApprovalEventCount: string; + totalValueTransfered: string; + holders: string; +}; + +export type EscrowStatisticsData = { + fundEventCount: string; + setupEventCount: string; + storeResultsEventCount: string; + bulkPayoutEventCount: string; + pendingStatusEventCount: string; + cancelledStatusEventCount: string; + partialStatusEventCount: string; + paidStatusEventCount: string; + completedStatusEventCount: string; + totalEventCount: string; + totalEscrowCount: string; +}; + +export type EventDayData = { + timestamp: string; + dailyFundEventCount: string; + dailySetupEventCount: string; + dailyStoreResultsEventCount: string; + dailyBulkPayoutEventCount: string; + dailyPendingStatusEventCount: string; + dailyCancelledStatusEventCount: string; + dailyPartialStatusEventCount: string; + dailyPaidStatusEventCount: string; + dailyCompletedStatusEventCount: string; + dailyTotalEventCount: string; + dailyEscrowCount: string; + dailyWorkerCount: string; + dailyPayoutCount: string; + dailyPayoutAmount: string; + dailyHMTTransferCount: string; + dailyHMTTransferAmount: string; }; export type RewardAddedEventData = { @@ -25,3 +71,62 @@ export type RewardAddedEventData = { slasher: string; amount: string; }; + +export type DailyEscrowData = { + timestamp: Date; + escrowsTotal: number; + escrowsPending: number; + escrowsSolved: number; + escrowsPaid: number; + escrowsCancelled: number; +}; + +export type EscrowStatistics = { + totalEscrows: number; + dailyEscrowsData: DailyEscrowData[]; +}; + +export type DailyWorkerData = { + timestamp: Date; + activeWorkers: number; + averageJobsSolved: number; +}; + +export type WorkerStatistics = { + dailyWorkersData: DailyWorkerData[]; +}; + +export type DailyPaymentData = { + timestamp: Date; + totalAmountPaid: BigNumber; + totalCount: number; + averageAmountPerJob: BigNumber; + averageAmountPerWorker: BigNumber; +}; + +export type PaymentStatistics = { + dailyPaymentsData: DailyPaymentData[]; +}; + +export type HMTHolderData = { + address: string; + balance: string; +}; + +export type HMTHolder = { + address: string; + balance: BigNumber; +}; + +export type DailyHMTData = { + timestamp: Date; + totalTransactionAmount: BigNumber; + totalTransactionCount: number; +}; + +export type HMTStatistics = { + totalTransferAmount: BigNumber; + totalHolders: number; + holders: HMTHolder[]; + dailyHMTData: DailyHMTData[]; +}; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/index.ts b/packages/sdk/typescript/human-protocol-sdk/src/index.ts index d0844e900c..177ecb96bf 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/index.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/index.ts @@ -2,10 +2,17 @@ import { StakingClient } from './staking'; import { StorageClient } from './storage'; import { KVStoreClient } from './kvstore'; import { EscrowClient } from './escrow'; +import { StatisticsClient } from './statistics'; export * from './constants'; export * from './types'; export * from './enums'; export * from './interfaces'; -export { StakingClient, StorageClient, KVStoreClient, EscrowClient }; +export { + StakingClient, + StorageClient, + KVStoreClient, + EscrowClient, + StatisticsClient, +}; diff --git a/packages/sdk/typescript/human-protocol-sdk/src/interfaces.ts b/packages/sdk/typescript/human-protocol-sdk/src/interfaces.ts index 3b3ef8c64a..5069a8913a 100644 --- a/packages/sdk/typescript/human-protocol-sdk/src/interfaces.ts +++ b/packages/sdk/typescript/human-protocol-sdk/src/interfaces.ts @@ -1,5 +1,4 @@ import { BigNumber } from 'ethers'; -import { EscrowStatus } from './types'; export interface IAllocation { escrowAddress: string; @@ -23,9 +22,18 @@ export interface IStaker { tokensAvailable: BigNumber; } +type EscrowStatus = + | 'Launched' + | 'Pending' + | 'Partial' + | 'Paid' + | 'Complete' + | 'Cancelled'; + export interface IEscrowsFilter { - launcherAddress?: string; - role?: number; + launcher?: string; + reputationOracle?: string; + recordingOracle?: string; status?: EscrowStatus; from?: Date; to?: Date; @@ -46,3 +54,8 @@ export interface IKeyPair { passphrase: string; revocationCertificate?: string; } + +export interface IStatisticsParams { + from?: Date; + to?: Date; +} diff --git a/packages/sdk/typescript/human-protocol-sdk/src/statistics.ts b/packages/sdk/typescript/human-protocol-sdk/src/statistics.ts new file mode 100644 index 0000000000..1ff5cf43f7 --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/src/statistics.ts @@ -0,0 +1,196 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import gqlFetch from 'graphql-request'; + +import { + GET_ESCROW_STATISTICS_QUERY, + GET_EVENT_DAY_DATA_QUERY, + GET_HOLDERS_QUERY, + GET_HMTOKEN_STATISTICS_QUERY, + EscrowStatistics, + EscrowStatisticsData, + EventDayData, + HMTStatistics, + HMTStatisticsData, + PaymentStatistics, + WorkerStatistics, + HMTHolderData, +} from './graphql'; +import { IStatisticsParams } from './interfaces'; +import { NetworkData } from './types'; +import { throwError } from './utils'; +import { BigNumber } from 'ethers'; + +export class StatisticsClient { + public network: NetworkData; + + /** + * **StatisticsClient constructor** + * + * @param {NetworkData} network - The network information required to connect to the Statistics contract + */ + constructor(network: NetworkData) { + this.network = network; + } + + /** + * Returns the escrow statistics data for the given date range + * + * @param {IStatisticsParams} params - Filter parameters. + * @returns {Promise} + * @throws {Error} - An error object if an error occurred. + */ + async getEscrowStatistics( + params: IStatisticsParams = {} + ): Promise { + try { + const { escrowStatistics } = await gqlFetch<{ + escrowStatistics: EscrowStatisticsData; + }>(this.network.subgraphUrl, GET_ESCROW_STATISTICS_QUERY); + + const { eventDayDatas } = await gqlFetch<{ + eventDayDatas: EventDayData[]; + }>(this.network.subgraphUrl, GET_EVENT_DAY_DATA_QUERY(params), { + from: params.from ? params.from.getTime() / 1000 : undefined, + to: params.to ? params.to.getTime() / 1000 : undefined, + }); + + return { + totalEscrows: +escrowStatistics.totalEscrowCount, + dailyEscrowsData: eventDayDatas.map((eventDayData) => ({ + timestamp: new Date(+eventDayData.timestamp * 1000), + escrowsTotal: +eventDayData.dailyEscrowCount, + escrowsPending: +eventDayData.dailyPendingStatusEventCount, + escrowsSolved: +eventDayData.dailyCompletedStatusEventCount, + escrowsPaid: +eventDayData.dailyPaidStatusEventCount, + escrowsCancelled: +eventDayData.dailyCancelledStatusEventCount, + })), + }; + } catch (e: any) { + return throwError(e); + } + } + + /** + * Returns the worker statistics data for the given date range + * + * @param {IStatisticsParams} params - Filter parameters. + * @returns {Promise} + * @throws {Error} - An error object if an error occurred. + */ + async getWorkerStatistics( + params: IStatisticsParams = {} + ): Promise { + try { + const { eventDayDatas } = await gqlFetch<{ + eventDayDatas: EventDayData[]; + }>(this.network.subgraphUrl, GET_EVENT_DAY_DATA_QUERY(params), { + from: params.from ? params.from.getTime() / 1000 : undefined, + to: params.to ? params.to.getTime() / 1000 : undefined, + }); + + return { + dailyWorkersData: eventDayDatas.map((eventDayData) => ({ + timestamp: new Date(+eventDayData.timestamp * 1000), + activeWorkers: +eventDayData.dailyWorkerCount, + averageJobsSolved: + eventDayData.dailyWorkerCount === '0' + ? 0 + : +eventDayData.dailyBulkPayoutEventCount / + +eventDayData.dailyWorkerCount, + })), + }; + } catch (e: any) { + return throwError(e); + } + } + + /** + * Returns the payment statistics data for the given date range + * + * @param {IStatisticsParams} params - Filter parameters. + * @returns {Promise} + * @throws {Error} - An error object if an error occurred. + */ + async getPaymentStatistics( + params: IStatisticsParams = {} + ): Promise { + try { + const { eventDayDatas } = await gqlFetch<{ + eventDayDatas: EventDayData[]; + }>(this.network.subgraphUrl, GET_EVENT_DAY_DATA_QUERY(params), { + from: params.from ? params.from.getTime() / 1000 : undefined, + to: params.to ? params.to.getTime() / 1000 : undefined, + }); + + return { + dailyPaymentsData: eventDayDatas.map((eventDayData) => ({ + timestamp: new Date(+eventDayData.timestamp * 1000), + totalAmountPaid: BigNumber.from(eventDayData.dailyPayoutAmount), + totalCount: +eventDayData.dailyPayoutCount, + averageAmountPerJob: + eventDayData.dailyBulkPayoutEventCount === '0' + ? BigNumber.from(0) + : BigNumber.from(eventDayData.dailyPayoutAmount).div( + eventDayData.dailyBulkPayoutEventCount + ), + averageAmountPerWorker: + eventDayData.dailyWorkerCount === '0' + ? BigNumber.from(0) + : BigNumber.from(eventDayData.dailyPayoutAmount).div( + eventDayData.dailyWorkerCount + ), + })), + }; + } catch (e: any) { + return throwError(e); + } + } + + /** + * Returns the HMToken statistics data for the given date range + * + * @param {IStatisticsParams} params - Filter parameters. + * @returns {Promise} + * @throws {Error} - An error object if an error occurred. + */ + async getHMTStatistics( + params: IStatisticsParams = {} + ): Promise { + try { + const { hmtokenStatistics } = await gqlFetch<{ + hmtokenStatistics: HMTStatisticsData; + }>(this.network.subgraphUrl, GET_HMTOKEN_STATISTICS_QUERY); + + const { holders } = await gqlFetch<{ + holders: HMTHolderData[]; + }>(this.network.subgraphUrl, GET_HOLDERS_QUERY); + + const { eventDayDatas } = await gqlFetch<{ + eventDayDatas: EventDayData[]; + }>(this.network.subgraphUrl, GET_EVENT_DAY_DATA_QUERY(params), { + from: params.from ? params.from.getTime() / 1000 : undefined, + to: params.to ? params.to.getTime() / 1000 : undefined, + }); + + return { + totalTransferAmount: BigNumber.from( + hmtokenStatistics.totalValueTransfered + ), + totalHolders: +hmtokenStatistics.holders, + holders: holders.map((holder) => ({ + address: holder.address, + balance: BigNumber.from(holder.balance), + })), + dailyHMTData: eventDayDatas.map((eventDayData) => ({ + timestamp: new Date(+eventDayData.timestamp * 1000), + totalTransactionAmount: BigNumber.from( + eventDayData.dailyHMTTransferAmount + ), + totalTransactionCount: +eventDayData.dailyHMTTransferCount, + })), + }; + } catch (e: any) { + return throwError(e); + } + } +} diff --git a/packages/sdk/typescript/human-protocol-sdk/test/escrow.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/escrow.test.ts index 08988405a5..2530e459ad 100644 --- a/packages/sdk/typescript/human-protocol-sdk/test/escrow.test.ts +++ b/packages/sdk/typescript/human-protocol-sdk/test/escrow.test.ts @@ -5,6 +5,7 @@ import { HMToken__factory, } from '@human-protocol/core/typechain-types'; import { BigNumber, ethers } from 'ethers'; +import * as gqlFetch from 'graphql-request'; import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; import { DEFAULT_TX_ID } from '../src/constants'; import { ChainId } from '../src/enums'; @@ -38,7 +39,13 @@ import { FAKE_URL, VALID_URL, } from './utils/constants'; +import { GET_ESCROWS_QUERY } from '../src/graphql'; +vi.mock('graphql-request', () => { + return { + default: vi.fn(), + }; +}); vi.mock('../src/init'); describe('EscrowClient', () => { @@ -79,8 +86,7 @@ describe('EscrowClient', () => { finalResultsUrl: vi.fn(), token: vi.fn(), status: vi.fn(), - getLaunchedEscrows: vi.fn(), - getEscrowsFiltered: vi.fn(), + getEscrows: vi.fn(), address: ethers.constants.AddressZero, canceler: vi.fn(), recordingOracle: vi.fn(), @@ -1385,57 +1391,99 @@ describe('EscrowClient', () => { }); }); - describe('getLaunchedEscrows', () => { - test('should throw an error if requesterAddress is an invalid address', async () => { - const requesterAddress = FAKE_ADDRESS; - - await expect( - escrowClient.getLaunchedEscrows(requesterAddress) - ).rejects.toThrow(ErrorInvalidAddress); - }); + describe('getEscrows', () => { + test('should throw an error if launcher is an invalid address', async () => { + const launcher = FAKE_ADDRESS; - test('should successfully getLaunchedEscrows', async () => { - const requesterAddress = FAKE_ADDRESS; - const mockLaunchedEscrowsResult = { id: ethers.constants.AddressZero }; - - vi.spyOn(escrowClient, 'getLaunchedEscrows').mockImplementation(() => - Promise.resolve([mockLaunchedEscrowsResult, mockLaunchedEscrowsResult]) + await expect(escrowClient.getEscrows({ launcher })).rejects.toThrow( + ErrorInvalidAddress ); + }); - const results = await escrowClient.getLaunchedEscrows(requesterAddress); + test('should throw an error if recordingOracle is an invalid address', async () => { + const recordingOracle = FAKE_ADDRESS; - expect(results).toEqual([ - mockLaunchedEscrowsResult, - mockLaunchedEscrowsResult, - ]); + await expect( + escrowClient.getEscrows({ recordingOracle }) + ).rejects.toThrow(ErrorInvalidAddress); }); - }); - describe('getEscrowsFiltered', () => { - test('should throw an error if requesterAddress is an invalid address', async () => { - const requesterAddress = FAKE_ADDRESS; + test('should throw an error if reputationOracle is an invalid address', async () => { + const reputationOracle = FAKE_ADDRESS; await expect( - escrowClient.getEscrowsFiltered({ launcherAddress: requesterAddress }) + escrowClient.getEscrows({ reputationOracle }) ).rejects.toThrow(ErrorInvalidAddress); }); - test('should successfully getEscrowsFiltered', async () => { - const requesterAddress = FAKE_ADDRESS; - const mockLaunchedEscrowsResult = { id: ethers.constants.AddressZero }; - - vi.spyOn(escrowClient, 'getEscrowsFiltered').mockImplementation(() => - Promise.resolve([mockLaunchedEscrowsResult, mockLaunchedEscrowsResult]) - ); + test('should successfully getEscrows', async () => { + const escrows = [ + { + id: '1', + address: '0x0', + amountPaid: '3', + balance: '0', + count: '1', + factoryAddress: '0x0', + launcher: '0x0', + status: 'Completed', + token: '0x0', + totalFundedAmount: '3', + }, + { + id: '2', + address: '0x0', + amountPaid: '0', + balance: '3', + count: '2', + factoryAddress: '0x0', + launcher: '0x0', + status: 'Pending', + token: '0x0', + totalFundedAmount: '3', + }, + ]; + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockResolvedValue({ escrows }); + + const result = await escrowClient.getEscrows(); + + expect(result).toEqual(escrows); + expect(gqlFetchSpy).toHaveBeenCalledWith('', GET_ESCROWS_QUERY({}), {}); + }); + + test('should successfully getEscrows for the filter', async () => { + const escrows = [ + { + id: '1', + address: '0x0', + amountPaid: '3', + balance: '0', + count: '1', + factoryAddress: '0x0', + launcher: '0x0', + status: 'Completed', + token: '0x0', + totalFundedAmount: '3', + }, + ]; + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockResolvedValue({ escrows }); - const results = await escrowClient.getEscrowsFiltered({ - launcherAddress: requesterAddress, + const result = await escrowClient.getEscrows({ + launcher: ethers.constants.AddressZero, }); - expect(results).toEqual([ - mockLaunchedEscrowsResult, - mockLaunchedEscrowsResult, - ]); + expect(result).toEqual(escrows); + expect(gqlFetchSpy).toHaveBeenCalledWith( + '', + GET_ESCROWS_QUERY({ launcher: ethers.constants.AddressZero }), + { + launcher: ethers.constants.AddressZero, + } + ); }); }); diff --git a/packages/sdk/typescript/human-protocol-sdk/test/statistics.test.ts b/packages/sdk/typescript/human-protocol-sdk/test/statistics.test.ts new file mode 100644 index 0000000000..d3db3f16fd --- /dev/null +++ b/packages/sdk/typescript/human-protocol-sdk/test/statistics.test.ts @@ -0,0 +1,302 @@ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import * as gqlFetch from 'graphql-request'; +import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest'; +import { NETWORKS } from '../src/constants'; +import { ChainId } from '../src/enums'; +import { StatisticsClient } from '../src/statistics'; +import { + GET_ESCROW_STATISTICS_QUERY, + GET_EVENT_DAY_DATA_QUERY, +} from '../src/graphql/queries'; +import { BigNumber } from 'ethers'; + +vi.mock('graphql-request', () => { + return { + default: vi.fn(), + }; +}); + +describe('EscrowClient', () => { + let statisticsClient: any; + + beforeEach(async () => { + if (NETWORKS[ChainId.MAINNET]) { + statisticsClient = new StatisticsClient(NETWORKS[ChainId.MAINNET]); + } + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + describe('getEscrowStatistics', () => { + test('should successfully get escrow statistics', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockResolvedValueOnce({ + escrowStatistics: { + totalEscrowCount: '1', + }, + }) + .mockResolvedValueOnce({ + eventDayDatas: [ + { + timestamp: 1, + dailyEscrowCount: '1', + dailyPendingStatusEventCount: '1', + dailyCancelledStatusEventCount: '1', + dailyPartialStatusEventCount: '1', + dailyPaidStatusEventCount: '1', + dailyCompletedStatusEventCount: '1', + }, + ], + }); + + const from = new Date(); + const to = new Date(from.setDate(from.getDate() + 1)); + + const result = await statisticsClient.getEscrowStatistics({ + from, + to, + }); + + expect(gqlFetchSpy).toHaveBeenCalledWith( + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', + GET_ESCROW_STATISTICS_QUERY + ); + expect(gqlFetchSpy).toHaveBeenCalledWith( + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', + GET_EVENT_DAY_DATA_QUERY({ from, to }), + { + from: from.getTime() / 1000, + to: to.getTime() / 1000, + } + ); + + expect(result).toEqual({ + totalEscrows: 1, + dailyEscrowsData: [ + { + timestamp: new Date(1000), + escrowsTotal: 1, + escrowsPending: 1, + escrowsSolved: 1, + escrowsPaid: 1, + escrowsCancelled: 1, + }, + ], + }); + }); + + test('should throw error in case gql fetch fails from subgraph', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockRejectedValueOnce(new Error('Error')); + + await expect( + statisticsClient.getEscrowStatistics({ + from: new Date(), + to: new Date(), + }) + ).rejects.toThrow('Error'); + + expect(gqlFetchSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getWorkerStatistics', () => { + test('should successfully get worker statistics', async () => { + const gqlFetchSpy = vi.spyOn(gqlFetch, 'default').mockResolvedValueOnce({ + eventDayDatas: [ + { + timestamp: 1, + dailyBulkPayoutEventCount: '1', + dailyWorkerCount: '2', + }, + ], + }); + + const from = new Date(); + const to = new Date(from.setDate(from.getDate() + 1)); + + const result = await statisticsClient.getWorkerStatistics({ + from, + to, + }); + + expect(gqlFetchSpy).toHaveBeenCalledWith( + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', + GET_EVENT_DAY_DATA_QUERY({ from, to }), + { + from: from.getTime() / 1000, + to: to.getTime() / 1000, + } + ); + + expect(result).toEqual({ + dailyWorkersData: [ + { + timestamp: new Date(1000), + activeWorkers: 2, + averageJobsSolved: 0.5, + }, + ], + }); + }); + + test('should throw error in case gql fetch fails from subgraph', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockRejectedValueOnce(new Error('Error')); + + await expect( + statisticsClient.getWorkerStatistics({ + from: new Date(), + to: new Date(), + }) + ).rejects.toThrow('Error'); + + expect(gqlFetchSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getPaymentStatistics', () => { + test('should successfully get payment statistics', async () => { + const gqlFetchSpy = vi.spyOn(gqlFetch, 'default').mockResolvedValueOnce({ + eventDayDatas: [ + { + timestamp: 1, + dailyPayoutCount: '4', + dailyPayoutAmount: '100', + dailyWorkerCount: '4', + dailyBulkPayoutEventCount: '2', + }, + ], + }); + + const from = new Date(); + const to = new Date(from.setDate(from.getDate() + 1)); + + const result = await statisticsClient.getPaymentStatistics({ + from, + to, + }); + + expect(gqlFetchSpy).toHaveBeenCalledWith( + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', + GET_EVENT_DAY_DATA_QUERY({ from, to }), + { + from: from.getTime() / 1000, + to: to.getTime() / 1000, + } + ); + + expect(result).toEqual({ + dailyPaymentsData: [ + { + timestamp: new Date(1000), + totalAmountPaid: BigNumber.from(100), + totalCount: 4, + averageAmountPerJob: BigNumber.from(50), + averageAmountPerWorker: BigNumber.from(25), + }, + ], + }); + }); + + test('should throw error in case gql fetch fails from subgraph', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockRejectedValueOnce(new Error('Error')); + + await expect( + statisticsClient.getPaymentStatistics({ + from: new Date(), + to: new Date(), + }) + ).rejects.toThrow('Error'); + + expect(gqlFetchSpy).toHaveBeenCalledTimes(1); + }); + }); + + describe('getHMTStatistics', () => { + test('should successfully get HMT statistics', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockResolvedValueOnce({ + hmtokenStatistics: { + totalValueTransfered: '100', + holders: '2', + }, + }) + .mockResolvedValueOnce({ + holders: [ + { + address: '0x123', + balance: '10', + }, + ], + }) + .mockResolvedValueOnce({ + eventDayDatas: [ + { + timestamp: 1, + dailyHMTTransferCount: '4', + dailyHMTTransferAmount: '100', + }, + ], + }); + + const from = new Date(); + const to = new Date(from.setDate(from.getDate() + 1)); + + const result = await statisticsClient.getHMTStatistics({ + from, + to, + }); + + expect(gqlFetchSpy).toHaveBeenCalledWith( + 'https://api.thegraph.com/subgraphs/name/humanprotocol/mainnet-v2', + GET_EVENT_DAY_DATA_QUERY({ from, to }), + { + from: from.getTime() / 1000, + to: to.getTime() / 1000, + } + ); + + expect(result).toEqual({ + totalTransferAmount: BigNumber.from(100), + totalHolders: 2, + holders: [ + { + address: '0x123', + balance: BigNumber.from(10), + }, + ], + dailyHMTData: [ + { + timestamp: new Date(1000), + totalTransactionAmount: BigNumber.from(100), + totalTransactionCount: 4, + }, + ], + }); + }); + + test('should throw error in case gql fetch fails from subgraph', async () => { + const gqlFetchSpy = vi + .spyOn(gqlFetch, 'default') + .mockRejectedValueOnce(new Error('Error')); + + await expect( + statisticsClient.getHMTStatistics({ + from: new Date(), + to: new Date(), + }) + ).rejects.toThrow('Error'); + + expect(gqlFetchSpy).toHaveBeenCalledTimes(1); + }); + }); +});