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 @@
+
+
+
+
+
+
\ 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) / )',