@decentrys/ui-sdk
v0.1.2
Published
Drop-in React components for rendering Decentrys risk assessments, built so a new project can never be shown as dangerous for being new.
Maintainers
Readme
@decentrys/ui-sdk
Drop-in React components that render a Decentrys assessment — built so a new project can never be shown as dangerous for being new.
Install
npm install @decentrys/ui-sdk @decentrys/protectReact 18 or 19, as a peer dependency alongside react-dom. Never bundled — two
copies of React in one app is a real breakage. No styling dependency, no font
fetch, no network calls of its own.
Quick start
import { useEffect, useState } from 'react';
import { Decentrys, type ProtectResult, type TransactionRequest } from '@decentrys/protect';
import { DecentrysUiProvider, TransactionRiskBanner, RiskDetailsModal } from '@decentrys/ui-sdk';
const decentrys = new Decentrys({ apiKey: 'dk_pub_live_...' });
export function SigningScreen({ tx, onSign, onCancel }: {
tx: TransactionRequest;
onSign: () => void;
onCancel: () => void;
}) {
const [result, setResult] = useState<ProtectResult | null>(null);
const [detailsOpen, setDetailsOpen] = useState(false);
useEffect(() => {
let live = true;
decentrys.assessTransaction(tx).then((r) => { if (live) setResult(r); });
return () => { live = false; };
}, [tx]);
if (!result) return null;
return (
<DecentrysUiProvider theme={{ surface: '#0b0d10', text: '#e8ebef', radius: '12px' }}>
<TransactionRiskBanner
result={result}
onViewDetails={() => setDetailsOpen(true)}
actions={
<>
{/* Your buttons, your policy. This package never disables them. */}
<button onClick={onSign}>Confirm</button>
<button onClick={onCancel}>Cancel</button>
</>
}
/>
<RiskDetailsModal result={result} open={detailsOpen} onClose={() => setDetailsOpen(false)} />
</DecentrysUiProvider>
);
}DecentrysUiProvider sets theme and copy once for everything beneath it and
emits the stylesheet a single time. Without a provider every component emits
its own — so a single <AddressRiskBadge> dropped into a page still renders
correctly.
Placing the CSS yourself instead — in a <head>, a bundled stylesheet or a
shadow root — is <DecentrysUiProvider injectStyles={false}>, or export the
string directly:
import { DECENTRYS_UI_CSS } from '@decentrys/ui-sdk';
<style>{DECENTRYS_UI_CSS}</style>Components
Note which prop each one takes — some want the whole ProtectResult, some want
a piece of it.
| Component | Required props | Use it when |
|---|---|---|
| TransactionRiskBanner | result | A user is about to sign. The main one. |
| ApprovalWarning | result, approval | A token approval — leads with what is being granted, not a score. |
| ContractFacts | assessment | Showing what a contract can do (upgrade, mint, pause). |
| TokenSecurityPanel | result | A token detail screen. |
| ThreatSignalList | signals | You want signals only, in your own layout. |
| AddressRiskBadge | assessment or level | Compact inline badge beside an address. |
| RiskDetailsModal | result, open, onClose | "Why?" — every fact, signal, evidence item and score component. |
// An approval. `approval` describes what is being granted; the SDK does not
// infer it from the assessment.
<ApprovalWarning
result={approvalResult} // from decentrys.screenApproval(...)
approval={{
token: '0xa0b8…',
symbol: 'USDC',
decimals: 6,
spender: '0x1111…',
spenderLabel: '1inch Router',
amount: 'unlimited', // base units, or the literal 'unlimited'
}}
/>
// "This grants 1inch Router permission to spend an unlimited amount of your USDC,
// now and at any time in the future, until it is revoked."
//
// Omit `amount` and it says the allowance could not be decoded — never that it
// is small. Omit `decimals` and the figure is labelled as base units rather
// than silently shown 10^18 times too small.
<ContractFacts assessment={result.assessment} demotedSignals={result.demotedSignals} />
<TokenSecurityPanel result={result} tokenSymbol="USDC" showComponents />
<ThreatSignalList signals={result.assessment.threatSignals} />
<AddressRiskBadge assessment={result.assessment} address="0x1111…" />
<AddressRiskBadge level="CAUTION" /> {/* when you only have the level */}Every component also accepts className, style, theme, copy and
levelLabels. AddressRiskBadge takes exactly one of assessment or level,
enforced by the type rather than a runtime throw — a component that throws
inside a wallet's render tree takes that wallet's screen down.
What these will not do
Nothing blocks. DecisionPresentation.blocksUi is typed as the literal
false, including for a block policy action. No control here is ever
rendered disabled — the decision belongs to your application, and you wire
your own buttons through actions.
LIMITED history is never a warning. HistoryPresentation types tone as
the literal 'neutral' and isWarning as the literal false, so rendering it
as an alert doesn't compile. It's the correct, expected state for anything
recently deployed.
Facts never look like threat signals. fact, capability and unknown
are separate tone families from the risk tones, each row carries its kind as a
word and its own non-colour glyph, and a stale or low-confidence signal that
did not raise the level is toned inactive. Colour is the third carrier of
meaning, not the first — a colour-blind user still has to be able to tell
"deployed three days ago" from "attributed to a drainer family".
Anything above zero hops says so in a sentence, not just a badge. A hop is an inference about a counterparty, and a badge reading "2" tells nobody that.
Theming
31 CSS custom properties. Pass them by their bare token name — the
--dcy- prefix is added for you:
<TransactionRiskBanner
result={result}
theme={{ surface: '#1a1a2e', radius: '12px', 'tone-caution': '#ab9ff2' }}
className="my-wallet-card"
/>The tokens (THEME_TOKENS is exported, and DecentrysTheme is
Partial<Record<ThemeToken, string>>):
- Layout and type —
font,font-mono,radius,gap - Surfaces and text —
surface,surface-raised,border,text,text-muted,text-faint,focus - A foreground and a
-bgwash for each of ten tones —tone-neutral,tone-info,tone-caution,tone-elevated,tone-high,tone-critical,tone-fact,tone-capability,tone-unknown,tone-inactive
Inline theme is applied on the component's own root, so it beats both the
defaults and the prefers-color-scheme dark block — a fixed brand palette
doesn't flip on a user's OS setting. Props win over the provider, so one banner
can be re-themed inside an otherwise uniform tree.
Every meaningful element also carries a data-dcy-* attribute
(data-dcy-level, data-dcy-kind, data-dcy-tone) if you'd rather style from
your own CSS.
Copy and translation
Every string is overridable, on the provider or per component:
<DecentrysUiProvider copy={{ /* Partial<UiCopy> */ }}>
<TransactionRiskBanner result={result} levelLabels={{ CAUTION: 'Attention' }} />DEFAULT_COPY and resolveCopy are exported.
Building your own presentation
The presentation logic is pure, React-free and separately exported, so a renderer you write yourself cannot drift from the one here:
import {
levelPresentation, factPresentation, capabilityPresentation, signalPresentation,
historyPresentation, unknownPresentation, approvalPresentation, assessmentSections,
componentRows, decisionPresentation, evidencePresentation, availabilityPresentation,
hopPresentation, formatConfidence, formatTokenAmount, shortenAddress,
MIN_RAISING_CONFIDENCE, RISK_LEVEL_LABEL,
} from '@decentrys/ui-sdk';MIN_RAISING_CONFIDENCE is re-exported from @decentrys/protect rather than
copied, so a renderer cannot put a signal in the "did not raise the level" list
while the classifier was in fact raising it.
React Native
These render React DOM and won't work in RN. The presentation logic above is React-free and reusable as-is.
The rest of the SDK
| Package | For |
|---|---|
| @decentrys/protect | Pre-sign risk assessment for wallets and dapps |
| @decentrys/ui-sdk | React components that render Protect results |
| @decentrys/sentinel-sdk | Monitoring deployed contracts and treasuries |
| @decentrys/risk-sdk | Screening for exchanges and custodians |
| @decentrys/dri-sdk | Fund tracing and recovery intelligence |
| @decentrys/agent | Policy enforcement for autonomous agents |
Licence
MIT © Decentrys Labs
