Skip to content

Commit 558ead5

Browse files
authored
feat(positions): add super properties to track positions data (#3820)
### Description This adds new super properties to track data related to positions. ### Test plan - Updated unit tests ### Related issues - Fixes RET-713 ### Backwards compatibility Yes
1 parent f909e59 commit 558ead5

File tree

7 files changed

+244
-31
lines changed

7 files changed

+244
-31
lines changed

src/analytics/ValoraAnalytics.test.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ import { PincodeType } from 'src/account/reducer'
33
import { HomeEvents } from 'src/analytics/Events'
44
import ValoraAnalyticsModule from 'src/analytics/ValoraAnalytics'
55
import { store } from 'src/redux/store'
6-
import { getDefaultStatsigUser } from 'src/statsig'
6+
import { getDefaultStatsigUser, getFeatureGate } from 'src/statsig'
77
import { Statsig } from 'statsig-react-native'
88
import { getMockStoreData } from 'test/utils'
99
import {
1010
mockCeloAddress,
1111
mockCeurAddress,
1212
mockCusdAddress,
13+
mockPositions,
1314
mockTestTokenAddress,
1415
} from 'test/values'
1516
import { mocked } from 'ts-jest/utils'
@@ -94,6 +95,9 @@ const state = getMockStoreData({
9495
},
9596
},
9697
},
98+
positions: {
99+
positions: mockPositions,
100+
},
97101
web3: {
98102
account: mockWalletAddress,
99103
mtwAddress: null,
@@ -127,13 +131,19 @@ const defaultSuperProperties = {
127131
sHasVerifiedNumberCPV: true,
128132
sLanguage: 'es-419',
129133
sLocalCurrencyCode: 'PHP',
134+
sNetWorthUsd: 43.910872728527195,
130135
sOtherTenTokens: 'UBE:1,TT:10',
131136
sPhoneCountryCallingCode: '+1',
132137
sPhoneCountryCodeAlpha2: 'US',
133138
sPincodeType: 'CustomPin',
139+
sPositionsAppsCount: 1,
140+
sPositionsCount: 3,
141+
sPositionsTopTenApps: 'ubeswap:7.91',
134142
sPrevScreenId: undefined,
135143
sTokenCount: 4,
144+
sTopTenPositions: 'ubeswap-G$ / cUSD:4.08,ubeswap-MOO / CELO:2.51,ubeswap-CELO / cUSD:1.32',
136145
sTotalBalanceUsd: 36,
146+
sTotalPositionsBalanceUsd: 7.910872728527196,
137147
sWalletAddress: mockWalletAddress.toLowerCase(), // test for backwards compatibility (this field is lower-cased)
138148
sSuperchargingAmountInUsd: 24,
139149
sSuperchargingToken: 'cEUR',
@@ -173,6 +183,7 @@ describe('ValoraAnalytics', () => {
173183
ValoraAnalytics = require('src/analytics/ValoraAnalytics').default
174184
})
175185
mockStore.getState.mockImplementation(() => state)
186+
mocked(getFeatureGate).mockReturnValue(true)
176187
})
177188

