-
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 8 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 |
|---|---|---|
|
|
@@ -730,6 +730,12 @@ | |
| "account_page_payment_channels": "Payment Channels", | ||
| "account_page_payment_channels_text": "{{currency}} available in {{number}} channel(s)", | ||
| "account_page_nft_minter": "NFT Minter", | ||
| "account_page_sponsored_fees_reserves_title": "Sponsored Fees & Reserves", | ||
| "account_page_sponsored_scope": "Scope", | ||
| "account_page_sponsored_by": "Sponsored by", | ||
| "account_page_sponsored_scope_transaction_fees": "Transaction Fees", | ||
| "account_page_sponsored_scope_base_reserve": "Base Reserve", | ||
| "account_page_sponsored_none": "No Sponsors", | ||
| "account_page_asset_held_title": "Assets Held", | ||
| "account_page_asset_issued_title": "Assets Issued", | ||
| "account_page_asset_tab_iou": "IOUs ({{count}})", | ||
|
|
@@ -946,5 +952,27 @@ | |
| "registered": "Registered", | ||
| "included": "Included", | ||
| "maintenance_banner.notice": "Scheduled maintenance: {{window}} (~{{duration}} min downtime).", | ||
| "maintenance_banner.countdown_prefix": "Starts in" | ||
| "maintenance_banner.countdown_prefix": "Starts in", | ||
| "sponsor": "Sponsor", | ||
| "sponsee": "Sponsee", | ||
| "new_sponsor": "New Sponsor", | ||
| "fee_amount": "Fee Amount", | ||
| "max_fee": "Max Fee", | ||
| "reserve_count": "Reserve Count", | ||
| "require_sign_for_fee": "Require Sign For Fee", | ||
| "require_sign_for_reserve": "Require Sign For Reserve", | ||
| "sponsorship_deleted": "Sponsorship Deleted", | ||
| "operation": "Operation", | ||
| "object_id": "Object ID", | ||
| "sponsorship_operation_create": "Create", | ||
| "sponsorship_operation_reassign": "Reassign", | ||
| "sponsorship_operation_end": "End", | ||
| "sponsorship_set_description": "<Sponsor/> sponsors transaction fees for <Sponsee/>", | ||
|
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. Only
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. There is now a branch to account for whether |
||
| "sponsorship_set_delete": "<Sponsor/> ends the fee sponsorship for <Sponsee/>", | ||
| "sponsorship_transfer_create": "<Account/> assigns <Sponsor/> as its reserve sponsor", | ||
| "sponsorship_transfer_reassign": "<Account/> reassigns reserve sponsorship to <Sponsor/>", | ||
| "sponsorship_transfer_end_other": "<Account/> ends reserve sponsorship for <Sponsee/>", | ||
| "sponsorship_transfer_end_self": "<Account/> ends its reserve sponsorship", | ||
| "sponsor_detail": "<Account/> is sponsoring this transaction", | ||
| "sponsor_co_signed": "The sponsor co-signed this transaction" | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| import { useTranslation } from 'react-i18next' | ||
| import { Account } from '../../shared/components/Account' | ||
| import { CollapsibleSection } from '../../shared/components/CollapsibleSection' | ||
| import { EmptyMessageTableRow } from '../../shared/EmptyMessageTableRow' | ||
| 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 }[] = [ | ||
| ...(account.sponsorship ?? []).map(({ owner }) => ({ | ||
| scopeKey: 'account_page_sponsored_scope_transaction_fees' as ScopeKey, | ||
| sponsor: owner, | ||
|
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. Should we check the values of FeeAmount and RemainingOwnerCount before claiming that the sponsor is sponsoring both fees and reserves? For example, if FeeAmount is 0 or RemainingOwnerCount is 0, do we still want to say that the sponsor is sponsoring both?
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. Added a Number(feeAmount) > 0 filter before rendering a |
||
| })), | ||
| ...(account.info?.sponsor | ||
| ? [ | ||
| { | ||
| scopeKey: 'account_page_sponsored_scope_base_reserve' as ScopeKey, | ||
| sponsor: account.info.sponsor, | ||
| }, | ||
| ] | ||
| : []), | ||
| ] | ||
|
|
||
| return ( | ||
| <CollapsibleSection | ||
| title={t('account_page_sponsored_fees_reserves_title')} | ||
| ariaLabel="Toggle sponsored fees & reserves section" | ||
| className="sponsored-fees-reserves-section" | ||
| defaultOpen={false} | ||
| > | ||
| <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> | ||
| </tr> | ||
| </thead> | ||
| <tbody> | ||
| {rows.length === 0 ? ( | ||
| <EmptyMessageTableRow colSpan={2}> | ||
| {t('account_page_sponsored_none')} | ||
| </EmptyMessageTableRow> | ||
| ) : ( | ||
| rows.map(({ scopeKey, sponsor }) => ( | ||
| <tr key={`${scopeKey}-${sponsor}`}> | ||
| <td>{t(scopeKey)}</td> | ||
| <td> | ||
| <Account account={sponsor} /> | ||
| </td> | ||
| </tr> | ||
| )) | ||
| )} | ||
| </tbody> | ||
| </table> | ||
| </div> | ||
| </CollapsibleSection> | ||
| ) | ||
| } | ||
| 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-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; | ||
|
|
||
| &.empty-message { | ||
| padding: 16px; | ||
| color: $black-40; | ||
| font-size: 16px; | ||
| text-align: center; | ||
| } | ||
| } | ||
|
|
||
| tbody tr:first-child td { | ||
| border-top: 1px solid $black-80; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,168 @@ | ||
| import { render, screen, fireEvent } 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, | ||
| } | ||
|
|
||
| const openSection = () => { | ||
| fireEvent.click( | ||
| screen.getByLabelText('Toggle sponsored fees & reserves section'), | ||
| ) | ||
| } | ||
|
|
||
| describe('SponsoredFeesReserves Component', () => { | ||
| it('renders the title and starts collapsed', () => { | ||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={baseAccount} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| expect(screen.getByText('Sponsored Fees & Reserves')).toBeInTheDocument() | ||
| expect(screen.queryByText('No Sponsors')).not.toBeInTheDocument() | ||
| }) | ||
|
|
||
| it('shows "No Sponsors" when the account has no sponsorship', () => { | ||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={baseAccount} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| openSection() | ||
|
|
||
| expect(screen.getByText('No Sponsors')).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>, | ||
| ) | ||
|
|
||
| openSection() | ||
|
|
||
| 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>, | ||
| ) | ||
|
|
||
| openSection() | ||
|
|
||
| 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>, | ||
| ) | ||
|
|
||
| openSection() | ||
|
|
||
| expect(screen.getByText('Transaction Fees')).toBeInTheDocument() | ||
| expect(screen.getByText('Base Reserve')).toBeInTheDocument() | ||
| expect(screen.getAllByTestId('account-component')).toHaveLength(2) | ||
| }) | ||
|
|
||
| it('renders one Transaction Fees row per sponsor when there are multiple fee sponsors', () => { | ||
| const account: AccountState = { | ||
| ...baseAccount, | ||
| sponsorship: [ | ||
| { | ||
| owner: 'rFeeSponsor2222222222222222222222', | ||
| sponsee: baseAccount.account, | ||
| }, | ||
| { | ||
| owner: 'rFeeSponsor3333333333333333333333', | ||
| sponsee: baseAccount.account, | ||
| }, | ||
| ], | ||
| } | ||
|
|
||
| render( | ||
| <TestWrapper> | ||
| <SponsoredFeesReserves account={account} /> | ||
| </TestWrapper>, | ||
| ) | ||
|
|
||
| openSection() | ||
|
|
||
| expect(screen.getAllByText('Transaction Fees')).toHaveLength(2) | ||
| expect(screen.getAllByTestId('account-component')).toHaveLength(2) | ||
| expect(screen.getAllByTestId('account-component')[0]).toHaveTextContent( | ||
| 'rFeeSponsor2222222222222222222222', | ||
| ) | ||
| expect(screen.getAllByTestId('account-component')[1]).toHaveTextContent( | ||
| 'rFeeSponsor3333333333333333333333', | ||
| ) | ||
| }) | ||
| }) |
Uh oh!
There was an error while loading. Please reload this page.