@mandateops/audit 0.0.0-stage → 0.1.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/dist/index.js ADDED
@@ -0,0 +1,532 @@
1
+ /**
2
+ * Thin client of a `mandateops session` host. It carries declarations and
3
+ * readings; the host owns the deployment, faults and effect records, and the
4
+ * Rust checker alone decides results.
5
+ *
6
+ * Uses Node's standard modules only, so `auditRun` runs under Node or Bun and
7
+ * any test runner on them. `./bun` adds `auditTest` for Bun's own runner.
8
+ */
9
+ import { spawn } from "node:child_process";
10
+ import { randomUUID } from "node:crypto";
11
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
12
+ import { tmpdir } from "node:os";
13
+ import { dirname, join, resolve } from "node:path";
14
+ import { setTimeout as sleep } from "node:timers/promises";
15
+ import { parseCapacityReading, parseHistoryReading, requireHistoryFits, } from "./reading.js";
16
+ export { bridge, circleMint, adyen, airwallex, checkout, dwolla, fireblocks, flutterwave, modernTreasury, molliePayouts, paypal, paystack, peach, razorpayx, revolut, stitch, stripePayouts, stripeTransfers, wise, } from "./providers/index.js";
17
+ export { parseCapacityReading, parseHistoryReading, ReadingError, requireHistoryFits, } from "./reading.js";
18
+ const MINOR_UNITS = /^[1-9][0-9]*$/;
19
+ /**
20
+ * The largest amount the stand-in holds, the audit core's `u128` maximum.
21
+ * `fixtures/wire/vectors.json` pins it for every reader.
22
+ */
23
+ export const SYNTHETIC_MAX_MINOR_UNITS = 340282366920938463463374607431768211455n;
24
+ const MESSAGE_LIMIT_CHARS = 2000;
25
+ /** How long an aborted body may take to stop, unless the config says. */
26
+ const DEFAULT_ABORT_GRACE_MS = 1000;
27
+ /** Integer minor units as a decimal string, so no language loses precision. */
28
+ export function minorUnits(value) {
29
+ if (!MINOR_UNITS.test(value)) {
30
+ throw new TypeError("amountMinor must be a canonical decimal string");
31
+ }
32
+ if (BigInt(value) > SYNTHETIC_MAX_MINOR_UNITS) {
33
+ throw new RangeError("amountMinor is more than the provider can hold");
34
+ }
35
+ return value;
36
+ }
37
+ const PROPERTIES = [
38
+ "instruction_binding",
39
+ "commitment_retention",
40
+ "retry_identity",
41
+ "effect_within_authorisation",
42
+ "reconciliation_consistency",
43
+ "external_outcome",
44
+ ];
45
+ const PROPERTY_RESULTS = [
46
+ "satisfied",
47
+ "violated",
48
+ "inconclusive",
49
+ "not_applicable",
50
+ ];
51
+ const ASSURANCES = [
52
+ "backend_and_provider",
53
+ "backend_and_provider_commitments_not_checked",
54
+ "money_out_only_commitments_not_checked",
55
+ "no_assurance",
56
+ ];
57
+ export const defineAdapter = (adapter) => adapter;
58
+ export const defineConfig = (config) => config;
59
+ /** The test body failed. The run is sealed and `result` records it. */
60
+ export class AuditTestFailure extends Error {
61
+ result;
62
+ cause;
63
+ constructor(result, cause) {
64
+ super(`test body failed in ${result.run_id}: ${String(cause)}`);
65
+ this.result = result;
66
+ this.cause = cause;
67
+ }
68
+ }
69
+ /** The SDK was used against its contract; the message says how to correct it. */
70
+ export class AuditUsageError extends Error {
71
+ name = "AuditUsageError";
72
+ }
73
+ const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
74
+ /**
75
+ * The session protocol this SDK speaks. The host announces its own when
76
+ * ready; the two must match, as they do for an SDK shipped with its CLI.
77
+ */
78
+ export const SESSION_PROTOCOL = 2;
79
+ /** Why a host of another protocol is refused, and how to fix it. */
80
+ export function protocolMismatch(announced) {
81
+ const host = announced === undefined
82
+ ? "an older protocol"
83
+ : `protocol ${String(announced)}`;
84
+ return `the mandateops CLI speaks ${host} and this SDK speaks protocol ${String(SESSION_PROTOCOL)}: add the SDK shipped with your CLI (sdk/mandateops-audit-VERSION.tgz in its install) with \`bun add\``;
85
+ }
86
+ /** A line from the host, or `undefined` for one this client cannot trust. */
87
+ function parseReply(line) {
88
+ let value;
89
+ try {
90
+ value = JSON.parse(line);
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ if (!isRecord(value))
96
+ return undefined;
97
+ const { id, ok, error, ready, backend, credentials, protocol } = value;
98
+ const limit = value["reply_limit_ms"];
99
+ const recovery = value["recover_command"];
100
+ const terminal = value["terminal"];
101
+ const typed = (id === undefined || typeof id === "number") &&
102
+ (ok === undefined || typeof ok === "boolean") &&
103
+ (error === undefined || typeof error === "string") &&
104
+ (ready === undefined || typeof ready === "boolean") &&
105
+ (protocol === undefined || isPositiveInteger(protocol)) &&
106
+ (backend === undefined || typeof backend === "string") &&
107
+ (credentials === undefined || isCredentials(credentials)) &&
108
+ (limit === undefined || isPositiveInteger(limit)) &&
109
+ (recovery === undefined || typeof recovery === "string") &&
110
+ (terminal === undefined || typeof terminal === "boolean");
111
+ return typed ? value : undefined;
112
+ }
113
+ const isPositiveInteger = (value) => Number.isSafeInteger(value) && value > 0;
114
+ const isCredentials = (value) => isRecord(value) &&
115
+ ["client", "operator", "viewer"].every((name) => typeof value[name] === "string" && value[name] !== "");
116
+ const isOneOf = (allowed, value) => typeof value === "string" && allowed.includes(value);
117
+ const isDecision = (value) => isRecord(value) &&
118
+ isOneOf(PROPERTIES, value["property"]) &&
119
+ isOneOf(PROPERTY_RESULTS, value["result"]) &&
120
+ typeof value["required"] === "boolean";
121
+ const isFinding = (value) => isRecord(value) && typeof value["rule"] === "string";
122
+ const isVerdict = (value) => isRecord(value) &&
123
+ typeof value["verdict"] === "string" &&
124
+ (value["findings"] === undefined ||
125
+ (Array.isArray(value["findings"]) && value["findings"].every(isFinding))) &&
126
+ (value["reasons"] === undefined || Array.isArray(value["reasons"]));
127
+ /** A sealed result, checked before anything reads it. */
128
+ export function parseResult(value) {
129
+ const valid = isRecord(value) &&
130
+ typeof value["run_id"] === "string" &&
131
+ typeof value["ci_passed"] === "boolean" &&
132
+ isOneOf(ASSURANCES, value["assurance"]) &&
133
+ (value["assumption"] === undefined ||
134
+ typeof value["assumption"] === "string") &&
135
+ Array.isArray(value["properties"]) &&
136
+ value["properties"].every(isDecision) &&
137
+ isVerdict(value["verdict"]) &&
138
+ typeof value["record_published"] === "boolean";
139
+ if (!valid)
140
+ throw new TypeError("audit host sealed a result this SDK cannot read; the @mandateops/audit package and the mandateops CLI are probably different versions: add the SDK shipped with your CLI (its sdk/mandateops-audit-VERSION.tgz)");
141
+ return value;
142
+ }
143
+ /** A line protocol client; one request in flight at a time per caller. */
144
+ class Channel {
145
+ host;
146
+ buffered = "";
147
+ nextId = 1;
148
+ waiting = new Map();
149
+ announced;
150
+ /** The host announces it; nothing is sent before it has. */
151
+ replyLimitMs = 0;
152
+ /** The first reply ending the channel: after it, nothing more will answer. */
153
+ terminal;
154
+ ready;
155
+ constructor(host) {
156
+ this.host = host;
157
+ // A host that dies mid-write ends stdout too, and `read` reports that.
158
+ host.stdin.on("error", () => { });
159
+ this.ready = new Promise((settle) => {
160
+ this.announced = settle;
161
+ });
162
+ // A program that never started has no stream to read.
163
+ this.read().catch(() => {
164
+ this.settleAll({ ok: false, error: "host_stopped" });
165
+ });
166
+ }
167
+ settleAll(reply) {
168
+ this.terminal ??= reply;
169
+ this.announced?.(reply);
170
+ for (const settle of this.waiting.values())
171
+ settle(reply);
172
+ }
173
+ accept(line) {
174
+ const reply = parseReply(line);
175
+ if (!reply) {
176
+ this.settleAll({ ok: false, error: "unreadable_reply" });
177
+ return;
178
+ }
179
+ if (reply.ready) {
180
+ this.replyLimitMs = reply.reply_limit_ms ?? 0;
181
+ this.announced?.(reply);
182
+ }
183
+ else if (reply.id !== undefined)
184
+ this.waiting.get(reply.id)?.(reply);
185
+ else if (reply.terminal)
186
+ this.settleAll(reply);
187
+ // Refused one unreadable message: its caller stops, the channel stays open.
188
+ else
189
+ for (const settle of this.waiting.values())
190
+ settle(reply);
191
+ }
192
+ async read() {
193
+ const decoder = new TextDecoder();
194
+ for await (const chunk of this.host.stdout) {
195
+ this.buffered += decoder.decode(chunk, { stream: true });
196
+ for (;;) {
197
+ const end = this.buffered.indexOf("\n");
198
+ if (end < 0)
199
+ break;
200
+ const line = this.buffered.slice(0, end);
201
+ this.buffered = this.buffered.slice(end + 1);
202
+ this.accept(line);
203
+ }
204
+ }
205
+ this.settleAll({ ok: false, error: "host_stopped" });
206
+ }
207
+ async send(message) {
208
+ if (this.terminal)
209
+ throw new Error(`audit host refused: ${String(this.terminal.error)}`);
210
+ const id = this.nextId;
211
+ this.nextId += 1;
212
+ let timer;
213
+ const answered = new Promise((settle) => {
214
+ this.waiting.set(id, settle);
215
+ timer = setTimeout(() => {
216
+ settle({ ok: false, error: "reply_timed_out" });
217
+ }, this.replyLimitMs);
218
+ });
219
+ this.host.stdin.write(`${JSON.stringify({ id, ...message })}\n`);
220
+ const reply = await answered;
221
+ clearTimeout(timer);
222
+ this.waiting.delete(id);
223
+ if (!reply.ok)
224
+ throw new Error(`audit host refused: ${String(reply.error)}`);
225
+ return reply.body;
226
+ }
227
+ }
228
+ /**
229
+ * The whole run, for a test runner's own timeout: startup, the body, closing
230
+ * work on either side of the longest wait, and teardown, which may take as
231
+ * long as startup. The host enforces each bound; this only stays above them.
232
+ */
233
+ export const runLimit = (config) => config.system.startupTimeoutS * 2000 +
234
+ config.testBudgetMs +
235
+ Math.max(config.drainBoundMs, config.system.faultDeadlineMs) * 2;
236
+ /** Profiles judged on backend readings; the other is on provider records alone. */
237
+ const readingProfile = (config) => config.financialProfile === "payout-effects-only"
238
+ ? undefined
239
+ : config.financialProfile;
240
+ /**
241
+ * Refuses, before anything starts, a run that could only end inconclusive.
242
+ *
243
+ * @throws {AuditUsageError} when the profile and adapter do not go together.
244
+ */
245
+ export function requireRunnable(config) {
246
+ const profile = readingProfile(config);
247
+ if (profile === undefined && config.adapter) {
248
+ throw new AuditUsageError("payout-effects-only is judged on provider records alone: remove the adapter");
249
+ }
250
+ if (profile !== undefined && !config.adapter) {
251
+ throw new AuditUsageError(`${profile} needs an adapter with history and capacity readings ` +
252
+ "(contracts/history-reading.md). A backend without reservations or " +
253
+ "capacity cannot use it; use payout-effects-only");
254
+ }
255
+ }
256
+ function spawnHost(config, runId, systemFile) {
257
+ const compose = resolve(config.system.compose);
258
+ const system = {
259
+ ...config.system,
260
+ compose,
261
+ projectDirectory: resolve(config.system.projectDirectory ?? dirname(compose)),
262
+ };
263
+ // A file only its owner reads: declared values stay out of process listings.
264
+ writeFileSync(systemFile, JSON.stringify(system), { mode: 0o600 });
265
+ const [command, ...leading] = hostProgram(config);
266
+ return spawn(command, [
267
+ ...leading,
268
+ "session",
269
+ "--system-file",
270
+ systemFile,
271
+ "--run-id",
272
+ runId,
273
+ "--runs-dir",
274
+ runsDirectoryOf(config),
275
+ "--financial-profile",
276
+ config.financialProfile,
277
+ "--drain-ms",
278
+ String(config.drainMs),
279
+ "--drain-bound-ms",
280
+ String(config.drainBoundMs),
281
+ "--host-program",
282
+ JSON.stringify(hostProgram(config)),
283
+ "--provider",
284
+ config.provider,
285
+ ], { stdio: ["pipe", "pipe", "inherit"] });
286
+ }
287
+ /** Settles when the host is gone, including one that never started. */
288
+ const hostGone = (host) => new Promise((settle) => {
289
+ host.once("error", () => {
290
+ settle(null);
291
+ });
292
+ host.once("close", (code) => {
293
+ settle(code);
294
+ });
295
+ });
296
+ export function wireInstruction(expected) {
297
+ const { reference, amountMinor, outcome: _declared, ...scope } = expected;
298
+ return { payment: reference, ...scope, amount: minorUnits(amountMinor) };
299
+ }
300
+ const FAULT_STAGE_MEANS = {
301
+ none: "no fault is armed; call loseResponseAfterCommit first",
302
+ armed: "a fault is already armed; release it first",
303
+ established: "a fault is already armed; release it first",
304
+ released: "the fault was already released; arm another first",
305
+ };
306
+ /**
307
+ * Run one audited test and return the recorded result without judging it. A
308
+ * body that fails still seals the run, then rejects with [`AuditTestFailure`].
309
+ */
310
+ export async function auditRun(config, body, runId = `sdk-${randomUUID().slice(0, 18)}`) {
311
+ requireRunnable(config);
312
+ const declared = mkdtempSync(join(tmpdir(), "audit-system-"));
313
+ const host = spawnHost(config, runId, join(declared, "system.json"));
314
+ const gone = hostGone(host);
315
+ const channel = new Channel(host);
316
+ try {
317
+ const announced = await channel.ready;
318
+ if (announced.ready && announced.protocol !== SESSION_PROTOCOL) {
319
+ throw new Error(protocolMismatch(announced.protocol));
320
+ }
321
+ if (!announced.backend ||
322
+ !announced.credentials ||
323
+ !announced.reply_limit_ms ||
324
+ !announced.recover_command) {
325
+ throw new Error(`audit host did not start: ${announced.error ?? "incomplete_announcement"}`);
326
+ }
327
+ const result = await session(config, body, channel, announced.backend, announced.credentials);
328
+ return afterTeardown(result, await gone, announced.recover_command);
329
+ }
330
+ finally {
331
+ // End of input: a host that has not sealed recovers what it has.
332
+ host.stdin.end();
333
+ await gone;
334
+ rmSync(declared, { recursive: true, force: true });
335
+ }
336
+ }
337
+ /** Where runs are written: the config, else `MANDATEOPS_RUNS_DIR` (set by the
338
+ * GitHub Action), else `runs` in the working directory. */
339
+ const runsDirectoryOf = (config) => resolve(config.runsDirectory ?? (process.env["MANDATEOPS_RUNS_DIR"] || "runs"));
340
+ /** What this config runs as the host, ahead of a subcommand. */
341
+ const hostProgram = (config) => {
342
+ const [command = "mandateops", ...leading] = config.hostCommand ?? [];
343
+ return [command, ...leading];
344
+ };
345
+ /** A seal precedes teardown, so a pass stands only if the host then exits clean. */
346
+ const afterTeardown = (result, exit, recoverCommand) => result.ci_passed && exit !== 0
347
+ ? {
348
+ ...result,
349
+ ci_passed: false,
350
+ assurance: "no_assurance",
351
+ teardown_failure: { recover_command: recoverCommand },
352
+ }
353
+ : result;
354
+ async function session(config, body, channel, backend, credentials) {
355
+ const viewerCredential = credentials.viewer;
356
+ const profile = readingProfile(config);
357
+ let providerPayment;
358
+ let authorised;
359
+ let fault = "none";
360
+ const problems = [];
361
+ /** Closed when the body ends: a timed-out body cannot speak during capture. */
362
+ let bodyOpen = true;
363
+ const fromBody = async (message) => {
364
+ if (!bodyOpen)
365
+ throw new Error("the test body already ended");
366
+ return channel.send(message);
367
+ };
368
+ const reading = async (slot, read) => {
369
+ let taken;
370
+ try {
371
+ taken = await read();
372
+ }
373
+ catch (error) {
374
+ const reason = String(error).slice(0, MESSAGE_LIMIT_CHARS);
375
+ problems.push({ slot, reason });
376
+ console.error(`audit reading ${slot} refused: ${reason}`);
377
+ await channel.send({ type: "reading_failed", slot, reason });
378
+ return;
379
+ }
380
+ await channel.send({ type: "reading", slot, body: taken });
381
+ };
382
+ const context = () => providerPayment === undefined
383
+ ? undefined
384
+ : { backend, viewerCredential, providerPayment };
385
+ const readHistory = async (slot) => {
386
+ const read = context();
387
+ if (config.adapter && profile && read && authorised) {
388
+ const { history } = config.adapter;
389
+ const reference = authorised.expected.reference;
390
+ await reading(slot, async () => {
391
+ const taken = parseHistoryReading(await history(read));
392
+ requireHistoryFits(taken, reference, profile);
393
+ return taken;
394
+ });
395
+ }
396
+ };
397
+ const readCapacity = async (slot) => {
398
+ const read = context();
399
+ const capacity = config.adapter?.capacity;
400
+ if (capacity && read) {
401
+ await reading(slot, async () => parseCapacityReading(await capacity(read)));
402
+ }
403
+ };
404
+ const requireBound = (doing) => {
405
+ if (profile && authorised && providerPayment === undefined) {
406
+ throw new AuditUsageError(`${doing} under ${profile} needs bindProviderPayment first: adapters read by that identifier`);
407
+ }
408
+ };
409
+ const requireFault = (doing, stages) => {
410
+ if (!bodyOpen)
411
+ throw new Error("the test body already ended");
412
+ if (!stages.includes(fault)) {
413
+ throw new AuditUsageError(`${doing}: ${FAULT_STAGE_MEANS[fault]}`);
414
+ }
415
+ };
416
+ const budgetSpent = new AbortController();
417
+ const fixture = {
418
+ signal: budgetSpent.signal,
419
+ app: backend,
420
+ credentials,
421
+ provider: {
422
+ expectPayment: async (expected) => {
423
+ await fromBody({
424
+ type: "authorise",
425
+ authorisation: {
426
+ instruction: wireInstruction(expected),
427
+ capacity_limit: minorUnits(config.capacityLimitMinor),
428
+ provider: {
429
+ account: config.system.providerTerms.account,
430
+ operation: config.system.providerTerms.operation,
431
+ idempotency_scope: config.system.providerTerms.idempotencyScope,
432
+ },
433
+ ...(expected.outcome ? { expected_outcome: expected.outcome } : {}),
434
+ },
435
+ });
436
+ authorised = {
437
+ expected,
438
+ bindProviderPayment: async (payment) => {
439
+ await fromBody({ type: "bind_provider_payment", payment });
440
+ providerPayment = payment;
441
+ },
442
+ };
443
+ return authorised;
444
+ },
445
+ },
446
+ faults: {
447
+ loseResponseAfterCommit: async ({ payment, mode }) => {
448
+ requireFault("loseResponseAfterCommit", ["none", "released"]);
449
+ if (payment !== authorised) {
450
+ throw new AuditUsageError("loseResponseAfterCommit: payment must be what expectPayment returned");
451
+ }
452
+ await fromBody({ type: "arm_fault", kind: mode });
453
+ fault = "armed";
454
+ },
455
+ established: async () => {
456
+ requireFault("established", ["armed", "established"]);
457
+ await fromBody({ type: "await_fault" });
458
+ fault = "established";
459
+ },
460
+ release: async () => {
461
+ requireFault("release", ["armed", "established"]);
462
+ await fromBody({ type: "release_fault" });
463
+ fault = "released";
464
+ },
465
+ refuseNext: async () => {
466
+ await fromBody({ type: "refuse_next" });
467
+ },
468
+ },
469
+ inspect: async () => {
470
+ if (!bodyOpen)
471
+ throw new Error("the test body already ended");
472
+ requireBound("inspect");
473
+ await readHistory("inspect_history");
474
+ await readCapacity("inspect_capacity");
475
+ await readHistory("reinspect_history");
476
+ },
477
+ };
478
+ let status = "passed";
479
+ let failure;
480
+ let timer;
481
+ const running = body(fixture).then(() => {
482
+ requireBound("a test");
483
+ });
484
+ try {
485
+ await Promise.race([
486
+ running,
487
+ new Promise((_, reject) => {
488
+ timer = setTimeout(() => {
489
+ status = "timed_out";
490
+ budgetSpent.abort();
491
+ reject(new Error("test budget exceeded"));
492
+ }, config.testBudgetMs);
493
+ }),
494
+ ]);
495
+ }
496
+ catch (error) {
497
+ if (status === "passed")
498
+ status = "threw";
499
+ failure = error;
500
+ }
501
+ finally {
502
+ clearTimeout(timer);
503
+ bodyOpen = false;
504
+ }
505
+ if (status === "timed_out") {
506
+ await Promise.race([
507
+ running.catch(() => { }),
508
+ sleep(config.abortGraceMs ?? DEFAULT_ABORT_GRACE_MS, undefined, {
509
+ ref: false,
510
+ }),
511
+ ]);
512
+ }
513
+ await channel.send({
514
+ type: "test_ended",
515
+ status,
516
+ ...(status === "passed"
517
+ ? {}
518
+ : { message: String(failure).slice(0, MESSAGE_LIMIT_CHARS) }),
519
+ });
520
+ await channel.send({ type: "drain" });
521
+ await readHistory("capture_history");
522
+ await readCapacity("capture_capacity");
523
+ await channel.send({ type: "read_effects" });
524
+ await readHistory("recapture_history");
525
+ const result = {
526
+ ...parseResult(await channel.send({ type: "seal" })),
527
+ reading_problems: problems,
528
+ };
529
+ if (status !== "passed")
530
+ throw new AuditTestFailure(result, failure);
531
+ return result;
532
+ }
@@ -0,0 +1,9 @@
1
+ import type { Reply } from "./types.js";
2
+ /**
3
+ * An empty body marks a failure whose body the provider does not document, and
4
+ * its status is then the conservative choice. OAuth token calls are not
5
+ * imitated: a client of such a provider must hold its bearer token directly.
6
+ */
7
+ export declare const undocumented: (status: number) => Reply;
8
+ export declare const CREATED_AT = "2026-01-01T00:00:00Z";
9
+ export declare const CREATED_UNIX = 1767225600;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * An empty body marks a failure whose body the provider does not document, and
3
+ * its status is then the conservative choice. OAuth token calls are not
4
+ * imitated: a client of such a provider must hold its bearer token directly.
5
+ */
6
+ export const undocumented = (status) => ({ status, body: {} });
7
+ export const CREATED_AT = "2026-01-01T00:00:00Z";
8
+ export const CREATED_UNIX = 1_767_225_600;
@@ -0,0 +1,36 @@
1
+ import type { BridgeTerms, FireblocksTerms, LedgerTerms, ProviderWire } from "./types.js";
2
+ /**
3
+ * `POST /v0/transfers`, USDC out on the rail the request names. `amount` is in
4
+ * the source currency if fiat, else the fiat it pegs, so a USDC source reads at
5
+ * USD scale while staying its own term; `destination.amount` is in the destination currency, with the term
6
+ * written `usdc-from-eur` so what funds it stays bound, and giving both is
7
+ * refused. The source currencies listed are unverified. A new transfer
8
+ * starts `awaiting_funds`; later states are not imitated. Bridge replays the
9
+ * original for a key for 24 hours and errors on a changed body; that error's
10
+ * status and body are unverified, and 400 is the listed status that fits.
11
+ * `client_reference_id` is optional, so the payment then takes the key.
12
+ */
13
+ export declare const bridge: (terms: BridgeTerms) => ProviderWire;
14
+ /**
15
+ * `POST /v1/businessAccount/transfers`, USDC only, written as currency `USD`.
16
+ * The destination is a registered address id, whose chain Circle looks up, so
17
+ * asset and network are declared. `source.id` names the paying wallet;
18
+ * `account` is what its absence means. The reply's `{ data }` wrapper is
19
+ * documented, and a failure is a 201 with `status: failed`. The key must be a
20
+ * UUID v4. The repeat-key reply, behaviour on a changed body and the scale of
21
+ * six are unverified.
22
+ */
23
+ export declare const circleMint: ({ account, ...terms }: LedgerTerms) => ProviderWire;
24
+ /**
25
+ * `POST /v1/transactions`, USDC and ETH to a one-time address from a vault,
26
+ * signed per request. Fireblocks keys transactions on `externalTxId` and
27
+ * refuses a reused one with 400, so a retrying client must read that as already
28
+ * sent. A refused id stays refused. The network is declared and must be the one
29
+ * the asset implies; only the assets listed are supported. A
30
+ * reply of `SUBMITTED` is not completion. A create answers 200. Failure bodies
31
+ * follow the documented `{ message, code }`; their messages are unverified.
32
+ * Sources disagree on whether `externalTxId` is mandatory: the official SDK
33
+ * marks it optional, so a call without one has no idempotency and is its own
34
+ * payment. It is at most 255 characters.
35
+ */
36
+ export declare const fireblocks: (terms: FireblocksTerms) => ProviderWire;