
Research
2025 Report: Destructive Malware in Open Source Packages
Destructive malware is rising across open source registries, using delays and kill switches to wipe code, break builds, and disrupt CI/CD.
@penumbra-zone/react
Advanced tools
React package for connecting to any Penumbra extension, including Prax.
@penumbra-zone/reactThis package contains a React context provider and some simple hooks for using
the page API described in @penumbra-zone/client. You might want to use this if
you're writing a Penumbra dapp in React.
To use this package, you need to enable the Buf Schema Registry:
npm config set @buf:registry https://buf.build/gen/npm/v1
The components in this package interact with a browser extension, so can only be
executed in a browser, not in any server-side rendering context. To encourage
this, <PenumbraContextProvider> uses the penumbra input prop which may only
be obtained client-side. It's recommended to use methods from
@penumbra-zone/client to obtain this value, as described below.
If a user has a Penumbra provider in their browser, it may be present (injected)
in the record at the window global window[Symbol.for('penumbra')] identified
by a URL origin at which the provider can serve a manifest. For example, Prax
Wallet's origin is chrome-extension://lkpmkhpnhknhmibgnmmhdhgdilepfghe, so its
provider record may be accessed like
const prax: PenumbraProvider | undefined =
window[Symbol.for('penumbra')]?.['chrome-extension://lkpmkhpnhknhmibgnmmhdhgdilepfghe'];
or with helpers available from @penumbra-zone/client, like
import { assertProvider } from '@penumbra-zone/client';
const prax = assertProvider('chrome-extension://lkpmkhpnhknhmibgnmmhdhgdilepfghe');
Use of <PenumbraContextProvider> with a penumbra prop identifying your
provider will result in automatic progress towards a successful connection.
Connection requires user approval, so it's recommended provide UI on your page
controlling the makeApprovalRequest prop.
Hooks usePenumbraTransport and usePenumbraService will promise a transport
or client that inits when the configured provider becomes connected, or rejects
with a failure before connection.
Hooks usePenumbraTransportSync or usePenumbraServiceSync will
unconditionally provide a transport or client to the Penumbra extension that
queues requests while connection is pending, and begins returning responses when
appropriate. If the provider fails to connect, requests via the transport or
client may time out.
<PenumbraContextProvider>This wrapping component will provide a context available to all child components
that is directly accessible by usePenumbra, or by usePenumbraTransport or
usePenumbraService. Accepts a makeApprovalRequest prop, off by default, to
configure conditional use of the request method of the Penumbra interface,
which may trigger a popup or require user interaction.
@connectrpc/connect-queryIf you'd like to use @connectrpc/connect-query, you may call
usePenumbraTransport to satisfy <TransportProvider>.
Be aware that connect query only supports unary requests at the moment (no streaming).
A wrapping component:
import { Outlet } from 'react-router-dom';
import { assertProvider } from '@penumbra-zone/client';
import { PenumbraContextProvider } from '@penumbra-zone/react';
import { usePenumbraTransportSync } from '@penumbra-zone/react/hooks/use-penumbra-transport';
import { TransportProvider } from '@connectrpc/connect-query';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
const praxOrigin = 'chrome-extension://lkpmkhpnhknhmibgnmmhdhgdilepfghe';
const queryClient = new QueryClient();
export const PenumbraDappPage = () => (
<PenumbraContextProvider penumbra={assertProvider(praxOrigin)} makeApprovalRequest>
<TransportProvider transport={usePenumbraTransportSync()}>
<QueryClientProvider client={queryClient}>
<Outlet />
</QueryClientProvider>
</TransportProvider>
</PenumbraContextProvider>
);
A querying component:
import { addressByIndex } from '@buf/penumbra-zone_penumbra.connectrpc_query-es/penumbra/view/v1/view-ViewService_connectquery';
import { useQuery } from '@connectrpc/connect-query';
import { bech32mAddress } from '@penumbra-zone/bech32m/penumbra';
export const PraxAddress = ({ account }: { account?: number }) => {
// note this is not tanstack's useQuery
const { data } = useQuery(addressByIndex, { addressIndex: { account } });
return data?.address && bech32mAddress(data.address);
};
PromiseClientIf you'd like to make streaming queries, or you just want to manage queries
yourself, you can call usePenumbraService with the ServiceType you're
interested in to acquire a PromiseClient of that service. A simplistic example
is below.
Some streaming queries may return large amounts of data, or stream updates continuosuly until aborted. For a good user experience with those queries, you may need more complex query and state management.
import { AssetId } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/core/asset/v1/asset_pb.js';
import { BalancesResponse } from '@buf/penumbra-zone_penumbra.bufbuild_es/penumbra/view/v1/view_pb.js';
import { usePenumbraServiceSync } from '@penumbra-zone/react/hooks/use-penumbra-service';
import { ViewService } from '@penumbra-zone/protobuf';
import { useQuery } from '@tanstack/react-query';
import { AccountBalancesTable } from './imaginary-components';
export default function AssetBalancesByAccount({ assetIdFilter }: { assetIdFilter: AssetId }) {
const viewClient = usePenumbraServiceSync(ViewService);
const { isPending, data: groupedBalances } = useQuery({
queryKey: ['balances', assetIdFilter.inner],
queryFn: ({ signal }): Promise<BalancesResponse[]> =>
// wait for stream to collect
Array.fromAsync(viewClient.balances({ assetIdFilter }, { signal })),
select: (data: BalancesResponse[]) =>
Map.groupBy(
// filter undefined
data.filter(({ balanceView, accountAddress }) => accountAddress?.addressView?.value),
// group by account
({ accountAddress }) => accountAddress.addressView.value.index,
),
});
if (isPending) return <LoadingSpinner />;
if (groupedBalances)
return Array.from(groupedBalances.entries()).map(([accountIndex, balanceResponses]) => (
<AccountBalancesTable key={accountIndex} asset={assetIdFilter} balances={balanceResponses} />
));
}
Each Penumbra provider exposes a simple .isConnected() method and a more
complex .state() method, which also tracks pending transitions. It is
generally robust and should asynchronously progress towards an active connection
if possible, even if steps are performed slightly 'out-of-order'.
This package's exported <PenumbraContextProvider> component handles this state
and all of these transitions for you.
During this progress, the context exposes an explicit status, so you may easily
condition your layout and display. You can access this status via
usePenumbra().state. All possible values are represented by the enum
PenumbraState available from @penumbra-zone/client.
Hooks usePenumbraTransportSync and usePenumbraServiceSync conceal this
state, and unconditionally provide a transport or client.
Connected is the only state in which a MessagePort, working Transport, or
working client is available.
This flowchart reads from top (page load) to bottom (page unload). Each labelled
chart node is a possible value of PenumbraState. Diamond-shaped nodes
are conditions described by the surrounding path labels.
There are more possible transitions than diagrammed here - for instance once
methods are exposed, a disconnect() call will always transition directly into
a Disconnected state. A developer not using this wrapper, calling methods
directly, may enjoy failures at any moment. This diagram only represents a
typical state flow.
The far right side path is the "happy path".
stateDiagram-v2
classDef GoodNode fill:chartreuse
classDef BadNode fill:salmon
classDef PossibleNode fill:thistle
state global_exists <<choice>>
state manifest_present <<choice>>
state make_request <<choice>>
[*] --> global_exists: p = window[Symbol.for('penumbra')][validOrigin]
global_exists --> [*]: undefined
Failed:::BadNode --> [*]: p.failure
Disconnected --> [*]
Connected:::GoodNode --> [*]
manifest_present --> Failed
RequestPending --> Failed
ConnectPending --> Failed
global_exists --> manifest_present: fetch(p.manifest)
manifest_present --> Present: json
Present:::PossibleNode --> make_request: makeApprovalRequest
make_request --> RequestPending: p.request()
RequestPending:::PossibleNode --> Requested
Requested:::PossibleNode --> ConnectPending: p.connect()
make_request --> ConnectPending: p.connect()
ConnectPending:::PossibleNode --> Connected:::PossibleNode
Connected --> Disconnected: p.disconnect()
note left of Present
Methods on the injection may
be called after this point.
end note
note left of Connected
Port is acquired and
transports become active.
end note
FAQs
React package for connecting to any Penumbra extension, including Prax.
We found that @penumbra-zone/react demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 3 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Research
Destructive malware is rising across open source registries, using delays and kill switches to wipe code, break builds, and disrupt CI/CD.

Security News
Socket CTO Ahmad Nassri shares practical AI coding techniques, tools, and team workflows, plus what still feels noisy and why shipping remains human-led.

Research
/Security News
A five-month operation turned 27 npm packages into durable hosting for browser-run lures that mimic document-sharing portals and Microsoft sign-in, targeting 25 organizations across manufacturing, industrial automation, plastics, and healthcare for credential theft.