@spield/sdk 0.4.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,1474 @@
1
+ // src/errors.ts
2
+ var SpieldSdkError = class extends Error {
3
+ constructor(message, code, cause) {
4
+ super(message);
5
+ this.code = code;
6
+ this.cause = cause;
7
+ this.name = "SpieldSdkError";
8
+ }
9
+ code;
10
+ cause;
11
+ };
12
+ var SpieldValidationError = class extends SpieldSdkError {
13
+ constructor(message, cause) {
14
+ super(message, "validation_error", cause);
15
+ this.name = "SpieldValidationError";
16
+ }
17
+ };
18
+ var SpieldSignerError = class extends SpieldSdkError {
19
+ constructor(message, cause) {
20
+ super(message, "signer_error", cause);
21
+ this.name = "SpieldSignerError";
22
+ }
23
+ };
24
+ var SpieldRpcError = class extends SpieldSdkError {
25
+ constructor(message, cause) {
26
+ super(message, "rpc_error", cause);
27
+ this.name = "SpieldRpcError";
28
+ }
29
+ };
30
+ var SpieldContractError = class extends SpieldSdkError {
31
+ constructor(message, cause) {
32
+ super(message, "contract_error", cause);
33
+ this.name = "SpieldContractError";
34
+ }
35
+ };
36
+ var SpieldConfigurationError = class extends SpieldSdkError {
37
+ constructor(message, cause) {
38
+ super(message, "configuration_error", cause);
39
+ this.name = "SpieldConfigurationError";
40
+ }
41
+ };
42
+
43
+ // src/amount.ts
44
+ var toBaseUnits = (amount, decimals = 7) => {
45
+ if (typeof amount === "bigint") return amount;
46
+ const s = String(amount).trim();
47
+ if (!/^-?\d+(\.\d+)?$/.test(s)) {
48
+ throw new SpieldValidationError(`Invalid token amount: ${String(amount)}`);
49
+ }
50
+ const neg = s.startsWith("-");
51
+ const [wholeRaw, fracRaw = ""] = s.replace("-", "").split(".");
52
+ if (fracRaw.length > decimals) {
53
+ throw new SpieldValidationError(`Amount has more than ${decimals} decimal places.`);
54
+ }
55
+ const scale = 10n ** BigInt(decimals);
56
+ const whole = BigInt(wholeRaw || "0") * scale;
57
+ const frac = BigInt((fracRaw + "0".repeat(decimals)).slice(0, decimals) || "0");
58
+ const value = whole + frac;
59
+ return neg ? -value : value;
60
+ };
61
+ var assertPositiveAmount = (amount, decimals = 7) => {
62
+ const raw = toBaseUnits(amount, decimals);
63
+ if (raw <= 0n) throw new SpieldValidationError("Amount must be greater than zero.");
64
+ return raw;
65
+ };
66
+ var assertNonNegativeAmount = (amount, decimals = 7) => {
67
+ const raw = toBaseUnits(amount, decimals);
68
+ if (raw < 0n) throw new SpieldValidationError("Amount must be zero or greater.");
69
+ return raw;
70
+ };
71
+ var fromBaseUnits = (units, decimals = 7) => {
72
+ const value = BigInt(units);
73
+ const sign = value < 0n ? "-" : "";
74
+ const abs = value < 0n ? -value : value;
75
+ const scale = 10n ** BigInt(decimals);
76
+ const whole = abs / scale;
77
+ const frac = (abs % scale).toString().padStart(decimals, "0").replace(/0+$/, "");
78
+ return `${sign}${whole.toString()}${frac ? `.${frac}` : ""}`;
79
+ };
80
+ var tokenAmount = (raw, symbol, decimals = 7) => ({
81
+ raw: BigInt(raw),
82
+ formatted: fromBaseUnits(raw, decimals),
83
+ decimals,
84
+ symbol
85
+ });
86
+
87
+ // src/soroban.ts
88
+ import {
89
+ Account,
90
+ Address,
91
+ BASE_FEE,
92
+ Contract,
93
+ TransactionBuilder,
94
+ nativeToScVal,
95
+ rpc,
96
+ scValToNative
97
+ } from "@stellar/stellar-sdk";
98
+
99
+ // src/config.ts
100
+ var STELLAR_NETWORK_PASSPHRASES = {
101
+ PUBLIC: "Public Global Stellar Network ; September 2015",
102
+ TESTNET: "Test SDF Network ; September 2015"
103
+ };
104
+ var testnet = () => ({
105
+ key: "testnet",
106
+ networkName: "TESTNET",
107
+ networkPassphrase: STELLAR_NETWORK_PASSPHRASES.TESTNET,
108
+ rpcUrl: "https://soroban-testnet.stellar.org",
109
+ horizonUrl: "https://horizon-testnet.stellar.org",
110
+ explorerUrl: "https://stellar.expert/explorer/testnet",
111
+ contracts: {
112
+ sr: "CDYAM3NGY5I3SUGPCDQUS25MGCIWT2YOBDSWYT6SJNIPN6A6OOUSSCZY",
113
+ strategy: "CDPNSWSBVBRF52SED6UD7T2VQH6XODLEHXSCFTZHQP73SNYTKIHP5R2B",
114
+ yieldEngine: "CDS2Q6L3QCUK4KX633M7QH53GC76EVOUAK7WJ54T3AP3J6IGXIM3LURD",
115
+ market: "CBL7Z3BONITNSWO7NJLT67464HLVVQF5G3REI3XT6KUCK7YNHPICJX5A",
116
+ vault: "CDKV7Z7FF3DA57LSO2JA6GFKAIDDIDMZD5XWCMV7G5I3E4ZA3NN2NMH7",
117
+ router: "CDP3VUYH3GEGNOF4XUHKMP5GBTH3SBCL5R3GM5ZTVQAGV7FOAPAQJTGE",
118
+ pt: "CB3T6FOMAH77Z2FMSA2IEVLQEIYOQRRFP7JMJMJGMUCO6HGOOZJZJ7OC",
119
+ usdc: "CAQCFVLOBK5GIULPNZRGATJJMIZL5BSP7X5YJVMGCPTUEPFM4AVSRCJU"
120
+ },
121
+ assets: {
122
+ pt: { code: "SPLDPT7", issuer: "GDTM2UMJEO6LV5HE2SI56IEWNX5OAF5HV2XNZMVZEDXMMPHZUWXSSLQU" }
123
+ },
124
+ decimals: 7
125
+ });
126
+ var customNetwork = (config) => config;
127
+ var explorerTx = (network, hash) => `${network.explorerUrl}/tx/${hash}`;
128
+
129
+ // src/soroban.ts
130
+ var DEFAULT_SIMULATION_SOURCE = "GBNK7ZOQIZL3HM2LPY7WWJJL3C5YCOEUIJAF4UBSPIEWCEIC2HFSBVVI";
131
+ var DEFAULT_CONFIRMATION_TIMEOUT_MS = 12e4;
132
+ var DEFAULT_CONFIRMATION_POLL_INTERVAL_MS = 1e3;
133
+ var createRpcServer = (network) => new rpc.Server(network.rpcUrl, { allowHttp: network.rpcUrl.startsWith("http://") });
134
+ var i128 = (v) => nativeToScVal(v, { type: "i128" });
135
+ var u64 = (v) => nativeToScVal(BigInt(v), { type: "u64" });
136
+ var u32 = (v) => nativeToScVal(v, { type: "u32" });
137
+ var addr = (a) => new Address(a).toScVal();
138
+ var toBig = (v) => {
139
+ if (typeof v === "bigint") return v;
140
+ if (typeof v === "number") return BigInt(Math.trunc(v));
141
+ if (typeof v === "string") return BigInt(v);
142
+ return 0n;
143
+ };
144
+ var toNum = (v) => Number(toBig(v));
145
+ var SorobanClient = class {
146
+ constructor(context) {
147
+ this.context = context;
148
+ this.server = createRpcServer(context.network);
149
+ }
150
+ context;
151
+ server;
152
+ async read(contractId, method, args = []) {
153
+ const contract = new Contract(contractId);
154
+ const account = new Account(this.context.simulationSource, "0");
155
+ const tx = new TransactionBuilder(account, {
156
+ fee: BASE_FEE,
157
+ networkPassphrase: this.context.network.networkPassphrase
158
+ }).addOperation(contract.call(method, ...args)).setTimeout(30).build();
159
+ let sim;
160
+ try {
161
+ sim = await this.server.simulateTransaction(tx);
162
+ } catch (err) {
163
+ throw new SpieldContractError(
164
+ `${method} response could not be parsed (SDK/protocol version mismatch): ${err instanceof Error ? err.message : String(err)}`,
165
+ err
166
+ );
167
+ }
168
+ if (rpc.Api.isSimulationError(sim)) {
169
+ throw new SpieldContractError(`${method} failed: ${sim.error}`, sim.error);
170
+ }
171
+ const retval = sim.result?.retval;
172
+ return retval ? scValToNative(retval) : void 0;
173
+ }
174
+ /**
175
+ * **Simulate a value-moving entry point AS `invoker`, and return what it would actually pay.**
176
+ *
177
+ * The `quote_*` views cannot synchronize SR — a view may not write — so they price on SR's stored
178
+ * high-water rate, which lags whenever nothing has synced since the last mutation. The real entry
179
+ * points *do* synchronize (`FINAL_CHECK.md` V2-01), so a quote can OVER-state a sale: derive
180
+ * `minOut` from it and the trade can trip its own slippage bound and revert.
181
+ *
182
+ * Simulating the real function runs the same synchronized code the submission will. Nothing is
183
+ * signed and nothing is sent.
184
+ *
185
+ * Unlike {@link read}, this simulates as the **user** rather than the shared simulation source,
186
+ * because the result depends on their balances.
187
+ *
188
+ * Returns `null` whenever the simulation cannot run — an unaffordable size, a route the pool
189
+ * cannot fill, a deployment predating the entry point — so callers fall back to the quote instead
190
+ * of failing the trade.
191
+ */
192
+ async simulateAs(invoker, contractId, method, args = []) {
193
+ try {
194
+ const tx = new TransactionBuilder(new Account(invoker, "0"), {
195
+ fee: BASE_FEE,
196
+ networkPassphrase: this.context.network.networkPassphrase
197
+ }).addOperation(new Contract(contractId).call(method, ...args)).setTimeout(30).build();
198
+ const sim = await this.server.simulateTransaction(tx);
199
+ if (rpc.Api.isSimulationError(sim)) return null;
200
+ const retval = sim.result?.retval;
201
+ if (!retval) return null;
202
+ const native = scValToNative(retval);
203
+ return typeof native === "bigint" ? native : BigInt(String(native));
204
+ } catch {
205
+ return null;
206
+ }
207
+ }
208
+ async write(contractId, method, args = [], signer = this.context.signer) {
209
+ if (!signer) throw new SpieldSignerError("A signer is required for write transactions.");
210
+ const walletAddress = await signer.getAddress();
211
+ const contract = new Contract(contractId);
212
+ const sourceAccount = await this.server.getAccount(walletAddress);
213
+ const built = new TransactionBuilder(sourceAccount, {
214
+ fee: BASE_FEE,
215
+ networkPassphrase: this.context.network.networkPassphrase
216
+ }).addOperation(contract.call(method, ...args)).setTimeout(60).build();
217
+ const sim = await this.server.simulateTransaction(built);
218
+ if (rpc.Api.isSimulationError(sim)) {
219
+ throw new SpieldContractError(parseContractError(sim.error, method, this.context.network), sim.error);
220
+ }
221
+ const prepared = rpc.assembleTransaction(built, sim).build();
222
+ const { signedTxXdr } = await signer.signTransaction(prepared.toXDR(), {
223
+ networkPassphrase: this.context.network.networkPassphrase,
224
+ address: walletAddress
225
+ });
226
+ const signed = TransactionBuilder.fromXDR(signedTxXdr, this.context.network.networkPassphrase);
227
+ const sent = await this.server.sendTransaction(signed);
228
+ if (sent.status === "ERROR") {
229
+ throw new SpieldRpcError(`Submission failed: ${JSON.stringify(sent.errorResult)}`, sent.errorResult);
230
+ }
231
+ return this.waitForConfirmation(sent.hash);
232
+ }
233
+ async waitForConfirmation(hash) {
234
+ const timeoutMs = positiveMs(this.context.confirmationTimeoutMs, DEFAULT_CONFIRMATION_TIMEOUT_MS);
235
+ const pollIntervalMs = positiveMs(this.context.confirmationPollIntervalMs, DEFAULT_CONFIRMATION_POLL_INTERVAL_MS);
236
+ const deadline = Date.now() + timeoutMs;
237
+ let lastRpcError;
238
+ do {
239
+ try {
240
+ const result = await this.server.getTransaction(hash);
241
+ if (result.status === rpc.Api.GetTransactionStatus.SUCCESS) {
242
+ return { hash, status: "success", explorerUrl: explorerTx(this.context.network, hash) };
243
+ }
244
+ if (result.status === rpc.Api.GetTransactionStatus.FAILED) {
245
+ throw new SpieldRpcError(`Transaction failed on-chain: ${hash}`, result);
246
+ }
247
+ } catch (err) {
248
+ if (err instanceof SpieldRpcError) throw err;
249
+ lastRpcError = err;
250
+ }
251
+ const remaining = deadline - Date.now();
252
+ if (remaining <= 0) break;
253
+ await sleep(Math.min(pollIntervalMs, remaining));
254
+ } while (Date.now() <= deadline);
255
+ throw new SpieldRpcError(
256
+ `Timed out waiting ${timeoutMs}ms for transaction confirmation: ${hash}. The transaction was submitted and may still settle; check the explorer before resubmitting.`,
257
+ lastRpcError
258
+ );
259
+ }
260
+ };
261
+ var sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
262
+ var positiveMs = (value, fallback) => Number.isFinite(value) && (value ?? 0) > 0 ? Math.floor(value) : fallback;
263
+ var CONTRACT_ERRORS = {
264
+ 1: "The contract is already initialized.",
265
+ 2: "The contract is not initialized yet.",
266
+ 3: "The caller is not authorized for this operation.",
267
+ 4: "The protocol is currently paused.",
268
+ 5: "Amount must be greater than zero.",
269
+ 6: "The operation overflowed.",
270
+ 11: "The token does not use the 7 decimals the protocol is calibrated for.",
271
+ 22: "PT can only be redeemed at or after maturity.",
272
+ 23: "Insufficient balance for this operation.",
273
+ 24: "The operation would break the solvency invariant and was refused.",
274
+ 40: "Blend reported a rate outside the configured sanity bound.",
275
+ 41: "The strategy holds no Blend position for this asset.",
276
+ 42: "Blend returned less underlying than required. Try a smaller amount.",
277
+ 60: "That vault receipt does not exist.",
278
+ 61: "The caller does not own that vault receipt.",
279
+ 62: "The vault has not matured yet.",
280
+ 63: "That vault receipt has already been redeemed.",
281
+ 64: "The vault lacks the spare PT to back a coupon for this deposit.",
282
+ 65: "The quoted fixed rate is outside the allowed range.",
283
+ 66: "The vault series has expired; deposits are closed.",
284
+ 80: "Not enough market liquidity for this trade.",
285
+ 81: "The output fell below your slippage floor and the trade was reverted.",
286
+ 82: "The market has expired; PT now redeems at face value instead of trading.",
287
+ 83: "You do not hold that many LP shares.",
288
+ 84: "Liquidity must be added in the pool's current PT/SR proportion.",
289
+ 100: "The spender allowance is smaller than the amount being moved.",
290
+ 101: "The series has expired; this operation is only valid before expiry.",
291
+ 102: "This operation is only valid at or after the series expiry.",
292
+ 103: "The caller is not the contract permitted to drive this entrypoint.",
293
+ 105: "The output fell below your minimum and the transaction was reverted.",
294
+ 106: "The amount rounds to zero at the current exchange rate. Use a larger amount.",
295
+ 107: "The deposit would exceed the SR deposit cap. Check the remaining headroom."
296
+ };
297
+ var parseContractError = (raw, method, network) => {
298
+ const text = raw || "";
299
+ const events = [...text.matchAll(
300
+ /contract:(C[A-Z2-7]{55}), topics:\[error, Error\(Contract, #(\d+)\)\], data:(?:"([^"]*)"|\[)/g
301
+ )];
302
+ const root = events.at(-1);
303
+ const rootContract = root?.[1];
304
+ const rootCode = root ? Number(root[2]) : void 0;
305
+ const rootMessage = root?.[3];
306
+ const assetOf = (id) => id === network.contracts.usdc ? "USDC" : id === network.contracts.pt ? "PT" : void 0;
307
+ if (/trustline entry is missing/.test(text)) {
308
+ const asset = assetOf(rootContract) ?? assetOf(/contract:(C[A-Z2-7]{55})[^\n]*trustline entry is missing/.exec(text)?.[1]);
309
+ if (asset === "PT") {
310
+ return "The wallet needs a PT trustline before it can hold PT. Call tokens.ensurePtTrustline().";
311
+ }
312
+ if (asset === "USDC") {
313
+ return "The wallet needs a USDC trustline (and a USDC balance) for this transaction.";
314
+ }
315
+ return "The wallet is missing a required asset trustline for this transaction.";
316
+ }
317
+ const isSpieldContract = !!rootContract && ["sr", "strategy", "yieldEngine", "market", "vault", "router"].some((key) => network.contracts[key] === rootContract);
318
+ if (rootCode !== void 0 && isSpieldContract) {
319
+ const mapped = CONTRACT_ERRORS[rootCode];
320
+ if (mapped) return mapped;
321
+ return `${method} failed with Spield contract error #${rootCode}.`;
322
+ }
323
+ if (rootMessage) return `${method} failed: ${rootMessage}`;
324
+ const code = /Error\(Contract, #(\d+)\)/.exec(text);
325
+ if (code) return `${method} failed with contract error #${code[1]}.`;
326
+ return `${method} simulation failed: ${text.slice(0, 200)}`;
327
+ };
328
+
329
+ // src/blend-strategy.ts
330
+ var BlendStrategyClient = class {
331
+ constructor(network, soroban) {
332
+ this.network = network;
333
+ this.soroban = soroban;
334
+ }
335
+ network;
336
+ soroban;
337
+ get contract() {
338
+ return this.network.contracts.strategy;
339
+ }
340
+ async getAvailableLiquidity() {
341
+ const value = await this.soroban.read(this.contract, "available_liquidity");
342
+ return tokenAmount(toBig(value), "USDC", this.network.decimals);
343
+ }
344
+ /**
345
+ * **BLND accrued to the protocol and not yet claimed.**
346
+ *
347
+ * Measured on mainnet 2026-08-30: Blend allocates emissions to XLM *suppliers* and USDC
348
+ * *borrowers*, not USDC suppliers — which is the side Spield is on — so this reads `0` and has
349
+ * never been anything else. Allocations carry an expiration and rotate each cycle, so it will not
350
+ * necessarily stay zero.
351
+ *
352
+ * Claiming is permissionless and pays a destination fixed in contract storage, so an integrator
353
+ * can trigger it but can never redirect it.
354
+ */
355
+ async getClaimableEmissions() {
356
+ try {
357
+ const value = await this.soroban.read(this.contract, "claimable_emissions");
358
+ return tokenAmount(toBig(value), "BLND", this.network.decimals);
359
+ } catch {
360
+ return tokenAmount(0n, "BLND", this.network.decimals);
361
+ }
362
+ }
363
+ /**
364
+ * **What the whole Blend position is really worth**, live rate, no monotonicity guard, no write.
365
+ * Unlike {@link getPositionValue} this keeps answering while a rate decrease has exits frozen.
366
+ */
367
+ async getPositionValueUnguarded() {
368
+ try {
369
+ const value = await this.soroban.read(this.contract, "position_value_unguarded");
370
+ return tokenAmount(toBig(value), "USDC", this.network.decimals);
371
+ } catch {
372
+ return null;
373
+ }
374
+ }
375
+ async getRateBound() {
376
+ const tuple = await this.soroban.read(this.contract, "rate_bound");
377
+ const [lastRate, lastTimestamp, maxAprBps] = tuple ?? [];
378
+ return {
379
+ lastRate: toBig(lastRate),
380
+ lastTimestamp: toNum(lastTimestamp),
381
+ maxAprBps: toNum(maxAprBps)
382
+ };
383
+ }
384
+ async getPositionValue(input) {
385
+ const shares = assertPositiveAmount(input.shares, this.network.decimals);
386
+ const value = await this.soroban.read(this.contract, "position_value", [i128(shares)]);
387
+ return tokenAmount(toBig(value), "USDC", this.network.decimals);
388
+ }
389
+ async getState() {
390
+ const [currentRate, shares, liquidity, underlying, pool, rateBound] = await Promise.all([
391
+ this.soroban.read(this.contract, "current_rate"),
392
+ this.soroban.read(this.contract, "total_shares"),
393
+ this.getAvailableLiquidity(),
394
+ this.soroban.read(this.contract, "underlying"),
395
+ this.soroban.read(this.contract, "pool"),
396
+ this.getRateBound()
397
+ ]);
398
+ return {
399
+ currentRate: toBig(currentRate),
400
+ totalShares: tokenAmount(toBig(shares), "STRATEGY_SHARES", this.network.decimals),
401
+ availableLiquidity: liquidity,
402
+ underlying: String(underlying),
403
+ pool: String(pool),
404
+ rateBound
405
+ };
406
+ }
407
+ };
408
+
409
+ // src/horizon.ts
410
+ import { Asset, BASE_FEE as BASE_FEE2, Horizon, Operation, TransactionBuilder as TransactionBuilder2 } from "@stellar/stellar-sdk";
411
+ var HorizonClient = class {
412
+ constructor(network) {
413
+ this.network = network;
414
+ this.horizon = new Horizon.Server(network.horizonUrl);
415
+ }
416
+ network;
417
+ horizon;
418
+ async getPtTrustline(address) {
419
+ const { code, issuer } = this.network.assets.pt;
420
+ try {
421
+ const account = await this.horizon.loadAccount(address);
422
+ const pt = account.balances.some(
423
+ (balance) => "asset_code" in balance && balance.asset_code === code && "asset_issuer" in balance && balance.asset_issuer === issuer
424
+ );
425
+ return { pt, ready: pt };
426
+ } catch {
427
+ return { pt: false, ready: false };
428
+ }
429
+ }
430
+ async setupPtTrustline(address, signer) {
431
+ if (!signer) throw new SpieldSignerError("A signer is required to setup the PT trustline.");
432
+ if ((await this.getPtTrustline(address)).ready) return null;
433
+ const { code, issuer } = this.network.assets.pt;
434
+ const account = await this.horizon.loadAccount(address);
435
+ const tx = new TransactionBuilder2(account, {
436
+ fee: BASE_FEE2,
437
+ networkPassphrase: this.network.networkPassphrase
438
+ }).addOperation(Operation.changeTrust({ asset: new Asset(code, issuer) })).setTimeout(120).build();
439
+ const { signedTxXdr } = await signer.signTransaction(tx.toXDR(), {
440
+ networkPassphrase: this.network.networkPassphrase,
441
+ address
442
+ });
443
+ const signed = TransactionBuilder2.fromXDR(signedTxXdr, this.network.networkPassphrase);
444
+ const result = await this.horizon.submitTransaction(
445
+ signed
446
+ );
447
+ return { hash: result.hash, status: "success", explorerUrl: explorerTx(this.network, result.hash) };
448
+ }
449
+ };
450
+
451
+ // src/internal.ts
452
+ var signerAddress = async (signer) => {
453
+ if (!signer) throw new SpieldSignerError("A signer is required for this transaction.");
454
+ return signer.getAddress();
455
+ };
456
+
457
+ // src/sr.ts
458
+ var SCALAR_12 = 10n ** 12n;
459
+ var fromScalar12 = (v) => Number(v) / Number(SCALAR_12);
460
+ var StandardizedReturnClient = class {
461
+ constructor(network, soroban, signer) {
462
+ this.network = network;
463
+ this.soroban = soroban;
464
+ this.signer = signer;
465
+ }
466
+ network;
467
+ soroban;
468
+ signer;
469
+ get contract() {
470
+ return this.network.contracts.sr;
471
+ }
472
+ async getExchangeRate() {
473
+ return toBig(await this.soroban.read(this.contract, "exchange_rate"));
474
+ }
475
+ /**
476
+ * **The honest twin of {@link getExchangeRate} — what a share is ACTUALLY worth.**
477
+ *
478
+ * `exchange_rate` is a high-water mark: it ratchets up and never falls. That stops the protocol
479
+ * repricing on a transient dip, but it means every value built on it — `previewRedeem`,
480
+ * `getAssetsOf` — reports MORE underlying than a redemption pays once Blend has taken a real loss.
481
+ *
482
+ * This reads the venue with no monotonicity guard, so unlike the guarded paths it keeps answering
483
+ * while exits are frozen — which is exactly when somebody needs the number.
484
+ *
485
+ * Returns `0n` on a deployment predating it. Sitting slightly ABOVE `exchange_rate` in normal
486
+ * operation is expected (it sees accrual the stored rate has not synced); only the other
487
+ * direction is interesting.
488
+ */
489
+ async getRealizableRate() {
490
+ try {
491
+ return toBig(await this.soroban.read(this.contract, "realizable_rate"));
492
+ } catch {
493
+ return 0n;
494
+ }
495
+ }
496
+ /** A holder's actual pro-rata claim. See {@link getRealizableRate}. `null` if unavailable. */
497
+ async getRealizableValue(input) {
498
+ const shares = assertPositiveAmount(input.shares, this.network.decimals);
499
+ try {
500
+ const value = await this.soroban.read(this.contract, "realizable_value", [i128(shares)]);
501
+ return tokenAmount(toBig(value), "USDC", this.network.decimals);
502
+ } catch {
503
+ return null;
504
+ }
505
+ }
506
+ srToUnderlying(shares, rate) {
507
+ return rate > 0n ? shares * rate / SCALAR_12 : 0n;
508
+ }
509
+ underlyingToSr(amount, rate) {
510
+ return rate > 0n ? amount * SCALAR_12 / rate : 0n;
511
+ }
512
+ async previewDeposit(input) {
513
+ const amount = assertPositiveAmount(input.amount, this.network.decimals);
514
+ const raw = await this.soroban.read(this.contract, "preview_deposit", [i128(amount)]);
515
+ return tokenAmount(toBig(raw), "SR", this.network.decimals);
516
+ }
517
+ async previewRedeem(input) {
518
+ const shares = assertPositiveAmount(input.shares, this.network.decimals);
519
+ const raw = await this.soroban.read(this.contract, "preview_redeem", [i128(shares)]);
520
+ return tokenAmount(toBig(raw), "USDC", this.network.decimals);
521
+ }
522
+ async getBalance(input) {
523
+ const raw = await this.soroban.read(this.contract, "balance", [addr(input.owner)]);
524
+ return tokenAmount(toBig(raw), "SR", this.network.decimals);
525
+ }
526
+ async getAssetsOf(input) {
527
+ const raw = await this.soroban.read(this.contract, "assets_of", [addr(input.owner)]);
528
+ return tokenAmount(toBig(raw), "USDC", this.network.decimals);
529
+ }
530
+ async getState() {
531
+ const [rateRaw, supplyRaw, assetsRaw, maxRaw, capRaw, headroomRaw, paused] = await Promise.all([
532
+ this.soroban.read(this.contract, "exchange_rate"),
533
+ this.soroban.read(this.contract, "total_supply"),
534
+ this.soroban.read(this.contract, "total_assets"),
535
+ this.soroban.read(this.contract, "max_redeemable"),
536
+ this.soroban.read(this.contract, "deposit_cap"),
537
+ this.soroban.read(this.contract, "deposit_headroom"),
538
+ this.soroban.read(this.contract, "is_paused")
539
+ ]);
540
+ const rate = toBig(rateRaw);
541
+ const max = toBig(maxRaw);
542
+ const cap = toBig(capRaw);
543
+ const headroom = toBig(headroomRaw);
544
+ return {
545
+ exchangeRate: rate,
546
+ exchangeRateNumber: Number(rate) / Number(SCALAR_12),
547
+ totalSupply: tokenAmount(toBig(supplyRaw), "SR", this.network.decimals),
548
+ totalAssets: tokenAmount(toBig(assetsRaw), "USDC", this.network.decimals),
549
+ maxRedeemable: max === (1n << 127n) - 1n ? null : tokenAmount(max, "SR", this.network.decimals),
550
+ depositCap: cap === 0n ? null : tokenAmount(cap, "USDC", this.network.decimals),
551
+ depositHeadroom: cap === 0n ? null : tokenAmount(headroom, "USDC", this.network.decimals),
552
+ depositCapReached: cap > 0n && headroom === 0n,
553
+ paused
554
+ };
555
+ }
556
+ async deposit(input) {
557
+ const signer = input.signer ?? this.signer;
558
+ const from = await signerAddress(signer);
559
+ return this.soroban.write(this.contract, "deposit", [
560
+ addr(from),
561
+ addr(input.receiver ?? from),
562
+ i128(assertPositiveAmount(input.amount, this.network.decimals)),
563
+ i128(assertNonNegativeAmount(input.minSharesOut ?? 0n, this.network.decimals))
564
+ ], signer);
565
+ }
566
+ async redeem(input) {
567
+ const signer = input.signer ?? this.signer;
568
+ const from = await signerAddress(signer);
569
+ return this.soroban.write(this.contract, "redeem", [
570
+ addr(from),
571
+ addr(input.receiver ?? from),
572
+ i128(assertPositiveAmount(input.shares, this.network.decimals)),
573
+ i128(assertNonNegativeAmount(input.minUnderlyingOut ?? 0n, this.network.decimals))
574
+ ], signer);
575
+ }
576
+ async redeemPartial(input) {
577
+ const signer = input.signer ?? this.signer;
578
+ const from = await signerAddress(signer);
579
+ return this.soroban.write(this.contract, "redeem_partial", [
580
+ addr(from),
581
+ addr(input.receiver ?? from),
582
+ i128(assertPositiveAmount(input.shares, this.network.decimals)),
583
+ i128(assertNonNegativeAmount(input.minUnderlyingOut ?? 0n, this.network.decimals))
584
+ ], signer);
585
+ }
586
+ /** Permissionless update of SR's stored high-water exchange rate. */
587
+ syncRate(input = {}) {
588
+ return this.soroban.write(this.contract, "sync_rate", [], input.signer ?? this.signer);
589
+ }
590
+ /** Permissionless keep-alive for an SR holder's persistent balance entry. */
591
+ bumpHolder(input = {}) {
592
+ const signer = input.signer ?? this.signer;
593
+ return signerAddress(signer).then((signerOwner) => this.soroban.write(
594
+ this.contract,
595
+ "bump_holder",
596
+ [addr(input.owner ?? signerOwner)],
597
+ signer
598
+ ));
599
+ }
600
+ };
601
+
602
+ // src/sr-market.ts
603
+ var YT_MAX_IN_PAD_BPS = 300n;
604
+ var SrMarketClient = class {
605
+ constructor(network, soroban, signer) {
606
+ this.network = network;
607
+ this.soroban = soroban;
608
+ this.signer = signer;
609
+ }
610
+ network;
611
+ soroban;
612
+ signer;
613
+ latestLiquidityAbi;
614
+ get contract() {
615
+ return this.network.contracts.market;
616
+ }
617
+ async getStats() {
618
+ const [reserves, assetRaw, sharesRaw, priceRaw, apyRaw, expiryRaw, earnedRaw, feeShareRaw, paused] = await Promise.all([
619
+ this.soroban.read(this.contract, "reserves"),
620
+ this.soroban.read(this.contract, "asset_reserve"),
621
+ this.soroban.read(this.contract, "total_shares"),
622
+ this.soroban.read(this.contract, "pt_price"),
623
+ this.soroban.read(this.contract, "implied_apy"),
624
+ this.soroban.read(this.contract, "expiry"),
625
+ this.soroban.read(this.contract, "treasury_earned"),
626
+ this.soroban.read(this.contract, "treasury_fee_share_bps"),
627
+ this.soroban.read(this.contract, "is_paused")
628
+ ]);
629
+ const [ptRaw, srRaw] = reserves ?? [];
630
+ const price = toBig(priceRaw);
631
+ const apy = toBig(apyRaw);
632
+ const expiry = toNum(expiryRaw);
633
+ return {
634
+ ptReserve: tokenAmount(toBig(ptRaw), "PT", this.network.decimals),
635
+ srReserve: tokenAmount(toBig(srRaw), "SR", this.network.decimals),
636
+ assetReserve: tokenAmount(toBig(assetRaw), "USDC", this.network.decimals),
637
+ totalShares: tokenAmount(toBig(sharesRaw), "LP", this.network.decimals),
638
+ ptPrice: price,
639
+ ptPriceNumber: Number(price) / Number(SCALAR_12),
640
+ impliedApy: apy,
641
+ impliedApyPct: Number(apy) / Number(SCALAR_12) * 100,
642
+ expiry,
643
+ expiryDate: new Date(expiry * 1e3),
644
+ treasuryEarned: tokenAmount(toBig(earnedRaw), "SR", this.network.decimals),
645
+ treasuryFeeShareBps: toNum(feeShareRaw),
646
+ paused
647
+ };
648
+ }
649
+ async quote(method, amount, inputSymbol, outputSymbol) {
650
+ const input = assertPositiveAmount(amount, this.network.decimals);
651
+ const output = toBig(await this.soroban.read(this.contract, method, [i128(input)]));
652
+ return {
653
+ input: tokenAmount(input, inputSymbol, this.network.decimals),
654
+ output: tokenAmount(output, outputSymbol, this.network.decimals),
655
+ available: output > 0n
656
+ };
657
+ }
658
+ quoteBuyPt(input) {
659
+ return this.quote("quote_buy_pt", input.srIn, "SR", "PT");
660
+ }
661
+ quoteSellPt(input) {
662
+ return this.quote("quote_sell_pt", input.ptIn, "PT", "SR");
663
+ }
664
+ quoteBuyYt(input) {
665
+ return this.quote("quote_buy_yt", input.ytOut, "YT", "SR");
666
+ }
667
+ quoteSellYt(input) {
668
+ return this.quote("quote_sell_yt", input.ytIn, "YT", "SR");
669
+ }
670
+ async getLpPosition(input) {
671
+ const tuple = await this.soroban.read(this.contract, "lp_position", [addr(input.owner)]);
672
+ const [shares, ptClaim, srClaim] = tuple ?? [];
673
+ return {
674
+ shares: tokenAmount(toBig(shares), "LP", this.network.decimals),
675
+ ptClaim: tokenAmount(toBig(ptClaim), "PT", this.network.decimals),
676
+ srClaim: tokenAmount(toBig(srClaim), "SR", this.network.decimals)
677
+ };
678
+ }
679
+ async buyPt(input) {
680
+ const signer = input.signer ?? this.signer;
681
+ const owner = await signerAddress(signer);
682
+ return this.soroban.write(this.contract, "swap_exact_sr_for_pt", [
683
+ addr(owner),
684
+ i128(assertPositiveAmount(input.srIn, this.network.decimals)),
685
+ i128(assertNonNegativeAmount(input.minPtOut ?? 0n, this.network.decimals)),
686
+ u32(input.deadlineLedger ?? 0)
687
+ ], signer);
688
+ }
689
+ async sellPt(input) {
690
+ const signer = input.signer ?? this.signer;
691
+ const owner = await signerAddress(signer);
692
+ return this.soroban.write(this.contract, "swap_exact_pt_for_sr", [
693
+ addr(owner),
694
+ i128(assertPositiveAmount(input.ptIn, this.network.decimals)),
695
+ i128(assertNonNegativeAmount(input.minSrOut ?? 0n, this.network.decimals)),
696
+ u32(input.deadlineLedger ?? 0)
697
+ ], signer);
698
+ }
699
+ async buyYt(input) {
700
+ const signer = input.signer ?? this.signer;
701
+ const owner = await signerAddress(signer);
702
+ const ytOut = assertPositiveAmount(input.ytOut, this.network.decimals);
703
+ let maxSrIn;
704
+ if (input.maxSrIn === void 0) {
705
+ const quoted = (await this.quoteBuyYt({ ytOut })).output.raw;
706
+ if (quoted <= 0n) throw new Error("No YT quote is available for that size.");
707
+ maxSrIn = quoted + quoted * YT_MAX_IN_PAD_BPS / 10000n + 1n;
708
+ } else {
709
+ maxSrIn = assertPositiveAmount(input.maxSrIn, this.network.decimals);
710
+ }
711
+ return this.soroban.write(this.contract, "buy_yt_exact_out", [
712
+ addr(owner),
713
+ i128(ytOut),
714
+ i128(maxSrIn),
715
+ u32(input.deadlineLedger ?? 0)
716
+ ], signer);
717
+ }
718
+ async sellYt(input) {
719
+ const signer = input.signer ?? this.signer;
720
+ const owner = await signerAddress(signer);
721
+ return this.soroban.write(this.contract, "sell_yt_exact_in", [
722
+ addr(owner),
723
+ i128(assertPositiveAmount(input.ytIn, this.network.decimals)),
724
+ i128(assertNonNegativeAmount(input.minSrOut ?? 0n, this.network.decimals)),
725
+ u32(input.deadlineLedger ?? 0)
726
+ ], signer);
727
+ }
728
+ async addLiquidity(input) {
729
+ const signer = input.signer ?? this.signer;
730
+ const owner = await signerAddress(signer);
731
+ const minShares = assertNonNegativeAmount(input.minShares ?? 0n, this.network.decimals);
732
+ const args = [
733
+ addr(owner),
734
+ i128(assertPositiveAmount(input.ptIn, this.network.decimals)),
735
+ i128(assertPositiveAmount(input.srIn, this.network.decimals))
736
+ ];
737
+ if (await this.supportsLatestLiquidityAbi(owner)) {
738
+ return this.soroban.write(this.contract, "add_liquidity", [...args, i128(minShares)], signer);
739
+ }
740
+ if (minShares > 0n) {
741
+ throw new SpieldConfigurationError(
742
+ "This market deployment predates addLiquidity.minShares; use minShares=0 or upgrade the market contract."
743
+ );
744
+ }
745
+ return this.soroban.write(this.contract, "add_liquidity", args, signer);
746
+ }
747
+ async removeLiquidity(input) {
748
+ const signer = input.signer ?? this.signer;
749
+ const owner = await signerAddress(signer);
750
+ return this.soroban.write(this.contract, "remove_liquidity", [
751
+ addr(owner),
752
+ i128(assertPositiveAmount(input.shares, this.network.decimals)),
753
+ i128(assertNonNegativeAmount(input.minPtOut ?? 0n, this.network.decimals)),
754
+ i128(assertNonNegativeAmount(input.minSrOut ?? 0n, this.network.decimals))
755
+ ], signer);
756
+ }
757
+ /** Permissionless keep-alive for an LP share entry. */
758
+ async bumpLp(input = {}) {
759
+ const signer = input.signer ?? this.signer;
760
+ const signerOwner = await signerAddress(signer);
761
+ return this.soroban.write(this.contract, "bump_lp", [addr(input.owner ?? signerOwner)], signer);
762
+ }
763
+ supportsLatestLiquidityAbi(owner) {
764
+ this.latestLiquidityAbi ??= this.soroban.read(this.contract, "bump_lp", [addr(owner)]).then(() => true, () => false);
765
+ return this.latestLiquidityAbi;
766
+ }
767
+ };
768
+
769
+ // src/sr-portfolio.ts
770
+ var SrPortfolioClient = class {
771
+ constructor(tokens, sr, yieldEngine, market, vault, decimals) {
772
+ this.tokens = tokens;
773
+ this.sr = sr;
774
+ this.yieldEngine = yieldEngine;
775
+ this.market = market;
776
+ this.vault = vault;
777
+ this.decimals = decimals;
778
+ }
779
+ tokens;
780
+ sr;
781
+ yieldEngine;
782
+ market;
783
+ vault;
784
+ decimals;
785
+ async getPortfolio(input) {
786
+ const [balances, rate, claimableYield, interest, lpPosition, fixedReceipts, trustline] = await Promise.all([
787
+ this.tokens.getBalances({ owner: input.owner }),
788
+ this.sr.getExchangeRate(),
789
+ this.yieldEngine.getClaimableInterest({ owner: input.owner }),
790
+ this.yieldEngine.getInterest({ owner: input.owner }),
791
+ this.market.getLpPosition({ owner: input.owner }),
792
+ this.vault.getOwnerReceipts({ owner: input.owner, maxScan: input.maxReceiptScan }),
793
+ this.tokens.getPtTrustline({ owner: input.owner })
794
+ ]);
795
+ return {
796
+ owner: input.owner,
797
+ balances,
798
+ srAsUsdc: tokenAmount(this.sr.srToUnderlying(balances.sr.raw, rate), "USDC", this.decimals),
799
+ claimableYield,
800
+ claimableYieldAsUsdc: tokenAmount(this.sr.srToUnderlying(claimableYield.raw, rate), "USDC", this.decimals),
801
+ interest,
802
+ lpPosition,
803
+ fixedReceipts,
804
+ hasPtTrustline: trustline.ready
805
+ };
806
+ }
807
+ };
808
+
809
+ // src/sr-protocol.ts
810
+ var SrProtocolClient = class {
811
+ constructor(sr, strategy, yieldEngine, market, vault) {
812
+ this.sr = sr;
813
+ this.strategy = strategy;
814
+ this.yieldEngine = yieldEngine;
815
+ this.market = market;
816
+ this.vault = vault;
817
+ }
818
+ sr;
819
+ strategy;
820
+ yieldEngine;
821
+ market;
822
+ vault;
823
+ async getHealth() {
824
+ const [sr, strategy, yieldEngine, market, vault] = await Promise.all([
825
+ this.sr.getState(),
826
+ this.strategy.getState(),
827
+ this.yieldEngine.getState(),
828
+ this.market.getStats(),
829
+ this.vault.getStats()
830
+ ]);
831
+ return { sr, strategy, yieldEngine, market, vault };
832
+ }
833
+ getSolvency() {
834
+ return this.yieldEngine.getSolvency();
835
+ }
836
+ };
837
+
838
+ // src/sr-router.ts
839
+ var DEFAULT_ROUTER_SLIPPAGE_BPS = 100;
840
+ var assertSlippageBps = (bps) => {
841
+ if (!Number.isInteger(bps) || bps < 0 || bps > 1e4) {
842
+ throw new SpieldValidationError("slippageBps must be an integer between 0 and 10000.");
843
+ }
844
+ return bps;
845
+ };
846
+ var slippageFloor = (quote, bps) => {
847
+ assertSlippageBps(bps);
848
+ return quote - quote * BigInt(bps) / 10000n;
849
+ };
850
+ var SrRouterClient = class {
851
+ constructor(network, soroban, signer) {
852
+ this.network = network;
853
+ this.soroban = soroban;
854
+ this.signer = signer;
855
+ }
856
+ network;
857
+ soroban;
858
+ signer;
859
+ get contract() {
860
+ return this.network.contracts.router;
861
+ }
862
+ /**
863
+ * The basis for `minOut`: what the REAL entry point would pay, falling back to the quote.
864
+ *
865
+ * `FINAL_CHECK.md` V2-01 — the market synchronizes SR on every value-moving call, but `quote_*`
866
+ * is a view and cannot. A quote therefore lags, and for a sale it reports MORE than the trade
867
+ * will pay; sizing `minOut` from it can revert the trade on its own slippage bound.
868
+ *
869
+ * Simulation runs the same synchronized code path the submission runs, so its answer is the one
870
+ * that executes. It legitimately returns `null` (unaffordable size, unfillable route, older
871
+ * deployment) — in which case the quote is used exactly as before.
872
+ */
873
+ async executableBasis(owner, method, args, quoted) {
874
+ if (typeof this.soroban.simulateAs !== "function") return quoted;
875
+ const simulated = await this.soroban.simulateAs(owner, this.contract, method, args);
876
+ return simulated !== null && simulated > 0n ? simulated : quoted;
877
+ }
878
+ async quote(method, amount, symbol) {
879
+ const raw = await this.soroban.read(this.contract, method, [i128(amount)]);
880
+ return tokenAmount(toBig(raw), symbol, this.network.decimals);
881
+ }
882
+ quoteBuyPt(input) {
883
+ return this.quote(
884
+ "quote_buy_pt_with_usdc",
885
+ assertPositiveAmount(input.usdcIn, this.network.decimals),
886
+ "PT"
887
+ );
888
+ }
889
+ quoteBuyYt(input) {
890
+ return this.quote(
891
+ "quote_buy_yt_with_usdc",
892
+ assertPositiveAmount(input.ytOut, this.network.decimals),
893
+ "USDC"
894
+ );
895
+ }
896
+ quoteSellPt(input) {
897
+ return this.quote(
898
+ "quote_sell_pt_for_usdc",
899
+ assertPositiveAmount(input.ptIn, this.network.decimals),
900
+ "USDC"
901
+ );
902
+ }
903
+ quoteSellYt(input) {
904
+ return this.quote(
905
+ "quote_sell_yt_for_usdc",
906
+ assertPositiveAmount(input.ytIn, this.network.decimals),
907
+ "USDC"
908
+ );
909
+ }
910
+ quoteRedeemPy(input) {
911
+ return this.quote(
912
+ "quote_redeem_py_for_usdc",
913
+ assertPositiveAmount(input.amount, this.network.decimals),
914
+ "USDC"
915
+ );
916
+ }
917
+ async quoteClaimYield(input) {
918
+ const raw = await this.soroban.read(this.contract, "quote_claim_yield", [addr(input.owner)]);
919
+ return tokenAmount(toBig(raw), "USDC", this.network.decimals);
920
+ }
921
+ async buyPt(input) {
922
+ const signer = input.signer ?? this.signer;
923
+ const owner = await signerAddress(signer);
924
+ const usdcIn = assertPositiveAmount(input.usdcIn, this.network.decimals);
925
+ assertSlippageBps(input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS);
926
+ const minOut = input.minPtOut === void 0 ? slippageFloor(
927
+ await this.executableBasis(
928
+ owner,
929
+ "buy_pt_with_usdc",
930
+ [addr(owner), i128(usdcIn), i128(0n), u32(input.deadlineLedger ?? 0)],
931
+ (await this.quoteBuyPt({ usdcIn })).raw
932
+ ),
933
+ input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS
934
+ ) : assertNonNegativeAmount(input.minPtOut, this.network.decimals);
935
+ if (minOut <= 0n) throw new Error("No PT quote is available for that size.");
936
+ return this.soroban.write(this.contract, "buy_pt_with_usdc", [
937
+ addr(owner),
938
+ i128(usdcIn),
939
+ i128(minOut),
940
+ u32(input.deadlineLedger ?? 0)
941
+ ], signer);
942
+ }
943
+ async sellPt(input) {
944
+ const signer = input.signer ?? this.signer;
945
+ const owner = await signerAddress(signer);
946
+ const ptIn = assertPositiveAmount(input.ptIn, this.network.decimals);
947
+ assertSlippageBps(input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS);
948
+ const minOut = input.minUsdcOut === void 0 ? slippageFloor(
949
+ await this.executableBasis(
950
+ owner,
951
+ "sell_pt_for_usdc",
952
+ [addr(owner), i128(ptIn), i128(0n), u32(input.deadlineLedger ?? 0)],
953
+ (await this.quoteSellPt({ ptIn })).raw
954
+ ),
955
+ input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS
956
+ ) : assertNonNegativeAmount(input.minUsdcOut, this.network.decimals);
957
+ return this.soroban.write(this.contract, "sell_pt_for_usdc", [
958
+ addr(owner),
959
+ i128(ptIn),
960
+ i128(minOut),
961
+ u32(input.deadlineLedger ?? 0)
962
+ ], signer);
963
+ }
964
+ async redeemPy(input) {
965
+ const signer = input.signer ?? this.signer;
966
+ const owner = await signerAddress(signer);
967
+ const amount = assertPositiveAmount(input.amount, this.network.decimals);
968
+ assertSlippageBps(input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS);
969
+ const minOut = input.minUsdcOut === void 0 ? slippageFloor(
970
+ await this.executableBasis(
971
+ owner,
972
+ "redeem_py_for_usdc",
973
+ [addr(owner), i128(amount), i128(0n)],
974
+ (await this.quoteRedeemPy({ amount })).raw
975
+ ),
976
+ input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS
977
+ ) : assertNonNegativeAmount(input.minUsdcOut, this.network.decimals);
978
+ return this.soroban.write(this.contract, "redeem_py_for_usdc", [addr(owner), i128(amount), i128(minOut)], signer);
979
+ }
980
+ async claimYield(input = {}) {
981
+ const signer = input.signer ?? this.signer;
982
+ const owner = await signerAddress(signer);
983
+ const minOut = input.minUsdcOut === void 0 ? slippageFloor((await this.quoteClaimYield({ owner })).raw, input.slippageBps ?? DEFAULT_ROUTER_SLIPPAGE_BPS) : assertNonNegativeAmount(input.minUsdcOut, this.network.decimals);
984
+ return this.soroban.write(this.contract, "claim_yield_to_usdc", [addr(owner), i128(minOut)], signer);
985
+ }
986
+ };
987
+
988
+ // src/sr-tokens.ts
989
+ var SrTokensClient = class {
990
+ constructor(network, soroban, horizon, signer) {
991
+ this.network = network;
992
+ this.soroban = soroban;
993
+ this.horizon = horizon;
994
+ this.signer = signer;
995
+ }
996
+ network;
997
+ soroban;
998
+ horizon;
999
+ signer;
1000
+ async getBalance(input) {
1001
+ const contracts = this.network.contracts;
1002
+ const contract = {
1003
+ USDC: contracts.usdc,
1004
+ SR: contracts.sr,
1005
+ PT: contracts.pt,
1006
+ // The yield engine itself implements SEP-41 and is the YT token.
1007
+ YT: contracts.yieldEngine
1008
+ }[input.token];
1009
+ try {
1010
+ const raw = await this.soroban.read(contract, "balance", [addr(input.owner)]);
1011
+ return tokenAmount(toBig(raw), input.token, this.network.decimals);
1012
+ } catch {
1013
+ return tokenAmount(0n, input.token, this.network.decimals);
1014
+ }
1015
+ }
1016
+ async getBalances(input) {
1017
+ const [usdc, sr, pt, yt] = await Promise.all([
1018
+ this.getBalance({ token: "USDC", owner: input.owner }),
1019
+ this.getBalance({ token: "SR", owner: input.owner }),
1020
+ this.getBalance({ token: "PT", owner: input.owner }),
1021
+ this.getBalance({ token: "YT", owner: input.owner })
1022
+ ]);
1023
+ return { usdc, sr, pt, yt };
1024
+ }
1025
+ getPtTrustline(input) {
1026
+ return this.horizon.getPtTrustline(input.owner);
1027
+ }
1028
+ /** Set up the sole v2 trustline. YT is a contract token and must not get a trustline. */
1029
+ async ensurePtTrustline(input = {}) {
1030
+ const signer = input.signer ?? this.signer;
1031
+ const owner = input.owner ?? await signerAddress(signer);
1032
+ return this.horizon.setupPtTrustline(owner, signer);
1033
+ }
1034
+ };
1035
+
1036
+ // src/sr-vault.ts
1037
+ var SrVaultClient = class {
1038
+ constructor(network, soroban, signer) {
1039
+ this.network = network;
1040
+ this.soroban = soroban;
1041
+ this.signer = signer;
1042
+ }
1043
+ network;
1044
+ soroban;
1045
+ signer;
1046
+ get contract() {
1047
+ return this.network.contracts.vault;
1048
+ }
1049
+ async quoteFixedRate(input) {
1050
+ const amount = assertPositiveAmount(input.amount, this.network.decimals);
1051
+ const tuple = await this.soroban.read(this.contract, "quote", [i128(amount)]);
1052
+ const [payoutRaw, couponRaw, rateRaw] = tuple ?? [];
1053
+ const rateBps = toNum(rateRaw);
1054
+ return {
1055
+ input: tokenAmount(amount, "USDC", this.network.decimals),
1056
+ payout: tokenAmount(toBig(payoutRaw), "USDC", this.network.decimals),
1057
+ coupon: tokenAmount(toBig(couponRaw), "USDC", this.network.decimals),
1058
+ rateBps,
1059
+ ratePct: rateBps / 100
1060
+ };
1061
+ }
1062
+ async getStats() {
1063
+ const [raw, paused] = await Promise.all([
1064
+ this.soroban.read(this.contract, "stats"),
1065
+ this.soroban.read(this.contract, "is_paused")
1066
+ ]);
1067
+ const maturity = toNum(raw.maturity);
1068
+ const rateBps = toNum(raw.rate_bps);
1069
+ return {
1070
+ ptInventory: tokenAmount(toBig(raw.pt_inventory), "PT", this.network.decimals),
1071
+ ytInventory: tokenAmount(toBig(raw.yt_inventory), "YT", this.network.decimals),
1072
+ totalLiability: tokenAmount(toBig(raw.total_liability), "USDC", this.network.decimals),
1073
+ totalCollected: tokenAmount(toBig(raw.total_collected), "USDC", this.network.decimals),
1074
+ couponCapacity: tokenAmount(toBig(raw.coupon_capacity), "PT", this.network.decimals),
1075
+ rateBps,
1076
+ ratePct: rateBps / 100,
1077
+ maturity,
1078
+ maturityDate: new Date(maturity * 1e3),
1079
+ openReceipts: toNum(raw.open_receipts),
1080
+ paused
1081
+ };
1082
+ }
1083
+ async getReceipt(input) {
1084
+ if (!Number.isSafeInteger(input.receiptId) || input.receiptId < 0) {
1085
+ throw new SpieldValidationError("receiptId must be a non-negative safe integer.");
1086
+ }
1087
+ try {
1088
+ const raw = await this.soroban.read(this.contract, "get_receipt", [u64(input.receiptId)]);
1089
+ const maturity = toNum(raw.maturity);
1090
+ const rateBps = toNum(raw.rate_bps);
1091
+ const payout = toBig(raw.payout);
1092
+ const collected = toBig(raw.collected);
1093
+ return {
1094
+ receiptId: input.receiptId,
1095
+ owner: String(raw.owner),
1096
+ principal: tokenAmount(toBig(raw.principal), "USDC", this.network.decimals),
1097
+ payout: tokenAmount(payout, "USDC", this.network.decimals),
1098
+ rateBps,
1099
+ ratePct: rateBps / 100,
1100
+ maturity,
1101
+ maturityDate: new Date(maturity * 1e3),
1102
+ open: Boolean(raw.open),
1103
+ collected: tokenAmount(collected, "USDC", this.network.decimals),
1104
+ remaining: tokenAmount(payout > collected ? payout - collected : 0n, "USDC", this.network.decimals)
1105
+ };
1106
+ } catch {
1107
+ return null;
1108
+ }
1109
+ }
1110
+ async getOwnerReceipts(input) {
1111
+ const maxScan = input.maxScan ?? 64;
1112
+ if (!Number.isSafeInteger(maxScan) || maxScan < 0) {
1113
+ throw new SpieldValidationError("maxScan must be a non-negative safe integer.");
1114
+ }
1115
+ const receipts = [];
1116
+ let misses = 0;
1117
+ for (let id = 0; id < maxScan && misses < 5; id += 1) {
1118
+ const receipt = await this.getReceipt({ receiptId: id });
1119
+ if (!receipt) {
1120
+ misses += 1;
1121
+ } else {
1122
+ misses = 0;
1123
+ if (receipt.owner === input.owner && receipt.open) receipts.push(receipt);
1124
+ }
1125
+ }
1126
+ return receipts;
1127
+ }
1128
+ async deposit(input) {
1129
+ const signer = input.signer ?? this.signer;
1130
+ const owner = await signerAddress(signer);
1131
+ return this.soroban.write(this.contract, "deposit", [
1132
+ addr(owner),
1133
+ i128(assertPositiveAmount(input.amount, this.network.decimals))
1134
+ ], signer);
1135
+ }
1136
+ redeemReceipt(input) {
1137
+ if (!Number.isSafeInteger(input.receiptId) || input.receiptId < 0) {
1138
+ throw new SpieldValidationError("receiptId must be a non-negative safe integer.");
1139
+ }
1140
+ return this.soroban.write(this.contract, "redeem", [u64(input.receiptId)], input.signer ?? this.signer);
1141
+ }
1142
+ /** Remaining USDC owed before a receipt can close. Falls back to receipt data on the old ABI. */
1143
+ async getRedeemRemaining(input) {
1144
+ this.assertReceiptId(input.receiptId);
1145
+ try {
1146
+ const raw = await this.soroban.read(this.contract, "redeem_remaining", [u64(input.receiptId)]);
1147
+ return tokenAmount(toBig(raw), "USDC", this.network.decimals);
1148
+ } catch {
1149
+ const receipt = await this.getReceipt(input);
1150
+ return tokenAmount(receipt?.remaining.raw ?? 0n, "USDC", this.network.decimals);
1151
+ }
1152
+ }
1153
+ /** Read surplus SR, YT and USDC that the latest vault ABI can release after expiry. */
1154
+ async getSurplus() {
1155
+ try {
1156
+ const tuple = await this.soroban.read(this.contract, "surplus");
1157
+ const [sr, yt, usdc] = tuple ?? [];
1158
+ return {
1159
+ sr: tokenAmount(toBig(sr), "SR", this.network.decimals),
1160
+ yt: tokenAmount(toBig(yt), "YT", this.network.decimals),
1161
+ usdc: tokenAmount(toBig(usdc), "USDC", this.network.decimals)
1162
+ };
1163
+ } catch {
1164
+ return null;
1165
+ }
1166
+ }
1167
+ /** Permissionless keep-alive for a long-dated vault receipt. */
1168
+ bumpReceipt(input) {
1169
+ this.assertReceiptId(input.receiptId);
1170
+ return this.soroban.write(this.contract, "bump_receipt", [u64(input.receiptId)], input.signer ?? this.signer);
1171
+ }
1172
+ /** Permissionless claim and reinvestment of the vault's YT yield. */
1173
+ harvest(input = {}) {
1174
+ return this.soroban.write(this.contract, "harvest", [], input.signer ?? this.signer);
1175
+ }
1176
+ assertReceiptId(receiptId) {
1177
+ if (!Number.isSafeInteger(receiptId) || receiptId < 0) {
1178
+ throw new SpieldValidationError("receiptId must be a non-negative safe integer.");
1179
+ }
1180
+ }
1181
+ };
1182
+
1183
+ // src/yield-engine.ts
1184
+ var YieldEngineClient = class {
1185
+ constructor(network, soroban, signer) {
1186
+ this.network = network;
1187
+ this.soroban = soroban;
1188
+ this.signer = signer;
1189
+ }
1190
+ network;
1191
+ soroban;
1192
+ signer;
1193
+ get contract() {
1194
+ return this.network.contracts.yieldEngine;
1195
+ }
1196
+ async getYtBalance(input) {
1197
+ const raw = await this.soroban.read(this.contract, "balance", [addr(input.owner)]);
1198
+ return tokenAmount(toBig(raw), "YT", this.network.decimals);
1199
+ }
1200
+ async getClaimableInterest(input) {
1201
+ const raw = await this.soroban.read(this.contract, "claimable_interest", [addr(input.owner)]);
1202
+ return tokenAmount(toBig(raw), "SR", this.network.decimals);
1203
+ }
1204
+ async getInterest(input) {
1205
+ const raw = await this.soroban.read(this.contract, "interest_of", [addr(input.owner)]);
1206
+ return {
1207
+ index: toBig(raw.index),
1208
+ accrued: tokenAmount(toBig(raw.accrued), "SR", this.network.decimals),
1209
+ withdrawn: tokenAmount(toBig(raw.withdrawn), "SR", this.network.decimals)
1210
+ };
1211
+ }
1212
+ async getSolvency() {
1213
+ const tuple = await this.soroban.read(this.contract, "solvency");
1214
+ const [heldRaw, neededRaw, surplusRaw] = tuple ?? [];
1215
+ const held = toBig(heldRaw);
1216
+ const needed = toBig(neededRaw);
1217
+ return {
1218
+ held: tokenAmount(held, "SR", this.network.decimals),
1219
+ needed: tokenAmount(needed, "SR", this.network.decimals),
1220
+ surplus: tokenAmount(toBig(surplusRaw), "SR", this.network.decimals),
1221
+ solvent: held >= needed
1222
+ };
1223
+ }
1224
+ async getState() {
1225
+ const [solvency, totalPyRaw, totalAccruedRaw, indexRaw, feeRaw, expiryRaw, expired, paused] = await Promise.all([
1226
+ this.getSolvency(),
1227
+ this.soroban.read(this.contract, "total_py"),
1228
+ this.soroban.read(this.contract, "total_accrued"),
1229
+ this.soroban.read(this.contract, "py_index"),
1230
+ this.soroban.read(this.contract, "yield_fee_bps"),
1231
+ this.soroban.read(this.contract, "expiry"),
1232
+ this.soroban.read(this.contract, "is_expired"),
1233
+ this.soroban.read(this.contract, "is_paused")
1234
+ ]);
1235
+ const expiry = toNum(expiryRaw);
1236
+ const yieldFeeBps = toNum(feeRaw);
1237
+ return {
1238
+ ...solvency,
1239
+ totalPy: tokenAmount(toBig(totalPyRaw), "PT/YT", this.network.decimals),
1240
+ totalAccrued: tokenAmount(toBig(totalAccruedRaw), "SR", this.network.decimals),
1241
+ pyIndex: toBig(indexRaw),
1242
+ yieldFeeBps,
1243
+ yieldFeePct: yieldFeeBps / 100,
1244
+ expiry,
1245
+ expiryDate: new Date(expiry * 1e3),
1246
+ expired,
1247
+ paused
1248
+ };
1249
+ }
1250
+ async mintPy(input) {
1251
+ const signer = input.signer ?? this.signer;
1252
+ const from = await signerAddress(signer);
1253
+ return this.soroban.write(this.contract, "mint_py", [
1254
+ addr(from),
1255
+ addr(input.receiver ?? from),
1256
+ i128(assertPositiveAmount(input.srIn, this.network.decimals))
1257
+ ], signer);
1258
+ }
1259
+ async redeemPy(input) {
1260
+ const signer = input.signer ?? this.signer;
1261
+ const from = await signerAddress(signer);
1262
+ return this.soroban.write(this.contract, "redeem_py", [
1263
+ addr(from),
1264
+ addr(input.receiver ?? from),
1265
+ i128(assertPositiveAmount(input.amount, this.network.decimals))
1266
+ ], signer);
1267
+ }
1268
+ async claimYield(input = {}) {
1269
+ const signer = input.signer ?? this.signer;
1270
+ const owner = await signerAddress(signer);
1271
+ return this.soroban.write(this.contract, "redeem_due_interest", [addr(owner)], signer);
1272
+ }
1273
+ async transferYt(input) {
1274
+ const signer = input.signer ?? this.signer;
1275
+ const from = await signerAddress(signer);
1276
+ return this.soroban.write(this.contract, "transfer", [
1277
+ addr(from),
1278
+ addr(input.to),
1279
+ i128(assertPositiveAmount(input.amount, this.network.decimals))
1280
+ ], signer);
1281
+ }
1282
+ /** Permissionless expiry-index stamp; only succeeds at or after expiry. */
1283
+ stampExpiryIndex(input = {}) {
1284
+ return this.soroban.write(this.contract, "stamp_expiry_index", [], input.signer ?? this.signer);
1285
+ }
1286
+ /** Keep both a holder's YT balance and interest entries alive. Permissionless on the latest ABI. */
1287
+ async bumpHolder(input = {}) {
1288
+ const signer = input.signer ?? this.signer;
1289
+ const signerOwner = await signerAddress(signer);
1290
+ return this.soroban.write(this.contract, "bump_holder", [addr(input.owner ?? signerOwner)], signer);
1291
+ }
1292
+ };
1293
+
1294
+ // src/client.ts
1295
+ var SpieldClient = class {
1296
+ network;
1297
+ /** Wrap USDC into yield-bearing SR shares, and unwrap fully or partially. */
1298
+ sr;
1299
+ /** Blend rate guard, position value, and currently withdrawable liquidity. */
1300
+ strategy;
1301
+ /** Mint/redeem PT + YT, read and claim interest, transfer YT. */
1302
+ yield;
1303
+ /** PT/SR quotes, PT and YT trades, liquidity. */
1304
+ market;
1305
+ /** Fixed-rate quotes, receipts, deposit, redeem, harvest. */
1306
+ vault;
1307
+ /** One-transaction USDC routes for PT, redemption, and yield claims. */
1308
+ router;
1309
+ /** USDC/SR/PT/YT balances and the single required PT trustline. */
1310
+ tokens;
1311
+ /** Wallet-wide balances, interest, LP position, and vault receipts. */
1312
+ portfolio;
1313
+ /** Cross-contract health and yield-engine solvency. */
1314
+ protocol;
1315
+ signer;
1316
+ constructor(options = {}) {
1317
+ const network = options.network ?? testnet();
1318
+ this.network = network;
1319
+ this.signer = options.signer;
1320
+ const soroban = new SorobanClient({
1321
+ network,
1322
+ signer: options.signer,
1323
+ simulationSource: options.simulationSource ?? DEFAULT_SIMULATION_SOURCE,
1324
+ confirmationTimeoutMs: options.confirmationTimeoutMs,
1325
+ confirmationPollIntervalMs: options.confirmationPollIntervalMs
1326
+ });
1327
+ const horizon = new HorizonClient(network);
1328
+ this.sr = new StandardizedReturnClient(network, soroban, options.signer);
1329
+ this.strategy = new BlendStrategyClient(network, soroban);
1330
+ this.yield = new YieldEngineClient(network, soroban, options.signer);
1331
+ this.market = new SrMarketClient(network, soroban, options.signer);
1332
+ this.vault = new SrVaultClient(network, soroban, options.signer);
1333
+ this.router = new SrRouterClient(network, soroban, options.signer);
1334
+ this.tokens = new SrTokensClient(network, soroban, horizon, options.signer);
1335
+ this.portfolio = new SrPortfolioClient(
1336
+ this.tokens,
1337
+ this.sr,
1338
+ this.yield,
1339
+ this.market,
1340
+ this.vault,
1341
+ network.decimals
1342
+ );
1343
+ this.protocol = new SrProtocolClient(this.sr, this.strategy, this.yield, this.market, this.vault);
1344
+ }
1345
+ /** All-in USDC cost for an exact YT face amount, using the deployed two-step route. */
1346
+ async quoteBuyYtFromUsdc(input) {
1347
+ const quote = await this.market.quoteBuyYt(input);
1348
+ if (!quote.available) return tokenAmount(0n, "USDC", this.network.decimals);
1349
+ const rate = await this.sr.getExchangeRate();
1350
+ return tokenAmount(this.sr.srToUnderlying(quote.output.raw, rate), "USDC", this.network.decimals);
1351
+ }
1352
+ /**
1353
+ * Live-safe YT purchase: wrap only the missing SR, then buy YT. The one-call router version exceeds
1354
+ * the deployed Blend pool's Soroban budget, so this deliberately uses up to two signatures.
1355
+ */
1356
+ async buyYtFromUsdc(input) {
1357
+ const signer = input.signer ?? this.signer;
1358
+ const ytOut = assertPositiveAmount(input.ytOut, this.network.decimals);
1359
+ const quote = await this.market.quoteBuyYt({ ytOut });
1360
+ if (!quote.available) throw new Error("No YT quote is available for that size.");
1361
+ const budget = quote.output.raw + quote.output.raw * YT_MAX_IN_PAD_BPS / 10000n + 1n;
1362
+ const owner = await signer?.getAddress();
1363
+ if (!owner) throw new Error("A signer is required for this transaction.");
1364
+ const held = (await this.tokens.getBalance({ token: "SR", owner })).raw;
1365
+ let first;
1366
+ const needsWrap = held < budget;
1367
+ const total = needsWrap ? 2 : 1;
1368
+ if (needsWrap) {
1369
+ const rate = await this.sr.getExchangeRate();
1370
+ const missing = budget - held;
1371
+ const usdc = (missing * rate + SCALAR_12 - 1n) / SCALAR_12 + 1n;
1372
+ input.onProgress?.("wrap", 1, total);
1373
+ first = await this.sr.deposit({ amount: usdc, signer });
1374
+ }
1375
+ input.onProgress?.("buy", total, total);
1376
+ const second = await this.market.buyYt({ ytOut, maxSrIn: budget, signer });
1377
+ return { first, second };
1378
+ }
1379
+ /**
1380
+ * Live-safe YT exit: sell to SR, then unwrap the actual proceeds. The combined router route no
1381
+ * longer fits the deployed Blend-backed stack after the SR partial-exit/cap upgrade.
1382
+ */
1383
+ async sellYtToUsdc(input) {
1384
+ const signer = input.signer ?? this.signer;
1385
+ const owner = await signer?.getAddress();
1386
+ if (!owner) throw new Error("A signer is required for this transaction.");
1387
+ const quote = await this.market.quoteSellYt(input);
1388
+ if (!quote.available) throw new Error("No YT sale quote is available for that size.");
1389
+ const before = (await this.tokens.getBalance({ token: "SR", owner })).raw;
1390
+ input.onProgress?.("sell", 1, 2);
1391
+ const first = await this.market.sellYt({ ytIn: input.ytIn, minSrOut: quote.output.raw * 99n / 100n, signer });
1392
+ const after = (await this.tokens.getBalance({ token: "SR", owner })).raw;
1393
+ const proceeds = after - before;
1394
+ if (proceeds <= 0n) throw new Error("The YT sale completed but produced no SR to unwrap.");
1395
+ input.onProgress?.("unwrap", 2, 2);
1396
+ const second = await this.sr.redeem({ shares: proceeds, signer });
1397
+ return { first, second };
1398
+ }
1399
+ };
1400
+
1401
+ // src/signer.ts
1402
+ var FreighterSigner = class {
1403
+ constructor(api) {
1404
+ this.api = api;
1405
+ }
1406
+ api;
1407
+ get freighter() {
1408
+ const injected = this.api ?? globalThis.freighterApi;
1409
+ if (!injected) throw new SpieldSignerError("Freighter API is not available.");
1410
+ return injected;
1411
+ }
1412
+ async getAddress() {
1413
+ const getAddress = this.freighter.getAddress;
1414
+ if (!getAddress) throw new SpieldSignerError("Freighter getAddress() is not available.");
1415
+ const result = await getAddress();
1416
+ const address = typeof result === "string" ? result : result.address;
1417
+ if (!address) throw new SpieldSignerError("Freighter did not return an address.");
1418
+ return address;
1419
+ }
1420
+ async signTransaction(xdr2, opts) {
1421
+ const signTransaction = this.freighter.signTransaction;
1422
+ if (!signTransaction) throw new SpieldSignerError("Freighter signTransaction() is not available.");
1423
+ const result = await signTransaction(xdr2, opts);
1424
+ const signedTxXdr = typeof result === "string" ? result : result.signedTxXdr;
1425
+ if (!signedTxXdr) throw new SpieldSignerError("Freighter did not return a signed transaction.");
1426
+ return { signedTxXdr };
1427
+ }
1428
+ };
1429
+ var ManualSigner = class {
1430
+ constructor(address, sign) {
1431
+ this.address = address;
1432
+ this.sign = sign;
1433
+ }
1434
+ address;
1435
+ sign;
1436
+ async getAddress() {
1437
+ return this.address;
1438
+ }
1439
+ async signTransaction(xdr2, opts) {
1440
+ return this.sign(xdr2, opts);
1441
+ }
1442
+ };
1443
+ export {
1444
+ BlendStrategyClient,
1445
+ DEFAULT_ROUTER_SLIPPAGE_BPS,
1446
+ FreighterSigner,
1447
+ ManualSigner,
1448
+ SCALAR_12,
1449
+ STELLAR_NETWORK_PASSPHRASES,
1450
+ SpieldClient,
1451
+ SpieldConfigurationError,
1452
+ SpieldContractError,
1453
+ SpieldRpcError,
1454
+ SpieldSdkError,
1455
+ SpieldSignerError,
1456
+ SpieldValidationError,
1457
+ SrMarketClient,
1458
+ SrPortfolioClient,
1459
+ SrProtocolClient,
1460
+ SrRouterClient,
1461
+ SrTokensClient,
1462
+ SrVaultClient,
1463
+ StandardizedReturnClient,
1464
+ YT_MAX_IN_PAD_BPS,
1465
+ YieldEngineClient,
1466
+ assertNonNegativeAmount,
1467
+ customNetwork,
1468
+ fromBaseUnits,
1469
+ fromScalar12,
1470
+ testnet,
1471
+ toBaseUnits,
1472
+ tokenAmount
1473
+ };
1474
+ //# sourceMappingURL=index.js.map