@ar-agents/treasury 0.2.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,808 @@
1
+ // src/manteca.ts
2
+ var MantecaApiError = class extends Error {
3
+ constructor(message, status, body) {
4
+ super(message);
5
+ this.status = status;
6
+ this.body = body;
7
+ this.name = "MantecaApiError";
8
+ }
9
+ status;
10
+ body;
11
+ };
12
+ var MantecaAuthError = class extends MantecaApiError {
13
+ constructor(message, status, body) {
14
+ super(message, status, body);
15
+ this.name = "MantecaAuthError";
16
+ }
17
+ };
18
+ var MantecaRateLimitError = class extends MantecaApiError {
19
+ constructor(message, status, body) {
20
+ super(message, status, body);
21
+ this.name = "MantecaRateLimitError";
22
+ }
23
+ };
24
+ var DEFAULT_BASE_URL = "https://api.manteca.dev";
25
+ function num(v) {
26
+ if (typeof v === "number" && Number.isFinite(v)) return v;
27
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) {
28
+ return Number(v);
29
+ }
30
+ return void 0;
31
+ }
32
+ function parseDirectPrice(body, ticker) {
33
+ const fromObj = (o) => {
34
+ const direct = num(o);
35
+ if (direct !== void 0) return direct;
36
+ if (o && typeof o === "object") {
37
+ const rec = o;
38
+ for (const k of ["sell", "bid", "price", "ask", "buy", "value", "rate"]) {
39
+ const v = num(rec[k]);
40
+ if (v !== void 0) return v;
41
+ }
42
+ }
43
+ return void 0;
44
+ };
45
+ const env = body;
46
+ return fromObj(env?.[ticker]) ?? fromObj(body);
47
+ }
48
+ function normalizeMantecaStatus(raw) {
49
+ const s = (raw ?? "").toUpperCase();
50
+ if (["COMPLETED", "COMPLETE", "DONE", "SETTLED", "SUCCESS", "FINISHED"].includes(s))
51
+ return "COMPLETED";
52
+ if (["FAILED", "FAILURE", "ERROR", "REJECTED", "CANCELLED", "CANCELED"].includes(s))
53
+ return "FAILED";
54
+ if (["PROCESSING", "IN_PROGRESS", "INPROGRESS", "RUNNING", "PARTIAL"].includes(s))
55
+ return "PROCESSING";
56
+ if (["PENDING", "CREATED", "NEW", "QUEUED", "WAITING"].includes(s)) return "PENDING";
57
+ return "UNKNOWN";
58
+ }
59
+ var MantecaOffRampAdapter = class {
60
+ constructor(config) {
61
+ this.config = config;
62
+ if (!config.apiKey) throw new Error("MantecaConfig.apiKey is required");
63
+ if (!config.userId) throw new Error("MantecaConfig.userId is required");
64
+ if (!config.bankAccountId)
65
+ throw new Error("MantecaConfig.bankAccountId is required");
66
+ this.baseUrl = (config.baseUrl ?? DEFAULT_BASE_URL).replace(/\/+$/, "");
67
+ this.sellAsset = config.sellAsset ?? "USDC";
68
+ this.fiatAsset = config.fiatAsset ?? "ARS";
69
+ this.ticker = config.ticker ?? `${this.sellAsset}_${this.fiatAsset}`;
70
+ const f = config.fetchImpl ?? globalThis.fetch;
71
+ if (!f) throw new Error("no fetch available; pass MantecaConfig.fetchImpl");
72
+ this.fetchImpl = f;
73
+ this.now = config.now ?? Date.now;
74
+ }
75
+ config;
76
+ baseUrl;
77
+ sellAsset;
78
+ fiatAsset;
79
+ ticker;
80
+ fetchImpl;
81
+ now;
82
+ async request(method, path, body) {
83
+ const init = {
84
+ method,
85
+ headers: {
86
+ "md-api-key": this.config.apiKey,
87
+ "content-type": "application/json",
88
+ accept: "application/json"
89
+ }
90
+ };
91
+ if (body !== void 0) init.body = JSON.stringify(body);
92
+ let res;
93
+ try {
94
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
95
+ } catch (cause) {
96
+ throw new MantecaApiError(
97
+ `manteca ${method} ${path} transport error: ${String(cause)}`,
98
+ 0
99
+ );
100
+ }
101
+ const text = await res.text();
102
+ let parsed = void 0;
103
+ if (text) {
104
+ try {
105
+ parsed = JSON.parse(text);
106
+ } catch {
107
+ parsed = text;
108
+ }
109
+ }
110
+ if (!res.ok) {
111
+ const msg = `manteca ${method} ${path} -> ${res.status}`;
112
+ if (res.status === 401 || res.status === 403)
113
+ throw new MantecaAuthError(msg, res.status, parsed);
114
+ if (res.status === 429)
115
+ throw new MantecaRateLimitError(msg, res.status, parsed);
116
+ throw new MantecaApiError(msg, res.status, parsed);
117
+ }
118
+ return parsed;
119
+ }
120
+ /** Quote a USDC->ARS off-ramp using the direct sell-side price. */
121
+ async quote(amountUsd) {
122
+ const body = await this.request(
123
+ "GET",
124
+ `/v2/prices/direct/${encodeURIComponent(this.ticker)}`
125
+ );
126
+ const rate = parseDirectPrice(body, this.ticker);
127
+ if (rate === void 0 || rate <= 0) {
128
+ throw new MantecaApiError(
129
+ `manteca: could not parse a sell price for ${this.ticker}`,
130
+ 200,
131
+ body
132
+ );
133
+ }
134
+ return { amountUsd, arsOut: amountUsd * rate, rate, spread: 0 };
135
+ }
136
+ /**
137
+ * Fire the off-ramp synthetic: sell `amountUsd` of crypto and withdraw the ARS
138
+ * to the configured CVU. IRREVERSIBLE — gate behind requireConfirmation (RFC-001)
139
+ * and write to the signed audit log. Returns immediately with the synthetic id;
140
+ * the ARS settles asynchronously (poll getStatus). `arsReceived` is the EXPECTED
141
+ * amount at submission (from a fresh quote); the settled figure comes from getStatus.
142
+ */
143
+ async convert(amountUsd, opts) {
144
+ if (!opts?.externalId)
145
+ throw new Error("MantecaOffRampAdapter.convert: externalId (idempotency key) is required");
146
+ const q = await this.quote(amountUsd);
147
+ const externalId = opts.externalId;
148
+ const synthetic = await this.request(
149
+ "POST",
150
+ "/v2/synthetics/ramp-off",
151
+ {
152
+ userId: this.config.userId,
153
+ sellAmount: String(amountUsd),
154
+ sellAsset: this.sellAsset,
155
+ withdrawAsset: this.fiatAsset,
156
+ bankAccountId: this.config.bankAccountId,
157
+ externalId
158
+ }
159
+ );
160
+ const txId = synthetic.id ?? synthetic._id;
161
+ if (!txId) {
162
+ throw new MantecaApiError(
163
+ "manteca ramp-off: response had no synthetic id",
164
+ 200,
165
+ synthetic
166
+ );
167
+ }
168
+ return { amountUsd, arsReceived: q.arsOut, rate: q.rate, txId };
169
+ }
170
+ /** Poll a ramp-off synthetic and normalize its settlement state. */
171
+ async getStatus(txId) {
172
+ const body = await this.request(
173
+ "GET",
174
+ `/v2/synthetics/${encodeURIComponent(txId)}`
175
+ );
176
+ const rawStatus = typeof body.status === "string" && body.status || typeof body.state === "string" && body.state || void 0;
177
+ const arsSettled = num(body.withdrawAmount) ?? num(body.fiatAmount) ?? num(body.arsAmount) ?? void 0;
178
+ const report = {
179
+ txId,
180
+ status: normalizeMantecaStatus(rawStatus)
181
+ };
182
+ if (rawStatus !== void 0) report.raw = rawStatus;
183
+ if (arsSettled !== void 0) report.arsSettled = arsSettled;
184
+ return report;
185
+ }
186
+ /**
187
+ * One-time onboarding helper: register the society's CBU/CVU/alias as a payout
188
+ * destination. The Manteca user must have `legalId` set first. Returns the
189
+ * created account id to use as `bankAccountId`.
190
+ */
191
+ async registerBankAccount(input) {
192
+ const body = await this.request(
193
+ "POST",
194
+ "/v2/onboarding-actions/add-bank-account",
195
+ {
196
+ userId: this.config.userId,
197
+ accountNumber: input.cbuOrCvuOrAlias,
198
+ description: input.label ?? "Sociedad Automatizada CVU"
199
+ }
200
+ );
201
+ const id = typeof body.id === "string" && body.id || typeof body._id === "string" && body._id || typeof body.bankAccountId === "string" && body.bankAccountId || "";
202
+ return { bankAccountId: id, raw: body };
203
+ }
204
+ };
205
+
206
+ // src/ripio.ts
207
+ var RIPIO_SANDBOX = "https://sandbox-b2b.ripio.com";
208
+ var RIPIO_PROD = "https://b2b-api.ripio.com";
209
+ var RipioApiError = class extends Error {
210
+ constructor(message, status, body) {
211
+ super(message);
212
+ this.status = status;
213
+ this.body = body;
214
+ this.name = "RipioApiError";
215
+ }
216
+ status;
217
+ body;
218
+ };
219
+ var RipioAuthError = class extends RipioApiError {
220
+ constructor(message, status, body) {
221
+ super(message, status, body);
222
+ this.name = "RipioAuthError";
223
+ }
224
+ };
225
+ var RipioRateLimitError = class extends RipioApiError {
226
+ constructor(message, status, body) {
227
+ super(message, status, body);
228
+ this.name = "RipioRateLimitError";
229
+ }
230
+ };
231
+ function num2(v) {
232
+ if (typeof v === "number" && Number.isFinite(v)) return v;
233
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
234
+ return void 0;
235
+ }
236
+ function normalizeRipioStatus(raw) {
237
+ const s = (raw ?? "").toUpperCase();
238
+ if (["COMPLETED", "COMPLETE", "DONE", "SETTLED", "SUCCESS", "FINISHED", "PAID"].includes(s))
239
+ return "COMPLETED";
240
+ if (["FAILED", "FAILURE", "ERROR", "REJECTED", "CANCELLED", "CANCELED", "EXPIRED"].includes(s))
241
+ return "FAILED";
242
+ if (["PROCESSING", "IN_PROGRESS", "INPROGRESS", "RUNNING", "PARTIAL", "CONFIRMING"].includes(s))
243
+ return "PROCESSING";
244
+ if (["PENDING", "CREATED", "NEW", "QUEUED", "WAITING", "OPEN", "AWAITING_DEPOSIT"].includes(s))
245
+ return "PENDING";
246
+ return "UNKNOWN";
247
+ }
248
+ var RipioOffRampAdapter = class {
249
+ constructor(config) {
250
+ this.config = config;
251
+ if (!config.clientId || !config.clientSecret)
252
+ throw new Error("RipioConfig.clientId/clientSecret are required");
253
+ if (!config.customerId) throw new Error("RipioConfig.customerId is required");
254
+ if (!config.fiatAccountId) throw new Error("RipioConfig.fiatAccountId is required");
255
+ this.baseUrl = (config.baseUrl ?? RIPIO_SANDBOX).replace(/\/+$/, "");
256
+ this.chain = config.chain ?? "BASE";
257
+ this.fromCurrency = config.fromCurrency ?? "USDC";
258
+ this.toCurrency = config.toCurrency ?? "ARS";
259
+ this.paymentMethodType = config.paymentMethodType ?? "bank_transfer";
260
+ const f = config.fetchImpl ?? globalThis.fetch;
261
+ if (!f) throw new Error("no fetch available; pass RipioConfig.fetchImpl");
262
+ this.fetchImpl = f;
263
+ this.now = config.now ?? Date.now;
264
+ }
265
+ config;
266
+ baseUrl;
267
+ chain;
268
+ fromCurrency;
269
+ toCurrency;
270
+ paymentMethodType;
271
+ fetchImpl;
272
+ now;
273
+ token = null;
274
+ async getToken() {
275
+ const now = this.now();
276
+ if (this.token && this.token.expiresAtMs > now + 3e4) return this.token.value;
277
+ const basic = btoa(`${this.config.clientId}:${this.config.clientSecret}`);
278
+ let res;
279
+ try {
280
+ res = await this.fetchImpl(`${this.baseUrl}/oauth2/token/`, {
281
+ method: "POST",
282
+ headers: {
283
+ authorization: `Basic ${basic}`,
284
+ "content-type": "application/x-www-form-urlencoded",
285
+ accept: "application/json"
286
+ },
287
+ body: "grant_type=client_credentials"
288
+ });
289
+ } catch (cause) {
290
+ throw new RipioApiError(`ripio token transport error: ${String(cause)}`, 0);
291
+ }
292
+ const text = await res.text();
293
+ let parsed = void 0;
294
+ if (text) {
295
+ try {
296
+ parsed = JSON.parse(text);
297
+ } catch {
298
+ parsed = text;
299
+ }
300
+ }
301
+ if (!res.ok) throw new RipioAuthError(`ripio token -> ${res.status}`, res.status, parsed);
302
+ const body = parsed;
303
+ if (!body.access_token) throw new RipioAuthError("ripio token: no access_token", 200, parsed);
304
+ this.token = {
305
+ value: body.access_token,
306
+ expiresAtMs: now + (body.expires_in ?? 3600) * 1e3
307
+ };
308
+ return this.token.value;
309
+ }
310
+ async request(method, path, body) {
311
+ const token = await this.getToken();
312
+ const init = {
313
+ method,
314
+ headers: {
315
+ authorization: `Bearer ${token}`,
316
+ "content-type": "application/json",
317
+ accept: "application/json"
318
+ }
319
+ };
320
+ if (body !== void 0) init.body = JSON.stringify(body);
321
+ let res;
322
+ try {
323
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
324
+ } catch (cause) {
325
+ throw new RipioApiError(`ripio ${method} ${path} transport error: ${String(cause)}`, 0);
326
+ }
327
+ const text = await res.text();
328
+ let parsed = void 0;
329
+ if (text) {
330
+ try {
331
+ parsed = JSON.parse(text);
332
+ } catch {
333
+ parsed = text;
334
+ }
335
+ }
336
+ if (!res.ok) {
337
+ const msg = `ripio ${method} ${path} -> ${res.status}`;
338
+ if (res.status === 401 || res.status === 403) throw new RipioAuthError(msg, res.status, parsed);
339
+ if (res.status === 429) throw new RipioRateLimitError(msg, res.status, parsed);
340
+ throw new RipioApiError(msg, res.status, parsed);
341
+ }
342
+ return parsed;
343
+ }
344
+ async quote(amountUsd) {
345
+ const body = await this.request("POST", "/api/v1/quotes/", {
346
+ fromCurrency: this.fromCurrency,
347
+ toCurrency: this.toCurrency,
348
+ fromAmount: String(amountUsd),
349
+ chain: this.chain,
350
+ paymentMethodType: this.paymentMethodType
351
+ });
352
+ const arsOut = num2(body.finalToAmount) ?? num2(body.toAmount);
353
+ if (arsOut === void 0 || arsOut <= 0) {
354
+ throw new RipioApiError("ripio quote: could not parse toAmount", 200, body);
355
+ }
356
+ const rate = num2(body.rate) ?? arsOut / amountUsd;
357
+ return { amountUsd, arsOut, rate, spread: 0 };
358
+ }
359
+ /**
360
+ * Create an off-ramp session for `amountUsd`. Returns a receipt whose
361
+ * `depositAddress` is where the society must send the USDC to complete the
362
+ * payout; `txId` is the session id for getStatus. `arsReceived` is the EXPECTED
363
+ * amount (from a fresh quote); the settled figure comes from getStatus.
364
+ */
365
+ async convert(amountUsd, opts) {
366
+ if (!opts?.externalId)
367
+ throw new Error("RipioOffRampAdapter.convert: externalId (idempotency key) is required");
368
+ const q = await this.quote(amountUsd);
369
+ const session = await this.request(
370
+ "POST",
371
+ "/api/v1/offrampSession/",
372
+ {
373
+ fiatAccountId: this.config.fiatAccountId,
374
+ fromCurrency: this.fromCurrency,
375
+ toCurrency: this.toCurrency,
376
+ fromAmount: String(amountUsd),
377
+ chain: this.chain,
378
+ externalId: opts.externalId
379
+ }
380
+ );
381
+ const txId = typeof session.sessionId === "string" && session.sessionId || typeof session.id === "string" && session.id || "";
382
+ if (!txId) throw new RipioApiError("ripio offrampSession: no session id", 200, session);
383
+ const receipt = {
384
+ amountUsd,
385
+ arsReceived: q.arsOut,
386
+ rate: q.rate,
387
+ txId
388
+ };
389
+ const deposit = this.pickDepositAddress(session.depositAddresses);
390
+ if (deposit) receipt.depositAddress = deposit;
391
+ return receipt;
392
+ }
393
+ pickDepositAddress(raw) {
394
+ if (!Array.isArray(raw)) return void 0;
395
+ const entries = raw;
396
+ const match = entries.find((e) => (e.chain ?? "").toUpperCase() === this.chain.toUpperCase());
397
+ return (match ?? entries[0])?.address;
398
+ }
399
+ async getStatus(txId) {
400
+ const body = await this.request(
401
+ "GET",
402
+ `/api/v1/offrampSession/${encodeURIComponent(txId)}`
403
+ );
404
+ const rawStatus = typeof body.status === "string" && body.status || typeof body.state === "string" && body.state || void 0;
405
+ const arsSettled = num2(body.finalToAmount) ?? num2(body.toAmount) ?? num2(body.arsAmount);
406
+ const report = {
407
+ txId,
408
+ status: normalizeRipioStatus(rawStatus)
409
+ };
410
+ if (rawStatus !== void 0) report.raw = rawStatus;
411
+ if (arsSettled !== void 0) report.arsSettled = arsSettled;
412
+ return report;
413
+ }
414
+ /**
415
+ * One-time onboarding helper: register the society's CBU/CVU/alias as a fiat
416
+ * account payout destination. Returns the id to use as `fiatAccountId`.
417
+ */
418
+ async registerFiatAccount(input) {
419
+ const body = await this.request("POST", "/api/v1/fiatAccounts/", {
420
+ customerId: this.config.customerId,
421
+ paymentMethodType: this.paymentMethodType,
422
+ accountFields: { alias_or_cvu_destination: input.cbuOrCvuOrAlias }
423
+ });
424
+ const id = typeof body.id === "string" && body.id || typeof body.fiatAccountId === "string" && body.fiatAccountId || "";
425
+ return { fiatAccountId: id, raw: body };
426
+ }
427
+ };
428
+
429
+ // src/mural.ts
430
+ var MURAL_PROD = "https://api.muralpay.com";
431
+ var MURAL_SANDBOX = "https://api-staging.muralpay.com";
432
+ var MuralApiError = class extends Error {
433
+ constructor(message, status, body) {
434
+ super(message);
435
+ this.status = status;
436
+ this.body = body;
437
+ this.name = "MuralApiError";
438
+ }
439
+ status;
440
+ body;
441
+ };
442
+ var MuralAuthError = class extends MuralApiError {
443
+ constructor(message, status, body) {
444
+ super(message, status, body);
445
+ this.name = "MuralAuthError";
446
+ }
447
+ };
448
+ var MuralRateLimitError = class extends MuralApiError {
449
+ constructor(message, status, body) {
450
+ super(message, status, body);
451
+ this.name = "MuralRateLimitError";
452
+ }
453
+ };
454
+ function num3(v) {
455
+ if (typeof v === "number" && Number.isFinite(v)) return v;
456
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
457
+ return void 0;
458
+ }
459
+ function normalizeMuralStatus(requestStatus, fiatPayoutType) {
460
+ const f = (fiatPayoutType ?? "").toLowerCase();
461
+ if (f) {
462
+ if (f === "completed") return "COMPLETED";
463
+ if (f === "failed" || f === "canceled" || f === "cancelled") return "FAILED";
464
+ if (f === "created") return "PENDING";
465
+ if (["pending", "on-hold", "refundinprogress", "refunded"].includes(f)) return "PROCESSING";
466
+ }
467
+ const s = (requestStatus ?? "").toUpperCase();
468
+ if (s === "EXECUTED" || s === "PENDING") return "PROCESSING";
469
+ if (s === "AWAITING_EXECUTION") return "PENDING";
470
+ if (s === "FAILED" || s === "CANCELED" || s === "CANCELLED") return "FAILED";
471
+ return "UNKNOWN";
472
+ }
473
+ var MuralOffRampAdapter = class {
474
+ constructor(config) {
475
+ this.config = config;
476
+ if (!config.apiKey) throw new Error("MuralConfig.apiKey is required");
477
+ if (!config.transferApiKey) throw new Error("MuralConfig.transferApiKey is required");
478
+ if (!config.sourceAccountId) throw new Error("MuralConfig.sourceAccountId is required");
479
+ if (!config.cvu) throw new Error("MuralConfig.cvu is required");
480
+ if (!config.documentNumber) throw new Error("MuralConfig.documentNumber is required");
481
+ this.baseUrl = (config.baseUrl ?? MURAL_PROD).replace(/\/+$/, "");
482
+ this.tokenSymbol = config.tokenSymbol ?? "USDC";
483
+ this.fiatRailCode = config.fiatRailCode ?? "ars";
484
+ const f = config.fetchImpl ?? globalThis.fetch;
485
+ if (!f) throw new Error("no fetch available; pass MuralConfig.fetchImpl");
486
+ this.fetchImpl = f;
487
+ this.now = config.now ?? Date.now;
488
+ }
489
+ config;
490
+ baseUrl;
491
+ tokenSymbol;
492
+ fiatRailCode;
493
+ fetchImpl;
494
+ now;
495
+ async request(method, path, body, extraHeaders) {
496
+ const headers = {
497
+ authorization: `Bearer ${this.config.apiKey}`,
498
+ "content-type": "application/json",
499
+ accept: "application/json",
500
+ ...this.config.organizationId ? { "on-behalf-of": this.config.organizationId } : {},
501
+ ...extraHeaders
502
+ };
503
+ const init = { method, headers };
504
+ if (body !== void 0) init.body = JSON.stringify(body);
505
+ let res;
506
+ try {
507
+ res = await this.fetchImpl(`${this.baseUrl}${path}`, init);
508
+ } catch (cause) {
509
+ throw new MuralApiError(`mural ${method} ${path} transport error: ${String(cause)}`, 0);
510
+ }
511
+ const text = await res.text();
512
+ let parsed = void 0;
513
+ if (text) {
514
+ try {
515
+ parsed = JSON.parse(text);
516
+ } catch {
517
+ parsed = text;
518
+ }
519
+ }
520
+ if (!res.ok) {
521
+ const msg = `mural ${method} ${path} -> ${res.status}`;
522
+ if (res.status === 401 || res.status === 403) throw new MuralAuthError(msg, res.status, parsed);
523
+ if (res.status === 429) throw new MuralRateLimitError(msg, res.status, parsed);
524
+ throw new MuralApiError(msg, res.status, parsed);
525
+ }
526
+ return parsed;
527
+ }
528
+ /** Quote a USDC->ARS off-ramp via the token-to-fiat fees endpoint. */
529
+ async quote(amountUsd) {
530
+ const body = {
531
+ tokenFeeRequests: [
532
+ {
533
+ amount: { tokenAmount: amountUsd, tokenSymbol: this.tokenSymbol },
534
+ fiatAndRailCode: this.fiatRailCode
535
+ }
536
+ ]
537
+ };
538
+ const res = await this.request("POST", "/api/payouts/fees/token-to-fiat", body);
539
+ const first = Array.isArray(res) ? res[0] : void 0;
540
+ if (!first) throw new MuralApiError("mural fees: empty response", 200, res);
541
+ if (first.type === "error") {
542
+ throw new MuralApiError(`mural fees error: ${String(first.message ?? "unknown")}`, 200, first);
543
+ }
544
+ const est = first.estimatedFiatAmount;
545
+ const arsOut = num3(est?.fiatAmount);
546
+ const rate = num3(first.exchangeRate);
547
+ if (arsOut === void 0 || rate === void 0 || rate <= 0) {
548
+ throw new MuralApiError("mural fees: could not parse rate/estimate", 200, first);
549
+ }
550
+ const spread = num3(first.exchangeFeePercentage) ?? 0;
551
+ return { amountUsd, arsOut, rate, spread };
552
+ }
553
+ /**
554
+ * Create + execute the off-ramp payout: convert `amountUsd` USDC and pay ARS to
555
+ * the configured CBU/CVU. IRREVERSIBLE — gate behind requireConfirmation (RFC-001)
556
+ * and write to the signed audit log. Returns the payout-request id as txId;
557
+ * `arsReceived` is the EXPECTED amount (from a fresh quote), the settled figure
558
+ * comes from getStatus.
559
+ */
560
+ async convert(amountUsd, opts) {
561
+ if (!opts?.externalId)
562
+ throw new Error("MuralOffRampAdapter.convert: externalId (idempotency key) is required");
563
+ const q = await this.quote(amountUsd);
564
+ const memo = opts.externalId;
565
+ const isBusiness = (this.config.recipient.type ?? "business") === "business";
566
+ const recipientInfo = isBusiness ? {
567
+ type: "business",
568
+ name: this.config.recipient.name ?? this.config.bankAccountOwner,
569
+ ...this.config.recipient.email ? { email: this.config.recipient.email } : {},
570
+ physicalAddress: this.config.recipient.physicalAddress
571
+ } : {
572
+ type: "individual",
573
+ firstName: this.config.recipient.firstName,
574
+ lastName: this.config.recipient.lastName,
575
+ ...this.config.recipient.email ? { email: this.config.recipient.email } : {},
576
+ physicalAddress: this.config.recipient.physicalAddress
577
+ };
578
+ const created = await this.request(
579
+ "POST",
580
+ "/api/payouts/payout",
581
+ {
582
+ sourceAccountId: this.config.sourceAccountId,
583
+ memo,
584
+ payouts: [
585
+ {
586
+ amount: { tokenAmount: amountUsd, tokenSymbol: this.tokenSymbol },
587
+ payoutDetails: {
588
+ type: "fiat",
589
+ bankName: this.config.bankName,
590
+ bankAccountOwner: this.config.bankAccountOwner,
591
+ fiatAndRailDetails: {
592
+ type: "ars",
593
+ symbol: "ARS",
594
+ bankAccountNumber: this.config.cvu,
595
+ documentNumber: this.config.documentNumber,
596
+ bankAccountNumberType: this.config.cvuType ?? "CVU"
597
+ }
598
+ },
599
+ recipientInfo
600
+ }
601
+ ]
602
+ }
603
+ );
604
+ const id = created.id;
605
+ if (!id) throw new MuralApiError("mural payout: response had no id", 200, created);
606
+ await this.request(
607
+ "POST",
608
+ `/api/payouts/payout/${encodeURIComponent(id)}/execute`,
609
+ void 0,
610
+ { "transfer-api-key": this.config.transferApiKey }
611
+ );
612
+ return { amountUsd, arsReceived: q.arsOut, rate: q.rate, txId: id };
613
+ }
614
+ /** Poll a payout request and normalize its settlement state. */
615
+ async getStatus(txId) {
616
+ const body = await this.request(
617
+ "GET",
618
+ `/api/payouts/payout/${encodeURIComponent(txId)}`
619
+ );
620
+ const requestStatus = typeof body.status === "string" ? body.status : void 0;
621
+ const payouts = Array.isArray(body.payouts) ? body.payouts : [];
622
+ const first = payouts[0];
623
+ const details = first?.details;
624
+ const fiatStatus = details?.fiatPayoutStatus;
625
+ const fiatType = typeof fiatStatus?.type === "string" ? fiatStatus.type : void 0;
626
+ const fiatAmount = details?.fiatAmount;
627
+ const arsSettled = fiatType === "completed" ? num3(fiatAmount?.fiatAmount) : void 0;
628
+ const report = {
629
+ txId,
630
+ status: normalizeMuralStatus(requestStatus, fiatType)
631
+ };
632
+ const raw = fiatType ?? requestStatus;
633
+ if (raw !== void 0) report.raw = raw;
634
+ if (arsSettled !== void 0) report.arsSettled = arsSettled;
635
+ return report;
636
+ }
637
+ };
638
+
639
+ // src/afip.ts
640
+ var WSCREATEVEP_IS_GOV_ONLY = "AFIP/ARCA WSCREATEVEP is enabled only for public organisms (organismos recaudadores), not private taxpayers. Do not build VEP creation on it. See TREASURY-FISCAL-RAIL.md \xA73.";
641
+ var MONOTRIBUTO_TABLE_EFFECTIVE = "2026-02-01";
642
+ var MONOTRIBUTO_2026 = [
643
+ { category: "A", annualCapArs: 1027798813e-2, cuotaServicios: 42386.74, cuotaBienes: 42386.74, bienesOnly: false },
644
+ { category: "B", annualCapArs: 1505844771e-2, cuotaServicios: 48250.78, cuotaBienes: 48250.78, bienesOnly: false },
645
+ { category: "C", annualCapArs: 2111369652e-2, cuotaServicios: 56501.85, cuotaBienes: 55227.06, bienesOnly: false },
646
+ { category: "D", annualCapArs: 2621285342e-2, cuotaServicios: 72414.1, cuotaBienes: 70661.26, bienesOnly: false },
647
+ { category: "E", annualCapArs: 3083396437e-2, cuotaServicios: 102537.97, cuotaBienes: 92658.35, bienesOnly: false },
648
+ { category: "F", annualCapArs: 3864204836e-2, cuotaServicios: 129045.32, cuotaBienes: 111198.27, bienesOnly: false },
649
+ { category: "G", annualCapArs: 4621110937e-2, cuotaServicios: 197108.23, cuotaBienes: 135918.34, bienesOnly: false },
650
+ { category: "H", annualCapArs: 7011340733e-2, cuotaServicios: 447346.93, cuotaBienes: 272063.4, bienesOnly: false },
651
+ { category: "I", annualCapArs: 7847921162e-2, cuotaServicios: 824802.26, cuotaBienes: 406512.05, bienesOnly: true },
652
+ { category: "J", annualCapArs: 898726403e-1, cuotaServicios: 999007.65, cuotaBienes: 497059.41, bienesOnly: true },
653
+ { category: "K", annualCapArs: 10835708405e-2, cuotaServicios: 13816879e-1, cuotaBienes: 600879.51, bienesOnly: true }
654
+ ];
655
+ function monotributoCuota(category, activity) {
656
+ const row = MONOTRIBUTO_2026.find((r) => r.category === category);
657
+ if (!row) throw new Error(`unknown monotributo category: ${category}`);
658
+ if (row.bienesOnly && activity === "servicios") {
659
+ throw new Error(
660
+ `category ${category} is only available for venta de bienes (servicios caps at H)`
661
+ );
662
+ }
663
+ return activity === "servicios" ? row.cuotaServicios : row.cuotaBienes;
664
+ }
665
+ function categoryForAnnualIncome(annualArs, activity) {
666
+ const rows = MONOTRIBUTO_2026.filter(
667
+ (r) => activity === "bienes" || !r.bienesOnly
668
+ );
669
+ for (const r of rows) {
670
+ if (annualArs <= r.annualCapArs) return r.category;
671
+ }
672
+ return null;
673
+ }
674
+ function settlementPlan(obligation, method) {
675
+ const base = {
676
+ amountArs: obligation.amountArs,
677
+ dueAtMs: obligation.dueAtMs,
678
+ canAutoExecute: false
679
+ };
680
+ switch (method) {
681
+ case "debito_automatico":
682
+ return {
683
+ ...base,
684
+ method,
685
+ autonomy: "passive",
686
+ instruction: "Manten\xE9 al menos ARS " + obligation.amountArs.toFixed(2) + " en el CVU antes del vencimiento; el d\xE9bito autom\xE1tico cobra la cuota solo.",
687
+ oneTimeSetup: "Adher\xED el CBU/CVU al d\xE9bito autom\xE1tico en el Portal Monotributo de ARCA (paso \xFAnico, no hay API; el alta debe estar antes del d\xEDa 20 para el d\xE9bito del d\xEDa 7 siguiente)."
688
+ };
689
+ case "vep_manual":
690
+ return {
691
+ ...base,
692
+ method,
693
+ autonomy: "human-required",
694
+ instruction: "Gener\xE1 el VEP/QR en ARCA (Mis Aplicaciones) y pagalo escaneando el QR o ingresando CUIT + n\xFAmero de VEP en tu billetera. El agente no puede pagar el VEP por API.",
695
+ oneTimeSetup: ""
696
+ };
697
+ case "mp_manual":
698
+ return {
699
+ ...base,
700
+ method,
701
+ autonomy: "human-required",
702
+ instruction: "Gener\xE1 el VEP en ARCA y pagalo en Mercado Pago (Cuentas y servicios \u2192 AFIP VEP, o escane\xE1 el QR). No existe API de Mercado Pago para pagar un VEP.",
703
+ oneTimeSetup: ""
704
+ };
705
+ }
706
+ }
707
+
708
+ // src/index.ts
709
+ var ZERO_STATE = { usd: 0, ars: 0, costBasisPerUsd: 1 };
710
+ var CEDULAR_RATE = { ARS: 0.05, FOREIGN: 0.15 };
711
+ function cedularTax(amountUsd, costBasisPerUsd, fxRate, denom = "ARS") {
712
+ const proceeds = amountUsd * fxRate;
713
+ const cost = amountUsd * costBasisPerUsd * fxRate;
714
+ const gain = Math.max(0, proceeds - cost);
715
+ return gain * CEDULAR_RATE[denom];
716
+ }
717
+ function nextObligation(obligations, nowMs) {
718
+ const upcoming = obligations.filter((o) => o.dueAtMs >= nowMs).sort((a, b) => a.dueAtMs - b.dueAtMs);
719
+ return upcoming[0] ?? null;
720
+ }
721
+ function requiredArsBuffer(obligations, nowMs, horizonMs, safety = 1.1) {
722
+ const due = obligations.filter((o) => o.dueAtMs >= nowMs && o.dueAtMs <= nowMs + horizonMs);
723
+ const total = due.reduce((sum, o) => sum + o.amountArs, 0);
724
+ return total * safety;
725
+ }
726
+ function planConversion(state, requiredArs, fxRate, spread = 0.01) {
727
+ const shortfall = requiredArs - state.ars;
728
+ if (shortfall <= 0) {
729
+ return { convertUsd: 0, expectedArs: 0, reason: "ars buffer sufficient" };
730
+ }
731
+ const effectiveRate = fxRate * (1 - spread);
732
+ if (effectiveRate <= 0 || state.usd <= 0) {
733
+ return { convertUsd: 0, expectedArs: 0, reason: "no usd available or invalid rate" };
734
+ }
735
+ const neededUsd = shortfall / effectiveRate;
736
+ const convertUsd = Math.min(neededUsd, state.usd);
737
+ const expectedArs = convertUsd * effectiveRate;
738
+ return {
739
+ convertUsd,
740
+ expectedArs,
741
+ reason: convertUsd < neededUsd ? "partial: usd insufficient for full buffer" : "top up to buffer"
742
+ };
743
+ }
744
+ function applyConversion(state, receipt) {
745
+ return {
746
+ usd: state.usd - receipt.amountUsd,
747
+ ars: state.ars + receipt.arsReceived,
748
+ costBasisPerUsd: state.costBasisPerUsd
749
+ };
750
+ }
751
+ function applyPayment(state, amountArs) {
752
+ if (amountArs > state.ars) {
753
+ throw new Error(
754
+ `insufficient ARS: need ${amountArs.toFixed(2)}, have ${state.ars.toFixed(2)}`
755
+ );
756
+ }
757
+ return { ...state, ars: state.ars - amountArs };
758
+ }
759
+ var InMemoryOffRampAdapter = class {
760
+ constructor(rate, spread = 0.01) {
761
+ this.rate = rate;
762
+ this.spread = spread;
763
+ }
764
+ rate;
765
+ spread;
766
+ settled = /* @__PURE__ */ new Map();
767
+ async quote(amountUsd) {
768
+ return {
769
+ amountUsd,
770
+ arsOut: amountUsd * this.rate * (1 - this.spread),
771
+ rate: this.rate,
772
+ spread: this.spread
773
+ };
774
+ }
775
+ async convert(amountUsd, opts) {
776
+ if (!opts?.externalId)
777
+ throw new Error("InMemoryOffRampAdapter.convert: externalId (idempotency key) is required");
778
+ const cached = this.settled.get(opts.externalId);
779
+ if (cached) return cached;
780
+ const q = await this.quote(amountUsd);
781
+ const receipt = {
782
+ amountUsd,
783
+ arsReceived: q.arsOut,
784
+ rate: this.rate * (1 - this.spread),
785
+ txId: `mem-${opts.externalId}`
786
+ };
787
+ this.settled.set(opts.externalId, receipt);
788
+ return receipt;
789
+ }
790
+ /** The in-memory adapter settles instantly: any tx it issued is COMPLETED. */
791
+ async getStatus(txId) {
792
+ return { txId, status: "COMPLETED", raw: "in-memory" };
793
+ }
794
+ };
795
+ async function fundTaxBuffer(args) {
796
+ const required = requiredArsBuffer(args.obligations, args.nowMs, args.horizonMs, args.safety);
797
+ const plan = planConversion(args.state, required, args.fxRate, args.spread);
798
+ if (plan.convertUsd <= 0 || !args.offramp) {
799
+ return { plan, state: args.state };
800
+ }
801
+ const externalId = args.externalId ?? `fund-${args.obligations.map((o) => o.id).join("+")}-${plan.convertUsd.toFixed(2)}`;
802
+ const receipt = await args.offramp.convert(plan.convertUsd, { externalId });
803
+ return { plan, receipt, state: applyConversion(args.state, receipt) };
804
+ }
805
+
806
+ export { CEDULAR_RATE, InMemoryOffRampAdapter, MONOTRIBUTO_2026, MONOTRIBUTO_TABLE_EFFECTIVE, MURAL_PROD, MURAL_SANDBOX, MantecaApiError, MantecaAuthError, MantecaOffRampAdapter, MantecaRateLimitError, MuralApiError, MuralAuthError, MuralOffRampAdapter, MuralRateLimitError, RIPIO_PROD, RIPIO_SANDBOX, RipioApiError, RipioAuthError, RipioOffRampAdapter, RipioRateLimitError, WSCREATEVEP_IS_GOV_ONLY, ZERO_STATE, applyConversion, applyPayment, categoryForAnnualIncome, cedularTax, fundTaxBuffer, monotributoCuota, nextObligation, normalizeMuralStatus, normalizeRipioStatus, planConversion, requiredArsBuffer, settlementPlan };
807
+ //# sourceMappingURL=index.js.map
808
+ //# sourceMappingURL=index.js.map