diff --git a/package-lock.json b/package-lock.json index 429ed2855..5257faf8d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "0.0.0", "hasInstallScript": true, "dependencies": { - "@algorandfoundation/algokit-subscriber": "^2.1.0", + "@algorandfoundation/algokit-subscriber": "^2.2.0", "@algorandfoundation/algokit-utils": "^7.0.0", "@auth0/auth0-react": "^2.2.4", "@blockshake/defly-connect": "^1.1.6", @@ -45,7 +45,7 @@ "@txnlab/use-wallet": "^3.11.0", "@txnlab/use-wallet-react": "^3.11.0", "@xstate/react": "^4.1.1", - "algosdk": "2.9.0", + "algosdk": "2.10.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", @@ -166,9 +166,9 @@ "license": "MIT" }, "node_modules/@algorandfoundation/algokit-subscriber": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-subscriber/-/algokit-subscriber-2.1.0.tgz", - "integrity": "sha512-k1bHnagef3sdN0VVS4uKA+rnMTULMchX+eBLn9sIw7laDslYYDcaDm0Rg4V6M87SvN6Ndbj8SMvKbjsP5k1zkw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-subscriber/-/algokit-subscriber-2.2.0.tgz", + "integrity": "sha512-7LU/QDVUW9tRuYNKS9ogm89TyRun5ag7ud2WPcb+kRw+0SWTPZpu3b7sd12mRdar0QKlUs0eIiMSXloTZOw+Fg==", "dependencies": { "algorand-msgpack": "^1.0.1", "buffer": "^6.0.3", @@ -179,14 +179,14 @@ "node": ">=18.0" }, "peerDependencies": { - "@algorandfoundation/algokit-utils": "^7.0.0", - "algosdk": ">=2.9.0 <3.0" + "@algorandfoundation/algokit-utils": "^7.1.0", + "algosdk": ">=2.10.0 <3.0" } }, "node_modules/@algorandfoundation/algokit-utils": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-7.0.0.tgz", - "integrity": "sha512-L+8ykFgQVEs802yozldp+QFwW5z0sFXxSeHFYztZIVAACwVK4C/z5DnIF/AA+g8L98sv3VPkOvjjvPSya5szZQ==", + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@algorandfoundation/algokit-utils/-/algokit-utils-7.1.0.tgz", + "integrity": "sha512-cetYNuqdxuWt3CyNcWPU1HWoPcsoCi4xkUD1u0/YTMSc+ttiEvPJBu2p6eS1MvCkVleG3h0N3ycEXpyS9Ox0bg==", "dependencies": { "buffer": "^6.0.3" }, @@ -4884,10 +4884,9 @@ } }, "node_modules/algosdk": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/algosdk/-/algosdk-2.9.0.tgz", - "integrity": "sha512-o0n0nLMbTX6SFQdMUk2/2sy50jmEmZk5OTPYSh2aAeP8DUPxrhjMPfwGsYNvaO+qk75MixC2eWpfA9vygCQ/Mg==", - "license": "MIT", + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/algosdk/-/algosdk-2.10.0.tgz", + "integrity": "sha512-fuw+HwHjGrH9BHMANTvtQmo2nuQarDH+pHAaR9wDB51aYTrIrhD2GtwPY0Sgd1KcqZjMfX1Z6xQaZqljUhTGLQ==", "dependencies": { "algo-msgpack-with-bigint": "^2.1.1", "buffer": "^6.0.3", diff --git a/package.json b/package.json index 47ee1dcc3..ec2d3c106 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "postinstall": "patch-package" }, "dependencies": { - "@algorandfoundation/algokit-subscriber": "^2.1.0", + "@algorandfoundation/algokit-subscriber": "^2.2.0", "@algorandfoundation/algokit-utils": "^7.0.0", "@auth0/auth0-react": "^2.2.4", "@blockshake/defly-connect": "^1.1.6", @@ -60,7 +60,7 @@ "@txnlab/use-wallet": "^3.11.0", "@txnlab/use-wallet-react": "^3.11.0", "@xstate/react": "^4.1.1", - "algosdk": "2.9.0", + "algosdk": "2.10.0", "class-variance-authority": "^0.7.0", "clsx": "^2.1.0", "cmdk": "^1.0.0", @@ -197,4 +197,4 @@ "ws@>7.0.0 <7.5.9": "7.5.10", "path-to-regexp@>= 0.2.0 <8.0.0": "8.0.0" } -} +} \ No newline at end of file diff --git a/src/features/common/components/badge.tsx b/src/features/common/components/badge.tsx index 4274d0dcd..11d119fcc 100644 --- a/src/features/common/components/badge.tsx +++ b/src/features/common/components/badge.tsx @@ -1,7 +1,7 @@ import * as React from 'react' import { cva, type VariantProps } from 'class-variance-authority' import { cn } from '@/features/common/utils' -import { CircleDollarSign, SquareArrowRight, Bolt, Snowflake, ShieldCheck, Key, Parentheses } from 'lucide-react' +import { CircleDollarSign, SquareArrowRight, Bolt, Snowflake, ShieldCheck, Key, Parentheses, HeartPulse } from 'lucide-react' import { TransactionType } from '@/features/transactions/models' const badgeVariants = cva( @@ -20,6 +20,7 @@ const badgeVariants = cva( [TransactionType.AssetFreeze]: 'border-transparent bg-asset-freeze text-primary-foreground', [TransactionType.StateProof]: 'border-transparent bg-state-proof text-primary-foreground', [TransactionType.KeyReg]: 'border-transparent bg-key-registration text-primary-foreground', + [TransactionType.Heartbeat]: 'border-transparent bg-heartbeat text-primary-foreground', }, }, defaultVariants: { @@ -40,6 +41,7 @@ const transactionTypeBadgeIcon = new Map([ [TransactionType.AssetFreeze.toString(), ], [TransactionType.StateProof.toString(), ], [TransactionType.KeyReg.toString(), ], + [TransactionType.Heartbeat.toString(), ], ]) function Badge({ className, variant, children, ...props }: BadgeProps) { diff --git a/src/features/transactions-graph/components/__snapshots__/heartbeat-transaction-graph.HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.html b/src/features/transactions-graph/components/__snapshots__/heartbeat-transaction-graph.HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.html new file mode 100644 index 000000000..fa2681c54 --- /dev/null +++ b/src/features/transactions-graph/components/__snapshots__/heartbeat-transaction-graph.HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ.html @@ -0,0 +1,162 @@ +
+
+
+
+
+
+
+
+
+ +
+
+ 1 +
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ +
+
+
+
+
+
+
+ +
+
\ No newline at end of file diff --git a/src/features/transactions-graph/components/heartbeat-transaction-tooltip-content.tsx b/src/features/transactions-graph/components/heartbeat-transaction-tooltip-content.tsx new file mode 100644 index 000000000..103988d4d --- /dev/null +++ b/src/features/transactions-graph/components/heartbeat-transaction-tooltip-content.tsx @@ -0,0 +1,63 @@ +import { useMemo } from 'react' +import { + transactionFeeLabel, + transactionIdLabel, + transactionRekeyToLabel, + transactionTypeLabel, +} from '@/features/transactions/components/transaction-info' +import { asTransactionLinkTextComponent, TransactionLink } from '@/features/transactions/components/transaction-link' +import { transactionSenderLabel } from '@/features/transactions/components/labels' +import { AccountLink } from '@/features/accounts/components/account-link' +import { cn } from '@/features/common/utils' +import { DescriptionList } from '@/features/common/components/description-list' +import { TransactionTypeDescriptionDetails } from '@/features/transactions/components/transaction-type-description-details' +import { DisplayAlgo } from '@/features/common/components/display-algo' +import { HeartbeatTransaction } from '@/features/transactions/models' +import { heartbeatAddressLabel } from '@/features/transactions/components/heartbeat-transaction-info' + +type Props = { + transaction: HeartbeatTransaction + isSimulated: boolean +} + +export function HeartbeatTransactionTooltipContent({ transaction, isSimulated }: Props) { + const items = useMemo( + () => [ + { + dt: transactionIdLabel, + dd: isSimulated ? asTransactionLinkTextComponent(transaction.id, true) : , + }, + { + dt: transactionTypeLabel, + dd: , + }, + { + dt: transactionSenderLabel, + dd: , + }, + { + dt: heartbeatAddressLabel, + dd: , + }, + { + dt: transactionFeeLabel, + dd: , + }, + ...(transaction.rekeyTo + ? [ + { + dt: transactionRekeyToLabel, + dd: , + }, + ] + : []), + ], + [isSimulated, transaction] + ) + + return ( +
+ +
+ ) +} diff --git a/src/features/transactions-graph/components/horizontal.tsx b/src/features/transactions-graph/components/horizontal.tsx index aab9db9c7..b03e49296 100644 --- a/src/features/transactions-graph/components/horizontal.tsx +++ b/src/features/transactions-graph/components/horizontal.tsx @@ -19,6 +19,7 @@ import { StateProofTransactionTooltipContent } from './state-proof-transaction-t import PointerRight from '@/features/common/components/svg/pointer-right' import { SubHorizontalTitle } from '@/features/transactions-graph/components/sub-horizontal-title' import { RenderAsyncAtom } from '@/features/common/components/render-async-atom' +import { HeartbeatTransactionTooltipContent } from './heartbeat-transaction-tooltip-content' function ConnectionsFromAncestorsToAncestorsNextSiblings({ ancestors }: { ancestors: HorizontalModel[] }) { return ancestors @@ -42,6 +43,7 @@ const colorClassMap = { [TransactionType.AssetFreeze]: { border: 'border-asset-freeze', text: 'text-asset-freeze' }, [TransactionType.KeyReg]: { border: 'border-key-registration', text: 'text-key-registration' }, [TransactionType.StateProof]: { border: 'border-state-proof', text: 'text-state-proof' }, + [TransactionType.Heartbeat]: { border: 'border-heartbeat', text: 'text-heartbeat' }, } function Circle({ className, text }: { className?: string; text?: string | number }) { @@ -81,6 +83,7 @@ function VectorLabelText({ type }: { type: LabelType }) { if (type === LabelType.KeyReg) return Key Reg if (type === LabelType.StateProof) return State Proof if (type === LabelType.Clawback) return Clawback + if (type === LabelType.Heartbeat) return Heartbeat return undefined } @@ -370,6 +373,9 @@ export function Horizontal({ horizontal, verticals, bgClassName, isSimulated }: {transaction.type === TransactionType.StateProof && ( )} + {transaction.type === TransactionType.Heartbeat && ( + + )} ) diff --git a/src/features/transactions-graph/components/transactions-graph.test.tsx b/src/features/transactions-graph/components/transactions-graph.test.tsx index bfc64ef34..1afc381d7 100644 --- a/src/features/transactions-graph/components/transactions-graph.test.tsx +++ b/src/features/transactions-graph/components/transactions-graph.test.tsx @@ -2,7 +2,13 @@ import { transactionResultMother } from '@/tests/object-mother/transaction-resul import { describe, expect, it, vi } from 'vitest' import { executeComponentTest } from '@/tests/test-component' import { render, prettyDOM } from '@/tests/testing-library' -import { asAppCallTransaction, asAssetTransferTransaction, asPaymentTransaction, asTransaction } from '../../transactions/mappers' +import { + asAppCallTransaction, + asAssetTransferTransaction, + asPaymentTransaction, + asTransaction, + asHeartbeatTransaction, +} from '../../transactions/mappers' import { AssetResult, TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' import { assetResultMother } from '@/tests/object-mother/asset-result' import { useParams } from 'react-router-dom' @@ -250,6 +256,28 @@ describe('group-graph', () => { ) }) +describe('heartbeat-transaction-graph', () => { + describe.each([ + { + transactionResult: transactionResultMother['mainnet-HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ']().build(), + }, + ])('when rendering transaction $transactionResult.id', ({ transactionResult }: { transactionResult: TransactionResult }) => { + it('should match snapshot', () => { + vi.mocked(useParams).mockImplementation(() => ({ transactionId: transactionResult.id })) + const model = asHeartbeatTransaction(transactionResult) + const graphData = asTransactionsGraphData([model]) + return executeComponentTest( + () => render(), + async (component) => { + expect(prettyDOM(component.container, prettyDomMaxLength, { highlight: false })).toMatchFileSnapshot( + `__snapshots__/heartbeat-transaction-graph.${transactionResult.id}.html` + ) + } + ) + }) + }) +}) + const createAssetResolver = (assetResults: AssetResult[]) => (assetId: number) => { const assetResult = assetResults.find((a) => a.index === assetId) invariant(assetResult, `Could not find asset result ${assetId}`) diff --git a/src/features/transactions-graph/mappers/horizontals.ts b/src/features/transactions-graph/mappers/horizontals.ts index d8b8d4526..e4a7f5e93 100644 --- a/src/features/transactions-graph/mappers/horizontals.ts +++ b/src/features/transactions-graph/mappers/horizontals.ts @@ -6,6 +6,7 @@ import { AssetFreezeTransaction, AssetTransferTransaction, AssetTransferTransactionSubType, + HeartbeatTransaction, InnerAppCallTransaction, InnerAssetConfigTransaction, InnerAssetFreezeTransaction, @@ -90,6 +91,8 @@ const getTransactionRepresentations = ( return getPaymentTransactionRepresentations(transaction, verticals, parent) case TransactionType.StateProof: return getStateProofTransactionRepresentations(transaction, verticals) + case TransactionType.Heartbeat: + return getHeartbeatTransactionRepresentations(transaction, verticals) } } @@ -299,6 +302,21 @@ const getStateProofTransactionRepresentations = (transaction: StateProofTransact ] } +const getHeartbeatTransactionRepresentations = (transaction: HeartbeatTransaction, verticals: Vertical[]): Representation[] => { + const from = calculateFromWithoutParent(transaction.sender, verticals) + + return [ + { + fromVerticalIndex: from.verticalId, + fromAccountIndex: from.accountNumber, + type: RepresentationType.Point, + label: { + type: LabelType.Heartbeat, + }, + }, + ] +} + const asTransactionGraphRepresentation = (from: RepresentationFromTo, to: RepresentationFromTo, description: Label): Representation => { if (from.verticalId === to.verticalId) { return { diff --git a/src/features/transactions-graph/models/index.ts b/src/features/transactions-graph/models/index.ts index 9154a7074..90f56d207 100644 --- a/src/features/transactions-graph/models/index.ts +++ b/src/features/transactions-graph/models/index.ts @@ -43,6 +43,7 @@ export enum LabelType { KeyReg = 'KeyReg', StateProof = 'StateProof', Clawback = 'Clawback', + Heartbeat = 'Heartbeat', } export type Label = @@ -73,6 +74,7 @@ export type Label = | { type: LabelType.AssetFreeze } | { type: LabelType.KeyReg } | { type: LabelType.StateProof } + | { type: LabelType.Heartbeat } export type Vector = { type: RepresentationType.Vector diff --git a/src/features/transactions/components/heartbeat-transaction-details.tsx b/src/features/transactions/components/heartbeat-transaction-details.tsx new file mode 100644 index 000000000..dc9792aa8 --- /dev/null +++ b/src/features/transactions/components/heartbeat-transaction-details.tsx @@ -0,0 +1,26 @@ +import { Card, CardContent } from '@/features/common/components/card' +import { cn } from '@/features/common/utils' +import { TransactionNote } from './transaction-note' +import { HeartbeatTransaction, SignatureType } from '../models' +import { MultisigDetails } from './multisig-details' +import { LogicsigDetails } from './logicsig-details' +import { TransactionViewTabs } from './transaction-view-tabs' +import { HeartbeatTransactionInfo } from './heartbeat-transaction-info' + +type Props = { + transaction: HeartbeatTransaction +} + +export function HeartbeatTransactionDetails({ transaction }: Props) { + return ( + + + + + {transaction.note && } + {transaction.signature?.type === SignatureType.Multi && } + {transaction.signature?.type === SignatureType.Logic && } + + + ) +} diff --git a/src/features/transactions/components/heartbeat-transaction-info.tsx b/src/features/transactions/components/heartbeat-transaction-info.tsx new file mode 100644 index 000000000..50ef8bd83 --- /dev/null +++ b/src/features/transactions/components/heartbeat-transaction-info.tsx @@ -0,0 +1,37 @@ +import { cn } from '@/features/common/utils' +import { useMemo } from 'react' +import { HeartbeatTransaction } from '../models' +import { DescriptionList } from '@/features/common/components/description-list' +import { AccountLink } from '@/features/accounts/components/account-link' +import { transactionSenderLabel } from './labels' + +type Props = { + transaction: HeartbeatTransaction +} + +export const heartbeatAddressLabel = 'Address' + +// TODO: HB - Present the data we want to show + +export function HeartbeatTransactionInfo({ transaction }: Props) { + const items = useMemo( + () => [ + { + dt: transactionSenderLabel, + dd: , + }, + { + dt: heartbeatAddressLabel, + dd: , + }, + ], + [transaction.address, transaction.sender] + ) + + return ( +
+

Heartbeat

+ +
+ ) +} diff --git a/src/features/transactions/components/transaction-details.tsx b/src/features/transactions/components/transaction-details.tsx index 0c09405c3..1a5111e43 100644 --- a/src/features/transactions/components/transaction-details.tsx +++ b/src/features/transactions/components/transaction-details.tsx @@ -7,6 +7,7 @@ import { AssetFreezeTransactionDetails } from './asset-freeze-transaction-detail import { StateProofTransactionDetails } from './state-proof-transaction-details' import { KeyRegTransactionDetails } from './key-reg-transaction-details' import { TransactionInfo } from './transaction-info' +import { HeartbeatTransactionDetails } from './heartbeat-transaction-details' type Props = { transaction: Transaction | InnerTransaction @@ -30,6 +31,8 @@ export function TransactionDetails({ transaction }: Props) { ) : transaction.type === TransactionType.KeyReg ? ( + ) : transaction.type === TransactionType.Heartbeat ? ( + ) : undefined}
) diff --git a/src/features/transactions/mappers/app-call-transaction-mappers.ts b/src/features/transactions/mappers/app-call-transaction-mappers.ts index 8f270e60e..3cec5b74c 100644 --- a/src/features/transactions/mappers/app-call-transaction-mappers.ts +++ b/src/features/transactions/mappers/app-call-transaction-mappers.ts @@ -18,7 +18,6 @@ import { asInnerAssetConfigTransaction } from './asset-config-transaction-mapper import { asInnerAssetFreezeTransaction } from './asset-freeze-transaction-mappers' import { asInnerKeyRegTransaction } from './key-reg-transaction-mappers' import { AsyncMaybeAtom } from '@/features/common/data/types' -import { asInnerStateProofTransaction } from './state-proof-transaction-mappers' import { Atom } from 'jotai/index' import { GroupId, GroupResult } from '@/features/groups/data/types' import { Round } from '@/features/blocks/data/types' @@ -181,10 +180,6 @@ const asInnerTransaction = ( if (transactionResult['tx-type'] === AlgoSdkTransactionType.keyreg) { return asInnerKeyRegTransaction(networkTransactionId, index, transactionResult) } - // I don't believe it's possible to have an inner stpf transaction, handling just in case. - if (transactionResult['tx-type'] === AlgoSdkTransactionType.stpf) { - return asInnerStateProofTransaction(networkTransactionId, index, transactionResult) - } throw new Error(`Unsupported inner transaction type: ${transactionResult['tx-type']}`) } diff --git a/src/features/transactions/mappers/heartbeat-transaction-mappers.ts b/src/features/transactions/mappers/heartbeat-transaction-mappers.ts new file mode 100644 index 000000000..dcfa30e86 --- /dev/null +++ b/src/features/transactions/mappers/heartbeat-transaction-mappers.ts @@ -0,0 +1,17 @@ +import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' +import { HeartbeatTransaction, TransactionType } from '../models' +import { mapCommonTransactionProperties } from './transaction-common-properties-mappers' +import { invariant } from '@/utils/invariant' + +export const asHeartbeatTransaction = (transactionResult: TransactionResult): HeartbeatTransaction => { + invariant(transactionResult['heartbeat-transaction'], 'heartbeat-transaction is not set') + const heartbeat = transactionResult['heartbeat-transaction'] + + return { + id: transactionResult.id, + type: TransactionType.Heartbeat, + subType: undefined, + address: heartbeat['hb-address'], + ...mapCommonTransactionProperties(transactionResult), + } +} diff --git a/src/features/transactions/mappers/index.ts b/src/features/transactions/mappers/index.ts index 38aa8351c..b8a1db9f0 100644 --- a/src/features/transactions/mappers/index.ts +++ b/src/features/transactions/mappers/index.ts @@ -6,3 +6,4 @@ export * from './transaction-summary-mappers' export * from './transactions-summary-mappers' export * from './arc-2' export * from './state-delta-mappers' +export * from './heartbeat-transaction-mappers' diff --git a/src/features/transactions/mappers/state-proof-transaction-mappers.ts b/src/features/transactions/mappers/state-proof-transaction-mappers.ts index cc861d70e..e705eee84 100644 --- a/src/features/transactions/mappers/state-proof-transaction-mappers.ts +++ b/src/features/transactions/mappers/state-proof-transaction-mappers.ts @@ -1,6 +1,6 @@ import { TransactionResult } from '@algorandfoundation/algokit-utils/types/indexer' -import { InnerStateProofTransaction, StateProofTransaction, TransactionType } from '../models' -import { asInnerTransactionId, mapCommonTransactionProperties } from './transaction-common-properties-mappers' +import { StateProofTransaction, TransactionType } from '../models' +import { mapCommonTransactionProperties } from './transaction-common-properties-mappers' export const asStateProofTransaction = (transactionResult: TransactionResult): StateProofTransaction => { return { @@ -10,15 +10,3 @@ export const asStateProofTransaction = (transactionResult: TransactionResult): S ...mapCommonTransactionProperties(transactionResult), } } - -export const asInnerStateProofTransaction = ( - networkTransactionId: string, - index: string, - transactionResult: TransactionResult -): InnerStateProofTransaction => { - const { id: _id, ...rest } = asStateProofTransaction(transactionResult) - return { - ...asInnerTransactionId(networkTransactionId, index), - ...rest, - } -} diff --git a/src/features/transactions/mappers/transaction-mappers.ts b/src/features/transactions/mappers/transaction-mappers.ts index ce0bb75a0..829a575c4 100644 --- a/src/features/transactions/mappers/transaction-mappers.ts +++ b/src/features/transactions/mappers/transaction-mappers.ts @@ -14,6 +14,7 @@ import { GroupId, GroupResult } from '@/features/groups/data/types' import { Round } from '@/features/blocks/data/types' import { getGroupResultAtom } from '@/features/groups/data/group-result' import { DecodedAbiMethod } from '@/features/abi-methods/models' +import { asHeartbeatTransaction } from './heartbeat-transaction-mappers' export const asTransaction = ( transactionResult: TransactionResult, @@ -45,6 +46,9 @@ export const asTransaction = ( case algosdk.TransactionType.keyreg: { return asKeyRegTransaction(transactionResult) } + case algosdk.TransactionType.hb: { + return asHeartbeatTransaction(transactionResult) + } default: throw new Error(`Unknown transaction type ${transactionResult['tx-type']}`) } diff --git a/src/features/transactions/mappers/transaction-summary-mappers.ts b/src/features/transactions/mappers/transaction-summary-mappers.ts index 7145471a2..500b749ac 100644 --- a/src/features/transactions/mappers/transaction-summary-mappers.ts +++ b/src/features/transactions/mappers/transaction-summary-mappers.ts @@ -69,6 +69,13 @@ export const asTransactionSummary = (transactionResult: TransactionResult): Tran type: TransactionType.KeyReg, } } + case algosdk.TransactionType.hb: { + invariant(transactionResult['heartbeat-transaction'], 'heartbeat-transaction is not set') + return { + ...common, + type: TransactionType.Heartbeat, + } + } default: throw new Error(`Unknown Transaction type ${transactionResult['tx-type']}`) } diff --git a/src/features/transactions/models/index.ts b/src/features/transactions/models/index.ts index 5123f5dd1..057f5ce43 100644 --- a/src/features/transactions/models/index.ts +++ b/src/features/transactions/models/index.ts @@ -28,6 +28,7 @@ export enum TransactionType { AssetFreeze = 'Asset Freeze', StateProof = 'State Proof', KeyReg = 'Key Registration', + Heartbeat = 'Heartbeat', } export enum AssetTransferTransactionSubType { @@ -81,6 +82,7 @@ export type Transaction = | AssetFreezeTransaction | StateProofTransaction | KeyRegTransaction + | HeartbeatTransaction export type TransactionSummary = Pick & { id: string @@ -194,7 +196,6 @@ export type InnerTransaction = | InnerAssetConfigTransaction | InnerAssetFreezeTransaction | InnerKeyRegTransaction - | InnerStateProofTransaction export type BaseAssetConfigTransaction = CommonTransactionProperties & { type: TransactionType.AssetConfig @@ -250,8 +251,6 @@ export type StateProofTransaction = CommonTransactionProperties & { id: string } -export type InnerStateProofTransaction = Omit & InnerTransactionId - export type BaseKeyRegTransaction = CommonTransactionProperties & { type: TransactionType.KeyReg subType: KeyRegTransactionSubType @@ -274,3 +273,10 @@ export type KeyRegTransaction = BaseKeyRegTransaction & { } export type InnerKeyRegTransaction = BaseKeyRegTransaction & InnerTransactionId + +export type HeartbeatTransaction = CommonTransactionProperties & { + type: TransactionType.Heartbeat + subType: undefined + id: string + address: Address +} diff --git a/src/features/transactions/pages/transaction-page.test.tsx b/src/features/transactions/pages/transaction-page.test.tsx index f9c1d7e35..673719795 100644 --- a/src/features/transactions/pages/transaction-page.test.tsx +++ b/src/features/transactions/pages/transaction-page.test.tsx @@ -89,6 +89,7 @@ import { algod } from '@/features/common/data/algo-client' import Arc56TestAppSpecSampleOne from '@/tests/test-app-specs/arc56/sample-one.json' import { Arc56Contract } from '@algorandfoundation/algokit-utils/types/app-arc56' import Arc56TestAppSpecSampleThree from '@/tests/test-app-specs/arc56/sample-three.json' +import { heartbeatAddressLabel } from '../components/heartbeat-transaction-info' vi.mock('@/features/common/data/algo-client', async () => { const original = await vi.importActual('@/features/common/data/algo-client') @@ -1565,3 +1566,42 @@ describe('when rendering an app call transaction with ARC-56 app spec loaded', ( }) }) }) + +describe.only('when rendering a heartbeat transaction', () => { + const transaction = transactionResultMother['localnet-HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ']().build() + + it('should be rendered with the correct data', () => { + vi.mocked(useParams).mockImplementation(() => ({ transactionId: transaction.id })) + const myStore = createStore() + myStore.set(transactionResultsAtom, new Map([[transaction.id, createReadOnlyAtomAndTimestamp(transaction)]])) + + return executeComponentTest( + () => { + return render(, undefined, myStore) + }, + async (component) => { + await waitFor(() => { + descriptionListAssertion({ + container: component.container, + items: [ + { term: transactionIdLabel, description: transaction.id }, + { term: transactionTypeLabel, description: 'Heartbeat' }, + { term: transactionTimestampLabel, description: expect.any(String) }, + { term: transactionBlockLabel, description: expect.any(String) }, + { term: transactionFeeLabel, description: '0.001' }, + { term: transactionSenderLabel, description: 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW' }, + { term: heartbeatAddressLabel, description: 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW' }, + ], + }) + }) + + const viewTransactionTabList = component.getByRole('tablist', { name: transactionDetailsLabel }) + expect(viewTransactionTabList).toBeTruthy() + expect( + component.getByRole('tabpanel', { name: transactionVisualGraphTabLabel }).getAttribute('data-state'), + 'Visual tab should be active' + ).toBe('active') + } + ) + }) +}) diff --git a/src/index.css b/src/index.css index ee9904342..7d7d12b7f 100644 --- a/src/index.css +++ b/src/index.css @@ -59,6 +59,9 @@ --key-registration: 107 94 9; --key-registration-foreground: 255 255 255; + --heartbeat: 53 130 56; + --heartbeat-foreground: 255 255 255; + --error: 203 69 83; --error-foreground: 255 255 255; @@ -124,6 +127,9 @@ --key-registration: 164 144 20; --key-registration-foreground: 0 19 36; + --heartbeat: 76 175 79; + --heartbeat-foreground: 255 255 255; + --error: 203 69 83; --error-foreground: 255 255 255; } diff --git a/src/tests/builders/transaction-result-builder.ts b/src/tests/builders/transaction-result-builder.ts index bb0e81c0c..40cc2d5c3 100644 --- a/src/tests/builders/transaction-result-builder.ts +++ b/src/tests/builders/transaction-result-builder.ts @@ -71,6 +71,14 @@ export class TransactionResultBuilder extends DataBuilder { this.thing['state-proof-transaction'] = {} as StateProofTransactionResult return this } + + public heartbeatTransaction() { + this.thing['tx-type'] = algosdk.TransactionType.hb + this.thing['heartbeat-transaction'] = { + 'hb-address': randomString(52, 52), + } + return this + } } export const transactionResultBuilder = dossierProxy(TransactionResultBuilder) diff --git a/src/tests/object-mother/transaction-result.ts b/src/tests/object-mother/transaction-result.ts index 6cbbeeeb1..ac40d1d8a 100644 --- a/src/tests/object-mother/transaction-result.ts +++ b/src/tests/object-mother/transaction-result.ts @@ -63,6 +63,53 @@ export const transactionResultMother = { }, }) }, + ['mainnet-HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ']: () => { + return new TransactionResultBuilder({ + id: 'HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'close-rewards': 0, + 'closing-amount': 0, + 'confirmed-round': 34675056, + fee: 1000, + 'first-valid': 34675052, + 'genesis-hash': 'wGHE2Pwdvd7S12BL5FaOP20EGYesN73ktiC1qzkkit8=', + 'genesis-id': 'mainnet-v1.0', + 'intra-round-offset': 26, + 'last-valid': 34676052, + 'heartbeat-transaction': { + 'hb-address': 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW', + }, + 'receiver-rewards': 0, + 'round-time': 1703439471, + sender: 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW', + 'sender-rewards': 0, + signature: { + sig: 'HEARTBEATSIGNATURE123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ==', + }, + 'tx-type': TransactionType.hb, + }) + }, + 'localnet-HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ': () => { + return new TransactionResultBuilder({ + 'confirmed-round': 1000, + fee: 1000, + 'first-valid': 995, + 'genesis-hash': 'x9maOhZVCNkkZCgV6CcLpxd1ZgIgHwuAfg6fdG2FJo8=', + 'genesis-id': 'dockernet-v1', + 'heartbeat-transaction': { + 'hb-address': 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW', + }, + id: 'HEARTBEAT1234567890ABCDEFGHIJKLMNOPQRSTUVWXYZ', + 'last-valid': 1995, + 'round-time': 1234567890, + sender: 'HEARTBEATADDRESS123456789ABCDEFGHIJKLMNOPQRSTUVW', + 'tx-type': TransactionType.hb, + 'close-rewards': 0, + 'closing-amount': 0, + 'intra-round-offset': 0, + 'receiver-rewards': 0, + 'sender-rewards': 0, + }) + }, ['mainnet-FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ']: () => { return new TransactionResultBuilder({ id: 'FBORGSDC4ULLWHWZUMUFIYQLSDC26HGLTFD7EATQDY37FHCIYBBQ', diff --git a/tailwind.config.js b/tailwind.config.js index 2dbf8a19c..615162d48 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -76,6 +76,10 @@ export default { DEFAULT: 'rgb(var(--key-registration) / )', foreground: 'rgb(var(--key-registration-foreground) / )', }, + ['heartbeat']: { + DEFAULT: 'rgb(var(--heartbeat) / )', + foreground: 'rgb(var(--heartbeat-foreground) / )', + }, error: { DEFAULT: 'rgb(var(--error) / )', foreground: 'rgb(var(--error-foreground) / )',