@t2000/sdk 0.16.1 → 0.16.6

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.d.cts CHANGED
@@ -582,6 +582,13 @@ interface AutoTopUpResult {
582
582
  suiReceived: number;
583
583
  }
584
584
  declare function shouldAutoTopUp(client: SuiJsonRpcClient, address: string): Promise<boolean>;
585
+ /**
586
+ * Self-fund a USDC→SUI swap to replenish gas.
587
+ *
588
+ * Uses the agent's remaining SUI to pay for the swap gas (~0.007 SUI).
589
+ * This avoids the chicken-and-egg problem of needing gas station sponsorship
590
+ * to get gas, and works even when the gas station is down.
591
+ */
585
592
  declare function executeAutoTopUp(client: SuiJsonRpcClient, keypair: Ed25519Keypair): Promise<AutoTopUpResult>;
586
593
 
587
594
  type GasRequestType = 'bootstrap' | 'auto-topup' | 'fallback';
package/dist/index.d.ts CHANGED
@@ -582,6 +582,13 @@ interface AutoTopUpResult {
582
582
  suiReceived: number;
583
583
  }
584
584
  declare function shouldAutoTopUp(client: SuiJsonRpcClient, address: string): Promise<boolean>;
585
+ /**
586
+ * Self-fund a USDC→SUI swap to replenish gas.
587
+ *
588
+ * Uses the agent's remaining SUI to pay for the swap gas (~0.007 SUI).
589
+ * This avoids the chicken-and-egg problem of needing gas station sponsorship
590
+ * to get gas, and works even when the gas station is down.
591
+ */
585
592
  declare function executeAutoTopUp(client: SuiJsonRpcClient, keypair: Ed25519Keypair): Promise<AutoTopUpResult>;
586
593
 
587
594
  type GasRequestType = 'bootstrap' | 'auto-topup' | 'fallback';
package/dist/index.js CHANGED
@@ -2386,13 +2386,62 @@ function solveHashcash(challenge) {
2386
2386
  }
2387
2387
  }
2388
2388
 
2389
+ // src/gas/autoTopUp.ts
2390
+ var AUTO_TOPUP_MIN_SUI_FOR_GAS = 5000000n;
2391
+ async function shouldAutoTopUp(client, address) {
2392
+ const [suiBalance, usdcBalance] = await Promise.all([
2393
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
2394
+ client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
2395
+ ]);
2396
+ const suiRaw = BigInt(suiBalance.totalBalance);
2397
+ const usdcRaw = BigInt(usdcBalance.totalBalance);
2398
+ return suiRaw < AUTO_TOPUP_THRESHOLD && suiRaw >= AUTO_TOPUP_MIN_SUI_FOR_GAS && usdcRaw >= AUTO_TOPUP_MIN_USDC;
2399
+ }
2400
+ async function executeAutoTopUp(client, keypair) {
2401
+ const address = keypair.getPublicKey().toSuiAddress();
2402
+ const topupAmountHuman = Number(AUTO_TOPUP_AMOUNT) / 1e6;
2403
+ const { tx } = await buildSwapTx({
2404
+ client,
2405
+ address,
2406
+ fromAsset: "USDC",
2407
+ toAsset: "SUI",
2408
+ amount: topupAmountHuman
2409
+ });
2410
+ const result = await client.signAndExecuteTransaction({
2411
+ signer: keypair,
2412
+ transaction: tx,
2413
+ options: { showEffects: true, showBalanceChanges: true }
2414
+ });
2415
+ await client.waitForTransaction({ digest: result.digest });
2416
+ let suiReceived = 0;
2417
+ if (result.balanceChanges) {
2418
+ for (const change of result.balanceChanges) {
2419
+ if (change.coinType === SUPPORTED_ASSETS.SUI.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
2420
+ suiReceived += Number(change.amount) / Number(MIST_PER_SUI);
2421
+ }
2422
+ }
2423
+ }
2424
+ return {
2425
+ success: true,
2426
+ tx: result.digest,
2427
+ usdcSpent: topupAmountHuman,
2428
+ suiReceived: Math.abs(suiReceived)
2429
+ };
2430
+ }
2431
+
2389
2432
  // src/gas/gasStation.ts
