@fil-b/foc-storage-mcp 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +136 -163
  2. package/dist/mcp-server.js +1094 -429
  3. package/package.json +4 -2
@@ -15,50 +15,51 @@ import { Agent } from "@mastra/core/agent";
15
15
  // src/mastra/tools/dataset-tools.ts
16
16
  import { createTool } from "@mastra/core";
17
17
 
18
- // src/types/core.ts
19
- import {
20
- TIME_CONSTANTS,
21
- SIZE_CONSTANTS,
22
- TOKENS
23
- } from "@filoz/synapse-sdk";
24
-
25
18
  // src/types/schemas.ts
26
19
  import { z as z2 } from "zod";
27
20
 
28
21
  // src/config/index.ts
22
+ import { calibration, mainnet } from "@filoz/synapse-core/chains";
29
23
  import { config } from "dotenv";
30
24
  import { z } from "zod";
31
25
  config();
32
26
  var EnvSchema = z.object({
33
27
  PRIVATE_KEY: z.string().min(1, "PRIVATE_KEY is required"),
34
28
  FILECOIN_NETWORK: z.enum(["mainnet", "calibration"]).default("calibration"),
35
- TOTAL_STORAGE_NEEDED_GiB: z.coerce.number().default(1024),
36
- PERSISTENCE_PERIOD_DAYS: z.coerce.number().default(365),
37
- RUNOUT_NOTIFICATION_THRESHOLD_DAYS: z.coerce.number().default(10)
29
+ TOTAL_STORAGE_NEEDED_GiB: z.coerce.number().default(150),
30
+ PERSISTENCE_PERIOD_DAYS: z.coerce.number().default(365).refine((value) => value >= 30, { message: "PERSISTENCE_PERIOD_DAYS must be greater than or equal to 30" }),
31
+ RUNOUT_NOTIFICATION_THRESHOLD_DAYS: z.coerce.number().default(45).refine((value) => value >= 30, { message: "RUNOUT_NOTIFICATION_THRESHOLD_DAYS must be greater than or equal to 30" })
38
32
  });
39
- var env = EnvSchema.parse(process.env);
33
+ var env = {
34
+ ...EnvSchema.parse(process.env),
35
+ ...{
36
+ CDN_DATASET_FEE: 10n ** 18n
37
+ }
38
+ };
40
39
  var NETWORK_CONFIGS = {
41
40
  mainnet: {
42
41
  chainId: 314,
43
42
  name: "Filecoin Mainnet",
44
- rpcUrl: "https://api.node.glif.io/rpc/v1"
43
+ rpcUrl: mainnet.rpcUrls.default.http
45
44
  },
46
45
  calibration: {
47
46
  chainId: 314159,
48
47
  name: "Filecoin Calibration",
49
- rpcUrl: "https://api.calibration.node.glif.io/rpc/v1"
48
+ rpcUrl: calibration.rpcUrls.default.http
50
49
  }
51
50
  };
51
+ var CONTRACTS = env.FILECOIN_NETWORK === "calibration" ? calibration.contracts : mainnet.contracts;
52
52
  var MAX_UINT256 = 2n ** 256n - 1n;
53
53
  var DATA_SET_CREATION_FEE = BigInt(0.1 * 10 ** 18);
54
54
  var BYTES_PER_TIB = 1024n * 1024n * 1024n * 1024n;
55
55
  var BYTES_PER_GIB = 1024n * 1024n * 1024n;
56
+ var CDN_EGRESS_RATE_PER_TIB = 7;
56
57
  var DEFAULT_EXPECTED_STORAGE_BYTES = 1024 * 1024 * 1024 * 1024;
57
58
 
58
59
  // src/types/schemas.ts
59
- import { SIZE_CONSTANTS as SIZE_CONSTANTS2 } from "@filoz/synapse-sdk";
60
+ import { SIZE_CONSTANTS } from "@filoz/synapse-core/utils";
60
61
  var GetDatasetsSchema = z2.object({
61
- includeAllDatasets: z2.boolean().optional().describe("Include all datasets. Default: false"),
62
+ includeAllDatasets: z2.boolean().optional().default(true).describe("Include all datasets. Default: true"),
62
63
  filterByCDN: z2.boolean().optional().describe("Filter to only CDN-enabled datasets. Default: false. If includeAllDatasets is true, this will be ignored")
63
64
  });
64
65
  var GetDatasetSchema = z2.object({
@@ -83,13 +84,16 @@ var UploadFileSchema = z2.object({
83
84
  autoPayment: z2.boolean().optional().default(true).describe("Automatically process payment if insufficient balance. Default: true")
84
85
  });
85
86
  var GetBalancesSchema = z2.object({
86
- storageCapacityBytes: z2.number().optional().default(env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS2.GiB)).describe("Storage capacity in bytes. Default: 1 TB. This is used to calculate the storage needs and the deposit needed."),
87
+ storageCapacityBytes: z2.number().optional().default(env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS.GiB)).describe("Storage capacity in bytes. Default: 1 TB. This is used to calculate the storage needs and the deposit needed."),
87
88
  persistencePeriodDays: z2.number().optional().default(env.PERSISTENCE_PERIOD_DAYS).describe("Persistence period in days. Default: 365. This is used to calculate the storage needs and the deposit needed."),
88
89
  notificationThresholdDays: z2.number().optional().default(env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS).describe("Notification threshold in days. Default: 10. This is used check if the user needs to top up their storage balance before the storage balance runs out.")
89
90
  });
90
91
  var ProcessPaymentSchema = z2.object({
91
92
  depositAmount: z2.number().optional().default(0).describe("Amount to deposit in USDFC. Default: 0. If not provided, the tool will check the balance and deposit the necessary amount.")
92
93
  });
94
+ var ProcessWithdrawalSchema = z2.object({
95
+ withdrawalAmount: z2.number().optional().default(0).describe("Amount to withdraw in USDFC. Default: 0. If not provided, the tool will check the balance and withdraw the necessary amount.")
96
+ });
93
97
  var GetProvidersSchema = z2.object({
94
98
  onlyApproved: z2.boolean().optional().default(true).describe("Filter to only approved providers. Default: true")
95
99
  });
@@ -121,7 +125,24 @@ var DataSetSchema = z2.object({
121
125
  totalDatasetSizeMessage: z2.string(),
122
126
  dataSetPieces: z2.array(DataSetPieceSchema)
123
127
  });
