@fun-xyz/fiat-contract 0.22.2 → 0.24.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,15 +13,16 @@ 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
  | --- | --- | --- | --- |
20
20
  | `@fun-xyz/fiat-contract/types` | **0.1 KB** (types erase) | every type; no runtime values | production, both repos |
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
- | `@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 |
22
+ | `@fun-xyz/fiat-contract/moonpay` | zero deps | the MoonPay flow on `POST /fiat/next`: action requests, prompts, responses, 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,
@@ -101,7 +102,7 @@ A provider can run its own flow instead of the shared step machine. `POST /fiat/
101
102
  and prices. When the request's `handoffs` lists the winning provider, the response carries
102
103
  `handoff: { provider }` and no transitions. The client shows that provider's `CHECKOUT` documents
103
104
  from `PROVIDER_CHECKOUT_DOCUMENTS`, records them with `POST /fiat/terms` on Continue, and then calls
104
- `POST /fiat/providers/{provider}/intent`. The schema refuses a handoff on any state but `QUOTE`, a
105
+ `POST /fiat/next`. The schema refuses a handoff on any state but `QUOTE`, a
105
106
  handoff to a provider other than the response's own, and a handoff that still offers transitions.
106
107
 
107
108
  **Terms (`./terms`).** Every provider records acceptances through the platform `POST /fiat/terms`
@@ -120,48 +121,86 @@ 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
 
123
- **MoonPay (`./moonpay`).** One route, `MOONPAY_ENDPOINTS.intent`. A `MoonPayIntentRequest` is one of
124
- three kinds:
125
-
126
- | `kind` | Carries | A repeat |
127
- | --- | --- | --- |
128
- | `observe` | `checkout`, and `credentials` (`customerId`, `accessToken`) once the SDK has them | writes nothing new |
129
- | `kyc` | `checkout`, `credentials`, `values` keyed by `KycFieldId` (no form id) | drops values MoonPay no longer asks for |
130
- | `order` | `orderId`, `transactionId?`, `quotedAmount`, `geo?` (the checkout's) | returns the recorded order |
131
-
132
- `observe` and `kyc` answer a `MoonPayStepResponse`, which says what the backend needs next:
133
-
134
- | `status` | Carries | The client then |
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 |
135
142
  | --- | --- | --- |
136
- | `TERMS` | `stage`, `documents` (at least one, all at `stage`) | sends `POST /fiat/terms`, then `observe` |
137
- | `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 |
138
- | `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
- | `PENDING` | `retryAfterMs` (at most 60 s) | sends `observe` after the delay, never `kyc` again |
140
- | `READY` | `orderId`, `quoteInput` | mounts Apple Pay, then sends `order` after the charge |
141
- | `REFUSED` | `reason` (`NOT_ELIGIBLE`, `DOCUMENTS_REQUIRED`, `FINAL_REJECTION`, `CUSTOMER_MISMATCH`), `recovery: REQUOTE \| NONE` | exits the provider flow |
142
-
143
- `order` runs after every charge attempt, a failed one too, and answers a `MoonPayOrderResponse`.
144
- `transactionId` is optional because a failed charge may have none; without it the backend records the
145
- failed charges MoonPay lists for `orderId`. `quotedAmount` is the fiat amount the quote showed.
146
- `geo` is the same object `observe` sends in `checkout`; the order records its `alpha2` as its
147
- country, and no country without it. A
148
- failed charge answers `DONE` with the order in `FAILED`, and the client requotes for a new `orderId`.
149
- After a failed charge with no `transactionId`, the client caps its `PENDING` polls.
150
-
151
- | `status` | Carries | The client then |
152
- | --- | --- | --- |
153
- | `DONE` | `order`, the `GET /fiat/orders/:orderId` response | shows the shared order status |
154
- | `PENDING` | `retryAfterMs` (at most 60 s) | sends the same `order` after the delay |
155
- | `REFUSED` | `reason` (`CUSTOMER_MISMATCH`, `WALLET_MISMATCH`, `UNRECORDABLE`), `recovery: NONE` | exits the provider flow |
156
-
157
- `MoonPayIntentResponseFor` maps each `kind` to its union, and `MoonPayIntentResponse` is both.
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: `POST /fiat/next`
160
+ replaces it for each provider that moves there.
161
+
162
+ **MoonPay (`./moonpay`).** MoonPay runs on the shared `POST /fiat/next` (`MOONPAY_ENDPOINTS.next`).
163
+ Every `/next` body is a `FiatNextRequest`: `provider`, `geo` (a `FiatGeo`, the geo endpoint's
164
+ response), and the provider's own request, which names its `action`. The orchestrator reads
165
+ `provider` and `geo`; the provider's action schema reads the rest. MoonPay's body is a
166
+ `MoonPayNextRequest`, and every MoonPay request also carries the context `checkout` and, after
167
+ `connect`, `credentials` (`customerId`, `accessToken`).
168
+
169
+ Each `MoonPayAction` has one name. A response's prompt names it in `next` and carries what the
170
+ client needs; the client's request names it in `action` and carries the result:
171
+
172
+ | Action | Prompt carries | Request carries | A repeat |
173
+ | --- | --- | --- | --- |
174
+ | `observe` | no prompt: the answer is the current prompt | nothing | writes nothing |
175
+ | `consent` | `documents` (at least one, unique ids) | `acceptances`, the `POST /fiat/terms` items | writes nothing new |
176
+ | `connect` | `sessionToken`, `quoteInput`, `resetConnection?` (`true` when the device's MoonPay connection is another buyer's) | `customerId`, `accessToken` | rebinds the same customer |
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) | `credentials`, `values` keyed by `KycFieldId` (no form id) | drops values MoonPay no longer asks for |
178
+ | `wait` | `retryAfterMs` (at most 60 s) | no request: the client sends `observe` after the delay | |
179
+ | `order` | `orderId`, `quoteInput` | `orderId`, `quotedAmount`, `transactionId?` | returns the recorded order |
180
+
181
+ Every response is the next prompt, `done`, or `refused` (`MoonPayRefused`), and
182
+ `MoonPayResponseFor` maps each request action to its union. `observe`, `consent`, `connect` and `kyc`
183
+ answer a `MoonPayStepResponse`: any `MoonPayPrompt`, or `refused` with `reason` (`NOT_ELIGIBLE`,
184
+ `DOCUMENTS_REQUIRED`, `FINAL_REJECTION`, `CUSTOMER_MISMATCH`) and `recovery: REQUOTE | NONE`.
185
+
186
+ `order` runs after every charge attempt, a failed one too, and answers a `MoonPayOrderResponse`:
187
+ `done` with `order`, the `GET /fiat/orders/:orderId` response; `order` again while MoonPay does not
188
+ list the transaction yet; or `refused` with `reason` (`CUSTOMER_MISMATCH`, `WALLET_MISMATCH`,
189
+ `UNRECORDABLE`) and `recovery: NONE`. `transactionId` is optional because a failed charge may have
190
+ none; without it the backend records the failed charges MoonPay lists for `orderId`. `quotedAmount`
191
+ is the fiat amount the device quote showed. The order records the body's `geo.alpha2` as its country.
192
+ A failed charge answers `done` with the order in `FAILED`, and the client requotes for a new `orderId`.
193
+
194
+ A first-time buyer goes `observe` → `consent` → `connect` → `kyc` → `wait` → `observe` → `order` →
195
+ `done`; a returning buyer `observe` → `connect` → `order` → `done`. Both walks are recorded in
196
+ `test/fixtures/moonpay-next/`.
158
197
 
