From c0f6aee9ccdfde88e9dea02506f952af3d95d7a3 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 25 Jan 2024 17:46:12 +0900 Subject: [PATCH 01/15] Add HEADLESS_GQL_JWT_SECRET env. variable --- .github/workflows/deploy.yml | 4 ++++ .github/workflows/main.yml | 3 +++ .github/workflows/synth.yml | 3 +++ common/__init__.py | 1 + iap/iap_cdk_stack.py | 1 + worker/worker_cdk_stack.py | 1 + 6 files changed, 13 insertions(+) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 651d4a6a..3b0a5baf 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -51,6 +51,8 @@ on: required: true REFUND_SHEET_ID: required: true + HEADLESS_GQL_JWT_SECRET: + required: true jobs: deployment: @@ -152,6 +154,7 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} run: | source $VENV yarn cdk synth @@ -187,6 +190,7 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} run: | source $VENV yarn cdk deploy --all --require-approval never -O output.txt diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 6bd6d7a9..e672b77e 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -56,6 +56,7 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} deploy_without_approval: # This is for development / internal deployment @@ -83,6 +84,7 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} approval: runs-on: ubuntu-latest @@ -122,3 +124,4 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} diff --git a/.github/workflows/synth.yml b/.github/workflows/synth.yml index 0d5a1358..4fd2fc20 100644 --- a/.github/workflows/synth.yml +++ b/.github/workflows/synth.yml @@ -46,6 +46,8 @@ on: required: true REFUND_SHEET_ID: required: true + HEADLESS_GQL_JWT_SECRET: + required: true jobs: synth: @@ -139,6 +141,7 @@ jobs: VOUCHER_JWT_SECRET: ${{ secrets.VOUCHER_JWT_SECRET }} BRIDGE_DATA: ${{ secrets.BRIDGE_DATA }} REFUND_SHEET_ID : ${{ secrets.REFUND_SHEET_ID }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} run: | source $VENV yarn cdk synth diff --git a/common/__init__.py b/common/__init__.py index 9a45b7a5..5337d46d 100644 --- a/common/__init__.py +++ b/common/__init__.py @@ -38,6 +38,7 @@ class Config: cdn_host: str odin_gql_url: str heimdall_gql_url: str + headless_gql_jwt_secret: str # Multiplanetary planet_url: str diff --git a/iap/iap_cdk_stack.py b/iap/iap_cdk_stack.py index 6acb2b2c..03fd189f 100644 --- a/iap/iap_cdk_stack.py +++ b/iap/iap_cdk_stack.py @@ -103,6 +103,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "CDN_HOST": config.cdn_host, "PLANET_URL": config.planet_url, "BRIDGE_DATA": config.bridge_data, + "HEADLESS_GQL_JWT_SECRET": config.headless_gql_jwt_secret, } # Lambda Function diff --git a/worker/worker_cdk_stack.py b/worker/worker_cdk_stack.py index 665858aa..154133dc 100644 --- a/worker/worker_cdk_stack.py +++ b/worker/worker_cdk_stack.py @@ -91,6 +91,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: "HEADLESS": config.headless, "PLANET_URL": config.planet_url, "BRIDGE_DATA": config.bridge_data, + "HEADLESS_GQL_JWT_SECRET": config.headless_gql_jwt_secret, } # Cloudwatch Events From d3b2cc1f9dfd2f624b8c8ce6b934c14eaec7af63 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 25 Jan 2024 18:17:50 +0900 Subject: [PATCH 02/15] [WIP] Add JWT authorization header to GQL request --- common/_graphql.py | 13 ++++++++++++- iap/api/purchase.py | 4 +++- worker/worker/status_monitor.py | 3 ++- 3 files changed, 17 insertions(+), 3 deletions(-) diff --git a/common/_graphql.py b/common/_graphql.py index af6ddb78..0b65d8ea 100644 --- a/common/_graphql.py +++ b/common/_graphql.py @@ -3,6 +3,7 @@ import os from typing import Union, Dict, Any, Tuple, Optional +import jwt from gql import Client from gql.dsl import DSLSchema, dsl_gql, DSLQuery, DSLMutation from gql.transport.requests import RequestsHTTPTransport @@ -16,12 +17,22 @@ def __init__(self, url: str = f"{os.environ.get('HEADLESS')}/graphql"): self._url = url self.client = None self.ds = None - transport = RequestsHTTPTransport(url=self._url, verify=True, retries=2) + transport = RequestsHTTPTransport(url=self._url, verify=True, retries=2, headers=self.__create_header()) self.client = Client(transport=transport, fetch_schema_from_transport=True) with self.client as _: assert self.client.schema is not None self.ds = DSLSchema(self.client.schema) + @staticmethod + def create_token() -> str: + return jwt.encode({ + "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10), + "iss": "NineChronicles.IAP", + }, os.environ.get("HEADLESS_GQL_JWT_SECRET")) + + def __create_header(self): + return {"Authorization": f"Bearer {self.create_token()}"} + def execute(self, query: DocumentNode) -> Union[Dict[str, Any], ExecutionResult]: with self.client as sess: return sess.execute(query) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index f60130da..d465a42d 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -13,6 +13,7 @@ from sqlalchemy.orm import joinedload from starlette.responses import JSONResponse +from common._graphql import GQL from common.enums import ReceiptStatus, Store from common.models.product import Product from common.models.receipt import Receipt @@ -390,7 +391,8 @@ def free_product(receipt_data: FreeReceiptSchema, sess=Depends(session)): query = f"""{{ stateQuery {{ avatar (avatarAddress: "{receipt_data.avatarAddress}") {{ level}} }} }}""" try: - resp = requests.post(gql_url, json={"query": query}, timeout=1) + resp = requests.post(gql_url, json={"query": query}, timeout=1, + header={"Authorization": f"Bearer {GQL.create_token()}"}) avatar_level = resp.json()["data"]["stateQuery"]["avatar"]["level"] except: # Whether request is failed or no fitted data found diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py index f1b419d6..2d2c42de 100644 --- a/worker/worker/status_monitor.py +++ b/worker/worker/status_monitor.py @@ -7,6 +7,7 @@ from sqlalchemy.orm import sessionmaker, scoped_session from common import logger +from common._graphql import GQL from common.enums import ReceiptStatus, TxStatus from common.models.receipt import Receipt from common.utils.aws import fetch_secrets @@ -143,7 +144,7 @@ def check_garage(): } }""" - resp = requests.post(GQL_URL, json={"query": query}) + resp = requests.post(GQL_URL, json={"query": query}, headers={"Authorization": f"Bearer {GQL.create_token()}"}) data = resp.json()["data"]["stateQuery"]["garages"] fav_data = data["garageBalances"] item_data = data["fungibleItemGarages"] From ebe3ee799252bcfe235118668f137c6af7ff5d77 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 09:49:19 +0900 Subject: [PATCH 03/15] Set HEADLESS_GQL_JWT_SECRET to parameter store --- common/shared_stack.py | 1 + 1 file changed, 1 insertion(+) diff --git a/common/shared_stack.py b/common/shared_stack.py index e13db572..d0571b4e 100644 --- a/common/shared_stack.py +++ b/common/shared_stack.py @@ -98,6 +98,7 @@ def __init__(self, scope: Construct, construct_id: str, **kwargs) -> None: ("APPLE_CREDENTIAL", True), ("SEASON_PASS_JWT_SECRET", True), ("VOUCHER_JWT_SECRET", True), + ("HEADLESS_GQL_JWT_SECRET", True) ) ssm = boto3.client("ssm", region_name=config.region_name, aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID"), From bba77d673d615fb2e05ec54e6dce35f0c1927235 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 09:49:54 +0900 Subject: [PATCH 04/15] Get HEADLESS_GQL_JWT_SECRET from parameter store --- iap/settings/__init__.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/iap/settings/__init__.py b/iap/settings/__init__.py index 881fe4b7..a10ad3c0 100644 --- a/iap/settings/__init__.py +++ b/iap/settings/__init__.py @@ -10,6 +10,7 @@ google_credential = None apple_credential = None season_pass_jwt_secret = None +headless_gql_jwt_secret = None if not stage: logging.error("Config file not found") @@ -51,6 +52,15 @@ except Exception as e: season_pass_jwt_secret = os.environ.get("SEASON_PASS_JWT_SECRET", "") + try: + headless_gql_jwt_secret = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{stage}_9c_IAP_HEADLESS_GQL_JWT_SECRET", + True + )["Value"] + except Exception as e: + headless_gql_jwt_secret = os.environ.get("HEADLESS_GQL_JWT_SECRET", "") + # Prepare settings DEBUG = config("DEBUG", cast=bool, default=False) LOGGING_LEVEL = logging.getLevelName(config("LOGGING_LEVEL", default="INFO")) @@ -71,3 +81,4 @@ REGION_NAME = config("REGION_NAME") SEASON_PASS_JWT_SECRET = season_pass_jwt_secret or config("SEASON_PASS_JWT_SECRET") +HEADLESS_JWT_GQL_SECRET = headless_gql_jwt_secret or config("HEADLESS_GQL_JWT_SECRET") From 226c8f1c6296bc08aa5c9d5194e041052758e4bd Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 09:50:28 +0900 Subject: [PATCH 05/15] Use JWT token --- common/_graphql.py | 12 +++++++----- worker/worker/handler.py | 11 +++++++---- worker/worker/tracker.py | 9 +++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/common/_graphql.py b/common/_graphql.py index 0b65d8ea..0fcfc825 100644 --- a/common/_graphql.py +++ b/common/_graphql.py @@ -13,8 +13,9 @@ class GQL: - def __init__(self, url: str = f"{os.environ.get('HEADLESS')}/graphql"): + def __init__(self, url: str = f"{os.environ.get('HEADLESS')}/graphql", jwt_secret: str = None): self._url = url + self.__jwt_secret = jwt_secret self.client = None self.ds = None transport = RequestsHTTPTransport(url=self._url, verify=True, retries=2, headers=self.__create_header()) @@ -23,12 +24,13 @@ def __init__(self, url: str = f"{os.environ.get('HEADLESS')}/graphql"): assert self.client.schema is not None self.ds = DSLSchema(self.client.schema) - @staticmethod - def create_token() -> str: + def create_token(self) -> str: + iat = datetime.datetime.utcnow() return jwt.encode({ - "exp": datetime.datetime.utcnow() + datetime.timedelta(minutes=10), + "iat": iat, + "exp": iat + datetime.timedelta(minutes=1), "iss": "NineChronicles.IAP", - }, os.environ.get("HEADLESS_GQL_JWT_SECRET")) + }, self.__jwt_secret) def __create_header(self): return {"Authorization": f"Bearer {self.create_token()}"} diff --git a/worker/worker/handler.py b/worker/worker/handler.py index e5c753c0..2062e443 100644 --- a/worker/worker/handler.py +++ b/worker/worker/handler.py @@ -3,12 +3,10 @@ import json import logging import os -import traceback import uuid from dataclasses import dataclass from typing import List, Optional, Tuple, Union -import requests from sqlalchemy import create_engine, select from sqlalchemy.orm import Session, joinedload, scoped_session, sessionmaker @@ -19,7 +17,7 @@ from common.models.product import Product from common.models.receipt import Receipt from common.utils.actions import create_unload_my_garages_action_plain_value -from common.utils.aws import fetch_secrets, fetch_kms_key_id +from common.utils.aws import fetch_secrets, fetch_kms_key_id, fetch_parameter from common.utils.receipt import PlanetID from common.utils.transaction import create_unsigned_tx, append_signature_to_unsigned_tx @@ -28,6 +26,11 @@ DB_URI = DB_URI.replace("[DB_PASSWORD]", db_password) CURRENT_PLANET = PlanetID.ODIN if os.environ.get("STAGE") == "mainnet" else PlanetID.ODIN_INTERNAL GQL_URL = f"{os.environ.get('HEADLESS')}/graphql" +HEADLESS_GQL_JWT_SECRET = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{os.environ.get('STAGE')}_9c_IAP_HEADLESS_GQL_JWT_SECRET", + True +)["Value"] engine = create_engine(DB_URI, pool_size=5, max_overflow=5) @@ -90,7 +93,7 @@ def process(sess: Session, message: SQSMessageRecord, nonce: int = None) -> Tupl region_name = os.environ.get("REGION_NAME", "us-east-2") logging.debug(f"STAGE: {stage} || REGION: {region_name}") account = Account(fetch_kms_key_id(stage, region_name)) - gql = GQL(GQL_URL) + gql = GQL(GQL_URL, HEADLESS_GQL_JWT_SECRET) if not nonce: nonce = gql.get_next_nonce(account.address) diff --git a/worker/worker/tracker.py b/worker/worker/tracker.py index 0e2c682a..33adc8f1 100644 --- a/worker/worker/tracker.py +++ b/worker/worker/tracker.py @@ -11,7 +11,7 @@ from common._graphql import GQL from common.enums import TxStatus from common.models.receipt import Receipt -from common.utils.aws import fetch_secrets +from common.utils.aws import fetch_secrets, fetch_parameter from common.utils.receipt import PlanetID DB_URI = os.environ.get("DB_URI") @@ -19,6 +19,11 @@ DB_URI = DB_URI.replace("[DB_PASSWORD]", db_password) CURRENT_PLANET = PlanetID.ODIN if os.environ.get("STAGE") == "mainnet" else PlanetID.ODIN_INTERNAL GQL_URL = f"{os.environ.get('HEADLESS')}/graphql" +HEADLESS_GQL_JWT_SECRET = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{os.environ.get('STAGE')}_9c_IAP_HEADLESS_GQL_JWT_SECRET", + True +)["Value"] BLOCK_LIMIT = 50 @@ -26,7 +31,7 @@ def process(tx_id: str) -> Tuple[str, Optional[TxStatus], Optional[str]]: - client = GQL(GQL_URL) + client = GQL(GQL_URL, HEADLESS_GQL_JWT_SECRET) query = dsl_gql( DSLQuery( client.ds.StandaloneQuery.transaction.select( From de986802d5b72319c4f1a02a24a84f2ab760d7d1 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 09:50:40 +0900 Subject: [PATCH 06/15] Conventions --- common/_graphql.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/common/_graphql.py b/common/_graphql.py index 0fcfc825..7bcd6f13 100644 --- a/common/_graphql.py +++ b/common/_graphql.py @@ -65,7 +65,7 @@ def get_next_nonce(self, address: str) -> int: return resp["transaction"]["nextTxNonce"] def _unload_from_garage(self, pubkey: bytes, nonce: int, **kwargs) -> bytes: - ts = kwargs.get("timestamp", (datetime.datetime.utcnow()+datetime.timedelta(days=1)).isoformat()) + ts = kwargs.get("timestamp", (datetime.datetime.utcnow() + datetime.timedelta(days=1)).isoformat()) fav_data = kwargs.get("fav_data") avatar_addr = kwargs.get("avatar_addr") item_data = kwargs.get("item_data") @@ -94,7 +94,7 @@ def _unload_from_garage(self, pubkey: bytes, nonce: int, **kwargs) -> bytes: return bytes.fromhex(result["actionTxQuery"]["unloadFromMyGarages"]) def _claim_items(self, pubkey: bytes, nonce: int, **kwargs) -> bytes: - ts = kwargs.get("timestamp", (datetime.datetime.utcnow()+datetime.timedelta(days=1)).isoformat()) + ts = kwargs.get("timestamp", (datetime.datetime.utcnow() + datetime.timedelta(days=1)).isoformat()) claim_data = kwargs.get("claim_data") memo = kwargs.get("memo") @@ -116,7 +116,7 @@ def _claim_items(self, pubkey: bytes, nonce: int, **kwargs) -> bytes: return bytes.fromhex(result["actionTxQuery"]["unloadFromMyGarages"]) def _transfer_asset(self, pubkey: bytes, nonce: int, **kwargs) -> bytes: - ts = kwargs.get("timestamp", (datetime.datetime.utcnow()+datetime.timedelta(days=1)).isoformat()) + ts = kwargs.get("timestamp", (datetime.datetime.utcnow() + datetime.timedelta(days=1)).isoformat()) sender = kwargs.get("sender") recipient = kwargs.get("recipient") currency = kwargs.get("currency") From adf2ddbecd50c3e01a8b232e48bdbda4c06c566d Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 11:09:10 +0900 Subject: [PATCH 07/15] Have to use provided issuer --- common/_graphql.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/_graphql.py b/common/_graphql.py index 7bcd6f13..d779c24b 100644 --- a/common/_graphql.py +++ b/common/_graphql.py @@ -29,7 +29,7 @@ def create_token(self) -> str: return jwt.encode({ "iat": iat, "exp": iat + datetime.timedelta(minutes=1), - "iss": "NineChronicles.IAP", + "iss": "planetariumhq.com" }, self.__jwt_secret) def __create_header(self): From 9bf70a24eebdb7496830bd4bcb7fff025dc05692 Mon Sep 17 00:00:00 2001 From: hyeon Date: Tue, 26 Mar 2024 11:23:49 +0900 Subject: [PATCH 08/15] Add JWT headless test --- tests/common/test_graphql.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 tests/common/test_graphql.py diff --git a/tests/common/test_graphql.py b/tests/common/test_graphql.py new file mode 100644 index 00000000..3a69821c --- /dev/null +++ b/tests/common/test_graphql.py @@ -0,0 +1,14 @@ +import pytest + +from common._graphql import GQL +from iap import settings + + +@pytest.mark.parametrize("headless", [ + "https://9c-main-jwt.nine-chronicles.com/graphql", + "https://heimdall-jwt.nine-chronicles.com/graphql", +]) +def test_gql_jwt(headless): + gql = GQL(headless, settings.HEADLESS_JWT_GQL_SECRET) + test_nonce = gql.get_next_nonce("0x0000000000000000000000000000000000000000") + assert test_nonce == 0 From 51cc080de43dd4c5773876261de8cabc292228a3 Mon Sep 17 00:00:00 2001 From: hyeon Date: Wed, 5 Jun 2024 11:22:09 +0900 Subject: [PATCH 09/15] Check sku for K - Add apple_k SKU when finding product - Update error message for free purchase --- iap/api/purchase.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 2c3cb644..75c35bad 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -378,7 +378,11 @@ def free_product(receipt_data: FreeReceiptSchema, .options(joinedload(Product.fav_list)).options(joinedload(Product.fungible_item_list)) .where( Product.active.is_(True), - or_(Product.google_sku == receipt_data.sku, Product.apple_sku == receipt_data.sku) + or_( + Product.google_sku == receipt_data.sku, + Product.apple_sku == receipt_data.sku, + Product.apple_sku_k == receipt_data.sku + ) ) ) order_id = f"FREE-{uuid4()}" @@ -400,8 +404,8 @@ def free_product(receipt_data: FreeReceiptSchema, # Validation if not product: receipt.status = ReceiptStatus.INVALID - receipt.msg = f"Product {receipt_data.product_id} not exists or inactive" - raise_error(sess, receipt, ValueError(f"Product {receipt_data.product_id} not found or inactive")) + receipt.msg = f"Product {receipt_data.sku} not exists or inactive" + raise_error(sess, receipt, ValueError(f"Product {receipt_data.sku} not found or inactive")) if not product.is_free: receipt.status = ReceiptStatus.INVALID From afcb51bf73fd17e8a0c7ce4ea69f875f85d0330f Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 13 Jun 2024 10:33:09 +0900 Subject: [PATCH 10/15] Add jwt secret to env. variables --- .github/workflows/main.yml | 1 + .github/workflows/test.yml | 3 +++ 2 files changed, 4 insertions(+) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index f5587ba6..0c64ac53 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,7 @@ jobs: APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} GOOGLE_CREDENTIAL: ${{ secrets.GOOGLE_CREDENTIAL }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} build_frontend: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 47b63ca7..647de6a0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -15,6 +15,8 @@ on: required: true SEASON_PASS_JWT_SECRET: required: true + HEADLESS_GQL_JWT_SECRET: + required: true SLACK_WEBHOOK_URL: required: true @@ -87,6 +89,7 @@ jobs: APPLE_ISSUER_ID: ${{ secrets.APPLE_ISSUER_ID }} APPLE_BUNDLE_ID: ${{ vars.APPLE_BUNDLE_ID }} SEASON_PASS_JWT_SECRET: ${{ secrets.SEASON_PASS_JWT_SECRET }} + HEADLESS_GQL_JWT_SECRET: ${{ secrets.HEADLESS_GQL_JWT_SECRET }} run: | poetry run pytest tests From aece458efc53a8517d636c5dec22e5b33f38027a Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 13 Jun 2024 11:03:38 +0900 Subject: [PATCH 11/15] Use JWT header to check IAP garage --- worker/worker/status_monitor.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/worker/worker/status_monitor.py b/worker/worker/status_monitor.py index c1d816e6..8ffdff4b 100644 --- a/worker/worker/status_monitor.py +++ b/worker/worker/status_monitor.py @@ -10,7 +10,7 @@ from common._graphql import GQL from common.enums import ReceiptStatus, TxStatus from common.models.receipt import Receipt -from common.utils.aws import fetch_secrets +from common.utils.aws import fetch_parameter, fetch_secrets from common.utils.receipt import PlanetID STAGE = os.environ.get("STAGE") @@ -21,6 +21,11 @@ GQL_URL = f"{os.environ.get('HEADLESS')}/graphql" IAP_ALERT_WEBHOOK_URL = os.environ.get("IAP_ALERT_WEBHOOK_URL") IAP_GARAGE_WEBHOOK_URL = os.environ.get("IAP_GARAGE_WEBHOOK_URL") +HEADLESS_GQL_JWT_SECRET = fetch_parameter( + os.environ.get("REGION_NAME"), + f"{os.environ.get('STAGE')}_9c_IAP_HEADLESS_GQL_JWT_SECRET", + True +)["Value"] FUNGIBLE_DICT = { "3991e04dd808dc0bc24b21f5adb7bf1997312f8700daf1334bf34936e8a0813a": "Hourglass (400000)", @@ -136,6 +141,7 @@ def check_no_tx(sess): def check_garage(): """Report IAP Garage stock""" + gql = GQL(jwt_secret=HEADLESS_GQL_JWT_SECRET) query = """{ stateQuery { garages( @@ -159,7 +165,7 @@ def check_garage(): } }""" - resp = requests.post(GQL_URL, json={"query": query}, headers={"Authorization": f"Bearer {GQL.create_token()}"}) + resp = requests.post(GQL_URL, json={"query": query}, headers={"Authorization": f"Bearer {gql.create_token()}"}) data = resp.json()["data"]["stateQuery"]["garages"] fav_data = data["garageBalances"] item_data = data["fungibleItemGarages"] From f6d6d2eec48a3d16907a8e3b753bbdf3b3881388 Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 28 Jun 2024 16:45:43 +0900 Subject: [PATCH 12/15] Use provided package name to ACK purchase --- iap/api/purchase.py | 2 +- iap/validator/google.py | 19 ++++--------------- 2 files changed, 5 insertions(+), 16 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index 75c35bad..efb3b2bd 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -223,7 +223,7 @@ def request_product(receipt_data: ReceiptSchema, # raise_error(sess, receipt, ValueError( # f"Invalid Product ID: Given {product.google_sku} is not identical to found from receipt: {purchase.productId}")) if success: - ack_google(product_id, token) + ack_google(x_iap_packagename, product_id, token) ## Apple elif receipt_data.store in (Store.APPLE, Store.APPLE_TEST): success, msg, purchase = validate_apple(receipt.package_name, order_id) diff --git a/iap/validator/google.py b/iap/validator/google.py index bbb41b40..e14a139f 100644 --- a/iap/validator/google.py +++ b/iap/validator/google.py @@ -1,29 +1,18 @@ from typing import Tuple, Optional from common import logger -from common.enums import GooglePurchaseState +from common.enums import GooglePurchaseState, PackageName from common.utils.google import get_google_client from iap import settings from iap.schemas.receipt import GooglePurchaseSchema -def ack_google(sku: str, token: str): +def ack_google(package_name: PackageName, sku: str, token: str): client = get_google_client(settings.GOOGLE_CREDENTIAL) try: (client.purchases().products() - .acknowledge(packageName=settings.GOOGLE_PACKAGE_NAME, productId=sku, token=token) - .execute() - ) - except Exception as e: - logger.error(e) - -def consume_google(sku: str, token: str): - client = get_google_client(settings.GOOGLE_CREDENTIAL) - try: - (client.purchases().products() - .consume(packageName=settings.GOOGLE_PACKAGE_NAME, productId=sku, token=token) - .execute() - ) + .acknowledge(packageName=package_name.value, productId=sku, token=token) + .execute()) except Exception as e: logger.error(e) From 3c9971711499f02912bb2314e4fe453801975f23 Mon Sep 17 00:00:00 2001 From: hyeon Date: Fri, 28 Jun 2024 17:16:27 +0900 Subject: [PATCH 13/15] Track refunded purchases for both google packages --- worker/worker/google_refund_tracker.py | 54 ++++++++++++++++---------- 1 file changed, 34 insertions(+), 20 deletions(-) diff --git a/worker/worker/google_refund_tracker.py b/worker/worker/google_refund_tracker.py index a3590c7c..5f850055 100644 --- a/worker/worker/google_refund_tracker.py +++ b/worker/worker/google_refund_tracker.py @@ -5,10 +5,20 @@ from typing import Optional from common import logger +from common.enums import PackageName from common.utils.aws import fetch_parameter from common.utils.google import get_google_client, Spreadsheet -GOOGLE_PACKAGE_NAME = os.environ.get("GOOGLE_PACKAGE_NAME") +GOOGLE_PACKAGE_DICT = { + PackageName.NINE_CHRONICLES_M: { + "app_name": "9c M", + "sheet_name": "Google", + }, + PackageName.NINE_CHRONICLES_K: { + "app_name": "9c K", + "sheet_name": "Google_K" + }, +} GOOGLE_CREDENTIAL = fetch_parameter( os.environ.get("REGION_NAME"), f"{os.environ.get('STAGE')}_9c_IAP_GOOGLE_CREDENTIAL", True @@ -56,25 +66,29 @@ def __post_init__(self): def handle(event, context): client = get_google_client(GOOGLE_CREDENTIAL) sheet = Spreadsheet(GOOGLE_CREDENTIAL, SHEET_ID) - prev_data = sheet.get_values("Google!A2:B").get("values", []) - prev_order_id = set([x[1] for x in prev_data]) - last_num = int(prev_data[-1][0]) + 1 if prev_data else 1 - logger.info(f"{len(prev_data)} refunded data are present.") - voided_list = client.purchases().voidedpurchases().list(packageName=GOOGLE_PACKAGE_NAME).execute() - voided_list = sorted([RefundData(**x) for x in voided_list["voidedPurchases"]], key=lambda x: x.voidedTimeMillis) - - new_data = [] - index = last_num - for void in voided_list: - if void.orderId in prev_order_id: - continue - new_data.append( - [index, void.orderId, void.purchaseTime.isoformat(), void.voidedTime.isoformat(), void.voidedSource.name, - void.voidedReason.name]) - index += 1 - - sheet.set_values(f"Google!A{last_num + 1}:F", new_data) - logger.info(f"{len(new_data)} Refunds are added") + + for package_name, data in GOOGLE_PACKAGE_DICT.items(): + prev_data = sheet.get_values(f"{data['sheet_name']}!A2:B").get("values", []) + prev_order_id = set([x[1] for x in prev_data]) + last_num = int(prev_data[-1][0]) + 1 if prev_data else 1 + logger.info(f"{len(prev_data)} refunded data are present in {data['app_name']}") + voided_list = client.purchases().voidedpurchases().list(packageName=package_name.value).execute() + voided_list = sorted([RefundData(**x) for x in voided_list["voidedPurchases"]], + key=lambda x: x.voidedTimeMillis) + + new_data = [] + index = last_num + for void in voided_list: + if void.orderId in prev_order_id: + continue + new_data.append( + [index, void.orderId, void.purchaseTime.isoformat(), void.voidedTime.isoformat(), + void.voidedSource.name, + void.voidedReason.name]) + index += 1 + + sheet.set_values(f"{data['sheet_name']}!A{last_num + 1}:F", new_data) + logger.info(f"{len(new_data)} Refunds are added to {data['app_name']}") if __name__ == "__main__": From 632f29e45c5a046b22ead77f08b25f45d4d32747 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 1 Aug 2024 15:14:45 +0900 Subject: [PATCH 14/15] Add apple_sku_k into product schema --- iap/schemas/product.py | 1 + 1 file changed, 1 insertion(+) diff --git a/iap/schemas/product.py b/iap/schemas/product.py index 9f1d2011..a725ae9c 100644 --- a/iap/schemas/product.py +++ b/iap/schemas/product.py @@ -10,6 +10,7 @@ class SimpleProductSchema(BaseSchema): order: int google_sku: str = "" apple_sku: str = "" + apple_sku_k: str = "" is_free: bool # product_type: ProductType daily_limit: Optional[int] = None From 986018f2adedd7c1aca78ada1ab2fc1d66af5703 Mon Sep 17 00:00:00 2001 From: hyeon Date: Thu, 1 Aug 2024 15:34:19 +0900 Subject: [PATCH 15/15] Update message to send SKU by platform --- iap/api/purchase.py | 1 + worker/worker/handler.py | 9 +++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/iap/api/purchase.py b/iap/api/purchase.py index efb3b2bd..3febf547 100644 --- a/iap/api/purchase.py +++ b/iap/api/purchase.py @@ -279,6 +279,7 @@ def request_product(receipt_data: ReceiptSchema, "agent_addr": receipt.agent_addr, "avatar_addr": receipt.avatar_addr, "planet_id": receipt_data.planetId.decode(), + "package_name": receipt.package_name, })) logger.info(f"Voucher message: {resp['MessageId']}") diff --git a/worker/worker/handler.py b/worker/worker/handler.py index 2062e443..dfedaaf8 100644 --- a/worker/worker/handler.py +++ b/worker/worker/handler.py @@ -13,7 +13,7 @@ from common import logger from common._crypto import Account from common._graphql import GQL -from common.enums import TxStatus +from common.enums import TxStatus, PackageName from common.models.product import Product from common.models.receipt import Receipt from common.utils.actions import create_unload_my_garages_action_plain_value @@ -106,7 +106,12 @@ def process(sess: Session, message: SQSMessageRecord, nonce: int = None) -> Tupl planet_id: PlanetID = PlanetID(bytes(message.body["planet_id"], 'utf-8')) agent_address = message.body.get("agent_addr") avatar_address = message.body.get("avatar_addr") - memo = json.dumps({"iap": {"g_sku": product.google_sku, "a_sku": product.apple_sku}}) + package_name = PackageName(message.body.get("package_name")) + memo = json.dumps({"iap": + {"g_sku": product.google_sku, + "a_sku": product.apple_sku_k if package_name == PackageName.NINE_CHRONICLES_K + else product.apple_sku} + }) # Through bridge if planet_id != CURRENT_PLANET: agent_address = planet_dict[planet_id]["agent"]