Skip to content

Commit

Permalink
feat: add support for SetHook transaction (#757)
Browse files Browse the repository at this point in the history
## High Level Overview of Change

Title says it all.

### Context of Change

Better hooks support

### Type of Change

- [x] New feature (non-breaking change which adds functionality)

### TypeScript/Hooks Update

Files added used TS/hooks

## Before / After

`SetHook` now has a custom `Simple` page.

![image](https://github.com/ripple/explorer/assets/8029314/7b6b7875-9997-4943-a0e6-4b8671851ef0)

![image](https://github.com/ripple/explorer/assets/8029314/a2d8c085-3b4e-4029-a76d-6ed046c6dac9)

## Test Plan

Added tests. CI passes. Works locally.
  • Loading branch information
mvadari authored Jul 18, 2023
1 parent d311e54 commit 8aa684f
Show file tree
Hide file tree
Showing 15 changed files with 779 additions and 6 deletions.
8 changes: 7 additions & 1 deletion public/locales/en-US/translations.json
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,7 @@
"transaction_type_name_PaymentChannelClaim": "Payment Channel Claim",
"transaction_type_name_PaymentChannelCreate": "Payment Channel Create",
"transaction_type_name_PaymentChannelFund": "Payment Channel Fund",
"transaction_type_name_SetHook": "Set Hook",
"transaction_type_name_SetRegularKey": "Set Regular Key",
"transaction_type_name_SignerListSet": "Signer List Set",
"transaction_type_name_TicketCreate": "Ticket Create",
Expand Down Expand Up @@ -472,5 +473,10 @@
"hook_exec_hash": "It triggered the hook <0>{{hash}}</0>",
"hook_exec_account": "On the account <0>{{account}}</0>",
"hook_exec_return": "Returned the code <0>{{code}}</0> with string \"<1>{{string}}</1>\"",
"hook_exec_emit_count": "Emitted <0>{{count}}</0> transactions"
"hook_exec_emit_count": "Emitted <0>{{count}}</0> transactions",
"hash": "Hash",
"grant": "Grant",
"namespace": "Namespace",
"api_version": "API Version",
"triggered_on": "Triggered On"
}
9 changes: 9 additions & 0 deletions src/containers/Transactions/simpleTab.scss
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ $subdued-color: $black-40;
.value {
text-align: right;

.grant {
.account {
padding-bottom: 8px;
font-size: 11px;
text-align: right;
@include medium;
}
}

.amount {
text-align: right;

Expand Down
59 changes: 59 additions & 0 deletions src/containers/shared/components/Transaction/SetHook/Simple.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { useTranslation } from 'react-i18next'
import { buildHookFlags } from '../../../transactionUtils'
import { Account } from '../../Account'
import { SimpleGroup } from '../SimpleGroup'
import { SimpleRow } from '../SimpleRow'
import { TransactionSimpleProps } from '../types'
import { HookData, SetHookInstructions } from './types'
import { hookOnToTxList } from './utils'

export const Simple = ({
data,
}: TransactionSimpleProps<SetHookInstructions>) => {
const { hooks } = data.instructions
const { t } = useTranslation()

const renderHook = (hook: HookData) => (
<SimpleGroup title={t('hook')} key={hook.HookHash || hook.CreateCode}>
<SimpleRow label={t('hash')} data-test="hook-hash">
{hook.HookHash ?? 'undefined'}
</SimpleRow>
{hook.HookOn && (
<SimpleRow label={t('triggered_on')} data-test="hook-on">
{/* // TODO: use the transaction badges here instead of just text */}
{hookOnToTxList(hook.HookOn)?.join(', ') ?? <em>None</em>}
</SimpleRow>
)}
{hook.HookGrants && (
<SimpleRow label={t('grant')} data-test="hook-grant">
{hook.HookGrants.map((hookGrant) => {
const grant = hookGrant.HookGrant
return (
<div className="grant" key={grant.HookHash}>
<div className="hash">{grant.HookHash}</div>
{grant.Authorize && <Account account={grant.Authorize} />}
</div>
)
})}
</SimpleRow>
)}
{hook.HookNamespace && (
<SimpleRow label={t('namespace')} data-test="hook-namespace">
{hook.HookNamespace}
</SimpleRow>
)}
{hook.Flags && (
<SimpleRow label={t('flags')} data-test="hook-flags">
<em>{buildHookFlags(hook.Flags).join(', ')}</em>
</SimpleRow>
)}
{hook.HookApiVersion != null && (
<SimpleRow label={t('api_version')} data-test="hook-api-version">
{hook.HookApiVersion}
</SimpleRow>
)}
</SimpleGroup>
)

return <>{hooks.map(renderHook)}</>
}
15 changes: 15 additions & 0 deletions src/containers/shared/components/Transaction/SetHook/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {
TransactionAction,
TransactionCategory,
TransactionMapping,
} from '../types'

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

export const SetHookTransaction: TransactionMapping = {
Simple,
action: TransactionAction.CREATE,
category: TransactionCategory.ACCOUNT,
parser,
}
23 changes: 23 additions & 0 deletions src/containers/shared/components/Transaction/SetHook/parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { SetHook, SetHookInstructions } from './types'

export const parser = (tx: SetHook, meta: any): SetHookInstructions => {
const hooks = tx.Hooks.map((hook) => hook.Hook)
const affectedNodes = meta.AffectedNodes.filter(
(node: any) =>
node.CreatedNode?.LedgerEntryType === 'Hook' ||
(node.ModifiedNode?.LedgerEntryType === 'Hook' &&
!!node.ModifiedNode?.PreviousFields?.Hooks),
)
const hashes = affectedNodes.flatMap((node: any) =>
(node.ModifiedNode?.FinalFields ?? node.CreatedNode?.NewFields)?.Hooks?.map(
(hook: any) => hook.Hook.HookHash,
),
)
// TODO: there may be bugs here when a `HookHash` is already specified in a hook
// It's difficult to understand what situation that would be in, so this is left here for now
hashes.forEach((element, index) => {
if (hooks[index] != null) hooks[index].HookHash = element
})

return { hooks: hooks.filter((hook) => hook.CreateCode || hook.HookHash) }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { createSimpleWrapperFactory } from '../../test/createWrapperFactory'
import { Simple } from '../Simple'
import mockSetHook from './mock_data/SetHook.json'
import mockSetHook2 from './mock_data/SetHook2.json'
import mockSetHookFailure from './mock_data/SetHookFailure.json'
import { expectSimpleRowText } from '../../test/expectations'

const createWrapper = createSimpleWrapperFactory(Simple)

describe('SetHookSimple', () => {
it('renders', () => {
const wrapper = createWrapper(mockSetHook)

expect(wrapper.find('.group')).toHaveLength(2)

const hook1 = wrapper.find('.group').at(0)
const hook2 = wrapper.find('.group').at(1)

expectSimpleRowText(
hook1,
'hook-hash',
'4E57C7FE7A84ABFA53CFE411DE9BA3420B94F55038BF238EBE1EB89095ABA4DE',
)
expectSimpleRowText(hook1, 'hook-on', 'Invoke')
expectSimpleRowText(
hook1,
'hook-namespace',
'0000000000000000000000000000000000000000000000000000000000000000',
)
expectSimpleRowText(hook1, 'hook-flags', 'hsfOverride')
expectSimpleRowText(hook1, 'hook-api-version', '0')

expectSimpleRowText(
hook2,
'hook-hash',
'C04E2043B656B578CB30E9FF465304AF402B7AFE38B6CE2D8CEFECDD669E3424',
)
expectSimpleRowText(hook2, 'hook-on', '98')
expectSimpleRowText(
hook2,
'hook-namespace',
'0000000000000000000000000000000000000000000000000000000000000000',
)
expectSimpleRowText(hook2, 'hook-flags', 'hsfOverride')
expectSimpleRowText(hook2, 'hook-api-version', '0')
})

it('renders a different SetHook tx', () => {
const wrapper = createWrapper(mockSetHook2)

expect(wrapper.find('.group')).toHaveLength(1)

const hook = wrapper.find('.group').at(0)

expectSimpleRowText(
hook,
'hook-hash',
'548BBB700F5841C2D41E227456E8A80E6A6335D1149BA3B5FF745A00CC0EBECE',
)

expect(hook.find('.grant')).toHaveLength(2)

const grant1 = hook.find('.grant').at(0)
const grant2 = hook.find('.grant').at(1)

expect(grant1.find('.hash')).toHaveText(
'096A70632BBB67488F4804AE55604A01F52226BD556E3589270D0D30C9A6AF81',
)
expect(grant1.find('.account').at(0)).toHaveText(
'rQUhXd7sopuga3taru3jfvc1BgVbscrb1X',
)
expect(grant1.find(`.account a`)).toExist()

expect(grant2.find('.hash')).toHaveText(
'3F47684053E1A653E54EAC1C5F50BCBAF7F69078CEFB5846BB046CE44B8ECDC2',
)
expect(grant2.find('.account').at(0)).toHaveText(
'raPSFU999HcwpyRojdNh2i96T22gY9fgxL',
)
expect(grant2.find(`.account a`)).toExist()
})

it('renders a failed SetHook tx', () => {
const wrapper = createWrapper(mockSetHookFailure)

expect(wrapper.find('.group')).toHaveLength(1)

const hook = wrapper.find('.group').at(0)

expectSimpleRowText(hook, 'hook-hash', 'undefined')

expectSimpleRowText(hook, 'hook-on', 'Payment')
expectSimpleRowText(
hook,
'hook-namespace',
'CAE662172FD450BB0CD710A769079C05BFC5D8E35EFA6576EDC7D0377AFDD4A2',
)
expectSimpleRowText(hook, 'hook-flags', 'hsfOverride')
expectSimpleRowText(hook, 'hook-api-version', '0')
})
})

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
{
"tx": {
"Account": "rGVHr1PrfL93UAjyw3DWZoi9adz2sLp2yL",
"Fee": "20",
"Flags": 0,
"Hooks": [
{
"Hook": {
"HookGrants": [
{
"HookGrant": {
"Authorize": "rQUhXd7sopuga3taru3jfvc1BgVbscrb1X",
"HookHash": "096A70632BBB67488F4804AE55604A01F52226BD556E3589270D0D30C9A6AF81"
}
},
{
"HookGrant": {
"Authorize": "raPSFU999HcwpyRojdNh2i96T22gY9fgxL",
"HookHash": "3F47684053E1A653E54EAC1C5F50BCBAF7F69078CEFB5846BB046CE44B8ECDC2"
}
}
],
"HookHash": "548BBB700F5841C2D41E227456E8A80E6A6335D1149BA3B5FF745A00CC0EBECE"
}
}
],
"LastLedgerSequence": 1976819,
"NetworkID": 21338,
"Sequence": 1784919,
"SigningPubKey": "025137610C314DA06E4CD804541F2A7CDD0483EB85BA3F74067A026B5F170C8047",
"TransactionType": "SetHook",
"TxnSignature": "30450221008A7DD8DE50A7D107CF36DEA92D06B64926E865EADA243F18901DD7D9FB9D450D02203505F2C40D516D9208DF3172B6A45CB74C8DB0C18260C180B3185C7C872E0BAE",
"ctid": "C01E29E10000535A",
"date": 1680842731000
},
"meta": {
"AffectedNodes": [
{
"ModifiedNode": {
"FinalFields": {
"Account": "rGVHr1PrfL93UAjyw3DWZoi9adz2sLp2yL",
"Flags": 0,
"Hooks": [
{
"Hook": {
"HookGrants": [
{
"HookGrant": {
"Authorize": "rQUhXd7sopuga3taru3jfvc1BgVbscrb1X",
"HookHash": "096A70632BBB67488F4804AE55604A01F52226BD556E3589270D0D30C9A6AF81"
}
},
{
"HookGrant": {
"Authorize": "raPSFU999HcwpyRojdNh2i96T22gY9fgxL",
"HookHash": "3F47684053E1A653E54EAC1C5F50BCBAF7F69078CEFB5846BB046CE44B8ECDC2"
}
}
],
"HookHash": "548BBB700F5841C2D41E227456E8A80E6A6335D1149BA3B5FF745A00CC0EBECE"
}
},
{
"Hook": {}
},
{
"Hook": {}
},
{
"Hook": {}
}
],
"OwnerNode": "0"
},
"LedgerEntryType": "Hook",
"LedgerIndex": "0BCDDA47012D784783CE787017E103BA542DFC451168074C9AA4704016893ED4",
"PreviousFields": {
"Hooks": [
{
"Hook": {
"Flags": 0,
"HookHash": "548BBB700F5841C2D41E227456E8A80E6A6335D1149BA3B5FF745A00CC0EBECE"
}
},
{
"Hook": {}
},
{
"Hook": {}
},
{
"Hook": {}
}
]
}
}
},
{
"ModifiedNode": {
"FinalFields": {
"Account": "rGVHr1PrfL93UAjyw3DWZoi9adz2sLp2yL",
"Balance": "9879550122",
"Flags": 0,
"HookNamespaces": [
"01EAF09326B4911554384121FF56FA8FECC215FDDE2EC35D9E59F2C53EC665A0"
],
"HookStateCount": 74,
"OwnerCount": 78,
"Sequence": 1784920
},
"LedgerEntryType": "AccountRoot",
"LedgerIndex": "BB71F3181F2B9D63FF4D7439AB82C32ABE4ED8F70CE00E0EFC68FD9C37149435",
"PreviousFields": {
"Balance": "9879550142",
"OwnerCount": 76,
"Sequence": 1784919
}
}
}
],
"TransactionIndex": 0,
"TransactionResult": "tesSUCCESS"
},
"hash": "62A6257455F2366CC54DE43EE40258E51FD1695459521D48DE70ECB4D53D677E",
"ledger_index": 1976801,
"date": 1680842731000
}
Loading

0 comments on commit 8aa684f

Please sign in to comment.