Skip to content

Commit

Permalink
feat: support Clawback transaction (#764)
Browse files Browse the repository at this point in the history
If the tx is a success, the _actual_ amount that was clawed back is
returned and shown in all components.

If the tx failed, the _attempted_ amount is shown in all components.

### Context of Change

Clawback transaction. See
https://github.com/XRPLF/XRPL-Standards/pull/104/files?short_path=cb82c33#diff-cb82c337d579659136224b35dbbaac97d0ffaa3cfaa79981f1bebf4f4888feb5
  • Loading branch information
shawnxie999 authored Jul 10, 2023
1 parent 345de6a commit a8b5944
Show file tree
Hide file tree
Showing 16 changed files with 415 additions and 30 deletions.
7 changes: 6 additions & 1 deletion public/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -453,5 +453,10 @@
"language_ja-JP": "Japanese",
"xchain_account_claim_count": "XChain Account Claim Count",
"xchain_account_create_count": "XChain Account Create Count",
"min_signer_quorum": "Minimum weight <0>{{quorum}}</0> required"
"min_signer_quorum": "Minimum weight <0>{{quorum}}</0> required",
"holder": "Holder",
"action_from": "<0><0>{{action}}</0></0> <1><0>{{amount}}</0></1> from <3><0>{{destination}}</0></3>",
"claws_back": "Claws back",
"claws_back_from": "<source/> claws back from <destination/>",
"instruct_to_claw": "The max clawback amount is <amount/>"
}
32 changes: 10 additions & 22 deletions src/containers/Transactions/Meta/RippleState.jsx
Original file line number Diff line number Diff line change
@@ -1,29 +1,17 @@
import { Trans } from 'react-i18next'
import { CURRENCY_OPTIONS } from '../../shared/transactionUtils'
import { localizeNumber } from '../../shared/utils'
import { localizeNumber, computeBalanceChange } from '../../shared/utils'
import { Account } from '../../shared/components/Account'

