@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 +46 -7
- package/dist/{chunk-I63WALQK.mjs → chunk-3HCZ6JBJ.mjs} +1 -1
- package/dist/{chunk-PP6S7FSJ.mjs → chunk-EI56NQ7K.mjs} +2 -2
- package/dist/chunk-KYE2HYV5.mjs +11 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +130 -6
- package/dist/index.mjs +127 -7
- package/dist/providers/moonpay.d.ts +12 -2
- package/dist/providers/moonpay.mjs +1 -1
- package/dist/routing.d.ts +168 -0
- package/dist/routing.js +31 -0
- package/dist/routing.mjs +7 -0
- package/dist/schemas.d.ts +6 -0
- package/dist/table.js +1 -1
- package/dist/table.mjs +1 -1
- package/package.json +11 -1
- package/routing.d.ts +1 -0
- package/routing.js +2 -0
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
|
-
##
|
|
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` |
|
|
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
|
|
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 |
|
|
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
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// package.json
|
|
2
|
-
var version = "0.
|
|
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-
|
|
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:
|
|
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
|
-
|
|
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.
|
|
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
|
|
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-
|
|
15
|
+
} from "./chunk-EI56NQ7K.mjs";
|
|
16
16
|
import {
|
|
17
17
|
MOONPAY_ENDPOINTS
|
|
18
|
-
} from "./chunk-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
| {
|
|
@@ -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
|
package/dist/routing.js
ADDED
|
@@ -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
|
package/dist/routing.mjs
ADDED
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
package/dist/table.mjs
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fun-xyz/fiat-contract",
|
|
3
|
-
"version": "0.
|
|
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