@capxul/sdk-react 4.20.0-beta.1 → 4.20.0-beta.10

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/dist/index.mjs CHANGED
@@ -1,8 +1,8 @@
1
1
  "use client";
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-DXmEHrnw.mjs";
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, formatMoney, isCapxulError, isClaimed, isClaimed as isClaimed$1, isMoneyParseError, isRestoring, isRestoring as isRestoring$1, parseMoney, paymentPhase, 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
  /**
@@ -546,22 +546,6 @@ function useCapxulCreateOrg() {
546
546
  });
547
547
  }
548
548
  //#endregion
549
- //#region src/hooks/use-capxul-invite-member.ts
550
- function useCapxulInviteMember(orgId) {
551
- const client = useCapxulClientOrNull();
552
- const queryClient = useQueryClient();
553
- return useMutation({
554
- mutationFn: async ({ options, ...input }) => {
555
- if (orgId === void 0) throw Errors.invalidInput("orgId", "required for useCapxulInviteMember");
556
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.org.invite).org(orgId).invite(input, toInvocationControls(options)));
557
- },
558
- onSuccess: async () => {
559
- if (orgId === void 0) return;
560
- await queryClient.invalidateQueries({ queryKey: capxulKeys.orgMembers(orgId) });
561
- }
562
- });
563
- }
564
- //#endregion
565
549
  //#region src/headless/journey/journey-state.ts
566
550
  const JOURNEY_KEY = "capxul.onboarding.journey.v1";
567
551
  const VALIDATED_OWNER_KEY = "capxul.onboarding.validated-owner.v1";
@@ -1079,7 +1063,7 @@ function useContactsContext() {
1079
1063
  if (engine === null) throw new Error("CapxulContacts parts must be used inside <CapxulContacts>");
1080
1064
  return engine;
1081
1065
  }
1082
- function Root$6({ actor, onAdded, onFailed, children }) {
1066
+ function Root$7({ actor, onAdded, onFailed, children }) {
1083
1067
  const engine = useContacts({
1084
1068
  actor,
1085
1069
  onAdded,
@@ -1103,7 +1087,7 @@ function Add({ children }) {
1103
1087
  return /* @__PURE__ */ jsx(Fragment, { children: children(useContactsContext().add) });
1104
1088
  }
