Repository: ripple/explorer (ts)

Issue:
Add `Simple` view for SetFee pseudotransactions
https://livenet.xrpl.org/transactions/5922A0BA30621C60B2B6DDBC3FF6B5BB509EB3685C4C3D56696A9FE4FE6D48A3

Pull request description:
feat: add SetFee transaction support
## High Level Overview of Change

Adds Simple and Description views for SetFee and includes support for the XRPFees amendment.

Closes #28

### Type of Change

- [x] New feature (non-breaking change which adds functionality)

## Before
![Screenshot 2023-02-28 at 3 42 31 PM](https://user-images.githubusercontent.com/464895/221987686-251a2bd6-3501-4a7c-8844-d9352e978924.png)

![Screenshot 2023-02-28 at 3 42 37 PM](https://user-images.githubusercontent.com/464895/221987705-8844b4a3-dbf3-48b8-9693-caaa983b56c0.png)

## After
![Screenshot 2023-02-28 at 3 43 26 PM](https://user-images.githubusercontent.com/464895/221987892-4a4d6b3b-f7f8-4c2a-a408-3059dda6c65e.png)

![Screenshot 2023-02-28 at 3 43 55 PM](https://user-images.githubusercontent.com/464895/221987947-f0e177f4-1069-43f3-9ede-20363fcf6341.png)






Patch:
diff --git a/public/locales/en-US/translations.json b/public/locales/en-US/translations.json
index 85a6517d..6e7363c0 100644
--- a/public/locales/en-US/translations.json
+++ b/public/locales/en-US/translations.json
@@ -131,6 +131,12 @@
   "paychannel_node_line1": "It <1><0>{{action}}</0></1> a PayChannel node from <3><0>{{account}}</0></3> to <5><0>{{counterAccount}}</0></5>",
   "paychannel_amount_changed": "Amount changed by <1><0>{{difference}}</0><1><0>{{currency}}</0></1></1> from <3><0>{{previous}}</0><1><0>{{currency}}</0></1></3> to <5><0>{{final}}</0><1><0>{{currency}}</0></1></5>",
   "paychannel_balance_changed": "Balance changed by <1><0>{{difference}}</0><1><0>{{currency}}</0></1></1> from <3><0>{{previous}}</0><1><0>{{currency}}</0></1></3> to <5><0>{{final}}</0><1><0>{{currency}}</0></1></5>",
+  "setfee_fees_description": "Future transactions will require a minimum fee of <amount />.",
+  "setfee_reserves_description": "Accounts must now hold a base of <base /> and an additional <increment /> for each additional object that account owns.",
+  "setfee_docs_description": "Visit the docs: <0>Fees</0>",
+  "setfee_base_fee": "Base Fee",
+  "setfee_reserve": "Reserve",
+  "setfee_reserve_increment": "Reserve Increment",
   "formatted_date": "Date/Time ({{timeZone}})",
   "transaction_AMMCreate": "AMM Create",
   "transaction_AMMDeposit": "AMM Deposit",
diff --git a/src/containers/shared/components/Transaction/SetFee/Description.tsx b/src/containers/shared/components/Transaction/SetFee/Description.tsx
new file mode 100644
index 00000000..874cb16d
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/Description.tsx
@@ -0,0 +1,37 @@
+import { Trans } from 'react-i18next'
+import { TransactionDescriptionProps } from '../types'
+import { parser } from './parser'
+import { Amount } from '../../Amount'
+
+export const Description = ({ data }: TransactionDescriptionProps) => {
+  const { fee, reserve, increment } = parser(data.tx)
+
+  return (
+    <>
+      <div data-test="fees-line">
+        <Trans
+          i18nKey="setfee_fees_description"
+          components={{
+            amount: <Amount value={fee} />,
+          }}
+        />
+      </div>
+      <div data-test="reserves-line">
+        <Trans
+          i18nKey="setfee_reserves_description"
+          components={{
+            base: <Amount value={reserve} />,
+            increment: <Amount value={increment} />,
+          }}
+        />
+      </div>
+      <br />
+      <div data-test="documentation-line">
+        <Trans
+          i18nKey="setfee_docs_description"
+          components={[<a href="https://xrpl.org/fees.html">Fees</a>]}
+        />
+      </div>
+    </>
+  )
+}
diff --git a/src/containers/shared/components/Transaction/SetFee/Simple.tsx b/src/containers/shared/components/Transaction/SetFee/Simple.tsx
new file mode 100644
index 00000000..207ad1c3
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/Simple.tsx
@@ -0,0 +1,29 @@
+import { useTranslation } from 'react-i18next'
+import { Amount } from '../../Amount'
+import { SimpleRow } from '../SimpleRow'
+import { TransactionSimpleProps } from '../types'
+import { SetFeeInstructions } from './types'
+
+export const Simple = ({
+  data,
+}: TransactionSimpleProps<SetFeeInstructions>) => {
+  const { t } = useTranslation()
+  const { fee, reserve, increment } = data.instructions
+
+  return (
+    <>
+      <SimpleRow label={t('setfee_base_fee')} data-test="base-fee">
+        <Amount value={fee} />
+      </SimpleRow>
+      <SimpleRow label={t('setfee_reserve')} data-test="reserve">
+        <Amount value={reserve} />
+      </SimpleRow>
+      <SimpleRow
+        label={t('setfee_reserve_increment')}
+        data-test="reserve-increment"
+      >
+        <Amount value={increment} />
+      </SimpleRow>
+    </>
+  )
+}
diff --git a/src/containers/shared/components/Transaction/SetFee/index.ts b/src/containers/shared/components/Transaction/SetFee/index.ts
new file mode 100644
index 00000000..6f799f5f
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/index.ts
@@ -0,0 +1,17 @@
+import {
+  TransactionAction,
+  TransactionCategory,
+  TransactionMapping,
+} from '../types'
+
+import { Simple } from './Simple'
+import { parser } from './parser'
+import { Description } from './Description'
+
+export const SetFeeTransaction: TransactionMapping = {
+  Description,
+  Simple,
+  action: TransactionAction.MODIFY,
+  category: TransactionCategory.ACCOUNT,
+  parser,
+}
diff --git a/src/containers/shared/components/Transaction/SetFee/parser.ts b/src/containers/shared/components/Transaction/SetFee/parser.ts
new file mode 100644
index 00000000..a6ae4c1a
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/parser.ts
@@ -0,0 +1,17 @@
+import { isPreAmendment, SetFee, SetFeeInstructions } from './types'
+
+export const parser = (tx: SetFee): SetFeeInstructions => {
+  if (isPreAmendment(tx)) {
+    return {
+      fee: parseInt(tx.BaseFee, 16).toString(),
+      reserve: tx.ReserveBase.toString(),
+      increment: tx.ReserveIncrement.toString(),
+    }
+  }
+
+  return {
+    fee: tx.BaseFeeDrops,
+    reserve: tx.ReserveBaseDrops,
+    increment: tx.ReserveIncrementDrops,
+  }
+}
diff --git a/src/containers/shared/components/Transaction/SetFee/types.ts b/src/containers/shared/components/Transaction/SetFee/types.ts
new file mode 100644
index 00000000..c6596ad5
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/types.ts
@@ -0,0 +1,35 @@
+import { TransactionCommonFields } from '../types'
+
+export interface SetFeePreAmendment extends TransactionCommonFields {
+  BaseFee: string
+  ReferenceFeeUnits: number
+  ReserveBase: number
+  ReserveIncrement: number
+
+  BaseFeeDrops?: never
+  ReserveBaseDrops?: never
+  ReserveIncrementDrops?: never
+}
+
+export interface SetFeePostAmendment extends TransactionCommonFields {
+  BaseFee?: never
+  ReferenceFeeUnits?: never
+  ReserveBase?: never
+  ReserveIncrement?: never
+
+  BaseFeeDrops: string
+  ReserveBaseDrops: string
+  ReserveIncrementDrops: string
+}
+
+export const isPreAmendment = (
+  tx: SetFeePreAmendment | SetFeePostAmendment,
+): tx is SetFeePreAmendment => (tx as SetFeePostAmendment).BaseFee !== undefined
+
+export type SetFee = SetFeePreAmendment | SetFeePostAmendment
+
+export interface SetFeeInstructions {
+  fee: string
+  reserve: string
+  increment: string
+}
diff --git a/src/containers/shared/components/Transaction/index.ts b/src/containers/shared/components/Transaction/index.ts
index 71197bf4..8ae8ba7b 100644
--- a/src/containers/shared/components/Transaction/index.ts
+++ b/src/containers/shared/components/Transaction/index.ts
@@ -18,6 +18,7 @@ import { PaymentTransaction as Payment } from './Payment'
 import { PaymentChannelClaimTransaction as PaymentChannelClaim } from './PaymentChannelClaim'
 import { PaymentChannelCreateTransaction as PaymentChannelCreate } from './PaymentChannelCreate'
 import { PaymentChannelFundTransaction as PaymentChannelFund } from './PaymentChannelFund'
+import { SetFeeTransaction as SetFee } from './SetFee'
 import { SetRegularKeyTransaction as SetRegularKey } from './SetRegularKey'
 import { SignerListSetTransaction as SignerListSet } from './SignerListSet'
 import { XChainAccountCreateCommitTransaction as XChainAccountCreateCommit } from './XChainAccountCreateCommit'
@@ -57,6 +58,7 @@ export const transactionTypes: { [key: string]: TransactionMapping } = {
   PaymentChannelCreate,
   PaymentChannelClaim,
   PaymentChannelFund,
+  SetFee,
   SetRegularKey,
   SignerListSet,
   XChainAccountCreateCommit,


Test patch:
diff --git a/src/containers/shared/components/Transaction/SetFee/test/SetFeeDescription.test.tsx b/src/containers/shared/components/Transaction/SetFee/test/SetFeeDescription.test.tsx
new file mode 100644
index 00000000..138745fc
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/test/SetFeeDescription.test.tsx
@@ -0,0 +1,37 @@
+import { createDescriptionWrapperFactory } from '../../test'
+import { Description } from '../Description'
+import i18nTestConfigEnUS from '../../../../../../i18nTestConfig.en-US'
+
+import SetFeePreAmendment from './mock_data/SetFee_PreAmendment.json'
+import SetFeePostAmendment from './mock_data/SetFee_PostAmendment.json'
+
+const createWrapper = createDescriptionWrapperFactory(
+  Description,
+  i18nTestConfigEnUS,
+)
+
+function testDescription(wrapper) {
+  expect(wrapper.find('[data-test="fees-line"]')).toHaveText(
+    `Future transactions will require a minimum fee of \uE9000.00001 XRP.`,
+  )
+  expect(wrapper.find('[data-test="reserves-line"]')).toHaveText(
+    `Accounts must now hold a base of \uE90010.00 XRP and an additional \uE9002.00 XRP for each additional object that account owns.`,
+  )
+  expect(wrapper.find('[data-test="documentation-line"]')).toHaveText(
+    `Visit the docs: Fees`,
+  )
+}
+
+describe('SetFee: Description', () => {
+  it('renders Description for transaction before XRPFees amendment', () => {
+    const wrapper = createWrapper(SetFeePreAmendment)
+    testDescription(wrapper)
+    wrapper.unmount()
+  })
+
+  it('renders Description for transaction after XRPFees amendment', () => {
+    const wrapper = createWrapper(SetFeePostAmendment)
+    testDescription(wrapper)
+    wrapper.unmount()
+  })
+})
diff --git a/src/containers/shared/components/Transaction/SetFee/test/SetFeeSimple.test.tsx b/src/containers/shared/components/Transaction/SetFee/test/SetFeeSimple.test.tsx
new file mode 100644
index 00000000..573bb494
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/test/SetFeeSimple.test.tsx
@@ -0,0 +1,35 @@
+import {
+  createSimpleWrapperFactory,
+  expectSimpleRowLabel,
+  expectSimpleRowText,
+} from '../../test'
+import { Simple } from '../Simple'
+import i18nTestConfigEnUS from '../../../../../../i18nTestConfig.en-US'
+
+import SetFeePreAmendment from './mock_data/SetFee_PreAmendment.json'
+import SetFeePostAmendment from './mock_data/SetFee_PostAmendment.json'
+
+const createWrapper = createSimpleWrapperFactory(Simple, i18nTestConfigEnUS)
+
+function testSimple(wrapper) {
+  expectSimpleRowLabel(wrapper, `base-fee`, 'Base Fee')
+  expectSimpleRowText(wrapper, `base-fee`, `\uE9000.00001 XRP`)
+  expectSimpleRowLabel(wrapper, `reserve`, 'Reserve')
+  expectSimpleRowText(wrapper, `reserve`, `\uE90010.00 XRP`)
+  expectSimpleRowLabel(wrapper, `reserve-increment`, 'Reserve Increment')
+  expectSimpleRowText(wrapper, `reserve-increment`, `\uE9002.00 XRP`)
+}
+
+describe('SetFee: Simple', () => {
+  it('renders Simple for transaction before XRPFees amendment', () => {
+    const wrapper = createWrapper(SetFeePreAmendment)
+    testSimple(wrapper)
+    wrapper.unmount()
+  })
+
+  it('renders Simple for transaction after XRPFees amendment', () => {
+    const wrapper = createWrapper(SetFeePostAmendment)
+    testSimple(wrapper)
+    wrapper.unmount()
+  })
+})
diff --git a/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PostAmendment.json b/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PostAmendment.json
new file mode 100644
index 00000000..87325210
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PostAmendment.json
@@ -0,0 +1,46 @@
+{
+  "tx": {
+    "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
+    "BaseFeeDrops": "10",
+    "Fee": "0",
+    "LedgerSequence": 66462465,
+    "ReserveBaseDrops": "10000000",
+    "ReserveIncrementDrops": "2000000",
+    "Sequence": 0,
+    "SigningPubKey": "",
+    "TransactionType": "SetFee",
+    "date": 1632045932000,
+    "warnings": [
+      {
+        "id": 1004,
+        "message": "This is a reporting server.  The default behavior of a reporting server is to only return validated data. If you are looking for not yet validated data, include \"ledger_index : current\" in your request, which will cause this server to forward the request to a p2p node. If the forward is successful the response will include \"forwarded\" : \"true\""
+      }
+    ]
+  },
+  "meta": {
+    "AffectedNodes": [
+      {
+        "ModifiedNode": {
+          "FinalFields": {
+            "BaseFeeDrops": "10",
+            "Flags": 0,
+            "ReferenceFeeUnits": 10,
+            "ReserveBaseDrops": "10000000",
+            "ReserveIncrementDrops": "2000000"
+          },
+          "LedgerEntryType": "FeeSettings",
+          "LedgerIndex": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651",
+          "PreviousFields": {
+            "ReserveBaseDrops": "20000000",
+            "ReserveIncrementDrops": "5000000"
+          }
+        }
+      }
+    ],
+    "TransactionIndex": 28,
+    "TransactionResult": "tesSUCCESS"
+  },
+  "hash": "5922A0BA30621C60B2B6DDBC3FF6B5BB509EB3685C4C3D56696A9FE4FE6D48A3",
+  "ledger_index": 66462465,
+  "date": 1632045932000
+}
diff --git a/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PreAmendment.json b/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PreAmendment.json
new file mode 100644
index 00000000..76fded3d
--- /dev/null
+++ b/src/containers/shared/components/Transaction/SetFee/test/mock_data/SetFee_PreAmendment.json
@@ -0,0 +1,47 @@
+{
+  "tx": {
+    "Account": "rrrrrrrrrrrrrrrrrrrrrhoLvTp",
+    "BaseFee": "a",
+    "Fee": "0",
+    "LedgerSequence": 66462465,
+    "ReferenceFeeUnits": 10,
+    "ReserveBase": 10000000,
+    "ReserveIncrement": 2000000,
+    "Sequence": 0,
+    "SigningPubKey": "",
+    "TransactionType": "SetFee",
+    "date": 1632045932000,
+    "warnings": [
+      {
+        "id": 1004,
+        "message": "This is a reporting server.  The default behavior of a reporting server is to only return validated data. If you are looking for not yet validated data, include \"ledger_index : current\" in your request, which will cause this server to forward the request to a p2p node. If the forward is successful the response will include \"forwarded\" : \"true\""
+      }
+    ]
+  },
+  "meta": {
+    "AffectedNodes": [
+      {
+        "ModifiedNode": {
+          "FinalFields": {
+            "BaseFee": "a",
+            "Flags": 0,
+            "ReferenceFeeUnits": 10,
+            "ReserveBase": 10000000,
+            "ReserveIncrement": 2000000
+          },
+          "LedgerEntryType": "FeeSettings",
+          "LedgerIndex": "4BC50C9B0D8515D3EAAE1E74B29A95804346C491EE1A95BF25E4AAB854A6A651",
+          "PreviousFields": {
+            "ReserveBase": 20000000,
+            "ReserveIncrement": 5000000
+          }
+        }
+      }
+    ],
+    "TransactionIndex": 28,
+    "TransactionResult": "tesSUCCESS"
+  },
+  "hash": "5922A0BA30621C60B2B6DDBC3FF6B5BB509EB3685C4C3D56696A9FE4FE6D48A3",
+  "ledger_index": 66462465,
+  "date": 1632045932000
+}