Skip to content

Commit 6767578

Browse files
committed
feat_: add alchemy activity client
1 parent 106791a commit 6767578

File tree

8 files changed

+21547
-0
lines changed

8 files changed

+21547
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package bigint
2+
3+
import (
4+
"encoding/json"
5+
"fmt"
6+
"math/big"
7+
)
8+
9+
// Unmarshals hex string as a variable-length hex string with 0x prefix and possible leading zeros
10+
type VarHexBigInt struct {
11+
*big.Int
12+
}
13+
14+
func (b *VarHexBigInt) UnmarshalJSON(input []byte) error {
15+
var hexStr string
16+
if err := json.Unmarshal(input, &hexStr); err != nil {
17+
return err
18+
}
19+
20+
var ok bool
21+
b.Int, ok = new(big.Int).SetString(hexStr[2:], 16)
22+
if !ok {
23+
return fmt.Errorf("not a valid big integer: %s", hexStr)
24+
}
25+
return nil
26+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
package alchemy
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"math/big"
7+
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
10+
"github.com/status-im/status-go/rpc"
11+
"github.com/status-im/status-go/services/wallet/connection"
12+
"github.com/status-im/status-go/services/wallet/thirdparty"
13+
14+
wc "github.com/status-im/status-go/services/wallet/common"
15+
)
16+
17+
const AlchemyID = "alchemy"
18+
19+
func areInternalTransfersSupported(chainID uint64) bool {
20+
return chainID == wc.EthereumMainnet
21+
}
22+
23+
type Client struct {
24+
ethClientGetter rpc.EthClientGetter
25+
connectionStatus *connection.Status
26+
}
27+
28+
func (c *Client) ID() string {
29+
return AlchemyID
30+
}
31+
32+
func (c *Client) IsConnected() bool {
33+
return c.connectionStatus.IsConnected()
34+
}
35+
36+
func NewClient(ethClientGetter rpc.EthClientGetter) *Client {
37+
return &Client{
38+
ethClientGetter: ethClientGetter,
39+
connectionStatus: connection.NewStatus(),
40+
}
41+
}
42+
43+
func (c *Client) FetchActivity(ctx context.Context, chainID uint64, parameters thirdparty.ActivityFetchParameters, cursor string, limit int) (thirdparty.ActivityEntryContainer, error) {
44+
response := thirdparty.ActivityEntryContainer{
45+
Provider: c.ID(),
46+
PreviousCursor: cursor,
47+
NextCursor: cursor,
48+
}
49+
maxCount := MaxAssetTransfersCount
50+
if limit > thirdparty.FetchNoLimit && limit < MaxAssetTransfersCount {
51+
maxCount = limit
52+
}
53+
54+
// We need to make one request for outgoing transfers and a separate one for incoming ones
55+
order := TransferOrderNewToOld
56+
if parameters.Order == thirdparty.OldToNew {
57+
order = TransferOrderOldToNew
58+
}
59+
60+
params := GetAssetTransfersParams{
61+
FromBlock: parameters.FromBlock,
62+
ToBlock: parameters.ToBlock,
63+
ToAddress: parameters.Address,
64+
Category: []TransferCategory{
65+
TransferCategoryExternal,
66+
TransferCategoryErc20,
67+
TransferCategoryErc721,
68+
TransferCategoryErc1155,
69+
TransferCategorySpecialNft,
70+
},
71+
Order: order,
72+
WithMetadata: true,
73+
ExcludeZeroValue: false,
74+
MaxCount: (*hexutil.Big)(big.NewInt((int64)(maxCount))),
75+
}
76+
77+
if areInternalTransfersSupported(chainID) {
78+
params.Category = append(params.Category, TransferCategoryInternal)
79+
}
80+
81+
for {
82+
outgoingCursor, outgoingDone, incomingCursor, incomingDone, err := decodeCursor(response.NextCursor)
83+
if err != nil {
84+
return response, err
85+
}
86+
87+
if !outgoingDone {
88+
params.FromAddress = parameters.Address
89+
params.ToAddress = nil
90+
params.PageKey = outgoingCursor
91+
tmpResponse, err := c.fetchActivity(ctx, chainID, params)
92+
if err != nil {
93+
return response, err
94+
}
95+
response.Items = append(response.Items, transfersToCommon(tmpResponse.Transfers, false, chainID)...)
96+
if tmpResponse.PageKey == "" {
97+
outgoingCursor = ""
98+
outgoingDone = true
99+
} else {
100+
outgoingCursor = tmpResponse.PageKey
101+
outgoingDone = false
102+
}
103+
}
104+
105+
if !incomingDone {
106+
params.FromAddress = nil
107+
params.ToAddress = parameters.Address
108+
params.PageKey = incomingCursor
109+
tmpResponse, err := c.fetchActivity(ctx, chainID, params)
110+
if err != nil {
111+
return response, err
112+
}
113+
response.Items = append(response.Items, transfersToCommon(tmpResponse.Transfers, true, chainID)...)
114+
if tmpResponse.PageKey == "" {
115+
incomingCursor = ""
116+
incomingDone = true
117+
} else {
118+
incomingCursor = tmpResponse.PageKey
119+
incomingDone = false
120+
}
121+
}
122+
123+
response.NextCursor = encodeCursor(outgoingCursor, outgoingDone, incomingCursor, incomingDone)
124+
if response.NextCursor == thirdparty.FetchFromStartCursor {
125+
break
126+
}
127+
if limit > thirdparty.FetchNoLimit && len(response.Items) >= limit {
128+
break
129+
}
130+
}
131+
132+
return response, nil
133+
}
134+
135+
func (c *Client) fetchActivity(ctx context.Context, chainID uint64, parameters GetAssetTransfersParams) (*GetAssetTranfersResponse, error) {
136+
client, err := c.ethClientGetter.GetEthClient(chainID)
137+
if err != nil {
138+
return nil, err
139+
}
140+
if client == nil {
141+
return nil, thirdparty.ErrChainIDNotSupported
142+
}
143+
144+
var response GetAssetTranfersResponse
145+
err = client.CallContext(ctx, &response, getAssetTransfersMethod, parameters)
146+
if err != nil {
147+
if ctx.Err() == nil {
148+
c.connectionStatus.SetIsConnected(false)
149+
}
150+
}
151+
c.connectionStatus.SetIsConnected(true)
152+
153+
return &response, err
154+
}
155+
156+
const cursorFormat = "%s///%t///%s///%t"
157+
158+
func decodeCursor(cursor string) (outgoingCursor string, outgoingDone bool, incomingCursor string, incomingDone bool, err error) {
159+
if cursor == thirdparty.FetchFromStartCursor {
160+
return "", false, "", false, nil
161+
}
162+
163+
_, err = fmt.Scanf(cursorFormat, &outgoingCursor, &outgoingDone, &incomingCursor, &incomingDone)
164+
return
165+
}
166+
167+
func encodeCursor(outgoingCursor string, outgoingDone bool, incomingCursor string, incomingDone bool) (cursor string) {
168+
if outgoingDone && incomingDone {
169+
return thirdparty.FetchFromStartCursor
170+
}
171+
return fmt.Sprintf(cursorFormat, outgoingCursor, outgoingDone, incomingCursor, incomingDone)
172+
}
Lines changed: 176 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,176 @@
1+
package alchemy
2+
3+
import (
4+
"math/big"
5+
"time"
6+
7+
"github.com/ethereum/go-ethereum/common"
8+
"github.com/ethereum/go-ethereum/common/hexutil"
9+
"github.com/ethereum/go-ethereum/rpc"
10+
11+
ac "github.com/status-im/status-go/services/wallet/activity/common"
12+
"github.com/status-im/status-go/services/wallet/bigint"
13+
wCommon "github.com/status-im/status-go/services/wallet/common"
14+
"github.com/status-im/status-go/services/wallet/thirdparty"
15+
)
16+
17+
const getAssetTransfersMethod = "alchemy_getAssetTransfers"
18+
const MaxAssetTransfersCount = 1000
19+
20+
type TransferCategory string
21+
22+
const (
23+
TransferCategoryExternal TransferCategory = "external"
24+
TransferCategoryInternal TransferCategory = "internal"
25+
TransferCategoryErc20 TransferCategory = "erc20"
26+
TransferCategoryErc721 TransferCategory = "erc721"
27+
TransferCategoryErc1155 TransferCategory = "erc1155"
28+
TransferCategorySpecialNft TransferCategory = "specialnft"
29+
)
30+
31+
type TransferOrder string
32+
33+
const (
34+
TransferOrderOldToNew TransferOrder = "asc"
35+
TransferOrderNewToOld TransferOrder = "desc"
36+
)
37+
38+
type GetAssetTransfersParams struct {
39+
FromBlock *rpc.BlockNumber `json:"fromBlock"`
40+
ToBlock *rpc.BlockNumber `json:"toBlock"`
41+
FromAddress *common.Address `json:"fromAddress,omitempty"`
42+
ToAddress *common.Address `json:"toAddress,omitempty"`
43+
ContractAddresses []common.Address `json:"contractAddresses,omitempty"`
44+
Category []TransferCategory `json:"category"`
45+
Order TransferOrder `json:"order"`
46+
WithMetadata bool `json:"withMetadata"`
47+
ExcludeZeroValue bool `json:"excludeZeroValue"`
48+
MaxCount *hexutil.Big `json:"maxCount"`
49+
PageKey string `json:"pageKey,omitempty"`
50+
}
51+
52+
type GetAssetTranfersResponse struct {
53+
Transfers []Transfer `json:"transfers"`
54+
PageKey string `json:"pageKey"`
55+
}
56+
57+
type transferData struct {
58+
Token ac.Token
59+
Value *hexutil.Big
60+
}
61+
62+
func transfersToCommon(tt []Transfer, isIncoming bool, chainID uint64) []thirdparty.ActivityEntry {
63+
entries := make([]thirdparty.ActivityEntry, 0, len(tt))
64+
65+
cChainID := wCommon.ChainID(chainID)
66+
for _, t := range tt {
67+
if t.ToAddress == nil {
68+
entry := thirdparty.ActivityEntry{
69+
ActivityType: ac.ContractDeploymentAT,
70+
ChainIDOut: &chainID,
71+
Timestamp: t.Metadata.BlockTimestamp.Unix(),
72+
Sender: &t.FromAddress,
73+
Recipient: t.ToAddress,
74+
TxHash: t.Hash,
75+
BlockNumber: (*hexutil.Big)(t.BlockNum.Int),
76+
}
77+
// Alchemy doesn't provide the contract address for contract deployment
78+
entries = append(entries, entry)
79+
} else {
80+
transfers := make([]transferData, 0, 1)
81+
switch t.Category {
82+
case TransferCategoryErc20:
83+
transfers = append(transfers, transferData{
84+
Token: ac.Token{
85+
ChainID: cChainID,
86+
TokenType: ac.Erc20,
87+
Address: *t.RawContract.Address,
88+
},
89+
Value: (*hexutil.Big)(t.RawContract.Value.Int),
90+
})
91+
case TransferCategoryErc721, TransferCategorySpecialNft:
92+
transfers = append(transfers, transferData{
93+
Token: ac.Token{
94+
ChainID: cChainID,
95+
TokenType: ac.Erc721,
96+
Address: *t.RawContract.Address,
97+
TokenID: (*hexutil.Big)(t.TokenID.Int),
98+
},
99+
Value: (*hexutil.Big)(big.NewInt(1)),
100+
})
101+
case TransferCategoryErc1155:
102+
for _, m := range t.Erc1155Metadata {
103+
transfers = append(transfers, transferData{
104+
Token: ac.Token{
105+
ChainID: cChainID,
106+
TokenType: ac.Erc1155,
107+
Address: *t.RawContract.Address,
108+
TokenID: (*hexutil.Big)(t.TokenID.Int),
109+
},
110+
Value: (*hexutil.Big)(m.Value.Int),
111+
})
112+
}
113+
default:
114+
transfers = append(transfers, transferData{
115+
Token: ac.Token{
116+
ChainID: cChainID,
117+
TokenType: ac.Native,
118+
},
119+
Value: (*hexutil.Big)(t.RawContract.Value.Int),
120+
})
121+
}
122+
123+
for _, transfer := range transfers {
124+
entry := thirdparty.ActivityEntry{
125+
Timestamp: t.Metadata.BlockTimestamp.Unix(),
126+
Sender: &t.FromAddress,
127+
Recipient: t.ToAddress,
128+
TxHash: t.Hash,
129+
BlockNumber: (*hexutil.Big)(t.BlockNum.Int),
130+
}
131+
if isIncoming {
132+
entry.ActivityType = ac.ReceiveAT
133+
entry.ChainIDIn = &chainID
134+
entry.TokenIn = &transfer.Token
135+
entry.AmountIn = transfer.Value
136+
} else {
137+
entry.ActivityType = ac.SendAT
138+
entry.ChainIDOut = &chainID
139+
entry.TokenOut = &transfer.Token
140+
entry.AmountOut = transfer.Value
141+
}
142+
entries = append(entries, entry)
143+
}
144+
}
145+
}
146+
return entries
147+
}
148+
149+
type Transfer struct {
150+
Category TransferCategory `json:"category"`
151+
BlockNum *bigint.VarHexBigInt `json:"blockNum"`
152+
FromAddress common.Address `json:"from"`
153+
ToAddress *common.Address `json:"to,omitempty"`
154+
Erc1155Metadata []Erc1155Metadata `json:"erc1155Metadata,omitempty"`
155+
TokenID *bigint.VarHexBigInt `json:"tokenId"`
156+
Asset string `json:"asset"`
157+
UniqueID string `json:"uniqueId"`
158+
Hash common.Hash `json:"hash"`
159+
RawContract RawContract `json:"rawContract"`
160+
Metadata Metadata `json:"metadata"`
161+
}
162+
163+
type Erc1155Metadata struct {
164+
TokenID *bigint.VarHexBigInt `json:"tokenId"`
165+
Value *bigint.VarHexBigInt `json:"value"`
166+
}
167+
168+
type RawContract struct {
169+
Value *bigint.VarHexBigInt `json:"value"` // nil if ERC721 or ERC1155 transfer
170+
Address *common.Address `json:"address"` // nil if external or internal transfer
171+
Decimal *bigint.VarHexBigInt `json:"decimal"` // nil if not available in the contract
172+
}
173+
174+
type Metadata struct {
175+
BlockTimestamp time.Time `json:"blockTimestamp"`
176+
}

0 commit comments

Comments
 (0)