From 97452d7785755575fe8b4c5663c3b9f5fd474877 Mon Sep 17 00:00:00 2001 From: aleka Date: Mon, 5 Feb 2024 12:24:12 -0500 Subject: [PATCH] Add historical trading rewards gated by feature flag (#260) * add historical trading rewards gated by feature flag --- package.json | 2 +- pnpm-lock.yaml | 179 ++++++++--------- src/components/Output.tsx | 11 +- src/components/Table.tsx | 12 +- src/constants/abacus.ts | 19 ++ src/constants/localization.ts | 2 +- src/constants/tooltips/trade.ts | 3 + src/lib/abacus/index.ts | 17 +- src/lib/abacus/stateNotification.ts | 7 + src/lib/testFlags.ts | 4 + src/pages/Profile.tsx | 180 +++++++++++------- src/pages/rewards/DYDXBalancePanel.tsx | 3 +- src/pages/rewards/RewardHistoryPanel.tsx | 105 ++++++++++ src/pages/rewards/RewardsHelpPanel.tsx | 28 ++- src/pages/rewards/RewardsPage.tsx | 148 ++++++++++---- .../rewards/TradingRewardsSummaryPanel.tsx | 134 +++++++++++++ src/state/account.ts | 6 + src/state/accountSelectors.ts | 35 ++++ .../tables/TradingRewardHistoryTable.tsx | 164 ++++++++++++++++ 19 files changed, 835 insertions(+), 224 deletions(-) create mode 100644 src/pages/rewards/RewardHistoryPanel.tsx create mode 100644 src/pages/rewards/TradingRewardsSummaryPanel.tsx create mode 100644 src/views/tables/TradingRewardHistoryTable.tsx diff --git a/package.json b/package.json index dee3e98..b03fda7 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@cosmjs/proto-signing": "^0.32.1", "@cosmjs/stargate": "^0.32.1", "@cosmjs/tendermint-rpc": "^0.32.1", - "@dydxprotocol/v4-abacus": "^1.3.2", + "@dydxprotocol/v4-abacus": "^1.4.0", "@dydxprotocol/v4-client-js": "^1.0.20", "@dydxprotocol/v4-localization": "^1.1.22", "@ethersproject/providers": "^5.7.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 40c1b4f..28f3fa6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,9 +1,5 @@ lockfileVersion: '6.0' -settings: - autoInstallPeers: true - excludeLinksFromLockfile: false - overrides: follow-redirects: 1.15.3 @@ -13,25 +9,25 @@ dependencies: version: 1.10.0 '@cosmjs/amino': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@cosmjs/crypto': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@cosmjs/encoding': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@cosmjs/proto-signing': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@cosmjs/stargate': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@cosmjs/tendermint-rpc': specifier: ^0.32.1 - version: 0.32.1 + version: 0.32.2 '@dydxprotocol/v4-abacus': - specifier: ^1.3.2 - version: 1.3.2 + specifier: ^1.4.0 + version: 1.4.0 '@dydxprotocol/v4-client-js': specifier: ^1.0.20 version: 1.0.20 @@ -382,7 +378,7 @@ packages: /@0xsquid/sdk@1.10.0: resolution: {integrity: sha512-NKxHYB+g/TMPY+XmCHs+LuhyfbhH4KvAbGpVBOBPXM9Q5FsKcKrDJpTd5YnGYCLF9B3qXAzVTR0XhiC73GmOOA==} dependencies: - '@cosmjs/encoding': 0.31.1 + '@cosmjs/encoding': 0.31.0 '@cosmjs/stargate': 0.31.0 axios: 0.27.2 cosmjs-types: 0.8.0 @@ -689,16 +685,16 @@ packages: resolution: {integrity: sha512-xJ5CCEK7H79FTpOuEmlpSzVI+ZeYESTVvO3wHDgbnceIyAne3C68SvyaKqLUR4uJB0Z4q4+DZHbqW6itUiv4lA==} dependencies: '@cosmjs/crypto': 0.31.0 - '@cosmjs/encoding': 0.31.1 - '@cosmjs/math': 0.31.1 - '@cosmjs/utils': 0.31.1 + '@cosmjs/encoding': 0.31.0 + '@cosmjs/math': 0.31.0 + '@cosmjs/utils': 0.31.0 dev: false - /@cosmjs/amino@0.32.1: - resolution: {integrity: sha512-5l2xQ2XuAhV/B3kTIMPBcVZ/OQ+9Yyddzw/lIVs4qE5e/oBI0PVNWXw1oyR0wgfGHrMUxgKjsoOOqE2IbXVyCw==} + /@cosmjs/amino@0.32.2: + resolution: {integrity: sha512-lcK5RCVm4OfdAooxKcF2+NwaDVVpghOq6o/A40c2mHXDUzUoRZ33VAHjVJ9Me6vOFxshrw/XEFn1f4KObntjYA==} dependencies: - '@cosmjs/crypto': 0.32.1 - '@cosmjs/encoding': 0.32.1 + '@cosmjs/crypto': 0.32.2 + '@cosmjs/encoding': 0.32.2 '@cosmjs/math': 0.32.2 '@cosmjs/utils': 0.32.2 dev: false @@ -753,19 +749,19 @@ packages: /@cosmjs/crypto@0.31.0: resolution: {integrity: sha512-UaqCe6Tgh0pe1QlZ66E13t6FlIF86QrnBXXq+EN7Xe1Rouza3fJ1ojGlPleJZkBoq3tAyYVIOOqdZIxtVj/sIQ==} dependencies: - '@cosmjs/encoding': 0.31.1 - '@cosmjs/math': 0.31.1 - '@cosmjs/utils': 0.31.1 + '@cosmjs/encoding': 0.31.0 + '@cosmjs/math': 0.31.0 + '@cosmjs/utils': 0.31.0 '@noble/hashes': 1.3.3 bn.js: 5.2.1 elliptic: 6.5.4 libsodium-wrappers-sumo: 0.7.11 dev: false - /@cosmjs/crypto@0.32.1: - resolution: {integrity: sha512-AsKucEg5o8evU0wXF/lDwX+ZSwCKF4bbc57nFzraHywlp3sNu4dfPPURoMrT0r7kT7wQZAy4Pdnvmm9nnCCm/Q==} + /@cosmjs/crypto@0.32.2: + resolution: {integrity: sha512-RuxrYKzhrPF9g6NmU7VEq++Hn1vZJjqqJpZ9Tmw9lOYOV8BUsv+j/0BE86kmWi7xVJ7EwxiuxYsKuM8IR18CIA==} dependencies: - '@cosmjs/encoding': 0.32.1 + '@cosmjs/encoding': 0.32.2 '@cosmjs/math': 0.32.2 '@cosmjs/utils': 0.32.2 '@noble/hashes': 1.3.3 @@ -790,16 +786,16 @@ packages: readonly-date: 1.0.0 dev: false - /@cosmjs/encoding@0.31.1: - resolution: {integrity: sha512-IuxP6ewwX6vg9sUJ8ocJD92pkerI4lyG8J5ynAM3NaX3q+n+uMoPRSQXNeL9bnlrv01FF1kIm8if/f5F7ZPtkA==} + /@cosmjs/encoding@0.31.0: + resolution: {integrity: sha512-NYGQDRxT7MIRSlcbAezwxK0FqnaSPKCH7O32cmfpHNWorFxhy9lwmBoCvoe59Kd0HmArI4h+NGzLEfX3OLnA4Q==} dependencies: base64-js: 1.5.1 bech32: 1.1.4 readonly-date: 1.0.0 dev: false - /@cosmjs/encoding@0.32.1: - resolution: {integrity: sha512-x60Lfds+Eq42rVV29NaoIAson3kBhATBI3zPp7X3GJTryBc5HFHQ6L/976tE1WB2DrvkfUdWS3ayCMVOY/qm1g==} + /@cosmjs/encoding@0.32.2: + resolution: {integrity: sha512-WX7m1wLpA9V/zH0zRcz4EmgZdAv1F44g4dbXOgNj1eXZw1PIGR12p58OEkLN51Ha3S4DKRtCv5CkhK1KHEvQtg==} dependencies: base64-js: 1.5.1 bech32: 1.1.4 @@ -859,12 +855,6 @@ packages: bn.js: 5.2.1 dev: false - /@cosmjs/math@0.31.1: - resolution: {integrity: sha512-kiuHV6m6DSB8/4UV1qpFhlc4ul8SgLXTGRlYkYiIIP4l0YNeJ+OpPYaOlEgx4Unk2mW3/O2FWYj7Jc93+BWXng==} - dependencies: - bn.js: 5.2.1 - dev: false - /@cosmjs/math@0.32.2: resolution: {integrity: sha512-b8+ruAAY8aKtVKWSft2IvtCVCUH1LigIlf9ALIiY8n9jtM4kMASiaRbQ/27etnSAInV88IaezKK9rQZrtxTjcw==} dependencies: @@ -888,19 +878,19 @@ packages: dependencies: '@cosmjs/amino': 0.31.0 '@cosmjs/crypto': 0.31.0 - '@cosmjs/encoding': 0.31.1 - '@cosmjs/math': 0.31.1 - '@cosmjs/utils': 0.31.1 + '@cosmjs/encoding': 0.31.0 + '@cosmjs/math': 0.31.0 + '@cosmjs/utils': 0.31.0 cosmjs-types: 0.8.0 long: 4.0.0 dev: false - /@cosmjs/proto-signing@0.32.1: - resolution: {integrity: sha512-IHJMXQ8XnfzR5K1hWb8VV/jEfJof6BL2mgGIA7X4hSPegwoVfb9hnFKPEPgFjGCTTvGZ8SfnCdXxpsOjianVIA==} + /@cosmjs/proto-signing@0.32.2: + resolution: {integrity: sha512-UV4WwkE3W3G3s7wwU9rizNcUEz2g0W8jQZS5J6/3fiN0mRPwtPKQ6EinPN9ASqcAJ7/VQH4/9EPOw7d6XQGnqw==} dependencies: - '@cosmjs/amino': 0.32.1 - '@cosmjs/crypto': 0.32.1 - '@cosmjs/encoding': 0.32.1 + '@cosmjs/amino': 0.32.2 + '@cosmjs/crypto': 0.32.2 + '@cosmjs/encoding': 0.32.2 '@cosmjs/math': 0.32.2 '@cosmjs/utils': 0.32.2 cosmjs-types: 0.9.0 @@ -968,7 +958,7 @@ packages: dependencies: '@confio/ics23': 0.6.8 '@cosmjs/amino': 0.31.0 - '@cosmjs/encoding': 0.31.1 + '@cosmjs/encoding': 0.31.0 '@cosmjs/math': 0.31.0 '@cosmjs/proto-signing': 0.31.0 '@cosmjs/stream': 0.31.0 @@ -984,16 +974,16 @@ packages: - utf-8-validate dev: false - /@cosmjs/stargate@0.32.1: - resolution: {integrity: sha512-S0E1qKQ2CMJU79G8bQTquTyrbU03gFsvCkbo3RvK8v2OltVCByjFNh+0nGN5do+uDOzwwmDvnNLhR+SaIyNQoQ==} + /@cosmjs/stargate@0.32.2: + resolution: {integrity: sha512-AsJa29fT7Jd4xt9Ai+HMqhyj7UQu7fyYKdXj/8+/9PD74xe6lZSYhQPcitUmMLJ1ckKPgXSk5Dd2LbsQT0IhZg==} dependencies: '@confio/ics23': 0.6.8 - '@cosmjs/amino': 0.32.1 - '@cosmjs/encoding': 0.32.1 + '@cosmjs/amino': 0.32.2 + '@cosmjs/encoding': 0.32.2 '@cosmjs/math': 0.32.2 - '@cosmjs/proto-signing': 0.32.1 + '@cosmjs/proto-signing': 0.32.2 '@cosmjs/stream': 0.32.2 - '@cosmjs/tendermint-rpc': 0.32.1 + '@cosmjs/tendermint-rpc': 0.32.2 '@cosmjs/utils': 0.32.2 cosmjs-types: 0.9.0 xstream: 11.14.0 @@ -1044,12 +1034,12 @@ packages: resolution: {integrity: sha512-yo9xbeuI6UoEKIhFZ9g0dvUKLqnBzwdpEc/uldQygQc51j38gQVwFko+6sjmhieJqRYYvrYumcbJMiV6GFM9aA==} dependencies: '@cosmjs/crypto': 0.31.0 - '@cosmjs/encoding': 0.31.1 + '@cosmjs/encoding': 0.31.0 '@cosmjs/json-rpc': 0.31.0 - '@cosmjs/math': 0.31.1 + '@cosmjs/math': 0.31.0 '@cosmjs/socket': 0.31.0 '@cosmjs/stream': 0.31.0 - '@cosmjs/utils': 0.31.1 + '@cosmjs/utils': 0.31.0 axios: 0.21.4 readonly-date: 1.0.0 xstream: 11.14.0 @@ -1059,11 +1049,11 @@ packages: - utf-8-validate dev: false - /@cosmjs/tendermint-rpc@0.32.1: - resolution: {integrity: sha512-4uGSxB2JejWhwBUgxca4GqcK/BGnCFMIP7ptwEledrC3AY/shPeIYcPXWEBwO7sfwCta8DhAOCLrc9zhVC+VAQ==} + /@cosmjs/tendermint-rpc@0.32.2: + resolution: {integrity: sha512-DXyJHDmcAfCix4H/7/dKR0UMdshP01KxJOXHdHxBCbLIpck94BsWD3B2ZTXwfA6sv98so9wOzhp7qGQa5malxg==} dependencies: - '@cosmjs/crypto': 0.32.1 - '@cosmjs/encoding': 0.32.1 + '@cosmjs/crypto': 0.32.2 + '@cosmjs/encoding': 0.32.2 '@cosmjs/json-rpc': 0.32.2 '@cosmjs/math': 0.32.2 '@cosmjs/socket': 0.32.2 @@ -1090,10 +1080,6 @@ packages: resolution: {integrity: sha512-nNcycZWUYLNJlrIXgpcgVRqdl6BXjF4YlXdxobQWpW9Tikk61bEGeAFhDYtC0PwHlokCNw0KxWiHGJL4nL7Q5A==} dev: false - /@cosmjs/utils@0.31.1: - resolution: {integrity: sha512-n4Se1wu4GnKwztQHNFfJvUeWcpvx3o8cWhSbNs9JQShEuB3nv3R5lqFBtDCgHZF/emFQAP+ZjF8bTfCs9UBGhA==} - dev: false - /@cosmjs/utils@0.32.2: resolution: {integrity: sha512-Gg5t+eR7vPJMAmhkFt6CZrzPd0EKpAslWwk5rFVYZpJsM8JG5KT9XQ99hgNM3Ov6ScNoIWbXkpX27F6A9cXR4Q==} dev: false @@ -1102,29 +1088,29 @@ packages: resolution: {integrity: sha512-RpfLEtTlyIxeNPGKcokS+p3BZII/Q3bYxryFRglh5H3A3T8q9fsLYm72VYAMEOOIBLEa8o93kFLiBDUWKrwXZA==} dev: true - /@dydxprotocol/v4-abacus@1.3.2: - resolution: {integrity: sha512-zo0IHjGMlJRKOYDgNqNFQ9GtBJKJP4+Y9YY7V0X3Wt61ppKAYzodaYQhc9V/RYchcZTtS/xkicLug444YrvehQ==} + /@dydxprotocol/v4-abacus@1.4.0: + resolution: {integrity: sha512-mkdkXQXTVF1K6UmZwIwLC9gjsXg+Nt3h5/iW+WriVv1cUMPmHfPyRWL31eDDvkVO5UeECf05kiYXPk1y+7dPuA==} dev: false /@dydxprotocol/v4-client-js@1.0.20: resolution: {integrity: sha512-dXKW2NC1XlVVIRKvHWVDofLZSCPTJAaRY5eXzxH5CcXpnl2kdXorr7ykqWZxW0jHFPWWvRSJtUDqZN1qFrEe/w==} dependencies: - '@cosmjs/amino': 0.32.1 - '@cosmjs/encoding': 0.32.1 + '@cosmjs/amino': 0.32.2 + '@cosmjs/encoding': 0.32.2 '@cosmjs/math': 0.32.2 - '@cosmjs/proto-signing': 0.32.1 - '@cosmjs/stargate': 0.32.1 - '@cosmjs/tendermint-rpc': 0.32.1 + '@cosmjs/proto-signing': 0.32.2 + '@cosmjs/stargate': 0.32.2 + '@cosmjs/tendermint-rpc': 0.32.2 '@cosmjs/utils': 0.32.2 '@dydxprotocol/v4-proto': 4.0.0-dev.0 '@osmonauts/lcd': 0.6.0 - '@scure/bip32': 1.3.3 - '@scure/bip39': 1.2.2 + '@scure/bip32': 1.3.2 + '@scure/bip39': 1.2.1 axios: 1.1.3 bech32: 1.1.4 bignumber.js: 9.1.1 cosmjs-types: 0.9.0 - ethereum-cryptography: 2.1.3 + ethereum-cryptography: 2.1.2 ethers: 6.6.1 long: 4.0.0 protobufjs: 6.11.4 @@ -2158,18 +2144,18 @@ packages: '@noble/hashes': 1.3.0 dev: false + /@noble/curves@1.1.0: + resolution: {integrity: sha512-091oBExgENk/kGj3AZmtBDMpxQPDtxQABR2B9lb1JbVTs6ytdzZNwvhxQ4MWasRNEzlbEH8jCWFCwhF/Obj5AA==} + dependencies: + '@noble/hashes': 1.3.1 + dev: false + /@noble/curves@1.2.0: resolution: {integrity: sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==} dependencies: '@noble/hashes': 1.3.2 dev: false - /@noble/curves@1.3.0: - resolution: {integrity: sha512-t01iSXPuN+Eqzb4eBX0S5oubSqXbK/xXa1Ne18Hj8f9pStxztHCE2gfboSp/dZRLSqfuLpRK2nDXDK+W9puocA==} - dependencies: - '@noble/hashes': 1.3.3 - dev: false - /@noble/ed25519@1.7.3: resolution: {integrity: sha512-iR8GBkDt0Q3GyaVcIu7mSsVIqnFbkbRzGLWlvhwunacoLwt4J3swfKhfaM6rN6WY+TBGoYT1GtT1mIh2/jGbRQ==} dev: true @@ -5035,18 +5021,18 @@ packages: '@scure/base': 1.1.1 dev: false - /@scure/bip32@1.3.2: - resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} + /@scure/bip32@1.3.1: + resolution: {integrity: sha512-osvveYtyzdEVbt3OfwwXFr4P2iVBL5u1Q3q4ONBfDY/UpOuXmOlbgwc1xECEboY8wIays8Yt6onaWMUdUbfl0A==} dependencies: - '@noble/curves': 1.2.0 + '@noble/curves': 1.1.0 '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 dev: false - /@scure/bip32@1.3.3: - resolution: {integrity: sha512-LJaN3HwRbfQK0X1xFSi0Q9amqOgzQnnDngIt+ZlsBC3Bm7/nE7K0kwshZHyaru79yIVRv/e1mQAjZyuZG6jOFQ==} + /@scure/bip32@1.3.2: + resolution: {integrity: sha512-N1ZhksgwD3OBlwTv3R6KFEcPojl/W4ElJOeCZdi+vuI5QmTFwLq3OFf2zd2ROpKvxFdgZ6hUpb0dx9bVNEwYCA==} dependencies: - '@noble/curves': 1.3.0 + '@noble/curves': 1.2.0 '@noble/hashes': 1.3.3 '@scure/base': 1.1.5 dev: false @@ -5065,13 +5051,6 @@ packages: '@scure/base': 1.1.5 dev: false - /@scure/bip39@1.2.2: - resolution: {integrity: sha512-HYf9TUXG80beW+hGAt3TRM8wU6pQoYur9iNypTROm42dorCGmLnFe3eWjz3gOq6G62H2WRh0FCzAR1PI+29zIA==} - dependencies: - '@noble/hashes': 1.3.3 - '@scure/base': 1.1.5 - dev: false - /@solana/buffer-layout@4.0.1: resolution: {integrity: sha512-E1ImOIAD1tBZFRdjeM4/pzTiTApC0AOBGwyAMS4fwIodCWArzJ3DWdoh8cKxeFM2fElkxBh2Aqts1BPC373rHA==} engines: {node: '>=5.10'} @@ -5083,7 +5062,7 @@ packages: resolution: {integrity: sha512-up5VG1dK+GPhykmuMIozJZBbVqpm77vbOG6/r5dS7NBGZonwHfTLdBbsYc3rjmaQ4DpCXUa3tUc4RZHRORvZrw==} dependencies: '@babel/runtime': 7.22.10 - '@noble/curves': 1.3.0 + '@noble/curves': 1.2.0 '@noble/hashes': 1.3.3 '@solana/buffer-layout': 4.0.1 agentkeepalive: 4.5.0 @@ -9307,13 +9286,13 @@ packages: fast-safe-stringify: 2.1.1 dev: false - /ethereum-cryptography@2.1.3: - resolution: {integrity: sha512-BlwbIL7/P45W8FGW2r7LGuvoEZ+7PWsniMvQ4p5s2xCyw9tmaDlpfsN9HjAucbF+t/qpVHwZUisgfK24TCW8aA==} + /ethereum-cryptography@2.1.2: + resolution: {integrity: sha512-Z5Ba0T0ImZ8fqXrJbpHcbpAvIswRte2wGNR/KePnu8GbbvgJ47lMxT/ZZPG6i9Jaht4azPDop4HaM00J0J59ug==} dependencies: - '@noble/curves': 1.3.0 - '@noble/hashes': 1.3.3 - '@scure/bip32': 1.3.3 - '@scure/bip39': 1.2.2 + '@noble/curves': 1.1.0 + '@noble/hashes': 1.3.1 + '@scure/bip32': 1.3.1 + '@scure/bip39': 1.2.1 dev: false /ethers@5.7.2: @@ -14933,3 +14912,7 @@ packages: /zwitch@2.0.4: resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} dev: true + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false diff --git a/src/components/Output.tsx b/src/components/Output.tsx index 8df09d2..ec55d8f 100644 --- a/src/components/Output.tsx +++ b/src/components/Output.tsx @@ -65,6 +65,9 @@ type ElementProps = { resolution?: number; stripRelativeWords?: boolean; }; + timeOptions?: { + useUTC?: boolean; + }; tag?: React.ReactNode; withParentheses?: boolean; locale?: string; @@ -89,6 +92,7 @@ export const Output = ({ relativeTimeFormatOptions = { format: 'singleCharacter', }, + timeOptions, tag, withParentheses, locale = navigator.language || 'en-US', @@ -166,16 +170,21 @@ export const Output = ({ if ((typeof value !== 'string' && typeof value !== 'number') || !value) return null; const date = new Date(value); const dateString = { - [OutputType.Date]: date.toLocaleString(selectedLocale, { dateStyle: 'medium' }), + [OutputType.Date]: date.toLocaleString(selectedLocale, { + dateStyle: 'medium', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, + }), [OutputType.DateTime]: date.toLocaleString(selectedLocale, { dateStyle: 'short', timeStyle: 'short', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, }), [OutputType.Time]: date.toLocaleString(selectedLocale, { hour12: false, hour: '2-digit', minute: '2-digit', second: '2-digit', + timeZone: timeOptions?.useUTC ? 'UTC' : undefined, }), }[type]; diff --git a/src/components/Table.tsx b/src/components/Table.tsx index ce2c89b..7c0d692 100644 --- a/src/components/Table.tsx +++ b/src/components/Table.tsx @@ -68,7 +68,7 @@ export type TableItem = { onSelect?: (key: TableRowData) => void; }; -type ColumnDef = { +export type ColumnDef = { columnKey: string; label: React.ReactNode; tag?: React.ReactNode; @@ -513,7 +513,7 @@ const TableColumnHeader = ({ export const ViewMoreRow = ({ colSpan, onClick }: { colSpan: number; onClick: () => void }) => { const stringGetter = useStringGetter(); return ( - + e.preventDefault()} @@ -523,7 +523,7 @@ export const ViewMoreRow = ({ colSpan, onClick }: { colSpan: number; onClick: () {stringGetter({ key: STRING_KEYS.VIEW_MORE })} - + ); }; @@ -673,6 +673,8 @@ Styled.TableWrapper = styled.div<{ --table-lastColumn-cell-align: end; // start | center | end | var(--table-cell-align) --tableCell-padding: 0 1rem; + --tableViewMore-borderColor: inherit; + // Rules flex: 1; @@ -984,3 +986,7 @@ Styled.ViewMoreButton = styled(Button)` margin-left: 0.5ch; } `; + +Styled.ViewMoreTr = styled(Styled.Tr)` + --border-color: var(--tableViewMore-borderColor); +`; diff --git a/src/constants/abacus.ts b/src/constants/abacus.ts index 1cdbfcc..ac44cf3 100644 --- a/src/constants/abacus.ts +++ b/src/constants/abacus.ts @@ -119,6 +119,13 @@ export const InputSelectionOption = Abacus.exchange.dydx.abacus.output.input.Sel // ------ Wallet ------ // export type Wallet = Abacus.exchange.dydx.abacus.output.Wallet; export type AccountBalance = Abacus.exchange.dydx.abacus.output.AccountBalance; +export type TradingRewards = Abacus.exchange.dydx.abacus.output.TradingRewards; +export type HistoricalTradingReward = Abacus.exchange.dydx.abacus.output.HistoricalTradingReward; +export const HistoricalTradingRewardsPeriod = + Abacus.exchange.dydx.abacus.state.manager.HistoricalTradingRewardsPeriod; +const historicalTradingRewardsPeriod = [...HistoricalTradingRewardsPeriod.values()] as const; +export type HistoricalTradingRewardsPeriods = (typeof historicalTradingRewardsPeriod)[number]; + export type Subaccount = Abacus.exchange.dydx.abacus.output.Subaccount; export type SubaccountPosition = Abacus.exchange.dydx.abacus.output.SubaccountPosition; export type SubaccountOrder = Abacus.exchange.dydx.abacus.output.SubaccountOrder; @@ -202,6 +209,9 @@ export const RestrictionType = Abacus.exchange.dydx.abacus.output.Restriction; const restrictionTypes = [...RestrictionType.values()] as const; export type RestrictionTypes = (typeof restrictionTypes)[number]; +// ------ Api data ------ // +export const ApiData = Abacus.exchange.dydx.abacus.state.manager.ApiData; + // ------ Enum Conversions ------ // type IfEquals = (() => T extends X ? 1 : 2) extends () => T extends Y ? 1 : 2 ? A @@ -236,6 +246,15 @@ export const HISTORICAL_PNL_PERIODS: Record< [HistoricalPnlPeriod.Period90d.name]: HistoricalPnlPeriod.Period90d, }; +export const HISTORICAL_TRADING_REWARDS_PERIODS: Record< + KotlinIrEnumValues, + HistoricalTradingRewardsPeriods +> = { + [HistoricalTradingRewardsPeriod.MONTHLY.name]: HistoricalTradingRewardsPeriod.MONTHLY, + [HistoricalTradingRewardsPeriod.WEEKLY.name]: HistoricalTradingRewardsPeriod.WEEKLY, + [HistoricalTradingRewardsPeriod.DAILY.name]: HistoricalTradingRewardsPeriod.DAILY, +}; + export const ORDER_STATUS_STRINGS: Record, string> = { [AbacusOrderStatus.open.name]: STRING_KEYS.OPEN_STATUS, [AbacusOrderStatus.open.rawValue]: STRING_KEYS.OPEN_STATUS, diff --git a/src/constants/localization.ts b/src/constants/localization.ts index 5ff02b2..3c77561 100644 --- a/src/constants/localization.ts +++ b/src/constants/localization.ts @@ -95,7 +95,7 @@ export type TooltipStrings = { stringParams?: any; urlConfigs?: LinksConfigs; }) => { - title: string; + title?: string; body: string; learnMoreLink?: string; }; diff --git a/src/constants/tooltips/trade.ts b/src/constants/tooltips/trade.ts index 081e3f0..4382ce8 100644 --- a/src/constants/tooltips/trade.ts +++ b/src/constants/tooltips/trade.ts @@ -198,4 +198,7 @@ export const tradeTooltips: TooltipStrings = { title: stringGetter({ key: TOOLTIP_STRING_KEYS.UNREALIZED_PNL_TITLE }), body: stringGetter({ key: TOOLTIP_STRING_KEYS.UNREALIZED_PNL_BODY }), }), + 'reward-history': ({ stringGetter }) => ({ + body: stringGetter({ key: TOOLTIP_STRING_KEYS.REWARD_HISTORY_BODY }), + }), } as const; diff --git a/src/lib/abacus/index.ts b/src/lib/abacus/index.ts index 199f834..c3be440 100644 --- a/src/lib/abacus/index.ts +++ b/src/lib/abacus/index.ts @@ -3,6 +3,8 @@ import type { LocalWallet } from '@dydxprotocol/v4-client-js'; import type { ClosePositionInputFields, Nullable, + HistoricalTradingRewardsPeriod, + HistoricalTradingRewardsPeriods, HumanReadablePlaceOrderPayload, HumanReadableCancelOrderPayload, TradeInputFields, @@ -23,6 +25,7 @@ import { CoroutineTimer, TransferType, AbacusAppConfig, + ApiData, } from '@/constants/abacus'; import { DEFAULT_MARKETID } from '@/constants/markets'; @@ -33,8 +36,6 @@ import type { RootStore } from '@/state/_store'; import { setTradeFormInputs } from '@/state/inputs'; import { getInputTradeOptions, getTransferInputs } from '@/state/inputsSelectors'; -import { testFlags } from '@/lib/testFlags'; - import AbacusRest from './rest'; import AbacusAnalytics from './analytics'; import AbacusWebsocket from './websocket'; @@ -227,6 +228,15 @@ class AbacusStateManager { this.stateManager.historicalPnlPeriod = period; }; + setHistoricalTradingRewardPeriod = ( + period: (typeof HistoricalTradingRewardsPeriod)[keyof typeof HistoricalTradingRewardsPeriod] + ) => { + this.stateManager.historicalTradingRewardPeriod = period; + }; + + refreshHistoricalTradingRewards = () => + this.stateManager.refresh(ApiData.HISTORICAL_TRADING_REWARDS); + switchNetwork = (network: DydxNetwork) => { this.stateManager.environmentId = network; @@ -278,6 +288,9 @@ class AbacusStateManager { getHistoricalPnlPeriod = (): Nullable => this.stateManager.historicalPnlPeriod; + getHistoricalTradingRewardPeriod = (): HistoricalTradingRewardsPeriods => + this.stateManager.historicalTradingRewardPeriod; + handleCandlesSubscription = ({ channelId, subscribe, diff --git a/src/lib/abacus/stateNotification.ts b/src/lib/abacus/stateNotification.ts index 216b7b7..b8b328c 100644 --- a/src/lib/abacus/stateNotification.ts +++ b/src/lib/abacus/stateNotification.ts @@ -29,6 +29,7 @@ import { setSubaccount, setTransfers, setWallet, + setTradingRewards, } from '@/state/account'; import { setApiState } from '@/state/app'; @@ -96,6 +97,12 @@ class AbacusStateNotifier implements AbacusStateNotificationProtocol { } } + if (changes.has(Changes.tradingRewards)) { + if (updatedState.account?.tradingRewards) { + dispatch(setTradingRewards(updatedState.account?.tradingRewards)); + } + } + if (changes.has(Changes.configs)) { dispatch(setConfigs(updatedState.configs)); } diff --git a/src/lib/testFlags.ts b/src/lib/testFlags.ts index 66c4b59..36f8a8b 100644 --- a/src/lib/testFlags.ts +++ b/src/lib/testFlags.ts @@ -26,6 +26,10 @@ class TestFlags { get addressOverride():string { return this.queryParams.address; } + + get showTradingRewards() { + return !!this.queryParams.tradingrewards; + } } export const testFlags = new TestFlags(); diff --git a/src/pages/Profile.tsx b/src/pages/Profile.tsx index 270131b..5136f43 100644 --- a/src/pages/Profile.tsx +++ b/src/pages/Profile.tsx @@ -23,7 +23,10 @@ import { AppRoute, PortfolioRoute, HistoryRoute } from '@/constants/routes'; import { wallets } from '@/constants/wallets'; import { useAccounts, useStringGetter, useTokenConfigs } from '@/hooks'; -import { getOnboardingState } from '@/state/accountSelectors'; +import { + getHistoricalTradingRewardsForCurrentWeek, + getOnboardingState, +} from '@/state/accountSelectors'; import { openDialog } from '@/state/dialogs'; import { isTruthy } from '@/lib/isTruthy'; @@ -52,6 +55,8 @@ const Profile = () => { chainId: ENS_CHAIN_ID, }); + const currentWeekTradingReward = useSelector(getHistoricalTradingRewardsForCurrentWeek); + const actions = [ { key: 'deposit', @@ -159,64 +164,62 @@ const Profile = () => { ); })} - - - - {stringGetter({ key: STRING_KEYS.SETTINGS })} - - } - onClick={() => navigate(AppRoute.Settings)} + + + + {stringGetter({ key: STRING_KEYS.SETTINGS })} + + } + onClick={() => navigate(AppRoute.Settings)} + /> + + + {stringGetter({ key: STRING_KEYS.HELP })} + + } + onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))} + /> + + + + + + + - - - {stringGetter({ key: STRING_KEYS.HELP })} - - } - onClick={() => dispatch(openDialog({ type: DialogTypes.Help }))} + + + - + - - - - - - - - - - - - - { ]} withInnerBorders={false} /> - + - - + + ); }; @@ -245,14 +248,27 @@ const Styled: Record = {}; Styled.MobileProfileLayout = styled.div` ${layoutMixins.contentContainerPage} + display: grid; + grid-template-columns: 1fr 1fr; gap: 1rem; padding: 1.25rem 0.9rem; max-width: 100vw; + + grid-template-areas: + 'header header' + 'actions actions' + 'settings help' + 'migrate migrate' + 'balance balance' + 'rewards fees' + 'history history' + 'governance governance' + 'staking staking'; `; Styled.Header = styled.header` + grid-area: header; ${layoutMixins.row} - padding: 0 1rem; `; @@ -299,6 +315,7 @@ Styled.Address = styled.h1` Styled.Actions = styled(Toolbar)` ${layoutMixins.spacedRow} --stickyArea-topHeight: 5rem; + grid-area: actions; > a, > label { @@ -329,22 +346,16 @@ Styled.ActionButton = styled(IconButton)<{ iconName?: IconName }>` `} `; -Styled.EqualGrid = styled.div` - ${layoutMixins.gridEqualColumns} - - gap: 1rem; -`; - Styled.Details = styled(Details)` font: var(--font-small-book); --details-value-font: var(--font-medium-book); `; Styled.RewardsPanel = styled(Panel)` + grid-area: rewards; align-self: flex-start; - - &, - > * { + height: 100%; + > div { height: 100%; } @@ -353,7 +364,12 @@ Styled.RewardsPanel = styled(Panel)` } `; -Styled.TablePanel = styled(Panel)` +Styled.FeesPanel = styled(Panel)` + grid-area: fees; +`; + +Styled.HistoryPanel = styled(Panel)` + grid-area: history; --panel-content-paddingY: 0; --panel-content-paddingX: 0; @@ -393,3 +409,27 @@ Styled.PanelButton = styled(Panel)` --panel-paddingY: 0 --panel-paddingX:0; `; + +Styled.SettingsButton = styled(Styled.PanelButton)` + grid-area: settings; +`; + +Styled.HelpButton = styled(Styled.PanelButton)` + grid-area: help; +`; + +Styled.MigratePanel = styled(MigratePanel)` + grid-area: migrate; +`; + +Styled.DYDXBalancePanel = styled(DYDXBalancePanel)` + grid-area: balance; +`; + +Styled.GovernancePanel = styled(GovernancePanel)` + grid-area: governance; +`; + +Styled.StakingPanel = styled(StakingPanel)` + grid-area: staking; +`; diff --git a/src/pages/rewards/DYDXBalancePanel.tsx b/src/pages/rewards/DYDXBalancePanel.tsx index 5300fc2..b601783 100644 --- a/src/pages/rewards/DYDXBalancePanel.tsx +++ b/src/pages/rewards/DYDXBalancePanel.tsx @@ -22,7 +22,7 @@ import { OnboardingTriggerButton } from '@/views/dialogs/OnboardingTriggerButton import { openDialog } from '@/state/dialogs'; import { calculateCanAccountTrade } from '@/state/accountCalculators'; -export const DYDXBalancePanel = () => { +export const DYDXBalancePanel = ({ className }: { className?: string }) => { const dispatch = useDispatch(); const stringGetter = useStringGetter(); @@ -33,6 +33,7 @@ export const DYDXBalancePanel = () => { return ( diff --git a/src/pages/rewards/RewardHistoryPanel.tsx b/src/pages/rewards/RewardHistoryPanel.tsx new file mode 100644 index 0000000..c79da79 --- /dev/null +++ b/src/pages/rewards/RewardHistoryPanel.tsx @@ -0,0 +1,105 @@ +import { useCallback, useState } from 'react'; +import styled, { AnyStyledComponent } from 'styled-components'; + +import { useStringGetter } from '@/hooks'; + +import { + HISTORICAL_TRADING_REWARDS_PERIODS, + HistoricalTradingRewardsPeriod, + HistoricalTradingRewardsPeriods, +} from '@/constants/abacus'; + +import { STRING_KEYS } from '@/constants/localization'; +import breakpoints from '@/styles/breakpoints'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { Panel } from '@/components/Panel'; +import { ToggleGroup } from '@/components/ToggleGroup'; +import { WithTooltip } from '@/components/WithTooltip'; +import { TradingRewardHistoryTable } from '@/views/tables/TradingRewardHistoryTable'; + +import abacusStateManager from '@/lib/abacus'; + +export const RewardHistoryPanel = () => { + const stringGetter = useStringGetter(); + + const [selectedPeriod, setSelectedPeriod] = useState( + abacusStateManager.getHistoricalTradingRewardPeriod() || HistoricalTradingRewardsPeriod.WEEKLY + ); + + const onSelectPeriod = useCallback( + (periodName: string) => { + const selectedPeriod = + HISTORICAL_TRADING_REWARDS_PERIODS[ + periodName as keyof typeof HISTORICAL_TRADING_REWARDS_PERIODS + ]; + setSelectedPeriod(selectedPeriod); + abacusStateManager.setHistoricalTradingRewardPeriod(selectedPeriod); + }, + [setSelectedPeriod, selectedPeriod] + ); + + return ( + + + +

{stringGetter({ key: STRING_KEYS.REWARD_HISTORY })}

+
+ {stringGetter({ key: STRING_KEYS.REWARD_HISTORY_DESCRIPTION })} +
+ + + } + > + +
+ ); +}; + +const Styled: Record = {}; + +Styled.Header = styled.div` + ${layoutMixins.spacedRow} + + padding: 1rem 1rem 0; + margin-bottom: -0.5rem; + + @media ${breakpoints.notTablet} { + padding: 1.25rem 1.5rem 0; + } +`; + +Styled.Title = styled.div` + ${layoutMixins.column} + color: var(--color-text-0); + font: var(--font-small-book); + + h3 { + font: var(--font-medium-book); + color: var(--color-text-2); + } +`; + +Styled.Content = styled.div` + ${layoutMixins.flexColumn} + gap: 0.75rem; +`; diff --git a/src/pages/rewards/RewardsHelpPanel.tsx b/src/pages/rewards/RewardsHelpPanel.tsx index ac26d95..6696e37 100644 --- a/src/pages/rewards/RewardsHelpPanel.tsx +++ b/src/pages/rewards/RewardsHelpPanel.tsx @@ -29,20 +29,18 @@ export const RewardsHelpPanel = () => { > @@ -72,7 +70,7 @@ Styled.Header = styled.div` font: var(--font-small-book); @media ${breakpoints.notTablet} { - padding: 1.5rem 1.25rem; + padding: 1.5rem; } h3 { diff --git a/src/pages/rewards/RewardsPage.tsx b/src/pages/rewards/RewardsPage.tsx index 4329ca6..c68df83 100644 --- a/src/pages/rewards/RewardsPage.tsx +++ b/src/pages/rewards/RewardsPage.tsx @@ -1,5 +1,4 @@ -import styled, { AnyStyledComponent } from 'styled-components'; -import { useDispatch } from 'react-redux'; +import styled, { AnyStyledComponent, css } from 'styled-components'; import { useNavigate } from 'react-router-dom'; import { STRING_KEYS } from '@/constants/localization'; @@ -11,18 +10,20 @@ import { breakpoints } from '@/styles'; import { layoutMixins } from '@/styles/layoutMixins'; import { BackButton } from '@/components/BackButton'; -import { Panel } from '@/components/Panel'; + +import { testFlags } from '@/lib/testFlags'; import { DYDXBalancePanel } from './DYDXBalancePanel'; import { LaunchIncentivesPanel } from './LaunchIncentivesPanel'; import { MigratePanel } from './MigratePanel'; import { RewardsHelpPanel } from './RewardsHelpPanel'; +import { TradingRewardsSummaryPanel } from './TradingRewardsSummaryPanel'; +import { RewardHistoryPanel } from './RewardHistoryPanel'; import { GovernancePanel } from './GovernancePanel'; import { StakingPanel } from './StakingPanel'; import { NewMarketsPanel } from './NewMarketsPanel'; const RewardsPage = () => { - const dispatch = useDispatch(); const stringGetter = useStringGetter(); const { isTablet, isNotTablet } = useBreakpoints(); const navigate = useNavigate(); @@ -35,25 +36,38 @@ const RewardsPage = () => { {stringGetter({ key: STRING_KEYS.TRADING_REWARDS })} )} - {import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && } + + {import.meta.env.VITE_V3_TOKEN_ADDRESS && isNotTablet && } - {isTablet ? ( - - ) : ( - - - - - )} - {isNotTablet && ( - - - - - - )} + {isTablet ? ( + + ) : ( + <> + + + + )} - + {testFlags.showTradingRewards && ( + + + {isTablet && } + + + )} + + {isNotTablet && ( + + + + + + + )} + ); }; @@ -64,7 +78,6 @@ const Styled: Record = {}; Styled.Page = styled.div` ${layoutMixins.contentContainerPage} - gap: 1.5rem; padding: 2rem; align-items: center; @@ -89,27 +102,98 @@ Styled.MobileHeader = styled.header` ${layoutMixins.stickyHeader} z-index: 2; padding: 1.25rem 0; - margin-bottom: -1.5rem; font: var(--font-large-medium); color: var(--color-text-2); background-color: var(--color-layer-2); `; -Styled.Panel = styled(Panel)` - height: fit-content; -`; +Styled.GridLayout = styled.div<{ showTradingRewards?: boolean; showMigratePanel?: boolean }>` + --gap: 1.5rem; + display: grid; + grid-template-columns: 2fr 1fr; + gap: var(--gap); -Styled.PanelRow = styled.div` - ${layoutMixins.gridEqualColumns} - gap: 1.5rem; + > * { + gap: var(--gap); + } + + ${({ showTradingRewards, showMigratePanel }) => + showTradingRewards && showMigratePanel + ? css` + grid-template-areas: + 'migrate migrate' + 'incentives balance' + 'rewards other'; + ` + : showTradingRewards + ? css` + grid-template-areas: 'incentives balance' 'rewards other'; + ` + : showMigratePanel + ? css` + grid-template-areas: 'migrate migrate' 'incentives balance' 'other other'; + ` + : css` + grid-template-areas: 'incentives balance' 'other other'; + `}; @media ${breakpoints.tablet} { - grid-auto-flow: row; + --gap: 1rem; grid-template-columns: 1fr; + + ${({ showTradingRewards }) => + showTradingRewards + ? css` + grid-template-areas: + 'incentives' + 'rewards'; + ` + : css` + grid-template-areas: 'incentives'; + `} } `; -Styled.PanelRowIncentivesAndBalance = styled(Styled.PanelRow)` - grid-template-columns: 2fr 1fr; +Styled.MigratePanel = styled(MigratePanel)` + grid-area: migrate; +`; + +Styled.LaunchIncentivesPanel = styled(LaunchIncentivesPanel)` + grid-area: incentives; +`; + +Styled.DYDXBalancePanel = styled(DYDXBalancePanel)` + grid-area: balance; +`; + +Styled.TradingRewardsColumn = styled.div` + grid-area: rewards; + ${layoutMixins.flexColumn} +`; + +Styled.OtherColumn = styled.div<{ showTradingRewards?: boolean }>` + grid-area: other; + ${layoutMixins.flexColumn} + + ${({ showTradingRewards }) => + !showTradingRewards && + css` + display: grid; + grid-template-columns: repeat(3, 1fr); + + > section:last-of-type { + grid-column: 1 / -1; + } + `} +`; + +Styled.RewardHistoryHeader = styled.div` + h3 { + font: var(--font-medium-book); + color: var(--color-text-2); + } + + padding: 1rem 1.5rem 0; + margin-bottom: -0.5rem; `; diff --git a/src/pages/rewards/TradingRewardsSummaryPanel.tsx b/src/pages/rewards/TradingRewardsSummaryPanel.tsx new file mode 100644 index 0000000..13871bf --- /dev/null +++ b/src/pages/rewards/TradingRewardsSummaryPanel.tsx @@ -0,0 +1,134 @@ +import { useEffect } from 'react'; +import styled, { AnyStyledComponent } from 'styled-components'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { STRING_KEYS } from '@/constants/localization'; +import { layoutMixins } from '@/styles/layoutMixins'; +import { useStringGetter, useTokenConfigs } from '@/hooks'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Details } from '@/components/Details'; +import { Output, OutputType } from '@/components/Output'; +import { Panel } from '@/components/Panel'; + +import { getHistoricalTradingRewardsForCurrentWeek } from '@/state/accountSelectors'; + +import abacusStateManager from '@/lib/abacus'; + +export const TradingRewardsSummaryPanel = () => { + const stringGetter = useStringGetter(); + const { chainTokenLabel } = useTokenConfigs(); + const currentWeekTradingReward = useSelector(getHistoricalTradingRewardsForCurrentWeek, shallowEqual); + + useEffect(() => { + abacusStateManager.refreshHistoricalTradingRewards(); + }, []); + + return !currentWeekTradingReward ? null : ( + {stringGetter({ key: STRING_KEYS.TRADING_REWARDS_SUMMARY })} + } + > + + +

{stringGetter({ key: STRING_KEYS.THIS_WEEK })}

+ + ), + value: ( + + } + type={OutputType.Asset} + value={currentWeekTradingReward.amount} + /> + + + → + + + + ), + }, + // TODO(@aforaleka): add all-time when supported + ]} + /> +
+
+ ); +}; + +const Styled: Record = {}; + +Styled.Header = styled.div` + padding: var(--panel-paddingY) var(--panel-paddingX) 0; + font: var(--font-medium-book); + color: var(--color-text-2); +`; + +Styled.Content = styled.div` + ${layoutMixins.flexColumn} + gap: 0.75rem; +`; + +Styled.TradingRewardsDetails = styled(Details)` + --details-item-backgroundColor: var(--color-layer-6); + + grid-template-columns: 1fr; // TODO(@aforaleka): change to 1fr 1fr when all-time is supported + gap: 1rem; + + > div { + gap: 0.5rem; + padding: 1rem; + border-radius: 0.75em; + background-color: var(--color-layer-5); + } + + dt { + width: 100%; + } + + output { + color: var(--color-text-2); + font: var(--font-large-book); + } +`; + +Styled.Label = styled.div` + ${layoutMixins.spacedRow} + + font: var(--font-base-book); + color: var(--color-text-1); +`; + +Styled.TimePeriod = styled.div` + ${layoutMixins.inlineRow} + + &, output { + color: var(--color-text-0); + font: var(--font-small-book); + } +`; + +Styled.Column = styled.div` + ${layoutMixins.flexColumn} + gap: 0.33rem; +`; + +Styled.AssetIcon = styled(AssetIcon)` + margin-left: 0.5ch; +`; diff --git a/src/state/account.ts b/src/state/account.ts index aae7b2d..57d7a54 100644 --- a/src/state/account.ts +++ b/src/state/account.ts @@ -13,6 +13,7 @@ import type { HistoricalPnlPeriods, SubAccountHistoricalPNLs, UsageRestriction, + TradingRewards, } from '@/constants/abacus'; import { OnboardingGuard, OnboardingState } from '@/constants/account'; @@ -24,6 +25,7 @@ import { getLocalStorage } from '@/lib/localStorage'; export type AccountState = { balances?: Record; stakingBalances?: Record; + tradingRewards?: TradingRewards; wallet?: Nullable; walletType?: WalletType; @@ -179,6 +181,9 @@ export const accountSlice = createSlice({ setStakingBalances: (state, action: PayloadAction>) => { state.stakingBalances = action.payload; }, + setTradingRewards: (state, action: PayloadAction) => { + state.tradingRewards = action.payload; + }, addUncommittedOrderClientId: (state, action: PayloadAction) => { state.uncommittedOrderClientIds.push(action.payload); }, @@ -206,6 +211,7 @@ export const { viewedOrders, setBalances, setStakingBalances, + setTradingRewards, addUncommittedOrderClientId, removeUncommittedOrderClientId, } = accountSlice.actions; diff --git a/src/state/accountSelectors.ts b/src/state/accountSelectors.ts index 809a027..e7390eb 100644 --- a/src/state/accountSelectors.ts +++ b/src/state/accountSelectors.ts @@ -1,3 +1,4 @@ +import type { Nullable, kollections } from '@dydxprotocol/v4-abacus'; import { OrderSide } from '@dydxprotocol/v4-client-js'; import { createSelector } from 'reselect'; @@ -9,6 +10,8 @@ import { AbacusOrderStatus, AbacusPositionSide, ORDER_SIDES, + HistoricalTradingReward, + HistoricalTradingRewardsPeriod, } from '@/constants/abacus'; import { OnboardingState } from '@/constants/account'; @@ -349,6 +352,38 @@ export const getBalances = (state: RootState) => state.account?.balances; * */ export const getStakingBalances = (state: RootState) => state.account?.stakingBalances; +/** + * @returns account all time trading rewards + */ +export const getTotalTradingRewards = (state: RootState) => state.account?.tradingRewards?.total; + +/** + * @returns account trading rewards aggregated by period + */ +export const getHistoricalTradingRewards = (state: RootState) => + state.account?.tradingRewards?.historical; + +/** + * @returns account historical trading rewards for the specified perid + */ +export const getHistoricalTradingRewardsForPeriod = (period: string) => + createSelector( + [getHistoricalTradingRewards], + ( + historicalTradingRewards: Nullable< + kollections.Map> + > + ) => historicalTradingRewards?.get(period) + ); + +/** + * @returns account historical trading rewards for the current week + */ +export const getHistoricalTradingRewardsForCurrentWeek = createSelector( + [getHistoricalTradingRewardsForPeriod(HistoricalTradingRewardsPeriod.WEEKLY.name)], + (historicalTradingRewards) => historicalTradingRewards?.firstOrNull() +); + /** * @returns UsageRestriction of the current session */ diff --git a/src/views/tables/TradingRewardHistoryTable.tsx b/src/views/tables/TradingRewardHistoryTable.tsx new file mode 100644 index 0000000..fd27bae --- /dev/null +++ b/src/views/tables/TradingRewardHistoryTable.tsx @@ -0,0 +1,164 @@ +import styled, { type AnyStyledComponent } from 'styled-components'; +import { shallowEqual, useSelector } from 'react-redux'; + +import { HistoricalTradingRewardsPeriods, HistoricalTradingReward } from '@/constants/abacus'; +import { STRING_KEYS, StringGetterFunction } from '@/constants/localization'; +import { useStringGetter, useTokenConfigs } from '@/hooks'; +import { layoutMixins } from '@/styles/layoutMixins'; + +import { AssetIcon } from '@/components/AssetIcon'; +import { Output, OutputType } from '@/components/Output'; +import { Table, TableCell, type ColumnDef } from '@/components/Table'; + +import { + getHistoricalTradingRewards, + getHistoricalTradingRewardsForPeriod, +} from '@/state/accountSelectors'; + +export enum TradingRewardHistoryTableColumnKey { + Event = 'Event', + Earned = 'Earned', +} + +const getTradingRewardHistoryTableColumnDef = ({ + key, + chainTokenLabel, + stringGetter, +}: { + key: TradingRewardHistoryTableColumnKey; + chainTokenLabel: string; + stringGetter: StringGetterFunction; +}): ColumnDef => ({ + ...( + { + [TradingRewardHistoryTableColumnKey.Event]: { + columnKey: TradingRewardHistoryTableColumnKey.Event, + getCellValue: (row) => row.startedAtInMilliseconds, + label: stringGetter({ key: STRING_KEYS.EVENT }), + renderCell: ({ startedAtInMilliseconds, endedAtInMilliseconds }) => ( + + {stringGetter({ key: STRING_KEYS.REWARDED })} + + {stringGetter({ + key: STRING_KEYS.FOR_TRADING, + params: { + PERIOD: ( + <> + + → + + + ), + }, + })} + + + ), + }, + [TradingRewardHistoryTableColumnKey.Earned]: { + columnKey: TradingRewardHistoryTableColumnKey.Earned, + getCellValue: (row) => row.amount, + label: stringGetter({ key: STRING_KEYS.EARNED }), + renderCell: ({ amount }) => ( + } + /> + ), + }, + } as Record> + )[key], +}); + +type ElementProps = { + columnKeys?: TradingRewardHistoryTableColumnKey[]; + period: HistoricalTradingRewardsPeriods; +}; + +type StyleProps = { + withOuterBorder?: boolean; + withInnerBorders?: boolean; +}; + +export const TradingRewardHistoryTable = ({ + period, + columnKeys = Object.values(TradingRewardHistoryTableColumnKey), + withOuterBorder, + withInnerBorders = true, +}: ElementProps & StyleProps) => { + const stringGetter = useStringGetter(); + const { chainTokenLabel } = useTokenConfigs(); + + const periodTradingRewards = useSelector( + getHistoricalTradingRewardsForPeriod(period.name), + shallowEqual + ); + + return ( + row.startedAtInMilliseconds} + columns={columnKeys.map((key: TradingRewardHistoryTableColumnKey) => + getTradingRewardHistoryTableColumnDef({ + key, + chainTokenLabel, + stringGetter, + }) + )} + slotEmpty={ +
{stringGetter({ key: STRING_KEYS.EMPTY_HISTORICAL_REWARDS_DESCRIPTION })}
+ } + selectionBehavior="replace" + withOuterBorder={withOuterBorder} + withInnerBorders={withInnerBorders} + initialNumRowsToShow={5} + withScrollSnapColumns + withScrollSnapRows + /> + ); +}; + +const Styled: Record = {}; + +Styled.Table = styled(Table)` + --tableCell-padding: 0.5rem 0; + --tableHeader-backgroundColor: var(--color-layer-3); + --tableRow-backgroundColor: var(--color-layer-3); + --tableViewMore-borderColor: var(--color-layer-3); + + tbody { + font: var(--font-medium-book); + } +`; + +Styled.Rewarded = styled.span` + color: var(--color-text-2); +`; + +Styled.TimePeriod = styled.div` + ${layoutMixins.inlineRow} + + && { + color: var(--color-text-0); + font: var(--font-base-book); + } + + output { + color: var(--color-text-1); + font: var(--font-base-book); + } +`; + +Styled.AssetIcon = styled(AssetIcon)` + margin-left: 0.5ch; +`;