diff --git a/services/wallet/thirdparty/activity/alchemy/client.go b/services/wallet/thirdparty/activity/alchemy/client.go index c1e68cfe547..e7561bd2fe5 100644 --- a/services/wallet/thirdparty/activity/alchemy/client.go +++ b/services/wallet/thirdparty/activity/alchemy/client.go @@ -103,7 +103,11 @@ func (c *Client) FetchTransfers(ctx context.Context, chainID uint64, parameters if err != nil { return nil, nextCursor, err } - responseTransfers = append(responseTransfers, tmpResponse.Transfers...) + for _, t := range tmpResponse.Transfers { + if t.IsValid() { + responseTransfers = append(responseTransfers, t) + } + } if tmpResponse.PageKey == "" { outgoingCursor = "" outgoingDone = true @@ -121,7 +125,11 @@ func (c *Client) FetchTransfers(ctx context.Context, chainID uint64, parameters if err != nil { return nil, nextCursor, err } - responseTransfers = append(responseTransfers, tmpResponse.Transfers...) + for _, t := range tmpResponse.Transfers { + if t.IsValid() { + responseTransfers = append(responseTransfers, t) + } + } if tmpResponse.PageKey == "" { incomingCursor = "" incomingDone = true diff --git a/services/wallet/thirdparty/activity/alchemy/types.go b/services/wallet/thirdparty/activity/alchemy/types.go index 3de4e79c34b..1e228d82d11 100644 --- a/services/wallet/thirdparty/activity/alchemy/types.go +++ b/services/wallet/thirdparty/activity/alchemy/types.go @@ -78,6 +78,31 @@ func (t Transfer) IsIncoming(accountAddress common.Address) bool { return t.FromAddress != accountAddress } +func (t Transfer) IsValid() bool { + if t.BlockNum == nil { + return false + } + switch t.Category { + case TransferCategoryExternal: + return t.RawContract.Value != nil + case TransferCategoryErc20: + return t.RawContract.Address != nil && t.RawContract.Value != nil + case TransferCategoryErc721: + return t.TokenID != nil && t.RawContract.Address != nil + case TransferCategoryErc1155: + if t.RawContract.Address == nil || len(t.Erc1155Metadata) == 0 { + return false + } + for _, m := range t.Erc1155Metadata { + if m.TokenID == nil || m.Value == nil { + return false + } + } + return true + } + return true +} + type Erc1155Metadata struct { TokenID *bigint.VarHexBigInt `json:"tokenId"` Value *bigint.VarHexBigInt `json:"value"` diff --git a/services/wallet/thirdparty/activity/alchemy/types_test.go b/services/wallet/thirdparty/activity/alchemy/types_test.go index 8f78c383d1b..de4435fa702 100644 --- a/services/wallet/thirdparty/activity/alchemy/types_test.go +++ b/services/wallet/thirdparty/activity/alchemy/types_test.go @@ -49,3 +49,186 @@ func TestTransferToCommon(t *testing.T) { require.Equal(t, result[i], len(commonResponse)) } } + +var malformedErc721Response = `{ + "transfers": [{ + "blockNum": "0x6a7f8e", + "uniqueId": "0x0e0c4834de556d22a03c1727435f88fb38cefd7c67ee0ab98c6aa1ccfb19a7cb:log:66", + "hash": "0x0e0c4834de556d22a03c1727435f88fb38cefd7c67ee0ab98c6aa1ccfb19a7cb", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "value": null, + "erc721TokenId": null, + "erc1155Metadata": null, + "tokenId": null, + "asset": "FA721", + "category": "erc721", + "rawContract": { + "value": null, + "address": "0x9f64932be34d5d897c4253d17707b50921f372b6", + "decimal": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +var malformedErc1155Response = `{ + "transfers": [{ + "blockNum": "0x6a7f93", + "uniqueId": "0xde60d8ac4248630da5ec7f1547e55b64f432204799c16b44384e493590cd5a70:log:68", + "hash": "0xde60d8ac4248630da5ec7f1547e55b64f432204799c16b44384e493590cd5a70", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "value": null, + "erc721TokenId": null, + "erc1155Metadata": [ + { + "tokenId": null, + "value": "0x01" + } + ], + "tokenId": null, + "asset": "FA1155", + "category": "erc1155", + "rawContract": { + "value": null, + "address": "0x1ed60fedff775d500dde21a974cd4e92e0047cc8", + "decimal": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:41:00.000Z" + } + }] +}` + +var erc20NullAddress = `{ + "transfers": [{ + "blockNum": "0x100", + "uniqueId": "test:erc20:null:address", + "hash": "0x1234567890123456789012345678901234567890123456789012345678901234", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "category": "erc20", + "rawContract": { + "value": "0x100", + "address": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +var erc721NullAddress = `{ + "transfers": [{ + "blockNum": "0x100", + "uniqueId": "test:erc721:null:address", + "hash": "0x1234567890123456789012345678901234567890123456789012345678901234", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "category": "erc721", + "tokenId": "0x1", + "rawContract": { + "value": null, + "address": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +var erc1155NullAddress = `{ + "transfers": [{ + "blockNum": "0x100", + "uniqueId": "test:erc1155:null:address", + "hash": "0x1234567890123456789012345678901234567890123456789012345678901234", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "category": "erc1155", + "erc1155Metadata": [{"tokenId": "0x1", "value": "0x1"}], + "rawContract": { + "value": null, + "address": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +var externalNullValue = `{ + "transfers": [{ + "blockNum": "0x100", + "uniqueId": "test:external:null:value", + "hash": "0x1234567890123456789012345678901234567890123456789012345678901234", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "category": "external", + "rawContract": { + "value": null, + "address": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +var nullBlockNum = `{ + "transfers": [{ + "blockNum": null, + "uniqueId": "test:null:blocknum", + "hash": "0x1234567890123456789012345678901234567890123456789012345678901234", + "from": "0xa1e277ea6b97effc5b61b3bf5de03f438981247e", + "to": "0x0adb6caa256a5375c638c00e2ff80a9ac1b2d3a7", + "category": "external", + "rawContract": { + "value": "0x100", + "address": null + }, + "metadata": { + "blockTimestamp": "2024-10-30T22:40:00.000Z" + } + }] +}` + +func TestMalformedAlchemyResponses(t *testing.T) { + testCases := []struct { + name string + data string + }{ + {"empty transfers array", `{"transfers": []}`}, + {"missing transfers field", `{}`}, + {"ERC721 with nil tokenId", malformedErc721Response}, + {"ERC1155 with nil tokenId", malformedErc1155Response}, + {"ERC20 with null rawContract.address", erc20NullAddress}, + {"ERC721 with null rawContract.address", erc721NullAddress}, + {"ERC1155 with null rawContract.address", erc1155NullAddress}, + {"external with null rawContract.value", externalNullValue}, + {"null blockNum", nullBlockNum}, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + var response alchemy.GetAssetTranfersResponse + err := json.Unmarshal([]byte(tc.data), &response) + require.NoError(t, err) + + // Filter valid transfers (simulates what FetchTransfers does) + validTransfers := make([]alchemy.Transfer, 0) + for _, t := range response.Transfers { + if t.IsValid() { + validTransfers = append(validTransfers, t) + } + } + + // Should not panic with filtered transfers + result := alchemy.TransfersToThirdpartyActivityEntries( + validTransfers, 1, common.HexToAddress("0xa1e277ea6b97effc5b61b3bf5de03f438981247e")) + require.NotNil(t, result) + }) + } +}