-
Notifications
You must be signed in to change notification settings - Fork 1
EVM activity #31
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ivpusic
wants to merge
1
commit into
sim/evm-stablecoins
Choose a base branch
from
sim/evm-activity
base: sim/evm-stablecoins
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+315
−0
Open
EVM activity #31
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,194 @@ | ||
| package evm | ||
|
|
||
| import ( | ||
| "encoding/json" | ||
| "fmt" | ||
| "net/url" | ||
|
|
||
| "github.com/spf13/cobra" | ||
|
|
||
| "github.com/duneanalytics/cli/output" | ||
| ) | ||
|
|
||
| // NewActivityCmd returns the `sim evm activity` command. | ||
| func NewActivityCmd() *cobra.Command { | ||
| cmd := &cobra.Command{ | ||
| Use: "activity <address>", | ||
| Short: "Get EVM activity feed for a wallet address", | ||
| Long: "Return a chronological feed of on-chain activity for the given wallet address\n" + | ||
| "including native transfers, ERC20 movements, NFT transfers, swaps, and contract calls.\n\n" + | ||
| "Examples:\n" + | ||
| " dune sim evm activity 0xd8da6bf26964af9d7eed9e03e53415d37aa96045\n" + | ||
| " dune sim evm activity 0xd8da... --activity-type send,receive --chain-ids 1\n" + | ||
| " dune sim evm activity 0xd8da... --asset-type erc20 --limit 50 -o json", | ||
| Args: cobra.ExactArgs(1), | ||
| RunE: runActivity, | ||
| } | ||
|
|
||
| cmd.Flags().String("chain-ids", "", "Comma-separated chain IDs or tags (default: all default chains)") | ||
| cmd.Flags().String("token-address", "", "Filter by token contract address (comma-separated for multiple)") | ||
| cmd.Flags().String("activity-type", "", "Filter by type: send,receive,mint,burn,swap,approve,call") | ||
| cmd.Flags().String("asset-type", "", "Filter by asset standard: native,erc20,erc721,erc1155") | ||
| cmd.Flags().Int("limit", 0, "Max results (1-100)") | ||
| cmd.Flags().String("offset", "", "Pagination cursor from previous response") | ||
| output.AddFormatFlag(cmd, "text") | ||
|
|
||
| return cmd | ||
| } | ||
|
|
||
| type activityResponse struct { | ||
| Activity []activityItem `json:"activity"` | ||
| NextOffset string `json:"next_offset,omitempty"` | ||
| Warnings []warningEntry `json:"warnings,omitempty"` | ||
| } | ||
|
|
||
| type activityItem struct { | ||
| ChainID int64 `json:"chain_id"` | ||
| BlockNumber int64 `json:"block_number"` | ||
| BlockTime string `json:"block_time"` | ||
| TxHash string `json:"tx_hash"` | ||
| Type string `json:"type"` | ||
| AssetType string `json:"asset_type"` | ||
| TokenAddress string `json:"token_address,omitempty"` | ||
| From string `json:"from,omitempty"` | ||
| To string `json:"to,omitempty"` | ||
| Value string `json:"value,omitempty"` | ||
| ValueUSD float64 `json:"value_usd"` | ||
| ID string `json:"id,omitempty"` // ERC721/ERC1155 token ID | ||
| Spender string `json:"spender,omitempty"` | ||
| TokenMeta *tokenMetadata `json:"token_metadata,omitempty"` | ||
|
|
||
| // Swap-specific fields. | ||
| FromTokenAddress string `json:"from_token_address,omitempty"` | ||
| FromTokenValue string `json:"from_token_value,omitempty"` | ||
| FromTokenMetadata *tokenMetadata `json:"from_token_metadata,omitempty"` | ||
| ToTokenAddress string `json:"to_token_address,omitempty"` | ||
| ToTokenValue string `json:"to_token_value,omitempty"` | ||
| ToTokenMetadata *tokenMetadata `json:"to_token_metadata,omitempty"` | ||
|
|
||
| // Contract call fields. | ||
| Function *functionInfo `json:"function,omitempty"` | ||
| ContractMetadata *contractMetaObj `json:"contract_metadata,omitempty"` | ||
| } | ||
|
|
||
| type tokenMetadata struct { | ||
| Symbol string `json:"symbol"` | ||
| Decimals int `json:"decimals"` | ||
| Name string `json:"name,omitempty"` | ||
| Logo string `json:"logo,omitempty"` | ||
| PriceUSD float64 `json:"price_usd"` | ||
| PoolSize float64 `json:"pool_size,omitempty"` | ||
| } | ||
|
|
||
| type functionInfo struct { | ||
| Signature string `json:"signature,omitempty"` | ||
| Name string `json:"name,omitempty"` | ||
| } | ||
|
|
||
| type contractMetaObj struct { | ||
| Name string `json:"name,omitempty"` | ||
| } | ||
|
|
||
| func runActivity(cmd *cobra.Command, args []string) error { | ||
| client, err := requireSimClient(cmd) | ||
|
Check failure on line 93 in cmd/sim/evm/activity.go
|
||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| address := args[0] | ||
| params := url.Values{} | ||
|
|
||
| if v, _ := cmd.Flags().GetString("chain-ids"); v != "" { | ||
| params.Set("chain_ids", v) | ||
| } | ||
| if v, _ := cmd.Flags().GetString("token-address"); v != "" { | ||
| params.Set("token_address", v) | ||
| } | ||
| if v, _ := cmd.Flags().GetString("activity-type"); v != "" { | ||
| params.Set("activity_type", v) | ||
| } | ||
| if v, _ := cmd.Flags().GetString("asset-type"); v != "" { | ||
| params.Set("asset_type", v) | ||
| } | ||
| if v, _ := cmd.Flags().GetInt("limit"); v > 0 { | ||
| params.Set("limit", fmt.Sprintf("%d", v)) | ||
| } | ||
| if v, _ := cmd.Flags().GetString("offset"); v != "" { | ||
| params.Set("offset", v) | ||
| } | ||
|
|
||
| data, err := client.Get(cmd.Context(), "/v1/evm/activity/"+address, params) | ||
| if err != nil { | ||
| return err | ||
| } | ||
|
|
||
| w := cmd.OutOrStdout() | ||
| switch output.FormatFromCmd(cmd) { | ||
| case output.FormatJSON: | ||
| var raw json.RawMessage = data | ||
| return output.PrintJSON(w, raw) | ||
| default: | ||
| var resp activityResponse | ||
| if err := json.Unmarshal(data, &resp); err != nil { | ||
| return fmt.Errorf("parsing response: %w", err) | ||
| } | ||
|
|
||
| // Print warnings to stderr. | ||
| printWarnings(cmd, resp.Warnings) | ||
|
|
||
| columns := []string{"CHAIN_ID", "TYPE", "ASSET_TYPE", "SYMBOL", "VALUE_USD", "TX_HASH", "BLOCK_TIME"} | ||
| rows := make([][]string, len(resp.Activity)) | ||
| for i, a := range resp.Activity { | ||
| rows[i] = []string{ | ||
| fmt.Sprintf("%d", a.ChainID), | ||
| a.Type, | ||
| a.AssetType, | ||
| activitySymbol(a), | ||
| formatUSD(a.ValueUSD), | ||
| truncateHash(a.TxHash), | ||
| a.BlockTime, | ||
| } | ||
| } | ||
| output.PrintTable(w, columns, rows) | ||
|
|
||
| if resp.NextOffset != "" { | ||
| fmt.Fprintf(w, "\nNext offset: %s\n", resp.NextOffset) | ||
| } | ||
| return nil | ||
| } | ||
| } | ||
|
|
||
| // activitySymbol returns the best symbol to display for the activity. | ||
| // For swaps it shows "FROM -> TO", for regular activities it uses token_metadata. | ||
| func activitySymbol(a activityItem) string { | ||
| if a.Type == "swap" { | ||
| from := "" | ||
| to := "" | ||
| if a.FromTokenMetadata != nil { | ||
| from = a.FromTokenMetadata.Symbol | ||
| } | ||
| if a.ToTokenMetadata != nil { | ||
| to = a.ToTokenMetadata.Symbol | ||
| } | ||
| if from != "" || to != "" { | ||
| return from + " -> " + to | ||
| } | ||
| return "" | ||
| } | ||
| if a.TokenMeta != nil { | ||
| return a.TokenMeta.Symbol | ||
| } | ||
| // Native transfers may not have token_metadata. | ||
| if a.AssetType == "native" { | ||
| return "ETH" | ||
| } | ||
| return "" | ||
| } | ||
|
|
||
| // truncateHash shortens a hex hash for table display. | ||
| func truncateHash(hash string) string { | ||
| if len(hash) <= 14 { | ||
| return hash | ||
| } | ||
| return hash[:8] + "..." + hash[len(hash)-4:] | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,120 @@ | ||
| package evm_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "encoding/json" | ||
| "testing" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| "github.com/stretchr/testify/require" | ||
| ) | ||
|
|
||
| func TestEvmActivity_Text(t *testing.T) { | ||
| key := simAPIKey(t) | ||
|
|
||
| root := newSimTestRoot() | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "5"}) | ||
|
|
||
| require.NoError(t, root.Execute()) | ||
|
|
||
| out := buf.String() | ||
| assert.Contains(t, out, "CHAIN_ID") | ||
| assert.Contains(t, out, "TYPE") | ||
| assert.Contains(t, out, "ASSET_TYPE") | ||
| assert.Contains(t, out, "SYMBOL") | ||
| assert.Contains(t, out, "VALUE_USD") | ||
| assert.Contains(t, out, "TX_HASH") | ||
| assert.Contains(t, out, "BLOCK_TIME") | ||
| } | ||
|
|
||
| func TestEvmActivity_JSON(t *testing.T) { | ||
| key := simAPIKey(t) | ||
|
|
||
| root := newSimTestRoot() | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "5", "-o", "json"}) | ||
|
|
||
| require.NoError(t, root.Execute()) | ||
|
|
||
| var resp map[string]interface{} | ||
| require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) | ||
| assert.Contains(t, resp, "activity") | ||
| } | ||
|
|
||
| func TestEvmActivity_ActivityTypeFilter(t *testing.T) { | ||
| key := simAPIKey(t) | ||
|
|
||
| root := newSimTestRoot() | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--activity-type", "receive", "--limit", "5", "-o", "json"}) | ||
|
|
||
| require.NoError(t, root.Execute()) | ||
|
|
||
| var resp struct { | ||
| Activity []struct { | ||
| Type string `json:"type"` | ||
| } `json:"activity"` | ||
| } | ||
| require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) | ||
|
|
||
| // All returned activities should be of the filtered type. | ||
| for _, a := range resp.Activity { | ||
| assert.Equal(t, "receive", a.Type) | ||
| } | ||
| } | ||
|
|
||
| func TestEvmActivity_AssetTypeFilter(t *testing.T) { | ||
| key := simAPIKey(t) | ||
|
|
||
| root := newSimTestRoot() | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--asset-type", "erc20", "--limit", "5", "-o", "json"}) | ||
|
|
||
| require.NoError(t, root.Execute()) | ||
|
|
||
| var resp struct { | ||
| Activity []struct { | ||
| AssetType string `json:"asset_type"` | ||
| } `json:"activity"` | ||
| } | ||
| require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) | ||
|
|
||
| for _, a := range resp.Activity { | ||
| assert.Equal(t, "erc20", a.AssetType) | ||
| } | ||
| } | ||
|
|
||
| func TestEvmActivity_Pagination(t *testing.T) { | ||
| key := simAPIKey(t) | ||
|
|
||
| // Fetch page 1 with a small limit to trigger pagination. | ||
| root := newSimTestRoot() | ||
| var buf bytes.Buffer | ||
| root.SetOut(&buf) | ||
| root.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "2", "-o", "json"}) | ||
|
|
||
| require.NoError(t, root.Execute()) | ||
|
|
||
| var resp map[string]interface{} | ||
| require.NoError(t, json.Unmarshal(buf.Bytes(), &resp)) | ||
| assert.Contains(t, resp, "activity") | ||
|
|
||
| // If next_offset is present, fetch page 2. | ||
| if offset, ok := resp["next_offset"].(string); ok && offset != "" { | ||
| root2 := newSimTestRoot() | ||
| var buf2 bytes.Buffer | ||
| root2.SetOut(&buf2) | ||
| root2.SetArgs([]string{"sim", "--sim-api-key", key, "evm", "activity", evmTestAddress, "--chain-ids", "1", "--limit", "2", "--offset", offset, "-o", "json"}) | ||
|
|
||
| require.NoError(t, root2.Execute()) | ||
|
|
||
| var resp2 map[string]interface{} | ||
| require.NoError(t, json.Unmarshal(buf2.Bytes(), &resp2)) | ||
| assert.Contains(t, resp2, "activity") | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Hardcoded "ETH" symbol wrong for non-Ethereum chains
Medium Severity
The
activitySymbolfallback forAssetType == "native"always returns"ETH", but the activity command supports querying any EVM chain via--chain-ids. For chains like BSC (BNB), Polygon (MATIC/POL), or Avalanche (AVAX), native transfers withouttoken_metadatawill incorrectly display "ETH" as the symbol in the text table output.