@dvmkit/sdk 0.0.0 → 0.1.0-rc.2
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/NOTICE +2 -0
- package/README.md +38 -2
- package/dist/chunk-27V2ILSR.js +291 -0
- package/dist/chunk-365P52XQ.js +4121 -0
- package/dist/chunk-5GFED3GJ.js +955 -0
- package/dist/chunk-6JZIX5WW.js +1155 -0
- package/dist/chunk-7IH5SG2A.js +1038 -0
- package/dist/chunk-AT6V3SY7.js +102 -0
- package/dist/chunk-DCNT4PJS.js +733 -0
- package/dist/chunk-DMNLFNTW.js +135 -0
- package/dist/chunk-FROTD5XQ.js +70 -0
- package/dist/chunk-H25M54MI.js +149 -0
- package/dist/chunk-KQAJVVZT.js +712 -0
- package/dist/chunk-KXWROQGK.js +74 -0
- package/dist/chunk-L4OYF4DQ.js +67 -0
- package/dist/chunk-OJ5WFIB2.js +1266 -0
- package/dist/chunk-S3XAHZQY.js +63 -0
- package/dist/chunk-YG7G4DPZ.js +25 -0
- package/dist/credit-ledger-RO4FGSHG.js +28 -0
- package/dist/index.d.ts +144 -0
- package/dist/index.js +303 -0
- package/dist/job-store-6gR4pZRP.d.ts +5350 -0
- package/dist/memory-credit-ledger-I2G64DDK.js +9 -0
- package/dist/mpp-secret-state-WNAQQ6K4.js +127 -0
- package/dist/mpp-setup-MOBWGTWJ.js +30 -0
- package/dist/payout-reporter-4TNWRS5F.js +753 -0
- package/dist/postgres-consumed-credential-store-VHBT4KEA.js +72 -0
- package/dist/postgres-job-store-J5F4GUWU.js +7 -0
- package/dist/postgres-kv-store-JFBDP5IP.js +7 -0
- package/dist/postgres-replay-store-UJXRT6VO.js +7 -0
- package/dist/pricing-4CEB34RM.js +48 -0
- package/dist/processed-payment-store-HAA4SFNK.js +11 -0
- package/dist/revenue-reporter-GB4WKLDC.js +510 -0
- package/dist/server/index.d.ts +4168 -0
- package/dist/server/index.js +22716 -0
- package/dist/ssrf-DZi-xJyn.d.ts +325 -0
- package/dist/tempo-charge-store-6GJEMNUU.js +130 -0
- package/dist/tempo-session-store-FTEEGZXA.js +467 -0
- package/dist/testing/index.d.ts +135 -0
- package/dist/testing/index.js +151 -0
- package/dist/x402-35VLYFKZ.js +1272 -0
- package/package.json +89 -6
package/NOTICE
ADDED
package/README.md
CHANGED
|
@@ -1,5 +1,41 @@
|
|
|
1
1
|
# @dvmkit/sdk
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Build Digital Vending Machines: accountless HTTPS services with typed inputs, jobs, and payment rails.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## Development
|
|
6
|
+
|
|
7
|
+
Use the Node.js release in `.nvmrc` (Node 22). Install with `npm ci`, then run the same gate CI runs with `npm run verify`: the toolchain check, lint, build, typecheck (including the release scripts under `checkJs`), unit and database tests, the public API surface and package-shape checks, a consumer install, a public-safety scan, the release contract, and a clean-install smoke.
|
|
8
|
+
|
|
9
|
+
`DATABASE_URL` is optional locally. Without it the database suite is skipped rather than passed, and `verify` says so in its summary, because a skipped suite verified nothing and must not be reported as green. Point `DATABASE_URL` at a scratch Postgres to run it, and pass `npm run verify -- --require-database` to make a missing one a failure, which is what CI does.
|
|
10
|
+
|
|
11
|
+
The public package exports exactly `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. Documentation is at [dvmkit.ai](https://dvmkit.ai/docs/sdk).
|
|
12
|
+
|
|
13
|
+
## Compatibility and versioning
|
|
14
|
+
|
|
15
|
+
This is pre-1.0 alpha software and the promises match that. Breaking changes can land in any release, they are announced in the release notes for the release that carries them, and there is no long-term support release. Security fixes are made on the latest published release rather than backported.
|
|
16
|
+
|
|
17
|
+
The public surface is exactly three entry points: `@dvmkit/sdk`, `@dvmkit/sdk/server`, and `@dvmkit/sdk/testing`. Nothing reachable only through a deep path into `dist/` is public, and `npm run check:api` pins every exported declaration name against `api-surface.json`, so a change to that surface is a deliberate, reviewed act rather than a side effect.
|
|
18
|
+
|
|
19
|
+
Wire compatibility with callers is separate from the package version and is declared per endpoint. A server built on this SDK answers `DVM-Protocol-Version: 1`, and an individual endpoint may require a set of named capability tokens the caller advertises, a minimum caller version, or both. Capability tokens are additive, so an endpoint that declares no requirement stays reachable by a caller that sends no compatibility headers at all. A caller that cannot meet what an endpoint declares gets HTTP 426 naming the required capabilities and, where one is declared, the minimum version. The protocol is specified at [dvmkit.ai/docs/protocol](https://dvmkit.ai/docs/protocol).
|
|
20
|
+
|
|
21
|
+
If an upgrade broke something for you, an [API compatibility report](https://github.com/dvmkit/sdk/issues/new/choose) is the most useful thing you can send us. It tells us which parts of the published surface people actually build on, which no test of ours can.
|
|
22
|
+
|
|
23
|
+
## Contributing
|
|
24
|
+
|
|
25
|
+
**This repository does not accept unsolicited pull requests.** One opened without an invitation is closed automatically, with a pointer to where the report belongs.
|
|
26
|
+
|
|
27
|
+
**Issues and discussions are welcome and read.** We do not merge a change we cannot reproduce, and once we can reproduce a problem our own pipeline writes and reviews the fix. That makes a precise reproduction worth more to us than a patch, so that is what we ask for.
|
|
28
|
+
|
|
29
|
+
[CONTRIBUTING.md](./CONTRIBUTING.md) is the full policy. [SUPPORT.md](./SUPPORT.md) routes a question to the right place, [SECURITY.md](./SECURITY.md) covers vulnerability reports, which never go in a public issue, and the [Code of Conduct](./CODE_OF_CONDUCT.md) governs both trackers.
|
|
30
|
+
|
|
31
|
+
## Trademarks
|
|
32
|
+
|
|
33
|
+
The Apache License 2.0 covers the source. It grants no rights in the dvmkit name or branding, and nothing in this repository implies a trademark licence.
|
|
34
|
+
|
|
35
|
+
`dvmkit` identifies products and services of DVM Technologies Ltd. You may say truthfully that your software uses, works with, or is built on `@dvmkit/sdk`, name the package in dependency listings and documentation, and describe an unmodified official release by its name. You need written permission to name a product, service, company, domain, or account in a way likely to be read as official dvmkit work, to use dvmkit branding as your own identity, or to claim sponsorship, certification, partnership, or endorsement. A fork is yours to redistribute under the Apache licence; name and present it so that nobody mistakes it for an official release.
|
|
36
|
+
|
|
37
|
+
None of this claims exclusive rights in the phrase "digital vending machine" or in "DVM" used descriptively. Permission requests go to `support@dvmkit.com` with the subject `dvmkit trademark use request`, and nothing here limits rights you have under applicable law, including nominative fair use.
|
|
38
|
+
|
|
39
|
+
## License
|
|
40
|
+
|
|
41
|
+
Apache-2.0. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE).
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// src/sdk/pricing/currency.ts
|
|
2
|
+
var InvalidCurrencyError = class extends Error {
|
|
3
|
+
code = "invalid_currency";
|
|
4
|
+
raw;
|
|
5
|
+
constructor(raw) {
|
|
6
|
+
super(
|
|
7
|
+
`invalid currency "${rawCurrencyDisplay(raw)}" \u2014 expected ISO 4217 lowercase (e.g. "usd")`
|
|
8
|
+
);
|
|
9
|
+
this.raw = raw;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
function rawCurrencyDisplay(raw) {
|
|
13
|
+
if (typeof raw === "string") return raw;
|
|
14
|
+
return "(non-string)";
|
|
15
|
+
}
|
|
16
|
+
var UnsupportedCurrencyError = class extends Error {
|
|
17
|
+
constructor(raw, supported) {
|
|
18
|
+
super(`fx fetcher supports [${supported.join(",")}]; got "${raw}"`);
|
|
19
|
+
this.raw = raw;
|
|
20
|
+
this.supported = supported;
|
|
21
|
+
}
|
|
22
|
+
raw;
|
|
23
|
+
supported;
|
|
24
|
+
code = "unsupported_currency";
|
|
25
|
+
};
|
|
26
|
+
var CURRENCY_RE = /^[a-z]{3}$/;
|
|
27
|
+
function validateCurrency(raw) {
|
|
28
|
+
if (typeof raw !== "string" || !CURRENCY_RE.test(raw)) {
|
|
29
|
+
throw new InvalidCurrencyError(raw);
|
|
30
|
+
}
|
|
31
|
+
return raw;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// src/sdk/pricing/fx.ts
|
|
35
|
+
var DEFAULT_FX_CURRENCIES = ["usd", "eur", "gbp", "jpy"];
|
|
36
|
+
var DEFAULT_FX_RATE_SOURCE = `https://api.coingecko.com/api/v3/simple/price?ids=bitcoin&vs_currencies=${DEFAULT_FX_CURRENCIES.join(",")}`;
|
|
37
|
+
var FX_CACHE_TTL_MS = 3e5;
|
|
38
|
+
var FX_RETRY_COUNT = 2;
|
|
39
|
+
var FX_RETRY_BASE_DELAY_MS = 200;
|
|
40
|
+
var FxRateUnavailableError = class extends Error {
|
|
41
|
+
code = "fx_rate_unavailable";
|
|
42
|
+
display;
|
|
43
|
+
hint;
|
|
44
|
+
/**
|
|
45
|
+
* Set only when the platform explicitly refused a *known-stale* snapshot
|
|
46
|
+
* (internal-review). Absent means transient/unreachable — a distinction the payment
|
|
47
|
+
* rail's amount computation branches on: refuse on `knownStale`, degrade to
|
|
48
|
+
* the last-known rate without it. See {@link FxKnownStale}.
|
|
49
|
+
*/
|
|
50
|
+
knownStale;
|
|
51
|
+
constructor(message, copy, knownStale) {
|
|
52
|
+
super(message);
|
|
53
|
+
this.name = "FxRateUnavailableError";
|
|
54
|
+
this.display = copy?.display ?? "Couldn't fetch the BTC reference rate to lock pricing";
|
|
55
|
+
this.hint = copy?.hint ?? "transient \u2014 try the quote again in a few seconds";
|
|
56
|
+
this.knownStale = knownStale;
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
function createFxFetcher(opts) {
|
|
60
|
+
const currencies = opts?.currencies ?? DEFAULT_FX_CURRENCIES;
|
|
61
|
+
const source = opts?.source ?? DEFAULT_FX_RATE_SOURCE;
|
|
62
|
+
const fetchFn = opts?.fetchFn ?? fetch;
|
|
63
|
+
const now = opts?.now ?? (() => Date.now());
|
|
64
|
+
const cacheTtlMs = opts?.cacheTtlMs ?? FX_CACHE_TTL_MS;
|
|
65
|
+
const retries = opts?.retries ?? FX_RETRY_COUNT;
|
|
66
|
+
const retryDelayMs = opts?.retryDelayMs ?? FX_RETRY_BASE_DELAY_MS;
|
|
67
|
+
let cached = null;
|
|
68
|
+
let inFlight = null;
|
|
69
|
+
let partialEpisode = null;
|
|
70
|
+
return {
|
|
71
|
+
supportedCurrencies() {
|
|
72
|
+
return currencies;
|
|
73
|
+
},
|
|
74
|
+
lastKnown() {
|
|
75
|
+
return cached;
|
|
76
|
+
},
|
|
77
|
+
async fetch() {
|
|
78
|
+
const t = now();
|
|
79
|
+
if (cached && t - cached.cachedAt < cacheTtlMs) return cached.snapshot;
|
|
80
|
+
if (inFlight) return inFlight;
|
|
81
|
+
const promise = (async () => {
|
|
82
|
+
let lastErr = null;
|
|
83
|
+
for (let attempt = 0; attempt <= retries; attempt++) {
|
|
84
|
+
try {
|
|
85
|
+
const fresh = await fetchOnce(source, currencies, fetchFn);
|
|
86
|
+
const snapshot = carryForward(fresh, cached?.snapshot, currencies);
|
|
87
|
+
partialEpisode = warnPartialRates(sourceLabel(source), snapshot, partialEpisode);
|
|
88
|
+
cached = { snapshot, cachedAt: now() };
|
|
89
|
+
return snapshot;
|
|
90
|
+
} catch (err) {
|
|
91
|
+
lastErr = err;
|
|
92
|
+
if (err instanceof FxRateUnavailableError && err.knownStale) throw err;
|
|
93
|
+
if (attempt < retries && retryDelayMs > 0) {
|
|
94
|
+
await delay(retryDelayMs * 2 ** attempt);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
throw new FxRateUnavailableError(
|
|
99
|
+
lastErr instanceof Error ? lastErr.message : String(lastErr)
|
|
100
|
+
);
|
|
101
|
+
})();
|
|
102
|
+
inFlight = promise;
|
|
103
|
+
try {
|
|
104
|
+
return await promise;
|
|
105
|
+
} finally {
|
|
106
|
+
inFlight = null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function warmFxSnapshot(fetcher) {
|
|
112
|
+
if (fetcher.lastKnown?.()) return;
|
|
113
|
+
try {
|
|
114
|
+
void fetcher.fetch().catch(() => void 0);
|
|
115
|
+
} catch {
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
function fxRateFor(snapshot, currency) {
|
|
119
|
+
if (!Object.prototype.hasOwnProperty.call(snapshot.rates, currency)) {
|
|
120
|
+
if (snapshot.unavailable?.includes(currency)) {
|
|
121
|
+
throw new FxRateUnavailableError(
|
|
122
|
+
`fx source carries no ${currency.toUpperCase()} rate and this DVM has no cached one.`,
|
|
123
|
+
{
|
|
124
|
+
display: `The ${currency.toUpperCase()} reference rate is unavailable right now.`,
|
|
125
|
+
hint: `The rate source stopped carrying ${currency.toUpperCase()}. Retry in a minute; if it persists the source needs to be reconfigured.`
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
throw new UnsupportedCurrencyError(currency, Object.keys(snapshot.rates));
|
|
130
|
+
}
|
|
131
|
+
return snapshot.rates[currency];
|
|
132
|
+
}
|
|
133
|
+
async function fetchOnce(source, currencies, fetchFn) {
|
|
134
|
+
if (typeof source === "string") return fetchCoinGeckoShape(source, currencies, fetchFn);
|
|
135
|
+
return fetchPlatformShape(source, currencies, fetchFn);
|
|
136
|
+
}
|
|
137
|
+
async function fetchCoinGeckoShape(source, currencies, fetchFn) {
|
|
138
|
+
const res = await fetchFn(source);
|
|
139
|
+
if (!res.ok) {
|
|
140
|
+
throw new Error(`fx source ${source} returned ${res.status.toString()}`);
|
|
141
|
+
}
|
|
142
|
+
const body = await res.json();
|
|
143
|
+
const bitcoin = body.bitcoin;
|
|
144
|
+
if (!bitcoin || typeof bitcoin !== "object") {
|
|
145
|
+
throw new Error(`fx source ${source} returned an unparsable rate body`);
|
|
146
|
+
}
|
|
147
|
+
return { rates: collectRates(source, bitcoin, currencies), as_of: (/* @__PURE__ */ new Date()).toISOString() };
|
|
148
|
+
}
|
|
149
|
+
async function fetchPlatformShape(source, currencies, fetchFn) {
|
|
150
|
+
const res = await fetchFn(source.url, {
|
|
151
|
+
headers: { Authorization: `Bearer ${source.token}`, Accept: "application/json" }
|
|
152
|
+
});
|
|
153
|
+
if (!res.ok) {
|
|
154
|
+
const knownStale = await parsePlatformStaleRefusal(res);
|
|
155
|
+
if (knownStale) {
|
|
156
|
+
throw new FxRateUnavailableError(
|
|
157
|
+
`fx source ${source.url} refused: rate ${knownStale.staleDays.toString()}d stale` + (knownStale.thresholdDays !== void 0 ? ` (threshold ${knownStale.thresholdDays.toString()}d)` : ""),
|
|
158
|
+
void 0,
|
|
159
|
+
knownStale
|
|
160
|
+
);
|
|
161
|
+
}
|
|
162
|
+
throw new Error(`fx source ${source.url} returned ${res.status.toString()}`);
|
|
163
|
+
}
|
|
164
|
+
const body = await res.json();
|
|
165
|
+
const raw = body.rates;
|
|
166
|
+
if (!raw || typeof raw !== "object") {
|
|
167
|
+
throw new Error(`fx source ${source.url} returned an unparsable rate body`);
|
|
168
|
+
}
|
|
169
|
+
const rates = collectRates(source.url, raw, currencies);
|
|
170
|
+
const as_of = typeof body.as_of === "string" ? body.as_of : (/* @__PURE__ */ new Date()).toISOString();
|
|
171
|
+
return { rates, as_of };
|
|
172
|
+
}
|
|
173
|
+
function collectRates(source, raw, currencies) {
|
|
174
|
+
const rates = {};
|
|
175
|
+
for (const currency of currencies) {
|
|
176
|
+
const value = raw[currency];
|
|
177
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) continue;
|
|
178
|
+
rates[currency] = value;
|
|
179
|
+
}
|
|
180
|
+
if (Object.keys(rates).length === 0) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
`fx source ${source} returned no usable rate for any of [${currencies.join(", ")}]`
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
return rates;
|
|
186
|
+
}
|
|
187
|
+
function carryForward(fresh, previous, currencies) {
|
|
188
|
+
const carried = [];
|
|
189
|
+
const unavailable = [];
|
|
190
|
+
const rates = { ...fresh.rates };
|
|
191
|
+
for (const currency of currencies) {
|
|
192
|
+
if (Object.prototype.hasOwnProperty.call(fresh.rates, currency)) continue;
|
|
193
|
+
const prior = previous?.rates[currency];
|
|
194
|
+
if (typeof prior === "number") {
|
|
195
|
+
rates[currency] = prior;
|
|
196
|
+
carried.push(currency);
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
unavailable.push(currency);
|
|
200
|
+
}
|
|
201
|
+
if (carried.length === 0 && unavailable.length === 0) return fresh;
|
|
202
|
+
return {
|
|
203
|
+
rates,
|
|
204
|
+
as_of: fresh.as_of,
|
|
205
|
+
...carried.length > 0 && { carried_forward: carried },
|
|
206
|
+
...unavailable.length > 0 && { unavailable }
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
function warnPartialRates(source, snapshot, episode) {
|
|
210
|
+
const carried = snapshot.carried_forward ?? [];
|
|
211
|
+
const unavailable = snapshot.unavailable ?? [];
|
|
212
|
+
if (carried.length === 0 && unavailable.length === 0) return null;
|
|
213
|
+
const key = `${carried.join(",")}|${unavailable.join(",")}`;
|
|
214
|
+
if (key === episode) return episode;
|
|
215
|
+
console.warn(
|
|
216
|
+
JSON.stringify({
|
|
217
|
+
level: "warn",
|
|
218
|
+
event: "fx_rates_partial",
|
|
219
|
+
source,
|
|
220
|
+
...carried.length > 0 && { carried_forward: carried },
|
|
221
|
+
...unavailable.length > 0 && { unavailable }
|
|
222
|
+
})
|
|
223
|
+
);
|
|
224
|
+
return key;
|
|
225
|
+
}
|
|
226
|
+
function sourceLabel(source) {
|
|
227
|
+
return typeof source === "string" ? source : source.url;
|
|
228
|
+
}
|
|
229
|
+
function delay(ms) {
|
|
230
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
231
|
+
}
|
|
232
|
+
async function parsePlatformStaleRefusal(res) {
|
|
233
|
+
let body;
|
|
234
|
+
try {
|
|
235
|
+
body = await res.json();
|
|
236
|
+
} catch {
|
|
237
|
+
return null;
|
|
238
|
+
}
|
|
239
|
+
const error = body.error;
|
|
240
|
+
if (!error || typeof error !== "object") return null;
|
|
241
|
+
const { code, detail } = error;
|
|
242
|
+
if (code !== "fx_rate_unavailable" || !detail || typeof detail !== "object") return null;
|
|
243
|
+
const { stale_days, stale_source, threshold_days } = detail;
|
|
244
|
+
if (typeof stale_days !== "number" || !Number.isFinite(stale_days)) return null;
|
|
245
|
+
return {
|
|
246
|
+
staleDays: stale_days,
|
|
247
|
+
staleSource: typeof stale_source === "string" ? stale_source : void 0,
|
|
248
|
+
thresholdDays: typeof threshold_days === "number" && Number.isFinite(threshold_days) ? threshold_days : void 0
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
function resolveFxSourceFromEnv(env) {
|
|
252
|
+
const raw = env.DVMKIT_FX_SOURCE;
|
|
253
|
+
if (!raw) return void 0;
|
|
254
|
+
if (raw === "platform") {
|
|
255
|
+
const url = env.DVMKIT_PLATFORM_URL;
|
|
256
|
+
const token = env.DVMKIT_PLATFORM_TOKEN;
|
|
257
|
+
if (!url || !token) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
'DVMKIT_FX_SOURCE="platform" requires DVMKIT_PLATFORM_URL and DVMKIT_PLATFORM_TOKEN to be set'
|
|
260
|
+
);
|
|
261
|
+
}
|
|
262
|
+
return { kind: "platform", url: `${url.replace(/\/+$/, "")}/_internal/fx`, token };
|
|
263
|
+
}
|
|
264
|
+
return raw;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// src/sdk/pricing/upfront.ts
|
|
268
|
+
function buildUpfront(input) {
|
|
269
|
+
const currency = validateCurrency(input.currency);
|
|
270
|
+
const amount = input.amount;
|
|
271
|
+
if (!Number.isFinite(amount) || amount < 0) {
|
|
272
|
+
throw new Error(`invalid upfront amount: ${String(amount)} (must be a non-negative number)`);
|
|
273
|
+
}
|
|
274
|
+
return { amount, currency };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
export {
|
|
278
|
+
InvalidCurrencyError,
|
|
279
|
+
UnsupportedCurrencyError,
|
|
280
|
+
validateCurrency,
|
|
281
|
+
DEFAULT_FX_CURRENCIES,
|
|
282
|
+
DEFAULT_FX_RATE_SOURCE,
|
|
283
|
+
FX_CACHE_TTL_MS,
|
|
284
|
+
FX_RETRY_COUNT,
|
|
285
|
+
FxRateUnavailableError,
|
|
286
|
+
createFxFetcher,
|
|
287
|
+
warmFxSnapshot,
|
|
288
|
+
fxRateFor,
|
|
289
|
+
resolveFxSourceFromEnv,
|
|
290
|
+
buildUpfront
|
|
291
|
+
};
|