const render = (t, language, action, node, index) => {
const fields = node.FinalFields || node.NewFields
const prev = node.PreviousFields
const { currency } = fields.Balance
const numberOption = { ...CURRENCY_OPTIONS, currency }
let finalBalance = fields.Balance.value
let previousBalance = prev && prev.Balance ? prev.Balance.value : 0
let account
let counterAccount

if (finalBalance < 0) {
account = fields.HighLimit.issuer
counterAccount = fields.LowLimit.issuer
finalBalance = 0 - finalBalance
previousBalance = 0 - previousBalance
} else {
account = fields.LowLimit.issuer
counterAccount = fields.HighLimit.issuer
}

const change = finalBalance - previousBalance
const {
change,
numberOption,
previousBalance,
finalBalance,
currency,
account,
counterAccount,
} = computeBalanceChange(node)

const line1 = (
<Trans i18nKey="transaction_balance_line_one">
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { Trans } from 'react-i18next'
import { TransactionDescriptionProps } from '../types'
import { Amount } from '../../Amount'
import { Account } from '../../Account'
import { formatAmount } from '../../../../../rippled/lib/txSummary/formatAmount'

export const Description = ({ data }: TransactionDescriptionProps) => {
const issuer = data.tx.Account
const holder = data.tx.Amount.issuer
const amount = data.tx.Amount
amount.issuer = issuer
return (
<>
<div data-test="from-to-line">
<Trans
i18nKey="claws_back_from"
components={{
source: <Account account={issuer} />,
destination: <Account account={holder} />,
}}
/>
</div>
<div data-test="amount-line">
<Trans
i18nKey="instruct_to_claw"
components={{
amount: <Amount value={formatAmount(amount)} />,
}}
/>
</div>
</>
)
}
28 changes: 28 additions & 0 deletions src/containers/shared/components/Transaction/Clawback/Simple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
import { useTranslation } from 'react-i18next'
import { SimpleRow } from '../SimpleRow'
import { TransactionSimpleComponent, TransactionSimpleProps } from '../types'
import { ClawbackInstructions } from './types'
import { Account } from '../../Account'
import { Amount } from '../../Amount'

export const Simple: TransactionSimpleComponent = ({
data,
}: TransactionSimpleProps<ClawbackInstructions>) => {
const { amount, holder } = data.instructions
const { t } = useTranslation()

return (
<>
{holder && (
<SimpleRow label={t('holder')} data-test="holder">
<Account account={holder} />
</SimpleRow>
)}
{amount && (
<SimpleRow label={t('amount')} data-test="amount">
<Amount value={amount} displayIssuer />
</SimpleRow>
)}
</>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { useTranslation, Trans } from 'react-i18next'
import { Amount } from '../../Amount'
import { TransactionTableDetailProps } from '../types'
import { ClawbackInstructions } from './types'
import { Account } from '../../Account'

export const TableDetail = ({
instructions,
}: TransactionTableDetailProps<ClawbackInstructions>) => {
const { t } = useTranslation()
const { amount, holder } = instructions

return (
<div>
{amount && holder && (
<div className="clawback">
<Trans i18nKey="action_from">
<span className="label">{t('claws_back')}</span>
<Amount value={amount} displayIssuer />
from
<Account account={holder} />
</Trans>
</div>
)}
</div>
)
}
19 changes: 19 additions & 0 deletions src/containers/shared/components/Transaction/Clawback/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {
TransactionAction,
TransactionCategory,
TransactionMapping,
} from '../types'

import { Simple } from './Simple'
import { parser } from './parser'
import { TableDetail } from './TableDetail'
import { Description } from './Description'

export const ClawbackTransaction: TransactionMapping = {
Simple,
TableDetail,
Description,
action: TransactionAction.CANCEL,
category: TransactionCategory.PAYMENT,
parser,
}
46 changes: 46 additions & 0 deletions src/containers/shared/components/Transaction/Clawback/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { Clawback, ClawbackInstructions } from './types'
import { TransactionParser } from '../types'
import { formatAmount } from '../../../../../rippled/lib/txSummary/formatAmount'
import { computeBalanceChange } from '../../../utils'

export const parser: TransactionParser<Clawback, ClawbackInstructions> = (
tx,
meta,
) => {
const account = tx.Account
const amount = formatAmount(tx.Amount)
const holder = amount.issuer
amount.issuer = account

// At this point, we need to get the ACTUAL balance change as a
// result of Clawback. If the issuer tries to claw back more than
// what holder has, only the max available balance is clawed.
const trustlineNode = meta.AffectedNodes.filter(
(node: any) =>
node.DeletedNode?.LedgerEntryType === 'RippleState' ||
node.ModifiedNode?.LedgerEntryType === 'RippleState',
)

// If no trustline is modified, it means the tx failed.
// We just return the amount that was attempted to claw.
if (!trustlineNode || trustlineNode.length !== 1)
return {
amount,
account,
holder,
}

const { change } = computeBalanceChange(
trustlineNode[0].ModifiedNode ?? trustlineNode[0].DeletedNode,
)

// Update the amount that was actually clawed back
// (could be different from what was submitted)
amount.amount = Math.abs(change)

return {
account,
amount,
holder,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { createDescriptionWrapperFactory } from '../../test'
import { Description } from '../Description'
import transaction from './mock_data/Clawback.json'
import i18n from '../../../../../../i18n/testConfigEnglish'

const createWrapper = createDescriptionWrapperFactory(Description, i18n)

describe('Clawback', () => {
it('handles Clawback Description ', () => {
const wrapper = createWrapper(transaction)
expect(wrapper.find('[data-test="from-to-line"]')).toHaveText(
`rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9 claws back from rscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP`,
)
expect(wrapper.find('[data-test="amount-line"]')).toHaveText(
`The max clawback amount is 4,840.00 FOO.rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9`,
)
wrapper.unmount()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { createSimpleWrapperFactory, expectSimpleRowText } from '../../test'
import { Simple } from '../Simple'
import transaction from './mock_data/Clawback.json'
import transactionFailure from './mock_data/Clawback_Failure.json'

const createWrapper = createSimpleWrapperFactory(Simple)

describe('Clawback', () => {
it('handles Clawback simple view ', () => {
const wrapper = createWrapper(transaction)
expectSimpleRowText(wrapper, 'holder', 'rscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP')
expectSimpleRowText(
wrapper,
'amount',
'3,840.00 FOO.rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9',
)
wrapper.unmount()
})

it('handles failed Clawback simple view ', () => {
const wrapper = createWrapper(transactionFailure)
expectSimpleRowText(wrapper, 'holder', 'rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9')
expectSimpleRowText(
wrapper,
'amount',
'4,840.00 FOO.rscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP',
)
wrapper.unmount()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { createTableDetailWrapperFactory } from '../../test'
import { TableDetail } from '../TableDetail'
import transaction from './mock_data/Clawback.json'

const createWrapper = createTableDetailWrapperFactory(TableDetail)

describe('Clawback', () => {
it('handles Clawback TableDetail ', () => {
const wrapper = createWrapper(transaction)
expect(wrapper.find('.clawback')).toHaveText(
`claws_back3,840.00 FOO.rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9fromrscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP`,
)
wrapper.unmount()
})
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
{
"tx": {
"Account": "rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9",
"Amount": {
"currency": "FOO",
"issuer": "rscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP",
"value": "4840"
},
"Fee": "10",
"Flags": 0,
"LastLedgerSequence": 515,
"Sequence": 492,
"SigningPubKey": "ED4B74169976E689D549ED4A50BF06C1174115D363CF9E14031030D2BB5CAA274D",
"TransactionType": "Clawback",
"TxnSignature": "E3C2138CA0C09DD4243DB47B562BDE334CFADFDFF39B50DE7E2A3D6FBCB1ADD89CCAF19A7DE08C3C295974492C3FC7E7CD71FB040A582154EC26DB09D1AEF800",
"date": 1688136937000
},
"meta": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Account": "rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9",
"Balance": "99999999960",
"Flags": 2155872256,
"OwnerCount": 0,
"Sequence": 493
},
"LedgerEntryType": "AccountRoot",
"LedgerIndex": "057A552C60FE5AC6C77FC70C28209BB4D33C60C6A350DACEF609C48FE12A4387",
"PreviousFields": {
"Balance": "99999999970",
"Sequence": 492
},
"PreviousTxnID": "E6EF19434F7FF87AB6EE7127A19D9FBC822B3E8E7C26264A902703424AB1F188",
"PreviousTxnLgrSeq": 493
}
},
{
"ModifiedNode": {
"FinalFields": {
"Balance": {
"currency": "FOO",
"issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji",
"value": "0"
},
"Flags": 65536,
"HighLimit": {
"currency": "FOO",
"issuer": "rDZ713igKfedN4hhY6SjQse4Mv3ZrBxnn9",
"value": "0"
},
"HighNode": "0",
"LowLimit": {
"currency": "FOO",
"issuer": "rscBWQpyZEmQvupeB1quu7Ky8YX4f5CHDP",
"value": "0"
},
"LowNode": "0"
},
"LedgerEntryType": "RippleState",
"LedgerIndex": "907573E62311BAC99A985BBBB38DAB09D89B0C47167AB6E280DD2B309CFAF31B",
"PreviousFields": {
"Balance": {
"currency": "FOO",
"issuer": "rrrrrrrrrrrrrrrrrrrrBZbvji",
"value": "3840"
}
},
"PreviousTxnID": "FEF8B23EB453ECC8BE0D4104CEB0B481E029213848EC1BD11470F8B8C3E6B424",
"PreviousTxnLgrSeq": 494
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS"
},
"hash": "0E09D8C61C799AF206D66F81561EC5B52641B439EEA47CB4F5637A918FB51536",
"ledger_index": 496,
"date": 1688136937000
}
Loading

0 comments on commit a8b5944

Please sign in to comment.