From 17a78ed4e14b5c4a33f9fd74875cabd62eaa76e1 Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Mon, 23 Mar 2026 15:42:24 +0100 Subject: [PATCH 1/2] Adopt defi-positions sim command for a new payload --- cmd/sim/evm/defi_positions.go | 115 +++++++------- cmd/sim/evm/defi_positions_unit_test.go | 197 ++++++++++++++++++++---- 2 files changed, 230 insertions(+), 82 deletions(-) diff --git a/cmd/sim/evm/defi_positions.go b/cmd/sim/evm/defi_positions.go index eb8c2f3..56044bb 100644 --- a/cmd/sim/evm/defi_positions.go +++ b/cmd/sim/evm/defi_positions.go @@ -46,64 +46,67 @@ type defiPositionsResponse struct { } type defiAggregations struct { - TotalUSDValue float64 `json:"total_usd_value"` + TotalUSDValue float64 `json:"total_value_usd"` TotalByChain map[string]float64 `json:"total_by_chain,omitempty"` } -// defiPosition is a flat struct matching the polymorphic DefiPosition schema. +// defiTokenInfo represents a token object returned by the API with address, +// name, symbol, and optional numeric fields depending on position type. +type defiTokenInfo struct { + Address string `json:"address,omitempty"` + Name string `json:"name,omitempty"` + Symbol string `json:"symbol,omitempty"` + Decimals int `json:"decimals,omitempty"` + Holdings float64 `json:"holdings,omitempty"` + PriceUSD float64 `json:"price_usd,omitempty"` +} + +// nftTokenDetails holds per-token data inside an NFT concentrated-liquidity position. +type nftTokenDetails struct { + PriceUSD float64 `json:"price_usd"` + Holdings float64 `json:"holdings,omitempty"` + Rewards float64 `json:"rewards,omitempty"` +} + +// defiPosition matches the polymorphic DefiPosition schema returned by the API. // Fields are optional depending on the `type` discriminator. type defiPosition struct { Type string `json:"type"` + Chain string `json:"chain,omitempty"` ChainID int64 `json:"chain_id"` - USDVal float64 `json:"usd_value"` + USDVal float64 `json:"value_usd"` Logo *string `json:"logo,omitempty"` // Erc4626 / Tokenized fields - TokenType string `json:"token_type,omitempty"` - Token string `json:"token,omitempty"` - TokenName string `json:"token_name,omitempty"` - TokenSymbol string `json:"token_symbol,omitempty"` - UnderlyingToken string `json:"underlying_token,omitempty"` - UnderlyingTokenName string `json:"underlying_token_name,omitempty"` - UnderlyingTokenSymbol string `json:"underlying_token_symbol,omitempty"` - UnderlyingTokenDecimals int `json:"underlying_token_decimals,omitempty"` + TokenType string `json:"token_type,omitempty"` + Token *defiTokenInfo `json:"token,omitempty"` + UnderlyingToken *defiTokenInfo `json:"underlying_token,omitempty"` + LendingPool string `json:"lending_pool,omitempty"` // Erc4626 / Tokenized / UniswapV2 fields - CalculatedBalance float64 `json:"calculated_balance,omitempty"` - PriceInUSD float64 `json:"price_in_usd,omitempty"` + Balance float64 `json:"balance,omitempty"` + PriceUSD float64 `json:"price_usd,omitempty"` // UniswapV2 / Nft / NftV4 fields - Protocol string `json:"protocol,omitempty"` - Pool string `json:"pool,omitempty"` - PoolID []int `json:"pool_id,omitempty"` - PoolManager string `json:"pool_manager,omitempty"` - Salt []int `json:"salt,omitempty"` - Token0 string `json:"token0,omitempty"` - Token0Name string `json:"token0_name,omitempty"` - Token0Symbol string `json:"token0_symbol,omitempty"` - Token0Decimals int `json:"token0_decimals,omitempty"` - Token1 string `json:"token1,omitempty"` - Token1Name string `json:"token1_name,omitempty"` - Token1Symbol string `json:"token1_symbol,omitempty"` - Token1Decimals int `json:"token1_decimals,omitempty"` - LPBalance string `json:"lp_balance,omitempty"` - Token0Price float64 `json:"token0_price,omitempty"` - Token1Price float64 `json:"token1_price,omitempty"` + Protocol string `json:"protocol,omitempty"` + Pool string `json:"pool,omitempty"` + PoolID string `json:"pool_id,omitempty"` + PoolManager string `json:"pool_manager,omitempty"` + Salt string `json:"salt,omitempty"` + Token0 *defiTokenInfo `json:"token0,omitempty"` + Token1 *defiTokenInfo `json:"token1,omitempty"` + LPBalance string `json:"lp_balance,omitempty"` // Nft / NftV4 concentrated liquidity positions Positions []nftPositionDetails `json:"positions,omitempty"` } type nftPositionDetails struct { - TickLower int `json:"tick_lower"` - TickUpper int `json:"tick_upper"` - TokenID string `json:"token_id"` - Token0Price float64 `json:"token0_price"` - Token0Holdings float64 `json:"token0_holdings,omitempty"` - Token0Rewards float64 `json:"token0_rewards,omitempty"` - Token1Price float64 `json:"token1_price"` - Token1Holdings float64 `json:"token1_holdings,omitempty"` - Token1Rewards float64 `json:"token1_rewards,omitempty"` + TickLower int `json:"tick_lower"` + TickUpper int `json:"tick_upper"` + TokenID string `json:"token_id"` + Token0 *nftTokenDetails `json:"token0,omitempty"` + Token1 *nftTokenDetails `json:"token1,omitempty"` } func runDefiPositions(cmd *cobra.Command, args []string) error { @@ -165,18 +168,26 @@ func runDefiPositions(cmd *cobra.Command, args []string) error { // positionDetails returns a human-readable summary for a DeFi position, // varying by position type. +// tokenSymbol safely extracts the symbol from a token info pointer. +func tokenSymbol(t *defiTokenInfo) string { + if t == nil { + return "" + } + return t.Symbol +} + func positionDetails(p defiPosition) string { switch p.Type { case "Erc4626": parts := []string{} - if p.TokenSymbol != "" { - parts = append(parts, p.TokenSymbol) + if sym := tokenSymbol(p.Token); sym != "" { + parts = append(parts, sym) } - if p.UnderlyingTokenSymbol != "" { - parts = append(parts, fmt.Sprintf("-> %s", p.UnderlyingTokenSymbol)) + if sym := tokenSymbol(p.UnderlyingToken); sym != "" { + parts = append(parts, fmt.Sprintf("-> %s", sym)) } - if p.CalculatedBalance != 0 { - parts = append(parts, fmt.Sprintf("bal=%.6g", p.CalculatedBalance)) + if p.Balance != 0 { + parts = append(parts, fmt.Sprintf("bal=%.6g", p.Balance)) } return strings.Join(parts, " ") @@ -185,23 +196,23 @@ func positionDetails(p defiPosition) string { if p.TokenType != "" { parts = append(parts, p.TokenType) } - if p.TokenSymbol != "" { - parts = append(parts, p.TokenSymbol) + if sym := tokenSymbol(p.Token); sym != "" { + parts = append(parts, sym) } - if p.CalculatedBalance != 0 { - parts = append(parts, fmt.Sprintf("bal=%.6g", p.CalculatedBalance)) + if p.Balance != 0 { + parts = append(parts, fmt.Sprintf("bal=%.6g", p.Balance)) } return strings.Join(parts, " ") case "UniswapV2": - pair := formatPair(p.Token0Symbol, p.Token1Symbol) - if p.CalculatedBalance != 0 { - return fmt.Sprintf("%s bal=%.6g", pair, p.CalculatedBalance) + pair := formatPair(tokenSymbol(p.Token0), tokenSymbol(p.Token1)) + if p.Balance != 0 { + return fmt.Sprintf("%s bal=%.6g", pair, p.Balance) } return pair case "Nft", "NftV4": - pair := formatPair(p.Token0Symbol, p.Token1Symbol) + pair := formatPair(tokenSymbol(p.Token0), tokenSymbol(p.Token1)) nPos := len(p.Positions) if nPos == 1 { return fmt.Sprintf("%s (1 position)", pair) diff --git a/cmd/sim/evm/defi_positions_unit_test.go b/cmd/sim/evm/defi_positions_unit_test.go index f596a58..62fbf7e 100644 --- a/cmd/sim/evm/defi_positions_unit_test.go +++ b/cmd/sim/evm/defi_positions_unit_test.go @@ -1,17 +1,146 @@ package evm import ( + "encoding/json" "testing" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) +// apiResponseJSON is the exact JSON returned by the defi-positions API endpoint, +// used to verify that our structs can unmarshal every field correctly. +const apiResponseJSON = `{"positions":[{"type":"Erc4626","chain":"ethereum","chain_id":1,"token":{"address":"0xa3931d71877c0e7a3148cb7eb4463524fec27fbd","name":"Savings USDS","symbol":"sUSDS"},"underlying_token":{"address":"0xdc035d45d973e3ec169d2276ddab16f1e407384f","name":"USDS Stablecoin","symbol":"USDS","decimals":18,"holdings":47.00372505463423},"balance":43.091714709517426,"price_usd":1.0906557333153313,"value_usd":46.99822570632377,"logo":"https://api.sim.dune.com/beta/token/logo/1/0xdc035d45d973e3ec169d2276ddab16f1e407384f"},{"type":"Tokenized","chain":"ethereum","chain_id":1,"token_type":"AtokenV2","token":{"address":"0x030ba81f1c18d280636f32af80b9aad02cf0854e","name":"Aave interest bearing WETH","symbol":"aWETH"},"underlying_token":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","holdings":0.0515820177322061},"lending_pool":"0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9","balance":0.04961716041594229,"price_usd":2257.7211358982086,"value_usd":112.02171177432484,"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"},{"type":"UniswapV2","chain":"ethereum","chain_id":1,"protocol":"ShibaSwapV2","pool":"0x76ec974feaf0293f64cf8643e0f42dea5b71689b","token0":{"address":"0x198065e69a86cb8a9154b333aad8efe7a3c256f8","name":"KOYO","symbol":"KOY","decimals":18,"price_usd":0.00009108374058938265,"holdings":72267.17195098454},"token1":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18,"price_usd":2171.720237,"holdings":0.00297532741832308},"lp_balance":"0x8ac7230489e80000","balance":10.0,"price_usd":1.3043943109184983,"value_usd":13.043943109184983,"logo":"https://api.sim.dune.com/beta/token/logo/1/0x198065e69a86cb8a9154b333aad8efe7a3c256f8"},{"type":"UniswapV2","chain":"ethereum","chain_id":1,"protocol":"UniswapV2","pool":"0x09c29277d081a1b347f41277ff53116a30d4ddff","token0":{"address":"0x4206975c6d7135ad73129476ebe2b06e42f41f50","name":"FWOG","symbol":"FWOG","decimals":18,"price_usd":2.3754275952306002e-11,"holdings":609179876998.8339},"token1":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18,"price_usd":2171.720237,"holdings":0.006683389542586471},"lp_balance":"0xca7455529bd53680000","balance":59754.0,"price_usd":0.00048507345490195365,"value_usd":28.98507922421134,"logo":null},{"type":"Nft","chain":"ethereum","chain_id":1,"protocol":"UniswapV3","pool":"0x7625d7f67e4e44341ddfb1e698801fd5a1574b48","token0":{"address":"0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","name":"Wrapped Ether","symbol":"WETH","decimals":18},"token1":{"address":"0xd78959df1ff28b45e6b4ea234bdcf9d6609d16e1","name":"Moneda de Caca","symbol":"Mierda","decimals":18},"positions":[{"tick_lower":0,"tick_upper":184200,"token_id":"0xe8ac0","token0":{"price_usd":2171.720237,"holdings":0.0,"rewards":0.000201014351146556},"token1":{"price_usd":0.0,"holdings":100000000.0,"rewards":19678.323702556045}}],"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","value_usd":0.43654693431239977},{"type":"NftV4","chain":"ethereum","chain_id":1,"protocol":"UniswapV4","pool_id":"0x21fb293b9dc53b42fa6e63fa24e1212de76c88eb7a15b94cd220fc66274851bf","pool_manager":"0x000000000004444c5dc75cb358380d2e3de08a90","salt":"0x0000000000000000000000000000000000000000000000000000000000002118","token0":{"address":"0x0000000000000000000000000000000000000000","name":"Ether","symbol":"ETH","decimals":18},"token1":{"address":"0xf9c8631fba291bac14ed549a2dde7c7f2ddff1a8","name":"Mighty Morphin Power Rangers","symbol":"GoGo","decimals":18},"positions":[{"tick_lower":-184220,"tick_upper":207220,"token_id":"0x2118","token0":{"price_usd":2171.720237,"holdings":0.000252141460675072,"rewards":8.852246853764e-6},"token1":{"price_usd":0.0,"holdings":479748570.0393271,"rewards":8399.799985973925}}],"logo":"https://api.sim.dune.com/beta/token/logo/1/0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2","value_usd":0.5668053163700324}],"aggregations":{"total_value_usd":6153.428535761592,"total_by_chain":{"1":4206.858757536072,"8453":1946.5539365060883,"42161":0.015841719431772122}}}` + +func TestUnmarshal_FullAPIResponse(t *testing.T) { + var resp defiPositionsResponse + err := json.Unmarshal([]byte(apiResponseJSON), &resp) + require.NoError(t, err, "unmarshal must not fail on real API response") + + require.Len(t, resp.Positions, 6) + + // --- Erc4626 --- + erc := resp.Positions[0] + assert.Equal(t, "Erc4626", erc.Type) + assert.Equal(t, "ethereum", erc.Chain) + assert.Equal(t, int64(1), erc.ChainID) + assert.InDelta(t, 46.998, erc.USDVal, 0.01) + assert.InDelta(t, 43.0917, erc.Balance, 0.001) + assert.InDelta(t, 1.0906, erc.PriceUSD, 0.001) + require.NotNil(t, erc.Logo) + assert.Contains(t, *erc.Logo, "api.sim.dune.com") + // token + require.NotNil(t, erc.Token) + assert.Equal(t, "0xa3931d71877c0e7a3148cb7eb4463524fec27fbd", erc.Token.Address) + assert.Equal(t, "Savings USDS", erc.Token.Name) + assert.Equal(t, "sUSDS", erc.Token.Symbol) + // underlying_token + require.NotNil(t, erc.UnderlyingToken) + assert.Equal(t, "0xdc035d45d973e3ec169d2276ddab16f1e407384f", erc.UnderlyingToken.Address) + assert.Equal(t, "USDS Stablecoin", erc.UnderlyingToken.Name) + assert.Equal(t, "USDS", erc.UnderlyingToken.Symbol) + assert.Equal(t, 18, erc.UnderlyingToken.Decimals) + assert.InDelta(t, 47.003, erc.UnderlyingToken.Holdings, 0.001) + + // --- Tokenized --- + tok := resp.Positions[1] + assert.Equal(t, "Tokenized", tok.Type) + assert.Equal(t, "AtokenV2", tok.TokenType) + assert.Equal(t, "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9", tok.LendingPool) + assert.InDelta(t, 0.04961, tok.Balance, 0.0001) + assert.InDelta(t, 2257.72, tok.PriceUSD, 0.01) + assert.InDelta(t, 112.02, tok.USDVal, 0.01) + require.NotNil(t, tok.Token) + assert.Equal(t, "aWETH", tok.Token.Symbol) + assert.Equal(t, "Aave interest bearing WETH", tok.Token.Name) + require.NotNil(t, tok.UnderlyingToken) + assert.Equal(t, "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2", tok.UnderlyingToken.Address) + assert.InDelta(t, 0.05158, tok.UnderlyingToken.Holdings, 0.0001) + + // --- UniswapV2 (with logo) --- + uni := resp.Positions[2] + assert.Equal(t, "UniswapV2", uni.Type) + assert.Equal(t, "ShibaSwapV2", uni.Protocol) + assert.Equal(t, "0x76ec974feaf0293f64cf8643e0f42dea5b71689b", uni.Pool) + assert.Equal(t, "0x8ac7230489e80000", uni.LPBalance) + assert.InDelta(t, 10.0, uni.Balance, 0.001) + require.NotNil(t, uni.Token0) + assert.Equal(t, "KOY", uni.Token0.Symbol) + assert.Equal(t, 18, uni.Token0.Decimals) + assert.InDelta(t, 0.0000911, uni.Token0.PriceUSD, 0.00001) + assert.InDelta(t, 72267.17, uni.Token0.Holdings, 0.01) + require.NotNil(t, uni.Token1) + assert.Equal(t, "WETH", uni.Token1.Symbol) + assert.InDelta(t, 2171.72, uni.Token1.PriceUSD, 0.01) + + // --- UniswapV2 (logo: null) --- + uniNull := resp.Positions[3] + assert.Equal(t, "UniswapV2", uniNull.Type) + assert.Nil(t, uniNull.Logo, "null logo should be nil pointer") + assert.InDelta(t, 59754.0, uniNull.Balance, 0.1) + + // --- Nft (UniswapV3) --- + nft := resp.Positions[4] + assert.Equal(t, "Nft", nft.Type) + assert.Equal(t, "UniswapV3", nft.Protocol) + assert.Equal(t, "0x7625d7f67e4e44341ddfb1e698801fd5a1574b48", nft.Pool) + require.NotNil(t, nft.Token0) + assert.Equal(t, "WETH", nft.Token0.Symbol) + require.NotNil(t, nft.Token1) + assert.Equal(t, "Mierda", nft.Token1.Symbol) + assert.InDelta(t, 0.4365, nft.USDVal, 0.001) + // NFT position details + require.Len(t, nft.Positions, 1) + nftPos := nft.Positions[0] + assert.Equal(t, 0, nftPos.TickLower) + assert.Equal(t, 184200, nftPos.TickUpper) + assert.Equal(t, "0xe8ac0", nftPos.TokenID) + require.NotNil(t, nftPos.Token0) + assert.InDelta(t, 2171.72, nftPos.Token0.PriceUSD, 0.01) + assert.InDelta(t, 0.0, nftPos.Token0.Holdings, 0.001) + assert.InDelta(t, 0.000201, nftPos.Token0.Rewards, 0.00001) + require.NotNil(t, nftPos.Token1) + assert.InDelta(t, 0.0, nftPos.Token1.PriceUSD, 0.001) + assert.InDelta(t, 100000000.0, nftPos.Token1.Holdings, 1.0) + assert.InDelta(t, 19678.32, nftPos.Token1.Rewards, 0.01) + + // --- NftV4 (UniswapV4) --- + nft4 := resp.Positions[5] + assert.Equal(t, "NftV4", nft4.Type) + assert.Equal(t, "UniswapV4", nft4.Protocol) + assert.Equal(t, "0x21fb293b9dc53b42fa6e63fa24e1212de76c88eb7a15b94cd220fc66274851bf", nft4.PoolID) + assert.Equal(t, "0x000000000004444c5dc75cb358380d2e3de08a90", nft4.PoolManager) + assert.Equal(t, "0x0000000000000000000000000000000000000000000000000000000000002118", nft4.Salt) + require.NotNil(t, nft4.Token0) + assert.Equal(t, "ETH", nft4.Token0.Symbol) + assert.Equal(t, "0x0000000000000000000000000000000000000000", nft4.Token0.Address) + require.NotNil(t, nft4.Token1) + assert.Equal(t, "GoGo", nft4.Token1.Symbol) + assert.InDelta(t, 0.5668, nft4.USDVal, 0.001) + require.Len(t, nft4.Positions, 1) + nft4Pos := nft4.Positions[0] + assert.Equal(t, -184220, nft4Pos.TickLower) + assert.Equal(t, 207220, nft4Pos.TickUpper) + assert.Equal(t, "0x2118", nft4Pos.TokenID) + require.NotNil(t, nft4Pos.Token0) + assert.InDelta(t, 0.000252, nft4Pos.Token0.Holdings, 0.00001) + assert.InDelta(t, 8.852e-6, nft4Pos.Token0.Rewards, 1e-7) + + // --- Aggregations --- + require.NotNil(t, resp.Aggregations) + assert.InDelta(t, 6153.43, resp.Aggregations.TotalUSDValue, 0.01) + assert.Len(t, resp.Aggregations.TotalByChain, 3) + assert.InDelta(t, 4206.86, resp.Aggregations.TotalByChain["1"], 0.01) + assert.InDelta(t, 1946.55, resp.Aggregations.TotalByChain["8453"], 0.01) + assert.InDelta(t, 0.01584, resp.Aggregations.TotalByChain["42161"], 0.0001) +} + func TestPositionDetails_Erc4626(t *testing.T) { p := defiPosition{ - Type: "Erc4626", - TokenSymbol: "alUSD", - UnderlyingTokenSymbol: "USDC", - CalculatedBalance: 0.0673736869415349, + Type: "Erc4626", + Token: &defiTokenInfo{Symbol: "alUSD"}, + UnderlyingToken: &defiTokenInfo{Symbol: "USDC"}, + Balance: 0.0673736869415349, } got := positionDetails(p) assert.Contains(t, got, "alUSD") @@ -21,19 +150,19 @@ func TestPositionDetails_Erc4626(t *testing.T) { func TestPositionDetails_Erc4626_NoBalance(t *testing.T) { p := defiPosition{ - Type: "Erc4626", - TokenSymbol: "yvDAI", - UnderlyingTokenSymbol: "DAI", + Type: "Erc4626", + Token: &defiTokenInfo{Symbol: "yvDAI"}, + UnderlyingToken: &defiTokenInfo{Symbol: "DAI"}, } assert.Equal(t, "yvDAI -> DAI", positionDetails(p)) } func TestPositionDetails_Tokenized(t *testing.T) { p := defiPosition{ - Type: "Tokenized", - TokenType: "AtokenV2", - TokenSymbol: "aWETH", - CalculatedBalance: 0.0496171604159423, + Type: "Tokenized", + TokenType: "AtokenV2", + Token: &defiTokenInfo{Symbol: "aWETH"}, + Balance: 0.0496171604159423, } got := positionDetails(p) assert.Contains(t, got, "AtokenV2") @@ -43,19 +172,19 @@ func TestPositionDetails_Tokenized(t *testing.T) { func TestPositionDetails_Tokenized_NoBalance(t *testing.T) { p := defiPosition{ - Type: "Tokenized", - TokenType: "AtokenV2", - TokenSymbol: "aWBTC", + Type: "Tokenized", + TokenType: "AtokenV2", + Token: &defiTokenInfo{Symbol: "aWBTC"}, } assert.Equal(t, "AtokenV2 aWBTC", positionDetails(p)) } func TestPositionDetails_UniswapV2(t *testing.T) { p := defiPosition{ - Type: "UniswapV2", - Token0Symbol: "FWOG", - Token1Symbol: "WETH", - CalculatedBalance: 59754, + Type: "UniswapV2", + Token0: &defiTokenInfo{Symbol: "FWOG"}, + Token1: &defiTokenInfo{Symbol: "WETH"}, + Balance: 59754, } got := positionDetails(p) assert.Contains(t, got, "FWOG/WETH") @@ -64,18 +193,18 @@ func TestPositionDetails_UniswapV2(t *testing.T) { func TestPositionDetails_UniswapV2_NoBalance(t *testing.T) { p := defiPosition{ - Type: "UniswapV2", - Token0Symbol: "USDC", - Token1Symbol: "WETH", + Type: "UniswapV2", + Token0: &defiTokenInfo{Symbol: "USDC"}, + Token1: &defiTokenInfo{Symbol: "WETH"}, } assert.Equal(t, "USDC/WETH", positionDetails(p)) } func TestPositionDetails_Nft(t *testing.T) { p := defiPosition{ - Type: "Nft", - Token0Symbol: "WETH", - Token1Symbol: "USDC", + Type: "Nft", + Token0: &defiTokenInfo{Symbol: "WETH"}, + Token1: &defiTokenInfo{Symbol: "USDC"}, Positions: []nftPositionDetails{ {TickLower: -100, TickUpper: 100, TokenID: "0x1"}, {TickLower: -200, TickUpper: 200, TokenID: "0x2"}, @@ -87,9 +216,9 @@ func TestPositionDetails_Nft(t *testing.T) { func TestPositionDetails_NftV4(t *testing.T) { p := defiPosition{ - Type: "NftV4", - Token0Symbol: "WBTC", - Token1Symbol: "WETH", + Type: "NftV4", + Token0: &defiTokenInfo{Symbol: "WBTC"}, + Token1: &defiTokenInfo{Symbol: "WETH"}, Positions: []nftPositionDetails{ {TickLower: -50, TickUpper: 50, TokenID: "0xabc"}, }, @@ -99,9 +228,9 @@ func TestPositionDetails_NftV4(t *testing.T) { func TestPositionDetails_NftNoPositions(t *testing.T) { p := defiPosition{ - Type: "Nft", - Token0Symbol: "DAI", - Token1Symbol: "USDC", + Type: "Nft", + Token0: &defiTokenInfo{Symbol: "DAI"}, + Token1: &defiTokenInfo{Symbol: "USDC"}, } assert.Equal(t, "DAI/USDC", positionDetails(p)) } @@ -117,3 +246,11 @@ func TestFormatPair(t *testing.T) { assert.Equal(t, "USDC", formatPair("", "USDC")) assert.Equal(t, "", formatPair("", "")) } + +func TestTokenSymbol_Nil(t *testing.T) { + assert.Equal(t, "", tokenSymbol(nil)) +} + +func TestTokenSymbol_NonNil(t *testing.T) { + assert.Equal(t, "ETH", tokenSymbol(&defiTokenInfo{Symbol: "ETH"})) +} From 7157b58c3d5f9649a12229f51e3edda7e7d509a5 Mon Sep 17 00:00:00 2001 From: Ivan Pusic <450140+ivpusic@users.noreply.github.com> Date: Mon, 23 Mar 2026 16:09:22 +0100 Subject: [PATCH 2/2] rename fields too --- cmd/sim/evm/defi_positions.go | 16 ++++++++-------- cmd/sim/evm/defi_positions_unit_test.go | 10 +++++----- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cmd/sim/evm/defi_positions.go b/cmd/sim/evm/defi_positions.go index 56044bb..c0b05d2 100644 --- a/cmd/sim/evm/defi_positions.go +++ b/cmd/sim/evm/defi_positions.go @@ -46,7 +46,7 @@ type defiPositionsResponse struct { } type defiAggregations struct { - TotalUSDValue float64 `json:"total_value_usd"` + TotalValueUSD float64 `json:"total_value_usd"` TotalByChain map[string]float64 `json:"total_by_chain,omitempty"` } @@ -71,11 +71,11 @@ type nftTokenDetails struct { // defiPosition matches the polymorphic DefiPosition schema returned by the API. // Fields are optional depending on the `type` discriminator. type defiPosition struct { - Type string `json:"type"` - Chain string `json:"chain,omitempty"` - ChainID int64 `json:"chain_id"` - USDVal float64 `json:"value_usd"` - Logo *string `json:"logo,omitempty"` + Type string `json:"type"` + Chain string `json:"chain,omitempty"` + ChainID int64 `json:"chain_id"` + ValueUSD float64 `json:"value_usd"` + Logo *string `json:"logo,omitempty"` // Erc4626 / Tokenized fields TokenType string `json:"token_type,omitempty"` @@ -153,7 +153,7 @@ func runDefiPositions(cmd *cobra.Command, args []string) error { p.Type, fmt.Sprintf("%d", p.ChainID), p.Protocol, - output.FormatUSD(p.USDVal), + output.FormatUSD(p.ValueUSD), positionDetails(p), } } @@ -244,7 +244,7 @@ func printAggregations(w io.Writer, agg *defiAggregations) { return } - fmt.Fprintf(w, "\nTotal USD Value: %s\n", output.FormatUSD(agg.TotalUSDValue)) + fmt.Fprintf(w, "\nTotal USD Value: %s\n", output.FormatUSD(agg.TotalValueUSD)) if len(agg.TotalByChain) > 0 { fmt.Fprintln(w, "Breakdown by chain:") diff --git a/cmd/sim/evm/defi_positions_unit_test.go b/cmd/sim/evm/defi_positions_unit_test.go index 62fbf7e..55f3f1d 100644 --- a/cmd/sim/evm/defi_positions_unit_test.go +++ b/cmd/sim/evm/defi_positions_unit_test.go @@ -24,7 +24,7 @@ func TestUnmarshal_FullAPIResponse(t *testing.T) { assert.Equal(t, "Erc4626", erc.Type) assert.Equal(t, "ethereum", erc.Chain) assert.Equal(t, int64(1), erc.ChainID) - assert.InDelta(t, 46.998, erc.USDVal, 0.01) + assert.InDelta(t, 46.998, erc.ValueUSD, 0.01) assert.InDelta(t, 43.0917, erc.Balance, 0.001) assert.InDelta(t, 1.0906, erc.PriceUSD, 0.001) require.NotNil(t, erc.Logo) @@ -49,7 +49,7 @@ func TestUnmarshal_FullAPIResponse(t *testing.T) { assert.Equal(t, "0x7d2768de32b0b80b7a3454c06bdac94a69ddc7a9", tok.LendingPool) assert.InDelta(t, 0.04961, tok.Balance, 0.0001) assert.InDelta(t, 2257.72, tok.PriceUSD, 0.01) - assert.InDelta(t, 112.02, tok.USDVal, 0.01) + assert.InDelta(t, 112.02, tok.ValueUSD, 0.01) require.NotNil(t, tok.Token) assert.Equal(t, "aWETH", tok.Token.Symbol) assert.Equal(t, "Aave interest bearing WETH", tok.Token.Name) @@ -88,7 +88,7 @@ func TestUnmarshal_FullAPIResponse(t *testing.T) { assert.Equal(t, "WETH", nft.Token0.Symbol) require.NotNil(t, nft.Token1) assert.Equal(t, "Mierda", nft.Token1.Symbol) - assert.InDelta(t, 0.4365, nft.USDVal, 0.001) + assert.InDelta(t, 0.4365, nft.ValueUSD, 0.001) // NFT position details require.Len(t, nft.Positions, 1) nftPos := nft.Positions[0] @@ -116,7 +116,7 @@ func TestUnmarshal_FullAPIResponse(t *testing.T) { assert.Equal(t, "0x0000000000000000000000000000000000000000", nft4.Token0.Address) require.NotNil(t, nft4.Token1) assert.Equal(t, "GoGo", nft4.Token1.Symbol) - assert.InDelta(t, 0.5668, nft4.USDVal, 0.001) + assert.InDelta(t, 0.5668, nft4.ValueUSD, 0.001) require.Len(t, nft4.Positions, 1) nft4Pos := nft4.Positions[0] assert.Equal(t, -184220, nft4Pos.TickLower) @@ -128,7 +128,7 @@ func TestUnmarshal_FullAPIResponse(t *testing.T) { // --- Aggregations --- require.NotNil(t, resp.Aggregations) - assert.InDelta(t, 6153.43, resp.Aggregations.TotalUSDValue, 0.01) + assert.InDelta(t, 6153.43, resp.Aggregations.TotalValueUSD, 0.01) assert.Len(t, resp.Aggregations.TotalByChain, 3) assert.InDelta(t, 4206.86, resp.Aggregations.TotalByChain["1"], 0.01) assert.InDelta(t, 1946.55, resp.Aggregations.TotalByChain["8453"], 0.01)