@fun-xyz/fiat-contract 0.22.1 → 0.23.0

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
@@ -13,7 +13,7 @@ Four things, zero runtime logic beyond validation:
13
13
  | `src/table.ts` | the transition table **as data**: per state, its legal transition set, the states any call from it may return, and `terminal: boolean` |
14
14
  | `src/assert.ts` + `src/fixtures/` | `assertFiatStepResponse` · `assertLegalEmission` · `assertLegalReturn` · `walkTable` · fixture loader + 18 recorded step responses (inlined as data — no filesystem, so React Native can bundle it) |
15
15
 
16
- ## Five entry points — production vs test-time
16
+ ## Six entry points — production vs test-time
17
17
 
18
18
  | Import | Weight | Contains | Used by |
19
19
  | --- | --- | --- | --- |
@@ -21,7 +21,8 @@ Four things, zero runtime logic beyond validation:
21
21
  | `@fun-xyz/fiat-contract/table` | **13.8 KB**, zero deps | `TRANSITION_TABLE`, `stateKey`, `tableEntry`, `isTerminal`, `walkTable`, `TABLE_VERSION`, `TERMINAL_ORDER_STATUSES`, `DOCUMENTED_ENDPOINTS`, `UNOFFERED_ENDPOINTS`, `UNREACHABLE_STATES`, `unofferedEndpoints`, `unreachableStates` | **production frontend** + backend |
22
22
  | `@fun-xyz/fiat-contract/moonpay` | zero deps | the MoonPay provider-owned flow: `intent` request kinds, response statuses and fault types, and `MOONPAY_ENDPOINTS` | **production frontend** (MoonPay adapter) + backend |
23
23
  | `@fun-xyz/fiat-contract/terms` | zero deps | the platform terms route: `TermsDocument`, `FiatTermsRequest`, `FiatTermsResponse`, fault types, and `FIAT_TERMS_ENDPOINT` | **production frontend** + backend |
24
- | `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 46 zod schemas + assertions + 18 fixtures | tests, and backend dev/test guards |
24
+ | `@fun-xyz/fiat-contract/routing` | zero deps | the routing table and routing quote: `FiatRoutingTableQuery`, `FiatRoutingTableResponse`, `FiatRoutingQuoteRequest`, both routes' error bodies, and `ROUTING_ENDPOINTS` | **production frontend** + backend |
25
+ | `@fun-xyz/fiat-contract` | 47.6 KB, needs zod | the above + 77 zod schemas + assertions + 18 fixtures | tests, and backend dev/test guards |
25
26
 
26
27
  `./table` is not a micro-optimisation. Terminality is table data a **shipped** client must read
27
28
  (litmus rule 3 — clients never infer it), and Metro has no cross-module tree-shaking on by default,
@@ -120,6 +121,44 @@ text. `PROVIDER_CHECKOUT_DOCUMENTS` maps each provider to its current `CHECKOUT`
120
121
  stays valid: the backend refuses only an unknown version, a wrong digest, or an `acceptedAt` older
121
122
  than 5 minutes. A repeat of a version the login already holds writes nothing and returns the ids.
122
123
 
124
+ **Routing table (`./routing`).** `ROUTING_ENDPOINTS.table` (`GET /fiat/routing/table`) answers,
125
+ for each requested method, every amount interval and its providers best first, each with `next`
126
+ (`READY`, `AUTH`, `DECLARATIVE_KYC`, `DOCUMENTARY_KYC`, `PENDING_REVIEW`) and `visibility`. The
127
+ client shows min, max, provider and the Continue label for an amount with no call. A method with no
128
+ interval may carry `unavailableReason`, which is not the step response's `BlockedReason`:
129
+ `USER_LIMIT`, `KYC_REJECTED`, `PROVIDER_OUTAGE`, or `NOT_SERVED` when every provider was ruled out
130
+ for reasons that depend on neither the buyer nor provider health (it does not offer the corridor,
131
+ currency, method or asset, or config has not enabled it). An empty method carries no reason when the
132
+ cause is unknown. Refetch the table at `expiresAt`, on `refreshLimits: true`, and after auth, KYC or
133
+ a `REQUOTE`. The schemas hold the table's invariants: each method once, sorted, non-overlapping
134
+ intervals, at least one provider each and none twice, and a reason only on an empty method.
135
+
136
+ `ROUTING_ENDPOINTS.quote` (`POST /fiat/routing/quote`) takes the `POST /fiat/quote` body plus the
137
+ interval's first `provider`, never `routingOverrides`, and answers the same `FiatStepResponse`. A
138
+ refusal is a `FiatRoutingQuoteErrorBody`, and it decides whether to quote the interval's next
139
+ provider:
140
+
141
+ | Status | `errorCode` | Then |
142
+ | --- | --- | --- |
143
+ | 404 | `FiatNoRouteError`, `reason` `NO_CANDIDATE`, `PROVIDER_DECLINED`, `PROVIDER_OUTAGE` or `KYC_REJECTED` | quote the next provider |
144
+ | 400 | `FiatNoRouteError`, `reason: USER_LIMIT`, `code: LIMITS_EXCEEDED`, `recovery: REQUOTE` | quote the next provider |
145
+ | 400 | `FiatAmountBelowMinimumError` (`minFiatAmount`) or `FiatAmountAboveMaximumError` (`maxFiatAmount`) | quote the next provider |
146
+ | 400 | `InvalidParameterError`: a bad request, an asset Fun cannot deliver, or no verified email | stop |
147
+ | 401, 5xx | any other: a missing or invalid identity assertion, or a server error | stop |
148
+
149
+ Every fall-through refusal is the named provider's alone, because the route prices only that
150
+ provider: `KYC_REJECTED` there means that provider refused the buyer's identity, not every provider.
151
+ With no provider left in the interval, refetch the table. The table route refuses with a
152
+ `FiatRoutingTableErrorBody`: a 429 `TooManyRequestsError` with `retryAfterSeconds` when the
153
+ per-login budget is spent, or a 400 or 5xx as above. The quote route has no rate limit of its own.
154
+
155
+ Both routes take geo under the `POST /fiat/quote` rules: a known `CountryCode`, a `UsStateCode`
156
+ region for a US buyer, and an empty `region` or `ip` read as absent. The quote route's `geo` is the
157
+ Fun geo endpoint's response forwarded as is. The request schemas are strict like every schema here,
158
+ so a client sends only the declared keys and no blank strings. `provider` is one of the five
159
+ providers the backend routes (`RoutingProvider`). The quote route is temporary: each provider's
160
+ `intent` route replaces it.
161
+
123
162
  **MoonPay (`./moonpay`).** One route, `MOONPAY_ENDPOINTS.intent`. A `MoonPayIntentRequest` is one of
124
163
  three kinds:
125
164
 
@@ -134,8 +173,8 @@ three kinds:
134
173
  | `status` | Carries | The client then |
135
174
  | --- | --- | --- |
136
175
  | `TERMS` | `stage`, `documents` (at least one, all at `stage`) | sends `POST /fiat/terms`, then `observe` |
137
- | `SDK_AUTH` | `sessionToken`, `quoteInput` | runs the MoonPay connection check, then `observe` with credentials |
138
- | `KYC` | `fields`, `fieldErrors?` (each naming a field in `fields`) | shows the shared form, then sends `kyc` |
176
+ | `SDK_AUTH` | `sessionToken`, `quoteInput`, `resetConnection?` (`true` when the device's MoonPay connection is another buyer's) | resets the connection when asked, runs the MoonPay connection check, then `observe` with credentials |
177
+ | `KYC` | `fields`, `fieldErrors?` (each naming a field in `fields`, with `code?` `PHONE_IN_USE` on `PHONE_NUMBER` or `TAX_ID_IN_USE` on `TAX_IDENTIFIER` when another MoonPay customer holds the value) | shows the shared form, then sends `kyc` |
139
178
  | `PENDING` | `retryAfterMs` (at most 60 s) | sends `observe` after the delay, never `kyc` again |
140
179
  | `READY` | `orderId`, `quoteInput` | mounts Apple Pay, then sends `order` after the charge |
141
180
  | `REFUSED` | `reason` (`NOT_ELIGIBLE`, `DOCUMENTS_REQUIRED`, `FINAL_REJECTION`, `CUSTOMER_MISMATCH`), `recovery: REQUOTE \| NONE` | exits the provider flow |
@@ -420,7 +459,7 @@ loadFixture('screen-10-kyc-on-hold'); // raw JSON, fresh deep copy, `unknown`
420
459
 
421
460
  ### Use a schema directly
422
461
 
423
- All 46 schemas are exported when you need to validate a fragment rather than a whole step response.
462
+ All 77 schemas are exported when you need to validate a fragment rather than a whole step response.
424
463
  They are typed `z.ZodType<T>`, so you get `.parse` / `.safeParse` / `.optional()` — not `.shape` or
425
464
  `.extend`, deliberately.
426
465
 
@@ -513,8 +552,8 @@ Dual CJS + ESM, same shape as `@funkit/fun-relay`, because the two consumers loa
513
552
  | --- | --- | --- |
514
553
  | `require()` must work | `fun-backend/apps/api-server` compiles `module: commonjs` and runs plain node — no bundler | esbuild emits `dist/index.js` (CJS) + `dist/index.mjs` (ESM); `exports` maps `require`/`import`; no `"type": "module"` |
515
554
  | Zero Node builtins | `connect-core` is React Native — Metro cannot resolve `node:fs` | fixtures are inlined as generated TS data (`src/fixtures/data.ts`), so nothing touches the filesystem; `platform: browser`, every bare import external |
516
- | Subpath imports must resolve without `exports` support | Metro only reads `package.json#exports` from RN 0.79 on, and `@funkit/connect-rn` accepts `react-native: >=0.74` | root compat stubs `table.js` / `types.js` / `moonpay.js` / `terms.js` (+ `.d.ts`) that re-export `dist/`. Without them Metro fails with *"Unable to resolve module"* — verified, not theorised. Resolvers that do read `exports` never see the stubs |
517
- | Production code must not pull zod or fixtures | `connect-core` ships to React Native, where Metro does not tree-shake unused exports by default | five entry points: `./types` (erased), `./table` (13.8 KB, zero deps), `./moonpay` and `./terms` (zero deps), root (schemas + fixtures, test-time). CI asserts `require('.../table')`, `require('.../moonpay')` and `require('.../terms')` never load zod |
555
+ | Subpath imports must resolve without `exports` support | Metro only reads `package.json#exports` from RN 0.79 on, and `@funkit/connect-rn` accepts `react-native: >=0.74` | root compat stubs `table.js` / `types.js` / `moonpay.js` / `terms.js` / `routing.js` (+ `.d.ts`) that re-export `dist/`. Without them Metro fails with *"Unable to resolve module"* — verified, not theorised. Resolvers that do read `exports` never see the stubs |
556
+ | Production code must not pull zod or fixtures | `connect-core` ships to React Native, where Metro does not tree-shake unused exports by default | six entry points: `./types` (erased), `./table` (13.8 KB, zero deps), `./moonpay`, `./terms` and `./routing` (zero deps), root (schemas + fixtures, test-time). CI asserts `require('.../table')`, `require('.../moonpay')`, `require('.../terms')` and `require('.../routing')` never load zod |
518
557
  | `.d.ts` must not lock a zod major | Both repos run `skipLibCheck: true`, which turns a broken declaration into a silent `any` | every exported schema is annotated `z.ZodType<T>`, so declarations name only `z.ZodType`; the structural drift checks stay module-private |
