@toon-protocol/sdk 2.0.0 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -29,7 +29,7 @@ import {
29
29
  unwrapSwapPacketFromToon,
30
30
  wrapSwapPacket,
31
31
  wrapSwapPacketToToon
32
- } from "./chunk-Z6S56VPV.js";
32
+ } from "./chunk-7LBYFU4L.js";
33
33
  import "./chunk-UP2VWCW5.js";
34
34
 
35
35
  // src/handler-context.ts
@@ -1997,6 +1997,360 @@ async function uploadBlobChunked(node, blob, destination, options) {
1997
1997
  return lastResult.eventId;
1998
1998
  }
1999
1999
 
2000
+ // src/adaptive-controller.ts
2001
+ import { ToonError as ToonError3 } from "@toon-protocol/core";
2002
+ var SwapControllerError = class extends ToonError3 {
2003
+ constructor(message, cause) {
2004
+ super(message, "SWAP_CONTROLLER_ERROR", cause);
2005
+ this.name = "SwapControllerError";
2006
+ }
2007
+ };
2008
+ var DECIMAL_UINT_REGEX = /^(0|[1-9]\d*)$/;
2009
+ function isSwapControllerState(value) {
2010
+ if (!value || typeof value !== "object") return false;
2011
+ const s = value;
2012
+ return s["v"] === 1 && typeof s["delta"] === "string" && DECIMAL_UINT_REGEX.test(s["delta"]) && typeof s["W"] === "number" && Number.isInteger(s["W"]) && s["W"] >= 1 && typeof s["vEwma"] === "number" && Number.isFinite(s["vEwma"]) && s["vEwma"] >= 0 && typeof s["tauEwma"] === "number" && Number.isFinite(s["tauEwma"]) && s["tauEwma"] >= 0 && typeof s["cleanStreak"] === "number" && Number.isInteger(s["cleanStreak"]) && s["cleanStreak"] >= 0 && typeof s["everShrunk"] === "boolean" && (s["lastWidened"] === "delta" || s["lastWidened"] === "window") && typeof s["updatedAt"] === "number";
2013
+ }
2014
+ function swapControllerStateKey(params) {
2015
+ const { makerPubkey, pair } = params;
2016
+ const from = `${pair.from.assetCode}@${pair.from.chain}`;
2017
+ const to = `${pair.to.assetCode}@${pair.to.chain}`;
2018
+ return `${pair.from.chain}:${makerPubkey}:${from}:${to}`;
2019
+ }
2020
+ var InMemorySwapControllerStateStore = class {
2021
+ states = /* @__PURE__ */ new Map();
2022
+ async load(key) {
2023
+ const hit = this.states.get(key);
2024
+ return hit === void 0 ? void 0 : { ...hit };
2025
+ }
2026
+ async save(key, state) {
2027
+ this.states.set(key, { ...state });
2028
+ }
2029
+ };
2030
+ var JsonFileSwapControllerStateStore = class {
2031
+ constructor(filePath) {
2032
+ this.filePath = filePath;
2033
+ if (typeof filePath !== "string" || filePath.length === 0) {
2034
+ throw new SwapControllerError(
2035
+ "JsonFileSwapControllerStateStore requires a non-empty filePath"
2036
+ );
2037
+ }
2038
+ }
2039
+ async readAll() {
2040
+ const fs = await import("fs/promises");
2041
+ let raw;
2042
+ try {
2043
+ raw = await fs.readFile(this.filePath, "utf8");
2044
+ } catch (err) {
2045
+ if (err.code === "ENOENT") return {};
2046
+ throw new SwapControllerError(
2047
+ `Failed to read controller state file ${this.filePath}`,
2048
+ err instanceof Error ? err : void 0
2049
+ );
2050
+ }
2051
+ try {
2052
+ const parsed = JSON.parse(raw);
2053
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
2054
+ return parsed;
2055
+ }
2056
+ } catch {
2057
+ }
2058
+ return {};
2059
+ }
2060
+ async load(key) {
2061
+ const all = await this.readAll();
2062
+ const candidate = all[key];
2063
+ return isSwapControllerState(candidate) ? { ...candidate } : void 0;
2064
+ }
2065
+ async save(key, state) {
2066
+ const fs = await import("fs/promises");
2067
+ const path = await import("path");
2068
+ const all = await this.readAll();
2069
+ all[key] = { ...state };
2070
+ const dir = path.dirname(this.filePath);
2071
+ await fs.mkdir(dir, { recursive: true });
2072
+ const tmp = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
2073
+ await fs.writeFile(tmp, JSON.stringify(all, null, 2), "utf8");
2074
+ await fs.rename(tmp, this.filePath);
2075
+ }
2076
+ };
2077
+ var DEFAULT_EPSILON_HALF_SPREAD_FRACTION = 0.5;
2078
+ var DEFAULT_MAX_WINDOW = 8;
2079
+ var DEFAULT_CLEAN_STREAK_LENGTH = 16;
2080
+ var DEFAULT_COLD_START_DIVISOR = 256;
2081
+ var DEFAULT_EWMA_ALPHA = 0.2;
2082
+ var CAP_SCALE = 1000000000000n;
2083
+ var CAP_SCALE_NUM = 1e12;
2084
+ function rateToNumber(rate) {
2085
+ const n = Number(rate);
2086
+ return Number.isFinite(n) && n > 0 ? n : NaN;
2087
+ }
2088
+ var AdaptiveDeltaController = class _AdaptiveDeltaController {
2089
+ /** Persistence key for this tuple (see {@link swapControllerStateKey}). */
2090
+ key;
2091
+ store;
2092
+ pair;
2093
+ epsilon;
2094
+ maxPacketAmount;
2095
+ minPacketAmount;
2096
+ maxWindow;
2097
+ cleanStreakLength;
2098
+ coldStartDivisor;
2099
+ alpha;
2100
+ now;
2101
+ stateInternal;
2102
+ /** δ in-memory as bigint (mirrors `stateInternal.delta`). */
2103
+ delta;
2104
+ /** Cold-start δ_0 — also the additive widen increment. Set on first nextDelta(). */
2105
+ delta0 = null;
2106
+ /** Previous tape entry for the per-second volatility measurement. */
2107
+ lastRate = null;
2108
+ lastRateTimestamp = null;
2109
+ constructor(config, key, store, state) {
2110
+ this.key = key;
2111
+ this.store = store;
2112
+ this.pair = config.pair;
2113
+ const epsilonFraction = config.epsilonHalfSpreadFraction ?? DEFAULT_EPSILON_HALF_SPREAD_FRACTION;
2114
+ this.epsilon = epsilonFraction * (config.advertisedSpread / 2);
2115
+ this.maxPacketAmount = config.maxPacketAmount;
2116
+ this.minPacketAmount = config.minPacketAmount ?? 1n;
2117
+ this.maxWindow = config.maxWindow ?? DEFAULT_MAX_WINDOW;
2118
+ this.cleanStreakLength = config.cleanStreakLength ?? DEFAULT_CLEAN_STREAK_LENGTH;
2119
+ this.coldStartDivisor = BigInt(
2120
+ config.coldStartDivisor ?? DEFAULT_COLD_START_DIVISOR
2121
+ );
2122
+ this.alpha = config.ewmaAlpha ?? DEFAULT_EWMA_ALPHA;
2123
+ this.now = config.now ?? Date.now;
2124
+ this.stateInternal = state;
2125
+ this.delta = BigInt(state.delta);
2126
+ }
2127
+ /**
2128
+ * Create a controller for one swap session: loads the persisted state for
2129
+ * the tuple from the store, or starts cold (`δ` unset until the first
2130
+ * `nextDelta()`, `W = 1`).
2131
+ */
2132
+ static async create(config) {
2133
+ validateConfig(config);
2134
+ const key = swapControllerStateKey({
2135
+ makerPubkey: config.makerPubkey,
2136
+ pair: config.pair
2137
+ });
2138
+ const store = config.store ?? new InMemorySwapControllerStateStore();
2139
+ const now = config.now ?? Date.now;
2140
+ const loaded = await store.load(key);
2141
+ const state = isSwapControllerState(loaded) ? loaded : {
2142
+ v: 1,
2143
+ delta: "0",
2144
+ W: 1,
2145
+ vEwma: 0,
2146
+ tauEwma: 0,
2147
+ cleanStreak: 0,
2148
+ everShrunk: false,
2149
+ // 'window' so the FIRST widen step hits δ (alternation starts on δ).
2150
+ lastWidened: "window",
2151
+ updatedAt: now()
2152
+ };
2153
+ return new _AdaptiveDeltaController(config, key, store, state);
2154
+ }
2155
+ /** Defensive snapshot of the current controller state. */
2156
+ get state() {
2157
+ return { ...this.stateInternal, delta: this.delta.toString() };
2158
+ }
2159
+ /** Current in-flight window W. */
2160
+ get window() {
2161
+ return this.stateInternal.W;
2162
+ }
2163
+ /**
2164
+ * Current `delta_cap = ε/(v·τ)` as a fraction of remaining notional.
2165
+ * `Infinity` while `v·τ` has no measurement yet (cold tape) — the
2166
+ * cold-start `δ_0` and absolute caps bound δ in that regime.
2167
+ */
2168
+ get deltaCapFraction() {
2169
+ const vTau = this.stateInternal.vEwma * this.stateInternal.tauEwma;
2170
+ if (!(vTau > 0)) return Infinity;
2171
+ return this.epsilon / vTau;
2172
+ }
2173
+ /** `delta_cap` in absolute source micro-units for a given remaining notional. */
2174
+ deltaCapAbsolute(remaining) {
2175
+ const frac = this.deltaCapFraction;
2176
+ if (!Number.isFinite(frac) || frac >= 1) return remaining;
2177
+ if (frac <= 0) return this.minPacketAmount;
2178
+ const scaled = BigInt(Math.floor(frac * CAP_SCALE_NUM));
2179
+ return remaining * scaled / CAP_SCALE;
2180
+ }
2181
+ /**
2182
+ * Decide the next packet size (source micro-units) for a session with
2183
+ * `remaining` notional left. Enforces, in order: the measured
2184
+ * `delta_cap = ε/(v·τ)` bound, the absolute `maxPacketAmount` cap, the
2185
+ * remaining notional, and the `minPacketAmount` floor. Seeds the
2186
+ * cold-start `δ_0 = min(delta_cap, notional/256, maxPacketAmount)` on
2187
+ * first call.
2188
+ */
2189
+ nextDelta(remaining) {
2190
+ if (typeof remaining !== "bigint" || remaining <= 0n) return 0n;
2191
+ if (this.delta0 === null) {
2192
+ let d0 = remaining / this.coldStartDivisor;
2193
+ const capAbs2 = this.deltaCapAbsolute(remaining);
2194
+ if (d0 > capAbs2) d0 = capAbs2;
2195
+ if (this.maxPacketAmount !== void 0 && d0 > this.maxPacketAmount) {
2196
+ d0 = this.maxPacketAmount;
2197
+ }
2198
+ if (d0 < this.minPacketAmount) d0 = this.minPacketAmount;
2199
+ this.delta0 = d0;
2200
+ if (this.delta <= 0n) {
2201
+ this.delta = d0;
2202
+ this.stateInternal.delta = d0.toString();
2203
+ }
2204
+ }
2205
+ let d = this.delta;
2206
+ const capAbs = this.deltaCapAbsolute(remaining);
2207
+ if (d > capAbs) d = capAbs;
2208
+ if (this.maxPacketAmount !== void 0 && d > this.maxPacketAmount) {
2209
+ d = this.maxPacketAmount;
2210
+ }
2211
+ if (d > remaining) d = remaining;
2212
+ const floor = this.minPacketAmount < remaining ? this.minPacketAmount : remaining;
2213
+ if (d < floor) d = floor;
2214
+ return d;
2215
+ }
2216
+ /**
2217
+ * Feed one packet observation: updates the measured EWMAs (`v`, `τ`),
2218
+ * applies at most ONE knob adjustment (asymmetric: multiplicative shrink /
2219
+ * additive widen), and persists the state.
2220
+ */
2221
+ async observe(observation) {
2222
+ const s = this.stateInternal;
2223
+ if (typeof observation.rttMs === "number" && Number.isFinite(observation.rttMs) && observation.rttMs > 0) {
2224
+ const tauSec = observation.rttMs / 1e3;
2225
+ s.tauEwma = s.tauEwma === 0 ? tauSec : this.alpha * tauSec + (1 - this.alpha) * s.tauEwma;
2226
+ }
2227
+ if (typeof observation.rate === "string" && typeof observation.rateTimestamp === "number" && Number.isFinite(observation.rateTimestamp)) {
2228
+ const r = rateToNumber(observation.rate);
2229
+ if (!Number.isNaN(r)) {
2230
+ if (this.lastRate !== null && this.lastRateTimestamp !== null && observation.rateTimestamp > this.lastRateTimestamp) {
2231
+ const dtSec = (observation.rateTimestamp - this.lastRateTimestamp) / 1e3;
2232
+ const inst = Math.abs(r - this.lastRate) / this.lastRate / dtSec;
2233
+ if (Number.isFinite(inst)) {
2234
+ s.vEwma = s.vEwma === 0 ? inst : this.alpha * inst + (1 - this.alpha) * s.vEwma;
2235
+ }
2236
+ }
2237
+ if (this.lastRateTimestamp === null || observation.rateTimestamp >= this.lastRateTimestamp) {
2238
+ this.lastRate = r;
2239
+ this.lastRateTimestamp = observation.rateTimestamp;
2240
+ }
2241
+ }
2242
+ }
2243
+ const slip = this.realizedSlip(observation);
2244
+ const isShrink = observation.resolution !== "fulfill" || slip > this.epsilon;
2245
+ if (isShrink) {
2246
+ if (observation.resolution === "timeout") {
2247
+ s.W = Math.max(1, Math.ceil(s.W / 2));
2248
+ } else {
2249
+ let d = this.delta / 2n;
2250
+ if (d < this.minPacketAmount) d = this.minPacketAmount;
2251
+ this.delta = d;
2252
+ s.delta = d.toString();
2253
+ }
2254
+ s.cleanStreak = 0;
2255
+ s.everShrunk = true;
2256
+ } else {
2257
+ s.cleanStreak += 1;
2258
+ if (s.cleanStreak >= this.cleanStreakLength) {
2259
+ s.cleanStreak = 0;
2260
+ const knob = s.lastWidened === "delta" ? "window" : "delta";
2261
+ if (knob === "window") {
2262
+ s.W = Math.min(this.maxWindow, s.W + 1);
2263
+ } else {
2264
+ const increment = this.delta0 ?? this.minPacketAmount;
2265
+ let d = s.everShrunk ? this.delta + increment : this.delta * 2n;
2266
+ if (this.maxPacketAmount !== void 0 && d > this.maxPacketAmount) {
2267
+ d = this.maxPacketAmount;
2268
+ }
2269
+ if (observation.remaining !== void 0 && observation.remaining > 0n) {
2270
+ const capAbs = this.deltaCapAbsolute(observation.remaining);
2271
+ if (d > capAbs) d = capAbs;
2272
+ }
2273
+ if (d < this.minPacketAmount) d = this.minPacketAmount;
2274
+ if (d < this.delta) d = this.delta;
2275
+ this.delta = d;
2276
+ s.delta = d.toString();
2277
+ }
2278
+ s.lastWidened = knob;
2279
+ }
2280
+ }
2281
+ s.updatedAt = this.now();
2282
+ await this.store.save(this.key, { ...s });
2283
+ }
2284
+ /**
2285
+ * Realized per-packet slip as a fraction: shortfall of the delivered
2286
+ * target amount vs the amount implied by the packet's own tape rate `R_i`
2287
+ * (spec §7.1 `Δcumulative` vs `⌊δ · R_i⌋` cross-check). `0` when the
2288
+ * inputs are unavailable or delivery met the tape.
2289
+ */
2290
+ realizedSlip(observation) {
2291
+ if (observation.resolution !== "fulfill" || observation.rate === void 0 || observation.sourceAmount === void 0 || observation.targetAmount === void 0 || observation.sourceAmount <= 0n) {
2292
+ return 0;
2293
+ }
2294
+ let expected;
2295
+ try {
2296
+ expected = applyRate({
2297
+ sourceAmount: observation.sourceAmount,
2298
+ fromScale: this.pair.from.assetScale,
2299
+ toScale: this.pair.to.assetScale,
2300
+ rate: observation.rate
2301
+ });
2302
+ } catch {
2303
+ return 0;
2304
+ }
2305
+ if (expected <= 0n || observation.targetAmount >= expected) return 0;
2306
+ const shortfall = expected - observation.targetAmount;
2307
+ return Number(shortfall * 1000000n / expected) / 1e6;
2308
+ }
2309
+ };
2310
+ function validateConfig(config) {
2311
+ if (typeof config.makerPubkey !== "string" || config.makerPubkey.length === 0) {
2312
+ throw new SwapControllerError("makerPubkey must be a non-empty string");
2313
+ }
2314
+ if (!config.pair || typeof config.pair !== "object" || !config.pair.from || !config.pair.to || typeof config.pair.from.chain !== "string" || typeof config.pair.to.chain !== "string") {
2315
+ throw new SwapControllerError(
2316
+ "pair must be a SwapPair with from/to chain descriptors"
2317
+ );
2318
+ }
2319
+ if (typeof config.advertisedSpread !== "number" || !Number.isFinite(config.advertisedSpread) || config.advertisedSpread <= 0) {
2320
+ throw new SwapControllerError(
2321
+ `advertisedSpread must be a positive finite fraction, got ${String(
2322
+ config.advertisedSpread
2323
+ )}`
2324
+ );
2325
+ }
2326
+ if (config.epsilonHalfSpreadFraction !== void 0 && (typeof config.epsilonHalfSpreadFraction !== "number" || !Number.isFinite(config.epsilonHalfSpreadFraction) || config.epsilonHalfSpreadFraction <= 0)) {
2327
+ throw new SwapControllerError(
2328
+ "epsilonHalfSpreadFraction must be a positive finite number"
2329
+ );
2330
+ }
2331
+ if (config.maxPacketAmount !== void 0 && (typeof config.maxPacketAmount !== "bigint" || config.maxPacketAmount <= 0n)) {
2332
+ throw new SwapControllerError("maxPacketAmount must be a positive bigint");
2333
+ }
2334
+ if (config.minPacketAmount !== void 0 && (typeof config.minPacketAmount !== "bigint" || config.minPacketAmount <= 0n)) {
2335
+ throw new SwapControllerError("minPacketAmount must be a positive bigint");
2336
+ }
2337
+ if (config.minPacketAmount !== void 0 && config.maxPacketAmount !== void 0 && config.minPacketAmount > config.maxPacketAmount) {
2338
+ throw new SwapControllerError("minPacketAmount must be <= maxPacketAmount");
2339
+ }
2340
+ if (config.maxWindow !== void 0 && (!Number.isInteger(config.maxWindow) || config.maxWindow < 1)) {
2341
+ throw new SwapControllerError("maxWindow must be an integer >= 1");
2342
+ }
2343
+ if (config.cleanStreakLength !== void 0 && (!Number.isInteger(config.cleanStreakLength) || config.cleanStreakLength < 1)) {
2344
+ throw new SwapControllerError("cleanStreakLength must be an integer >= 1");
2345
+ }
2346
+ if (config.coldStartDivisor !== void 0 && (!Number.isInteger(config.coldStartDivisor) || config.coldStartDivisor < 1)) {
2347
+ throw new SwapControllerError("coldStartDivisor must be an integer >= 1");
2348
+ }
2349
+ if (config.ewmaAlpha !== void 0 && (typeof config.ewmaAlpha !== "number" || !(config.ewmaAlpha > 0) || config.ewmaAlpha > 1)) {
2350
+ throw new SwapControllerError("ewmaAlpha must be in (0, 1]");
2351
+ }
2352
+ }
2353
+
2000
2354
  // src/settlement/evm.ts