124
- var FormattedStorageBalanceResultSchema = z2.object({
128
+ var StorageBalanceResultSchema = z2.object({
129
+ filBalance: z2.bigint(),
130
+ usdfcBalance: z2.bigint(),
131
+ availableStorageFundsUsdfc: z2.bigint(),
132
+ depositNeeded: z2.bigint(),
133
+ availableToFreeUp: z2.bigint(),
134
+ daysLeftAtMaxBurnRate: z2.number(),
135
+ daysLeftAtBurnRate: z2.number(),
136
+ isRateSufficient: z2.boolean(),
137
+ isLockupSufficient: z2.boolean(),
138
+ isSufficient: z2.boolean(),
139
+ currentStorageMonthlyRate: z2.bigint(),
140
+ maxStorageMonthlyRate: z2.bigint()
141
+ });
142
+ var StorageBalanceResultSerializedSchema = z2.object({
143
+ filBalance: z2.string(),
144
+ usdfcBalance: z2.string(),
145
+ availableStorageFundsUsdfc: z2.string(),
125
146
  depositNeeded: z2.string(),
126
147
  availableToFreeUp: z2.string(),
127
148
  daysLeftAtMaxBurnRate: z2.number(),
@@ -129,14 +150,27 @@ var FormattedStorageBalanceResultSchema = z2.object({
129
150
  isRateSufficient: z2.boolean(),
130
151
  isLockupSufficient: z2.boolean(),
131
152
  isSufficient: z2.boolean(),
132
- availableFunds: z2.string(),
133
- currentMonthlyRate: z2.string(),
134
- maxMonthlyRate: z2.string()
153
+ currentStorageMonthlyRate: z2.string(),
154
+ maxStorageMonthlyRate: z2.string()
155
+ });
156
+ var FormattedStorageBalanceResultSchema = z2.object({
157
+ filBalance: z2.string(),
158
+ usdfcBalance: z2.string(),
159
+ availableStorageFundsUsdfc: z2.string(),
160
+ currentStorageMonthlyRate: z2.string(),
161
+ maxStorageMonthlyRate: z2.string(),
162
+ daysLeftAtMaxBurnRate: z2.string(),
163
+ daysLeftAtBurnRate: z2.string(),
164
+ depositNeeded: z2.string(),
165
+ availableToFreeUp: z2.string(),
166
+ isRateSufficient: z2.boolean(),
167
+ isLockupSufficient: z2.boolean(),
168
+ isSufficient: z2.boolean()
135
169
  });
136
170
  var GetProvidersOutputSchema = z2.object({
137
171
  success: z2.boolean(),
138
172
  // Success fields
139
- providers: z2.array(z2.any()).optional(),
173
+ providers: z2.array(z2.custom()),
140
174
  count: z2.number().optional(),
141
175
  // Error fields
142
176
  error: z2.string().optional(),
@@ -166,14 +200,24 @@ var ProcessPaymentOutputSchema = z2.object({
166
200
  deposit: z2.number()
167
201
  }).optional(),
168
202
  available: z2.number().optional(),
203
+ progressLog: z2.array(z2.string()).optional(),
204
+ // Error fields
205
+ error: z2.string().optional()
206
+ });
207
+ var ProcessWithdrawalOutputSchema = z2.object({
208
+ success: z2.boolean(),
209
+ message: z2.string(),
210
+ txHash: z2.string().nullable().optional(),
211
+ progressLog: z2.array(z2.string()).optional(),
169
212
  // Error fields
170
213
  error: z2.string().optional()
171
214
  });
172
215
  var GetBalancesOutputSchema = z2.object({
173
216
  success: z2.boolean(),
174
217
  // Success fields
175
- checkStorageBalanceResultFormatted: z2.any().optional(),
176
- checkStorageBalanceResult: z2.any().optional(),
218
+ checkStorageBalanceResultFormatted: FormattedStorageBalanceResultSchema.optional(),
219
+ checkStorageBalanceResult: StorageBalanceResultSerializedSchema.optional(),
220
+ progressLog: z2.array(z2.string()).optional(),
177
221
  // Error fields
178
222
  error: z2.string().optional(),
179
223
  message: z2.string().optional()
@@ -181,8 +225,9 @@ var GetBalancesOutputSchema = z2.object({
181
225
  var GetDatasetsOutputSchema = z2.object({
182
226
  success: z2.boolean(),
183
227
  // Success fields
184
- datasets: z2.array(DataSetSchema).optional(),
228
+ datasets: z2.array(z2.custom()).optional(),
185
229
  count: z2.number().optional(),
230
+ progressLog: z2.array(z2.string()).optional(),
186
231
  // Error fields
187
232
  error: z2.string().optional(),
188
233
  // Common field
@@ -191,7 +236,7 @@ var GetDatasetsOutputSchema = z2.object({
191
236
  var GetDatasetOutputSchema = z2.object({
192
237
  success: z2.boolean(),
193
238
  // Success fields
194
- dataset: DataSetSchema,
239
+ dataset: z2.custom().optional(),
195
240
  // Error fields
196
241
  error: z2.string().optional(),
197
242
  // Common field
@@ -200,18 +245,19 @@ var GetDatasetOutputSchema = z2.object({
200
245
  var CreateDatasetOutputSchema = z2.object({
201
246
  success: z2.boolean(),
202
247
  // Success fields
203
- datasetId: z2.string().optional(),
204
- txHash: z2.string().optional(),
248
+ datasetId: z2.string().nullable().optional(),
249
+ txHash: z2.string().nullable().optional(),
250
+ progressLog: z2.array(z2.string()).optional(),
205
251
  // Error fields
206
- error: z2.string().optional(),
252
+ error: z2.string().nullable().optional(),
207
253
  // Common field
208
254
  message: z2.string()
209
255
  });
210
256
 
211
257
  // src/lib/calculations.ts
212
258
  import Decimal from "decimal.js";
213
- import { SIZE_CONSTANTS as SIZE_CONSTANTS3 } from "@filoz/synapse-sdk";
214
- import { getSizeFromPieceCID } from "@filoz/synapse-sdk/piece";
259
+ import { getSize } from "@filoz/synapse-core/piece";
260
+ import { SIZE_CONSTANTS as SIZE_CONSTANTS2 } from "@filoz/synapse-core/utils";
215
261
  Decimal.set({
216
262
  precision: 34,
217
263
  rounding: Decimal.ROUND_HALF_UP,
@@ -221,31 +267,6 @@ Decimal.set({
221
267
  minE: -9e15,
222
268
  modulo: Decimal.ROUND_DOWN
223
269
  });
224
- var toDecimal = (value) => value instanceof Decimal ? value : new Decimal(value.toString());
225
- var bytesToKiB = (bytes) => toDecimal(bytes).div(new Decimal(SIZE_CONSTANTS3.KiB.toString()));
226
- var bytesToMiB = (bytes) => toDecimal(bytes).div(new Decimal(SIZE_CONSTANTS3.MiB.toString()));
227
- var bytesToGiB = (bytes) => toDecimal(bytes).div(new Decimal(SIZE_CONSTANTS3.GiB.toString()));
228
- var getPieceInfoFromCidBytes = (input) => {
229
- const sizeBytes = BigInt(getSizeFromPieceCID(input));
230
- return {
231
- sizeBytes,
232
- sizeKiB: bytesToKiB(sizeBytes).toNumber(),
233
- sizeMiB: bytesToMiB(sizeBytes).toNumber(),
234
- sizeGiB: bytesToGiB(sizeBytes).toNumber()
235
- };
236
- };
237
- var sizeInfoMessage = (sizeInfo) => {
238
- if (sizeInfo.sizeInGB > 0.1) {
239
- return `Dataset size: ${sizeInfo.sizeInGB.toFixed(4)} GB`;
240
- }
241
- if (sizeInfo.sizeInMiB > 0.1) {
242
- return `Dataset size: ${sizeInfo.sizeInMiB.toFixed(4)} MB`;
243
- }
244
- if (sizeInfo.sizeInKiB > 0.1) {
245
- return `Dataset size: ${sizeInfo.sizeInKiB.toFixed(4)} KB`;
246
- }
247
- return `Dataset size: ${sizeInfo.sizeInBytes} Bytes`;
248
- };
249
270
 
250
271
  // src/lib/synapse.ts
251
272
  import { Synapse } from "@filoz/synapse-sdk";
@@ -254,7 +275,7 @@ var getSynapseInstance = async () => {
254
275
  const synapse = await Synapse.create(
255
276
  {
256
277
  privateKey: env.PRIVATE_KEY,
257
- rpcURL: NETWORK_CONFIGS[network].rpcUrl
278
+ rpcURL: NETWORK_CONFIGS[network].rpcUrl[0]
258
279
  }
259
280
  );
260
281
  return synapse;
@@ -284,6 +305,11 @@ function createErrorResponse(errorType, message, additionalData) {
284
305
  ...additionalData
285
306
  };
286
307
  }
308
+ function toBaseUnits(amount, decimals = 18) {
309
+ const [whole, decimal = ""] = amount.split(".");
310
+ const paddedDecimal = decimal.padEnd(decimals, "0").slice(0, decimals);
311
+ return BigInt(whole + paddedDecimal);
312
+ }
287
313
  function fromBaseUnits(amount, decimals = 18) {
288
314
  const str = amount.toString().padStart(decimals + 1, "0");
289
315
  const whole = str.slice(0, -decimals) || "0";
@@ -307,182 +333,223 @@ function serializeBigInt(obj) {
307
333
  }
308
334
  return obj;
309
335
  }
336
+ function getExplorerUrl(txHash) {
337
+ const isMainnet = env.FILECOIN_NETWORK === "mainnet";
338
+ const baseUrl = isMainnet ? "https://filecoin.blockscout.com/tx" : "https://filecoin-testnet.blockscout.com/tx";
339
+ return `${baseUrl}/${txHash}`;
340
+ }
310
341
 
311
- // src/services/wallet-service.ts
312
- import { ethers, Wallet, JsonRpcProvider } from "ethers";
313
-
314
- // src/services/payment-service.ts
315
- import { TIME_CONSTANTS as TIME_CONSTANTS2 } from "@filoz/synapse-sdk";
316
- async function processStoragePayment(synapse, depositAmount, persistenceDays) {
317
- const warmStorageAddress = synapse.getWarmStorageAddress();
318
- const epochs = TIME_CONSTANTS2.EPOCHS_PER_DAY * BigInt(persistenceDays);
319
- if (depositAmount > 0n) {
320
- const tx = await synapse.payments.depositWithPermitAndApproveOperator(
321
- depositAmount,
322
- warmStorageAddress,
323
- MAX_UINT256,
324
- MAX_UINT256,
325
- epochs
326
- );
327
- const receipt = await tx.wait(1);
328
- return { txHash: receipt?.hash || tx.hash, success: true };
329
- } else {
330
- const tx = await synapse.payments.approveService(
331
- warmStorageAddress,
332
- MAX_UINT256,
333
- MAX_UINT256,
334
- epochs
335
- );
336
- const receipt = await tx.wait(1);
337
- return { txHash: receipt?.hash || tx.hash, success: true };
342
+ // src/lib/errors.ts
343
+ function getTopUpMessage(isTestnet) {
344
+ if (isTestnet) {
345
+ return `
346
+ To resolve insufficient balance errors:
347
+ " For tFIL: Visit https://faucet.calibration.fildev.network/
348
+ " For tUSDFC: Visit https://forest-explorer.chainsafe.dev/faucet/calibnet_usdfc
349
+ `;
350
+ }
351
+ return `
352
+ To resolve insufficient balance errors:
353
+ " Top up your FIL or USDFC balance
354
+ " Ensure you have sufficient funds for the operation
355
+ `;
356
+ }
357
+ function synapseErrorHandler(error) {
358
+ const errorParts = ["Payment processing failed"];
359
+ if (error?.cause) {
360
+ const cause = typeof error.cause === "string" ? error.cause : JSON.stringify(error.cause);
361
+ errorParts.push(`Cause: ${cause}`);
362
+ }
363
+ if (error?.shortMessage) {
364
+ errorParts.push(`Message: ${error.shortMessage}`);
338
365
  }
366
+ if (error?.details) {
367
+ errorParts.push(`Details: ${error.details}`);
368
+ }
369
+ const isTestnet = env.FILECOIN_NETWORK === "calibration";
370
+ errorParts.push(getTopUpMessage(isTestnet));
371
+ return errorParts.join("\n");
339
372
  }
340
373
 
341
374
  // src/services/dataset-service.ts
342
- import { PDPServer, WarmStorageService } from "@filoz/synapse-sdk";
343
- var getDatasets = async (withCDN = false, includeAll = false, onlyDatasetId = void 0) => {
344
- try {
345
- const synapse = await getSynapseInstance();
346
- const warmStorageAddress = synapse.getWarmStorageAddress();
347
- const warmStorageService = await WarmStorageService.create(synapse.getProvider(), warmStorageAddress);
348
- const datasets = await synapse.storage.findDataSets();
349
- if (datasets.length === 0) {
375
+ import { metadataArrayToObject } from "@filoz/synapse-core/utils";
376
+
377
+ // src/services/viem.ts
378
+ import { createPublicClient, createWalletClient, http } from "viem";
379
+ import { privateKeyToAccount } from "viem/accounts";
380
+ import { calibration as calibration2, mainnet as mainnet2 } from "@filoz/synapse-core/chains";
381
+ var account = privateKeyToAccount(env.PRIVATE_KEY);
382
+ var chain = env.FILECOIN_NETWORK === "calibration" ? calibration2 : mainnet2;
383
+ var client = createWalletClient({
384
+ account,
385
+ chain,
386
+ transport: http()
387
+ });
388
+ var publicClient = createPublicClient({
389
+ chain,
390
+ transport: http()
391
+ });
392
+
393
+ // src/services/dataset-service.ts
394
+ import { createDataSet, getDataSet, getDataSets, getPieces } from "@filoz/synapse-core/warm-storage";
395
+ import * as SP from "@filoz/synapse-core/sp";
396
+ import { readContract } from "viem/actions";
397
+ var getPiecesWithMetadata = async (dataSetId, pieces) => {
398
+ return await Promise.all(
399
+ pieces.map(async (piece) => {
400
+ const metadata = await readContract(client, {
401
+ address: CONTRACTS.storageView.address,
402
+ abi: CONTRACTS.storageView.abi,
403
+ functionName: "getAllPieceMetadata",
404
+ args: [dataSetId, BigInt(piece.id)]
405
+ });
350
406
  return {
351
- success: true,
352
- datasets: [],
353
- count: 0,
354
- message: "No datasets found. Upload files to create your first dataset."
407
+ ...piece,
408
+ metadata: metadataArrayToObject(metadata)
355
409
  };
356
- }
357
- const providers = await Promise.all(
358
- datasets.map(
359
- (dataset) => synapse.getProviderInfo(dataset.providerId).catch(() => null)
360
- )
361
- );
362
- const userAddress = await synapse.getSigner().getAddress();
363
- const filteredDatasets = datasets.filter((dataset) => {
364
- if (onlyDatasetId !== void 0) {
365
- return dataset.pdpVerifierDataSetId === onlyDatasetId;
366
- }
367
- if (includeAll) {
368
- return true;
369
- }
370
- return dataset.withCDN === withCDN;
410
+ })
411
+ );
412
+ };
413
+ var getDatasetsService = async (withCDN = false, includeAll = false) => {
414
+ const dataSets = (await getDataSets(publicClient, {
415
+ address: account.address
416
+ })).filter((dataset) => includeAll || withCDN && dataset.cdn);
417
+ const datasetsWithPieces = await Promise.all(dataSets.map(async (dataset) => {
418
+ const pieces = await getPieces(publicClient, {
419
+ address: account.address,
420
+ dataSet: dataset
371
421
  });
372
- if (filteredDatasets.length === 0) {
373
- return {
374
- success: true,
375
- datasets: [],
376
- count: 0,
377
- message: `No datasets found with the given criteria`
378
- };
379
- }
380
- const enrichedDatasets = await Promise.all(
381
- filteredDatasets.map(async (dataset) => {
382
- const provider = providers.find((p) => p?.id === dataset.providerId);
383
- const serviceURL = provider.products.PDP?.data.serviceURL || "";
384
- try {
385
- const pdpServer = new PDPServer(null, serviceURL);
386
- const data = await pdpServer.getDataSet(dataset.pdpVerifierDataSetId).then((data2) => {
387
- data2.pieces.reverse();
388
- return data2;
389
- });
390
- const pieces = data.pieces.reduce(
391
- (acc, piece) => {
392
- acc[piece.pieceCid.toV1().toString()] = getPieceInfoFromCidBytes(piece.pieceCid);
393
- return acc;
394
- },
395
- {}
396
- );
397
- const piecesMetadata = (await Promise.all(data.pieces.map(async (piece) => {
398
- return { pieceId: piece.pieceId, metadata: await warmStorageService.getPieceMetadata(dataset.pdpVerifierDataSetId, piece.pieceId) };
399
- }))).reduce((acc, piece) => {
400
- acc[piece.pieceId] = piece.metadata;
401
- return acc;
402
- }, {});
403
- const getRetrievalUrl = (pieceCid) => {
404
- if (dataset.withCDN) {
405
- return `https://${userAddress}.calibration.filbeam.io/${pieceCid}`;
406
- } else {
407
- const endsWithSlash = serviceURL.endsWith("/");
408
- const serviceURLWithoutSlash = endsWithSlash ? serviceURL.slice(0, -1) : serviceURL;
409
- return `${serviceURLWithoutSlash}/piece/${pieceCid}`;
410
- }
411
- };
412
- const datasetSizeInfo = data.pieces.reduce((acc, piece) => {
413
- acc.sizeInBytes += Number(pieces[piece.pieceCid.toV1().toString()].sizeBytes);
414
- acc.sizeInKiB += Number(pieces[piece.pieceCid.toV1().toString()].sizeKiB);
415
- acc.sizeInMiB += Number(pieces[piece.pieceCid.toV1().toString()].sizeMiB);
416
- acc.sizeInGB += Number(pieces[piece.pieceCid.toV1().toString()].sizeGiB);
417
- return acc;
418
- }, { sizeInBytes: 0, sizeInKiB: 0, sizeInMiB: 0, sizeInGB: 0, message: "" });
419
- const dataSetPieces = data.pieces.map((piece) => ({
420
- pieceCid: piece.pieceCid.toV1().toString(),
421
- retrievalUrl: getRetrievalUrl(piece.pieceCid.toV1().toString()),
422
- sizes: pieces[piece.pieceCid.toV1().toString()],
423
- metadata: piecesMetadata[piece.pieceId]
424
- }));
425
- return {
426
- datasetId: dataset.pdpVerifierDataSetId,
427
- withCDN: dataset.withCDN,
428
- datasetMetadata: dataset.metadata,
429
- totalDatasetSizeMessage: sizeInfoMessage(datasetSizeInfo),
430
- dataSetPieces
431
- };
432
- } catch (error) {
433
- return null;
434
- }
435
- })
436
- );
422
+ return {
423
+ ...dataset,
424
+ pieces: await getPiecesWithMetadata(dataset.dataSetId, pieces.pieces)
425
+ };
426
+ }));
427
+ return datasetsWithPieces;
428
+ };
429
+ var getDataSetService = async (datasetId) => {
430
+ const dataset = await getDataSet(publicClient, {
431
+ dataSetId: BigInt(datasetId)
432
+ });
433
+ const pieces = await getPieces(publicClient, {
434
+ address: account.address,
435
+ dataSet: dataset
436
+ });
437
+ return {
438
+ ...dataset,
439
+ pieces: await getPiecesWithMetadata(dataset.dataSetId, pieces.pieces)
440
+ };
441
+ };
442
+ var createDataSetService = async (provider, cdn, metadata) => {
443
+ try {
444
+ const dataset = await createDataSet(client, {
445
+ cdn,
446
+ payee: provider.payee,
447
+ endpoint: provider.pdp.serviceURL,
448
+ metadata
449
+ });
450
+ const { dataSetId } = await SP.pollForDataSetCreationStatus({ statusUrl: dataset.statusUrl });
437
451
  return {
438
452
  success: true,
439
- datasets: enrichedDatasets.filter((dataset) => dataset !== null).map(serializeBigInt),
440
- count: enrichedDatasets.length,
441
- message: `Found ${enrichedDatasets.length} dataset(s)`
453
+ dataSetId: dataSetId.toString(),
454
+ txHash: dataset.txHash,
455
+ message: "Dataset created successfully"
442
456
  };
443
457
  } catch (error) {
444
- return createErrorResponse(
445
- "dataset_fetch_failed",
446
- `Failed to fetch datasets: ${error.message}`,
447
- { success: false }
448
- );
458
+ return {
459
+ success: false,
460
+ txHash: null,
461
+ dataSetId: null,
462
+ message: "Failed to create dataset: " + synapseErrorHandler(error)
463
+ };
449
464
  }
450
465
  };
451
466
 
467
+ // src/services/payment-service.ts
468
+ import * as Payments from "@filoz/synapse-core/pay";
469
+ import { waitForTransactionReceipt } from "viem/actions";
470
+ async function processPaymentService(depositAmount) {
471
+ try {
472
+ const hash = await Payments.depositAndApprove(client, {
473
+ amount: BigInt(depositAmount)
474
+ });
475
+ const receipt = await waitForTransactionReceipt(client, {
476
+ hash
477
+ });
478
+ return { txHash: hash, success: receipt.status === "success" };
479
+ } catch (error) {
480
+ return {
481
+ txHash: null,
482
+ success: false,
483
+ error: synapseErrorHandler(error)
484
+ };
485
+ }
486
+ }
487
+ async function processWithdrawalService(withdrawalAmount) {
488
+ try {
489
+ const hash = await Payments.withdraw(client, {
490
+ amount: BigInt(withdrawalAmount)
491
+ });
492
+ const receipt = await waitForTransactionReceipt(client, {
493
+ hash
494
+ });
495
+ return { txHash: hash, success: receipt.status === "success" };
496
+ } catch (error) {
497
+ return {
498
+ txHash: null,
499
+ success: false,
500
+ error: synapseErrorHandler(error)
501
+ };
502
+ }
503
+ }
504
+
452
505
  // src/services/storage-service.ts
453
506
  import {
454
- SIZE_CONSTANTS as SIZE_CONSTANTS4,
455
- TIME_CONSTANTS as TIME_CONSTANTS3,
456
- TOKENS as TOKENS2,
457
- WarmStorageService as WarmStorageService2
458
- } from "@filoz/synapse-sdk";
507
+ SIZE_CONSTANTS as SIZE_CONSTANTS3,
508
+ TIME_CONSTANTS
509
+ } from "@filoz/synapse-core/utils";
459
510
  import { formatUnits } from "viem";
460
- var checkStorageBalance = async (synapse, storageCapacityBytes = env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS4.GiB), persistencePeriodDays = env.PERSISTENCE_PERIOD_DAYS) => {
461
- const warmStorageService = await WarmStorageService2.create(synapse.getProvider(), synapse.getWarmStorageAddress());
462
- const [storageInfo, accountInfo, prices] = await Promise.all([
463
- synapse.storage.getStorageInfo(),
464
- synapse.payments.accountInfo(TOKENS2.USDFC),
465
- warmStorageService.calculateStorageCost(storageCapacityBytes)
511
+ import * as ERC20 from "@filoz/synapse-core/erc20";
512
+ import * as Payments2 from "@filoz/synapse-core/pay";
513
+ import { getBalance } from "viem/actions";
514
+ import * as WarmStorage from "@filoz/synapse-core/warm-storage";
515
+ var calculateStorageCost = (prices, sizeInBytes) => {
516
+ const { pricePerTiBPerMonthNoCDN, epochsPerMonth } = prices;
517
+ const sizeInBytesBigint = BigInt(sizeInBytes);
518
+ const perEpoch = pricePerTiBPerMonthNoCDN * sizeInBytesBigint / (SIZE_CONSTANTS3.TiB * epochsPerMonth);
519
+ const perDay = perEpoch * TIME_CONSTANTS.EPOCHS_PER_DAY;
520
+ const perMonth = perEpoch * epochsPerMonth;
521
+ return {
522
+ perEpoch,
523
+ perDay,
524
+ perMonth
525
+ };
526
+ };
527
+ var checkStorageBalance = async (storageCapacityBytes = env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS3.GiB), persistencePeriodDays = env.PERSISTENCE_PERIOD_DAYS) => {
528
+ const [filRaw, { value: usdfcRaw }, { availableFunds }, prices, operatorApprovals2] = await Promise.all([
529
+ getBalance(client, {
530
+ address: account.address
531
+ }),
532
+ ERC20.balance(client, {
533
+ address: account.address
534
+ }),
535
+ Payments2.accountInfo(client, {
536
+ address: account.address
537
+ }),
538
+ WarmStorage.servicePrice(client),
539
+ Payments2.operatorApprovals(client, {
540
+ address: account.address
541
+ })
466
542
  ]);
467
- let filRaw;
468
- try {
469
- filRaw = await synapse.payments.walletBalance();
470
- } catch (error) {
471
- console.error(error);
472
- throw new Error("Error fetching wallet balances. \n FIL balance not available. \n The main cause of this error is that your wallet doesn't have any FIL and has no transaction history on the network. Action: Top up your wallet with FIL.");
473
- }
474
- let usdfcRaw = await synapse.payments.walletBalance(TOKENS2.USDFC);
475
- const allowance = storageInfo.allowances;
476
- const availableFunds = accountInfo.availableFunds;
477
- const currentMonthlyRate = allowance.rateUsed * TIME_CONSTANTS3.EPOCHS_PER_MONTH;
478
- const maxMonthlyRate = prices.perMonth;
543
+ const storageCosts = calculateStorageCost(prices, storageCapacityBytes);
544
+ const currentMonthlyRate = operatorApprovals2.rateUsed * TIME_CONSTANTS.EPOCHS_PER_MONTH;
545
+ const maxMonthlyRate = storageCosts.perMonth;
479
546
  const daysLeftAtMaxBurnRate = maxMonthlyRate === 0n ? Infinity : Number(availableFunds) / Number(maxMonthlyRate) * 30;
480
547
  const daysLeftAtBurnRate = currentMonthlyRate === 0n ? Infinity : Number(availableFunds) / Number(currentMonthlyRate) * 30;
481
- const amountNeeded = prices.perDay * BigInt(persistencePeriodDays);
482
- const totalDepositNeeded = daysLeftAtMaxBurnRate >= env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS ? 0n : amountNeeded - accountInfo.availableFunds;
483
- const availableToFreeUp = accountInfo.availableFunds > amountNeeded ? accountInfo.availableFunds - amountNeeded : 0n;
484
- const isRateSufficient = allowance.rateAllowance === MAX_UINT256;
485
- const isLockupSufficient = allowance.lockupAllowance === MAX_UINT256;
548
+ const amountNeeded = storageCosts.perDay * BigInt(persistencePeriodDays);
549
+ const totalDepositNeeded = daysLeftAtMaxBurnRate >= env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS ? 0n : amountNeeded - availableFunds;
550
+ const availableToFreeUp = availableFunds > amountNeeded ? availableFunds - amountNeeded : 0n;
551
+ const isRateSufficient = operatorApprovals2.rateAllowance === MAX_UINT256;
552
+ const isLockupSufficient = operatorApprovals2.lockupAllowance === MAX_UINT256;
486
553
  const isSufficient = isRateSufficient && isLockupSufficient && daysLeftAtMaxBurnRate >= env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS;
487
554
  return {
488
555
  filBalance: filRaw,
@@ -515,8 +582,8 @@ var formatStorageBalanceResult = (checkStorageBalanceResult) => {
515
582
  isSufficient: checkStorageBalanceResult.isSufficient
516
583
  };
517
584
  };
518
- var formatBalance = (balance, ticker) => {
519
- return `${Number(Number(formatUnits(balance, 18)).toFixed(8))} ${ticker}`;
585
+ var formatBalance = (balance2, ticker) => {
586
+ return `${Number(Number(formatUnits(balance2, 18)).toFixed(8))} ${ticker}`;
520
587
  };
521
588
  var formatTime = (days) => {
522
589
  if (days === Infinity) {
@@ -533,17 +600,89 @@ var formatTime = (days) => {
533
600
  }
534
601
  return `${Math.fround(days / 365)} years`;
535
602
  };
603
+ var estimateStorageCost = async (sizeInBytes, durationInMonths, createCDNDataset = false) => {
604
+ const prices = await WarmStorage.servicePrice(client);
605
+ const { minimumPricePerMonth, pricePerTiBPerMonthNoCDN } = prices;
606
+ let pricePerMonth = pricePerTiBPerMonthNoCDN * BigInt(sizeInBytes) / BigInt(SIZE_CONSTANTS3.TiB);
607
+ const appliedMinimum = pricePerMonth < minimumPricePerMonth;
608
+ if (appliedMinimum) {
609
+ pricePerMonth = minimumPricePerMonth;
610
+ }
611
+ let totalCost = pricePerMonth * BigInt(durationInMonths);
612
+ const cdnSetupCost = createCDNDataset ? 10n ** 18n : 0n;
613
+ totalCost += cdnSetupCost;
614
+ const sizeInGiB = Number(sizeInBytes) / Number(SIZE_CONSTANTS3.GiB);
615
+ const sizeInTiB = Number(sizeInBytes) / Number(SIZE_CONSTANTS3.TiB);
616
+ let sizeFormatted;
617
+ if (sizeInTiB >= 1) {
618
+ sizeFormatted = `${sizeInTiB.toFixed(2)} TiB`;
619
+ } else if (sizeInGiB >= 1) {
620
+ sizeFormatted = `${sizeInGiB.toFixed(2)} GiB`;
621
+ } else {
622
+ const sizeInMiB = Number(sizeInBytes) / Number(SIZE_CONSTANTS3.MiB);
623
+ sizeFormatted = `${sizeInMiB.toFixed(2)} MiB`;
624
+ }
625
+ return {
626
+ monthlyCost: pricePerMonth,
627
+ totalCost,
628
+ totalCostFormatted: formatUnits(totalCost, 18),
629
+ cdnSetupCost,
630
+ cdnSetupCostFormatted: formatUnits(cdnSetupCost, 18),
631
+ details: {
632
+ sizeInBytes,
633
+ sizeFormatted,
634
+ durationInMonths,
635
+ pricePerTiBPerMonth: pricePerTiBPerMonthNoCDN,
636
+ minimumPricePerMonth,
637
+ appliedMinimum
638
+ }
639
+ };
640
+ };
641
+
642
+ // src/services/provider-service.ts
643
+ import { readProviders, getProvider as readProvider } from "@filoz/synapse-core/warm-storage";
644
+ var getProvider = async (providerId) => {
645
+ const provider = await readProvider(publicClient, {
646
+ providerId: BigInt(providerId)
647
+ });
648
+ return serializeBigInt(provider);
649
+ };
536
650
 
537
651
  // src/mastra/tools/dataset-tools.ts
538
- var getDatasets2 = createTool({
652
+ var getDatasets = createTool({
539
653
  id: "getDatasets",
540
654
  description: "Retrieve all datasets owned by the connected wallet with comprehensive information including piece CIDs, file sizes, provider details, and retrieval URLs. Filter by CDN status or view all datasets. Each dataset contains complete metadata about stored files and their blockchain storage proofs. Use this to inventory files, check storage status, or locate specific uploads.",
541
655
  inputSchema: GetDatasetsSchema,
542
656
  outputSchema: GetDatasetsOutputSchema,
543
657
  execute: async ({ context }) => {
544
- const withCDN = context.filterByCDN ?? false;
545
- const includeAll = context.includeAllDatasets ?? false;
546
- return await getDatasets(withCDN, includeAll);
658
+ const progressLog = [];
659
+ const log = (msg) => {
660
+ progressLog.push(msg);
661
+ };
662
+ try {
663
+ const withCDN = context.filterByCDN ?? false;
664
+ const includeAll = context.includeAllDatasets ?? false;
665
+ log("Fetching datasets from blockchain...");
666
+ const dataSets = await getDatasetsService(withCDN, includeAll);
667
+ if (dataSets.length > 0) {
668
+ log(`Retrieved ${dataSets.length} dataset(s)`);
669
+ log("Processing dataset metadata...");
670
+ }
671
+ return {
672
+ success: true,
673
+ datasets: dataSets.map((dataset) => serializeBigInt(dataset)),
674
+ count: dataSets.length,
675
+ message: `Found ${dataSets.length} dataset(s)`,
676
+ progressLog
677
+ };
678
+ } catch (error) {
679
+ return {
680
+ success: false,
681
+ error: error.message,
682
+ message: `Failed to fetch datasets: ${error.message}`,
683
+ progressLog
684
+ };
685
+ }
547
686
  }
548
687
  });
549
688
  var getDataset = createTool({
@@ -553,11 +692,11 @@ var getDataset = createTool({
553
692
  outputSchema: GetDatasetOutputSchema,
554
693
  execute: async ({ context }) => {
555
694
  try {
556
- const dataset = await getDatasets(void 0, void 0, Number(context.datasetId));
695
+ const dataset = await getDataSetService(Number(context.datasetId));
557
696
  return {
558
- success: dataset.success,
559
- dataset: serializeBigInt(dataset.datasets[0]),
560
- message: dataset.message
697
+ success: true,
698
+ dataset: serializeBigInt(dataset),
699
+ message: "Dataset fetched successfully"
561
700
  };
562
701
  } catch (error) {
563
702
  return createErrorResponse(
@@ -570,60 +709,60 @@ var getDataset = createTool({
570
709
  });
571
710
  var createDataset = createTool({
572
711
  id: "createDataset",
573
- description: "Create a new dataset container on Filecoin for organizing related files with consistent storage settings. Datasets define storage parameters (CDN enabled/disabled, provider selection) that apply to all files added to them. Creating datasets upfront allows for better file organization and consistent retrieval performance. Optionally specify a provider or let the system auto-select the optimal one. Note: Payment is processed automatically for CDN-enabled datasets.",
712
+ description: "Create a new dataset container on Filecoin for organizing related files with consistent storage settings. Datasets define storage parameters (CDN enabled/disabled, provider selection) that apply to all files added to them. Creating datasets upfront allows for better file organization and consistent retrieval performance. Provider ID is required - use getProviders to list available providers. Payment (1 USDFC) is processed automatically for CDN-enabled datasets. Returns dataset ID, transaction hash, and progress tracking through validation, payment, and creation steps.",
574
713
  inputSchema: CreateDatasetSchema,
575
714
  outputSchema: CreateDatasetOutputSchema,
576
715
  execute: async ({ context }) => {
577
- try {
578
- const synapse = await getSynapseInstance();
579
- const withCDN = true;
580
- if (withCDN) {
581
- const paymentResult = await processStoragePayment(synapse, BigInt(env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS.GiB)), env.PERSISTENCE_PERIOD_DAYS);
582
- if (!paymentResult.success) {
583
- return createErrorResponse(
584
- "payment_failed",
585
- `Failed to process payment: ${paymentResult.txHash ?? "Unknown error"}`,
586
- { success: false }
587
- );
588
- }
716
+ const progressLog = [];
717
+ const log = (msg) => {
718
+ progressLog.push(msg);
719
+ };
720
+ if (!context.providerId) {
721
+ return {
722
+ success: false,
723
+ error: "provider_id_required",
724
+ message: "Provider ID is required. Use getProviders tool to list available providers and select one by ID.",
725
+ progressLog
726
+ };
727
+ }
728
+ log("Validating provider ID...");
729
+ const provider = await getProvider(Number(context.providerId));
730
+ log(`Provider validated (ID: ${context.providerId})`);
731
+ const withCDN = context.withCDN ?? false;
732
+ if (withCDN) {
733
+ log("Processing CDN payment (1 USDFC)...");
734
+ const { success, error } = await processPaymentService(BigInt(env.CDN_DATASET_FEE));
735
+ if (!success) {
736
+ return {
737
+ success: false,
738
+ error: "payment_failed",
739
+ message: `Failed to process CDN payment (1 USDFC required): ${error}. Use processPayment tool to add funds first.`,
740
+ progressLog
741
+ };
742
+ }
743
+ log("CDN payment processed successfully");
744
+ }
745
+ log("Creating dataset on blockchain...");
746
+ const result = await createDataSetService(provider, withCDN, context.metadata ?? {});
747
+ if (result.success) {
748
+ log("Dataset created successfully");
749
+ if (result.txHash) {
750
+ log(`View transaction: ${getExplorerUrl(result.txHash)}`);
589
751
  }
590
- let datasetId;
591
- let txHash;
592
- await synapse.createStorage({
593
- providerId: context.providerId ? parseInt(context.providerId) : void 0,
594
- forceCreateDataSet: true,
595
- metadata: context.metadata,
596
- callbacks: {
597
- onDataSetCreationStarted: (txResponse) => {
598
- txHash = txResponse.hash;
599
- console.log(`[Dataset] Creation started (tx: ${txResponse.hash})`);
600
- },
601
- onDataSetCreationProgress: (status) => {
602
- if (status.serverConfirmed) {
603
- datasetId = status.dataSetId?.toString() || void 0;
604
- console.log(`[Dataset] Ready (ID: ${status.dataSetId?.toString()})`);
605
- }
606
- }
607
- }
608
- });
609
752
  return {
610
- success: true,
611
- datasetId,
612
- txHash,
613
- message: "Dataset created successfully"
753
+ ...result,
754
+ progressLog
614
755
  };
615
- } catch (error) {
616
- return createErrorResponse(
617
- "dataset_creation_failed",
618
- `Failed to create dataset: ${error.message}`,
619
- { success: false }
620
- );
621
756
  }
757
+ return {
758
+ ...result,
759
+ progressLog
760
+ };
622
761
  }
623
762
  });
624
763
  var datasetTools = {
625
764
  getDataset,
626
- getDatasets: getDatasets2,
765
+ getDatasets,
627
766
  createDataset
628
767
  };
629
768
 
@@ -653,7 +792,6 @@ var uploadFile = createTool2({
653
792
  const synapse = await getSynapseInstance();
654
793
  log("Checking storage balance...");
655
794
  const storageMetrics = await checkStorageBalance(
656
- synapse,
657
795
  fileInfo.size,
658
796
  env.PERSISTENCE_PERIOD_DAYS
659
797
  );
@@ -662,22 +800,25 @@ var uploadFile = createTool2({
662
800
  return {
663
801
  success: false,
664
802
  error: "insufficient_balance",
665
- message: "Insufficient balance. Enable autoPayment or call processPayment first",
666
- required: {
667
- deposit: fromBaseUnits(storageMetrics.depositNeeded, 18)
668
- }
803
+ message: `Insufficient balance: ${fromBaseUnits(storageMetrics.depositNeeded, 18)} USDFC required. Enable autoPayment parameter or use processPayment tool to deposit funds first.`
669
804
  };
670
805
  }
671
806
  log(storageMetrics.depositNeeded > 0n ? "Insufficient balance, processing payment..." : "Insufficient service approvals, approving service...");
672
- await processStoragePayment(
673
- synapse,
674
- storageMetrics.depositNeeded,
675
- env.PERSISTENCE_PERIOD_DAYS
676
- );
677
- log(storageMetrics.depositNeeded > 0n ? "Payment processed successfully" : "Service approved successfully");
807
+ log(`Deposit needed: ${fromBaseUnits(storageMetrics.depositNeeded, 18)} USDFC`);
808
+ if (storageMetrics.depositNeeded > 0n || !storageMetrics.isSufficient) {
809
+ const { success } = await processPaymentService(BigInt(storageMetrics.depositNeeded));
810
+ if (!success) {
811
+ return createErrorResponse(
812
+ "payment_failed",
813
+ "Failed to process payment",
814
+ { success: false }
815
+ );
816
+ }
817
+ log("Payment processed successfully");
818
+ }
678
819
  }
679
820
  log("Creating storage service...");
680
- const storageService = await synapse.createStorage({
821
+ const storageService = await synapse.storage.createContext({
681
822
  dataSetId: context.datasetId ? Number(context.datasetId) : void 0,
682
823
  withCDN: context.withCDN || false,
683
824
  callbacks: {
@@ -686,16 +827,8 @@ var uploadFile = createTool2({
686
827
  log(`Dataset provider: ${Number(info.provider.id)}`);
687
828
  log(`Is existing dataset: ${info.isExisting}`);
688
829
  },
689
- onDataSetCreationStarted: (txResponse) => {
690
- log(`Dataset creation started (tx: ${txResponse.hash})`);
691
- },
692
- onDataSetCreationProgress: (status) => {
693
- if (status.serverConfirmed) {
694
- log(`Dataset ready (ID: ${status.dataSetId})`);
695
- }
696
- },
697
- onProviderSelected: (provider) => {
698
- log(`Provider selected: ${provider.id}`);
830
+ onProviderSelected: (provider2) => {
831
+ log(`Provider selected: ${provider2.id}`);
699
832
  }
700
833
  }
701
834
  });
@@ -706,19 +839,21 @@ var uploadFile = createTool2({
706
839
  onUploadComplete: (piece) => {
707
840
  log(`Upload complete (pieceCid: ${piece.toV1().toString()})`);
708
841
  },
709
- onPieceAdded: (txResponse) => {
710
- uploadTxHash = txResponse?.hash;
711
- log(`Piece added to dataset (tx: ${txResponse?.hash || "pending"})`);
842
+ onPieceAdded: (hash) => {
843
+ uploadTxHash = hash;
844
+ log(`Piece added to dataset (tx: ${hash || "pending"})`);
712
845
  },
713
846
  onPieceConfirmed: () => {
714
847
  log("Piece confirmed on blockchain");
715
848
  }
716
849
  });
850
+ const provider = await getProvider(storageService.provider.id);
717
851
  const getRetrievalUrl = async (pieceCid2) => {
718
852
  if (context.withCDN) {
719
- return `https://${await synapse.getSigner().getAddress()}.calibration.filbeam.io/${pieceCid2}`;
853
+ const network = env.FILECOIN_NETWORK === "mainnet" ? "mainnet" : "calibration";
854
+ return `https://${await synapse.getSigner().getAddress()}.${network}.filbeam.io/${pieceCid2}`;
720
855
  } else {
721
- const serviceURL = (await storageService.getProviderInfo()).products.PDP?.data.serviceURL || "";
856
+ const serviceURL = provider.pdp.serviceURL;
722
857
  const endsWithSlash = serviceURL.endsWith("/");
723
858
  const serviceURLWithoutSlash = endsWithSlash ? serviceURL.slice(0, -1) : serviceURL;
724
859
  return `${serviceURLWithoutSlash}/piece/${pieceCid2}`;
@@ -730,6 +865,9 @@ var uploadFile = createTool2({
730
865
  console.log(`TX Hash: ${uploadTxHash}`);
731
866
  console.log(`File Name: ${fileName}`);
732
867
  console.log(`File Size: ${fileInfo.size}`);
868
+ if (uploadTxHash) {
869
+ log(`View transaction: ${getExplorerUrl(uploadTxHash)}`);
870
+ }
733
871
  return {
734
872
  success: true,
735
873
  pieceCid: pieceCid.toV1().toString(),
@@ -760,24 +898,53 @@ var fileTools = {
760
898
  import { createTool as createTool3 } from "@mastra/core";
761
899
  var getBalances = createTool3({
762
900
  id: "getBalances",
763
- description: "Check wallet balances (FIL and USDFC tokens) and comprehensive storage metrics including available funds, required deposits, days of storage remaining, and allowance status. Returns both human-readable formatted values and raw data. Use this before upload operations to verify sufficient balance, or to monitor storage budget and plan deposits. Calculates storage needs based on capacity and persistence period parameters.",
901
+ description: "Check wallet balances (FIL and USDFC tokens) and comprehensive storage metrics including available funds, required deposits, days of storage remaining, and allowance status. Returns both human-readable formatted values and raw data with progress log showing calculation parameters used. \u26A0\uFE0F CRITICAL: Storage providers consider accounts with less than 30 days of available balance as INSOLVENT and may refuse service. Default notice period is 45 days to ensure safe margin. IMPORTANT AGENT INSTRUCTIONS: (1) Before calling this tool, ASK the user if they want to calculate based on default storage requirements (150 GiB capacity, 365 days persistence, 45 days notice period) or if they have specific requirements. (2) After showing results, ALWAYS ASK the user if they want to see calculations for different storage configurations. (3) If days remaining falls below 45, WARN the user that they are approaching insolvency threshold (30 days) and should deposit funds immediately. Use this before upload operations to verify sufficient balance, or to monitor storage budget and plan deposits.",
764
902
  inputSchema: GetBalancesSchema,
765
903
  outputSchema: GetBalancesOutputSchema,
766
904
  execute: async ({ context }) => {
905
+ const progressLog = [];
906
+ const log = (msg) => {
907
+ progressLog.push(msg);
908
+ };
767
909
  try {
768
- const synapse = await getSynapseInstance();
769
- const checkStorageBalanceResult = await checkStorageBalance(synapse, context.storageCapacityBytes, env.PERSISTENCE_PERIOD_DAYS);
910
+ const storageCapacityBytes = context.storageCapacityBytes || env.TOTAL_STORAGE_NEEDED_GiB * 1024 * 1024 * 1024;
911
+ const persistencePeriodDays = context.persistencePeriodDays || env.PERSISTENCE_PERIOD_DAYS;
912
+ const notificationThresholdDays = context.notificationThresholdDays || env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS;
913
+ const capacityGB = (storageCapacityBytes / (1024 * 1024 * 1024)).toFixed(2);
914
+ const capacityTB = (storageCapacityBytes / (1024 * 1024 * 1024 * 1024)).toFixed(2);
915
+ log(`Calculating balance requirements with:`);
916
+ log(`- Storage Capacity: ${capacityGB} GB (${capacityTB} TB)`);
917
+ log(`- Persistence Period: ${persistencePeriodDays} days`);
918
+ log(`- Notification Threshold: ${notificationThresholdDays} days`);
919
+ log("Fetching wallet balances from blockchain...");
920
+ const checkStorageBalanceResult = await checkStorageBalance(storageCapacityBytes, persistencePeriodDays);
921
+ log("Calculating storage metrics and requirements...");
922
+ const formattedResult = formatStorageBalanceResult(checkStorageBalanceResult);
923
+ const serializedResult = serializeBigInt(checkStorageBalanceResult);
924
+ log(`Balance check complete. Available USDFC: ${formattedResult.usdfcBalance}`);
925
+ const daysLeft = checkStorageBalanceResult.daysLeftAtBurnRate;
926
+ if (daysLeft < 45) {
927
+ if (daysLeft < 30) {
928
+ log(`\u26D4 CRITICAL INSOLVENCY WARNING: Only ${daysLeft.toFixed(1)} days of balance remaining! Storage providers consider accounts with less than 30 days as INSOLVENT and will REFUSE SERVICE. Deposit funds IMMEDIATELY!`);
929
+ } else {
930
+ log(`\u26A0\uFE0F LOW BALANCE WARNING: Only ${daysLeft.toFixed(1)} days remaining (threshold: 45 days). You are approaching the insolvency threshold (30 days). Please deposit funds soon to avoid service interruption.`);
931
+ }
932
+ }
933
+ log("\u{1F4A1} To calculate for different storage requirements, call this tool again with custom storageCapacityBytes and persistencePeriodDays parameters.");
770
934
  return {
771
935
  success: true,
772
- checkStorageBalanceResultFormatted: formatStorageBalanceResult(checkStorageBalanceResult),
773
- checkStorageBalanceResult: serializeBigInt(checkStorageBalanceResult)
936
+ checkStorageBalanceResultFormatted: formattedResult,
937
+ checkStorageBalanceResult: serializedResult,
938
+ progressLog,
939
+ message: `Balance check complete for ${capacityTB} TB over ${persistencePeriodDays} days. Available: ${formattedResult.usdfcBalance}`
774
940
  };
775
941
  } catch (error) {
776
- return createErrorResponse(
777
- "balance_fetch_failed",
778
- `Failed to fetch balances: ${error.message}`,
779
- { success: false }
780
- );
942
+ return {
943
+ success: false,
944
+ error: "balance_fetch_failed",
945
+ message: `Failed to fetch balances: ${error.message}`,
946
+ progressLog
947
+ };
781
948
  }
782
949
  }
783
950
  });
@@ -789,61 +956,82 @@ var balanceTools = {
789
956
  import { createTool as createTool4 } from "@mastra/core";
790
957
  var processPayment = createTool4({
791
958
  id: "processPayment",
792
- description: "Deposit USDFC tokens and configure storage service allowances in a single transaction using EIP-2612 gasless permits. Sets both rate allowance (per-epoch spending limit) and lockup allowance (total committed funds) to unlimited for seamless storage operations. Use this to fund your storage account before uploads or when balance is insufficient. Validates wallet balance before processing to prevent failed transactions.",
959
+ description: "Deposit USDFC tokens (not in base units) and configure storage service allowances in a single transaction using EIP-2612 gasless permits. Sets both rate allowance (per-epoch spending limit) and lockup allowance (total committed funds) to unlimited for seamless storage operations. Use this to fund your storage account before uploads or when balance is insufficient. Validates wallet balance before processing to prevent failed transactions.",
793
960
  inputSchema: ProcessPaymentSchema,
794
961
  outputSchema: ProcessPaymentOutputSchema,
795
962
  execute: async ({ context }) => {
796
- try {
797
- const synapse = await getSynapseInstance();
798
- const accountInfo = await synapse.payments.accountInfo(TOKENS.USDFC);
799
- const availableFunds = Number(accountInfo.availableFunds);
800
- const { depositAmount } = context;
801
- if (depositAmount === 0) {
802
- return {
803
- success: true,
804
- message: `You have sufficient balance to cover the storage needs.`,
805
- txHash: null,
806
- required: {
807
- deposit: depositAmount
808
- },
809
- available: availableFunds
810
- };
811
- }
812
- if (availableFunds < depositAmount) {
813
- return {
814
- success: false,
815
- error: "insufficient_balance",
816
- message: `Insufficient USDFC balance. Required: ${depositAmount}, Available: ${availableFunds}`,
817
- required: depositAmount,
818
- available: Number(availableFunds)
819
- };
820
- }
821
- const result = await processStoragePayment(
822
- synapse,
823
- BigInt(depositAmount),
824
- env.PERSISTENCE_PERIOD_DAYS
825
- );
963
+ const progressLog = [];
964
+ const log = (msg) => {
965
+ progressLog.push(msg);
966
+ };
967
+ const { depositAmount } = context;
968
+ log("Converting amount to base units...");
969
+ const amount = toBaseUnits(depositAmount.toString(), 18);
970
+ log("Initiating payment transaction...");
971
+ const { success, txHash, error } = await processPaymentService(amount);
972
+ if (!success) {
826
973
  return {
827
- success: result.success,
828
- txHash: result.txHash,
829
- message: `Payment processed successfully now you can upload files to storage. You paid ${depositAmount} USDFC to cover the storage needs.`
974
+ success: false,
975
+ error: "payment_failed",
976
+ message: `Failed to process payment: ${error}`,
977
+ progressLog
830
978
  };
831
- } catch (error) {
832
- return createErrorResponse(
833
- "payment_failed",
834
- `Payment processing failed: ${error.message}`,
835
- { success: false }
836
- );
837
979
  }
980
+ log("Payment transaction confirmed");
981
+ if (txHash) {
982
+ log(`View transaction: ${getExplorerUrl(txHash)}`);
983
+ }
984
+ return {
985
+ success,
986
+ txHash,
987
+ message: `Payment processed successfully. You deposited ${depositAmount} USDFC to your storage account.`,
988
+ progressLog
989
+ };
990
+ }
991
+ });
992
+ var processWithdrawal = createTool4({
993
+ id: "processWithdrawal",
994
+ description: "Withdraw USDFC tokens (not in base units) from the storage account back to your wallet. Reduces storage service allowances and available balance. Use this to retrieve unused funds from the storage account. Returns transaction hash for verification and progress tracking through conversion, initiation, and confirmation steps.",
995
+ inputSchema: ProcessWithdrawalSchema,
996
+ outputSchema: ProcessWithdrawalOutputSchema,
997
+ execute: async ({ context }) => {
998
+ const progressLog = [];
999
+ const log = (msg) => {
1000
+ progressLog.push(msg);
1001
+ };
1002
+ const { withdrawalAmount } = context;
1003
+ log("Converting amount to base units...");
1004
+ const amount = toBaseUnits(withdrawalAmount.toString(), 18);
1005
+ log("Initiating withdrawal transaction...");
1006
+ const { success, txHash, error } = await processWithdrawalService(amount);
1007
+ if (!success) {
1008
+ return {
1009
+ success: false,
1010
+ error: "withdrawal_failed",
1011
+ message: `Failed to process withdrawal: ${error}`,
1012
+ progressLog
1013
+ };
1014
+ }
1015
+ log("Withdrawal transaction confirmed");
1016
+ if (txHash) {
1017
+ log(`View transaction: ${getExplorerUrl(txHash)}`);
1018
+ }
1019
+ return {
1020
+ success: true,
1021
+ txHash,
1022
+ message: `Withdrawal processed successfully. You withdrew ${withdrawalAmount} USDFC from your storage account.`,
1023
+ progressLog
1024
+ };
838
1025
  }
839
1026
  });
840
1027
  var paymentTools = {
841
- processPayment
1028
+ processPayment,
1029
+ processWithdrawal
842
1030
  };
843
1031
 
844
1032
  // src/mastra/tools/provider-tools.ts
845
1033
  import { createTool as createTool5 } from "@mastra/core";
846
- import { WarmStorageService as WarmStorageService3 } from "@filoz/synapse-sdk";
1034
+ import { readProviders as readProviders2 } from "@filoz/synapse-core/warm-storage";
847
1035
  var getProviders = createTool5({
848
1036
  id: "getProviders",
849
1037
  description: "List storage providers available on the Filecoin network with their service details, product offerings, and endpoint URLs. By default returns only approved providers for reliability. Use this to discover available providers, select specific providers for dataset creation, or verify provider availability before operations. Provider information includes service URLs needed for file retrieval.",
@@ -851,21 +1039,11 @@ var getProviders = createTool5({
851
1039
  outputSchema: GetProvidersOutputSchema,
852
1040
  execute: async ({ context }) => {
853
1041
  try {
854
- const synapse = await getSynapseInstance();
855
- const warmStorageService = await WarmStorageService3.create(
856
- synapse.getProvider(),
857
- synapse.getWarmStorageAddress()
1042
+ const providers = (await readProviders2(
1043
+ publicClient
1044
+ )).filter(
1045
+ (p) => context.onlyApproved ? p.isActive : true
858
1046
  );
859
- const approvedProviderIds = await warmStorageService.getApprovedProviderIds();
860
- const providersInfo = await Promise.all(
861
- approvedProviderIds.map(async (providerId) => {
862
- const providerInfo = await synapse.getProviderInfo(providerId);
863
- return providerInfo;
864
- })
865
- );
866
- const providers = context.onlyApproved !== false ? providersInfo.filter(
867
- (p) => approvedProviderIds.includes(p.id)
868
- ) : providersInfo;
869
1047
  return {
870
1048
  success: true,
871
1049
  providers: providers.map(serializeBigInt),
@@ -873,10 +1051,12 @@ var getProviders = createTool5({
873
1051
  message: `Found ${providers.length} provider(s)`
874
1052
  };
875
1053
  } catch (error) {
876
- return createErrorResponse(
877
- "provider_fetch_failed",
878
- `Failed to fetch providers: ${error.message}`
879
- );
1054
+ return {
1055
+ success: false,
1056
+ providers: [],
1057
+ error: error.message,
1058
+ message: `Failed to fetch providers: ${error.message}`
1059
+ };
880
1060
  }
881
1061
  }
882
1062
  });
@@ -884,35 +1064,496 @@ var providerTools = {
884
1064
  getProviders
885
1065
  };
886
1066
 
1067
+ // src/mastra/tools/storage-cost-tools.ts
1068
+ import { createTool as createTool6 } from "@mastra/core";
1069
+ import { z as z3 } from "zod";
1070
+ import { SIZE_CONSTANTS as SIZE_CONSTANTS4 } from "@filoz/synapse-core/utils";
1071
+ var EstimateStorageCostSchema = z3.object({
1072
+ sizeInGiB: z3.number().positive().optional().describe("Size of data to store in GiB (gibibytes). Example: 1.5 for 1.5 GiB. Provide either sizeInGiB or sizeInTiB, not both"),
1073
+ sizeInTiB: z3.number().positive().optional().describe("Size of data to store in TiB (tebibytes). Example: 0.5 for 0.5 TiB. Provide either sizeInGiB or sizeInTiB, not both"),
1074
+ durationInMonths: z3.number().positive().int().describe("Duration to store data in months. Example: 12 for one year"),
1075
+ createCDNDataset: z3.boolean().optional().default(false).describe("Whether this is for a new CDN-enabled dataset (adds 1 USDFC one-time cost). Default: false")
1076
+ }).refine(
1077
+ (data) => data.sizeInGiB !== void 0 !== (data.sizeInTiB !== void 0),
1078
+ {
1079
+ message: "Exactly one of sizeInGiB or sizeInTiB must be provided"
1080
+ }
1081
+ );
1082
+ var EstimateStorageCostOutputSchema = z3.object({
1083
+ success: z3.boolean(),
1084
+ // Success fields
1085
+ monthlyCost: z3.string().optional().describe("Monthly storage cost in USDFC"),
1086
+ totalCost: z3.string().optional().describe("Total storage cost for duration in USDFC (includes CDN setup if applicable)"),
1087
+ cdnSetupCost: z3.string().optional().describe("One-time CDN setup cost in USDFC (if createCDNDataset is true)"),
1088
+ cdnEgressCredits: z3.string().optional().describe("CDN egress credits topped up (in GiB) with the 1 USDFC setup cost"),
1089
+ details: z3.object({
1090
+ sizeInBytes: z3.number(),
1091
+ sizeFormatted: z3.string().describe('Human-readable size (e.g., "1.50 GiB")'),
1092
+ durationInMonths: z3.number(),
1093
+ pricePerTiBPerMonth: z3.string(),
1094
+ minimumPricePerMonth: z3.string(),
1095
+ appliedMinimum: z3.boolean().describe("Whether minimum pricing was applied (for storage < 24.567 GiB)")
1096
+ }).optional(),
1097
+ breakdown: z3.string().optional().describe("Human-readable cost breakdown"),
1098
+ pricingExplanation: z3.string().optional().describe("Detailed explanation of how storage pricing works"),
1099
+ cdnPricingExplanation: z3.string().optional().describe("Detailed explanation of how CDN egress pricing works"),
1100
+ // Error fields
1101
+ error: z3.string().optional(),
1102
+ message: z3.string()
1103
+ });
1104
+ var estimateStoragePricing = createTool6({
1105
+ id: "estimateStoragePricing",
1106
+ description: "Calculate storage costs for Filecoin OnchainCloud and explain pricing models. Provides: (1) Cost estimates with monthly/total breakdowns, (2) Comprehensive explanation of storage pricing (pay-per-epoch, $2.50/TiB/month, $0.06 minimum), (3) CDN egress pricing details ($7/TiB downloads, 1 USDFC = ~146 GiB credits). \u26A0\uFE0F CRITICAL: When explaining budgeting, ALWAYS warn that storage providers consider accounts with less than 30 days of remaining balance as INSOLVENT and may refuse service. Recommend maintaining at least 45 days of balance for safety margin. Use when users ask about storage costs, pricing models, CDN fees, or need to budget for storage. Clarifies that CDN 1 USDFC is NOT a fee but pre-paid egress credits that can be topped up anytime.",
1107
+ inputSchema: EstimateStorageCostSchema,
1108
+ outputSchema: EstimateStorageCostOutputSchema,
1109
+ execute: async ({ context }) => {
1110
+ try {
1111
+ const { sizeInGiB, sizeInTiB, durationInMonths, createCDNDataset } = context;
1112
+ let sizeInBytes;
1113
+ if (sizeInGiB !== void 0) {
1114
+ sizeInBytes = sizeInGiB * Number(SIZE_CONSTANTS4.GiB);
1115
+ } else if (sizeInTiB !== void 0) {
1116
+ sizeInBytes = sizeInTiB * Number(SIZE_CONSTANTS4.TiB);
1117
+ } else {
1118
+ throw new Error("Either sizeInGiB or sizeInTiB must be provided");
1119
+ }
1120
+ const estimate = await estimateStorageCost(
1121
+ sizeInBytes,
1122
+ durationInMonths,
1123
+ createCDNDataset
1124
+ );
1125
+ const cdnEgressCreditsGiB = createCDNDataset ? 1 / CDN_EGRESS_RATE_PER_TIB * 1024 : 0;
1126
+ const breakdown = [
1127
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1128
+ `Storage Cost Estimate for ${estimate.details.sizeFormatted}`,
1129
+ `Duration: ${durationInMonths} month${durationInMonths !== 1 ? "s" : ""}`,
1130
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1131
+ ``,
1132
+ `Monthly Storage Cost: ${Number(estimate.monthlyCost) / 1e18} USDFC`,
1133
+ `Total Storage Cost: ${Number(estimate.totalCost - estimate.cdnSetupCost) / 1e18} USDFC`
1134
+ ];
1135
+ if (createCDNDataset && estimate.cdnSetupCost > 0n) {
1136
+ breakdown.push(``);
1137
+ breakdown.push(`CDN Egress Credits Top-up: ${estimate.cdnSetupCostFormatted} USDFC`);
1138
+ breakdown.push(` \u2192 Provides ~${cdnEgressCreditsGiB.toFixed(2)} GiB of egress credits`);
1139
+ breakdown.push(` \u2192 You can top up more credits anytime`);
1140
+ breakdown.push(``);
1141
+ breakdown.push(`Grand Total (Storage + CDN Credits): ${estimate.totalCostFormatted} USDFC`);
1142
+ }
1143
+ breakdown.push(``);
1144
+ breakdown.push(`\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`);
1145
+ if (estimate.details.appliedMinimum) {
1146
+ breakdown.push(
1147
+ `Note: Minimum pricing of $0.06/month applied (storage < ~24.567 GiB)`
1148
+ );
1149
+ }
1150
+ const pricingExplanation = [
1151
+ ``,
1152
+ `HOW STORAGE PRICING WORKS:`,
1153
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1154
+ ``,
1155
+ `Storage operates on a pay-per-epoch model (epochs are 30 seconds):`,
1156
+ ``,
1157
+ `1. BASE RATE: $2.50 per TiB per month`,
1158
+ ` - Calculated per epoch: (size / TiB) \xD7 $2.50/month \xF7 epochs_per_month`,
1159
+ ` - You pay only for the storage space you use`,
1160
+ ``,
1161
+ `2. MINIMUM CHARGE: $0.06 per month`,
1162
+ ` - Applies to storage < ~24.567 GiB`,
1163
+ ` - Ensures service sustainability for small datasets`,
1164
+ ``,
1165
+ `3. PAYMENT MODEL:`,
1166
+ ` - Deposit USDFC tokens into your account`,
1167
+ ` - Set rate allowance (max spending per epoch)`,
1168
+ ` - Set lockup allowance (max total locked funds)`,
1169
+ ` - Storage service deducts costs automatically each epoch`,
1170
+ ``,
1171
+ `4. EPOCHS:`,
1172
+ ` - 1 epoch = 30 seconds`,
1173
+ ` - ~2,880 epochs per day`,
1174
+ ` - ~86,400 epochs per month (30 days)`,
1175
+ ``,
1176
+ `\u26A0\uFE0F CRITICAL - INSOLVENCY WARNING:`,
1177
+ ` - Storage providers consider accounts with LESS THAN 30 DAYS of`,
1178
+ ` remaining balance as INSOLVENT`,
1179
+ ` - Insolvent accounts may be REFUSED SERVICE or have data removed`,
1180
+ ` - ALWAYS maintain at least 45 days of balance for safety margin`,
1181
+ ` - Default notification threshold: 45 days (gives time to deposit)`,
1182
+ ` - Monitor your balance regularly and top up before hitting 30 days`,
1183
+ ``
1184
+ ].join("\n");
1185
+ const cdnPricingExplanation = [
1186
+ ``,
1187
+ `HOW CDN EGRESS PRICING WORKS:`,
1188
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1189
+ ``,
1190
+ `CDN enables fast file retrieval with egress (download) charges:`,
1191
+ ``,
1192
+ `1. EGRESS RATE: $7 per TiB downloaded`,
1193
+ ` - ~$0.0068 per GiB downloaded`,
1194
+ ` - Only pay for actual data transferred`,
1195
+ ``,
1196
+ `2. CDN CREDITS TOP-UP (Not a Fee!):`,
1197
+ ` - Creating a new CDN dataset requires 1 USDFC top-up`,
1198
+ ` - This is NOT a fee - it's pre-paid egress credits`,
1199
+ ` - 1 USDFC = ~146.29 GiB of download credits`,
1200
+ ` - Credits are deducted as files are downloaded`,
1201
+ ``,
1202
+ `3. REUSING DATASETS:`,
1203
+ ` - Adding files to existing CDN dataset = NO additional top-up`,
1204
+ ` - Only the first dataset creation requires the 1 USDFC top-up`,
1205
+ ` - Your egress credits work across all your CDN datasets`,
1206
+ ``,
1207
+ `4. TOPPING UP CREDITS:`,
1208
+ ` - Top up anytime to avoid running out`,
1209
+ ` - Monitor your remaining egress credits`,
1210
+ ` - No expiration on credits`,
1211
+ ``,
1212
+ `5. EXAMPLE CALCULATIONS:`,
1213
+ ` - 1 USDFC \u2192 ~146 GiB of downloads`,
1214
+ ` - 10 USDFC \u2192 ~1.43 TiB of downloads`,
1215
+ ` - 70 USDFC \u2192 ~10 TiB of downloads`,
1216
+ ``,
1217
+ `6. WHEN TO USE CDN:`,
1218
+ ` - Frequently accessed files`,
1219
+ ` - Public-facing content`,
1220
+ ` - Applications requiring fast retrieval`,
1221
+ ` - Skip CDN for archival/backup storage`,
1222
+ ``
1223
+ ].join("\n");
1224
+ return {
1225
+ success: true,
1226
+ monthlyCost: (Number(estimate.monthlyCost) / 1e18).toString(),
1227
+ totalCost: estimate.totalCostFormatted,
1228
+ cdnSetupCost: estimate.cdnSetupCostFormatted,
1229
+ cdnEgressCredits: createCDNDataset ? `${cdnEgressCreditsGiB.toFixed(2)} GiB (~${(cdnEgressCreditsGiB / 1024).toFixed(4)} TiB)` : "0 GiB",
1230
+ details: {
1231
+ ...estimate.details,
1232
+ pricePerTiBPerMonth: estimate.details.pricePerTiBPerMonth.toString(),
1233
+ minimumPricePerMonth: estimate.details.minimumPricePerMonth.toString()
1234
+ },
1235
+ breakdown: breakdown.join("\n"),
1236
+ pricingExplanation,
1237
+ cdnPricingExplanation,
1238
+ message: `Estimated cost: ${estimate.totalCostFormatted} USDFC for ${estimate.details.sizeFormatted} over ${durationInMonths} month${durationInMonths !== 1 ? "s" : ""}${createCDNDataset ? " (includes 1 USDFC CDN egress credits top-up = ~" + cdnEgressCreditsGiB.toFixed(2) + " GiB of downloads)" : ""}`
1239
+ };
1240
+ } catch (error) {
1241
+ return {
1242
+ success: false,
1243
+ error: error.message,
1244
+ message: `Failed to estimate storage cost: ${error.message}`
1245
+ };
1246
+ }
1247
+ }
1248
+ });
1249
+ var convertStorageSize = createTool6({
1250
+ id: "convertStorageSize",
1251
+ description: "Convert storage sizes between different units (bytes, KiB, MiB, GiB, TiB). Useful when users provide sizes in different formats and you need to calculate storage costs.",
1252
+ inputSchema: z3.object({
1253
+ value: z3.number().positive().describe("The numeric value to convert"),
1254
+ fromUnit: z3.enum(["bytes", "KiB", "MiB", "GiB", "TiB"]).describe("The unit to convert from"),
1255
+ toUnit: z3.enum(["bytes", "KiB", "MiB", "GiB", "TiB"]).optional().describe("The unit to convert to. If not specified, returns all units")
1256
+ }),
1257
+ outputSchema: z3.object({
1258
+ success: z3.boolean(),
1259
+ bytes: z3.number().optional(),
1260
+ KiB: z3.number().optional(),
1261
+ MiB: z3.number().optional(),
1262
+ GiB: z3.number().optional(),
1263
+ TiB: z3.number().optional(),
1264
+ message: z3.string(),
1265
+ error: z3.string().optional()
1266
+ }),
1267
+ execute: async ({ context }) => {
1268
+ try {
1269
+ const { value, fromUnit, toUnit } = context;
1270
+ let bytes;
1271
+ switch (fromUnit) {
1272
+ case "bytes":
1273
+ bytes = value;
1274
+ break;
1275
+ case "KiB":
1276
+ bytes = value * Number(SIZE_CONSTANTS4.KiB);
1277
+ break;
1278
+ case "MiB":
1279
+ bytes = value * Number(SIZE_CONSTANTS4.MiB);
1280
+ break;
1281
+ case "GiB":
1282
+ bytes = value * Number(SIZE_CONSTANTS4.GiB);
1283
+ break;
1284
+ case "TiB":
1285
+ bytes = value * Number(SIZE_CONSTANTS4.TiB);
1286
+ break;
1287
+ }
1288
+ if (toUnit) {
1289
+ let result;
1290
+ switch (toUnit) {
1291
+ case "bytes":
1292
+ result = bytes;
1293
+ break;
1294
+ case "KiB":
1295
+ result = bytes / Number(SIZE_CONSTANTS4.KiB);
1296
+ break;
1297
+ case "MiB":
1298
+ result = bytes / Number(SIZE_CONSTANTS4.MiB);
1299
+ break;
1300
+ case "GiB":
1301
+ result = bytes / Number(SIZE_CONSTANTS4.GiB);
1302
+ break;
1303
+ case "TiB":
1304
+ result = bytes / Number(SIZE_CONSTANTS4.TiB);
1305
+ break;
1306
+ }
1307
+ return {
1308
+ success: true,
1309
+ [toUnit]: result,
1310
+ message: `${value} ${fromUnit} = ${result.toFixed(4)} ${toUnit}`
1311
+ };
1312
+ }
1313
+ return {
1314
+ success: true,
1315
+ bytes,
1316
+ KiB: bytes / Number(SIZE_CONSTANTS4.KiB),
1317
+ MiB: bytes / Number(SIZE_CONSTANTS4.MiB),
1318
+ GiB: bytes / Number(SIZE_CONSTANTS4.GiB),
1319
+ TiB: bytes / Number(SIZE_CONSTANTS4.TiB),
1320
+ message: `Converted ${value} ${fromUnit} to all units`
1321
+ };
1322
+ } catch (error) {
1323
+ return {
1324
+ success: false,
1325
+ error: error.message,
1326
+ message: `Failed to convert storage size: ${error.message}`
1327
+ };
1328
+ }
1329
+ }
1330
+ });
1331
+ var getStoragePricingInfo = createTool6({
1332
+ id: "getStoragePricingInfo",
1333
+ description: "Get comprehensive information about Filecoin OnchainCloud storage pricing, including cost structure, CDN egress pricing, payment model, and a detailed example of storing 1 TiB for 1 year. \u26A0\uFE0F CRITICAL: ALWAYS include the insolvency warning (accounts with less than 30 days balance are considered insolvent and may be refused service). Recommend 45-day minimum balance. Use this when users ask general questions about 'how much does storage cost', 'explain storage pricing', 'how does billing work', or want to understand the pricing model before calculating specific costs.",
1334
+ inputSchema: z3.object({
1335
+ includeCDNExample: z3.boolean().optional().default(true).describe("Include CDN pricing example. Default: true")
1336
+ }),
1337
+ outputSchema: z3.object({
1338
+ success: z3.boolean(),
1339
+ pricingOverview: z3.string().optional(),
1340
+ storageModel: z3.object({
1341
+ baseRate: z3.string(),
1342
+ minimumCharge: z3.string(),
1343
+ minimumThreshold: z3.string(),
1344
+ epochDuration: z3.string(),
1345
+ epochsPerDay: z3.number(),
1346
+ epochsPerMonth: z3.number()
1347
+ }).optional(),
1348
+ cdnModel: z3.object({
1349
+ egressRate: z3.string(),
1350
+ creditsTopUp: z3.string(),
1351
+ creditsPerUSDFC: z3.string(),
1352
+ isTopUpAFee: z3.boolean(),
1353
+ canTopUpMore: z3.boolean()
1354
+ }).optional(),
1355
+ exampleCalculation: z3.object({
1356
+ scenario: z3.string(),
1357
+ size: z3.string(),
1358
+ duration: z3.string(),
1359
+ monthlyCost: z3.string(),
1360
+ yearlyStorageCost: z3.string(),
1361
+ cdnTopUp: z3.string().optional(),
1362
+ cdnEgressCredits: z3.string().optional(),
1363
+ totalCost: z3.string(),
1364
+ breakdown: z3.string()
1365
+ }).optional(),
1366
+ paymentModel: z3.string().optional(),
1367
+ error: z3.string().optional(),
1368
+ message: z3.string()
1369
+ }),
1370
+ execute: async ({ context }) => {
1371
+ try {
1372
+ const { includeCDNExample } = context;
1373
+ const exampleSizeBytes = Number(SIZE_CONSTANTS4.TiB);
1374
+ const exampleDurationMonths = 12;
1375
+ const estimate = await estimateStorageCost(
1376
+ exampleSizeBytes,
1377
+ exampleDurationMonths,
1378
+ includeCDNExample
1379
+ );
1380
+ const cdnEgressCreditsGiB = includeCDNExample ? 1 / CDN_EGRESS_RATE_PER_TIB * 1024 : 0;
1381
+ const pricingOverview = [
1382
+ `FILECOIN ONCHAINCLOUD STORAGE PRICING`,
1383
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1384
+ ``,
1385
+ `Filecoin OnchainCloud provides decentralized storage with transparent,`,
1386
+ `usage-based pricing. You only pay for what you store, when you store it.`,
1387
+ ``,
1388
+ `COST COMPONENTS:`,
1389
+ ` \u2022 Storage: Pay per epoch (30-second intervals) for stored data`,
1390
+ ` \u2022 CDN Egress: Pay per TiB downloaded (optional, for fast retrieval)`,
1391
+ ` \u2022 No hidden fees, no minimum contracts, no lock-in periods`,
1392
+ ``
1393
+ ].join("\n");
1394
+ const storageModel = {
1395
+ baseRate: "$2.50 per TiB per month",
1396
+ minimumCharge: "$0.06 per month",
1397
+ minimumThreshold: "~24.567 GiB (applies to storage smaller than this)",
1398
+ epochDuration: "30 seconds",
1399
+ epochsPerDay: 2880,
1400
+ epochsPerMonth: 86400
1401
+ // 30 days
1402
+ };
1403
+ const cdnModel = includeCDNExample ? {
1404
+ egressRate: "$7 per TiB downloaded (~$0.0068 per GiB)",
1405
+ creditsTopUp: "1 USDFC required when creating first CDN dataset",
1406
+ creditsPerUSDFC: "~146.29 GiB of egress credits",
1407
+ isTopUpAFee: false,
1408
+ canTopUpMore: true
1409
+ } : void 0;
1410
+ const exampleBreakdown = [
1411
+ `DETAILED CALCULATION:`,
1412
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1413
+ ``,
1414
+ `Size: 1 TiB (1,099,511,627,776 bytes)`,
1415
+ `Duration: 12 months (1 year)`,
1416
+ ``,
1417
+ `STORAGE COSTS:`,
1418
+ ` Base Rate: $2.50/TiB/month`,
1419
+ ` Calculation: 1 TiB \xD7 $2.50/month \xD7 12 months`,
1420
+ ` Monthly Cost: ${(Number(estimate.monthlyCost) / 1e18).toFixed(4)} USDFC`,
1421
+ ` Yearly Storage: ${(Number(estimate.totalCost - estimate.cdnSetupCost) / 1e18).toFixed(4)} USDFC`,
1422
+ ``
1423
+ ];
1424
+ if (includeCDNExample) {
1425
+ exampleBreakdown.push(
1426
+ `CDN EGRESS CREDITS (Optional):`,
1427
+ ` Initial Top-up: 1.0 USDFC (pre-paid credits, NOT a fee)`,
1428
+ ` Provides: ~${cdnEgressCreditsGiB.toFixed(2)} GiB of download credits`,
1429
+ ` Reusing dataset: No additional top-up required`,
1430
+ ` Additional top-ups: Available anytime`,
1431
+ ``
1432
+ );
1433
+ }
1434
+ exampleBreakdown.push(
1435
+ `TOTAL COST:`,
1436
+ ` Storage (12 months): ${(Number(estimate.totalCost - estimate.cdnSetupCost) / 1e18).toFixed(4)} USDFC`
1437
+ );
1438
+ if (includeCDNExample) {
1439
+ exampleBreakdown.push(
1440
+ ` CDN Credits Top-up: ${estimate.cdnSetupCostFormatted} USDFC`,
1441
+ ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
1442
+ ` Grand Total: ${estimate.totalCostFormatted} USDFC`,
1443
+ ``,
1444
+ `Note: The CDN top-up gives you ${cdnEgressCreditsGiB.toFixed(2)} GiB of downloads.`,
1445
+ ` If you download your 1 TiB once, you'd need ~7 USDFC more in credits.`
1446
+ );
1447
+ } else {
1448
+ exampleBreakdown.push(
1449
+ ` \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500`,
1450
+ ` Total: ${estimate.totalCostFormatted} USDFC`
1451
+ );
1452
+ }
1453
+ const paymentModel = [
1454
+ ``,
1455
+ `HOW PAYMENT WORKS:`,
1456
+ `\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501\u2501`,
1457
+ ``,
1458
+ `1. DEPOSIT FUNDS:`,
1459
+ ` \u2022 Add USDFC tokens to your storage account`,
1460
+ ` \u2022 Funds are held securely in your account`,
1461
+ ` \u2022 You maintain full control and can withdraw anytime`,
1462
+ ``,
1463
+ `2. SET ALLOWANCES (Safety Limits):`,
1464
+ ` \u2022 Rate Allowance: Max spending per epoch (prevents runaway costs)`,
1465
+ ` \u2022 Lockup Allowance: Max total funds that can be locked`,
1466
+ ` \u2022 Max Lockup Period: Maximum duration for any payment rail`,
1467
+ ``,
1468
+ `3. AUTOMATIC DEDUCTION:`,
1469
+ ` \u2022 Every epoch (30 seconds), storage costs are calculated`,
1470
+ ` \u2022 Amount is automatically deducted from your balance`,
1471
+ ` \u2022 No manual payments, no invoices, fully automated`,
1472
+ ``,
1473
+ `4. MONITORING:`,
1474
+ ` \u2022 Check your balance anytime`,
1475
+ ` \u2022 See your burn rate (spending per day/month)`,
1476
+ ` \u2022 Get notified when balance is low`,
1477
+ ` \u2022 Top up as needed to avoid service interruption`,
1478
+ ``,
1479
+ `5. TRANSPARENCY:`,
1480
+ ` \u2022 All costs calculated on-chain`,
1481
+ ` \u2022 Real-time pricing information available`,
1482
+ ` \u2022 No surprise charges or hidden fees`,
1483
+ ``,
1484
+ `\u26A0\uFE0F CRITICAL - INSOLVENCY WARNING:`,
1485
+ ` \u2022 Storage providers consider accounts with LESS THAN 30 DAYS of`,
1486
+ ` remaining balance as INSOLVENT`,
1487
+ ` \u2022 Insolvent accounts may be REFUSED SERVICE or have data removed`,
1488
+ ` \u2022 ALWAYS maintain at least 45 days of balance for safety margin`,
1489
+ ` \u2022 Default notification threshold: 45 days (gives time to deposit)`,
1490
+ ` \u2022 Set up monitoring and alerts to avoid running low on funds`,
1491
+ ``
1492
+ ].join("\n");
1493
+ return {
1494
+ success: true,
1495
+ pricingOverview,
1496
+ storageModel,
1497
+ cdnModel,
1498
+ exampleCalculation: {
1499
+ scenario: "Storing 1 TiB for 1 year" + (includeCDNExample ? " with CDN" : ""),
1500
+ size: "1 TiB (1,099,511,627,776 bytes)",
1501
+ duration: "12 months (1 year)",
1502
+ monthlyCost: `${(Number(estimate.monthlyCost) / 1e18).toFixed(4)} USDFC`,
1503
+ yearlyStorageCost: `${(Number(estimate.totalCost - estimate.cdnSetupCost) / 1e18).toFixed(4)} USDFC`,
1504
+ cdnTopUp: includeCDNExample ? estimate.cdnSetupCostFormatted + " USDFC" : void 0,
1505
+ cdnEgressCredits: includeCDNExample ? `~${cdnEgressCreditsGiB.toFixed(2)} GiB of downloads` : void 0,
1506
+ totalCost: estimate.totalCostFormatted + " USDFC",
1507
+ breakdown: exampleBreakdown.join("\n")
1508
+ },
1509
+ paymentModel,
1510
+ message: `Storage pricing: $2.50/TiB/month (minimum $0.06/month). Example: 1 TiB for 1 year = ${estimate.totalCostFormatted} USDFC${includeCDNExample ? " (includes 1 USDFC CDN credits = ~" + cdnEgressCreditsGiB.toFixed(2) + " GiB downloads)" : ""}`
1511
+ };
1512
+ } catch (error) {
1513
+ return {
1514
+ success: false,
1515
+ error: error.message,
1516
+ message: `Failed to get pricing info: ${error.message}`
1517
+ };
1518
+ }
1519
+ }
1520
+ });
1521
+ var storageCostTools = {
1522
+ estimateStoragePricing,
1523
+ convertStorageSize,
1524
+ getStoragePricingInfo
1525
+ };
1526
+
887
1527
  // src/mastra/tools/index.ts
888
1528
  var focStorageTools = {
889
1529
  ...datasetTools,
890
1530
  ...fileTools,
891
1531
  ...balanceTools,
892
1532
  ...paymentTools,
893
- ...providerTools
1533
+ ...providerTools,
1534
+ ...storageCostTools
894
1535
  };
895
1536
  var focStorageToolsArray = Object.values(focStorageTools);
896
1537
 
897
1538
  // src/mastra/workflows/e2e-file-upload.ts
898
1539
  import { createStep, createWorkflow } from "@mastra/core/workflows";
899
- import { z as z3 } from "zod";
900
- var e2eFileUploadInputSchema = z3.object({
901
- filePath: z3.string().describe("Absolute path to the file to upload"),
902
- datasetId: z3.string().optional().describe("Existing dataset ID to use"),
903
- withCDN: z3.boolean().optional().describe("Enable CDN for faster retrieval").default(false),
904
- persistenceDays: z3.number().optional().describe("Storage duration in days (default: 180)").default(env.PERSISTENCE_PERIOD_DAYS),
905
- notificationThresholdDays: z3.number().optional().describe("Notification threshold in days (default: 10)").default(env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS),
906
- fileMetadata: z3.record(z3.string(), z3.string()).optional().describe("Metadata for the file (max 4 key-value pairs)")
1540
+ import { z as z4 } from "zod";
1541
+ var e2eFileUploadInputSchema = z4.object({
1542
+ filePath: z4.string().describe("Absolute path to the file to upload"),
1543
+ datasetId: z4.string().optional().describe("Existing dataset ID to use"),
1544
+ withCDN: z4.boolean().optional().describe("Enable CDN for faster retrieval").default(false),
1545
+ persistenceDays: z4.number().optional().describe("Storage duration in days (default: 180)").default(env.PERSISTENCE_PERIOD_DAYS),
1546
+ notificationThresholdDays: z4.number().optional().describe("Notification threshold in days (default: 10)").default(env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS),
1547
+ fileMetadata: z4.record(z4.string(), z4.string()).optional().describe("Metadata for the file (max 4 key-value pairs)")
907
1548
  });
908
1549
  var checkBalanceStep = createStep({
909
1550
  id: "checkBalance",
910
1551
  description: "Check current FIL/USDFC balances and storage metrics",
911
1552
  inputSchema: e2eFileUploadInputSchema,
912
- outputSchema: z3.object({
913
- balances: z3.any(),
914
- needsPayment: z3.boolean(),
915
- depositNeeded: z3.number()
1553
+ outputSchema: z4.object({
1554
+ balances: z4.any(),
1555
+ needsPayment: z4.boolean(),
1556
+ depositNeeded: z4.number()
916
1557
  }),
917
1558
  execute: async ({ inputData, runtimeContext }) => {
918
1559
  console.log("\u{1F4CA} STEP 1: Checking balances and storage metrics...");
@@ -939,16 +1580,16 @@ var checkBalanceStep = createStep({
939
1580
  var processPaymentStep = createStep({
940
1581
  id: "processPayment",
941
1582
  description: "Deposit USDFC if insufficient balance detected",
942
- inputSchema: z3.object({
943
- balances: z3.any(),
944
- needsPayment: z3.boolean(),
945
- depositNeeded: z3.number()
1583
+ inputSchema: z4.object({
1584
+ balances: z4.any(),
1585
+ needsPayment: z4.boolean(),
1586
+ depositNeeded: z4.number()
946
1587
  }),
947
- outputSchema: z3.object({
948
- skipped: z3.boolean(),
949
- txHash: z3.string().optional(),
950
- depositAmount: z3.string().optional(),
951
- message: z3.string().optional()
1588
+ outputSchema: z4.object({
1589
+ skipped: z4.boolean(),
1590
+ txHash: z4.string().optional(),
1591
+ depositAmount: z4.string().optional(),
1592
+ message: z4.string().optional()
952
1593
  }),
953
1594
  execute: async ({ getStepResult, getInitData, runtimeContext }) => {
954
1595
  const balanceInfo = getStepResult("checkBalance");
@@ -999,18 +1640,18 @@ var processPaymentStep = createStep({
999
1640
  var uploadFileStep = createStep({
1000
1641
  id: "uploadFile",
1001
1642
  description: "Upload file to Filecoin storage",
1002
- inputSchema: z3.object({
1003
- skipped: z3.boolean(),
1004
- txHash: z3.string().optional(),
1005
- depositAmount: z3.string().optional(),
1006
- message: z3.string().optional()
1643
+ inputSchema: z4.object({
1644
+ skipped: z4.boolean(),
1645
+ txHash: z4.string().optional(),
1646
+ depositAmount: z4.string().optional(),
1647
+ message: z4.string().optional()
1007
1648
  }),
1008
- outputSchema: z3.object({
1009
- pieceCid: z3.string(),
1010
- txHash: z3.string().optional(),
1011
- fileName: z3.string(),
1012
- fileSize: z3.number(),
1013
- progressLog: z3.array(z3.string()).optional()
1649
+ outputSchema: z4.object({
1650
+ pieceCid: z4.string(),
1651
+ txHash: z4.string().optional(),
1652
+ fileName: z4.string(),
1653
+ fileSize: z4.number(),
1654
+ progressLog: z4.array(z4.string()).optional()
1014
1655
  }),
1015
1656
  execute: async ({ getInitData, runtimeContext }) => {
1016
1657
  const initData = getInitData();
@@ -1047,19 +1688,19 @@ var uploadFileStep = createStep({
1047
1688
  var summaryStep = createStep({
1048
1689
  id: "summary",
1049
1690
  description: "Generate final summary of the upload process",
1050
- inputSchema: z3.object({
1051
- pieceCid: z3.string(),
1052
- txHash: z3.string().optional(),
1053
- fileName: z3.string(),
1054
- fileSize: z3.number(),
1055
- progressLog: z3.array(z3.string()).optional()
1691
+ inputSchema: z4.object({
1692
+ pieceCid: z4.string(),
1693
+ txHash: z4.string().optional(),
1694
+ fileName: z4.string(),
1695
+ fileSize: z4.number(),
1696
+ progressLog: z4.array(z4.string()).optional()
1056
1697
  }),
1057
- outputSchema: z3.object({
1058
- success: z3.boolean(),
1059
- summary: z3.object({
1060
- balance: z3.any(),
1061
- payment: z3.any(),
1062
- upload: z3.any()
1698
+ outputSchema: z4.object({
1699
+ success: z4.boolean(),
1700
+ summary: z4.object({
1701
+ balance: z4.any(),
1702
+ payment: z4.any(),
1703
+ upload: z4.any()
1063
1704
  })
1064
1705
  }),
1065
1706
  execute: async ({ getStepResult }) => {
@@ -1097,12 +1738,12 @@ var e2eFileUploadWorkflow = createWorkflow({
1097
1738
  id: "e2eFileUpload",
1098
1739
  description: "Upload a file to Filecoin storage",
1099
1740
  inputSchema: e2eFileUploadInputSchema,
1100
- outputSchema: z3.object({
1101
- success: z3.boolean(),
1102
- summary: z3.object({
1103
- balance: z3.any(),
1104
- payment: z3.any(),
1105
- upload: z3.any()
1741
+ outputSchema: z4.object({
1742
+ success: z4.boolean(),
1743
+ summary: z4.object({
1744
+ balance: z4.any(),
1745
+ payment: z4.any(),
1746
+ upload: z4.any()
1106
1747
  })
1107
1748
  })
1108
1749
  }).then(checkBalanceStep).then(processPaymentStep).then(uploadFileStep).then(summaryStep).commit();
@@ -1137,6 +1778,7 @@ DATASET MANAGEMENT:
1137
1778
 
1138
1779
  - Returns: Datasets with piece CIDs, file sizes, provider details, retrieval URLs, blockchain storage proofs
1139
1780
  - Parameters: includeAllDatasets (boolean), filterByCDN (boolean)
1781
+ - Progress Tracking: Returns progressLog showing blockchain fetch and metadata processing steps
1140
1782
  - Use when: User wants to inventory files, check storage status, or locate specific uploads
1141
1783
 
1142
1784
  \u2022 getDataset: Retrieve detailed information about a specific dataset by its ID
@@ -1147,18 +1789,24 @@ DATASET MANAGEMENT:
1147
1789
 
1148
1790
  \u2022 createDataset: Create a new dataset container on Filecoin for organizing related files
1149
1791
 
1150
- - Parameters: withCDN (optional), providerId (optional), metadata (up to 10 key-value pairs)
1792
+ - Parameters: withCDN (optional), providerId (required), metadata (up to 10 key-value pairs)
1151
1793
  - Purpose: Define storage parameters (CDN, provider selection) that apply to all files added
1152
1794
  - Benefits: Better file organization, consistent retrieval performance
1153
- - Note: Payment is processed automatically for CDN-enabled datasets
1795
+ - Note: Payment is processed automatically for CDN-enabled datasets (1 USDFC)
1796
+ - Progress Tracking: Returns progressLog showing validation, CDN payment (if applicable), and dataset creation steps
1154
1797
  - Use when: User wants dedicated dataset or specific storage configuration
1798
+ - Important: providerId is required - use getProviders to list available providers first
1155
1799
 
1156
1800
  BALANCE & PAYMENT:
1157
1801
  \u2022 getBalances: Check wallet balances (FIL and USDFC tokens) and comprehensive storage metrics
1158
1802
 
1159
1803
  - Returns: Available funds, required deposits, days of storage remaining, allowance status
1160
- - Output: Both human-readable formatted values and raw data
1161
- - Parameters: storageCapacityBytes (optional), persistencePeriodDays (optional), notificationThresholdDays (optional)
1804
+ - Output: Both human-readable formatted values and raw data with progress log showing calculation parameters
1805
+ - Parameters: storageCapacityBytes (optional, default: 150 GiB), persistencePeriodDays (optional, default: 365 days), notificationThresholdDays (optional, default: 45 days)
1806
+ - Progress Log: Shows exact values used for calculations (capacity, persistence period, threshold)
1807
+ - \u26A0\uFE0F INSOLVENCY WARNING: Storage providers consider accounts with less than 30 days of remaining balance as INSOLVENT and may refuse service or remove data
1808
+ - Safety Margin: Default notification threshold is 45 days to ensure users have time to deposit before hitting the 30-day insolvency threshold
1809
+ - Agent Behavior: ALWAYS ask user before calling if they want default calculations or custom requirements. After showing results, ALWAYS offer to recalculate with different parameters. If days remaining is below 45, WARN user immediately about insolvency risk
1162
1810
  - Use when: Before upload operations to verify sufficient balance, or to monitor storage budget and plan deposits
1163
1811
 
1164
1812
  \u2022 processPayment: Deposit USDFC tokens and configure storage service allowances in a single transaction
@@ -1167,8 +1815,17 @@ BALANCE & PAYMENT:
1167
1815
  - Parameters: depositAmount (optional, default: 0)
1168
1816
  - Actions: Sets both rate allowance (per-epoch spending) and lockup allowance (total committed funds) to unlimited
1169
1817
  - Validation: Checks wallet balance before processing to prevent failed transactions
1818
+ - Progress Tracking: Returns progressLog showing conversion, transaction initiation, and confirmation steps
1170
1819
  - Use when: User needs to fund storage account before uploads or when balance is insufficient
1171
1820
 
1821
+ \u2022 processWithdrawal: Withdraw USDFC tokens from the storage account
1822
+
1823
+ - Parameters: withdrawalAmount (optional, default: 0)
1824
+ - Actions: Withdraws available funds from storage account back to wallet
1825
+ - Reduces storage service allowances and available balance
1826
+ - Progress Tracking: Returns progressLog showing conversion, transaction initiation, and confirmation steps
1827
+ - Use when: User wants to retrieve unused funds from storage account
1828
+
1172
1829
  PROVIDER MANAGEMENT:
1173
1830
  \u2022 getProviders: List storage providers available on the Filecoin network
1174
1831
 
@@ -1197,7 +1854,9 @@ PROVIDER MANAGEMENT:
1197
1854
 
1198
1855
  5. MONITOR STORAGE METRICS AND PERSISTENCE:
1199
1856
  - Check persistence days remaining regularly
1200
- - Top up allowances before they run out to avoid service interruption
1857
+ - \u26A0\uFE0F CRITICAL: Maintain at least 45 days of balance (insolvency threshold is 30 days)
1858
+ - Storage providers will refuse service if balance falls below 30 days
1859
+ - Top up allowances before they run out to avoid service interruption and potential data loss
1201
1860
 
1202
1861
  6. VALIDATE FILE PATHS:
1203
1862
  - Ensure filePath is absolute path
@@ -1223,10 +1882,11 @@ FOR DATASET MANAGEMENT:
1223
1882
 
1224
1883
  FOR BALANCE MANAGEMENT:
1225
1884
 
1226
- 1. Check Current State: getBalances with includeMetrics
1227
- 2. Calculate Needs: Estimate storage requirements
1228
- 3. Process Payment: processPayment with appropriate amounts
1229
- 4. Verify: Check balances again to confirm deposit
1885
+ 1. Check Current State: getBalances to view current balances and storage metrics
1886
+ 2. Evaluate Risk: If days remaining < 45, URGENT deposit needed (< 30 = INSOLVENT)
1887
+ 3. Calculate Needs: Estimate storage requirements using getBalances output
1888
+ 4. Process Payment: processPayment with appropriate depositAmount to maintain 45+ day buffer
1889
+ 5. Verify: Check balances again to confirm deposit and ensure above insolvency threshold
1230
1890
 
1231
1891
  \u{1F4A1} STRATEGIC CONSIDERATIONS:
1232
1892
 
@@ -1248,6 +1908,8 @@ COST MANAGEMENT:
1248
1908
  \u2022 Rate allowance: Controls per-epoch spending
1249
1909
  \u2022 Lockup allowance: Total committed for long-term storage
1250
1910
  \u2022 Monitor both to avoid overspending or service interruption
1911
+ \u2022 \u26A0\uFE0F INSOLVENCY THRESHOLD: Keep balance above 30 days minimum (recommend 45+ days)
1912
+ \u2022 Providers refuse service below 30 days - plan deposits accordingly
1251
1913
 
1252
1914
  \u{1F6A8} ERROR HANDLING:
1253
1915
 
@@ -1264,7 +1926,8 @@ DURING UPLOAD:
1264
1926
  \u2022 Each phase has status updates
1265
1927
 
1266
1928
  COMMON ERRORS:
1267
- \u2022 "Insufficient tUSDFC balance": Need to deposit more USDFC \u2192 call processPayment
1929
+ \u2022 "Insufficient tUSDFC balance": Need to deposit more USDFC \u2192 call processPayment (ensure 45+ days buffer)
1930
+ \u2022 "Insolvency detected": Balance below 30 days \u2192 URGENT deposit required, providers may refuse service
1268
1931
  \u2022 "Signer not found": Wallet not connected properly \u2192 check PRIVATE_KEY env var
1269
1932
  \u2022 "Transaction failed": User rejected signature or gas issue \u2192 explain and retry
1270
1933
  \u2022 "Provider connection failed": Try different provider or retry
@@ -1299,12 +1962,14 @@ ERROR RESPONSES:
1299
1962
  \u{1F3AF} AGENT BEHAVIOR GUIDELINES:
1300
1963
 
1301
1964
  1. BE PROACTIVE: Suggest checking balances before uploads
1302
- 2. BE CLEAR: Explain blockchain concepts simply
1303
- 3. BE PATIENT: Uploads take time (30-60 seconds typical)
1304
- 4. BE HELPFUL: Guide users through wallet signatures
1305
- 5. BE ACCURATE: Provide precise pieceCids and txHashes
1306
- 6. BE EFFICIENT: Reuse datasets when appropriate
1307
- 7. BE SECURE: Never store sensitive data without user confirmation
1965
+ 2. BE VIGILANT: ALWAYS warn if balance days remaining < 45 (insolvency risk at < 30)
1966
+ 3. BE CLEAR: Explain blockchain concepts simply
1967
+ 4. BE PATIENT: Uploads take time (30-60 seconds typical)
1968
+ 5. BE HELPFUL: Guide users through wallet signatures
1969
+ 6. BE ACCURATE: Provide precise pieceCids and txHashes
1970
+ 7. BE EFFICIENT: Reuse datasets when appropriate
1971
+ 8. BE SECURE: Never store sensitive data without user confirmation
1972
+ 9. BE URGENT: Treat insolvency warnings as critical - emphasize immediate action needed
1308
1973
 
1309
1974
  \u{1F510} SECURITY CONSIDERATIONS:
1310
1975
  \u2022 Never expose private keys or wallet seeds
@@ -1357,7 +2022,7 @@ var focStorageResources = {
1357
2022
  // src/mastra/index.ts
1358
2023
  var mcpServer = new MCPServer({
1359
2024
  name: "FOC Storage MCP",
1360
- version: "0.1.1",
2025
+ version: "0.1.3",
1361
2026
  description: "Professional-grade MCP server for decentralized file storage on Filecoin Onchain Cloud. Powered by the FOC-Synapse SDK, this server provides AI agents with seamless access to Filecoin's distributed storage network. Upload files with automatic payment handling, organize content in datasets, monitor storage balances, and manage providers - all through intuitive MCP tools. Supports both standard storage and CDN-enabled fast retrieval. Perfect for building AI applications that need persistent, censorship-resistant storage.",
1362
2027
  tools: focStorageTools,
1363
2028
  repository: {