Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions services/wallet/thirdparty/activity/alchemy/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
25 changes: 25 additions & 0 deletions services/wallet/thirdparty/activity/alchemy/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
183 changes: 183 additions & 0 deletions services/wallet/thirdparty/activity/alchemy/types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
})
}
}
Loading