2001
2355
  import { secp256k1 } from "@noble/curves/secp256k1.js";
2002
2356
  import { keccak_256 } from "@noble/hashes/sha3.js";
@@ -3039,17 +3393,21 @@ function verifyAccumulatedClaim(claim, signer, minaSignerClient) {
3039
3393
  };
3040
3394
  }
3041
3395
  export {
3396
+ AdaptiveDeltaController,
3042
3397
  ChunkManager,
3043
3398
  GiftWrapError,
3044
3399
  HandlerError,
3045
3400
  HandlerRegistry,
3046
3401
  IdentityError,
3402
+ InMemorySwapControllerStateStore,
3403
+ JsonFileSwapControllerStateStore,
3047
3404
  NodeError,
3048
3405
  PricingError,
3049
3406
  SWAP_HANDLER_REJECT_CODES,
3050
3407
  SWAP_HANDLER_REJECT_MESSAGES,
3051
3408
  SettlementTxError,
3052
3409
  StreamSwapError,
3410
+ SwapControllerError,
3053
3411
  SwapHandlerError,
3054
3412
  SwarmCoordinator,
3055
3413
  TurboUploadAdapter,
@@ -3085,10 +3443,12 @@ export {
3085
3443
  generateMnemonic,
3086
3444
  generateSolanaKeypair,
3087
3445
  hexToBytes2 as hexToBytes,
3446
+ isSwapControllerState,
3088
3447
  loadMinaSignerClient,
3089
3448
  minaHashToField,
3090
3449
  streamSwap,
3091
3450
  streamSwapControlled,
3451
+ swapControllerStateKey,
3092
3452
  unwrapSwapPacket,
3093
3453
  unwrapSwapPacketFromToon,
3094
3454
  uploadBlob,