Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 65 additions & 10 deletions packages/libs/dapp-kit/providers/SmartObjectProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,12 @@ import {
getInventoryEventTypes,
isRelevantAssemblyInventoryEvent,
} from '../utils/events/inventoryEventHandlers'
import {
applyStatusEventToAssembly,
getStatusEventTarget,
getStatusEventType,
isRelevantStatusEvent,
} from '../utils/events/statusEventHandlers'
import {
getInventoryTypeVolumeM3,
mergeSmartStorageInventoryFromRefetch,
Expand Down Expand Up @@ -126,6 +132,7 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
const lastDataHashRef = useRef<string | null>(null)
const lastConfirmedInventorySignatureRef = useRef<string | null>(null)
const lastConfirmedFuelSignatureRef = useRef<string | null>(null)
const lastConfirmedStateRef = useRef<string | null>(null)
const assemblyRef = useRef<AssemblyType<Assemblies> | null>(null)

assemblyRef.current = assembly
Expand Down Expand Up @@ -221,8 +228,26 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
primeInventoryTypeVolumes(transformed)

setAssembly((currentAssembly) => {
if (transformed?.type === Assemblies.NetworkNode) {
const { quantity, isBurning } = transformed.networkNode.fuel
if (transformed === null) return null

// State stale-refetch protection (all assembly types).
// If the indexer returns the same state we last confirmed, keep
// the optimistic state rather than reverting it.
let next: typeof transformed = transformed
const nextState = next.state
if (
!isInitialFetch &&
currentAssembly !== null &&
lastConfirmedStateRef.current !== null &&
nextState === lastConfirmedStateRef.current
) {
next = { ...next, state: currentAssembly.state }
} else {
lastConfirmedStateRef.current = nextState
}

if (next?.type === Assemblies.NetworkNode) {
const { quantity, isBurning } = next.networkNode.fuel
const nextSignature = `${quantity}:${isBurning}`

// On a refetch, if the indexer hasn't caught up yet it returns the
Expand All @@ -238,11 +263,11 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
}

lastConfirmedFuelSignatureRef.current = nextSignature
return transformed
return next
}

if (transformed?.type !== Assemblies.SmartStorageUnit) {
return transformed
if (next?.type !== Assemblies.SmartStorageUnit) {
return next
}

// Merge against the prior storage state only on refetches; an
Expand All @@ -256,7 +281,7 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
const { assembly, inventorySignature } =
mergeSmartStorageInventoryFromRefetch(
previousStorage,
transformed,
next,
lastConfirmedInventorySignatureRef.current,
)
lastConfirmedInventorySignatureRef.current = inventorySignature
Expand Down Expand Up @@ -353,6 +378,7 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
lastDataHashRef.current = null
lastConfirmedInventorySignatureRef.current = null
lastConfirmedFuelSignatureRef.current = null
lastConfirmedStateRef.current = null
}
}, [
selectedObjectId,
Expand All @@ -377,7 +403,15 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
const fuelEventTypes = getStreamEventTypes(selectedTenant, (pkg) => [
getFuelEventType(pkg),
])
const allEventTypes = [...inventoryEventTypes, ...fuelEventTypes]
const statusEventTypes = getStreamEventTypes(selectedTenant, (pkg) => [
getStatusEventType(pkg),
])

const allEventTypes = [
...inventoryEventTypes,
...fuelEventTypes,
...statusEventTypes,
]
const abortController = new AbortController()
const triggerRefetch = createEventRefetchScheduler(
() => fetchObjectData(input, false),
Expand Down Expand Up @@ -414,6 +448,10 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
assemblyRef.current,
fuelEventTypes,
)
const statusEventTarget = getStatusEventTarget(
assemblyRef.current,
statusEventTypes,
)

const relevantInventoryEvents = events.filter((event) =>
isRelevantAssemblyInventoryEvent(event, inventoryEventTarget),
Expand All @@ -423,27 +461,40 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
isRelevantFuelEvent(event, fuelEventTarget),
)
: []
const relevantStatusEvents = statusEventTarget
? events.filter((event) =>
isRelevantStatusEvent(event, statusEventTarget),
)
: []

if (
events.length > 0 &&
relevantInventoryEvents.length === 0 &&
relevantFuelEvents.length === 0
relevantFuelEvents.length === 0 &&
relevantStatusEvents.length === 0
) {
log.debug(
'[DappKit] SmartObjectProvider: Ignoring stream events for other assemblies',
{ count: events.length, inventoryEventTarget, fuelEventTarget },
{
count: events.length,
inventoryEventTarget,
fuelEventTarget,
statusEventTarget,
},
)
}

if (
relevantInventoryEvents.length > 0 ||
relevantFuelEvents.length > 0
relevantFuelEvents.length > 0 ||
relevantStatusEvents.length > 0
) {
log.info(
'[DappKit] SmartObjectProvider: Applying assembly stream events',
{
inventoryCount: relevantInventoryEvents.length,
fuelCount: relevantFuelEvents.length,
statusCount: relevantStatusEvents.length,
},
)
setAssembly((currentAssembly) => {
Expand All @@ -456,6 +507,10 @@ const SmartObjectProvider = ({ children }: { children: ReactNode }) => {
(asm, event) => applyFuelEventToAssembly(asm, event),
next,
)
next = relevantStatusEvents.reduce(
(asm, event) => applyStatusEventToAssembly(asm, event),
next,
)
return next
})
triggerRefetch()
Expand Down
22 changes: 11 additions & 11 deletions packages/libs/dapp-kit/utils/__tests__/fuelEventBcs.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest'

import {
decodeFuelEventBcs,
fuelEventBcsToParsedJson,
} from '../events/fuelEventBcs'
import { decodeFuelEventBcsToJson } from '../events/fuelEventBcs'

function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2)
Expand Down Expand Up @@ -31,9 +28,9 @@ const FUEL_EVENT_BCS_HEX =

describe('fuelEventBcs', () => {
it('decodes a FuelEvent from BCS bytes', () => {
const decoded = decodeFuelEventBcs(hexToBytes(FUEL_EVENT_BCS_HEX))

expect(fuelEventBcsToParsedJson(decoded)).toMatchObject({
expect(
decodeFuelEventBcsToJson(hexToBytes(FUEL_EVENT_BCS_HEX)),
).toMatchObject({
assembly_id:
'0x34d08b4e1afe6a4babcc0642d6a676160df6b777b49214d5c964b4e874cc951b',
assembly_key: {
Expand All @@ -48,13 +45,16 @@ describe('fuelEventBcs', () => {
})

it('decodes the action variant kind', () => {
const decoded = decodeFuelEventBcs(hexToBytes(FUEL_EVENT_BCS_HEX))
const json = fuelEventBcsToParsedJson(decoded)
const json = decodeFuelEventBcsToJson(hexToBytes(FUEL_EVENT_BCS_HEX))
// bcs.enum returns a discriminated union: { $kind: '<Variant>', <Variant>: true }
expect((json.action as Record<string, unknown>)?.$kind).toBe('WITHDRAWN')
expect((json['action'] as Record<string, unknown>)?.['$kind']).toBe(
'WITHDRAWN',
)
})

it('throws on malformed BCS bytes', () => {
expect(() => decodeFuelEventBcs(new Uint8Array([0x00, 0x01]))).toThrow()
expect(() =>
decodeFuelEventBcsToJson(new Uint8Array([0x00, 0x01])),
).toThrow()
})
})
13 changes: 4 additions & 9 deletions packages/libs/dapp-kit/utils/__tests__/inventoryEventBcs.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,6 @@
import { describe, expect, it } from 'vitest'

import {
decodeInventoryEventBcs,
inventoryEventBcsToParsedJson,
} from '../events/inventoryEventBcs'
import { decodeInventoryEventBcsToJson } from '../events/inventoryEventBcs'

function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2)
Expand All @@ -21,11 +18,9 @@ const INVENTORY_MOVE_EVENT_BCS_HEX =

describe('inventoryEventBcs', () => {
it('decodes an inventory move event from chain BCS bytes', () => {
const decoded = decodeInventoryEventBcs(
hexToBytes(INVENTORY_MOVE_EVENT_BCS_HEX),
)

expect(inventoryEventBcsToParsedJson(decoded)).toEqual({
expect(
decodeInventoryEventBcsToJson(hexToBytes(INVENTORY_MOVE_EVENT_BCS_HEX)),
).toEqual({
assembly_id:
'0x34d08b4e1afe6a4babcc0642d6a676160df6b777b49214d5c964b4e874cc951b',
assembly_key: {
Expand Down
66 changes: 66 additions & 0 deletions packages/libs/dapp-kit/utils/__tests__/statusEventBcs.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { describe, expect, it } from 'vitest'

import { decodeStatusChangedEventBcsToJson } from '../events/statusEventBcs'

function hexToBytes(hex: string): Uint8Array {
const bytes = new Uint8Array(hex.length / 2)
for (let index = 0; index < bytes.length; index += 1) {
bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16)
}
return bytes
}

// Manually constructed StatusChangedEvent BCS bytes matching the on-chain Move struct:
// assembly_id: 0x34d08b4e...cc951b (32 bytes)
// assembly_key: { item_id: 1 (u64 LE), tenant: "stillness" }
// status: ONLINE (variant index 2)
// action: ANCHORED (variant index 0)
const STATUS_CHANGED_EVENT_BCS_HEX =
'34d08b4e1afe6a4babcc0642d6a676160df6b777b49214d5c964b4e874cc951b' +
'0100000000000000' +
'09' +
'7374696c6c6e657373' +
'02' +
'00'

describe('statusEventBcs', () => {
it('decodes a StatusChangedEvent from BCS bytes', () => {
expect(
decodeStatusChangedEventBcsToJson(
hexToBytes(STATUS_CHANGED_EVENT_BCS_HEX),
),
).toMatchObject({
assembly_id:
'0x34d08b4e1afe6a4babcc0642d6a676160df6b777b49214d5c964b4e874cc951b',
assembly_key: {
item_id: '1',
tenant: 'stillness',
},
})
})

it('decodes the status variant kind', () => {
const json = decodeStatusChangedEventBcsToJson(
hexToBytes(STATUS_CHANGED_EVENT_BCS_HEX),
)
// bcs.enum returns a discriminated union: { $kind: '<Variant>', <Variant>: true }
expect((json['status'] as Record<string, unknown>)?.['$kind']).toBe(
'ONLINE',
)
})

it('decodes the action variant kind', () => {
const json = decodeStatusChangedEventBcsToJson(
hexToBytes(STATUS_CHANGED_EVENT_BCS_HEX),
)
expect((json['action'] as Record<string, unknown>)?.['$kind']).toBe(
'ANCHORED',
)
})

it('throws on malformed BCS bytes', () => {
expect(() =>
decodeStatusChangedEventBcsToJson(new Uint8Array([0x00, 0x01])),
).toThrow()
})
})
Original file line number Diff line number Diff line change
Expand Up @@ -1187,8 +1187,9 @@ describe('event refresh helpers', () => {
)

expect(events).toHaveLength(1)
expect(events[0].type).toBe(getFuelEventType(PACKAGE_ID))
expect(events[0].parsedJson).toMatchObject({
const fuelEvent = events[0]!
expect(fuelEvent.type).toBe(getFuelEventType(PACKAGE_ID))
expect(fuelEvent.parsedJson).toMatchObject({
assembly_id: ASSEMBLY_OBJECT_ID,
assembly_key: { item_id: '1', tenant: 'stillness' },
type_id: '77810',
Expand Down
Loading
Loading