178189
it('creates statsig client on initialization with default statsig user', async () => {

src/analytics/selectors.test.ts

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,15 @@
11
import { PincodeType } from 'src/account/reducer'
22
import { getCurrentUserTraits } from 'src/analytics/selectors'
3+
import { getFeatureGate } from 'src/statsig'
34
import { getMockStoreData } from 'test/utils'
5+
import { mocked } from 'ts-jest/utils'
6+
7+
jest.mock('src/statsig')
8+
9+
beforeEach(() => {
10+
jest.clearAllMocks()
11+
mocked(getFeatureGate).mockReturnValue(true)
12+
})
413

514
describe('getCurrentUserTraits', () => {
615
it('returns the current user traits', () => {
@@ -167,6 +176,60 @@ describe('getCurrentUserTraits', () => {
167176
},
168177
},
169178
},
179+
positions: {
180+
positions: [
181+
{
182+
type: 'contract-position',
183+
network: 'celo',
184+
address: '0xa',
185+
appId: 'a',
186+
displayProps: {
187+
title: 'Title A',
188+
},
189+
balanceUsd: '10',
190+
},
191+
{
192+
type: 'contract-position',
193+
network: 'celo',
194+
address: '0xb',
195+
appId: 'b',
196+
displayProps: {
197+
title: 'Title B',
198+
},
199+
balanceUsd: '1.11',
200+
},
201+
{
202+
type: 'contract-position',
203+
network: 'celo',
204+
address: '0xc',
205+
appId: 'c',
206+
displayProps: {
207+
title: 'Title C',
208+
},
209+
balanceUsd: '2.22',
210+
},
211+
{
212+
type: 'contract-position',
213+
network: 'celo',
214+
address: '0xd',
215+
appId: 'd',
216+
displayProps: {
217+
title: 'Title D which is really long and should be truncated',
218+
},
219+
balanceUsd: '0.01234',
220+
},
221+
{
222+
type: 'contract-position',
223+
network: 'celo',
224+
address: '0xe',
225+
appId: 'b',
226+
displayProps: {
227+
title: 'Title E',
228+
},
229+
balanceUsd: '70',
230+
},
231+
],
232+
},
170233
})
171234
expect(getCurrentUserTraits(state)).toStrictEqual({
172235
accountAddress: '0x123',
@@ -184,12 +247,19 @@ describe('getCurrentUserTraits', () => {
184247
hasVerifiedNumberCPV: true,
185248
language: 'es-419',
186249
localCurrencyCode: 'PHP',
250+
netWorthUsd: 5764.949123945,
187251
otherTenTokens: 'I:1000,K:80,0xi:11.003,G:10,H:9.12345,E:7,F:6,B:3,C:2,A:1',
188252
phoneCountryCallingCode: '+33',
189253
phoneCountryCodeAlpha2: 'FR',
190254
pincodeType: 'CustomPin',
255+
positionsAppsCount: 4,
256+
positionsCount: 5,
257+
positionsTopTenApps: 'b:71.11,a:10.00,c:2.22,d:0.01',
191258
tokenCount: 13,
259+
topTenPositions:
260+
'b-Title E:70.00,a-Title A:10.00,c-Title C:2.22,b-Title B:1.11,d-Title D which is rea:0.01',
192261
totalBalanceUsd: 5681.606783945,
262+
totalPositionsBalanceUsd: 83.34234,
193263
walletAddress: '0x0000000000000000000000000000000000007e57',
194264
superchargingToken: 'cEUR',
195265
superchargingAmountInUsd: 25.9245,

src/analytics/selectors.ts

Lines changed: 68 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,19 +10,70 @@ import { superchargeInfoSelector } from 'src/consumerIncentives/selectors'
1010
import { currentLanguageSelector } from 'src/i18n/selectors'
1111
import { getLocalCurrencyCode } from 'src/localCurrency/selectors'
1212
import { userLocationDataSelector } from 'src/networkInfo/selectors'
13+
import { getPositionBalanceUsd } from 'src/positions/getPositionBalanceUsd'
14+
import {
15+
positionsByBalanceUsdSelector,
16+
totalPositionsBalanceUsdSelector,
17+
} from 'src/positions/selectors'
1318
import { coreTokensSelector, tokensWithTokenBalanceSelector } from 'src/tokens/selectors'
1419
import { sortByUsdBalance } from 'src/tokens/utils'
1520
import { mtwAddressSelector, rawWalletAddressSelector } from 'src/web3/selectors'
1621

22+
const tokensSelector = createSelector(
23+
[tokensWithTokenBalanceSelector, coreTokensSelector],
24+
(tokens, coreTokens) => ({ tokens, coreTokens })
25+
)
26+
27+
const positionsAnalyticsSelector = createSelector(
28+
[positionsByBalanceUsdSelector, totalPositionsBalanceUsdSelector],
29+
(positionsByUsdBalance, totalPositionsBalanceUsd) => {
30+
const appsByBalanceUsd: Record<string, BigNumber> = {}
31+
for (const position of positionsByUsdBalance) {
32+
const appId = position.appId
33+
const positionBalanceUsd = getPositionBalanceUsd(position)
34+
if (appsByBalanceUsd[appId]) {
35+
appsByBalanceUsd[appId] = appsByBalanceUsd[appId].plus(positionBalanceUsd)
36+
} else {
37+
appsByBalanceUsd[appId] = positionBalanceUsd
38+
}
39+
}
40+
41+
return {
42+
totalPositionsBalanceUsd: totalPositionsBalanceUsd?.toNumber() ?? 0,
43+
positionsCount: positionsByUsdBalance.length,
44+
// Example: "ubeswap-cUSD / CELO:100.00,halofi-Hold CELO:50.00"
45+
topTenPositions: positionsByUsdBalance
46+
.slice(0, 10)
47+
.map(
48+
(position) =>
49+
// Note: title could be localized and can contain any character
50+
// But is best to get a sense of what the position is (without looking up address)
51+
// Also truncate the title to avoid reaching the 255 chars limit
52+
`${position.appId}-${position.displayProps.title.slice(0, 20)}:${getPositionBalanceUsd(
53+
position
54+
).toFixed(2)}`
55+
)
56+
.join(','),
57+
positionsAppsCount: new Set(positionsByUsdBalance.map((position) => position.appId)).size,
58+
// Example: "ubeswap:100.00,halofi:50.00"
59+
positionsTopTenApps: Object.entries(appsByBalanceUsd)
60+
.sort(([, balanceUsd1], [, balanceUsd2]) => balanceUsd2.comparedTo(balanceUsd1))
61+
.slice(0, 10)
62+
.map(([appId, balanceUsd]) => `${appId}:${balanceUsd.toFixed(2)}`)
63+
.join(','),
64+
}
65+
}
66+
)
67+
1768
export const getCurrentUserTraits = createSelector(
1869
[
1970
rawWalletAddressSelector,
2071
mtwAddressSelector,
2172
defaultCountryCodeSelector,
2273
userLocationDataSelector,
2374
currentLanguageSelector,
24-
tokensWithTokenBalanceSelector,
25-
coreTokensSelector,
75+
tokensSelector,
76+
positionsAnalyticsSelector,
2677
getLocalCurrencyCode,
2778
phoneVerificationStatusSelector,
2879
backupCompletedSelector,
@@ -35,8 +86,14 @@ export const getCurrentUserTraits = createSelector(
3586
phoneCountryCallingCode,
3687
{ countryCodeAlpha2 },
3788
language,
38-
tokens,
39-
coreTokens,
89+
{ tokens, coreTokens },
90+
{
91+
totalPositionsBalanceUsd,
92+
positionsCount,
93+
topTenPositions,
94+
positionsAppsCount,
95+
positionsTopTenApps,
96+
},
4097
localCurrencyCode,
4198
{ numberVerifiedDecentralized, numberVerifiedCentralized },
4299
hasCompletedBackup,
@@ -68,7 +125,8 @@ export const getCurrentUserTraits = createSelector(
68125
countryCodeAlpha2,
69126
language,
70127
deviceLanguage: RNLocalize.getLocales()[0]?.languageTag, // Example: "en-GB"
71-
totalBalanceUsd: totalBalanceUsd?.toNumber(),
128+
netWorthUsd: new BigNumber(totalBalanceUsd).plus(totalPositionsBalanceUsd).toNumber(), // Tokens + positions
129+
totalBalanceUsd: totalBalanceUsd?.toNumber(), // Only tokens (with a USD price), no positions
72130
tokenCount: tokensByUsdBalance.length,
73131
otherTenTokens: tokensByUsdBalance
74132
.filter((token) => !coreTokensAddresses.has(token.address))
@@ -89,6 +147,11 @@ export const getCurrentUserTraits = createSelector(
89147
token.balance.toNumber(),
90148
])
91149
),
150+
totalPositionsBalanceUsd,
151+
positionsCount,
152+
topTenPositions,
153+
positionsAppsCount,
154+
positionsTopTenApps,
92155
localCurrencyCode,
93156
hasVerifiedNumber: numberVerifiedDecentralized,
94157
hasVerifiedNumberCPV: numberVerifiedCentralized,
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
import BigNumber from 'bignumber.js'
2+
import { Position } from 'src/positions/types'
3+
4+
export function getPositionBalanceUsd(position: Position): BigNumber {
5+
let balanceUsd
6+
if (position.type === 'app-token') {
7+
const balance = new BigNumber(position.balance)
8+
balanceUsd = balance.multipliedBy(position.priceUsd)
9+
} else {
10+
balanceUsd = new BigNumber(position.balanceUsd)
11+
}
12+
13+
return balanceUsd
14+
}

src/positions/selectors.test.ts

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import BigNumber from 'bignumber.js'
2-
import { totalPositionsBalanceUsdSelector } from 'src/positions/selectors'
2+
import { getPositionBalanceUsd } from 'src/positions/getPositionBalanceUsd'
3+
import {
4+
positionsByBalanceUsdSelector,
5+
totalPositionsBalanceUsdSelector,
6+
} from 'src/positions/selectors'
37
import { getFeatureGate } from 'src/statsig'
48
import { mockPositions } from 'test/values'
59
import { mocked } from 'ts-jest/utils'
@@ -11,7 +15,7 @@ beforeEach(() => {
1115
mocked(getFeatureGate).mockReturnValue(true)
1216
})
1317

14-
describe(totalPositionsBalanceUsdSelector, () => {
18+
describe('totalPositionsBalanceUsdSelector', () => {
1519
it('returns the total balance of all positions', () => {
1620
const state: any = {
1721
positions: {
@@ -43,3 +47,45 @@ describe(totalPositionsBalanceUsdSelector, () => {
4347
expect(total).toBeNull()
4448
})
4549
})
50+
51+
describe('positionsByBalanceUsdSelector', () => {
52+
it('returns the positions sorted by USD balance in descending order', () => {
53+
const state: any = {
54+
positions: {
55+
positions: mockPositions,
56+
},
57+
}
58+
const positions = positionsByBalanceUsdSelector(state)
59+
expect(
60+
positions.map((position) => {
61+
return {
62+
appId: position.appId,
63+
address: position.address,
64+
title: position.displayProps.title,
65+
balanceUsd: getPositionBalanceUsd(position),
66+
}
67+
})
68+
).toMatchInlineSnapshot(`
69+
Array [
70+
Object {
71+
"address": "0x31f9dee850b4284b81b52b25a3194f2fc8ff18cf",
72+
"appId": "ubeswap",
73+
"balanceUsd": "4.0802397095730601528429330911456",
74+
"title": "G$ / cUSD",
75+
},
76+
Object {
77+
"address": "0x19a75250c5a3ab22a8662e55a2b90ff9d3334b00",
78+
"appId": "ubeswap",
79+
"balanceUsd": "2.5098739934779290118396726809999",
80+
"title": "MOO / CELO",
81+
},
82+
Object {
83+
"address": "0xda7f463c27ec862cfbf2369f3f74c364d050d93f",
84+
"appId": "ubeswap",
85+
"balanceUsd": "1.3207590254762067",
86+
"title": "CELO / cUSD",
87+
},
88+
]
89+
`)
90+
})
91+
})

0 commit comments

Comments
 (0)