Agentic Trading Lab
에이전트와 대화하고, 거래 아이디어를 테스트하며, 코드 없이 실제 시장 데이터에 AI 트레이딩 에이전트를 적용
The Trustware SDK provides a React provider, prebuilt UI widget, and typed core API for bridging and top-up routes. It powers seamless fund transfers across chains, reusing resolved configurations for quoting, route selection, and transaction execution.
저장소 README에서 가져옴
README 전체를 불러오는 중입니다.
README 전문 보기 ↗Any chain. Any token. Any wallet. One drop-in widget for everyone.
📚 Docs · 🌐 Website · 📦 npm · 💬 Issues
📚 Full documentation is now at docs.trustware.io. This README covers installation and a quick start — see the docs site for the complete API reference, integration guides, and examples.
Trustware SDK gives you three integration styles on top of the same routing and wallet infrastructure:
The current widget flow is:
Home -> Select Token -> Confirm Deposit -> Processing -> Success/Error
npm install @trustware/sdk
# or
pnpm add @trustware/sdk
Supports React 18.2+ and 19.
TrustwareProvideruseTrustwareTrustwareWidgetTrustwareTrustwareErrorRateLimitErrorassertValidPostHookassertRouteDeliversValue, isValueDestroying, routeNetUsdwalletManageruseWalletDetectionuseWalletInfoconnectDetectedWalletWagmiBridgeuseWagmiuseEIP1193WALLETS./identity, ./validation/address, ./types, ./constantsimport {
TrustwareProvider,
TrustwareWidget,
type TrustwareConfigOptions,
} from "@trustware/sdk";
const trustwareConfig = {
apiKey: process.env.NEXT_PUBLIC_TRUSTWARE_API_KEY!,
routes: {
toChain: "8453",
toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE",
defaultSlippage: 1,
options: {
routeRefreshMs: 15000,
},
},
autoDetectProvider: true,
messages: {
title: "Deposit",
description: "Move funds into the destination asset and chain.",
},
} satisfies TrustwareConfigOptions;
export function App() {
return (
<TrustwareProvider config={trustwareConfig}>
<TrustwareWidget />
</TrustwareProvider>
);
}
TrustwareConfigOptions is the single source of truth. The shape is a discriminated union
keyed on mode — routes is only required in the default "deposit" mode:
type TrustwareConfigOptions =
| {
apiKey: string;
mode?: "deposit"; // default, omit this field entirely for a normal deposit/top-up widget
routes: {
toChain: string;
toToken: string;
fromToken?: string;
fromChain?: string;
fromAddress?: string;
toAddress?: string;
defaultSlippage?: number;
options?: {
routeRefreshMs?: number;
fixedFromAmount?: string | number;
minAmountOut?: string | number;
maxAmountOut?: string | number;
};
};
autoDetectProvider?: boolean;
theme?: "light" | "dark" | "system"; // TrustwareTheme
messages?: Partial<TrustwareWidgetMessages>;
retry?: RetryConfig;
walletConnect?: WalletConnectConfig;
features?: FeatureFlags;
onError?: (error: TrustwareError) => void;
onSuccess?: (transaction: Transaction) => void;
onEvent?: (event: TrustwareEvent) => void;
}
| {
apiKey: string;
mode: "swap"; // swap widget — from/to chain+token are chosen entirely in-widget
routes?: Partial<TrustwareConfigOptions["routes"]>; // optional, not required
// ...same common fields as above
};
"deposit" (default — mode can be omitted): the standard bridge/top-up widget.
routes.toChain and routes.toToken are required.
"swap": an in-widget token swap UI where the user picks both sides themselves.
routes is not required at all:
const trustwareConfig = {
apiKey: process.env.NEXT_PUBLIC_TRUSTWARE_API_KEY!,
mode: "swap",
} satisfies TrustwareConfigOptions;
Use features.swapDefaultDestToken / features.swapLockDestToken /
features.swapAllowedDestTokens to steer the destination token selection (see
FeatureFlags in src/types/config.ts). Each token is { address, chainId },
where chainId is the chain's id in the routes catalog: a number for EVM chains,
or the catalog's string id otherwise. Native SOL, for example, is
{ chainId: "solana-mainnet-beta", address: "11111111111111111111111111111111" }.
The older
features: { swapMode: true }flag (combined with aroutesobject) still works and is treated as equivalent tomode: "swap", but is deprecated in favor of themodefield above.
routes.toChain: destination chain key or chain id string.routes.toToken: destination token address or registry token identifier.routes.fromToken: optional source token preference.routes.fromChain: optional source chain preference.routes.fromAddress: optional source wallet override.routes.toAddress: optional destination address override.routes.defaultSlippage: optional slippage percentage. Defaults to 1.Chain and token fields are strings, including numeric chain ids — "8453", not
8453.
routes.options.routeRefreshMs: how often the quote on the review screen
refreshes, in milliseconds, in both deposit and swap mode. Swap mode's
countdown ring runs over this interval. Defaults to 60000. Anything but a
number from 1 to 2147483647 is refused at init with an
INVALID_CONFIG error. A refresh never lands while the user is confirming,
so the route they confirmed is the one that's sent.routes.options.fixedFromAmount: locks the widget amount input to a fixed USD amount.routes.options.minAmountOut: minimum allowed USD amount.routes.options.maxAmountOut: maximum allowed USD amount.autoDetectProvider: enables Trustware-managed wallet discovery. Defaults to
false — set it to true if you are not attaching a wallet yourself.theme: widget color mode — "light" | "dark" | "system" (default
"system"). Switch it at runtime with Trustware.setTheme("dark").messages: top-level copy overrides.retry: rate-limit observability callbacks — see Rate Limiting.walletConnect: WalletConnect overrides.features: feature rollout controls, including swap-mode token selection constraints.onError, onSuccess, onEvent: lifecycle callbacks — see Events.Use this when your app does not already manage a connected wallet.
import { TrustwareProvider, TrustwareWidget } from "@trustware/sdk";
export function DepositPanel() {
return (
<TrustwareProvider config={trustwareConfig}>
<TrustwareWidget />
</TrustwareProvider>
);
}
Use this mode when:
autoDetectProvider should stay enabledUse this when your app already controls wallet connection through Wagmi or another adapter.
import { useMemo } from "react";
import { useWalletClient } from "wagmi";
import { TrustwareProvider, TrustwareWidget } from "@trustware/sdk";
import { useWagmi } from "@trustware/sdk/wallet";
export function DepositPanel() {
const { data: walletClient } = useWalletClient();
const wallet = useMemo(
() => (walletClient ? useWagmi(walletClient) : undefined),
[walletClient]
);
return (
<TrustwareProvider
config={trustwareConfig}
wallet={wallet}
autoDetect={false}
>
<TrustwareWidget />
</TrustwareProvider>
);
}
useWagmi (like useEIP1193) is a plain adapter factory, not a React hook,
despite the use prefix. If your ESLint setup runs react-hooks/rules-of-hooks
it will flag the call inside the useMemo callback above; silence it with an
eslint-disable-next-line react-hooks/rules-of-hooks comment — the call is
safe. See docs.trustware.io
for the same pattern with embedded wallets.
Use this mode when:
TrustwareWidget also supports basic shell control through props and a ref.
import { useRef } from "react";
import { TrustwareProvider, TrustwareWidget } from "@trustware/sdk";
// The widget's prop and ref types live on the widget entry, not the root barrel.
import type { TrustwareWidgetRef } from "@trustware/sdk/react";
export function ControlledWidget() {
const widgetRef = useRef<TrustwareWidgetRef>(null);
return (
<TrustwareProvider config={trustwareConfig}>
<button onClick={() => widgetRef.current?.open()}>Open</button>
<TrustwareWidget
ref={widgetRef}
defaultOpen={false}
initialStep="home"
showThemeToggle={false}
onOpen={() => console.log("opened")}
onClose={() => console.log("closed")}
/>
</TrustwareProvider>
);
}
Current widget props:
theme?: "light" | "dark" | "system" — initial mode only; Trustware.setTheme() drives it afterwardsinitialStep?: "home" | "select-token" | "crypto-pay" | "processing" | "success" | "error"defaultOpen?: boolean — defaults to true, which is what you want for inline usagestyle?: React.CSSProperties — merged onto the widget shellonOpen?: () => voidonClose?: () => voidshowThemeToggle?: boolean — defaults to trueAnd the ref (TrustwareWidgetRef): open(), close(), isOpen(). close()
asks for confirmation first if a transaction is in flight, so it is safe to wire
straight to a dismiss button.
Use this when you want Trustware’s routing and wallet plumbing without the widget UI.
import { Trustware } from "@trustware/sdk";
// buildRoute states both sides explicitly. fromAmount is in the source token's
// smallest unit, and the estimate comes back on the route — there is no
// separate quote call.
const route = await Trustware.buildRoute({
fromChain: "1",
fromToken: "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", // USDC on Ethereum
toChain: "8453",
toToken: "0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE", // native on Base
fromAmount: "25000000", // 25 USDC (6 decimals)
fromAddress: await Trustware.getAddress(),
toAddress: "0xDestination...",
});
console.log(route.route?.estimate, route.finalExchangeRate);
// Or let the SDK own route + submit + receipt + polling. Missing route fields
// come from the provider config; the sender comes from the attached wallet.
const tx = await Trustware.runTopUp({ fromAmount: "25000000" });
console.log(tx.status, tx.destTxHash);
A Solana route's transaction carries a blockhash that expires about a minute
after the route is built. If you show the route to the user and send it with
sendRouteTransaction after they confirm, the transaction may already be
expired, and the network drops it without an error. Build with
buildRouteForReview and send with sendReviewedRoute instead. On a Solana
route, sendReviewedRoute builds the route again from the same request right
before the wallet signs. On an EVM route it sends the route you reviewed.
import { Trustware } from "@trustware/sdk";
const reviewed = await Trustware.buildRouteForReview({
fromChain: "solana-mainnet-beta",
fromToken: "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", // USDC on Solana
toChain: "8453",
toToken: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", // USDC on Base
fromAmount: "25000000",
fromAddress: await Trustware.getAddress(),
toAddress: "0xDestination...",
slippage: 0.5, // required for a Solana route: it bounds the rebuild
});
console.log(reviewed.result.route?.estimate); // show this to the user
// After the user confirms. If signing takes so long that the Solana
// blockhash expires, the SDK rebuilds the route and asks the wallet once
// more. onRetry fires just before that second prompt: show its message.
const sent = await Trustware.sendReviewedRoute(reviewed, undefined, {
onRetry: (retry) => showNotice(retry.message), // your own UI
});
if (sent.kind === "route_changed") {
// Nothing was sent: the rebuilt route gives less than the slippage allows,
// or spends more. Show sent.rebuilt for review, and when the user confirms
// again, pass it to sendReviewedRoute.
} else {
// The receipt is already on its way. Don't call submitReceipt.
const result = await Trustware.pollStatus(sent.receipt.intentId);
}
sendReviewedRoute delivers the receipt itself, for EVM and Solana routes
alike, as soon as the network accepts the transaction, and retries it if
the post fails. Don't call submitReceipt after it. sent.receipt says
what was delivered: on a Solana route its intent is the rebuilt route's,
not the one you reviewed, so poll status with sent.receipt.intentId. A
failed rebuild throws, and nothing is sent. The automatic re-sign happens at most once: if the second signature
also takes too long, sendReviewedRoute throws an "expired before
inclusion" error. The types BuiltRoute, ReviewedRouteSend,
MaterialRouteChange, SendReviewedRouteOptions and SigningRetry are
exported from @trustware/sdk/core.
Every route carries expiresAt (a Date, in the server's time) and
serverClockOffsetMs, read from the response's Date header, so expiry is
judged on the server's clock however far off the device's is. Send it
before then; after it,
sendRouteTransaction, sendReviewedRoute and runTopUp throw
RouteExpiredError and send nothing. Build a new route instead.
import { RouteExpiredError, ReceiptCommitError } from "@trustware/sdk";
try {
await Trustware.sendReviewedRoute(reviewed);
} catch (err) {
if (err instanceof RouteExpiredError) {
// Nothing was sent. Build a new route and show it for review.
}
}
// If you commit yourself (after sendRouteTransaction), branch on `code`:
try {
await Trustware.submitReceipt(route.intentId, hash);
} catch (err) {
if (err instanceof ReceiptCommitError && err.retryable) {
// replacement_not_visible, signature_still_valid, tx_not_visible or
// idempotency_key_in_flight: retry shortly. Every other code is final.
}
}
sendReviewedRoute also commits the new hash when the wallet speeds up or
replaces an EVM transaction (EIP-1193 wallets). getStatus can report
"expired", which is terminal. Treat any status you don't recognize as
not finished, and keep polling.
See docs.trustware.io for the full headless flow.
const fixedAmountConfig = {
...trustwareConfig,
routes: {
...trustwareConfig.routes,
options: {
fixedFromAmount: "25",
},
},
} satisfies TrustwareConfigOptions;
const guardedConfig = {
...trustwareConfig,
routes: {
...trustwareConfig.routes,
options: {
minAmountOut: "10",
maxAmountOut: "250",
routeRefreshMs: 10000,
},
},
} satisfies TrustwareConfigOptions;
import { Trustware } from "@trustware/sdk";
Trustware.setDestinationAddress("0xDestination...");
Trustware is a plain object, not a class or an event emitter — there is no
Trustware.on(). Events reach you through config.onEvent.
Lifecycle and config
Trustware.init(config) loads the config and validates the API key once.Trustware.getConfig() returns the resolved config.Trustware.setTheme(mode) / getTheme() switch the widget's color mode at runtime.Trustware.setDestinationAddress/Chain/Token() update the route defaults in place.Wallets
Trustware.useWallet(wallet) attaches a wallet imperatively.Trustware.getWallet() and Trustware.getAddress() expose the active wallet.Trustware.autoDetect() can still be used if you want SDK-managed discovery outside the widget.Trustware.getIdentity() / resolveAddressForChain() / addIdentityAddress() handle
multi-chain identities, so an EVM address and a Solana address can belong to one user.Routes, status and balances
buildRoute, buildDepositAddress — build a route, or an address to deposit into.submitReceipt, submitStepReceipt, sendRouteTransaction, runTopUp — execution.buildRouteForReview, sendReviewedRoute — build a route for the user to review, then send it, rebuilding a Solana route right before signing.getStatus, pollStatus — read a transaction through to a terminal state.getBalances, getBalancesByAddress, getBalancesByAddressStream.useChains, useTokens — React hooks over the chain and token registries.validateAddressForChain, validateRouteAddresses — check addresses before you spend a request.There is no getQuote: the estimate is part of the route, on
route.route?.estimate and route.finalExchangeRate.
buildRoute, buildDepositAddress and sendRouteTransaction refuse a route
whose fees exceed what it delivers, throwing a RouteError with code
fees_exceed_output. A route the provider did not price in USD is not refused.
routeNetUsd(estimate) / isValueDestroying(estimate) expose the same check.
An address the backend cannot accept on its chain (wrong EIP-55 checksum, not a
Solana public key) comes back as a 400 RouteError with code invalid_address
and one rejected provider outcome per provider. No provider was called; fix
the request rather than retry it. validateRouteAddresses catches the same
cases client-side before the request is spent.
useTrustware() returns { status, errors, core, emitError, emitSuccess, emitEvent, revalidate }. status is "idle" | "initializing" | "ready" | "error" — gate anything that calls the API on "ready". core is the same
Trustware facade, handed to you so you don't have to import it separately, and
revalidate() re-runs init after a failure (an API key arriving late, for
instance).
For ERC-4337 flows, @trustware/sdk/smart-account sends a route as a user
operation rather than a wallet transaction:
import {
createTrustwareSmartAccountClient,
sendRouteAsUserOperation,
} from "@trustware/sdk/smart-account";
It is a separate entry point on purpose — 65 KB gzipped, and nothing pulls it in unless you import it.
sendRouteAsUserOperation commits the userOp hash as soon as the bundler
accepts it, so don't call submitReceipt after it. result.receipt is what
was committed and result.receiptDelivery says how that ended. result.txHash
is the bundle transaction, when it was seen within 45 s; it is undefined
otherwise, and the backend tracks the userOp either way.
config.onEvent receives a discriminated TrustwareEvent:
errortransaction_startedtransaction_successwallet_connectedtoken_page_loadedtoken_page_errorbalance_stream_chunkbalance_stream_fallbackswap_route_changedThe API is rate limited per API key, and that limit is shared by everyone using
your key — not per end user. Your key's limit, its remaining requests and the
window boundary come back on every response (X-RateLimit-Limit, -Remaining,
-Reset, plus Retry-After on a 429).
The SDK retries 429s on the schedule those headers state — it keeps no schedule of its own to disagree with them. It waits exactly as long as the server asks, for as long as the total stays under 10 seconds. Past that it stops and throws, because how long your app can afford to block is the one thing the server can't know. If a response carries no timing at all, the SDK stops rather than guessing: the limit is a fixed window, so an invented delay either lands inside the same closed window or overshoots one it could have read exactly.
None of that is configurable — the limit is enforced server-side, so no client
setting can widen it. If you need more headroom, ask us to raise the limit on
your key. retry configures observability only:
const config = {
...trustwareConfig,
retry: {
onRateLimitInfo: (info) => console.debug(info.remaining, "requests left"),
onRateLimited: (info, attempt) => console.warn("429", attempt, info),
onRateLimitApproaching: (info) => showSoftWarning(info),
approachingThreshold: 5,
},
} satisfies TrustwareConfigOptions;
RateLimitError distinguishes the two ways the SDK gives up:
retriesExhausted: false — the wait is known and simply longer than the SDK
will block for. rateLimitInfo.retryAfter holds it, so show the user when to
come back instead of leaving them on a spinner.retriesExhausted: true — the response carried no usable timing, so there was
nothing to retry against.Full API reference, integration guides, and examples live at docs.trustware.io.