@capxul/sdk-react 4.20.0-beta.5 → 4.20.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -56,6 +56,8 @@ export function Providers({ children }: { children: React.ReactNode }) {
56
56
  engine hook stays module-scoped. Both actor paths ship: a personal actor omits
57
57
  `.Source` and spends the personal Account; an organization actor holds the
58
58
  Budget the payment is drawn from and spends the treasury Account.
59
+ The submitted amount uses the selected registry AssetId and precision. This
60
+ includes 18-decimal WETH.
59
61
  - `CapxulContacts` is the compound address-book component. One root owns the
60
62
  book behaviour and draws nothing. The `.Summary`, `.List`, and `.Add` region
61
63
  parts each hand the screen one finished slice, and a `.List` row carries its
@@ -75,6 +77,9 @@ export function Providers({ children }: { children: React.ReactNode }) {
75
77
  and an amount the caller passed in `prefill` wins over it. Both engine hooks
76
78
  stay module-scoped.
77
79
 
80
+ Activity groups use AssetId. The SDK resolves symbols and precision only for
81
+ display and CSV output. Assets with the same peg remain separate.
82
+
78
83
  Payroll confirmation restores a saved run before enabling submission. An
79
84
  uncertain run keeps its period, amounts, recipients, and SDK request key across
80
85
  close/reopen and reload. Its fields remain fixed until Core confirms success or terminal closure.
@@ -98,6 +103,16 @@ The testing harness uses the real public provider and controllers and imports
98
103
  its client factory only through `@capxul/sdk/testing`; it owns no renderer or
99
104
  parallel identity state.
100
105
 
106
+ ## Activity documents
107
+
108
+ `CapxulActivity.Detail` exposes `documents`, `selectedDocument`, and
109
+ `selectDocument({ documentHash, contentHash })`. Selection defaults to the
110
+ instruction, then the Movement Memo. `renderDocument()` returns safe semantic
111
+ HTML. `downloadDocument()` returns exact bytes for a deliberate download.
112
+ The host owns the panel and download action. Do not log these results.
113
+ Account or Organization changes clear selection and cancel pending reads.
114
+ These methods replace the receipt-only selector and action.
115
+
101
116
  ## Documentation
102
117
 
103
118
  - [Maintainer context](./CONTEXT.md)
package/dist/index.d.mts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { A as entered, C as CreateOrganizationFailure, D as OrganizationDetails, E as InvocationOptions, F as useCapxulTransitions, M as useCapxulDestination, N as useCapxulIdentity, O as ProfileDetails, P as useCapxulSend, S as CapxulSend, T as Destination, _ as ReadyDestination, a as AuthenticationSlots, b as Slot, c as ControllerAction, d as OnboardingControllerProps, f as OrgFailure, g as ProfileSlotProps, h as PendingAuthState, i as AuthenticatedState, j as useCapxulAuth, k as SendResult, l as FaultedState, m as OtpPendingState, n as AccountProgress, o as CapxulAuthenticationController, p as OrgProgress, r as ActionResult, s as CapxulOnboardingController, t as AccountFailure, u as NavigationAction, v as RetryAction, w as CreateOrganizationSubmission, x as CapxulAuth, y as SignedOutState } from "./controllers-BWmKRFy6.mjs";
2
2
  import { ReactNode, RefCallback } from "react";
3
3
  import { QueryClient, UseMutationResult, UseQueryResult } from "@tanstack/react-query";
4
- import { Account, AccountRequirement, ActivityAnnotation, ActivityAnnotationInput, ActivityDetail, ActivityKind, ActivityListParams, ActivityPage, ActivityPhase, ActivityRange, ActivityReference, ActorReference, AddressBookEntry, BudgetId, CapxulClient, CapxulError, CapxulResult, CapxulSigner, CreateOrgInput, CurrentHoldings, HostObservability, IdentityDestination, IdentityState, InviteMemberInput, MemberView, Money, MoneyParseErrorReason, OrgId, OrgView, OrganizationPaymentBatchInput, OrganizationPaymentInput, PartyId, Payment, PaymentDirection, PaymentDocumentRef, PaymentDocumentRender, PaymentStatus, PaymentTiming, PaymentType, PaymentsPayInput, PayrollGroupId, PayrollGroupInput, PayrollRunId, PayrollRunStatus, Permission, PermissionMethods, PermissionReadResult, Profile, RoleView, SubmittedPermissionExecution, isClaimed, isRestoring } from "@capxul/sdk";
4
+ import { Account, AccountRequirement, ActivityAnnotation, ActivityAnnotationInput, ActivityDetail, ActivityKind, ActivityListParams, ActivityPage, ActivityPhase, ActivityRange, ActivityReference, ActivitySummaryTotal, ActorReference, AddressBookEntry, AssetAmount, AssetId, BudgetId, CapxulClient, CapxulError, CapxulResult, CapxulSigner, CreateOrgInput, CurrentHoldings, HostObservability, IdentityDestination, IdentityState, InviteMemberInput, MemberView, Money, MoneyParseErrorReason, OrgId, OrgView, OrganizationAccess, OrganizationPaymentBatchInput, OrganizationPaymentInput, PartyId, Payment, PaymentDirection, PaymentDocumentDownload, PaymentDocumentReadRef, PaymentDocumentRef, PaymentDocumentRender, PaymentStatus, PaymentTiming, PaymentType, PaymentsPayInput, PayrollGroupId, PayrollGroupInput, PayrollRunId, PayrollRunStatus, Permission, PermissionMethods, PermissionReadResult, Profile, RoleView, SubmittedPermissionExecution, isClaimed, isRestoring } from "@capxul/sdk";
5
5
  //#region src/provider.d.ts
6
6
  type CapxulProviderSharedProps = {
7
7
  /** Bring your own QueryClient; otherwise the provider creates one. */
@@ -82,7 +82,7 @@ declare function useCapxulAccountBalance(options?: UseCapxulAccountBalanceOption
82
82
  //#region src/hooks/use-capxul-account-fund.d.ts
83
83
  type UseCapxulAccountFundReturn = UseMutationResult<{
84
84
  readonly txHash: string;
85
- }, CapxulError, Money>;
85
+ }, CapxulError, AssetAmount>;
86
86
  declare function useCapxulAccountFund(): UseCapxulAccountFundReturn;
87
87
  //#endregion
88
88
  //#region src/hooks/use-capxul-money.d.ts
@@ -148,7 +148,7 @@ declare function useCapxulHoldings(params?: UseCapxulHoldingsParams): UseQueryRe
148
148
  * List the Organizations that the current user can access. Binds directly to
149
149
  * the locked `capxul.orgs()` SDK method.
150
150
  */
151
- type UseCapxulOrgsReturn = UseQueryResult<readonly OrgView[], CapxulError>;
151
+ type UseCapxulOrgsReturn = UseQueryResult<readonly OrganizationAccess[], CapxulError>;
152
152
  type UseCapxulOrgsOptions = {
153
153
  /**
154
154
  * Gate the query on auth readiness. `client.orgs()` is a session-scoped
@@ -525,8 +525,8 @@ type ActivityRow = {
525
525
  */
526
526
  type SummaryWindow = "all" | "last30days" | ActivityRange;
527
527
  /**
528
- * Finished money strings for ONE currency bucket, so no consumer indexes an
529
- * array of per-currency totals. All four strings are `null` and `count` is `0`
528
+ * Finished valuation strings in the display currency. Unrated assets remain
529
+ * separate. All four strings are `null` and `count` is `0`
530
530
  * when the window holds no activity — we say there is nothing, the app says it
531
531
  * in its own words.
532
532
  */
@@ -537,6 +537,7 @@ interface ActivitySummarySlice {
537
537
  readonly moneyOut: string | null;
538
538
  readonly net: string | null;
539
539
  readonly pending: string | null;
540
+ readonly unrated: readonly ActivitySummaryTotal[];
540
541
  /** A count is not money, so it is not formatted. */
541
542
  readonly count: number;
542
543
  readonly isLoading: boolean;
@@ -600,9 +601,12 @@ interface ActivityLoadMoreSlice {
600
601
  interface ActivityDetailSlice {
601
602
  /** Current Payment state from the existing detail read; absent for Movements. */
602
603
  readonly payment?: Pick<Payment, "status" | "execution">;
603
- /** ONE finished document, never the array, so no consumer writes `documents[0]`. */
604
- readonly receiptDocument: PaymentDocumentRef | null;
605
- readonly openReceiptDocument: () => Promise<PaymentDocumentRender>;
604
+ readonly documents: readonly PaymentDocumentRef[];
605
+ readonly selectedDocument: PaymentDocumentRef | null;
606
+ readonly selectDocument: (ref: PaymentDocumentReadRef) => void;
607
+ readonly renderDocument: () => Promise<PaymentDocumentRender>;
608
+ /** Return exact bytes on a deliberate action. The host owns the download URL. */
609
+ readonly downloadDocument: () => Promise<PaymentDocumentDownload>;
606
610
  readonly isOpeningDocument: boolean;
607
611
  readonly memo: string;
608
612
  readonly setMemo: (text: string) => void;
@@ -708,7 +712,7 @@ type SentPayment = {
708
712
  /** The backend payment row. */
709
713
  readonly payment: Payment;
710
714
  /** What we parsed and sent — `.value` is the comma-stripped major-unit string. */
711
- readonly amount: Money;
715
+ readonly amount: AssetAmount;
712
716
  /** The trimmed text the person typed — the app's success toast interpolates it. */
713
717
  readonly recipient: string;
714
718
  };
@@ -741,7 +745,7 @@ interface SendMoneySourceSlice {
741
745
  readonly isPending: boolean;
742
746
  }
743
747
  interface SendMoneyAssetOption {
744
- readonly id: string;
748
+ readonly id: AssetId;
745
749
  readonly symbol: string;
746
750
  readonly availableDisplay: string;
747
751
  }
@@ -1010,7 +1014,7 @@ interface PayrollRunAmountEntry {
1010
1014
  /** True while the value is one we seeded and the employer has not edited. */
1011
1015
  readonly prefilled: boolean;
1012
1016
  /** The treasury asset's currency. Rows stay absent until this value exists. */
1013
- readonly currency: Money["currency"];
1017
+ readonly currency: string;
1014
1018
  }
1015
1019
  interface PayrollRunAmountsSlice {
1016
1020
  readonly entries: readonly PayrollRunAmountEntry[];
package/dist/index.mjs CHANGED
@@ -2,7 +2,7 @@
2
2
  import { _ as useCapxul, a as useCapxulAuth, c as useCapxulIdentityOrNull, d as toInvocationControls, f as toInvocationOptions, g as useCapxulClientOrNull, h as capxulKeys, i as entered, l as useCapxulSend, m as actingAccount, n as CapxulOnboardingController, o as useCapxulDestination, p as accountKey, r as CapxulProvider, s as useCapxulIdentity, t as CapxulAuthenticationController, u as useCapxulTransitions } from "./controllers-L1UqwWS1.mjs";
3
3
  import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from "react";
4
4
  import { useInfiniteQuery, useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
5
- import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, fingerprintPaymentIntent, formatAssetAmount, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase, positionPeg, resolveIdentityDestination, toEvmAddress, toPartyId } from "@capxul/sdk";
5
+ import { CAPXUL_OPERATIONS, CapxulError, EVM_ADDRESS_RE, Errors, HANDLE_RE, PAYMENT_DIRECTIONS, PAYMENT_STATUSES, assetSymbolFor, fingerprintPaymentIntent, formatAssetAmount, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseAssetAmount, parseMoney, paymentPhase, requireAsset, resolveIdentityDestination, toEvmAddress, toPartyId, toRaw, valueActivitySummary } from "@capxul/sdk";
6
6
  import { Fragment, jsx } from "react/jsx-runtime";
7
7
  //#region src/internal/require-bootstrapped-client.ts
8
8
  /**
@@ -1141,15 +1141,8 @@ function useActivityEnabled() {
1141
1141
  }
1142
1142
  //#endregion
1143
1143
  //#region src/headless/activity/detail.ts
1144
- /**
1145
- * ONE finished document, never the array, so no consumer writes `documents[0]`:
1146
- * the payslip, else the receipt, else nothing. Those two kinds are the ones the
1147
- * Download receipt button is for. A Movement detail carries no documents.
1148
- */
1149
- function pickReceiptDocument(detail) {
1150
- if (detail === null || detail.kind !== "payment") return null;
1151
- const documents = detail.payment.documents;
1152
- return documents.find((document) => document.kind === "payslip") ?? documents.find((document) => document.kind === "receipt") ?? null;
1144
+ function detailDocuments(detail) {
1145
+ return detail === null ? [] : detail.kind === "payment" ? detail.payment.documents : detail.documents;
1153
1146
  }
1154
1147
  /**
1155
1148
  * The root actor and the whole reference are included. A payment reference can
@@ -1184,27 +1177,76 @@ function useActivityDetail(reference, actor) {
1184
1177
  const enabled = useActivityEnabled() && reference !== void 0;
1185
1178
  const detail = useQuery({
1186
1179
  queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
1187
- queryFn: async () => {
1180
+ queryFn: async ({ signal }) => {
1188
1181
  if (reference === void 0) throw Errors.invalidInput("reference", "required for activity.get");
1189
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, actor === void 0 ? void 0 : { actor }));
1182
+ return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, {
1183
+ ...actor === void 0 ? {} : { actor },
1184
+ signal
1185
+ }));
1190
1186
  },
1191
1187
  enabled
1192
1188
  });
1193
- const receiptDocument = pickReceiptDocument(detail.data ?? null);
1189
+ const documents = detailDocuments(detail.data ?? null);
1190
+ const [selected, setSelected] = useState(null);
1191
+ const selectedDocument = (selected === null ? void 0 : documents.find((doc) => doc.documentHash === selected.documentHash && doc.contentHash === selected.contentHash)) ?? documents.find((doc) => doc.role === "instruction") ?? documents.find((doc) => doc.role === "movement") ?? documents[0] ?? null;
1192
+ const pending = useRef(/* @__PURE__ */ new Set());
1193
+ const mounted = useRef(true);
1194
+ useEffect(() => {
1195
+ mounted.current = true;
1196
+ const reads = pending.current;
1197
+ return () => {
1198
+ mounted.current = false;
1199
+ for (const read of reads) read.abort();
1200
+ reads.clear();
1201
+ };
1202
+ }, []);
1203
+ const selectDocument = useCallback((ref) => {
1204
+ if (!documents.some((doc) => doc.documentHash === ref.documentHash && doc.contentHash === ref.contentHash)) throw Errors.invalidInput("documentHash", "document not found");
1205
+ for (const read of pending.current) read.abort();
1206
+ pending.current.clear();
1207
+ setIsOpeningDocument(false);
1208
+ setSelected({
1209
+ documentHash: ref.documentHash,
1210
+ contentHash: ref.contentHash
1211
+ });
1212
+ }, [documents]);
1194
1213
  const [isOpeningDocument, setIsOpeningDocument] = useState(false);
1195
- const openReceiptDocument = useCallback(async () => {
1196
- if (reference === void 0 || receiptDocument === null) throw Errors.invalidInput("receiptDocument", "no renderable document on this row");
1214
+ const readDocument = useCallback(async (download) => {
1215
+ const operation = download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render;
1216
+ if (!mounted.current) throw Errors.cancelled({ operation });
1217
+ if (reference === void 0 || selectedDocument === null) throw Errors.invalidInput("documentHash", "no document on this row");
1218
+ const controller = new AbortController();
1219
+ pending.current.add(controller);
1197
1220
  setIsOpeningDocument(true);
1198
1221
  try {
1199
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.paymentDocuments.render).paymentDocuments.render(receiptDocument.documentHash));
1222
+ const bootstrapped = requireBootstrappedClient(client, download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render);
1223
+ const pair = {
1224
+ documentHash: selectedDocument.documentHash,
1225
+ contentHash: selectedDocument.contentHash
1226
+ };
1227
+ const options = { signal: controller.signal };
1228
+ const result = download ? await bootstrapped.paymentDocuments.get(pair, options) : await bootstrapped.paymentDocuments.render(pair, options);
1229
+ if (controller.signal.aborted) throw Errors.cancelled({ operation });
1230
+ return unwrapCapxulResult(result);
1200
1231
  } finally {
1201
- setIsOpeningDocument(false);
1232
+ pending.current.delete(controller);
1233
+ if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
1202
1234
  }
1203
1235
  }, [
1204
1236
  client,
1205
- receiptDocument,
1237
+ selectedDocument,
1206
1238
  reference
1207
1239
  ]);
1240
+ const renderDocument = useCallback(async () => {
1241
+ const result = await readDocument(false);
1242
+ if (!("html" in result)) throw Errors.invalidInput("documentHash", "invalid document render");
1243
+ return result;
1244
+ }, [readDocument]);
1245
+ const downloadDocument = useCallback(async () => {
1246
+ const result = await readDocument(true);
1247
+ if (!("bytesBase64" in result)) throw Errors.invalidInput("documentHash", "invalid document download");
1248
+ return result;
1249
+ }, [readDocument]);
1208
1250
  const [typed, setTyped] = useState(null);
1209
1251
  const annotate = useCapxulAnnotateMovement();
1210
1252
  const saveMemo = useCallback(async () => {
@@ -1233,8 +1275,11 @@ function useActivityDetail(reference, actor) {
1233
1275
  status: detail.data.payment.status,
1234
1276
  ...detail.data.payment.execution === void 0 ? {} : { execution: detail.data.payment.execution }
1235
1277
  } } : {},
1236
- receiptDocument,
1237
- openReceiptDocument,
1278
+ documents,
1279
+ selectedDocument,
1280
+ selectDocument,
1281
+ renderDocument,
1282
+ downloadDocument,
1238
1283
  isOpeningDocument,
1239
1284
  memo: typed ?? detail.data?.annotation?.memo ?? "",
1240
1285
  setMemo: setTyped,
@@ -1247,6 +1292,7 @@ function useActivityDetail(reference, actor) {
1247
1292
  //#endregion
1248
1293
  //#region src/headless/activity/summary.ts
1249
1294
  const DAY_MS = 864e5;
1295
+ const display = (money) => money === null ? null : formatMoney(money);
1250
1296
  /**
1251
1297
  * `"last30days"` is the last 30 whole UTC days including today. Quantizing to
1252
1298
  * the UTC day keeps the cache key stable for a whole day instead of changing on
@@ -1257,52 +1303,6 @@ function resolveWindow(window, now) {
1257
1303
  if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
1258
1304
  return window;
1259
1305
  }
1260
- /** An integer raw-unit string lifted into a major-unit decimal string. */
1261
- function majorUnits(raw, decimals) {
1262
- const negative = raw.startsWith("-");
1263
- const digits = negative ? raw.slice(1) : raw;
1264
- const sign = negative ? "-" : "";
1265
- if (decimals === 0) return `${sign}${digits}`;
1266
- const padded = digits.padStart(decimals + 1, "0");
1267
- const cut = padded.length - decimals;
1268
- return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
1269
- }
1270
- function absoluteNet(total) {
1271
- const net = BigInt(total.net);
1272
- return net < 0n ? -net : net;
1273
- }
1274
- /**
1275
- * Buckets carry different `decimals`, so their raw integers are not comparable.
1276
- * Both are scaled to the wider precision before the magnitudes are ordered.
1277
- */
1278
- function compareNet(left, right) {
1279
- const width = Math.max(left.decimals, right.decimals);
1280
- const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
1281
- const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
1282
- if (scaledLeft === scaledRight) return 0;
1283
- return scaledLeft < scaledRight ? -1 : 1;
1284
- }
1285
- /**
1286
- * Which currency `.Summary` shows: the bucket matching `primaryCurrency` when
1287
- * one is given, else the bucket with the largest absolute `net`, breaking a tie
1288
- * on currency code ascending so the choice is stable across reloads.
1289
- */
1290
- function pickBucket(totals, primaryCurrency) {
1291
- if (primaryCurrency !== null) {
1292
- const matched = totals.find((total) => total.currency === primaryCurrency);
1293
- if (matched !== void 0) return matched;
1294
- }
1295
- let best = null;
1296
- for (const total of totals) {
1297
- if (best === null) {
1298
- best = total;
1299
- continue;
1300
- }
1301
- const order = compareNet(total, best);
1302
- if (order > 0 || order === 0 && total.currency < best.currency) best = total;
1303
- }
1304
- return best;
1305
- }
1306
1306
  /**
1307
1307
  * The current UTC day number, which advances ON ITS OWN at UTC midnight.
1308
1308
  *
@@ -1346,42 +1346,36 @@ function useActivitySummary(actor, window) {
1346
1346
  const params = {};
1347
1347
  if (actor !== void 0) params.actor = actor;
1348
1348
  if (range !== void 0) params.window = range;
1349
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1350
- },
1351
- enabled
1352
- });
1353
- const primary = useQuery({
1354
- queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
1355
- queryFn: async () => {
1356
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.holdings.primary).holdings.primary(actor === void 0 ? void 0 : { actor }));
1349
+ const result = unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1350
+ return {
1351
+ ...result,
1352
+ valuation: valueActivitySummary(result.totals)
1353
+ };
1357
1354
  },
1358
1355
  enabled
1359
1356
  });
1360
1357
  const totals = summary.data?.totals;
1361
- const isLoading = summary.isLoading || primary.isLoading;
1362
- const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, null), [isLoading, totals]);
1363
- if (bucket === null) return {
1358
+ const isLoading = summary.isLoading;
1359
+ if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
1364
1360
  currency: null,
1365
1361
  moneyIn: null,
1366
1362
  moneyOut: null,
1367
1363
  net: null,
1368
1364
  pending: null,
1365
+ unrated: [],
1369
1366
  count: summary.data?.count ?? 0,
1370
1367
  isLoading,
1371
1368
  error: asCapxulError$1(summary.error),
1372
1369
  ...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
1373
1370
  };
1374
- const display = (raw) => formatMoney({
1375
- currency: bucket.currency,
1376
- value: majorUnits(raw, bucket.decimals),
1377
- decimals: bucket.decimals
1378
- });
1371
+ const valuation = summary.data.valuation;
1379
1372
  return {
1380
- currency: bucket.currency,
1381
- moneyIn: display(bucket.in),
1382
- moneyOut: display(bucket.out),
1383
- net: display(bucket.net),
1384
- pending: display(bucket.pendingIn),
1373
+ currency: valuation.moneyIn?.currency ?? null,
1374
+ moneyIn: display(valuation.moneyIn),
1375
+ moneyOut: display(valuation.moneyOut),
1376
+ net: display(valuation.net),
1377
+ pending: display(valuation.pending),
1378
+ unrated: valuation.unrated,
1385
1379
  count: summary.data?.count ?? 0,
1386
1380
  isLoading,
1387
1381
  error: asCapxulError$1(summary.error),
@@ -1408,7 +1402,7 @@ const SIGN = Object.freeze({
1408
1402
  self: ""
1409
1403
  });
1410
1404
  function toActivityRow(item, actor) {
1411
- const amountDisplay = formatMoney(item.amount);
1405
+ const amountDisplay = formatAssetAmount(item.amount);
1412
1406
  const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
1413
1407
  if (item.kind === "movement") return {
1414
1408
  kind: "movement",
@@ -1420,7 +1414,7 @@ function toActivityRow(item, actor) {
1420
1414
  counterpartyLabel: item.counterpartyLabel,
1421
1415
  amountDisplay,
1422
1416
  signedAmountDisplay,
1423
- currency: item.amount.currency,
1417
+ currency: assetSymbolFor(item.amount.asset),
1424
1418
  createdAt: item.createdAt,
1425
1419
  classification: item.classification,
1426
1420
  phase: MOVEMENT_PHASE,
@@ -1442,7 +1436,7 @@ function toActivityRow(item, actor) {
1442
1436
  counterpartyLabel: item.counterpartyLabel,
1443
1437
  amountDisplay,
1444
1438
  signedAmountDisplay,
1445
- currency: item.amount.currency,
1439
+ currency: assetSymbolFor(item.amount.asset),
1446
1440
  createdAt: item.createdAt,
1447
1441
  ledgerKind: item.ledgerKind,
1448
1442
  paymentType: item.paymentType,
@@ -1523,7 +1517,7 @@ function buildCsv(items, actor, search) {
1523
1517
  row.counterpartyLabel,
1524
1518
  row.direction,
1525
1519
  item.amount.value,
1526
- item.amount.currency,
1520
+ assetSymbolFor(item.amount.asset),
1527
1521
  row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
1528
1522
  row.reference.id,
1529
1523
  row.receipt?.transactionHash ?? ""
@@ -1873,18 +1867,19 @@ function useOrgMeReading(orgId) {
1873
1867
  //#endregion
1874
1868
  //#region src/internal/write-names-asset.ts
1875
1869
  /**
1876
- * Identity law 5 (`docs/campaigns/capabilities/treasury-cut/HANDOFF.md`): "A
1877
- * payment names an asset or inherits it from an authorized fact. Missing asset
1878
- * refuses."
1870
+ * Identity law: "A payment names an asset or inherits it from an authorized
1871
+ * fact. Missing asset refuses." The rule was recorded in the retired
1872
+ * treasury-cut handoff (removed from the current tree by the exact-asset
1873
+ * contraction; its record survives in Git history).
1879
1874
  *
1880
1875
  * A Capxul payment writer takes `PaymentMoney` — a peg currency, a value and
1881
1876
  * `decimals` — and settles one fixed legacy token. No `AssetId` crosses that
1882
1877
  * seam, so a payment composed from a named position cannot name the asset it
1883
1878
  * moves: a position selected as USDT authorizes a different token.
1884
1879
  *
1885
- * `false` until slice 02 (`02-named-pay`) makes the writer take the command's
1886
- * asset. The Send and Payroll composers read this to refuse a submission
1887
- * rather than authorize a token the person did not pick.
1880
+ * `false` until the writer takes the command's asset. The Send and Payroll
1881
+ * composers read this to refuse a submission rather than authorize a token the
1882
+ * person did not pick.
1888
1883
  */
1889
1884
  function writeNamesAsset() {
1890
1885
  return false;
@@ -2054,7 +2049,6 @@ function useSelectedOption(options) {
2054
2049
  function assetSelectionsFromAccount$1(account) {
2055
2050
  if (account === null) return [];
2056
2051
  return account.balances.map((position) => {
2057
- const peg = positionPeg(position.asset);
2058
2052
  return {
2059
2053
  option: {
2060
2054
  id: position.asset,
@@ -2064,10 +2058,9 @@ function assetSelectionsFromAccount$1(account) {
2064
2058
  value: position.available
2065
2059
  })
2066
2060
  },
2067
- money: peg === null ? null : {
2068
- currency: peg,
2069
- value: position.available,
2070
- decimals: position.decimals
2061
+ money: {
2062
+ asset: position.asset,
2063
+ value: position.available
2071
2064
  }
2072
2065
  };
2073
2066
  });
@@ -2089,7 +2082,7 @@ function useSendMoney(input) {
2089
2082
  const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
2090
2083
  const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
2091
2084
  const [amountText, setAmountText] = useState("");
2092
- const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseMoney(amountText, selectedAssetMoney), [amountText, selectedAssetMoney]);
2085
+ const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
2093
2086
  const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
2094
2087
  const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
2095
2088
  const recipient = useRecipientResolution(client, JSON.stringify([
@@ -2538,11 +2531,6 @@ const CapxulDashboardAccess = Object.assign(Root$3, {
2538
2531
  });
2539
2532
  //#endregion
2540
2533
  //#region src/headless/payroll/money.ts
2541
- /** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
2542
- function toMinorUnits(money) {
2543
- const [whole = "0", fraction = ""] = money.value.split(".");
2544
- return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
2545
- }
2546
2534
  const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
2547
2535
  /**
2548
2536
  * Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
@@ -2575,6 +2563,17 @@ function fromMinorUnits(raw, asset) {
2575
2563
  decimals: asset.decimals
2576
2564
  };
2577
2565
  }
2566
+ /** Convert exact raw units to one asset quantity without a fiat denomination. */
2567
+ function assetFromMinorUnits(raw, asset) {
2568
+ const money = fromMinorUnits(raw, {
2569
+ currency: "USD",
2570
+ decimals: asset.decimals
2571
+ });
2572
+ return {
2573
+ asset: asset.asset,
2574
+ value: money.value
2575
+ };
2576
+ }
2578
2577
  //#endregion
2579
2578
  //#region src/headless/payroll/reads.ts
2580
2579
  function toRecipientOption(entry) {
@@ -2691,7 +2690,7 @@ function scopedClient(client, method, orgId) {
2691
2690
  //#endregion
2692
2691
  //#region src/headless/payroll/use-payroll.ts
2693
2692
  /** Money reads the same way everywhere on this screen: amount then code. */
2694
- const GRAMMAR$1 = { grammar: "code" };
2693
+ const GRAMMAR = { grammar: "code" };
2695
2694
  /**
2696
2695
  * One captured snapshot, or nothing — never a fabricated empty options object,
2697
2696
  * and never a DOM event a screen wired straight to `onClick`.
@@ -2707,7 +2706,7 @@ function toPayrollGroupRow(group) {
2707
2706
  name: group.name,
2708
2707
  tone: group.tone,
2709
2708
  memberCount: group.members.length,
2710
- totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR$1),
2709
+ totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
2711
2710
  members: group.members.map((member) => ({
2712
2711
  recipientId: member.partyId,
2713
2712
  amount: member.amount,
@@ -2797,8 +2796,8 @@ function usePayroll(orgId) {
2797
2796
  const started = orgId !== void 0;
2798
2797
  return {
2799
2798
  summary: {
2800
- walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR$1),
2801
- paidThisMonthDisplay: runs.data === void 0 ? null : formatMoney(runs.data.settledThisMonth, GRAMMAR$1),
2799
+ walletBalanceDisplay: treasury.data?.available === void 0 || treasury.data.available === null ? null : formatMoney(treasury.data.available, GRAMMAR),
2800
+ paidThisMonthDisplay: runs.data === void 0 ? null : runs.data.settledThisMonth.map(formatAssetAmount).join(", "),
2802
2801
  isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
2803
2802
  error: treasury.error ?? runs.error
2804
2803
  },
@@ -2882,8 +2881,9 @@ function decodeDraft(value, scope) {
2882
2881
  const items = input.items.map((raw) => {
2883
2882
  const item = record(raw);
2884
2883
  const to = record(item.to);
2884
+ const amount = record(item.amount);
2885
2885
  const partyId = toPartyId(identifier(item.partyId));
2886
- if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
2886
+ if (to.kind !== "party" || to.partyId !== partyId || item.gross !== item.net || typeof amount.asset !== "string" || typeof amount.value !== "string" || typeof item.currency !== "string" || typeof item.decimals !== "number" || !Number.isInteger(item.decimals) || !Array.isArray(item.adjustments) || item.adjustments.length !== 0) throw Errors.invalidInput("payrollDraft", "Invalid draft recipient");
2887
2887
  const net = minorAmount(item.net);
2888
2888
  return {
2889
2889
  to: {
@@ -2891,12 +2891,20 @@ function decodeDraft(value, scope) {
2891
2891
  partyId
2892
2892
  },
2893
2893
  partyId,
2894
+ amount: {
2895
+ asset: requireAsset(amount.asset).assetId,
2896
+ value: amount.value
2897
+ },
2894
2898
  gross: net,
2895
2899
  net,
2900
+ currency: item.currency,
2901
+ decimals: item.decimals,
2896
2902
  adjustments: []
2897
2903
  };
2898
2904
  });
2899
2905
  if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
2906
+ const exactAsset = requireAsset(identifier(asset.id));
2907
+ if (items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
2900
2908
  return {
2901
2909
  version: 1,
2902
2910
  scope,
@@ -2907,6 +2915,7 @@ function decodeDraft(value, scope) {
2907
2915
  },
2908
2916
  input: {
2909
2917
  permissionId: identifier(input.permissionId),
2918
+ asset: exactAsset.assetId,
2910
2919
  period: {
2911
2920
  start: period.start,
2912
2921
  end: period.start
@@ -3110,7 +3119,6 @@ function termsSeeds(terms, asset, period) {
3110
3119
  }
3111
3120
  //#endregion
3112
3121
  //#region src/headless/payroll/use-payroll-run.ts
3113
- const GRAMMAR = { grammar: "code" };
3114
3122
  /**
3115
3123
  * The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
3116
3124
  * stamps `start === end === runAt` at submit, and the payslip's period renders
@@ -3134,6 +3142,32 @@ const INSTANT_RUN_PERIOD = {
3134
3142
  function amountText(amounts, seeds, recipientId) {
3135
3143
  return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
3136
3144
  }
3145
+ function freshRunItems(input) {
3146
+ const items = [];
3147
+ for (const [index, recipientId] of input.selected.entries()) {
3148
+ const to = input.payableRefs[index];
3149
+ const amount = input.amounts[index];
3150
+ const terms = input.compensationTerms.get(recipientId);
3151
+ if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
3152
+ items.push({
3153
+ to: {
3154
+ kind: "party",
3155
+ partyId: toPartyId(recipientId)
3156
+ },
3157
+ partyId: recipientId,
3158
+ amount,
3159
+ gross: terms.rate,
3160
+ net: terms.rate,
3161
+ currency: terms.currency,
3162
+ decimals: terms.decimals,
3163
+ adjustments: []
3164
+ });
3165
+ }
3166
+ return items;
3167
+ }
3168
+ function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
3169
+ return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
3170
+ }
3137
3171
  /**
3138
3172
  * WHICH run the composer is composing, by MEANING rather than by object shape.
3139
3173
  *
@@ -3176,7 +3210,6 @@ function initialAmounts(prefill) {
3176
3210
  function assetSelectionsFromAccount(account) {
3177
3211
  if (account === null) return [];
3178
3212
  return account.balances.map((position) => {
3179
- const peg = positionPeg(position.asset);
3180
3213
  return {
3181
3214
  option: {
3182
3215
  id: position.asset,
@@ -3186,10 +3219,9 @@ function assetSelectionsFromAccount(account) {
3186
3219
  value: position.available
3187
3220
  })
3188
3221
  },
3189
- money: peg === null ? null : {
3190
- currency: peg,
3191
- value: position.available,
3192
- decimals: position.decimals
3222
+ money: {
3223
+ asset: position.asset,
3224
+ value: position.available
3193
3225
  }
3194
3226
  };
3195
3227
  });
@@ -3207,17 +3239,13 @@ function usePayrollRun(input) {
3207
3239
  const terms = useOrgTerms(orgId);
3208
3240
  const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
3209
3241
  const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
3210
- const restoreCurrency = treasury.data?.available.currency;
3211
3242
  const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
3212
3243
  setSelected(restored.input.items.map((item) => item.partyId));
3213
- if (restoreCurrency !== void 0 && restoreCurrency === restored.asset.currency) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3214
- value: fromMinorUnits(BigInt(item.net), {
3215
- currency: restoreCurrency,
3216
- decimals: restored.asset.decimals
3217
- }).value,
3244
+ if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3245
+ value: item.amount.value,
3218
3246
  prefilled: false
3219
3247
  }])));
3220
- }, [restoreCurrency]));
3248
+ }, []));
3221
3249
  const frozen = draft !== null;
3222
3250
  const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
3223
3251
  const [seededKey, setSeededKey] = useState(composerKey);
@@ -3279,12 +3307,19 @@ function usePayrollRun(input) {
3279
3307
  }
3280
3308
  return resolved;
3281
3309
  }, [refs, selected]);
3282
- const seeds = useMemo(() => termsSeeds(terms.data ?? [], assetMoney, INSTANT_RUN_PERIOD), [assetMoney, terms.data]);
3310
+ const seeds = useMemo(() => {
3311
+ if (assetMoney === null) return /* @__PURE__ */ new Map();
3312
+ const metadata = requireAsset(assetMoney.asset);
3313
+ return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
3314
+ currency: metadata.peg,
3315
+ decimals: metadata.decimals
3316
+ }, INSTANT_RUN_PERIOD);
3317
+ }, [assetMoney, terms.data]);
3283
3318
  const entries = useMemo(() => {
3284
3319
  if (assetMoney === null) return [];
3285
3320
  return selected.map((recipientId) => {
3286
3321
  const value = amountText(amounts, seeds, recipientId);
3287
- const parsed = value.trim() === "" ? null : parseMoney(value, assetMoney);
3322
+ const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
3288
3323
  return {
3289
3324
  recipientId,
3290
3325
  name: named.get(recipientId)?.name ?? recipientId,
@@ -3292,7 +3327,7 @@ function usePayrollRun(input) {
3292
3327
  change: (text) => change(recipientId, text),
3293
3328
  error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
3294
3329
  prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
3295
- currency: assetMoney.currency
3330
+ currency: requireAsset(assetMoney.asset).symbol
3296
3331
  };
3297
3332
  });
3298
3333
  }, [
@@ -3307,7 +3342,7 @@ function usePayrollRun(input) {
3307
3342
  if (assetMoney === null || selected.length === 0) return null;
3308
3343
  const money = [];
3309
3344
  for (const recipientId of selected) {
3310
- const parsed = parseMoney(amountText(amounts, seeds, recipientId), assetMoney);
3345
+ const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
3311
3346
  if (isMoneyParseError(parsed)) return null;
3312
3347
  money.push(parsed);
3313
3348
  }
@@ -3318,17 +3353,25 @@ function usePayrollRun(input) {
3318
3353
  seeds,
3319
3354
  selected
3320
3355
  ]);
3321
- const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : fromMinorUnits(parsedAmounts.reduce((sum, money) => sum + toMinorUnits(money), 0n), assetMoney), [assetMoney, parsedAmounts]);
3322
- const availableRaw = assetMoney === null ? null : toMinorUnits(assetMoney);
3323
- const totalRaw = total === null ? null : toMinorUnits(total);
3356
+ const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
3357
+ asset: assetMoney.asset,
3358
+ value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
3359
+ asset: assetMoney.asset,
3360
+ decimals: requireAsset(assetMoney.asset).decimals
3361
+ }).value
3362
+ }, [assetMoney, parsedAmounts]);
3363
+ const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
3364
+ const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
3324
3365
  const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
3325
3366
  const budgets = me.data?.budgets ?? [];
3367
+ const compensationTerms = useMemo(() => new Map((terms.data ?? []).filter((row) => row.unit === "paycheck" && /^[1-9][0-9]*$/u.test(row.rate)).map((row) => [row.partyId, row])), [terms.data]);
3368
+ const writerNamesSelectedAsset = writeNamesAsset();
3326
3369
  const blockedReason = firstFalseFact({
3327
3370
  signerReady: signerStatus === "ready",
3328
3371
  ...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
3329
3372
  recipientCount: payableRefs === null ? 0 : selected.length,
3330
- amountsParse: total !== null,
3331
- writeNamesAsset: writeNamesAsset()
3373
+ amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
3374
+ writeNamesAsset: writerNamesSelectedAsset
3332
3375
  });
3333
3376
  const authorize = useMutation({
3334
3377
  mutationFn: async ({ candidate, options }) => {
@@ -3350,7 +3393,7 @@ function usePayrollRun(input) {
3350
3393
  if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
3351
3394
  }
3352
3395
  });
3353
- const totalDisplay = total === null ? null : formatMoney(total, GRAMMAR);
3396
+ const totalDisplay = total === null ? null : formatAssetAmount(total);
3354
3397
  const authorizeMutate = authorize.mutate;
3355
3398
  const submit = useCallback((options) => {
3356
3399
  const attempt = toInvocationOptions(options);
@@ -3359,41 +3402,35 @@ function usePayrollRun(input) {
3359
3402
  if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
3360
3403
  if (payableRefs === null) return;
3361
3404
  const recipientCount = selected.length;
3362
- const items = [];
3363
- for (const [index, recipientId] of selected.entries()) {
3364
- const to = payableRefs[index];
3365
- const parsed = parsedAmounts[index];
3366
- if (to === void 0 || parsed === void 0) return;
3367
- const raw = toMinorUnits(parsed).toString();
3368
- items.push({
3369
- to: {
3370
- kind: "party",
3371
- partyId: toPartyId(recipientId)
3372
- },
3373
- partyId: recipientId,
3374
- gross: raw,
3375
- net: raw,
3376
- adjustments: []
3405
+ let candidate;
3406
+ if (frozen) candidate = draft;
3407
+ else {
3408
+ const items = freshRunItems({
3409
+ selected,
3410
+ payableRefs,
3411
+ amounts: parsedAmounts,
3412
+ compensationTerms
3377
3413
  });
3378
- }
3379
- const runAt = Date.now();
3380
- const candidate = frozen ? draft : {
3381
- version: 1,
3382
- scope: draftScope,
3383
- asset: {
3384
- id: selectedAsset.id,
3385
- currency: assetMoney.currency,
3386
- decimals: assetMoney.decimals
3387
- },
3388
- input: {
3389
- permissionId,
3390
- period: {
3391
- start: runAt,
3392
- end: runAt
3414
+ const runAt = Date.now();
3415
+ candidate = {
3416
+ version: 1,
3417
+ scope: draftScope,
3418
+ asset: {
3419
+ id: selectedAsset.id,
3420
+ currency: requireAsset(assetMoney.asset).symbol,
3421
+ decimals: requireAsset(assetMoney.asset).decimals
3393
3422
  },
3394
- items
3395
- }
3396
- };
3423
+ input: {
3424
+ permissionId,
3425
+ asset: selectedAsset.id,
3426
+ period: {
3427
+ start: runAt,
3428
+ end: runAt
3429
+ },
3430
+ items
3431
+ }
3432
+ };
3433
+ }
3397
3434
  submitGate.current = true;
3398
3435
  authorizeMutate({
3399
3436
  candidate,
@@ -3425,6 +3462,7 @@ function usePayrollRun(input) {
3425
3462
  selectedAsset,
3426
3463
  blockedReason,
3427
3464
  budgets,
3465
+ compensationTerms,
3428
3466
  onFailed,
3429
3467
  onRun,
3430
3468
  parsedAmounts,
@@ -3473,13 +3511,20 @@ function usePayrollRun(input) {
3473
3511
  };
3474
3512
  }
3475
3513
  function remainingBalanceDisplay(available, total, asset) {
3476
- return available === null || total === null || asset === null ? null : formatMoney(fromMinorUnits(available - total, asset), GRAMMAR);
3514
+ if (available === null || total === null || asset === null) return null;
3515
+ const remaining = available - total;
3516
+ const magnitude = remaining < 0n ? -remaining : remaining;
3517
+ const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
3518
+ asset: asset.asset,
3519
+ decimals: requireAsset(asset.asset).decimals
3520
+ }));
3521
+ return remaining < 0n ? `-${formatted}` : formatted;
3477
3522
  }
3478
3523
  /** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
3479
3524
  function runAuthorityFacts(draft, budgets, asset, money, funds) {
3480
3525
  return {
3481
3526
  budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
3482
- hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset.currency === money?.currency && draft.asset.decimals === money?.decimals),
3527
+ hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
3483
3528
  funds: draft?.input.requestKey === void 0 ? funds : "available"
3484
3529
  };
3485
3530
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@capxul/sdk-react",
3
- "version": "4.20.0-beta.5",
3
+ "version": "4.20.0-beta.6",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/Xelmar-tech/infrastructure.git",
@@ -26,7 +26,7 @@
26
26
  "access": "public"
27
27
  },
28
28
  "dependencies": {
29
- "@capxul/sdk": "4.20.0-beta.5"
29
+ "@capxul/sdk": "4.20.0-beta.6"
30
30
  },
31
31
  "devDependencies": {
32
32
  "@tanstack/react-query": "^5.66.9",
@@ -43,6 +43,7 @@
43
43
  "vite": "npm:@voidzero-dev/vite-plus-core@0.3.0",
44
44
  "vite-plus": "0.3.0",
45
45
  "vitest": "4.1.11",
46
+ "@capxul/config": "0.4.0-beta.0",
46
47
  "@capxul/errors": "0.3.0",
47
48
  "@capxul/types": "0.3.0",
48
49
  "@capxul/typescript-config": "0.0.0"