-
Notifications
You must be signed in to change notification settings - Fork 144
Sponsored fees reserves #1335
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Sponsored fees reserves #1335
Changes from 2 commits
414d6da
60878e1
e3e27d1
a83c853
cb5a90c
2750ca4
6766a52
4f7f90c
9032e91
24db7ae
e5ca919
7f40865
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,65 @@ | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Account } from '../../shared/components/Account' | ||
| import { shortenAccount } from '../../shared/utils' | ||
| import type { AccountState } from '../../../rippled/accountState' | ||
| import './styles.scss' | ||
|
|
||
| interface Props { | ||
| account: AccountState | ||
| } | ||
|
|
||
| type ScopeKey = | ||
| | 'account_page_sponsored_scope_transaction_fees' | ||
| | 'account_page_sponsored_scope_base_reserve' | ||
|
|
||
| export const SponsoredFeesReserves = ({ account }: Props) => { | ||
| const { t } = useTranslation() | ||
|
|
||
| const rows: { scopeKey: ScopeKey; sponsor: string }[] = ( | ||
| [ | ||
| { | ||
| scopeKey: 'account_page_sponsored_scope_transaction_fees', | ||
| sponsor: account.sponsorship?.owner, | ||
| }, | ||
| { | ||
| scopeKey: 'account_page_sponsored_scope_base_reserve', | ||
| sponsor: account.info?.sponsor, | ||
| }, | ||
| ] as { scopeKey: ScopeKey; sponsor: string | undefined }[] | ||
| ).filter((row): row is { scopeKey: ScopeKey; sponsor: string } => | ||
| Boolean(row.sponsor), | ||
| ) | ||
|
|
||
| return ( | ||
| <div className="sponsored-fees-reserves-section"> | ||
| <h2 className="sponsored-fees-reserves-title"> | ||
| {t('account_page_sponsored_fees_reserves_title')} | ||
| </h2> | ||
| <div className="sponsored-fees-reserves-table-wrapper"> | ||
| <table className="sponsored-fees-reserves-table"> | ||
| <thead> | ||
| <tr> | ||
| <th>{t('account_page_sponsored_scope')}</th> | ||
|
kuan121 marked this conversation as resolved.
|
||
| <th>{t('account_page_sponsored_by')}</th> | ||
| <th>{t('status')}</th> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {rows.map(({ scopeKey, sponsor }) => ( | ||
| <tr key={scopeKey}> | ||
| <td>{t(scopeKey)}</td> | ||
| <td> | ||
| <Account | ||
| account={sponsor} | ||
| displayText={shortenAccount(sponsor)} | ||
| /> | ||
| </td> | ||
| <td>{t('account_page_sponsored_status_active')}</td> | ||
| </tr> | ||
| ))} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| @use '../../shared/css/variables' as *; | ||
|
|
||
| .sponsored-fees-reserves-section { | ||
| padding: 24px 0; | ||
|
|
||
| .sponsored-fees-reserves-title { | ||
| @include bold; | ||
|
|
||
| margin: 0 0 20px; | ||
| font-size: 20px; | ||
| } | ||
| } | ||
|
|
||
| .sponsored-fees-reserves-table-wrapper { | ||
| -webkit-overflow-scrolling: touch; | ||
| overflow-x: auto; | ||
| } | ||
|
|
||
| .sponsored-fees-reserves-table { | ||
| width: 100%; | ||
| border-collapse: collapse; | ||
|
|
||
| thead th { | ||
| @include semibold; | ||
|
|
||
| padding: 10px 12px; | ||
| color: $black-50; | ||
| font-size: 12px; | ||
| text-align: left; | ||
| text-transform: uppercase; | ||
| } | ||
|
|
||
| tbody td { | ||
| padding: 10px 12px; | ||
| border-bottom: 1px solid $black-80; | ||
| color: $white; | ||
| font-size: 14px; | ||
| } | ||
|
|
||
| tbody tr:first-child td { | ||
| border-top: 1px solid $black-80; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,107 @@ | ||
| import { render, screen } from '@testing-library/react' | ||
| import { I18nextProvider } from 'react-i18next' | ||
| import { BrowserRouter as Router } from 'react-router' | ||
| import i18n from '../../../../i18n/testConfigEnglish' | ||
| import { SponsoredFeesReserves } from '../index' | ||
| import type { AccountState } from '../../../../rippled/accountState' | ||
|
|
||
| jest.mock('../../../shared/components/Account', () => ({ | ||
| Account: ({ account }: { account: string }) => ( | ||
| <span data-testid="account-component">{account}</span> | ||
| ), | ||
| })) | ||
|
|
||
| const TestWrapper = ({ children }: { children: React.ReactNode }) => ( | ||
| <I18nextProvider i18n={i18n}> | ||
| <Router>{children}</Router> | ||
| </I18nextProvider> | ||
| ) | ||
|
|
||
| const baseAccount: AccountState = { | ||
| account: 'rAccount1111111111111111111111111', | ||
| info: { ticketCount: 0, flags: [] }, | ||
| deleted: false, | ||
| } | ||
|
|
||
| describe('SponsoredFeesReserves Component', () => { | ||
| it('renders no rows when the account has no sponsorship', () => { | ||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={baseAccount} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| expect(screen.getByText('Sponsored fees & reserves')).toBeInTheDocument() | ||
| expect(screen.queryByText('Transaction Fees')).not.toBeInTheDocument() | ||
| expect(screen.queryByText('Base Reserve')).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('renders only the Base Reserve row when only the reserve is sponsored', () => { | ||
| const account: AccountState = { | ||
| ...baseAccount, | ||
| info: { | ||
| ...baseAccount.info, | ||
| sponsor: 'rBaseReserveSponsor11111111111111', | ||
| }, | ||
| } | ||
|
|
||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={account} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| expect(screen.getByText('Base Reserve')).toBeInTheDocument() | ||
| expect(screen.queryByText('Transaction Fees')).not.toBeInTheDocument() | ||
| expect(screen.getByTestId('account-component')).toHaveTextContent( | ||
| 'rBaseReserveSponsor11111111111111', | ||
| ) | ||
| }) | ||
|
|
||
| it('renders only the Transaction Fees row when only fees are sponsored', () => { | ||
| const account: AccountState = { | ||
| ...baseAccount, | ||
| sponsorship: { | ||
| owner: 'rFeeSponsor2222222222222222222222', | ||
| sponsee: baseAccount.account, | ||
| }, | ||
| } | ||
|
|
||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={account} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| expect(screen.getByText('Transaction Fees')).toBeInTheDocument() | ||
| expect(screen.queryByText('Base Reserve')).not.toBeInTheDocument() | ||
| expect(screen.getByTestId('account-component')).toHaveTextContent( | ||
| 'rFeeSponsor2222222222222222222222', | ||
| ) | ||
| }) | ||
|
|
||
| it('renders both rows when both fees and reserve are sponsored', () => { | ||
| const account: AccountState = { | ||
| ...baseAccount, | ||
| info: { | ||
| ...baseAccount.info, | ||
| sponsor: 'rBaseReserveSponsor11111111111111', | ||
| }, | ||
| sponsorship: { | ||
| owner: 'rFeeSponsor2222222222222222222222', | ||
| sponsee: baseAccount.account, | ||
| }, | ||
| } | ||
|
|
||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={account} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| expect(screen.getByText('Transaction Fees')).toBeInTheDocument() | ||
| expect(screen.getByText('Base Reserve')).toBeInTheDocument() | ||
| expect(screen.getAllByTestId('account-component')).toHaveLength(2) | ||
| expect(screen.getAllByText('Active')).toHaveLength(2) | ||
| }) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -27,6 +27,14 @@ const formatPaychannel = (d: any) => ({ | |
| settleDelay: d.SettleDelay, | ||
| }) | ||
|
|
||
| const formatSponsorship = (d: any) => ({ | ||
| owner: d.Owner, | ||
| sponsee: d.Sponsee, | ||
| feeAmount: d.FeeAmount, | ||
| maxFee: d.MaxFee, | ||
| reserveCount: d.ReserveCount, | ||
|
kuan121 marked this conversation as resolved.
Outdated
|
||
| }) | ||
|
|
||
| const executeQuery = async ( | ||
| rippledSocket: XrplClient, | ||
| params: any, | ||
|
|
@@ -366,6 +374,45 @@ const getAccountBridges = async ( | |
| return undefined | ||
| } | ||
|
|
||
| // get the sponsorship covering this account's fees/reserves, if any | ||
| const getAccountSponsorship = async ( | ||
| rippledSocket: ExplorerXrplClient, | ||
| account: string, | ||
| ledgerIndex: string | number = 'validated', | ||
| ): Promise<any> => { | ||
| const resp = await query(rippledSocket, { | ||
| command: 'account_objects', | ||
| account, | ||
| ledger_index: ledgerIndex, | ||
| type: 'sponsorship', | ||
| limit: 400, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The For example, if an account has 450 objects and its sponsorship objects happen to be the 405th and 420th objects, a request with limit: 400 could return zero sponsorship objects. I think we’ll need to loop through the paginated results, either until we’ve examined all account objects or until we reach a predefined cap, such as 4,000 objects.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| }) | ||
| if (resp.error === 'actNotFound') { | ||
| throw new Error('account not found', 404) | ||
| } | ||
| if (resp.error === 'invalidParams') { | ||
| // thrown when the Sponsorship amendment is not activated | ||
| // TODO: remove this when XLS-68 is live in mainnet | ||
| return undefined | ||
| } | ||
|
|
||
| if (resp.error_message) { | ||
| throw new Error(resp.error_message, 500) | ||
| } | ||
|
|
||
| if (!resp.account_objects.length) { | ||
| return undefined | ||
| } | ||
|
|
||
| // A Sponsorship object is linked into both the sponsor's and sponsee's | ||
| // owner directories, so only keep the one where this account is sponsored. | ||
| const sponsorship = resp.account_objects.find( | ||
| (d: any) => d.Sponsee === account, | ||
| ) | ||
|
|
||
| return sponsorship ? formatSponsorship(sponsorship) : undefined | ||
| } | ||
|
|
||
| // get Token balance summary | ||
| const getBalances = async ( | ||
| rippledSocket: ExplorerXrplClient, | ||
|
|
@@ -929,6 +976,7 @@ export { | |
| getAccountEscrows, | ||
| getAccountPaychannels, | ||
| getAccountBridges, | ||
| getAccountSponsorship, | ||
| getAccountNFTs, | ||
| getAccountObjects, | ||
| getNFTsIssuedByAccount, | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.