Trustless Work React Library
@trustless-work/escrow v5 — React/TypeScript client for Trustless Work Core API v2 escrows.
| Surface | What it does |
|---|---|
| REST operate | Build unsigned XDR → you sign → sendTransaction |
| REST reads | GET /escrows* (list, detail, events, milestones, financial) |
| GraphQL reads | POST /graphql (escrow / escrows) |
Identity is always contractId (Soroban C…). Types: single-release | multi-release.
Auth, users, platforms, wallets, admin, and access grants are out of scope for this package.
npm install @trustless-work/escrow@5
# or
yarn add @trustless-work/escrow@5
# or
pnpm add @trustless-work/escrow@5Peer dependencies: react and react-dom >=18 <20.
If you publish/install under an npm dist-tag (e.g. beta), use that tag instead of @5 so latest can stay on the audited Core v1 line.
"use client";
import {
development,
TrustlessWorkConfig,
} from "@trustless-work/escrow";
export function TrustlessWorkProvider({
children,
}: {
children: React.ReactNode;
}) {
const apiKey = process.env.NEXT_PUBLIC_API_KEY || "";
return (
<TrustlessWorkConfig baseURL={development} apiKey={apiKey}>
{children}
</TrustlessWorkConfig>
);
}Optional wallet-session auth and default platform header:
<TrustlessWorkConfig
baseURL={development}
apiKey={apiKey}
getAccessToken={() => sessionToken}
defaultHeaders={{ "X-TW-Platform": platformId }}
>
{children}
</TrustlessWorkConfig>| Prop | Role |
|---|---|
baseURL |
Core API host (development / mainNet helpers, or any URL string) |
apiKey |
x-api-key header |
getAccessToken |
Optional Bearer token getter (re-read per request) |
defaultHeaders |
Merged into every request (e.g. X-TW-Platform) |
import { TrustlessWorkClient, development } from "@trustless-work/escrow";
const client = new TrustlessWorkClient({
baseURL: development,
apiKey: process.env.API_KEY,
});
await client.rest.listEscrows({ scope: "mine", limit: 20 });
await client.graphql.getEscrow({ contractId });| Import | Contains |
|---|---|
@trustless-work/escrow |
Config, client, REST + GraphQL hooks, types, errors |
@trustless-work/escrow/rest |
EscrowRestService + REST hooks only |
@trustless-work/escrow/graphql |
GraphQL service, documents, GraphQL hooks |
@trustless-work/escrow/hooks |
All hooks (REST + GraphQL) |
@trustless-work/escrow/hooks/rest |
REST hooks only |
@trustless-work/escrow/hooks/graphql |
GraphQL hooks only |
@trustless-work/escrow/types |
Payloads, responses, read-model, entities |
import { useEscrowRest, useEscrowGraphql } from "@trustless-work/escrow";
import { useGraphqlGetEscrow } from "@trustless-work/escrow/hooks/graphql";
const rest = useEscrowRest(); // operate + GET /escrows*
const graphql = useEscrowGraphql(); // POST /graphql
const { getEscrow } = useGraphqlGetEscrow();Every mutate hook returns an unsigned transaction. You sign with the wallet, then submit.
import {
useDeployEscrow,
useSendTransaction,
toTrustlessWorkError,
TrustlessWorkApiError,
formatApiErrorMessage,
} from "@trustless-work/escrow";
import type { DeploySingleReleaseEscrowPayload } from "@trustless-work/escrow/types";
const { deployEscrow } = useDeployEscrow();
const { sendTransaction } = useSendTransaction();
const onSubmit = async (payload: DeploySingleReleaseEscrowPayload) => {
try {
const { unsignedXdr, contractId } = await deployEscrow(
payload,
"single-release",
// optional attribution → X-TW-Platform / X-TW-Subject:
// { platformId, subjectId }
);
const signedXdr = await signWithWallet(unsignedXdr);
const result = await sendTransaction(signedXdr);
// result.txHash, result.ledger, result.contractId?, result.escrow?, result.code?
if (result.code === "STELLAR_TX_SUBMITTED_INDEXER_LAGGING") {
// Tx is on-chain; poll GET /escrows/:contractId until the indexer catches up
}
} catch (error: unknown) {
const normalized = toTrustlessWorkError(error);
if (normalized instanceof TrustlessWorkApiError) {
console.error(formatApiErrorMessage(normalized), normalized.code);
}
throw error;
}
};Deploy trustline shape: { contractId, symbol } (Soroban SAC C… + asset code).
Operate responses:
| Step | Type | Shape |
|---|---|---|
| Deploy build | DeployEscrowResponse |
{ unsignedXdr, txHash, contractId } |
| Other builds | BuildTransactionResponse |
{ unsignedXdr, txHash } |
| Submit | SendTransactionResponse |
{ txHash, ledger, contractId?, escrow?, code?, message? } |
code may be STELLAR_TX_SUBMITTED or STELLAR_TX_SUBMITTED_INDEXER_LAGGING.
Import from @trustless-work/escrow, @trustless-work/escrow/hooks, or @trustless-work/escrow/hooks/rest.
| Hook | Action |
|---|---|
useDeployEscrow |
Create escrow (unsignedXdr + predicted contractId) |
useFundEscrow |
Fund |
useUpdateEscrow |
Update properties |
useChangeMilestoneStatus |
Status / evidence (batch) |
useApproveMilestones |
Approve (batch) |
useApproveAndReleaseMilestones |
Approve + release (multi-release) |
useManageMilestones |
Add / edit milestones |
useReleaseFunds |
Release |
useStartDispute |
Start dispute |
useResolveDispute |
Resolve dispute (distributions) |
useWithdrawRemainingFunds |
Withdraw remaining |
useSendTransaction |
Submit signed XDR (POST /stellar/send-transaction) |
Most operate methods take (payload, type) where type is "single-release" | "multi-release". Multi-only helpers (approveAndReleaseMilestones, etc.) omit the type argument.
| Hook | Endpoint |
|---|---|
useListEscrows |
GET /escrows |
useGetEscrow |
GET /escrows/:contractId |
useGetEscrowDetails |
GET /escrows/details?contractIds= |
useListEscrowEvents |
GET /escrows/:contractId/events |
useGetEscrowMilestones |
GET /escrows/:contractId/milestones |
useGetEscrowsMilestones |
GET /escrows/milestones?contractIds= |
useGetEscrowsFinancial |
GET /escrows/financial?contractIds= |
import { useListEscrows, useGetEscrow } from "@trustless-work/escrow/hooks/rest";
import { useQuery } from "@tanstack/react-query";
const { listEscrows } = useListEscrows();
const { getEscrow } = useGetEscrow();
useQuery({
queryKey: ["escrows", "mine"],
queryFn: () => listEscrows({ scope: "mine", limit: 20 }),
});
useQuery({
queryKey: ["escrow", contractId],
queryFn: () => getEscrow(contractId),
enabled: !!contractId,
});scope, status, contractType, engagementId, contractIds, participant, role, platformId, subjectId, createdAfter, createdBefore, limit, cursor, sort, order.
- Use
scope: "mine" | "all"for segmentation (not per-escrow access grants). - List/events return a keyset page:
{ data, hasMore, nextCursor }.
| Type | Notes |
|---|---|
EscrowSummary |
List/detail row: contractId, type, status, balance, asset, camelCased snapshot |
EscrowDetail |
{ escrow, events, deposits } |
EscrowFinancial |
Batch financial: deposited / released / pending / balance |
EscrowEvent |
Indexed event (kind, topics, payload, …) — no UUID event id |
EscrowDeposit |
Deposit row — no UUID deposit id |
There is no UUID id / escrowId. Amounts on reads are human decimal strings (e.g. "250.5") — do not divide by 1e7. Build/operate payloads still use human numbers.
| Hook | Query |
|---|---|
useGraphqlGetEscrow |
escrow(contractId) + financial / deposits / events |
useGraphqlListEscrows |
escrows(...) (same filters as REST list) |
Documents: GRAPHQL_GET_ESCROW, GRAPHQL_LIST_ESCROWS.
import { useGraphqlGetEscrow, useGraphqlListEscrows } from "@trustless-work/escrow/hooks/graphql";
const { getEscrow } = useGraphqlGetEscrow();
const { listEscrows } = useGraphqlListEscrows();
const detail = await getEscrow({
contractId,
eventsLimit: 20,
});
const page = await listEscrows({
scope: "mine",
limit: 20,
});GraphQL wire types (GraphqlEscrow, GraphqlEscrowPage, …) live on @trustless-work/escrow / @trustless-work/escrow/graphql.
HTTP failures from Core are RFC 9457 Problem Details. The transport surfaces them as TrustlessWorkApiError.
import {
toTrustlessWorkError,
TrustlessWorkApiError,
formatApiErrorMessage,
ESCROW_ERROR_CODES,
} from "@trustless-work/escrow";
try {
await fundEscrow(payload, "single-release");
} catch (error: unknown) {
const err = toTrustlessWorkError(error);
if (err instanceof TrustlessWorkApiError) {
// err.code, err.status, err.detail, err.traceId, err.extensions
toast.error(formatApiErrorMessage(err));
if (err.code === ESCROW_ERROR_CODES.ESCROW_NOT_FOUND) { /* … */ }
}
}Also exported: parseProblemDetails, parseProblemDetailsFromAxiosError, isProblemDetails, isEscrowErrorCode.
Import from @trustless-work/escrow/types (or the root package):
| Area | Examples |
|---|---|
| Operate payloads | DeploySingleReleaseEscrowPayload, FundEscrowPayload, ApproveMilestonesPayload, AttributionHeaders, … |
| Responses | DeployEscrowResponse, BuildTransactionResponse, SendTransactionResponse, ListEscrowsResponse, … |
| Reads | EscrowSummary, EscrowAsset, EscrowSnapshot, EscrowEvent, EscrowFinancial, KeysetPage, … |
| Entities | Escrow, Roles, DeployTrustline, SingleReleaseMilestone, … |
| Errors | ApiProblemDetails, EscrowErrorCode, ESCROW_ERROR_CODES |
development and mainNet currently point at:
https://trustless-core-production.up.railway.app
Pass any Core API baseURL string when you need another host. Get an API key from the Trustless Work dApp.
Breaking changes aligned with the Core v2 wire contract:
- Removed legacy helpers:
/helper/get-escrows-by-*,/helper/get-escrow-by-contract-ids,/helper/get-multiple-escrow-balanceand their hooks. - Use
useListEscrows/useGetEscrow/useGetEscrowDetails/ financial & milestones hooks (or GraphQL equivalents). - Escrow identity is
contractIdonly (no UUID). - Deploy response includes
contractId:{ unsignedXdr, txHash, contractId }. - Deploy trustline is
{ contractId, symbol }(Soroban SAC + asset code). - Use
useDeployEscrow+Deploy*EscrowPayload(noInitialize*aliases). - Milestone ops are batch (
useApproveMilestones,useChangeMilestoneStatus,useManageMilestones, …). - Prefer entry points
@trustless-work/escrow/restand@trustless-work/escrow/graphqlinstead of mixing surfaces.
EscrowSummaryincludes rootbalance(string) andasset { name, address, contractId }.- Read amounts (
balance,totalAmount, financial fields, snapshot amounts) are decimal strings. createdByUserId/creatorAddressare not on reads (on-chain state is public).- Listing uses
scope=mine|all— not per-escrow access grants. - After submit, handle
STELLAR_TX_SUBMITTED_INDEXER_LAGGINGand poll reads;balanceis eventually consistent.
MIT License — see LICENSE file for details.
