diff --git a/discovery-provider/integration_tests/tasks/entity_manager/test_track_entity_manager.py b/discovery-provider/integration_tests/tasks/entity_manager/test_track_entity_manager.py index b07638ffedb..d97865baea5 100644 --- a/discovery-provider/integration_tests/tasks/entity_manager/test_track_entity_manager.py +++ b/discovery-provider/integration_tests/tasks/entity_manager/test_track_entity_manager.py @@ -84,7 +84,7 @@ def test_index_valid_track(app, mocker): }, "track_id": 77955, "stem_of": None, - "ai_attribution_user_id": 2 + "ai_attribution_user_id": 2, }, "QmCreateTrack2": { "owner_id": 1, @@ -164,6 +164,45 @@ def test_index_valid_track(app, mocker): "iswc": "", "is_playlist_upload": False, }, + "QmCreateTrack4": { + "owner_id": 2, + "track_cid": "some-track-cid-4", + "title": "track 4", + "length": None, + "cover_art": None, + "cover_art_sizes": "QmQKXkVxGBbCFjcnhgxftzYDhph1CT8PJCuPEsRpffjjGC", + "tags": None, + "genre": "Rock", + "mood": None, + "credits_splits": None, + "created_at": None, + "create_date": None, + "updated_at": None, + "release_date": None, + "file_type": None, + "track_segments": [], + "has_current_user_reposted": False, + "is_current": True, + "is_unlisted": False, + "is_premium": False, + "premium_conditions": None, + "field_visibility": { + "genre": True, + "mood": True, + "tags": True, + "share": True, + "play_count": True, + "remixes": True, + }, + "remix_of": None, + "repost_count": 0, + "save_count": 0, + "description": "", + "license": "", + "isrc": "", + "iswc": "", + "is_playlist_upload": False, + }, "QmUpdateTrack1": { "owner_id": 1, "track_cid": "some-track-cid", @@ -214,11 +253,12 @@ def test_index_valid_track(app, mocker): "track_id": 77955, "stem_of": None, "is_playlist_upload": False, - "ai_attribution_user_id": 2 + "ai_attribution_user_id": 2, }, } track3_json = json.dumps(test_metadata["QmCreateTrack3"]) + track4_json = json.dumps(test_metadata["QmCreateTrack4"]) tx_receipts = { "CreateTrack1Tx": [ { @@ -290,6 +330,21 @@ def test_index_valid_track(app, mocker): ) }, ], + # Delegated track write + "CreateTrack4Tx": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET + 3, + "_entityType": "Track", + "_userId": 2, + "_action": "Create", + "_metadata": f'{{"cid": "QmCreateTrack4", "data": {track4_json}}}', + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb76c", + } + ) + }, + ], } entity_manager_txs = [ @@ -309,8 +364,27 @@ def get_events_side_effect(_, tx_receipt): entities = { "users": [ {"user_id": 1, "handle": "user-1", "wallet": "user1wallet"}, - {"user_id": 2, "handle": "user-2", "wallet": "user2wallet", "allow_ai_attribution": True}, - ] + { + "user_id": 2, + "handle": "user-2", + "wallet": "user2wallet", + "allow_ai_attribution": True, + }, + ], + "app_delegates": [ + { + "user_id": 1, + "name": "My App", + "address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4C4", + }, + ], + "delegations": [ + { + "user_id": 2, + "shared_address": "0xdB384D555480214632D08609848BbFB54CCeb76c", + "delegate_address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4C4", + } + ], } populate_mock_db(db, entities) @@ -329,7 +403,7 @@ def get_events_side_effect(_, tx_receipt): # validate db records all_tracks: List[Track] = session.query(Track).all() - assert len(all_tracks) == 5 + assert len(all_tracks) == 6 track_1: Track = ( session.query(Track) @@ -362,6 +436,17 @@ def get_events_side_effect(_, tx_receipt): assert track_3.title == "track 3" assert track_3.is_delete == False + track_4: Track = ( + session.query(Track) + .filter( + Track.is_current == True, + Track.track_id == TRACK_ID_OFFSET + 3, + ) + .first() + ) + assert track_4.title == "track 4" + assert track_4.is_delete == False + # Check that track routes are updated appropriately track_routes = ( session.query(TrackRoute) @@ -403,11 +488,7 @@ def test_index_invalid_tracks(app, mocker): db = get_db() web3 = Web3() update_task = UpdateTask(None, web3, None) - test_metadata = { - "QmAIDisabled": { - "ai_attribution_user_id": 2 - } - } + test_metadata = {"QmAIDisabled": {"ai_attribution_user_id": 2}} tx_receipts = { # invalid create "CreateTrackBelowOffset": [ @@ -479,7 +560,50 @@ def test_index_invalid_tracks(app, mocker): } ) }, - ], # invalid updates + ], + "CreateTrackInvalidDeletedDelegate": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET + 1, + "_entityType": "Track", + "_userId": 1, + "_action": "Create", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb76c", + } + ) + }, + ], + "CreateTrackInvalidRevokedDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET + 1, + "_entityType": "Track", + "_userId": 1, + "_action": "Create", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7AA", + } + ) + }, + ], + "CreateTrackInvalidWrongUserDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET + 1, + "_entityType": "Track", + "_userId": 1, + "_action": "Create", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7CC", + } + ) + }, + ], + # invalid updates "UpdateTrackInvalidSigner": [ { "args": AttributeDict( @@ -508,6 +632,48 @@ def test_index_invalid_tracks(app, mocker): ) }, ], + "UpdateTrackInvalidDeletedDelegate": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Update", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb76c", + } + ) + }, + ], + "UpdateTrackInvalidRevokedDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Update", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7AA", + } + ) + }, + ], + "UpdateTrackInvalidWrongUserDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Update", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7CC", + } + ) + }, + ], # invalid deletes "DeleteTrackInvalidSigner": [ { @@ -551,6 +717,48 @@ def test_index_invalid_tracks(app, mocker): ) }, ], + "DeleteTrackInvalidDeletedDelegate": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Delete", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb76c", + } + ) + }, + ], + "DeleteTrackInvalidRevokedDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Delete", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7AA", + } + ) + }, + ], + "DeleteTrackInvalidWrongUserDelegation": [ + { + "args": AttributeDict( + { + "_entityId": TRACK_ID_OFFSET, + "_entityType": "Track", + "_userId": 1, + "_action": "Delete", + "_metadata": "", + "_signer": "0xdB384D555480214632D08609848BbFB54CCeb7CC", + } + ) + }, + ], } entity_manager_txs = [ @@ -575,6 +783,37 @@ def get_events_side_effect(_, tx_receipt): "tracks": [ {"track_id": TRACK_ID_OFFSET, "owner_id": 1}, ], + "app_delegates": [ + { + "user_id": 2, + "name": "My App", + "address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4C4", + "is_delete": True, + }, + { + "user_id": 2, + "name": "My App", + "address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4ZZ", + }, + ], + "delegations": [ + { + "user_id": 1, + "shared_address": "0xdB384D555480214632D08609848BbFB54CCeb76c", + "delegate_address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4C4", + }, + { + "user_id": 1, + "shared_address": "0xdB384D555480214632D08609848BbFB54CCeb7AA", + "delegate_address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4ZZ", + "is_revoked": True, + }, + { + "user_id": 2, + "shared_address": "0xdB384D555480214632D08609848BbFB54CCeb7CC", + "delegate_address": "0x3a388671bb4D6E1Ea08D79Ee191b40FB45A8F4ZZ", + }, + ], } populate_mock_db(db, entities) @@ -593,4 +832,4 @@ def get_events_side_effect(_, tx_receipt): # validate db records all_tracks: List[Track] = session.query(Track).all() - assert len(all_tracks) == 1 # no new playlists indexed + assert len(all_tracks) == 1 # no new tracks indexed diff --git a/discovery-provider/integration_tests/utils.py b/discovery-provider/integration_tests/utils.py index f37f5227188..adf5443e9aa 100644 --- a/discovery-provider/integration_tests/utils.py +++ b/discovery-provider/integration_tests/utils.py @@ -287,6 +287,7 @@ def populate_mock_db(db, entities, block_offset=None): is_personal_access=delegate_meta.get("is_personal_access", False), blockhash=hex(i + block_offset), blocknumber=(i + block_offset), + is_delete=delegate_meta.get("is_delete", False), is_current=True, txhash=delegate_meta.get("txhash", str(i + block_offset)), updated_at=delegate_meta.get("updated_at", datetime.now()), diff --git a/discovery-provider/src/tasks/entity_manager/delegation.py b/discovery-provider/src/tasks/entity_manager/delegation.py index 71c9db71ef0..b0ec339ab23 100644 --- a/discovery-provider/src/tasks/entity_manager/delegation.py +++ b/discovery-provider/src/tasks/entity_manager/delegation.py @@ -93,6 +93,16 @@ def validate_delegation_tx(params: ManageEntityParameters, metadata): raise Exception( f"Invalid Create Delegation transaction, delegate address {metadata['delegate_address']} does not exist" ) + if ( + metadata["delegate_address"].lower() + in params.existing_records[EntityType.APP_DELEGATE] + and params.existing_records[EntityType.APP_DELEGATE][ + metadata["delegate_address"].lower() + ].is_delete + ): + raise Exception( + f"Invalid Delegation transaction, delegate address {metadata['delegate_address']} is invalid" + ) if ( metadata["shared_address"].lower() in params.existing_records[EntityType.DELEGATION] diff --git a/discovery-provider/src/tasks/entity_manager/entity_manager.py b/discovery-provider/src/tasks/entity_manager/entity_manager.py index 74125b0102b..30332fe284c 100644 --- a/discovery-provider/src/tasks/entity_manager/entity_manager.py +++ b/discovery-provider/src/tasks/entity_manager/entity_manager.py @@ -322,6 +322,8 @@ def collect_entities_to_fetch(update_task, entity_manager_txs, metadata): action = helpers.get_tx_arg(event, "_action") user_id = helpers.get_tx_arg(event, "_userId") cid = helpers.get_tx_arg(event, "_metadata") + signer = helpers.get_tx_arg(event, "_signer") + json_metadata = None # Check if metadata blob was passed directly and use if so. try: @@ -382,6 +384,8 @@ def collect_entities_to_fetch(update_task, entity_manager_txs, metadata): ) if user_id: entities_to_fetch[EntityType.USER].add(user_id) + if signer: + entities_to_fetch[EntityType.DELEGATION].add(signer.lower()) action = helpers.get_tx_arg(event, "_action") # Query social operations as needed @@ -580,22 +584,6 @@ def fetch_existing_entities(session: Session, entities_to_fetch: EntitiesToFetch for playlist_seen in playlist_seens } - # DELEGATES - if entities_to_fetch[EntityType.APP_DELEGATE]: - delegates: List[AppDelegate] = ( - session.query(AppDelegate) - .filter( - func.lower(AppDelegate.address).in_( - entities_to_fetch[EntityType.APP_DELEGATE] - ), - AppDelegate.is_current == True, - ) - .all() - ) - existing_entities[EntityType.APP_DELEGATE] = { - delegate.address.lower(): delegate for delegate in delegates - } - # DELEGATIONS if entities_to_fetch[EntityType.DELEGATION]: delegations: List[Delegation] = ( @@ -611,6 +599,27 @@ def fetch_existing_entities(session: Session, entities_to_fetch: EntitiesToFetch existing_entities[EntityType.DELEGATION] = { delegation.shared_address.lower(): delegation for delegation in delegations } + for delegation in delegations: + entities_to_fetch[EntityType.APP_DELEGATE].add( + delegation.delegate_address.lower() + ) + + # APP DELEGATES + if entities_to_fetch[EntityType.APP_DELEGATE]: + delegates: List[AppDelegate] = ( + session.query(AppDelegate) + .filter( + func.lower(AppDelegate.address).in_( + entities_to_fetch[EntityType.APP_DELEGATE] + ), + AppDelegate.is_current == True, + ) + .all() + ) + existing_entities[EntityType.APP_DELEGATE] = { + delegate.address.lower(): delegate for delegate in delegates + } + return existing_entities diff --git a/discovery-provider/src/tasks/entity_manager/track.py b/discovery-provider/src/tasks/entity_manager/track.py index 2f4d176af8c..06798105d5b 100644 --- a/discovery-provider/src/tasks/entity_manager/track.py +++ b/discovery-provider/src/tasks/entity_manager/track.py @@ -221,9 +221,31 @@ def validate_track_tx(params: ManageEntityParameters): if user_id not in params.existing_records[EntityType.USER]: raise Exception(f"User {user_id} does not exist") + # Ensure the signer is either the user or a delegate for the user + # TODO (nkang) - Extract to helper wallet = params.existing_records[EntityType.USER][user_id].wallet - if wallet and wallet.lower() != params.signer.lower(): - raise Exception(f"User {user_id} does not match signer") + signer = params.signer.lower() + signer_matches_user = wallet and wallet.lower() == signer + + if not signer_matches_user: + is_signer_delegate = ( + signer in params.existing_records[EntityType.DELEGATION] + and params.existing_records[EntityType.DELEGATION][signer].user_id + == user_id + ) + if is_signer_delegate: + delegation = params.existing_records[EntityType.DELEGATION][signer] + app_delegate = params.existing_records[EntityType.APP_DELEGATE][ + delegation.delegate_address.lower() + ] + if ( + (not app_delegate) + or (app_delegate.is_delete) + or (delegation.is_revoked) + ): + raise Exception(f"Signer is an invalid delegate for user {user_id}") + else: + raise Exception(f"Signer does not match user {user_id} or a valid delegate") if params.entity_type != EntityType.TRACK: raise Exception(f"Entity type {params.entity_type} is not a track") @@ -244,9 +266,11 @@ def validate_track_tx(params: ManageEntityParameters): if params.action != Action.DELETE: track_metadata = params.metadata[params.metadata_cid] - ai_attribution_user_id = track_metadata.get('ai_attribution_user_id') + ai_attribution_user_id = track_metadata.get("ai_attribution_user_id") if ai_attribution_user_id: - ai_attribution_user = params.existing_records[EntityType.USER][ai_attribution_user_id] + ai_attribution_user = params.existing_records[EntityType.USER][ + ai_attribution_user_id + ] if not ai_attribution_user or not ai_attribution_user.allow_ai_attribution: raise Exception(f"Cannot AI attribute user {ai_attribution_user}")