Skip to content

Commit

Permalink
feat(positions): multichain positions (#5093)
Browse files Browse the repository at this point in the history
### Description

gets positions and shortcuts for whichever networks are enabled in our
dynamic config, rather than just celo

relies on valora-inc/hooks#362

### Test plan

- ci
- manual test with local hooks api on wallet with both ethereum and celo
positions (also unearthed a redux migration bug with this! hooray for
testing!)

### Related issues

fixes
https://linear.app/valora/issue/RET-941/%5Bhooks%5D-multichain-support

### Backwards compatibility

there is only one place where the new API will affect application code
for old app versions, which is in the `keyExtractor` function in
`AssetList.tsx`. The function returns the following for positions:
`${activeTab}-${item.appId}-${item.networkId}-${item.address}-${index}`.
I think this is ok, since old app versions only get celo positions right
now, so when `item.networkId` evaluates to `undefined` for them, it
shouldn't cause collisions with other positions on other networks.

the other changes are all in analytics events, which should be something
we can live with

### Network scalability

If a new NetworkId and/or Network are added in the future, the changes
in this PR will:

- [x] Continue to work without code changes, OR trigger a compilation
error (guaranteeing we find it when a new network is added)
  • Loading branch information
cajubelt committed Apr 11, 2024
1 parent 71dd860 commit 7f4b5cf
Show file tree
Hide file tree
Showing 21 changed files with 376 additions and 58 deletions.
4 changes: 2 additions & 2 deletions src/analytics/Properties.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1390,7 +1390,7 @@ interface AssetsEventsProperties {
}
| {
assetType: 'position'
network: string // Example: 'celo'
network: NetworkId // Example: 'celo-mainnet'
appId: string // Example: 'ubeswap'
address: string
title: string // Example: MOO / CELO
Expand Down Expand Up @@ -1454,7 +1454,7 @@ interface DappShortcutClaimRewardEvent {
rewardId: string
appName: string
appId: string
network: string
network: NetworkId
shortcutId: string
}

Expand Down
2 changes: 1 addition & 1 deletion src/dapps/DappShortcutTransactionRequest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ function DappShortcutTransactionRequest({ route: { params } }: Props) {
rewardId,
appName: pendingAcceptShortcut?.appName ?? '',
appId: pendingAcceptShortcut?.appId ?? '',
network: pendingAcceptShortcut?.network ?? '',
network: pendingAcceptShortcut?.networkId ?? '',
shortcutId: pendingAcceptShortcut?.shortcutId ?? '',
}

Expand Down
12 changes: 6 additions & 6 deletions src/dapps/DappShortcutsRewards.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const mockUbeTokenId = `celo-alfajores:${mockUbeAddress}`

const getPositionWithClaimableBalance = (balance?: string): Position => ({
type: 'contract-position',
network: 'celo',
networkId: NetworkId['celo-mainnet'],
address: '0xda7f463c27ec862cfbf2369f3f74c364d050d93f',
appId: 'ubeswap',
appName: 'Ubeswap',
Expand All @@ -43,7 +43,7 @@ const getPositionWithClaimableBalance = (balance?: string): Position => ({
tokens: [
{
type: 'app-token',
network: 'celo',
networkId: NetworkId['celo-mainnet'],
address: '0x1e593f1fe7b61c53874b54ec0c59fd0d5eb8621e',
appId: 'ubeswap',
symbol: 'ULP',
Expand All @@ -57,7 +57,7 @@ const getPositionWithClaimableBalance = (balance?: string): Position => ({
tokens: [
{
type: 'base-token',
network: 'celo',
networkId: NetworkId['celo-mainnet'],
address: '0x471ece3750da237f93b8e339c536989b8978a438',
symbol: 'CELO',
decimals: 18,
Expand All @@ -67,7 +67,7 @@ const getPositionWithClaimableBalance = (balance?: string): Position => ({
},
{
type: 'base-token',
network: 'celo',
networkId: NetworkId['celo-mainnet'],
address: '0x765de816845861e75a25fca122bb6898b8b1282a',
symbol: 'cUSD',
decimals: 18,
Expand All @@ -86,7 +86,7 @@ const getPositionWithClaimableBalance = (balance?: string): Position => ({
type: 'base-token',
category: 'claimable',
decimals: 18,
network: 'celo',
networkId: NetworkId['celo-mainnet'],
balance: balance ?? '0.098322815093446616',
symbol: 'UBE',
address: '0x00be915b9dcf56a3cbe739d9b9c202ca692409ec',
Expand Down Expand Up @@ -182,7 +182,7 @@ describe('DappShortcutsRewards', () => {
"data": {
"address": "0x0000000000000000000000000000000000007e57",
"appId": "ubeswap",
"network": "celo",
"networkId": "celo-mainnet",
"positionAddress": "0xda7f463c27ec862cfbf2369f3f74c364d050d93f",
"shortcutId": "claim-reward",
},
Expand Down
4 changes: 2 additions & 2 deletions src/dapps/DappShortcutsRewards.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ function DappShortcutsRewards() {
shortcutId: claimableShortcut.id,
rewardId,
appId,
network: 'celo',
network: position.networkId,
rewardTokens: claimableShortcut.claimableTokens.map((token) => token.symbol).join(', '),
rewardAmounts: claimableShortcut.claimableTokens.map((token) => token.balance).join(', '),
claimableValueUsd: claimableValueUsd.toString(),
Expand All @@ -96,7 +96,7 @@ function DappShortcutsRewards() {
data: {
address,
appId,
network: 'celo',
networkId: position.networkId,
positionAddress: position.address,
shortcutId: claimableShortcut.id,
},
Expand Down
11 changes: 9 additions & 2 deletions src/positions/saga.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,14 @@ import {
triggerShortcutFailure,
triggerShortcutSuccess,
} from 'src/positions/slice'
import { getFeatureGate } from 'src/statsig'
import { getFeatureGate, getDynamicConfigParams } from 'src/statsig'
import Logger from 'src/utils/Logger'
import { getContractKit } from 'src/web3/contracts'
import networkConfig from 'src/web3/networkConfig'
import { getConnectedUnlockedAccount } from 'src/web3/saga'
import { walletAddressSelector } from 'src/web3/selectors'
import { mockAccount, mockPositions, mockShortcuts } from 'test/values'
import { NetworkId } from 'src/transactions/types'

jest.mock('src/sentry/SentryTransactionHub')
jest.mock('src/statsig')
Expand Down Expand Up @@ -89,6 +90,7 @@ describe(fetchPositionsSaga, () => {
it('fetches positions successfully', async () => {
mockFetch.mockResponse(JSON.stringify(MOCK_RESPONSE))
jest.mocked(getFeatureGate).mockReturnValue(true)
jest.mocked(getDynamicConfigParams).mockReturnValue({ showPositions: ['celo-mainnet'] })

await expectSaga(fetchPositionsSaga)
.provide([
Expand Down Expand Up @@ -141,6 +143,9 @@ describe(fetchShortcutsSaga, () => {
it('fetches shortcuts successfully', async () => {
mockFetch.mockResponse(JSON.stringify(MOCK_SHORTCUTS_RESPONSE))
jest.mocked(getFeatureGate).mockReturnValue(true)
jest.mocked(getDynamicConfigParams).mockReturnValue({
showShortcuts: ['celo-mainnet'],
})

await expectSaga(fetchShortcutsSaga)
.provide([
Expand All @@ -156,6 +161,7 @@ describe(fetchShortcutsSaga, () => {
it('fetches shortcuts if the previous fetch attempt failed', async () => {
mockFetch.mockResponse(JSON.stringify(MOCK_SHORTCUTS_RESPONSE))
jest.mocked(getFeatureGate).mockReturnValue(true)
jest.mocked(getDynamicConfigParams).mockReturnValue({ showShortcuts: ['celo-mainnet'] })

await expectSaga(fetchShortcutsSaga)
.provide([
Expand Down Expand Up @@ -205,6 +211,7 @@ describe(fetchShortcutsSaga, () => {
it('updates the shortcuts status there is an error', async () => {
mockFetch.mockResponse(JSON.stringify({ message: 'something went wrong' }), { status: 500 })
jest.mocked(getFeatureGate).mockReturnValue(true)
jest.mocked(getDynamicConfigParams).mockReturnValue({ showShortcuts: ['celo-mainnet'] })

await expectSaga(fetchShortcutsSaga)
.provide([
Expand Down Expand Up @@ -268,7 +275,7 @@ describe(triggerShortcutSaga, () => {
data: {
address: mockAccount,
appId: 'gooddollar',
network: 'celo',
networkId: NetworkId['celo-mainnet'],
positionAddress: '0x43d72Ff17701B2DA814620735C39C620Ce0ea4A1',
shortcutId: 'claim-reward',
},
Expand Down
33 changes: 20 additions & 13 deletions src/positions/saga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@ import { BuilderHooksEvents, DappShortcutsEvents } from 'src/analytics/Events'
import ValoraAnalytics from 'src/analytics/ValoraAnalytics'
import { HooksEnablePreviewOrigin } from 'src/analytics/types'
import { ErrorMessages } from 'src/app/ErrorMessages'
import { DEFAULT_TESTNET } from 'src/config'
import i18n from 'src/i18n'
import { isBottomSheetVisible, navigateBack } from 'src/navigator/NavigationService'
import { Screens } from 'src/navigator/Screens'
Expand Down Expand Up @@ -40,11 +39,11 @@ import {
import { Position, Shortcut } from 'src/positions/types'
import { SentryTransactionHub } from 'src/sentry/SentryTransactionHub'
import { SentryTransaction } from 'src/sentry/SentryTransactions'
import { getFeatureGate } from 'src/statsig'
import { StatsigFeatureGates } from 'src/statsig/types'
import { getDynamicConfigParams, getFeatureGate } from 'src/statsig'
import { StatsigDynamicConfigs, StatsigFeatureGates } from 'src/statsig/types'
import { fetchTokenBalances } from 'src/tokens/slice'
import { sendTransaction } from 'src/transactions/send'
import { newTransactionContext } from 'src/transactions/types'
import { NetworkId, newTransactionContext } from 'src/transactions/types'
import Logger from 'src/utils/Logger'
import { ensureError } from 'src/utils/ensureError'
import { fetchWithTimeout } from 'src/utils/fetchWithTimeout'
Expand All @@ -54,6 +53,7 @@ import { getConnectedUnlockedAccount } from 'src/web3/saga'
import { walletAddressSelector } from 'src/web3/selectors'
import { applyChainIdWorkaround, buildTxo } from 'src/web3/utils'
import { call, put, select, spawn, takeEvery, takeLeading } from 'typed-redux-saga'
import { DynamicConfigs } from 'src/statsig/constants'

const TAG = 'positions/saga'

Expand All @@ -71,14 +71,15 @@ function getHooksApiFunctionUrl(
async function fetchHooks(
hooksApiUrl: string,
functionName: 'getPositions' | 'v2/getShortcuts',
walletAddress: string
walletAddress: string,
networkIds: NetworkId[]
) {
const urlSearchParams = new URLSearchParams({
address: walletAddress,
})
networkIds.forEach((networkId) => urlSearchParams.append('networkIds', networkId))
const response = await fetchWithTimeout(
`${getHooksApiFunctionUrl(hooksApiUrl, functionName)}?` +
new URLSearchParams({
network: DEFAULT_TESTNET === 'mainnet' ? 'celo' : 'celoAlfajores',
address: walletAddress,
}),
`${getHooksApiFunctionUrl(hooksApiUrl, functionName)}?` + urlSearchParams,
null,
POSITIONS_FETCH_TIMEOUT
)
Expand All @@ -90,11 +91,17 @@ async function fetchHooks(
}

async function fetchPositions(hooksApiUrl: string, walletAddress: string) {
return (await fetchHooks(hooksApiUrl, 'getPositions', walletAddress)) as Position[]
const networkIds = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.MULTI_CHAIN_FEATURES]
).showPositions
return (await fetchHooks(hooksApiUrl, 'getPositions', walletAddress, networkIds)) as Position[]
}

async function fetchShortcuts(hooksApiUrl: string, walletAddress: string) {
return (await fetchHooks(hooksApiUrl, 'v2/getShortcuts', walletAddress)) as Shortcut[]
const networkIds = getDynamicConfigParams(
DynamicConfigs[StatsigDynamicConfigs.MULTI_CHAIN_FEATURES]
).showShortcuts
return (await fetchHooks(hooksApiUrl, 'v2/getShortcuts', walletAddress, networkIds)) as Shortcut[]
}

export function* fetchShortcutsSaga() {
Expand Down Expand Up @@ -254,7 +261,7 @@ export function* executeShortcutSaga({ payload }: ReturnType<typeof executeShort
const trackedShortcutProperties = {
appName: shortcut.appName,
appId: shortcut.appId,
network: shortcut.network,
network: shortcut.networkId,
shortcutId: shortcut.shortcutId,
rewardId: payload,
}
Expand Down
8 changes: 4 additions & 4 deletions src/positions/selectors.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ describe('positionsWithClaimableRewardsSelector', () => {
"balance": "0.098322815093446616",
"category": "claimable",
"decimals": 18,
"network": "celo",
"networkId": "celo-mainnet",
"priceUsd": "0.00904673476946796903",
"symbol": "UBE",
"type": "base-token",
Expand All @@ -125,7 +125,7 @@ describe('positionsWithClaimableRewardsSelector', () => {
"balance": "0.950545800159603456",
"category": "claimable",
"decimals": 18,
"network": "celo",
"networkId": "celo-mainnet",
"priceUsd": "0.6959536890241361",
"symbol": "CELO",
"type": "base-token",
Expand All @@ -134,8 +134,8 @@ describe('positionsWithClaimableRewardsSelector', () => {
"description": "Claim rewards for staked liquidity",
"id": "claim-reward",
"name": "Claim",
"networks": [
"celo",
"networkIds": [
"celo-mainnet",
],
},
]
Expand Down
7 changes: 4 additions & 3 deletions src/positions/slice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { createSlice, PayloadAction } from '@reduxjs/toolkit'
import { REHYDRATE, RehydrateAction } from 'redux-persist'
import { getRehydratePayload } from 'src/redux/persist-helper'
import { Position, Shortcut, ShortcutStatus } from './types'
import { NetworkId } from 'src/transactions/types'

type Status = 'idle' | 'loading' | 'success' | 'error'

Expand All @@ -13,7 +14,7 @@ export type TriggeredShortcuts = Record<
appName: string
appImage: string
appId: string
network: string
networkId: NetworkId
shortcutId: string
}
>
Expand Down Expand Up @@ -48,7 +49,7 @@ interface TriggerShortcut {
appName: string
appImage: string
data: {
network: string
networkId: NetworkId
address: string
appId: string
positionAddress: string
Expand Down Expand Up @@ -109,7 +110,7 @@ const slice = createSlice({
appImage: action.payload.appImage,
transactions: [],
appId: action.payload.data.appId,
network: action.payload.data.network,
networkId: action.payload.data.networkId,
shortcutId: action.payload.data.shortcutId,
}
},
Expand Down
8 changes: 5 additions & 3 deletions src/positions/types.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { NetworkId } from 'src/transactions/types'

// Decimal number serialized as a string
export type SerializedDecimalNumber = string

Expand All @@ -11,7 +13,7 @@ export type TokenCategory = 'claimable'

export interface AbstractPosition {
address: string // Example: 0x...
network: string // Example: celo
networkId: NetworkId // Example: celo-mainnet
appId: string // Example: ubeswap
appName: string
tokens: Token[]
Expand All @@ -23,7 +25,7 @@ export interface AbstractPosition {
// For now, we'll keep them separate
export interface AbstractToken {
address: string // Example: 0x...
network: string // Example: celo
networkId: NetworkId // Example: celo-mainnet
symbol: string // Example: cUSD
decimals: number // Example: 18
priceUsd: SerializedDecimalNumber // Example: "1.5"
Expand Down Expand Up @@ -56,7 +58,7 @@ export interface Shortcut {
appId: string
name: string
description: string
networks: string[]
networkIds: NetworkId[]
category?: 'claim'
}

Expand Down
37 changes: 37 additions & 0 deletions src/redux/migrations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ import {
v1Schema,
v200Schema,
v201Schema,
v203Schema,
v21Schema,
v28Schema,
v2Schema,
Expand Down Expand Up @@ -81,8 +82,12 @@ import {
import {
mockInvitableRecipient,
mockInvitableRecipient2,
mockPositions,
mockShortcuts,
mockRecipient,
mockRecipient2,
mockShortcutsLegacy,
mockPositionsLegacy,
} from 'test/values'

describe('Redux persist migrations', () => {
Expand Down Expand Up @@ -1574,4 +1579,36 @@ describe('Redux persist migrations', () => {
expectedSchema.walletConnect.pendingSessions = []
expect(migratedSchema).toStrictEqual(expectedSchema)
})

it('works from 203 to 204: recently inactive users', () => {
// users inactive since 4/2/2024 have legacy values in state
const oldSchema = {
...v203Schema,
positions: {
...v203Schema.positions,
positions: mockPositionsLegacy,
shortcuts: mockShortcutsLegacy,
},
}
const expectedSchema = _.cloneDeep(oldSchema)
expectedSchema.positions.positions = mockPositions
expectedSchema.positions.shortcuts = mockShortcuts
const migratedSchema = migrations[204](oldSchema)

expect(migratedSchema).toStrictEqual(expectedSchema)
})

it('works from 203 to 204: recently active users', () => {
// api has been returning 'networkId' and 'networkIds' fields since 4/2/2024. users active since then will have noop migration
const oldSchema = {
...v203Schema,
positions: {
positions: mockPositions,
shortcuts: mockShortcuts,
},
}
const expectedSchema = _.cloneDeep(oldSchema)
const migratedSchema = migrations[204](oldSchema)
expect(migratedSchema).toStrictEqual(expectedSchema)
})
})
Loading

0 comments on commit 7f4b5cf

Please sign in to comment.