519
558
 
520
559
  Declarations come from `tsc --emitDeclarationOnly`; esbuild only emits JS. The fixture `.json` files
@@ -6,4 +6,4 @@ var MOONPAY_ENDPOINTS = {
6
6
  export {
7
7
  MOONPAY_ENDPOINTS
8
8
  };
9
- //# sourceMappingURL=chunk-I63WALQK.mjs.map
9
+ //# sourceMappingURL=chunk-3HCZ6JBJ.mjs.map
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.22.1";
2
+ var version = "0.23.0";
3
3
 
4
4
  // src/table.ts
5
5
  var TABLE_VERSION = version;
@@ -514,4 +514,4 @@ export {
514
514
  tableEntry,
515
515
  isTerminal
516
516
  };
517
- //# sourceMappingURL=chunk-PP6S7FSJ.mjs.map
517
+ //# sourceMappingURL=chunk-EI56NQ7K.mjs.map
@@ -0,0 +1,11 @@
1
+ // src/routing.ts
2
+ var ROUTING_ENDPOINTS = {
3
+ table: "GET /fiat/routing/table",
4
+ /** Temporary: each provider's `intent` route replaces it. */
5
+ quote: "POST /fiat/routing/quote"
6
+ };
7
+
8
+ export {
9
+ ROUTING_ENDPOINTS
10
+ };
11
+ //# sourceMappingURL=chunk-KYE2HYV5.mjs.map
package/dist/index.d.ts CHANGED
@@ -11,6 +11,7 @@ export * from './schemas';
11
11
  export * from './table';
12
12
  export * from './assert';
13
13
  export * from './terms';
14
+ export * from './routing';
14
15
  export * from './providers/moonpay';
15
16
  export * from './providers/moonpay.schemas';
16
17
  export { FIXTURES, FIXTURE_COVERAGE_GAPS, fixtureMeta, loadFixture, loadFixtures, type FixtureMeta, type FixtureSource, type LoadedFixture, } from './fixtures/index';
package/dist/index.js CHANGED
@@ -43,6 +43,11 @@ __export(index_exports, {
43
43
  FiatCurrencyCode: () => FiatCurrencyCode,
44
44
  FiatEndpointSchema: () => FiatEndpointSchema,
45
45
  FiatProviderSchema: () => FiatProviderSchema,
46
+ FiatRoutingQuoteErrorBodySchema: () => FiatRoutingQuoteErrorBodySchema,
47
+ FiatRoutingQuoteRequestSchema: () => FiatRoutingQuoteRequestSchema,
48
+ FiatRoutingTableErrorBodySchema: () => FiatRoutingTableErrorBodySchema,
49
+ FiatRoutingTableQuerySchema: () => FiatRoutingTableQuerySchema,
50
+ FiatRoutingTableResponseSchema: () => FiatRoutingTableResponseSchema,
46
51
  FiatStepResponseSchema: () => FiatStepResponseSchema,
47
52
  FiatTermsErrorBodySchema: () => FiatTermsErrorBodySchema,
48
53
  FiatTermsRequestSchema: () => FiatTermsRequestSchema,
@@ -94,6 +99,7 @@ __export(index_exports, {
94
99
  QuoteLimitsSchema: () => QuoteLimitsSchema,
95
100
  QuoteSchema: () => QuoteSchema,
96
101
  QuoteStateSchema: () => QuoteStateSchema,
102
+ ROUTING_ENDPOINTS: () => ROUTING_ENDPOINTS,
97
103
  RecoverySchema: () => RecoverySchema,
98
104
  RefundSchema: () => RefundSchema,
99
105
  ReportSpecSchema: () => ReportSpecSchema,
@@ -975,6 +981,8 @@ var $providerBoundFlowState = import_zod.z.union([
975
981
  var $flowState = import_zod.z.union([$providerBoundFlowState, $blockedState]);
976
982
  var FlowStateSchema = $flowState;
977
983
  var $nonBlank = import_zod.z.string().refine((value) => value.trim().length > 0, "must not be blank");
984
+ var $isoInstant = import_zod.z.string().datetime({ offset: true });
985
+ var $errorBodyFields = { errorMsg: import_zod.z.string(), reqId: import_zod.z.string().optional() };
978
986
  var $termsStage = import_zod.z.enum(["CHECKOUT", "KYC"]);
979
987
  var $termsDocument = obj({
980
988
  id: $nonBlank,
@@ -990,7 +998,7 @@ var $termsAcceptance = obj({
990
998
  documentId: $nonBlank,
991
999
  version: $nonBlank,
992
1000
  contentDigest: $nonBlank,
993
- acceptedAt: import_zod.z.string().datetime({ offset: true })
1001
+ acceptedAt: $isoInstant
994
1002
  });
995
1003
  var TermsDocumentAcceptanceSchema = $termsAcceptance;
996
1004
  var $termsRequest = obj({
@@ -1002,12 +1010,117 @@ var $termsResponse = obj({ recorded: import_zod.z.array($nonBlank) });
1002
1010
  var FiatTermsResponseSchema = $termsResponse;
1003
1011
  var $termsErrorBody = obj({
1004
1012
  errorCode: $nonBlank,
1005
- errorMsg: import_zod.z.string(),
1006
- reqId: import_zod.z.string().optional(),
1013
+ ...$errorBodyFields,
1007
1014
  reason: import_zod.z.enum(["STALE_TERMS", "INVALID_REQUEST", "RATE_LIMITED", "PROVIDER_UNAVAILABLE"]),
1008
1015
  recovery: import_zod.z.enum(["RETRY", "REQUOTE", "NONE"])
1009
1016
  });
1010
1017
  var FiatTermsErrorBodySchema = $termsErrorBody;
1018
+ var $decimalAmount = import_zod.z.string().regex(/^(?:0|[1-9]\d*)(?:\.\d+)?$/, "must be a decimal string");
1019
+ var compareDecimals = (left, right) => {
1020
+ const [leftWhole = "", leftFraction = ""] = left.split(".");
1021
+ const [rightWhole = "", rightFraction = ""] = right.split(".");
1022
+ if (leftWhole.length !== rightWhole.length) return leftWhole.length - rightWhole.length;
1023
+ const width = Math.max(leftFraction.length, rightFraction.length);
1024
+ const leftDigits = leftWhole + leftFraction.padEnd(width, "0");
1025
+ const rightDigits = rightWhole + rightFraction.padEnd(width, "0");
1026
+ return leftDigits < rightDigits ? -1 : leftDigits > rightDigits ? 1 : 0;
1027
+ };
1028
+ var $countryCode = import_zod.z.enum(Object.keys(CountryCode));
1029
+ var $routingProvider = $provider.extract(["TRANSAK", "BANXA", "MOONPAY", "STRIPE", "COINBASE"]);
1030
+ var $optionalGeoField = import_zod.z.union([$nonBlank, import_zod.z.literal("")]).optional();
1031
+ var US_REGION_REQUIRED = "a US buyer needs a US state code as region";
1032
+ var hasUsRegion = (country, region) => country !== "US" || UsStateCodeSchema.safeParse(region).success;
1033
+ var $routingTableQuery = obj({
1034
+ fiatCurrencyCode: import_zod.z.enum(FIAT_CURRENCY_CODES).optional(),
1035
+ cryptoCurrencyCode: $nonBlank,
1036
+ chainId: $nonBlank,
1037
+ country: $countryCode,
1038
+ region: $optionalGeoField,
1039
+ methods: import_zod.z.string().refine(
1040
+ (methods) => methods.split(",").every((method) => PAYMENT_METHOD_CATEGORIES.includes(method.trim())),
1041
+ "must be comma-separated payment method categories"
1042
+ )
1043
+ }).refine((query) => hasUsRegion(query.country, query.region), { message: US_REGION_REQUIRED, path: ["region"] });
1044
+ var FiatRoutingTableQuerySchema = $routingTableQuery;
1045
+ var $routingTableProvider = obj({
1046
+ provider: $routingProvider,
1047
+ next: import_zod.z.enum(["READY", "AUTH", "DECLARATIVE_KYC", "DOCUMENTARY_KYC", "PENDING_REVIEW"]),
1048
+ visibility: import_zod.z.enum(["COMPLETE", "PARTIAL", "AUTH_GATED"])
1049
+ });
1050
+ var $routingTableInterval = obj({
1051
+ min: $decimalAmount,
1052
+ max: $decimalAmount,
1053
+ providers: import_zod.z.array($routingTableProvider).min(1).refine((providers) => hasUniqueIds(providers.map((entry) => entry.provider)), "each provider is listed once")
1054
+ }).refine((interval) => compareDecimals(interval.min, interval.max) <= 0, { message: "min must not exceed max", path: ["max"] });
1055
+ var $routingTableMethod = obj({
1056
+ method: import_zod.z.enum(PAYMENT_METHOD_CATEGORIES),
1057
+ unavailableReason: import_zod.z.enum(["USER_LIMIT", "KYC_REJECTED", "PROVIDER_OUTAGE", "NOT_SERVED"]).optional(),
1058
+ intervals: import_zod.z.array($routingTableInterval).superRefine((intervals, context) => {
1059
+ intervals.forEach((interval, index) => {
1060
+ const previous = intervals[index - 1];
1061
+ if (previous !== void 0 && compareDecimals(interval.min, previous.max) <= 0) {
1062
+ context.addIssue({ code: "custom", path: [index, "min"], message: "intervals are sorted and do not overlap" });
1063
+ }
1064
+ });
1065
+ })
1066
+ }).refine((method) => method.unavailableReason === void 0 || method.intervals.length === 0, {
1067
+ message: "unavailableReason only with no interval",
1068
+ path: ["unavailableReason"]
1069
+ });
1070
+ var $routingTableResponse = obj({
1071
+ expiresAt: $isoInstant,
1072
+ methods: import_zod.z.array($routingTableMethod).refine((methods) => hasUniqueIds(methods.map((entry) => entry.method)), "each method is listed once")
1073
+ });
1074
+ var FiatRoutingTableResponseSchema = $routingTableResponse;
1075
+ var $routingQuoteRequest = obj({
1076
+ provider: $routingProvider,
1077
+ fiatCurrency: import_zod.z.enum(FIAT_CURRENCY_CODES),
1078
+ fiatAmount: $decimalAmount.refine((amount) => Number(amount) > 0, "must be positive"),
1079
+ cryptoCurrencyCode: $nonBlank,
1080
+ chainId: $nonBlank,
1081
+ paymentMethodId: import_zod.z.enum(PAYMENT_METHOD_CATEGORIES),
1082
+ geo: obj({ alpha2: $countryCode, region: $optionalGeoField, ip: $optionalGeoField, city: import_zod.z.string().optional() }).refine(
1083
+ (geo) => hasUsRegion(geo.alpha2, geo.region),
1084
+ { message: US_REGION_REQUIRED, path: ["region"] }
1085
+ ),
1086
+ prepareSessionAuth: import_zod.z.boolean().optional()
1087
+ });
1088
+ var FiatRoutingQuoteRequestSchema = $routingQuoteRequest;
1089
+ var $routingServiceableRange = obj({ min: $money, max: $money, userMax: $money.nullable().optional() });
1090
+ var $noRouteFields = {
1091
+ ...$errorBodyFields,
1092
+ errorCode: import_zod.z.literal("FiatNoRouteError"),
1093
+ serviceableRanges: import_zod.z.array($routingServiceableRange),
1094
+ retryable: import_zod.z.boolean()
1095
+ };
1096
+ var $routingNoRouteErrorBody = import_zod.z.union([
1097
+ obj({ ...$noRouteFields, reason: import_zod.z.enum(["NO_CANDIDATE", "PROVIDER_DECLINED", "PROVIDER_OUTAGE", "KYC_REJECTED"]) }),
1098
+ obj({ ...$noRouteFields, reason: import_zod.z.literal("USER_LIMIT"), code: import_zod.z.literal("LIMITS_EXCEEDED"), recovery: import_zod.z.literal("REQUOTE") })
1099
+ ]);
1100
+ var $amountErrorFields = { ...$errorBodyFields, fiatCurrencyCode: import_zod.z.enum(FIAT_CURRENCY_CODES) };
1101
+ var $routingAmountErrorBody = import_zod.z.union([
1102
+ obj({ ...$amountErrorFields, errorCode: import_zod.z.literal("FiatAmountBelowMinimumError"), minFiatAmount: $decimalAmount }),
1103
+ obj({ ...$amountErrorFields, errorCode: import_zod.z.literal("FiatAmountAboveMaximumError"), maxFiatAmount: $decimalAmount })
1104
+ ]);
1105
+ var $routingRateLimitedErrorBody = obj({
1106
+ ...$errorBodyFields,
1107
+ errorCode: import_zod.z.literal("TooManyRequestsError"),
1108
+ retryAfterSeconds: import_zod.z.number().int().positive().optional()
1109
+ });
1110
+ var ROUTING_ERROR_CODES = [
1111
+ "FiatNoRouteError",
1112
+ "FiatAmountBelowMinimumError",
1113
+ "FiatAmountAboveMaximumError",
1114
+ "TooManyRequestsError"
1115
+ ];
1116
+ var $routingFaultErrorBody = obj({
1117
+ ...$errorBodyFields,
1118
+ errorCode: $nonBlank.refine((code) => !ROUTING_ERROR_CODES.includes(code), "this errorCode carries its own fields")
1119
+ });
1120
+ var $routingTableErrorBody = import_zod.z.union([$routingRateLimitedErrorBody, $routingFaultErrorBody]);
1121
+ var FiatRoutingTableErrorBodySchema = $routingTableErrorBody;
1122
+ var $routingQuoteErrorBody = import_zod.z.union([$routingNoRouteErrorBody, $routingAmountErrorBody, $routingFaultErrorBody]);
1123
+ var FiatRoutingQuoteErrorBodySchema = $routingQuoteErrorBody;
1011
1124
  var $providerBoundStepResponse = obj({
1012
1125
  state: $providerBoundFlowState,
1013
1126
  provider: $provider,
@@ -1067,7 +1180,7 @@ var $stepResponse = import_zod.z.union([$providerBoundStepResponse, $blockedStep
1067
1180
  var FiatStepResponseSchema = $stepResponse;
1068
1181
 
1069
1182
  // package.json
1070
- var version = "0.22.1";
1183
+ var version = "0.23.0";
1071
1184
 
1072
1185
  // src/table.ts
1073
1186
  var TABLE_VERSION = version;
@@ -2670,6 +2783,13 @@ var FUN_KYC_CONSENT_DOCUMENTS = {
2670
2783
  };
2671
2784
  var PUBLISHED_TERMS_DOCUMENTS = [MOONPAY_TERMS, ...Object.values(FUN_KYC_CONSENT_DOCUMENTS)];
2672
2785
 
2786
+ // src/routing.ts
2787
+ var ROUTING_ENDPOINTS = {
2788
+ table: "GET /fiat/routing/table",
2789
+ /** Temporary: each provider's `intent` route replaces it. */
2790
+ quote: "POST /fiat/routing/quote"
2791
+ };
2792
+
2673
2793
  // src/providers/moonpay.ts
2674
2794
  var MOONPAY_ENDPOINTS = {
2675
2795
  intent: "POST /fiat/providers/moonpay/intent"
@@ -2718,11 +2838,12 @@ var $quoteInput = obj2({
2718
2838
  var MoonPayQuoteInputSchema = $quoteInput;
2719
2839
  var $stepRefusal = import_zod2.z.enum(["NOT_ELIGIBLE", "DOCUMENTS_REQUIRED", "FINAL_REJECTION", "CUSTOMER_MISMATCH"]);
2720
2840
  var $orderRefusal = import_zod2.z.enum(["CUSTOMER_MISMATCH", "WALLET_MISMATCH", "UNRECORDABLE"]);
2721
- var $fieldError = obj2({ fieldId: $nonEmpty, message: $nonEmpty });
2841
+ var FIELD_OF_CODE = { PHONE_IN_USE: "PHONE_NUMBER", TAX_ID_IN_USE: "TAX_IDENTIFIER" };
2842
+ var $fieldError = obj2({ fieldId: $nonEmpty, message: $nonEmpty, code: import_zod2.z.enum(["PHONE_IN_USE", "TAX_ID_IN_USE"]).optional() });
2722
2843
  var $pending = obj2({ status: import_zod2.z.literal("PENDING"), retryAfterMs: import_zod2.z.number().int().positive().max(6e4) });
2723
2844
  var $stepStatus = import_zod2.z.discriminatedUnion("status", [
2724
2845
  obj2({ status: import_zod2.z.literal("TERMS"), stage: import_zod2.z.enum(["CHECKOUT", "KYC"]), documents: import_zod2.z.array(TermsDocumentSchema).min(1) }),
2725
- obj2({ status: import_zod2.z.literal("SDK_AUTH"), sessionToken: $nonEmpty, quoteInput: $quoteInput }),
2846
+ obj2({ status: import_zod2.z.literal("SDK_AUTH"), sessionToken: $nonEmpty, quoteInput: $quoteInput, resetConnection: import_zod2.z.literal(true).optional() }),
2726
2847
  obj2({ status: import_zod2.z.literal("KYC"), fields: import_zod2.z.array(FormFieldSchema).min(1), fieldErrors: import_zod2.z.array($fieldError).min(1).optional() }),
2727
2848
  $pending,
2728
2849
  obj2({ status: import_zod2.z.literal("READY"), orderId: $nonEmpty, quoteInput: $quoteInput }),
@@ -2745,6 +2866,9 @@ var $stepResponse2 = $stepStatus.superRefine((response, context) => {
2745
2866
  if (response.fieldErrors?.some((error) => !fieldIds.includes(error.fieldId))) {
2746
2867
  context.addIssue({ code: "custom", path: ["fieldErrors"], message: "a field error must name a field in this response" });
2747
2868
  }
2869
+ if (response.fieldErrors?.some((error) => error.code && FIELD_OF_CODE[error.code] !== error.fieldId)) {
2870
+ context.addIssue({ code: "custom", path: ["fieldErrors"], message: "a field error code must match its field" });
2871
+ }
2748
2872
  }
2749
2873
  });
2750
2874
  var MoonPayStepResponseSchema = $stepResponse2;
package/dist/index.mjs CHANGED
@@ -12,16 +12,19 @@ import {
12
12
  tableEntry,
13
13
  unofferedEndpoints,
14
14
  unreachableStates
15
- } from "./chunk-PP6S7FSJ.mjs";
15
+ } from "./chunk-EI56NQ7K.mjs";
16
16
  import {
17
17
  MOONPAY_ENDPOINTS
18
- } from "./chunk-I63WALQK.mjs";
18
+ } from "./chunk-3HCZ6JBJ.mjs";
19
19
  import {
20
20
  FIAT_TERMS_ENDPOINT,
21
21
  FUN_KYC_CONSENT_DOCUMENTS,
22
22
  PROVIDER_CHECKOUT_DOCUMENTS,
23
23
  PUBLISHED_TERMS_DOCUMENTS
24
24
  } from "./chunk-5GRPYE2U.mjs";
25
+ import {
26
+ ROUTING_ENDPOINTS
27
+ } from "./chunk-KYE2HYV5.mjs";
25
28
 
26
29
  // src/codes.ts
27
30
  var US_STATE_CODES = [
@@ -859,6 +862,8 @@ var $providerBoundFlowState = z.union([
859
862
  var $flowState = z.union([$providerBoundFlowState, $blockedState]);
860
863
  var FlowStateSchema = $flowState;
861
864
  var $nonBlank = z.string().refine((value) => value.trim().length > 0, "must not be blank");
865
+ var $isoInstant = z.string().datetime({ offset: true });
866
+ var $errorBodyFields = { errorMsg: z.string(), reqId: z.string().optional() };
862
867
  var $termsStage = z.enum(["CHECKOUT", "KYC"]);
863
868
  var $termsDocument = obj({
864
869
  id: $nonBlank,
@@ -874,7 +879,7 @@ var $termsAcceptance = obj({
874
879
  documentId: $nonBlank,
875
880
  version: $nonBlank,
876
881
  contentDigest: $nonBlank,
877
- acceptedAt: z.string().datetime({ offset: true })
882
+ acceptedAt: $isoInstant
878
883
  });
879
884
  var TermsDocumentAcceptanceSchema = $termsAcceptance;
880
885
  var $termsRequest = obj({
@@ -886,12 +891,117 @@ var $termsResponse = obj({ recorded: z.array($nonBlank) });
886
891
  var FiatTermsResponseSchema = $termsResponse;
887
892
  var $termsErrorBody = obj({
888
893
  errorCode: $nonBlank,
889
- errorMsg: z.string(),
890
- reqId: z.string().optional(),
894
+ ...$errorBodyFields,
891
895
  reason: z.enum(["STALE_TERMS", "INVALID_REQUEST", "RATE_LIMITED", "PROVIDER_UNAVAILABLE"]),
892
896
  recovery: z.enum(["RETRY", "REQUOTE", "NONE"])
893
897
  });
894
898
  var FiatTermsErrorBodySchema = $termsErrorBody;
899
+ var $decimalAmount = z.string().regex(/^(?:0|[1-9]\d*)(?:\.\d+)?$/, "must be a decimal string");
900
+ var compareDecimals = (left, right) => {
901
+ const [leftWhole = "", leftFraction = ""] = left.split(".");
902
+ const [rightWhole = "", rightFraction = ""] = right.split(".");
903
+ if (leftWhole.length !== rightWhole.length) return leftWhole.length - rightWhole.length;
904
+ const width = Math.max(leftFraction.length, rightFraction.length);
905
+ const leftDigits = leftWhole + leftFraction.padEnd(width, "0");
906
+ const rightDigits = rightWhole + rightFraction.padEnd(width, "0");
907
+ return leftDigits < rightDigits ? -1 : leftDigits > rightDigits ? 1 : 0;
908
+ };
909
+ var $countryCode = z.enum(Object.keys(CountryCode));
910
+ var $routingProvider = $provider.extract(["TRANSAK", "BANXA", "MOONPAY", "STRIPE", "COINBASE"]);
911
+ var $optionalGeoField = z.union([$nonBlank, z.literal("")]).optional();
912
+ var US_REGION_REQUIRED = "a US buyer needs a US state code as region";
913
+ var hasUsRegion = (country, region) => country !== "US" || UsStateCodeSchema.safeParse(region).success;
914
+ var $routingTableQuery = obj({
915
+ fiatCurrencyCode: z.enum(FIAT_CURRENCY_CODES).optional(),
916
+ cryptoCurrencyCode: $nonBlank,
917
+ chainId: $nonBlank,
918
+ country: $countryCode,
919
+ region: $optionalGeoField,
920
+ methods: z.string().refine(
921
+ (methods) => methods.split(",").every((method) => PAYMENT_METHOD_CATEGORIES.includes(method.trim())),
922
+ "must be comma-separated payment method categories"
923
+ )
924
+ }).refine((query) => hasUsRegion(query.country, query.region), { message: US_REGION_REQUIRED, path: ["region"] });
925
+ var FiatRoutingTableQuerySchema = $routingTableQuery;
926
+ var $routingTableProvider = obj({
927
+ provider: $routingProvider,
928
+ next: z.enum(["READY", "AUTH", "DECLARATIVE_KYC", "DOCUMENTARY_KYC", "PENDING_REVIEW"]),
929
+ visibility: z.enum(["COMPLETE", "PARTIAL", "AUTH_GATED"])
930
+ });
931
+ var $routingTableInterval = obj({
932
+ min: $decimalAmount,
933
+ max: $decimalAmount,
934
+ providers: z.array($routingTableProvider).min(1).refine((providers) => hasUniqueIds(providers.map((entry) => entry.provider)), "each provider is listed once")
935
+ }).refine((interval) => compareDecimals(interval.min, interval.max) <= 0, { message: "min must not exceed max", path: ["max"] });
936
+ var $routingTableMethod = obj({
937
+ method: z.enum(PAYMENT_METHOD_CATEGORIES),
938
+ unavailableReason: z.enum(["USER_LIMIT", "KYC_REJECTED", "PROVIDER_OUTAGE", "NOT_SERVED"]).optional(),
939
+ intervals: z.array($routingTableInterval).superRefine((intervals, context) => {
940
+ intervals.forEach((interval, index) => {
941
+ const previous = intervals[index - 1];
942
+ if (previous !== void 0 && compareDecimals(interval.min, previous.max) <= 0) {
943
+ context.addIssue({ code: "custom", path: [index, "min"], message: "intervals are sorted and do not overlap" });
944
+ }
945
+ });
946
+ })
947
+ }).refine((method) => method.unavailableReason === void 0 || method.intervals.length === 0, {
948
+ message: "unavailableReason only with no interval",
949
+ path: ["unavailableReason"]
950
+ });
951
+ var $routingTableResponse = obj({
952
+ expiresAt: $isoInstant,
953
+ methods: z.array($routingTableMethod).refine((methods) => hasUniqueIds(methods.map((entry) => entry.method)), "each method is listed once")
954
+ });
955
+ var FiatRoutingTableResponseSchema = $routingTableResponse;
956
+ var $routingQuoteRequest = obj({
957
+ provider: $routingProvider,
958
+ fiatCurrency: z.enum(FIAT_CURRENCY_CODES),
959
+ fiatAmount: $decimalAmount.refine((amount) => Number(amount) > 0, "must be positive"),
960
+ cryptoCurrencyCode: $nonBlank,
961
+ chainId: $nonBlank,
962
+ paymentMethodId: z.enum(PAYMENT_METHOD_CATEGORIES),
963
+ geo: obj({ alpha2: $countryCode, region: $optionalGeoField, ip: $optionalGeoField, city: z.string().optional() }).refine(
964
+ (geo) => hasUsRegion(geo.alpha2, geo.region),
965
+ { message: US_REGION_REQUIRED, path: ["region"] }
966
+ ),
967
+ prepareSessionAuth: z.boolean().optional()
968
+ });
969
+ var FiatRoutingQuoteRequestSchema = $routingQuoteRequest;
970
+ var $routingServiceableRange = obj({ min: $money, max: $money, userMax: $money.nullable().optional() });
971
+ var $noRouteFields = {
972
+ ...$errorBodyFields,
973
+ errorCode: z.literal("FiatNoRouteError"),
974
+ serviceableRanges: z.array($routingServiceableRange),
975
+ retryable: z.boolean()
976
+ };
977
+ var $routingNoRouteErrorBody = z.union([
978
+ obj({ ...$noRouteFields, reason: z.enum(["NO_CANDIDATE", "PROVIDER_DECLINED", "PROVIDER_OUTAGE", "KYC_REJECTED"]) }),
979
+ obj({ ...$noRouteFields, reason: z.literal("USER_LIMIT"), code: z.literal("LIMITS_EXCEEDED"), recovery: z.literal("REQUOTE") })
980
+ ]);
981
+ var $amountErrorFields = { ...$errorBodyFields, fiatCurrencyCode: z.enum(FIAT_CURRENCY_CODES) };
982
+ var $routingAmountErrorBody = z.union([
983
+ obj({ ...$amountErrorFields, errorCode: z.literal("FiatAmountBelowMinimumError"), minFiatAmount: $decimalAmount }),
984
+ obj({ ...$amountErrorFields, errorCode: z.literal("FiatAmountAboveMaximumError"), maxFiatAmount: $decimalAmount })
985
+ ]);
986
+ var $routingRateLimitedErrorBody = obj({
987
+ ...$errorBodyFields,
988
+ errorCode: z.literal("TooManyRequestsError"),
989
+ retryAfterSeconds: z.number().int().positive().optional()
990
+ });
991
+ var ROUTING_ERROR_CODES = [
992
+ "FiatNoRouteError",
993
+ "FiatAmountBelowMinimumError",
994
+ "FiatAmountAboveMaximumError",
995
+ "TooManyRequestsError"
996
+ ];
997
+ var $routingFaultErrorBody = obj({
998
+ ...$errorBodyFields,
999
+ errorCode: $nonBlank.refine((code) => !ROUTING_ERROR_CODES.includes(code), "this errorCode carries its own fields")
1000
+ });
1001
+ var $routingTableErrorBody = z.union([$routingRateLimitedErrorBody, $routingFaultErrorBody]);
1002
+ var FiatRoutingTableErrorBodySchema = $routingTableErrorBody;
1003
+ var $routingQuoteErrorBody = z.union([$routingNoRouteErrorBody, $routingAmountErrorBody, $routingFaultErrorBody]);
1004
+ var FiatRoutingQuoteErrorBodySchema = $routingQuoteErrorBody;
895
1005
  var $providerBoundStepResponse = obj({
896
1006
  state: $providerBoundFlowState,
897
1007
  provider: $provider,
@@ -2058,11 +2168,12 @@ var $quoteInput = obj2({
2058
2168
  var MoonPayQuoteInputSchema = $quoteInput;
2059
2169
  var $stepRefusal = z2.enum(["NOT_ELIGIBLE", "DOCUMENTS_REQUIRED", "FINAL_REJECTION", "CUSTOMER_MISMATCH"]);
2060
2170
  var $orderRefusal = z2.enum(["CUSTOMER_MISMATCH", "WALLET_MISMATCH", "UNRECORDABLE"]);
2061
- var $fieldError = obj2({ fieldId: $nonEmpty, message: $nonEmpty });
2171
+ var FIELD_OF_CODE = { PHONE_IN_USE: "PHONE_NUMBER", TAX_ID_IN_USE: "TAX_IDENTIFIER" };
2172
+ var $fieldError = obj2({ fieldId: $nonEmpty, message: $nonEmpty, code: z2.enum(["PHONE_IN_USE", "TAX_ID_IN_USE"]).optional() });
2062
2173
  var $pending = obj2({ status: z2.literal("PENDING"), retryAfterMs: z2.number().int().positive().max(6e4) });
2063
2174
  var $stepStatus = z2.discriminatedUnion("status", [
2064
2175
  obj2({ status: z2.literal("TERMS"), stage: z2.enum(["CHECKOUT", "KYC"]), documents: z2.array(TermsDocumentSchema).min(1) }),
2065
- obj2({ status: z2.literal("SDK_AUTH"), sessionToken: $nonEmpty, quoteInput: $quoteInput }),
2176
+ obj2({ status: z2.literal("SDK_AUTH"), sessionToken: $nonEmpty, quoteInput: $quoteInput, resetConnection: z2.literal(true).optional() }),
2066
2177
  obj2({ status: z2.literal("KYC"), fields: z2.array(FormFieldSchema).min(1), fieldErrors: z2.array($fieldError).min(1).optional() }),
2067
2178
  $pending,
2068
2179
  obj2({ status: z2.literal("READY"), orderId: $nonEmpty, quoteInput: $quoteInput }),
@@ -2085,6 +2196,9 @@ var $stepResponse2 = $stepStatus.superRefine((response, context) => {
2085
2196
  if (response.fieldErrors?.some((error) => !fieldIds.includes(error.fieldId))) {
2086
2197
  context.addIssue({ code: "custom", path: ["fieldErrors"], message: "a field error must name a field in this response" });
2087
2198
  }
2199
+ if (response.fieldErrors?.some((error) => error.code && FIELD_OF_CODE[error.code] !== error.fieldId)) {
2200
+ context.addIssue({ code: "custom", path: ["fieldErrors"], message: "a field error code must match its field" });
2201
+ }
2088
2202
  }
2089
2203
  });
2090
2204
  var MoonPayStepResponseSchema = $stepResponse2;
@@ -2128,6 +2242,11 @@ export {
2128
2242
  FiatCurrencyCode,
2129
2243
  FiatEndpointSchema,
2130
2244
  FiatProviderSchema,
2245
+ FiatRoutingQuoteErrorBodySchema,
2246
+ FiatRoutingQuoteRequestSchema,
2247
+ FiatRoutingTableErrorBodySchema,
2248
+ FiatRoutingTableQuerySchema,
2249
+ FiatRoutingTableResponseSchema,
2131
2250
  FiatStepResponseSchema,
2132
2251
  FiatTermsErrorBodySchema,
2133
2252
  FiatTermsRequestSchema,
@@ -2179,6 +2298,7 @@ export {
2179
2298
  QuoteLimitsSchema,
2180
2299
  QuoteSchema,
2181
2300
  QuoteStateSchema,
2301
+ ROUTING_ENDPOINTS,
2182
2302
  RecoverySchema,
2183
2303
  RefundSchema,
2184
2304
  ReportSpecSchema,
@@ -103,10 +103,16 @@ export type MoonPayStepRefusal = 'NOT_ELIGIBLE' | 'DOCUMENTS_REQUIRED' | 'FINAL_
103
103
  * assertion's. `UNRECORDABLE`: a charge no retry can record, held for refund review.
104
104
  */
105
105
  export type MoonPayOrderRefusal = 'CUSTOMER_MISMATCH' | 'WALLET_MISMATCH' | 'UNRECORDABLE';
106
- /** A reason on one field of a `KYC` response. `fieldId` names a field in that response. */
106
+ /** Another MoonPay customer already holds the value; only a different one goes through. */
107
+ export type MoonPayFieldErrorCode = 'PHONE_IN_USE' | 'TAX_ID_IN_USE';
108
+ /**
109
+ * A reason on one field of a `KYC` response. `fieldId` names a field in that response. `code` is set
110
+ * only for a refusal the client words itself: `PHONE_IN_USE` on `PHONE_NUMBER`, `TAX_ID_IN_USE` on `TAX_IDENTIFIER`.
111
+ */
107
112
  export interface MoonPayFieldError {
108
113
  fieldId: string;
109
114
  message: string;
115
+ code?: MoonPayFieldErrorCode;
110
116
  }
111
117
  /** The answer to `observe` and `kyc`. */
112
118
  export type MoonPayStepResponse =
@@ -116,11 +122,15 @@ export type MoonPayStepResponse =
116
122
  stage: TermsStage;
117
123
  documents: TermsDocument[];
118
124
  }
119
- /** Run the MoonPay connection check with `sessionToken`, then `observe` with the credentials. */
125
+ /**
126
+ * Run the MoonPay connection check with `sessionToken`, then `observe` with the credentials.
127
+ * `resetConnection`: the device's connection is another buyer's, so reset it before the check.
128
+ */
120
129
  | {
121
130
  status: 'SDK_AUTH';
122
131
  sessionToken: string;
123
132
  quoteInput: MoonPayQuoteInput;
133
+ resetConnection?: true;
124
134
  }
125
135
  /** Show the shared form, then send `kyc`. */
126
136
  | {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  MOONPAY_ENDPOINTS
3
- } from "../chunk-I63WALQK.mjs";
3
+ } from "../chunk-3HCZ6JBJ.mjs";
4
4
  export {
5
5
  MOONPAY_ENDPOINTS
6
6
  };
@@ -0,0 +1,168 @@
1
+ /**
2
+ * The routing table: `GET /fiat/routing/table` and `POST /fiat/routing/quote`.
3
+ *
4
+ * The client loads the table when the sheet opens. For each payment method it lists every amount
5
+ * interval and the providers that serve it, best first, each with what it needs from the buyer next.
6
+ * The client then shows min, max, provider and the Continue label for any amount with no call, and
7
+ * quotes only the interval's first provider, and the next one after a fall-through refusal
8
+ * (`FiatRoutingQuoteErrorBody`).
9
+ *
10
+ * The table is advisory: every routing quote checks the named provider's eligibility again. Refetch it
11
+ * at `expiresAt`, on `refreshLimits: true`, and after auth, KYC or a `REQUOTE` refusal.
12
+ *
13
+ * Changes to both routes only add fields and enum values. Map an unknown `next` to the generic
14
+ * Continue label.
15
+ *
16
+ * Zero runtime dependencies, so production React Native code can import it. The zod schemas are
17
+ * exported from the package root. Like every schema in the package they are strict: a request with
18
+ * an extra key or a blank string fails them, so send only the declared fields.
19
+ */
20
+ import type { CountryCode, FiatCurrencyCode, PaymentMethodCategory } from './codes';
21
+ import type { FiatAmount, FiatProvider, QuoteLimitRange } from './types';
22
+ export declare const ROUTING_ENDPOINTS: {
23
+ readonly table: "GET /fiat/routing/table";
24
+ /** Temporary: each provider's `intent` route replaces it. */
25
+ readonly quote: "POST /fiat/routing/quote";
26
+ };
27
+ /**
28
+ * The query string of `GET /fiat/routing/table`. `country` and `region` are the geo endpoint's
29
+ * `alpha2` and `region`, held to the `POST /fiat/quote` geo rules: a US buyer's `region` must be a
30
+ * `UsStateCode`, and an empty `region` counts as absent.
31
+ */
32
+ export interface FiatRoutingTableQuery {
33
+ /** Defaults to `USD`. */
34
+ fiatCurrencyCode?: FiatCurrencyCode;
35
+ cryptoCurrencyCode: string;
36
+ chainId: string;
37
+ country: CountryCode;
38
+ region?: string;
39
+ /** Comma-separated `PaymentMethodCategory` values, for example `apple_pay,card`. */
40
+ methods: string;
41
+ }
42
+ /** What a provider needs from this buyer before it can take the payment. */
43
+ export type RoutingNextStep = 'READY' | 'AUTH' | 'DECLARATIVE_KYC' | 'DOCUMENTARY_KYC' | 'PENDING_REVIEW';
44
+ /** How much past `next` the backend can see: the whole picture, part of it, or nothing before auth. */
45
+ export type RoutingVisibility = 'COMPLETE' | 'PARTIAL' | 'AUTH_GATED';
46
+ /**
47
+ * Why a method has no interval. Present only when `intervals` is empty, and absent there too when the
48
+ * cause is unknown. `NOT_SERVED`: every provider was ruled out for reasons that depend on neither the
49
+ * buyer nor provider health: it does not offer the corridor, currency, method or asset, or config has
50
+ * not enabled it.
51
+ */
52
+ export type RoutingUnavailableReason = 'USER_LIMIT' | 'KYC_REJECTED' | 'PROVIDER_OUTAGE' | 'NOT_SERVED';
53
+ /** The providers the backend can route and take an order through. */
54
+ export type RoutingProvider = Extract<FiatProvider, 'TRANSAK' | 'BANXA' | 'MOONPAY' | 'STRIPE' | 'COINBASE'>;
55
+ export interface RoutingTableProvider {
56
+ provider: RoutingProvider;
57
+ next: RoutingNextStep;
58
+ visibility: RoutingVisibility;
59
+ }
60
+ /**
61
+ * A `QuoteLimitRange` with two decimals (`"20.00"`) and the providers that serve it. The next
62
+ * interval starts one cent above `max`. Adjacent intervals always rank differently.
63
+ */
64
+ export interface RoutingTableInterval extends QuoteLimitRange {
65
+ /** Best first; never empty. Quote the first, and the next one after a decline. */
66
+ providers: RoutingTableProvider[];
67
+ }
68
+ /**
69
+ * One requested payment method. An amount in no interval has no provider: show the nearest edges
70
+ * ("Minimum $X", "Maximum $Y", or both around a gap), never a blocked state while `intervals` is not
71
+ * empty. With no interval at all, show `unavailableReason` instead of the amount picker.
72
+ */
73
+ export interface RoutingTableMethod {
74
+ method: PaymentMethodCategory;
75
+ unavailableReason?: RoutingUnavailableReason;
76
+ /** Sorted and non-overlapping. */
77
+ intervals: RoutingTableInterval[];
78
+ }
79
+ export interface FiatRoutingTableResponse {
80
+ /** ISO-8601. The earliest of 60 s after the build and the expiry of any limit, coverage or provider session behind it. */
81
+ expiresAt: string;
82
+ /** In request order, duplicates removed. */
83
+ methods: RoutingTableMethod[];
84
+ }
85
+ /**
86
+ * The `POST /fiat/quote` body plus the one `provider` to price, which is the interval's first
87
+ * provider, and never `routingOverrides`. The response is the same `FiatStepResponse` as
88
+ * `POST /fiat/quote`; a refusal is a `FiatRoutingQuoteErrorBody`.
89
+ */
90
+ export interface FiatRoutingQuoteRequest {
91
+ provider: RoutingProvider;
92
+ fiatCurrency: FiatCurrencyCode;
93
+ /** A positive decimal string with no leading zero, exponent or sign (`"20"`, `"20.5"`). */
94
+ fiatAmount: string;
95
+ cryptoCurrencyCode: string;
96
+ chainId: string;
97
+ paymentMethodId: PaymentMethodCategory;
98
+ /**
99
+ * The Fun geo endpoint's response, forwarded as is. An empty `region` or `ip` counts as absent, a
100
+ * US buyer's `region` must be a `UsStateCode`, and `city` is ignored.
101
+ */
102
+ geo: {
103
+ alpha2: CountryCode;
104
+ region?: string;
105
+ ip?: string;
106
+ city?: string;
107
+ };
108
+ prepareSessionAuth?: boolean;
109
+ }
110
+ /** One serviceable range of the named provider. `userMax` is this buyer's own ceiling inside it, `null` when unmeasured. */
111
+ export interface RoutingServiceableRange {
112
+ min: FiatAmount;
113
+ max: FiatAmount;
114
+ userMax?: FiatAmount | null;
115
+ }
116
+ /**
117
+ * Why the named provider cannot price the request. `NO_CANDIDATE`: it does not serve this corridor,
118
+ * method or amount. `PROVIDER_DECLINED`: it priced and then declined this buyer. `PROVIDER_OUTAGE`:
119
+ * it is down, so `retryable` is true. `KYC_REJECTED`: it refused this buyer's identity for good.
120
+ * `USER_LIMIT`: this buyer's limits at it do not allow the amount.
121
+ */
122
+ export type RoutingNoRouteReason = 'NO_CANDIDATE' | 'PROVIDER_DECLINED' | 'PROVIDER_OUTAGE' | 'KYC_REJECTED' | 'USER_LIMIT';
123
+ /** The shared fields of every `/fiat/*` error body. */
124
+ interface ErrorBodyFields {
125
+ errorMsg: string;
126
+ reqId?: string;
127
+ }
128
+ /** 404, or 400 for `USER_LIMIT`, which alone carries `code` and `recovery`. The same body `POST /fiat/quote` sends. */
129
+ export type RoutingNoRouteErrorBody = ErrorBodyFields & {
130
+ errorCode: 'FiatNoRouteError';
131
+ serviceableRanges: RoutingServiceableRange[];
132
+ retryable: boolean;
133
+ } & ({
134
+ reason: Exclude<RoutingNoRouteReason, 'USER_LIMIT'>;
135
+ } | {
136
+ reason: 'USER_LIMIT';
137
+ code: 'LIMITS_EXCEEDED';
138
+ recovery: 'REQUOTE';
139
+ });
140
+ /** 400: the amount is outside the named provider's bounds. */
141
+ export type RoutingAmountErrorBody = ErrorBodyFields & {
142
+ fiatCurrencyCode: FiatCurrencyCode;
143
+ } & ({
144
+ errorCode: 'FiatAmountBelowMinimumError';
145
+ minFiatAmount: string;
146
+ } | {
147
+ errorCode: 'FiatAmountAboveMaximumError';
148
+ maxFiatAmount: string;
149
+ });
150
+ /** 429: the per-login budget is spent. Retry after `retryAfterSeconds`. */
151
+ export type RoutingRateLimitedErrorBody = ErrorBodyFields & {
152
+ errorCode: 'TooManyRequestsError';
153
+ retryAfterSeconds?: number;
154
+ };
155
+ /** Any other fault: a bad request (400), a missing assertion (401) or a server error (5xx). `errorCode` is the error's name. */
156
+ export type RoutingFaultErrorBody = ErrorBodyFields & {
157
+ errorCode: string;
158
+ };
159
+ /** `GET /fiat/routing/table` refusals. */
160
+ export type FiatRoutingTableErrorBody = RoutingRateLimitedErrorBody | RoutingFaultErrorBody;
161
+ /**
162
+ * `POST /fiat/routing/quote` refusals. A `FiatNoRouteError` (any `reason`) or either amount error is
163
+ * the named provider's alone: quote the interval's next provider. Anything else stops the fall-through:
164
+ * a validation or auth refusal, a 5xx, or no provider left to try.
165
+ */
166
+ export type FiatRoutingQuoteErrorBody = RoutingNoRouteErrorBody | RoutingAmountErrorBody | RoutingFaultErrorBody;
167
+ export {};
168
+ //# sourceMappingURL=routing.d.ts.map
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/routing.ts
21
+ var routing_exports = {};
22
+ __export(routing_exports, {
23
+ ROUTING_ENDPOINTS: () => ROUTING_ENDPOINTS
24
+ });
25
+ module.exports = __toCommonJS(routing_exports);
26
+ var ROUTING_ENDPOINTS = {
27
+ table: "GET /fiat/routing/table",
28
+ /** Temporary: each provider's `intent` route replaces it. */
29
+ quote: "POST /fiat/routing/quote"
30
+ };
31
+ //# sourceMappingURL=routing.js.map
@@ -0,0 +1,7 @@
1
+ import {
2
+ ROUTING_ENDPOINTS
3
+ } from "./chunk-KYE2HYV5.mjs";
4
+ export {
5
+ ROUTING_ENDPOINTS
6
+ };
7
+ //# sourceMappingURL=routing.mjs.map
package/dist/schemas.d.ts CHANGED
@@ -30,6 +30,7 @@ import { z } from 'zod';
30
30
  import type { UsStateCode } from './codes';
31
31
  import type { BlockedReason, CryptoAmount, FailureReason, FeeLine, FiatAmount, FiatEndpoint, FiatProvider, FiatStepResponse, FieldSpec, FlowState, FormDescriptor, FormField, FormFieldType, InstructionField, Instructions, Instrument, JsonValue, KYCProvider, OrderRef, OrderStatus, OrderSummary, PollSpec, PreparedPayment, PaymentSurface, Quote, QuoteLimitRange, QuoteLimits, Recovery, Refund, ReportSpec, SelectOption, StatusHistoryEntry, Surface, PreferredIdentityDocument, KycFieldId, ClientExecutedDelivery, KycSdkResultReport, SurfaceKind, SurfaceProvider, TermsAcceptance, Transition, TransitionInputs, TransitionParams, Tx } from './types';
32
32
  import type { FiatTermsErrorBody, FiatTermsRequest, FiatTermsResponse, TermsDocument, TermsDocumentAcceptance } from './terms';
33
+ import type { FiatRoutingQuoteErrorBody, FiatRoutingQuoteRequest, FiatRoutingTableErrorBody, FiatRoutingTableQuery, FiatRoutingTableResponse } from './routing';
33
34
  export declare const JsonSchema: z.ZodType<JsonValue>;
34
35
  export declare const HTTP_VERBS: readonly ["GET", "POST"];
35
36
  /** `"POST /fiat/session/verify"`, `"GET /fiat/orders/o_31c"` — the verb rides the string. */
@@ -111,6 +112,11 @@ export declare const TermsDocumentAcceptanceSchema: z.ZodType<TermsDocumentAccep
111
112
  export declare const FiatTermsRequestSchema: z.ZodType<FiatTermsRequest>;
112
113
  export declare const FiatTermsResponseSchema: z.ZodType<FiatTermsResponse>;
113
114
  export declare const FiatTermsErrorBodySchema: z.ZodType<FiatTermsErrorBody>;
115
+ export declare const FiatRoutingTableQuerySchema: z.ZodType<FiatRoutingTableQuery>;
116
+ export declare const FiatRoutingTableResponseSchema: z.ZodType<FiatRoutingTableResponse>;
117
+ export declare const FiatRoutingQuoteRequestSchema: z.ZodType<FiatRoutingQuoteRequest>;
118
+ export declare const FiatRoutingTableErrorBodySchema: z.ZodType<FiatRoutingTableErrorBody>;
119
+ export declare const FiatRoutingQuoteErrorBodySchema: z.ZodType<FiatRoutingQuoteErrorBody>;
114
120
  export declare const FiatStepResponseSchema: z.ZodType<FiatStepResponse>;
115
121
  export {};
116
122
  //# sourceMappingURL=schemas.d.ts.map
package/dist/table.js CHANGED
@@ -36,7 +36,7 @@ __export(table_exports, {
36
36
  module.exports = __toCommonJS(table_exports);
37
37
 
38
38
  // package.json
39
- var version = "0.22.1";
39
+ var version = "0.23.0";
40
40
 
41
41
  // src/table.ts
42
42
  var TABLE_VERSION = version;
package/dist/table.mjs CHANGED
@@ -11,7 +11,7 @@ import {
11
11
  tableEntry,
12
12
  unofferedEndpoints,
13
13
  unreachableStates
14
- } from "./chunk-PP6S7FSJ.mjs";
14
+ } from "./chunk-EI56NQ7K.mjs";
15
15
  export {
16
16
  DOCUMENTED_ENDPOINTS,
17
17
  STATE_KEYS,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fun-xyz/fiat-contract",
3
- "version": "0.22.1",
3
+ "version": "0.23.0",
4
4
  "description": "Published conformance contract for Fun's headless fiat onramp: FlowState/Transition types, zod schemas, the transition table as data, recorded fixtures, assertion helpers.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,6 +38,11 @@
38
38
  "import": "./dist/terms.mjs",
39
39
  "require": "./dist/terms.js"
40
40
  },
41
+ "./routing": {
42
+ "types": "./dist/routing.d.ts",
43
+ "import": "./dist/routing.mjs",
44
+ "require": "./dist/routing.js"
45
+ },
41
46
  "./package.json": "./package.json"
42
47
  },
43
48
  "files": [
@@ -50,6 +55,8 @@
50
55
  "moonpay.d.ts",
51
56
  "terms.js",
52
57
  "terms.d.ts",
58
+ "routing.js",
59
+ "routing.d.ts",
53
60
  "README.md",
54
61
  "!dist/**/*.map"
55
62
  ],
@@ -95,6 +102,9 @@
95
102
  ],
96
103
  "terms": [
97
104
  "./dist/terms.d.ts"
105
+ ],
106
+ "routing": [
107
+ "./dist/routing.d.ts"
98
108
  ]
99
109
  }
100
110
  }
package/routing.d.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './dist/routing';
package/routing.js ADDED
@@ -0,0 +1,2 @@
1
+ // Compat stub — see table.js. Metro before RN 0.79 resolves `@fun-xyz/fiat-contract/routing` by path.
2
+ module.exports = require('./dist/routing.js');