1105
1089
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
1106
- const CapxulContacts = Object.assign(Root$6, {
1090
+ const CapxulContacts = Object.assign(Root$7, {
1107
1091
  Summary: Summary$3,
1108
1092
  List,
1109
1093
  Add
@@ -1141,15 +1125,8 @@ function useActivityEnabled() {
1141
1125
  }
1142
1126
  //#endregion
1143
1127
  //#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;
1128
+ function detailDocuments(detail) {
1129
+ return detail === null ? [] : detail.kind === "payment" ? detail.payment.documents : detail.documents;
1153
1130
  }
1154
1131
  /**
1155
1132
  * The root actor and the whole reference are included. A payment reference can
@@ -1184,27 +1161,76 @@ function useActivityDetail(reference, actor) {
1184
1161
  const enabled = useActivityEnabled() && reference !== void 0;
1185
1162
  const detail = useQuery({
1186
1163
  queryKey: accountKey(capxulKeys.activityDetail(reference, actor), identity),
1187
- queryFn: async () => {
1164
+ queryFn: async ({ signal }) => {
1188
1165
  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 }));
1166
+ return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.get).activity.get(reference, {
1167
+ ...actor === void 0 ? {} : { actor },
1168
+ signal
1169
+ }));
1190
1170
  },
1191
1171
  enabled
1192
1172
  });
1193
- const receiptDocument = pickReceiptDocument(detail.data ?? null);
1173
+ const documents = detailDocuments(detail.data ?? null);
1174
+ const [selected, setSelected] = useState(null);
1175
+ 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;
1176
+ const pending = useRef(/* @__PURE__ */ new Set());
1177
+ const mounted = useRef(true);
1178
+ useEffect(() => {
1179
+ mounted.current = true;
1180
+ const reads = pending.current;
1181
+ return () => {
1182
+ mounted.current = false;
1183
+ for (const read of reads) read.abort();
1184
+ reads.clear();
1185
+ };
1186
+ }, []);
1187
+ const selectDocument = useCallback((ref) => {
1188
+ if (!documents.some((doc) => doc.documentHash === ref.documentHash && doc.contentHash === ref.contentHash)) throw Errors.invalidInput("documentHash", "document not found");
1189
+ for (const read of pending.current) read.abort();
1190
+ pending.current.clear();
1191
+ setIsOpeningDocument(false);
1192
+ setSelected({
1193
+ documentHash: ref.documentHash,
1194
+ contentHash: ref.contentHash
1195
+ });
1196
+ }, [documents]);
1194
1197
  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");
1198
+ const readDocument = useCallback(async (download) => {
1199
+ const operation = download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render;
1200
+ if (!mounted.current) throw Errors.cancelled({ operation });
1201
+ if (reference === void 0 || selectedDocument === null) throw Errors.invalidInput("documentHash", "no document on this row");
1202
+ const controller = new AbortController();
1203
+ pending.current.add(controller);
1197
1204
  setIsOpeningDocument(true);
1198
1205
  try {
1199
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.paymentDocuments.render).paymentDocuments.render(receiptDocument.documentHash));
1206
+ const bootstrapped = requireBootstrappedClient(client, download ? CAPXUL_OPERATIONS.paymentDocuments.get : CAPXUL_OPERATIONS.paymentDocuments.render);
1207
+ const pair = {
1208
+ documentHash: selectedDocument.documentHash,
1209
+ contentHash: selectedDocument.contentHash
1210
+ };
1211
+ const options = { signal: controller.signal };
1212
+ const result = download ? await bootstrapped.paymentDocuments.get(pair, options) : await bootstrapped.paymentDocuments.render(pair, options);
1213
+ if (controller.signal.aborted) throw Errors.cancelled({ operation });
1214
+ return unwrapCapxulResult(result);
1200
1215
  } finally {
1201
- setIsOpeningDocument(false);
1216
+ pending.current.delete(controller);
1217
+ if (!controller.signal.aborted) setIsOpeningDocument(pending.current.size > 0);
1202
1218
  }
1203
1219
  }, [
1204
1220
  client,
1205
- receiptDocument,
1221
+ selectedDocument,
1206
1222
  reference
1207
1223
  ]);
1224
+ const renderDocument = useCallback(async () => {
1225
+ const result = await readDocument(false);
1226
+ if (!("html" in result)) throw Errors.invalidInput("documentHash", "invalid document render");
1227
+ return result;
1228
+ }, [readDocument]);
1229
+ const downloadDocument = useCallback(async () => {
1230
+ const result = await readDocument(true);
1231
+ if (!("bytesBase64" in result)) throw Errors.invalidInput("documentHash", "invalid document download");
1232
+ return result;
1233
+ }, [readDocument]);
1208
1234
  const [typed, setTyped] = useState(null);
1209
1235
  const annotate = useCapxulAnnotateMovement();
1210
1236
  const saveMemo = useCallback(async () => {
@@ -1229,8 +1255,15 @@ function useActivityDetail(reference, actor) {
1229
1255
  typed
1230
1256
  ]);
1231
1257
  return {
1232
- receiptDocument,
1233
- openReceiptDocument,
1258
+ ...detail.data?.kind === "payment" ? { payment: {
1259
+ status: detail.data.payment.status,
1260
+ ...detail.data.payment.execution === void 0 ? {} : { execution: detail.data.payment.execution }
1261
+ } } : {},
1262
+ documents,
1263
+ selectedDocument,
1264
+ selectDocument,
1265
+ renderDocument,
1266
+ downloadDocument,
1234
1267
  isOpeningDocument,
1235
1268
  memo: typed ?? detail.data?.annotation?.memo ?? "",
1236
1269
  setMemo: setTyped,
@@ -1243,6 +1276,7 @@ function useActivityDetail(reference, actor) {
1243
1276
  //#endregion
1244
1277
  //#region src/headless/activity/summary.ts
1245
1278
  const DAY_MS = 864e5;
1279
+ const display = (money) => money === null ? null : formatMoney(money);
1246
1280
  /**
1247
1281
  * `"last30days"` is the last 30 whole UTC days including today. Quantizing to
1248
1282
  * the UTC day keeps the cache key stable for a whole day instead of changing on
@@ -1253,57 +1287,6 @@ function resolveWindow(window, now) {
1253
1287
  if (window === "last30days") return { from: Math.floor(now / DAY_MS) * DAY_MS - 29 * DAY_MS };
1254
1288
  return window;
1255
1289
  }
1256
- /** An integer raw-unit string lifted into a major-unit decimal string. */
1257
- function majorUnits(raw, decimals) {
1258
- const negative = raw.startsWith("-");
1259
- const digits = negative ? raw.slice(1) : raw;
1260
- const sign = negative ? "-" : "";
1261
- if (decimals === 0) return `${sign}${digits}`;
1262
- const padded = digits.padStart(decimals + 1, "0");
1263
- const cut = padded.length - decimals;
1264
- return `${sign}${padded.slice(0, cut)}.${padded.slice(cut)}`;
1265
- }
1266
- function absoluteNet(total) {
1267
- const net = BigInt(total.net);
1268
- return net < 0n ? -net : net;
1269
- }
1270
- /**
1271
- * Buckets carry different `decimals`, so their raw integers are not comparable.
1272
- * Both are scaled to the wider precision before the magnitudes are ordered.
1273
- */
1274
- function compareNet(left, right) {
1275
- const width = Math.max(left.decimals, right.decimals);
1276
- const scaledLeft = absoluteNet(left) * 10n ** BigInt(width - left.decimals);
1277
- const scaledRight = absoluteNet(right) * 10n ** BigInt(width - right.decimals);
1278
- if (scaledLeft === scaledRight) return 0;
1279
- return scaledLeft < scaledRight ? -1 : 1;
1280
- }
1281
- /**
1282
- * Which currency `.Summary` shows: the bucket matching the primary holding's
1283
- * currency, else the bucket with the largest absolute `net`, breaking a tie on
1284
- * currency code ascending so the choice is stable across reloads.
1285
- *
1286
- * The primary holding is matched on `available.currency` — the consumer
1287
- * `CurrencyCode` — and NOT on `Holding.symbol`, which is the raw token ticker
1288
- * ("USDX") and can never equal a bucket's currency ("USD"). See the contract
1289
- * correction on #1449.
1290
- */
1291
- function pickBucket(totals, primaryCurrency) {
1292
- if (primaryCurrency !== null) {
1293
- const matched = totals.find((total) => total.currency === primaryCurrency);
1294
- if (matched !== void 0) return matched;
1295
- }
1296
- let best = null;
1297
- for (const total of totals) {
1298
- if (best === null) {
1299
- best = total;
1300
- continue;
1301
- }
1302
- const order = compareNet(total, best);
1303
- if (order > 0 || order === 0 && total.currency < best.currency) best = total;
1304
- }
1305
- return best;
1306
- }
1307
1290
  /**
1308
1291
  * The current UTC day number, which advances ON ITS OWN at UTC midnight.
1309
1292
  *
@@ -1347,47 +1330,36 @@ function useActivitySummary(actor, window) {
1347
1330
  const params = {};
1348
1331
  if (actor !== void 0) params.actor = actor;
1349
1332
  if (range !== void 0) params.window = range;
1350
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1351
- },
1352
- enabled
1353
- });
1354
- const primary = useQuery({
1355
- queryKey: accountKey([...capxulKeys.holdings(actor), "primary"], identity),
1356
- queryFn: async () => {
1357
- return unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.holdings.primary).holdings.primary(actor === void 0 ? void 0 : { actor }));
1333
+ const result = unwrapCapxulResult(await requireBootstrappedClient(client, CAPXUL_OPERATIONS.activity.summary).activity.summary(params));
1334
+ return {
1335
+ ...result,
1336
+ valuation: valueActivitySummary(result.totals)
1337
+ };
1358
1338
  },
1359
1339
  enabled
1360
1340
  });
1361
1341
  const totals = summary.data?.totals;
1362
- const primaryCurrency = primary.data?.available.currency ?? null;
1363
- const isLoading = summary.isLoading || primary.isLoading;
1364
- const bucket = useMemo(() => totals === void 0 || isLoading ? null : pickBucket(totals, primaryCurrency), [
1365
- isLoading,
1366
- primaryCurrency,
1367
- totals
1368
- ]);
1369
- if (bucket === null) return {
1342
+ const isLoading = summary.isLoading;
1343
+ if (summary.data === void 0 || totals === void 0 || totals.length === 0 || isLoading) return {
1370
1344
  currency: null,
1371
1345
  moneyIn: null,
1372
1346
  moneyOut: null,
1373
1347
  net: null,
1374
1348
  pending: null,
1349
+ unrated: [],
1375
1350
  count: summary.data?.count ?? 0,
1376
1351
  isLoading,
1377
1352
  error: asCapxulError$1(summary.error),
1378
1353
  ...summary.data?.readWarning === void 0 ? {} : { readWarning: summary.data.readWarning }
1379
1354
  };
1380
- const display = (raw) => formatMoney({
1381
- currency: bucket.currency,
1382
- value: majorUnits(raw, bucket.decimals),
1383
- decimals: bucket.decimals
1384
- });
1355
+ const valuation = summary.data.valuation;
1385
1356
  return {
1386
- currency: bucket.currency,
1387
- moneyIn: display(bucket.in),
1388
- moneyOut: display(bucket.out),
1389
- net: display(bucket.net),
1390
- pending: display(bucket.pendingIn),
1357
+ currency: valuation.moneyIn?.currency ?? null,
1358
+ moneyIn: display(valuation.moneyIn),
1359
+ moneyOut: display(valuation.moneyOut),
1360
+ net: display(valuation.net),
1361
+ pending: display(valuation.pending),
1362
+ unrated: valuation.unrated,
1391
1363
  count: summary.data?.count ?? 0,
1392
1364
  isLoading,
1393
1365
  error: asCapxulError$1(summary.error),
@@ -1414,7 +1386,7 @@ const SIGN = Object.freeze({
1414
1386
  self: ""
1415
1387
  });
1416
1388
  function toActivityRow(item, actor) {
1417
- const amountDisplay = formatMoney(item.amount);
1389
+ const amountDisplay = formatAssetAmount(item.amount);
1418
1390
  const signedAmountDisplay = `${SIGN[item.direction]}${amountDisplay}`;
1419
1391
  if (item.kind === "movement") return {
1420
1392
  kind: "movement",
@@ -1426,32 +1398,37 @@ function toActivityRow(item, actor) {
1426
1398
  counterpartyLabel: item.counterpartyLabel,
1427
1399
  amountDisplay,
1428
1400
  signedAmountDisplay,
1429
- currency: item.amount.currency,
1401
+ currency: assetSymbolFor(item.amount.asset),
1430
1402
  createdAt: item.createdAt,
1431
1403
  classification: item.classification,
1432
1404
  phase: MOVEMENT_PHASE,
1433
1405
  receipt: item.receipt
1434
1406
  };
1407
+ const reference = actor === void 0 ? {
1408
+ kind: "payment",
1409
+ id: item.id
1410
+ } : {
1411
+ kind: "payment",
1412
+ id: item.id,
1413
+ actor
1414
+ };
1435
1415
  return {
1436
1416
  kind: "payment",
1437
- reference: actor === void 0 ? {
1438
- kind: "payment",
1439
- id: item.id
1440
- } : {
1441
- kind: "payment",
1442
- id: item.id,
1443
- actor
1444
- },
1417
+ ...item.execution === void 0 ? {} : { execution: item.execution },
1418
+ reference,
1445
1419
  direction: item.direction,
1446
1420
  counterpartyLabel: item.counterpartyLabel,
1447
1421
  amountDisplay,
1448
1422
  signedAmountDisplay,
1449
- currency: item.amount.currency,
1423
+ currency: assetSymbolFor(item.amount.asset),
1450
1424
  createdAt: item.createdAt,
1451
1425
  ledgerKind: item.ledgerKind,
1452
1426
  paymentType: item.paymentType,
1453
1427
  status: item.status,
1454
- phase: paymentPhase(item.status),
1428
+ phase: item.execution?.notSent ? {
1429
+ ...paymentPhase(item.status),
1430
+ needsAction: item.direction !== "in"
1431
+ } : paymentPhase(item.status),
1455
1432
  receipt: item.receipt
1456
1433
  };
1457
1434
  }
@@ -1524,8 +1501,8 @@ function buildCsv(items, actor, search) {
1524
1501
  row.counterpartyLabel,
1525
1502
  row.direction,
1526
1503
  item.amount.value,
1527
- item.amount.currency,
1528
- row.kind === "payment" ? row.status : "",
1504
+ assetSymbolFor(item.amount.asset),
1505
+ row.kind === "payment" ? row.execution?.notSent ? "not_sent" : row.execution?.status === "unresolved" ? "verifying" : row.status : "",
1529
1506
  row.reference.id,
1530
1507
  row.receipt?.transactionHash ?? ""
1531
1508
  ]));
@@ -1642,6 +1619,16 @@ function useActivity(input) {
1642
1619
  searchText
1643
1620
  ]);
1644
1621
  const { fetchNextPage, hasNextPage, isFetchingNextPage, refetch } = query;
1622
+ const emptyContinuation = pages?.at(-1)?.items.length === 0 && hasNextPage && query.error === null;
1623
+ useEffect(() => {
1624
+ if (enabled && emptyContinuation && !isFetchingNextPage) fetchNextPage();
1625
+ }, [
1626
+ enabled,
1627
+ emptyContinuation,
1628
+ fetchNextPage,
1629
+ isFetchingNextPage,
1630
+ pages
1631
+ ]);
1645
1632
  const pagingRef = useRef({
1646
1633
  fetchNextPage,
1647
1634
  hasNextPage,
@@ -1744,7 +1731,7 @@ function useActivity(input) {
1744
1731
  },
1745
1732
  rows: {
1746
1733
  rows,
1747
- isLoading: query.isLoading,
1734
+ isLoading: query.isLoading || rows.length === 0 && emptyContinuation,
1748
1735
  error: asCapxulError$1(query.error),
1749
1736
  retry,
1750
1737
  observedAt: pages === void 0 ? null : pages[pages.length - 1]?.observedAt ?? null,
@@ -1770,7 +1757,7 @@ function useActivityContext() {
1770
1757
  if (engine === null) throw new Error("CapxulActivity parts must be used inside <CapxulActivity>");
1771
1758
  return engine;
1772
1759
  }
1773
- function Root$5({ actor, pageSize, children }) {
1760
+ function Root$6({ actor, pageSize, children }) {
1774
1761
  const engine = useActivity({
1775
1762
  ...actor === void 0 ? {} : { actor },
1776
1763
  ...pageSize === void 0 ? {} : { pageSize }
@@ -1821,7 +1808,7 @@ function Export({ children }) {
1821
1808
  return /* @__PURE__ */ jsx(Fragment, { children: children(useActivityContext().csv) });
1822
1809
  }
1823
1810
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
1824
- const CapxulActivity = Object.assign(Root$5, {
1811
+ const CapxulActivity = Object.assign(Root$6, {
1825
1812
  Summary: Summary$2,
1826
1813
  Filters,
1827
1814
  Search,
@@ -1862,6 +1849,26 @@ function useOrgMeReading(orgId) {
1862
1849
  ]);
1863
1850
  }
1864
1851
  //#endregion
1852
+ //#region src/internal/write-names-asset.ts
1853
+ /**
1854
+ * Identity law: "A payment names an asset or inherits it from an authorized
1855
+ * fact. Missing asset refuses." The rule was recorded in the retired
1856
+ * treasury-cut handoff (removed from the current tree by the exact-asset
1857
+ * contraction; its record survives in Git history).
1858
+ *
1859
+ * A Capxul payment writer takes `PaymentMoney` — a peg currency, a value and
1860
+ * `decimals` — and settles one fixed legacy token. No `AssetId` crosses that
1861
+ * seam, so a payment composed from a named position cannot name the asset it
1862
+ * moves: a position selected as USDT authorizes a different token.
1863
+ *
1864
+ * `false` until the writer takes the command's asset. The Send and Payroll
1865
+ * composers read this to refuse a submission rather than authorize a token the
1866
+ * person did not pick.
1867
+ */
1868
+ function writeNamesAsset() {
1869
+ return false;
1870
+ }
1871
+ //#endregion
1865
1872
  //#region src/headless/send-money/signer-status.ts
1866
1873
  const NO_SUBSCRIPTION = () => () => {};
1867
1874
  function useSignerStatus(client) {
@@ -2011,6 +2018,37 @@ function useSelectedOption(options) {
2011
2018
  }, [options]);
2012
2019
  return [options.find((option) => option.id === selectedId) ?? null, select];
2013
2020
  }
2021
+ /**
2022
+ * One Send `.Asset` option per position the Account holds, taken from
2023
+ * `account.balances` — never from `account.available`, which is the Account's
2024
+ * fiat VALUATION of those positions (ADR-0028 R5/R6). Labelling the valuation
2025
+ * was the false identity this cut removes, and it collapsed USDC+USDT into one
2026
+ * "USD" row.
2027
+ *
2028
+ * The option is keyed on the ASSET id, never the symbol and never the account:
2029
+ * several positions share one account id, and a symbol is display only
2030
+ * (ADR-0028 R3). Every row is listed, so a real multi-position Account still
2031
+ * offers its holdings.
2032
+ */
2033
+ function assetSelectionsFromAccount$1(account) {
2034
+ if (account === null) return [];
2035
+ return account.balances.map((position) => {
2036
+ return {
2037
+ option: {
2038
+ id: position.asset,
2039
+ symbol: position.symbol,
2040
+ availableDisplay: formatAssetAmount({
2041
+ asset: position.asset,
2042
+ value: position.available
2043
+ })
2044
+ },
2045
+ money: {
2046
+ asset: position.asset,
2047
+ value: position.available
2048
+ }
2049
+ };
2050
+ });
2051
+ }
2014
2052
  function useSendMoney(input) {
2015
2053
  const { actor, onSent, onFailed } = input;
2016
2054
  const client = useCapxulClientOrNull();
@@ -2023,15 +2061,12 @@ function useSendMoney(input) {
2023
2061
  const treasury = useCapxulOrgTreasury(orgId, { enabled: actor.kind === "organization" });
2024
2062
  const accountQuery = actor.kind === "personal" ? balance : treasury;
2025
2063
  const account = accountQuery.data ?? null;
2026
- const assetOptions = useMemo(() => account === null ? [] : [{
2027
- id: account.id,
2028
- symbol: account.available.currency,
2029
- availableDisplay: formatMoney(account.available, { grammar: "code" })
2030
- }], [account]);
2064
+ const assetSelections = useMemo(() => assetSelectionsFromAccount$1(account), [account]);
2065
+ const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
2031
2066
  const [selectedAssetOption, selectAsset] = useSelectedOption(assetOptions);
2032
- const selectedAssetMoney = selectedAssetOption !== null && account !== null ? account.available : null;
2067
+ const selectedAssetMoney = useMemo(() => assetSelections.find((selection) => selection.option.id === selectedAssetOption?.id)?.money ?? null, [assetSelections, selectedAssetOption]);
2033
2068
  const [amountText, setAmountText] = useState("");
2034
- const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseMoney(amountText, selectedAssetMoney), [amountText, selectedAssetMoney]);
2069
+ const parsed = useMemo(() => selectedAssetMoney === null || amountText.trim() === "" ? null : parseAssetAmount(amountText, requireAsset(selectedAssetMoney.asset)), [amountText, selectedAssetMoney]);
2035
2070
  const parsedMoney = parsed !== null && !isMoneyParseError(parsed) ? parsed : null;
2036
2071
  const amountError = parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null;
2037
2072
  const recipient = useRecipientResolution(client, JSON.stringify([
@@ -2043,7 +2078,7 @@ function useSendMoney(input) {
2043
2078
  const personalPay = useCapxulPay();
2044
2079
  const organizationPay = useCapxulOrganizationPay(orgId);
2045
2080
  const pay = actor.kind === "personal" ? personalPay : organizationPay;
2046
- const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : null;
2081
+ const blockedReason = signerStatus !== "ready" ? "signer-not-ready" : actor.kind === "organization" && selectedSource === null ? "no-source" : selectedAssetOption === null ? "no-asset" : parsedMoney === null ? "amount-invalid" : resolvedReference === null ? "recipient-unresolved" : writeNamesAsset() ? null : "asset-unnamed";
2047
2082
  const submitGate = useRef(false);
2048
2083
  const submit = useCallback(() => {
2049
2084
  if (blockedReason !== null || submitGate.current) return;
@@ -2130,7 +2165,7 @@ function useSendMoneyContext() {
2130
2165
  if (engine === null) throw new Error("CapxulSendMoney parts must be used inside <CapxulSendMoney>");
2131
2166
  return engine;
2132
2167
  }
2133
- function Root$4({ actor, onSent, onFailed, children }) {
2168
+ function Root$5({ actor, onSent, onFailed, children }) {
2134
2169
  const engine = useSendMoney({
2135
2170
  actor,
2136
2171
  onSent,
@@ -2157,7 +2192,7 @@ function Actions$1({ children }) {
2157
2192
  return /* @__PURE__ */ jsx(Fragment, { children: children(useSendMoneyContext().actions) });
2158
2193
  }
2159
2194
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2160
- const CapxulSendMoney = Object.assign(Root$4, {
2195
+ const CapxulSendMoney = Object.assign(Root$5, {
2161
2196
  Source,
2162
2197
  Asset: Asset$1,
2163
2198
  Amount,
@@ -2247,7 +2282,7 @@ function useOrgMemberContext() {
2247
2282
  if (state === null) throw new Error("CapxulOrgMember parts must be used inside <CapxulOrgMember>");
2248
2283
  return state;
2249
2284
  }
2250
- function Root$3({ orgId, children }) {
2285
+ function Root$4({ orgId, children }) {
2251
2286
  const state = useOrgMember(orgId);
2252
2287
  return /* @__PURE__ */ jsx(OrgMemberContext.Provider, {
2253
2288
  value: state,
@@ -2267,7 +2302,7 @@ function Standing({ children }) {
2267
2302
  return /* @__PURE__ */ jsx(Fragment, { children: children(useOrgMemberContext().standing) });
2268
2303
  }
2269
2304
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2270
- const CapxulOrgMember = Object.assign(Root$3, {
2305
+ const CapxulOrgMember = Object.assign(Root$4, {
2271
2306
  Can,
2272
2307
  Standing
2273
2308
  });
@@ -2427,7 +2462,7 @@ function usePart(phase) {
2427
2462
  }, [placed, phase]);
2428
2463
  return state.phase === phase ? state : null;
2429
2464
  }
2430
- function Root$2({ scope, children }) {
2465
+ function Root$3({ scope, children }) {
2431
2466
  const state = useDashboardAccess(scope);
2432
2467
  const [placed] = useState(() => /* @__PURE__ */ new Set());
2433
2468
  const value = useMemo(() => ({
@@ -2445,7 +2480,7 @@ function Root$2({ scope, children }) {
2445
2480
  });
2446
2481
  }
2447
2482
  /** Hands nothing — today's app renders `null` here, exactly as it does now. */
2448
- function Checking({ children }) {
2483
+ function Checking$1({ children }) {
2449
2484
  return usePart("checking") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
2450
2485
  }
2451
2486
  function DashboardError({ children }) {
@@ -2472,19 +2507,14 @@ function Granted({ children }) {
2472
2507
  return usePart("granted") === null ? null : /* @__PURE__ */ jsx(Fragment, { children });
2473
2508
  }
2474
2509
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
2475
- const CapxulDashboardAccess = Object.assign(Root$2, {
2476
- Checking,
2510
+ const CapxulDashboardAccess = Object.assign(Root$3, {
2511
+ Checking: Checking$1,
2477
2512
  Error: DashboardError,
2478
2513
  Redirect,
2479
2514
  Granted
2480
2515
  });
2481
2516
  //#endregion
2482
2517
  //#region src/headless/payroll/money.ts
2483
- /** `{ value: "12.34", decimals: 6 }` → `12340000n`. */
2484
- function toMinorUnits(money) {
2485
- const [whole = "0", fraction = ""] = money.value.split(".");
2486
- return BigInt(`${whole}${fraction.padEnd(money.decimals, "0")}`);
2487
- }
2488
2518
  const TYPED_AMOUNT = /^(-?)(\d+)(?:\.(\d+))?$/;
2489
2519
  /**
2490
2520
  * Σ of the employer's TYPED amounts on a saved group — a DISPLAY total.
@@ -2517,6 +2547,17 @@ function fromMinorUnits(raw, asset) {
2517
2547
  decimals: asset.decimals
2518
2548
  };
2519
2549
  }
2550
+ /** Convert exact raw units to one asset quantity without a fiat denomination. */
2551
+ function assetFromMinorUnits(raw, asset) {
2552
+ const money = fromMinorUnits(raw, {
2553
+ currency: "USD",
2554
+ decimals: asset.decimals
2555
+ });
2556
+ return {
2557
+ asset: asset.asset,
2558
+ value: money.value
2559
+ };
2560
+ }
2520
2561
  //#endregion
2521
2562
  //#region src/headless/payroll/reads.ts
2522
2563
  function toRecipientOption(entry) {
@@ -2633,7 +2674,7 @@ function scopedClient(client, method, orgId) {
2633
2674
  //#endregion
2634
2675
  //#region src/headless/payroll/use-payroll.ts
2635
2676
  /** Money reads the same way everywhere on this screen: amount then code. */
2636
- const GRAMMAR$1 = { grammar: "code" };
2677
+ const GRAMMAR = { grammar: "code" };
2637
2678
  /**
2638
2679
  * One captured snapshot, or nothing — never a fabricated empty options object,
2639
2680
  * and never a DOM event a screen wired straight to `onClick`.
@@ -2649,7 +2690,7 @@ function toPayrollGroupRow(group) {
2649
2690
  name: group.name,
2650
2691
  tone: group.tone,
2651
2692
  memberCount: group.members.length,
2652
- totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR$1),
2693
+ totalDisplay: currency === void 0 ? "" : formatMoney(sumTypedAmounts(group.members.map((member) => member.amount), currency), GRAMMAR),
2653
2694
  members: group.members.map((member) => ({
2654
2695
  recipientId: member.partyId,
2655
2696
  amount: member.amount,
@@ -2739,8 +2780,8 @@ function usePayroll(orgId) {
2739
2780
  const started = orgId !== void 0;
2740
2781
  return {
2741
2782
  summary: {
2742
- walletBalanceDisplay: treasury.data === void 0 ? null : formatMoney(treasury.data.available, GRAMMAR$1),
2743
- paidThisMonthDisplay: runs.data === void 0 ? null : formatMoney(runs.data.settledThisMonth, GRAMMAR$1),
2783
+ walletBalanceDisplay: treasury.data?.available === void 0 || treasury.data.available === null ? null : formatMoney(treasury.data.available, GRAMMAR),
2784
+ paidThisMonthDisplay: runs.data === void 0 ? null : runs.data.settledThisMonth.map(formatAssetAmount).join(", "),
2744
2785
  isLoading: started && (treasury.data === void 0 && treasury.error === null || runs.data === void 0 && runs.error === null),
2745
2786
  error: treasury.error ?? runs.error
2746
2787
  },
@@ -2773,7 +2814,7 @@ function usePayrollContext() {
2773
2814
  if (engine === null) throw new Error("CapxulPayroll parts must be used inside <CapxulPayroll>");
2774
2815
  return engine;
2775
2816
  }
2776
- function Root$1({ orgId, children }) {
2817
+ function Root$2({ orgId, children }) {
2777
2818
  const engine = usePayroll(orgId);
2778
2819
  return /* @__PURE__ */ jsx(PayrollContext.Provider, {
2779
2820
  value: engine,
@@ -2791,7 +2832,7 @@ function Groups({ children }) {
2791
2832
  * There is NO `.Runs` part (ruling Q6): no shipped region has the right shape
2792
2833
  * for a run list, and a part without a production caller may not ship.
2793
2834
  */
2794
- const CapxulPayroll = Object.assign(Root$1, {
2835
+ const CapxulPayroll = Object.assign(Root$2, {
2795
2836
  Summary: Summary$1,
2796
2837
  Groups
2797
2838
  });
@@ -2801,8 +2842,14 @@ function record(value) {
2801
2842
  if (typeof value !== "object" || value === null || Array.isArray(value)) throw Errors.invalidInput("payrollDraft", "Invalid run draft");
2802
2843
  return value;
2803
2844
  }
2845
+ /**
2846
+ * A draft-scoped opaque key. `/` is in the class so `asset.id` can be the
2847
+ * canonical CAIP-19 `AssetId` (`eip155:<chainId>/erc20:<address>`): the draft
2848
+ * records WHICH asset was authorized, and a symbol is not an identity
2849
+ * (ADR-0028 R3). The field never reaches the backend — only `input` is sent.
2850
+ */
2804
2851
  function identifier(value) {
2805
- if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
2852
+ if (typeof value !== "string" || !/^[A-Za-z0-9][A-Za-z0-9._:/-]{0,127}$/u.test(value)) throw Errors.invalidInput("payrollDraft", "Invalid draft identifier");
2806
2853
  return value;
2807
2854
  }
2808
2855
  function minorAmount(value) {
@@ -2818,8 +2865,9 @@ function decodeDraft(value, scope) {
2818
2865
  const items = input.items.map((raw) => {
2819
2866
  const item = record(raw);
2820
2867
  const to = record(item.to);
2868
+ const amount = record(item.amount);
2821
2869
  const partyId = toPartyId(identifier(item.partyId));
2822
- 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");
2870
+ 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");
2823
2871
  const net = minorAmount(item.net);
2824
2872
  return {
2825
2873
  to: {
@@ -2827,12 +2875,20 @@ function decodeDraft(value, scope) {
2827
2875
  partyId
2828
2876
  },
2829
2877
  partyId,
2878
+ amount: {
2879
+ asset: requireAsset(amount.asset).assetId,
2880
+ value: amount.value
2881
+ },
2830
2882
  gross: net,
2831
2883
  net,
2884
+ currency: item.currency,
2885
+ decimals: item.decimals,
2832
2886
  adjustments: []
2833
2887
  };
2834
2888
  });
2835
2889
  if (new Set(items.map((item) => item.partyId)).size !== items.length) throw Errors.invalidInput("payrollDraft", "Duplicate draft recipient");
2890
+ const exactAsset = requireAsset(identifier(asset.id));
2891
+ if (items.some((item) => item.amount.asset !== exactAsset.assetId)) throw Errors.invalidInput("payrollDraft", "Draft item asset does not match the run asset");
2836
2892
  return {
2837
2893
  version: 1,
2838
2894
  scope,
@@ -2843,6 +2899,7 @@ function decodeDraft(value, scope) {
2843
2899
  },
2844
2900
  input: {
2845
2901
  permissionId: identifier(input.permissionId),
2902
+ asset: exactAsset.assetId,
2846
2903
  period: {
2847
2904
  start: period.start,
2848
2905
  end: period.start
@@ -3046,7 +3103,6 @@ function termsSeeds(terms, asset, period) {
3046
3103
  }
3047
3104
  //#endregion
3048
3105
  //#region src/headless/payroll/use-payroll-run.ts
3049
- const GRAMMAR = { grammar: "code" };
3050
3106
  /**
3051
3107
  * The period this composer composes: an INSTANT run (ruling Q3). `authorizeRun`
3052
3108
  * stamps `start === end === runAt` at submit, and the payslip's period renders
@@ -3070,6 +3126,32 @@ const INSTANT_RUN_PERIOD = {
3070
3126
  function amountText(amounts, seeds, recipientId) {
3071
3127
  return amounts[recipientId]?.value ?? seeds.get(recipientId) ?? "";
3072
3128
  }
3129
+ function freshRunItems(input) {
3130
+ const items = [];
3131
+ for (const [index, recipientId] of input.selected.entries()) {
3132
+ const to = input.payableRefs[index];
3133
+ const amount = input.amounts[index];
3134
+ const terms = input.compensationTerms.get(recipientId);
3135
+ if (to === void 0 || amount === void 0 || terms === void 0) throw Errors.unknown();
3136
+ items.push({
3137
+ to: {
3138
+ kind: "party",
3139
+ partyId: toPartyId(recipientId)
3140
+ },
3141
+ partyId: recipientId,
3142
+ amount,
3143
+ gross: terms.rate,
3144
+ net: terms.rate,
3145
+ currency: terms.currency,
3146
+ decimals: terms.decimals,
3147
+ adjustments: []
3148
+ });
3149
+ }
3150
+ return items;
3151
+ }
3152
+ function hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, terms) {
3153
+ return frozen || !writerNamesSelectedAsset || selected.every((recipientId) => terms.has(recipientId));
3154
+ }
3073
3155
  /**
3074
3156
  * WHICH run the composer is composing, by MEANING rather than by object shape.
3075
3157
  *
@@ -3101,6 +3183,33 @@ function initialAmounts(prefill) {
3101
3183
  };
3102
3184
  return seeded;
3103
3185
  }
3186
+ /**
3187
+ * One `.Asset` option per position the treasury Account holds, taken from
3188
+ * `account.balances` — never from `account.available`, which is the Account's
3189
+ * fiat VALUATION of those positions (ADR-0028 R5/R6). The option is keyed on
3190
+ * the ASSET id: several positions share one account id, and a symbol is display
3191
+ * only (ADR-0028 R3). Every row is listed, so a real multi-position treasury
3192
+ * still offers its holdings.
3193
+ */
3194
+ function assetSelectionsFromAccount(account) {
3195
+ if (account === null) return [];
3196
+ return account.balances.map((position) => {
3197
+ return {
3198
+ option: {
3199
+ id: position.asset,
3200
+ symbol: position.symbol,
3201
+ availableDisplay: formatAssetAmount({
3202
+ asset: position.asset,
3203
+ value: position.available
3204
+ })
3205
+ },
3206
+ money: {
3207
+ asset: position.asset,
3208
+ value: position.available
3209
+ }
3210
+ };
3211
+ });
3212
+ }
3104
3213
  function usePayrollRun(input) {
3105
3214
  const { orgId, prefill, onRun, onFailed } = input;
3106
3215
  const client = useCapxulClientOrNull();
@@ -3114,17 +3223,13 @@ function usePayrollRun(input) {
3114
3223
  const terms = useOrgTerms(orgId);
3115
3224
  const [selected, setSelected] = useState(() => prefill?.recipientIds ?? []);
3116
3225
  const [amounts, setAmounts] = useState(() => initialAmounts(prefill));
3117
- const restoreCurrency = treasury.data?.available.currency;
3118
3226
  const { draftScope, currentScope, draft, draftError, restorationPending, activateDraft, restoreDraft, setDraft, recovery, draftBlocked } = useRunDraft(client, orgId, useCallback((restored) => {
3119
3227
  setSelected(restored.input.items.map((item) => item.partyId));
3120
- if (restoreCurrency !== void 0 && restoreCurrency === restored.asset.currency) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3121
- value: fromMinorUnits(BigInt(item.net), {
3122
- currency: restoreCurrency,
3123
- decimals: restored.asset.decimals
3124
- }).value,
3228
+ if (requireAsset(restored.input.asset).assetId === restored.input.asset) setAmounts(Object.fromEntries(restored.input.items.map((item) => [item.partyId, {
3229
+ value: item.amount.value,
3125
3230
  prefilled: false
3126
3231
  }])));
3127
- }, [restoreCurrency]));
3232
+ }, []));
3128
3233
  const frozen = draft !== null;
3129
3234
  const composerKey = composerIdentity(orgId, prefill, draftScope, frozen);
3130
3235
  const [seededKey, setSeededKey] = useState(composerKey);
@@ -3164,11 +3269,8 @@ function usePayrollRun(input) {
3164
3269
  draftError
3165
3270
  ]);
3166
3271
  const account = treasury.data ?? null;
3167
- const assetOptions = useMemo(() => account === null ? [] : [{
3168
- id: account.id,
3169
- symbol: account.available.currency,
3170
- availableDisplay: formatMoney(account.available, GRAMMAR)
3171
- }], [account]);
3272
+ const assetSelections = useMemo(() => assetSelectionsFromAccount(account), [account]);
3273
+ const assetOptions = useMemo(() => assetSelections.map((selection) => selection.option), [assetSelections]);
3172
3274
  const [selectedAssetId, setSelectedAssetId] = useState(null);
3173
3275
  useEffect(() => {
3174
3276
  if (!assetOptions.some((option) => option.id === selectedAssetId)) setSelectedAssetId(assetOptions[0]?.id ?? null);
@@ -3177,7 +3279,7 @@ function usePayrollRun(input) {
3177
3279
  const selectAsset = useCallback((id) => {
3178
3280
  if (!submitGate.current && !frozen) setSelectedAssetId((current) => assetOptions.some((option) => option.id === id) ? id : current);
3179
3281
  }, [assetOptions, frozen]);
3180
- const assetMoney = selectedAsset !== null && account !== null ? account.available : null;
3282
+ const assetMoney = assetSelections.find((selection) => selection.option.id === selectedAsset?.id)?.money ?? null;
3181
3283
  const named = useMemo(() => new Map(roster.options.map((option) => [option.id, option])), [roster.options]);
3182
3284
  const refs = roster.refs;
3183
3285
  const payableRefs = useMemo(() => {
@@ -3189,12 +3291,19 @@ function usePayrollRun(input) {
3189
3291
  }
3190
3292
  return resolved;
3191
3293
  }, [refs, selected]);
3192
- const seeds = useMemo(() => termsSeeds(terms.data ?? [], assetMoney, INSTANT_RUN_PERIOD), [assetMoney, terms.data]);
3294
+ const seeds = useMemo(() => {
3295
+ if (assetMoney === null) return /* @__PURE__ */ new Map();
3296
+ const metadata = requireAsset(assetMoney.asset);
3297
+ return termsSeeds(terms.data ?? [], metadata.peg === null ? null : {
3298
+ currency: metadata.peg,
3299
+ decimals: metadata.decimals
3300
+ }, INSTANT_RUN_PERIOD);
3301
+ }, [assetMoney, terms.data]);
3193
3302
  const entries = useMemo(() => {
3194
3303
  if (assetMoney === null) return [];
3195
3304
  return selected.map((recipientId) => {
3196
3305
  const value = amountText(amounts, seeds, recipientId);
3197
- const parsed = value.trim() === "" ? null : parseMoney(value, assetMoney);
3306
+ const parsed = value.trim() === "" ? null : parseAssetAmount(value, requireAsset(assetMoney.asset));
3198
3307
  return {
3199
3308
  recipientId,
3200
3309
  name: named.get(recipientId)?.name ?? recipientId,
@@ -3202,7 +3311,7 @@ function usePayrollRun(input) {
3202
3311
  change: (text) => change(recipientId, text),
3203
3312
  error: parsed !== null && isMoneyParseError(parsed) ? parsed.reason : null,
3204
3313
  prefilled: amounts[recipientId]?.prefilled ?? seeds.has(recipientId),
3205
- currency: assetMoney.currency
3314
+ currency: requireAsset(assetMoney.asset).symbol
3206
3315
  };
3207
3316
  });
3208
3317
  }, [
@@ -3217,7 +3326,7 @@ function usePayrollRun(input) {
3217
3326
  if (assetMoney === null || selected.length === 0) return null;
3218
3327
  const money = [];
3219
3328
  for (const recipientId of selected) {
3220
- const parsed = parseMoney(amountText(amounts, seeds, recipientId), assetMoney);
3329
+ const parsed = parseAssetAmount(amountText(amounts, seeds, recipientId), requireAsset(assetMoney.asset));
3221
3330
  if (isMoneyParseError(parsed)) return null;
3222
3331
  money.push(parsed);
3223
3332
  }
@@ -3228,16 +3337,25 @@ function usePayrollRun(input) {
3228
3337
  seeds,
3229
3338
  selected
3230
3339
  ]);
3231
- const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : fromMinorUnits(parsedAmounts.reduce((sum, money) => sum + toMinorUnits(money), 0n), assetMoney), [assetMoney, parsedAmounts]);
3232
- const availableRaw = assetMoney === null ? null : toMinorUnits(assetMoney);
3233
- const totalRaw = total === null ? null : toMinorUnits(total);
3340
+ const total = useMemo(() => parsedAmounts === null || assetMoney === null ? null : {
3341
+ asset: assetMoney.asset,
3342
+ value: assetFromMinorUnits(parsedAmounts.reduce((sum, amount) => sum + toRaw(amount, requireAsset(amount.asset)), 0n), {
3343
+ asset: assetMoney.asset,
3344
+ decimals: requireAsset(assetMoney.asset).decimals
3345
+ }).value
3346
+ }, [assetMoney, parsedAmounts]);
3347
+ const availableRaw = assetMoney === null ? null : toRaw(assetMoney, requireAsset(assetMoney.asset));
3348
+ const totalRaw = total === null ? null : toRaw(total, requireAsset(total.asset));
3234
3349
  const funds = availableRaw === null || totalRaw === null ? "unknown" : totalRaw > availableRaw ? "insufficient" : "available";
3235
3350
  const budgets = me.data?.budgets ?? [];
3351
+ 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]);
3352
+ const writerNamesSelectedAsset = writeNamesAsset();
3236
3353
  const blockedReason = firstFalseFact({
3237
3354
  signerReady: signerStatus === "ready",
3238
3355
  ...runAuthorityFacts(draft, budgets, selectedAsset, assetMoney, funds),
3239
3356
  recipientCount: payableRefs === null ? 0 : selected.length,
3240
- amountsParse: total !== null
3357
+ amountsParse: total !== null && hasRequiredCompensation(frozen, writerNamesSelectedAsset, selected, compensationTerms),
3358
+ writeNamesAsset: writerNamesSelectedAsset
3241
3359
  });
3242
3360
  const authorize = useMutation({
3243
3361
  mutationFn: async ({ candidate, options }) => {
@@ -3259,7 +3377,7 @@ function usePayrollRun(input) {
3259
3377
  if (candidate.scope === currentScope.current) restoreDraft(candidate.scope);
3260
3378
  }
3261
3379
  });
3262
- const totalDisplay = total === null ? null : formatMoney(total, GRAMMAR);
3380
+ const totalDisplay = total === null ? null : formatAssetAmount(total);
3263
3381
  const authorizeMutate = authorize.mutate;
3264
3382
  const submit = useCallback((options) => {
3265
3383
  const attempt = toInvocationOptions(options);
@@ -3268,41 +3386,35 @@ function usePayrollRun(input) {
3268
3386
  if (permissionId === void 0 || totalDisplay === null || parsedAmounts === null) return;
3269
3387
  if (payableRefs === null) return;
3270
3388
  const recipientCount = selected.length;
3271
- const items = [];
3272
- for (const [index, recipientId] of selected.entries()) {
3273
- const to = payableRefs[index];
3274
- const parsed = parsedAmounts[index];
3275
- if (to === void 0 || parsed === void 0) return;
3276
- const raw = toMinorUnits(parsed).toString();
3277
- items.push({
3278
- to: {
3279
- kind: "party",
3280
- partyId: toPartyId(recipientId)
3281
- },
3282
- partyId: recipientId,
3283
- gross: raw,
3284
- net: raw,
3285
- adjustments: []
3389
+ let candidate;
3390
+ if (frozen) candidate = draft;
3391
+ else {
3392
+ const items = freshRunItems({
3393
+ selected,
3394
+ payableRefs,
3395
+ amounts: parsedAmounts,
3396
+ compensationTerms
3286
3397
  });
3287
- }
3288
- const runAt = Date.now();
3289
- const candidate = frozen ? draft : {
3290
- version: 1,
3291
- scope: draftScope,
3292
- asset: {
3293
- id: selectedAsset.id,
3294
- currency: assetMoney.currency,
3295
- decimals: assetMoney.decimals
3296
- },
3297
- input: {
3298
- permissionId,
3299
- period: {
3300
- start: runAt,
3301
- end: runAt
3398
+ const runAt = Date.now();
3399
+ candidate = {
3400
+ version: 1,
3401
+ scope: draftScope,
3402
+ asset: {
3403
+ id: selectedAsset.id,
3404
+ currency: requireAsset(assetMoney.asset).symbol,
3405
+ decimals: requireAsset(assetMoney.asset).decimals
3302
3406
  },
3303
- items
3304
- }
3305
- };
3407
+ input: {
3408
+ permissionId,
3409
+ asset: selectedAsset.id,
3410
+ period: {
3411
+ start: runAt,
3412
+ end: runAt
3413
+ },
3414
+ items
3415
+ }
3416
+ };
3417
+ }
3306
3418
  submitGate.current = true;
3307
3419
  authorizeMutate({
3308
3420
  candidate,
@@ -3334,6 +3446,7 @@ function usePayrollRun(input) {
3334
3446
  selectedAsset,
3335
3447
  blockedReason,
3336
3448
  budgets,
3449
+ compensationTerms,
3337
3450
  onFailed,
3338
3451
  onRun,
3339
3452
  parsedAmounts,
@@ -3382,18 +3495,25 @@ function usePayrollRun(input) {
3382
3495
  };
3383
3496
  }
3384
3497
  function remainingBalanceDisplay(available, total, asset) {
3385
- return available === null || total === null || asset === null ? null : formatMoney(fromMinorUnits(available - total, asset), GRAMMAR);
3498
+ if (available === null || total === null || asset === null) return null;
3499
+ const remaining = available - total;
3500
+ const magnitude = remaining < 0n ? -remaining : remaining;
3501
+ const formatted = formatAssetAmount(assetFromMinorUnits(magnitude, {
3502
+ asset: asset.asset,
3503
+ decimals: requireAsset(asset.asset).decimals
3504
+ }));
3505
+ return remaining < 0n ? `-${formatted}` : formatted;
3386
3506
  }
3387
3507
  /** Persisted replay keeps its authorized Budget and asset; a prior debit cannot block its own recovery. */
3388
3508
  function runAuthorityFacts(draft, budgets, asset, money, funds) {
3389
3509
  return {
3390
3510
  budgetCount: draft !== null && !budgets.some((budget) => budget.id === draft.input.permissionId) ? 0 : budgets.length,
3391
- hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.asset.currency === money?.currency && draft.asset.decimals === money?.decimals),
3511
+ hasAsset: asset !== null && (draft === null || draft.asset.id === asset.id && draft.input.asset === money?.asset),
3392
3512
  funds: draft?.input.requestKey === void 0 ? funds : "available"
3393
3513
  };
3394
3514
  }
3395
3515
  /**
3396
- * The seven facts, in the ONE order the contract fixes. Exactly one code comes
3516
+ * The facts, in the ONE order the contract fixes. Exactly one code comes
3397
3517
  * out, and it is always the first thing that is false — so two problems never
3398
3518
  * race to name the button.
3399
3519
  */
@@ -3404,7 +3524,8 @@ function firstFalseFact(facts) {
3404
3524
  if (!facts.hasAsset) return "no-asset";
3405
3525
  if (facts.recipientCount === 0) return "no-recipients";
3406
3526
  if (!facts.amountsParse) return "amount-invalid";
3407
- return facts.funds === "insufficient" ? "insufficient-funds" : null;
3527
+ if (facts.funds === "insufficient") return "insufficient-funds";
3528
+ return facts.writeNamesAsset ? null : "asset-unnamed";
3408
3529
  }
3409
3530
  //#endregion
3410
3531
  //#region src/headless/payroll/payroll-run.tsx
@@ -3414,7 +3535,7 @@ function usePayrollRunContext() {
3414
3535
  if (engine === null) throw new Error("CapxulPayrollRun parts must be used inside <CapxulPayrollRun>");
3415
3536
  return engine;
3416
3537
  }
3417
- function Root({ orgId, prefill, onRun, onFailed, children }) {
3538
+ function Root$1({ orgId, prefill, onRun, onFailed, children }) {
3418
3539
  const engine = usePayrollRun({
3419
3540
  orgId,
3420
3541
  prefill,
@@ -3444,7 +3565,7 @@ function Actions({ children }) {
3444
3565
  return /* @__PURE__ */ jsx(Fragment, { children: children(engine.actions) });
3445
3566
  }
3446
3567
  /** The compound shape the app's own design system already uses (`StatusTabs`). */
3447
- const CapxulPayrollRun = Object.assign(Root, {
3568
+ const CapxulPayrollRun = Object.assign(Root$1, {
3448
3569
  Recipients,
3449
3570
  Amounts,
3450
3571
  Asset,
@@ -3452,4 +3573,230 @@ const CapxulPayrollRun = Object.assign(Root, {
3452
3573
  Actions
3453
3574
  });
3454
3575
  //#endregion
3455
- export { CapxulActivity, CapxulAuthenticationController, CapxulContacts, CapxulDashboardAccess, CapxulOnboardingController, CapxulOrgMember, CapxulPayroll, CapxulPayrollRun, CapxulProvider, CapxulSendMoney, acknowledgeOnboardingDestination, activeOnboardingRecovery, capxulKeys, clearOnboardingJourney, currentOnboardingJourneyId, entered, invalidateOnboardingJourneyObservation, isClaimed, isRestoring, loadOnboardingJourney, onboardingJourneyPosition, saveOnboardingJourney, startOnboardingJourney, useCapxul, useCapxulAccountBalance, useCapxulAccountFund, useCapxulActivity, useCapxulActivityDetail, useCapxulAnnotateMovement, useCapxulAuth, useCapxulCancelPayment, useCapxulClaimPayment, useCapxulClientOrNull, useCapxulCreateCommitment, useCapxulCreateOrg, useCapxulDestination, useCapxulHandleAvailability, useCapxulHoldings, useCapxulIdentity, useCapxulImageUpload, useCapxulInviteMember, useCapxulOrgMembers, useCapxulOrgRoles, useCapxulOrgTreasury, useCapxulOrganizationPay, useCapxulOrganizationPayBatch, useCapxulOrgs, useCapxulPay, useCapxulPayment, useCapxulPayments, useCapxulPermission, useCapxulPermissionAssign, useCapxulPermissionChange, useCapxulPermissionCreate, useCapxulPermissionReplace, useCapxulPermissionRevoke, useCapxulPermissions, useCapxulProfile, useCapxulRedirectPayment, useCapxulSend, useCapxulTransitions, useCapxulUploadImage };
3576
+ //#region src/headless/client-connection/use-client-connection.ts
3577
+ function useClientConnection(input) {
3578
+ const [view, setView] = useState(null);
3579
+ const [pending, setPending] = useState(false);
3580
+ const [error, setError] = useState(null);
3581
+ const [resendReady, setResendReady] = useState(false);
3582
+ const identityRef = useRef({
3583
+ client: input.client,
3584
+ requestId: input.requestId
3585
+ });
3586
+ identityRef.current = {
3587
+ client: input.client,
3588
+ requestId: input.requestId
3589
+ };
3590
+ const belongsHere = useCallback((client, requestId) => identityRef.current.client === client && identityRef.current.requestId === requestId, []);
3591
+ const refresh = useCallback(async (signal) => {
3592
+ const { client, requestId } = identityRef.current;
3593
+ const result = await client.auth.clientRequests.read({ requestId }, signal === void 0 ? void 0 : { signal });
3594
+ if (signal?.aborted || !belongsHere(client, requestId)) return;
3595
+ if (result.ok) {
3596
+ setView(result.value);
3597
+ setError(null);
3598
+ return;
3599
+ }
3600
+ setError(result.error);
3601
+ }, [belongsHere]);
3602
+ useEffect(() => {
3603
+ setView(null);
3604
+ setError(null);
3605
+ setPending(false);
3606
+ setResendReady(false);
3607
+ const controller = new AbortController();
3608
+ refresh(controller.signal);
3609
+ return () => {
3610
+ controller.abort();
3611
+ };
3612
+ }, [
3613
+ input.client,
3614
+ input.requestId,
3615
+ refresh
3616
+ ]);
3617
+ useEffect(() => {
3618
+ if (view?.phase !== "otp_required") {
3619
+ setResendReady(false);
3620
+ return;
3621
+ }
3622
+ const remaining = view.resendAt - Date.now();
3623
+ if (remaining <= 0) {
3624
+ setResendReady(true);
3625
+ return;
3626
+ }
3627
+ setResendReady(false);
3628
+ const timer = setTimeout(() => setResendReady(true), remaining);
3629
+ return () => clearTimeout(timer);
3630
+ }, [view]);
3631
+ const run = useCallback(async (work) => {
3632
+ const { client, requestId } = identityRef.current;
3633
+ setPending(true);
3634
+ try {
3635
+ const result = await work();
3636
+ if (!belongsHere(client, requestId)) return;
3637
+ if (!result.ok) {
3638
+ setError(result.error);
3639
+ return;
3640
+ }
3641
+ await refresh();
3642
+ } finally {
3643
+ if (belongsHere(client, requestId)) setPending(false);
3644
+ }
3645
+ }, [belongsHere, refresh]);
3646
+ return useMemo(() => {
3647
+ if (error !== null && (view === null || view.phase !== "refused")) return {
3648
+ phase: "failure",
3649
+ otp: null,
3650
+ consent: null,
3651
+ complete: null,
3652
+ failure: {
3653
+ code: error.code,
3654
+ retry: () => refresh(),
3655
+ cancel: async () => {
3656
+ if (view === null) return;
3657
+ const { client, requestId } = identityRef.current;
3658
+ await run(() => client.auth.clientRequests.cancel({
3659
+ requestId,
3660
+ revision: view.revision
3661
+ }));
3662
+ }
3663
+ }
3664
+ };
3665
+ if (view === null) return {
3666
+ phase: "checking",
3667
+ otp: null,
3668
+ consent: null,
3669
+ complete: null,
3670
+ failure: null
3671
+ };
3672
+ if (view.phase === "otp_required") return {
3673
+ phase: view.phase,
3674
+ otp: {
3675
+ email: view.email,
3676
+ pending,
3677
+ canResend: resendReady,
3678
+ verifyCode: async (code) => {
3679
+ const { client, requestId } = identityRef.current;
3680
+ await run(() => client.auth.clientRequests.verifyOtp({
3681
+ requestId,
3682
+ code
3683
+ }));
3684
+ },
3685
+ resend: async () => {
3686
+ const { client, requestId } = identityRef.current;
3687
+ await run(() => client.auth.clientRequests.sendOtp({ requestId }));
3688
+ }
3689
+ },
3690
+ consent: null,
3691
+ complete: null,
3692
+ failure: null
3693
+ };
3694
+ if (view.phase === "review") return {
3695
+ phase: view.phase,
3696
+ otp: null,
3697
+ consent: {
3698
+ verifiedEmail: view.email,
3699
+ clientLabel: view.clientLabel,
3700
+ scopeCodes: view.scopes,
3701
+ purpose: view.intent.mode,
3702
+ setupState: view.setupState,
3703
+ pending,
3704
+ accept: async () => {
3705
+ const { client, requestId } = identityRef.current;
3706
+ await run(() => client.auth.clientRequests.accept({
3707
+ requestId,
3708
+ revision: view.revision
3709
+ }));
3710
+ },
3711
+ cancel: async () => {
3712
+ const { client, requestId } = identityRef.current;
3713
+ await run(() => client.auth.clientRequests.cancel({
3714
+ requestId,
3715
+ revision: view.revision
3716
+ }));
3717
+ }
3718
+ },
3719
+ complete: null,
3720
+ failure: null
3721
+ };
3722
+ if (view.phase === "connected") return {
3723
+ phase: view.phase,
3724
+ otp: null,
3725
+ consent: null,
3726
+ complete: { outcome: "connected" },
3727
+ failure: null
3728
+ };
3729
+ if (view.phase === "refused") return {
3730
+ phase: view.phase,
3731
+ otp: null,
3732
+ consent: null,
3733
+ complete: null,
3734
+ failure: {
3735
+ code: view.code,
3736
+ ...view.field === void 0 ? {} : { field: view.field },
3737
+ retry: () => refresh(),
3738
+ cancel: async () => void 0
3739
+ }
3740
+ };
3741
+ return {
3742
+ phase: view.phase,
3743
+ otp: null,
3744
+ consent: null,
3745
+ complete: null,
3746
+ failure: null
3747
+ };
3748
+ }, [
3749
+ error,
3750
+ pending,
3751
+ refresh,
3752
+ resendReady,
3753
+ run,
3754
+ view
3755
+ ]);
3756
+ }
3757
+ //#endregion
3758
+ //#region src/headless/client-connection/client-connection.tsx
3759
+ const ConnectionContext = createContext(null);
3760
+ function useConnection() {
3761
+ const engine = useContext(ConnectionContext);
3762
+ if (engine === null) throw new Error("CapxulClientConnection parts must be used inside <CapxulClientConnection>");
3763
+ return engine;
3764
+ }
3765
+ function Root({ client, requestId, children }) {
3766
+ const engine = useClientConnection({
3767
+ client,
3768
+ requestId
3769
+ });
3770
+ return /* @__PURE__ */ jsx(ConnectionContext.Provider, {
3771
+ value: engine,
3772
+ children
3773
+ });
3774
+ }
3775
+ function Checking({ children }) {
3776
+ return useConnection().phase === "checking" ? /* @__PURE__ */ jsx(Fragment, { children }) : null;
3777
+ }
3778
+ function Otp({ children }) {
3779
+ const otp = useConnection().otp;
3780
+ return otp === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(otp) });
3781
+ }
3782
+ function Consent({ children }) {
3783
+ const consent = useConnection().consent;
3784
+ return consent === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(consent) });
3785
+ }
3786
+ function Complete({ children }) {
3787
+ const complete = useConnection().complete;
3788
+ return complete === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(complete) });
3789
+ }
3790
+ function Failure({ children }) {
3791
+ const failure = useConnection().failure;
3792
+ return failure === null ? null : /* @__PURE__ */ jsx(Fragment, { children: children(failure) });
3793
+ }
3794
+ const CapxulClientConnection = Object.assign(Root, {
3795
+ Checking,
3796
+ Otp,
3797
+ Consent,
3798
+ Complete,
3799
+ Failure
3800
+ });
3801
+ //#endregion
3802
+ export { CapxulActivity, CapxulAuthenticationController, CapxulClientConnection, CapxulContacts, CapxulDashboardAccess, CapxulOnboardingController, CapxulOrgMember, CapxulPayroll, CapxulPayrollRun, CapxulProvider, CapxulSendMoney, acknowledgeOnboardingDestination, activeOnboardingRecovery, capxulKeys, clearOnboardingJourney, currentOnboardingJourneyId, entered, invalidateOnboardingJourneyObservation, isClaimed, isRestoring, loadOnboardingJourney, onboardingJourneyPosition, saveOnboardingJourney, startOnboardingJourney, useCapxul, useCapxulAccountBalance, useCapxulAccountFund, useCapxulActivity, useCapxulActivityDetail, useCapxulAnnotateMovement, useCapxulAuth, useCapxulCancelPayment, useCapxulClaimPayment, useCapxulClientOrNull, useCapxulCreateCommitment, useCapxulCreateOrg, useCapxulDestination, useCapxulHandleAvailability, useCapxulHoldings, useCapxulIdentity, useCapxulImageUpload, useCapxulOrgMembers, useCapxulOrgRoles, useCapxulOrgTreasury, useCapxulOrganizationPay, useCapxulOrganizationPayBatch, useCapxulOrgs, useCapxulPay, useCapxulPayment, useCapxulPayments, useCapxulPermission, useCapxulPermissionAssign, useCapxulPermissionChange, useCapxulPermissionCreate, useCapxulPermissionReplace, useCapxulPermissionRevoke, useCapxulPermissions, useCapxulProfile, useCapxulRedirectPayment, useCapxulSend, useCapxulTransitions, useCapxulUploadImage };