2390
- async function requestGasSponsorship(txJson, sender, type) {
2391
- const txBytes = Buffer.from(txJson).toString("base64");
2433
+ async function requestGasSponsorship(txJson, sender, type, txBcsBytes) {
2434
+ const payload = { sender, type };
2435
+ if (txBcsBytes) {
2436
+ payload.txBcsBytes = txBcsBytes;
2437
+ } else {
2438
+ payload.txJson = txJson;
2439
+ payload.txBytes = Buffer.from(txJson).toString("base64");
2440
+ }
2392
2441
  const res = await fetch(`${API_BASE_URL}/api/gas`, {
2393
2442
  method: "POST",
2394
2443
  headers: { "Content-Type": "application/json" },
2395
- body: JSON.stringify({ txJson, txBytes, sender, type })
2444
+ body: JSON.stringify(payload)
2396
2445
  });
2397
2446
  const data = await res.json();
2398
2447
  if (!res.ok) {
@@ -2442,53 +2491,6 @@ async function getGasStatus(address) {
2442
2491
  return await res.json();
2443
2492
  }
2444
2493
 
2445
- // src/gas/autoTopUp.ts
2446
- async function shouldAutoTopUp(client, address) {
2447
- const [suiBalance, usdcBalance] = await Promise.all([
2448
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.SUI.type }),
2449
- client.getBalance({ owner: address, coinType: SUPPORTED_ASSETS.USDC.type })
2450
- ]);
2451
- const suiRaw = BigInt(suiBalance.totalBalance);
2452
- const usdcRaw = BigInt(usdcBalance.totalBalance);
2453
- return suiRaw < AUTO_TOPUP_THRESHOLD && usdcRaw >= AUTO_TOPUP_MIN_USDC;
2454
- }
2455
- async function executeAutoTopUp(client, keypair) {
2456
- const address = keypair.getPublicKey().toSuiAddress();
2457
- const topupAmountHuman = Number(AUTO_TOPUP_AMOUNT) / 1e6;
2458
- const { tx } = await buildSwapTx({
2459
- client,
2460
- address,
2461
- fromAsset: "USDC",
2462
- toAsset: "SUI",
2463
- amount: topupAmountHuman
2464
- });
2465
- const txJson = tx.serialize();
2466
- const sponsoredResult = await requestGasSponsorship(txJson, address, "auto-topup");
2467
- const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
2468
- const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
2469
- const result = await client.executeTransactionBlock({
2470
- transactionBlock: sponsoredResult.txBytes,
2471
- signature: [agentSig, sponsoredResult.sponsorSignature],
2472
- options: { showEffects: true, showBalanceChanges: true }
2473
- });
2474
- await client.waitForTransaction({ digest: result.digest });
2475
- let suiReceived = 0;
2476
- if (result.balanceChanges) {
2477
- for (const change of result.balanceChanges) {
2478
- if (change.coinType === SUPPORTED_ASSETS.SUI.type && change.owner && typeof change.owner === "object" && "AddressOwner" in change.owner && change.owner.AddressOwner === address) {
2479
- suiReceived += Number(change.amount) / Number(MIST_PER_SUI);
2480
- }
2481
- }
2482
- }
2483
- reportGasUsage(address, result.digest, 0, 0, "auto-topup");
2484
- return {
2485
- success: true,
2486
- tx: result.digest,
2487
- usdcSpent: topupAmountHuman,
2488
- suiReceived: Math.abs(suiReceived)
2489
- };
2490
- }
2491
-
2492
2494
  // src/gas/manager.ts
2493
2495
  function extractGasCost(effects) {
2494
2496
  if (!effects?.gasUsed) return 0;
@@ -2516,11 +2518,12 @@ async function trySelfFunded(client, keypair, tx) {
2516
2518
  gasCostSui: extractGasCost(result.effects)
2517
2519
  };
2518
2520
  }
2519
- async function tryAutoTopUpThenSelfFund(client, keypair, tx) {
2521
+ async function tryAutoTopUpThenSelfFund(client, keypair, buildTx) {
2520
2522
  const address = keypair.getPublicKey().toSuiAddress();
2521
2523
  const canTopUp = await shouldAutoTopUp(client, address);
2522
2524
  if (!canTopUp) return null;
2523
2525
  await executeAutoTopUp(client, keypair);
2526
+ const tx = await buildTx();
2524
2527
  tx.setSender(address);
2525
2528
  const result = await client.signAndExecuteTransaction({
2526
2529
  signer: keypair,
@@ -2538,8 +2541,15 @@ async function tryAutoTopUpThenSelfFund(client, keypair, tx) {
2538
2541
  async function trySponsored(client, keypair, tx) {
2539
2542
  const address = keypair.getPublicKey().toSuiAddress();
2540
2543
  tx.setSender(address);
2541
- const txJson = tx.serialize();
2542
- const sponsoredResult = await requestGasSponsorship(txJson, address);
2544
+ let txJson;
2545
+ let txBcsBase64;
2546
+ try {
2547
+ txJson = tx.serialize();
2548
+ } catch {
2549
+ const bcsBytes = await tx.build({ client });
2550
+ txBcsBase64 = Buffer.from(bcsBytes).toString("base64");
2551
+ }
2552
+ const sponsoredResult = await requestGasSponsorship(txJson ?? "", address, void 0, txBcsBase64);
2543
2553
  const sponsoredTxBytes = Buffer.from(sponsoredResult.txBytes, "base64");
2544
2554
  const { signature: agentSig } = await keypair.signTransaction(sponsoredTxBytes);
2545
2555
  const result = await client.executeTransactionBlock({
@@ -2575,8 +2585,7 @@ async function executeWithGas(client, keypair, buildTx, options) {
2575
2585
  errors.push(`self-funded: ${msg}`);
2576
2586
  }
2577
2587
  try {
2578
- const tx = await buildTx();
2579
- const result = await tryAutoTopUpThenSelfFund(client, keypair, tx);
2588
+ const result = await tryAutoTopUpThenSelfFund(client, keypair, buildTx);
2580
2589
  if (result) return result;
2581
2590
  errors.push("auto-topup: not eligible (low USDC or sufficient SUI)");
2582
2591
  } catch (err) {
@@ -4338,20 +4347,22 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4338
4347
  if (!params.usdAmount || params.usdAmount <= 0) {
4339
4348
  throw new T2000Error("INVALID_AMOUNT", "Strategy investment must be > $0");
4340
4349
  }
4350
+ this.enforcer.check({ operation: "invest", amount: params.usdAmount });
4341
4351
  const bal = await queryBalance(this.client, this._address);
4342
4352
  if (bal.available < params.usdAmount) {
4343
4353
  throw new T2000Error("INSUFFICIENT_BALANCE", `Insufficient balance. Available: $${bal.available.toFixed(2)}, requested: $${params.usdAmount.toFixed(2)}`);
4344
4354
  }
4345
4355
  const buys = [];
4356
+ const allocEntries = Object.entries(definition.allocations);
4346
4357
  if (params.dryRun) {
4347
- const swapAdapter = this.registry.listSwap()[0];
4348
- for (const [asset, pct] of Object.entries(definition.allocations)) {
4358
+ const swapAdapter2 = this.registry.listSwap()[0];
4359
+ for (const [asset, pct] of allocEntries) {
4349
4360
  const assetUsd = params.usdAmount * (pct / 100);
4350
4361
  let estAmount = 0;
4351
4362
  let estPrice = 0;
4352
4363
  try {
4353
- if (swapAdapter) {
4354
- const quote = await swapAdapter.getQuote("USDC", asset, assetUsd);
4364
+ if (swapAdapter2) {
4365
+ const quote = await swapAdapter2.getQuote("USDC", asset, assetUsd);
4355
4366
  estAmount = quote.expectedOutput;
4356
4367
  estPrice = assetUsd / estAmount;
4357
4368
  }
@@ -4361,27 +4372,76 @@ To sell investment: t2000 invest sell ${params.amount} ${fromAsset}`,
4361
4372
  }
4362
4373
  return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: 0, gasMethod: "self-funded" };
4363
4374
  }
4364
- let totalGas = 0;
4365
- let gasMethod = "self-funded";
4366
- for (const [asset, pct] of Object.entries(definition.allocations)) {
4367
- const assetUsd = params.usdAmount * (pct / 100);
4368
- const result = await this.investBuy({ asset, usdAmount: assetUsd });
4375
+ const swapAdapter = this.registry.listSwap()[0];
4376
+ if (!swapAdapter?.addSwapToTx) {
4377
+ throw new T2000Error("PROTOCOL_UNAVAILABLE", "Swap adapter does not support composable PTB");
4378
+ }
4379
+ const swapMetas = [];
4380
+ const gasResult = await executeWithGas(this.client, this.keypair, async () => {
4381
+ const tx = new Transaction();
4382
+ tx.setSender(this._address);
4383
+ const usdcCoins = await this._fetchCoins(SUPPORTED_ASSETS.USDC.type);
4384
+ if (usdcCoins.length === 0) throw new T2000Error("INSUFFICIENT_BALANCE", "No USDC coins found");
4385
+ const mergedUsdc = this._mergeCoinsInTx(tx, usdcCoins);
4386
+ const splitAmounts = allocEntries.map(
4387
+ ([, pct]) => BigInt(Math.floor(params.usdAmount * (pct / 100) * 10 ** SUPPORTED_ASSETS.USDC.decimals))
4388
+ );
4389
+ const splitCoins = tx.splitCoins(mergedUsdc, splitAmounts);
4390
+ const outputCoins = [];
4391
+ for (let i = 0; i < allocEntries.length; i++) {
4392
+ const [asset] = allocEntries[i];
4393
+ const assetUsd = params.usdAmount * (allocEntries[i][1] / 100);
4394
+ const { outputCoin, estimatedOut, toDecimals } = await swapAdapter.addSwapToTx(
4395
+ tx,
4396
+ this._address,
4397
+ splitCoins[i],
4398
+ "USDC",
4399
+ asset,
4400
+ assetUsd
4401
+ );
4402
+ outputCoins.push(outputCoin);
4403
+ swapMetas.push({ asset, usdAmount: assetUsd, estimatedOut, toDecimals });
4404
+ }
4405
+ tx.transferObjects(outputCoins, this._address);
4406
+ return tx;
4407
+ });
4408
+ const digest = gasResult.digest;
4409
+ const now = (/* @__PURE__ */ new Date()).toISOString();
4410
+ for (const meta of swapMetas) {
4411
+ const amount = meta.estimatedOut / 10 ** meta.toDecimals;
4412
+ const price = meta.usdAmount / amount;
4413
+ this.portfolio.recordBuy({
4414
+ id: `inv_${Date.now()}_${meta.asset}`,
4415
+ type: "buy",
4416
+ asset: meta.asset,
4417
+ amount,
4418
+ price,
4419
+ usdValue: meta.usdAmount,
4420
+ fee: 0,
4421
+ tx: digest,
4422
+ timestamp: now
4423
+ });
4369
4424
  this.portfolio.recordStrategyBuy(params.strategy, {
4370
- id: `strat_${Date.now()}_${asset}`,
4425
+ id: `strat_${Date.now()}_${meta.asset}`,
4371
4426
  type: "buy",
4372
- asset,
4373
- amount: result.amount,
4374
- price: result.price,
4375
- usdValue: assetUsd,
4376
- fee: result.fee,
4377
- tx: result.tx,
4378
- timestamp: (/* @__PURE__ */ new Date()).toISOString()
4427
+ asset: meta.asset,
4428
+ amount,
4429
+ price,
4430
+ usdValue: meta.usdAmount,
4431
+ fee: 0,
4432
+ tx: digest,
4433
+ timestamp: now
4379
4434
  });
4380
- buys.push({ asset, usdAmount: assetUsd, amount: result.amount, price: result.price, tx: result.tx });
4381
- totalGas += result.gasCost;
4382
- gasMethod = result.gasMethod;
4435
+ buys.push({ asset: meta.asset, usdAmount: meta.usdAmount, amount, price, tx: digest });
4383
4436
  }
4384
- return { success: true, strategy: params.strategy, totalInvested: params.usdAmount, buys, gasCost: totalGas, gasMethod };
4437
+ return {
4438
+ success: true,
4439
+ strategy: params.strategy,
4440
+ totalInvested: params.usdAmount,
4441
+ buys,
4442
+ gasCost: gasResult.gasCostSui,
4443
+ gasMethod: gasResult.gasMethod
4444
+ };
4385
4445
  }
4386
4446
  async sellStrategy(params) {
4387
4447
  this.enforcer.assertNotLocked();