159
198
  A fault is a 4xx or 5xx with a `MoonPayErrorBody`: the shared `errorCode`, `errorMsg` and `reqId`,
160
199
  plus `reason` (`SESSION_BUDGET_SPENT`, `RATE_LIMITED`, `PROVIDER_UNAVAILABLE`, `INVALID_REQUEST`)
161
200
  and `recovery`:
162
- `RETRY` (send the same request again), `REQUOTE`, or `NONE`. A field error is a `KYC`
163
- response, not a fault. The zod schemas (`MoonPay*Schema`, `TermsDocumentSchema`, `FiatTerms*Schema`)
164
- are exported from the root only.
201
+ `RETRY` (send the same request again), `REQUOTE`, or `NONE`. A field error is a `kyc`
202
+ prompt, not a fault. The zod schemas (`MoonPay*Schema`, `FiatGeoSchema`, `FiatNextRequestSchema`,
203
+ `TermsDocumentSchema`, `FiatTerms*Schema`) are exported from the root only.
165
204
 
166
205
  ## KYC SDK hints, results, and client delivery
167
206
 
@@ -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
@@ -0,0 +1,11 @@
1
+ // src/routing.ts
2
+ var ROUTING_ENDPOINTS = {
3
+ table: "GET /fiat/routing/table",
4
+ /** Temporary: `POST /fiat/next` replaces it for each provider that moves there. */
5
+ quote: "POST /fiat/routing/quote"
6
+ };
7
+
8
+ export {
9
+ ROUTING_ENDPOINTS
10
+ };
11
+ //# sourceMappingURL=chunk-5UEPWL7B.mjs.map
@@ -41,4 +41,4 @@ export {
41
41
  FUN_KYC_CONSENT_DOCUMENTS,
42
42
  PUBLISHED_TERMS_DOCUMENTS
43
43
  };
44
- //# sourceMappingURL=chunk-5GRPYE2U.mjs.map
44
+ //# sourceMappingURL=chunk-CRNKRIKR.mjs.map
@@ -1,5 +1,5 @@
1
1
  // package.json
2
- var version = "0.22.2";
2
+ var version = "0.24.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-DTHUNDA7.mjs.map
517
+ //# sourceMappingURL=chunk-IPOPANXS.mjs.map
@@ -0,0 +1,9 @@
1
+ // src/providers/moonpay.ts
2
+ var MOONPAY_ENDPOINTS = {
3
+ next: "POST /fiat/next"
4
+ };
5
+
6
+ export {
7
+ MOONPAY_ENDPOINTS
8
+ };
9
+ //# sourceMappingURL=chunk-T5JAYOWY.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';