@fil-b/foc-storage-mcp 0.1.1 → 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 +1097 -427
  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,180 +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
- return `${serviceURL}/piece/${pieceCid}`;
408
- }
409
- };
410
- const datasetSizeInfo = data.pieces.reduce((acc, piece) => {
411
- acc.sizeInBytes += Number(pieces[piece.pieceCid.toV1().toString()].sizeBytes);
412
- acc.sizeInKiB += Number(pieces[piece.pieceCid.toV1().toString()].sizeKiB);
413
- acc.sizeInMiB += Number(pieces[piece.pieceCid.toV1().toString()].sizeMiB);
414
- acc.sizeInGB += Number(pieces[piece.pieceCid.toV1().toString()].sizeGiB);
415
- return acc;
416
- }, { sizeInBytes: 0, sizeInKiB: 0, sizeInMiB: 0, sizeInGB: 0, message: "" });
417
- const dataSetPieces = data.pieces.map((piece) => ({
418
- pieceCid: piece.pieceCid.toV1().toString(),
419
- retrievalUrl: getRetrievalUrl(piece.pieceCid.toV1().toString()),
420
- sizes: pieces[piece.pieceCid.toV1().toString()],
421
- metadata: piecesMetadata[piece.pieceId]
422
- }));
423
- return {
424
- datasetId: dataset.pdpVerifierDataSetId,
425
- withCDN: dataset.withCDN,
426
- datasetMetadata: dataset.metadata,
427
- totalDatasetSizeMessage: sizeInfoMessage(datasetSizeInfo),
428
- dataSetPieces
429
- };
430
- } catch (error) {
431
- return null;
432
- }
433
- })
434
- );
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 });
435
451
  return {
436
452
  success: true,
437
- datasets: enrichedDatasets.filter((dataset) => dataset !== null).map(serializeBigInt),
438
- count: enrichedDatasets.length,
439
- message: `Found ${enrichedDatasets.length} dataset(s)`
453
+ dataSetId: dataSetId.toString(),
454
+ txHash: dataset.txHash,
455
+ message: "Dataset created successfully"
440
456
  };
441
457
  } catch (error) {
442
- return createErrorResponse(
443
- "dataset_fetch_failed",
444
- `Failed to fetch datasets: ${error.message}`,
445
- { success: false }
446
- );
458
+ return {
459
+ success: false,
460
+ txHash: null,
461
+ dataSetId: null,
462
+ message: "Failed to create dataset: " + synapseErrorHandler(error)
463
+ };
447
464
  }
448
465
  };
449
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
+
450
505
  // src/services/storage-service.ts
451
506
  import {
452
- SIZE_CONSTANTS as SIZE_CONSTANTS4,
453
- TIME_CONSTANTS as TIME_CONSTANTS3,
454
- TOKENS as TOKENS2,
455
- WarmStorageService as WarmStorageService2
456
- } from "@filoz/synapse-sdk";
507
+ SIZE_CONSTANTS as SIZE_CONSTANTS3,
508
+ TIME_CONSTANTS
509
+ } from "@filoz/synapse-core/utils";
457
510
  import { formatUnits } from "viem";
458
- var checkStorageBalance = async (synapse, storageCapacityBytes = env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS4.GiB), persistencePeriodDays = env.PERSISTENCE_PERIOD_DAYS) => {
459
- const warmStorageService = await WarmStorageService2.create(synapse.getProvider(), synapse.getWarmStorageAddress());
460
- const [storageInfo, accountInfo, prices] = await Promise.all([
461
- synapse.storage.getStorageInfo(),
462
- synapse.payments.accountInfo(TOKENS2.USDFC),
463
- 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
+ })
464
542
  ]);
465
- let filRaw;
466
- try {
467
- filRaw = await synapse.payments.walletBalance();
468
- } catch (error) {
469
- console.error(error);
470
- 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.");
471
- }
472
- let usdfcRaw = await synapse.payments.walletBalance(TOKENS2.USDFC);
473
- const allowance = storageInfo.allowances;
474
- const availableFunds = accountInfo.availableFunds;
475
- const currentMonthlyRate = allowance.rateUsed * TIME_CONSTANTS3.EPOCHS_PER_MONTH;
476
- 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;
477
546
  const daysLeftAtMaxBurnRate = maxMonthlyRate === 0n ? Infinity : Number(availableFunds) / Number(maxMonthlyRate) * 30;
478
547
  const daysLeftAtBurnRate = currentMonthlyRate === 0n ? Infinity : Number(availableFunds) / Number(currentMonthlyRate) * 30;
479
- const amountNeeded = prices.perDay * BigInt(persistencePeriodDays);
480
- const totalDepositNeeded = daysLeftAtMaxBurnRate >= env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS ? 0n : amountNeeded - accountInfo.availableFunds;
481
- const availableToFreeUp = accountInfo.availableFunds > amountNeeded ? accountInfo.availableFunds - amountNeeded : 0n;
482
- const isRateSufficient = allowance.rateAllowance === MAX_UINT256;
483
- 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;
484
553
  const isSufficient = isRateSufficient && isLockupSufficient && daysLeftAtMaxBurnRate >= env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS;
485
554
  return {
486
555
  filBalance: filRaw,
@@ -513,8 +582,8 @@ var formatStorageBalanceResult = (checkStorageBalanceResult) => {
513
582
  isSufficient: checkStorageBalanceResult.isSufficient
514
583
  };
515
584
  };
516
- var formatBalance = (balance, ticker) => {
517
- return `${Number(Number(formatUnits(balance, 18)).toFixed(8))} ${ticker}`;
585
+ var formatBalance = (balance2, ticker) => {
586
+ return `${Number(Number(formatUnits(balance2, 18)).toFixed(8))} ${ticker}`;
518
587
  };
519
588
  var formatTime = (days) => {
520
589
  if (days === Infinity) {
@@ -531,17 +600,89 @@ var formatTime = (days) => {
531
600
  }
532
601
  return `${Math.fround(days / 365)} years`;
533
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
+ };
534
650
 
535
651
  // src/mastra/tools/dataset-tools.ts
536
- var getDatasets2 = createTool({
652
+ var getDatasets = createTool({
537
653
  id: "getDatasets",
538
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.",
539
655
  inputSchema: GetDatasetsSchema,
540
656
  outputSchema: GetDatasetsOutputSchema,
541
657
  execute: async ({ context }) => {
542
- const withCDN = context.filterByCDN ?? false;
543
- const includeAll = context.includeAllDatasets ?? false;
544
- 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
+ }
545
686
  }
546
687
  });
547
688
  var getDataset = createTool({
@@ -551,11 +692,11 @@ var getDataset = createTool({
551
692
  outputSchema: GetDatasetOutputSchema,
552
693
  execute: async ({ context }) => {
553
694
  try {
554
- const dataset = await getDatasets(void 0, void 0, Number(context.datasetId));
695
+ const dataset = await getDataSetService(Number(context.datasetId));
555
696
  return {
556
- success: dataset.success,
557
- dataset: serializeBigInt(dataset.datasets[0]),
558
- message: dataset.message
697
+ success: true,
698
+ dataset: serializeBigInt(dataset),
699
+ message: "Dataset fetched successfully"
559
700
  };
560
701
  } catch (error) {
561
702
  return createErrorResponse(
@@ -568,60 +709,60 @@ var getDataset = createTool({
568
709
  });
569
710
  var createDataset = createTool({
570
711
  id: "createDataset",
571
- 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.",
572
713
  inputSchema: CreateDatasetSchema,
573
714
  outputSchema: CreateDatasetOutputSchema,
574
715
  execute: async ({ context }) => {
575
- try {
576
- const synapse = await getSynapseInstance();
577
- const withCDN = true;
578
- if (withCDN) {
579
- const paymentResult = await processStoragePayment(synapse, BigInt(env.TOTAL_STORAGE_NEEDED_GiB * Number(SIZE_CONSTANTS.GiB)), env.PERSISTENCE_PERIOD_DAYS);
580
- if (!paymentResult.success) {
581
- return createErrorResponse(
582
- "payment_failed",
583
- `Failed to process payment: ${paymentResult.txHash ?? "Unknown error"}`,
584
- { success: false }
585
- );
586
- }
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)}`);
587
751
  }
588
- let datasetId;
589
- let txHash;
590
- await synapse.createStorage({
591
- providerId: context.providerId ? parseInt(context.providerId) : void 0,
592
- forceCreateDataSet: true,
593
- metadata: context.metadata,
594
- callbacks: {
595
- onDataSetCreationStarted: (txResponse) => {
596
- txHash = txResponse.hash;
597
- console.log(`[Dataset] Creation started (tx: ${txResponse.hash})`);
598
- },
599
- onDataSetCreationProgress: (status) => {
600
- if (status.serverConfirmed) {
601
- datasetId = status.dataSetId?.toString() || void 0;
602
- console.log(`[Dataset] Ready (ID: ${status.dataSetId?.toString()})`);
603
- }
604
- }
605
- }
606
- });
607
752
  return {
608
- success: true,
609
- datasetId,
610
- txHash,
611
- message: "Dataset created successfully"
753
+ ...result,
754
+ progressLog
612
755
  };
613
- } catch (error) {
614
- return createErrorResponse(
615
- "dataset_creation_failed",
616
- `Failed to create dataset: ${error.message}`,
617
- { success: false }
618
- );
619
756
  }
757
+ return {
758
+ ...result,
759
+ progressLog
760
+ };
620
761
  }
621
762
  });
622
763
  var datasetTools = {
623
764
  getDataset,
624
- getDatasets: getDatasets2,
765
+ getDatasets,
625
766
  createDataset
626
767
  };
627
768
 
@@ -651,7 +792,6 @@ var uploadFile = createTool2({
651
792
  const synapse = await getSynapseInstance();
652
793
  log("Checking storage balance...");
653
794
  const storageMetrics = await checkStorageBalance(
654
- synapse,
655
795
  fileInfo.size,
656
796
  env.PERSISTENCE_PERIOD_DAYS
657
797
  );
@@ -660,22 +800,25 @@ var uploadFile = createTool2({
660
800
  return {
661
801
  success: false,
662
802
  error: "insufficient_balance",
663
- message: "Insufficient balance. Enable autoPayment or call processPayment first",
664
- required: {
665
- deposit: fromBaseUnits(storageMetrics.depositNeeded, 18)
666
- }
803
+ message: `Insufficient balance: ${fromBaseUnits(storageMetrics.depositNeeded, 18)} USDFC required. Enable autoPayment parameter or use processPayment tool to deposit funds first.`
667
804
  };
668
805
  }
669
806
  log(storageMetrics.depositNeeded > 0n ? "Insufficient balance, processing payment..." : "Insufficient service approvals, approving service...");
670
- await processStoragePayment(
671
- synapse,
672
- storageMetrics.depositNeeded,
673
- env.PERSISTENCE_PERIOD_DAYS
674
- );
675
- 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
+ }
676
819
  }
677
820
  log("Creating storage service...");
678
- const storageService = await synapse.createStorage({
821
+ const storageService = await synapse.storage.createContext({
679
822
  dataSetId: context.datasetId ? Number(context.datasetId) : void 0,
680
823
  withCDN: context.withCDN || false,
681
824
  callbacks: {
@@ -684,16 +827,8 @@ var uploadFile = createTool2({
684
827
  log(`Dataset provider: ${Number(info.provider.id)}`);
685
828
  log(`Is existing dataset: ${info.isExisting}`);
686
829
  },
687
- onDataSetCreationStarted: (txResponse) => {
688
- log(`Dataset creation started (tx: ${txResponse.hash})`);
689
- },
690
- onDataSetCreationProgress: (status) => {
691
- if (status.serverConfirmed) {
692
- log(`Dataset ready (ID: ${status.dataSetId})`);
693
- }
694
- },
695
- onProviderSelected: (provider) => {
696
- log(`Provider selected: ${provider.id}`);
830
+ onProviderSelected: (provider2) => {
831
+ log(`Provider selected: ${provider2.id}`);
697
832
  }
698
833
  }
699
834
  });
@@ -704,19 +839,24 @@ var uploadFile = createTool2({
704
839
  onUploadComplete: (piece) => {
705
840
  log(`Upload complete (pieceCid: ${piece.toV1().toString()})`);
706
841
  },
707
- onPieceAdded: (txResponse) => {
708
- uploadTxHash = txResponse?.hash;
709
- log(`Piece added to dataset (tx: ${txResponse?.hash || "pending"})`);
842
+ onPieceAdded: (hash) => {
843
+ uploadTxHash = hash;
844
+ log(`Piece added to dataset (tx: ${hash || "pending"})`);
710
845
  },
711
846
  onPieceConfirmed: () => {
712
847
  log("Piece confirmed on blockchain");
713
848
  }
714
849
  });
850
+ const provider = await getProvider(storageService.provider.id);
715
851
  const getRetrievalUrl = async (pieceCid2) => {
716
852
  if (context.withCDN) {
717
- 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}`;
718
855
  } else {
719
- return `${(await storageService.getProviderInfo()).products.PDP?.data.serviceURL || ""}/piece/${pieceCid2}`;
856
+ const serviceURL = provider.pdp.serviceURL;
857
+ const endsWithSlash = serviceURL.endsWith("/");
858
+ const serviceURLWithoutSlash = endsWithSlash ? serviceURL.slice(0, -1) : serviceURL;
859
+ return `${serviceURLWithoutSlash}/piece/${pieceCid2}`;
720
860
  }
721
861
  };
722
862
  const retrievalUrl = await getRetrievalUrl(pieceCid.toV1().toString());
@@ -725,6 +865,9 @@ var uploadFile = createTool2({
725
865
  console.log(`TX Hash: ${uploadTxHash}`);
726
866
  console.log(`File Name: ${fileName}`);
727
867
  console.log(`File Size: ${fileInfo.size}`);
868
+ if (uploadTxHash) {
869
+ log(`View transaction: ${getExplorerUrl(uploadTxHash)}`);
870
+ }
728
871
  return {
729
872
  success: true,
730
873
  pieceCid: pieceCid.toV1().toString(),
@@ -755,24 +898,53 @@ var fileTools = {
755
898
  import { createTool as createTool3 } from "@mastra/core";
756
899
  var getBalances = createTool3({
757
900
  id: "getBalances",
758
- 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.",
759
902
  inputSchema: GetBalancesSchema,
760
903
  outputSchema: GetBalancesOutputSchema,
761
904
  execute: async ({ context }) => {
905
+ const progressLog = [];
906
+ const log = (msg) => {
907
+ progressLog.push(msg);
908
+ };
762
909
  try {
763
- const synapse = await getSynapseInstance();
764
- 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.");
765
934
  return {
766
935
  success: true,
767
- checkStorageBalanceResultFormatted: formatStorageBalanceResult(checkStorageBalanceResult),
768
- 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}`
769
940
  };
770
941
  } catch (error) {
771
- return createErrorResponse(
772
- "balance_fetch_failed",
773
- `Failed to fetch balances: ${error.message}`,
774
- { success: false }
775
- );
942
+ return {
943
+ success: false,
944
+ error: "balance_fetch_failed",
945
+ message: `Failed to fetch balances: ${error.message}`,
946
+ progressLog
947
+ };
776
948
  }
777
949
  }
778
950
  });
@@ -784,61 +956,82 @@ var balanceTools = {
784
956
  import { createTool as createTool4 } from "@mastra/core";
785
957
  var processPayment = createTool4({
786
958
  id: "processPayment",
787
- 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.",
788
960
  inputSchema: ProcessPaymentSchema,
789
961
  outputSchema: ProcessPaymentOutputSchema,
790
962
  execute: async ({ context }) => {
791
- try {
792
- const synapse = await getSynapseInstance();
793
- const accountInfo = await synapse.payments.accountInfo(TOKENS.USDFC);
794
- const availableFunds = Number(accountInfo.availableFunds);
795
- const { depositAmount } = context;
796
- if (depositAmount === 0) {
797
- return {
798
- success: true,
799
- message: `You have sufficient balance to cover the storage needs.`,
800
- txHash: null,
801
- required: {
802
- deposit: depositAmount
803
- },
804
- available: availableFunds
805
- };
806
- }
807
- if (availableFunds < depositAmount) {
808
- return {
809
- success: false,
810
- error: "insufficient_balance",
811
- message: `Insufficient USDFC balance. Required: ${depositAmount}, Available: ${availableFunds}`,
812
- required: depositAmount,
813
- available: Number(availableFunds)
814
- };
815
- }
816
- const result = await processStoragePayment(
817
- synapse,
818
- BigInt(depositAmount),
819
- env.PERSISTENCE_PERIOD_DAYS
820
- );
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) {
821
973
  return {
822
- success: result.success,
823
- txHash: result.txHash,
824
- 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
825
978
  };
826
- } catch (error) {
827
- return createErrorResponse(
828
- "payment_failed",
829
- `Payment processing failed: ${error.message}`,
830
- { success: false }
831
- );
832
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
+ };
833
1025
  }
834
1026
  });
835
1027
  var paymentTools = {
836
- processPayment
1028
+ processPayment,
1029
+ processWithdrawal
837
1030
  };
838
1031
 
839
1032
  // src/mastra/tools/provider-tools.ts
840
1033
  import { createTool as createTool5 } from "@mastra/core";
841
- import { WarmStorageService as WarmStorageService3 } from "@filoz/synapse-sdk";
1034
+ import { readProviders as readProviders2 } from "@filoz/synapse-core/warm-storage";
842
1035
  var getProviders = createTool5({
843
1036
  id: "getProviders",
844
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.",
@@ -846,21 +1039,11 @@ var getProviders = createTool5({
846
1039
  outputSchema: GetProvidersOutputSchema,
847
1040
  execute: async ({ context }) => {
848
1041
  try {
849
- const synapse = await getSynapseInstance();
850
- const warmStorageService = await WarmStorageService3.create(
851
- synapse.getProvider(),
852
- synapse.getWarmStorageAddress()
1042
+ const providers = (await readProviders2(
1043
+ publicClient
1044
+ )).filter(
1045
+ (p) => context.onlyApproved ? p.isActive : true
853
1046
  );
854
- const approvedProviderIds = await warmStorageService.getApprovedProviderIds();
855
- const providersInfo = await Promise.all(
856
- approvedProviderIds.map(async (providerId) => {
857
- const providerInfo = await synapse.getProviderInfo(providerId);
858
- return providerInfo;
859
- })
860
- );
861
- const providers = context.onlyApproved !== false ? providersInfo.filter(
862
- (p) => approvedProviderIds.includes(p.id)
863
- ) : providersInfo;
864
1047
  return {
865
1048
  success: true,
866
1049
  providers: providers.map(serializeBigInt),
@@ -868,10 +1051,12 @@ var getProviders = createTool5({
868
1051
  message: `Found ${providers.length} provider(s)`
869
1052
  };
870
1053
  } catch (error) {
871
- return createErrorResponse(
872
- "provider_fetch_failed",
873
- `Failed to fetch providers: ${error.message}`
874
- );
1054
+ return {
1055
+ success: false,
1056
+ providers: [],
1057
+ error: error.message,
1058
+ message: `Failed to fetch providers: ${error.message}`
1059
+ };
875
1060
  }
876
1061
  }
877
1062
  });
@@ -879,35 +1064,496 @@ var providerTools = {
879
1064
  getProviders
880
1065
  };
881
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
+
882
1527
  // src/mastra/tools/index.ts
883
1528
  var focStorageTools = {
884
1529
  ...datasetTools,
885
1530
  ...fileTools,
886
1531
  ...balanceTools,
887
1532
  ...paymentTools,
888
- ...providerTools
1533
+ ...providerTools,
1534
+ ...storageCostTools
889
1535
  };
890
1536
  var focStorageToolsArray = Object.values(focStorageTools);
891
1537
 
892
1538
  // src/mastra/workflows/e2e-file-upload.ts
893
1539
  import { createStep, createWorkflow } from "@mastra/core/workflows";
894
- import { z as z3 } from "zod";
895
- var e2eFileUploadInputSchema = z3.object({
896
- filePath: z3.string().describe("Absolute path to the file to upload"),
897
- datasetId: z3.string().optional().describe("Existing dataset ID to use"),
898
- withCDN: z3.boolean().optional().describe("Enable CDN for faster retrieval").default(false),
899
- persistenceDays: z3.number().optional().describe("Storage duration in days (default: 180)").default(env.PERSISTENCE_PERIOD_DAYS),
900
- notificationThresholdDays: z3.number().optional().describe("Notification threshold in days (default: 10)").default(env.RUNOUT_NOTIFICATION_THRESHOLD_DAYS),
901
- 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)")
902
1548
  });
903
1549
  var checkBalanceStep = createStep({
904
1550
  id: "checkBalance",
905
1551
  description: "Check current FIL/USDFC balances and storage metrics",
906
1552
  inputSchema: e2eFileUploadInputSchema,
907
- outputSchema: z3.object({
908
- balances: z3.any(),
909
- needsPayment: z3.boolean(),
910
- depositNeeded: z3.number()
1553
+ outputSchema: z4.object({
1554
+ balances: z4.any(),
1555
+ needsPayment: z4.boolean(),
1556
+ depositNeeded: z4.number()
911
1557
  }),
912
1558
  execute: async ({ inputData, runtimeContext }) => {
913
1559
  console.log("\u{1F4CA} STEP 1: Checking balances and storage metrics...");
@@ -934,16 +1580,16 @@ var checkBalanceStep = createStep({
934
1580
  var processPaymentStep = createStep({
935
1581
  id: "processPayment",
936
1582
  description: "Deposit USDFC if insufficient balance detected",
937
- inputSchema: z3.object({
938
- balances: z3.any(),
939
- needsPayment: z3.boolean(),
940
- depositNeeded: z3.number()
1583
+ inputSchema: z4.object({
1584
+ balances: z4.any(),
1585
+ needsPayment: z4.boolean(),
1586
+ depositNeeded: z4.number()
941
1587
  }),
942
- outputSchema: z3.object({
943
- skipped: z3.boolean(),
944
- txHash: z3.string().optional(),
945
- depositAmount: z3.string().optional(),
946
- 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()
947
1593
  }),
948
1594
  execute: async ({ getStepResult, getInitData, runtimeContext }) => {
949
1595
  const balanceInfo = getStepResult("checkBalance");
@@ -994,18 +1640,18 @@ var processPaymentStep = createStep({
994
1640
  var uploadFileStep = createStep({
995
1641
  id: "uploadFile",
996
1642
  description: "Upload file to Filecoin storage",
997
- inputSchema: z3.object({
998
- skipped: z3.boolean(),
999
- txHash: z3.string().optional(),
1000
- depositAmount: z3.string().optional(),
1001
- 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()
1002
1648
  }),
1003
- outputSchema: z3.object({
1004
- pieceCid: z3.string(),
1005
- txHash: z3.string().optional(),
1006
- fileName: z3.string(),
1007
- fileSize: z3.number(),
1008
- 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()
1009
1655
  }),
1010
1656
  execute: async ({ getInitData, runtimeContext }) => {
1011
1657
  const initData = getInitData();
@@ -1042,19 +1688,19 @@ var uploadFileStep = createStep({
1042
1688
  var summaryStep = createStep({
1043
1689
  id: "summary",
1044
1690
  description: "Generate final summary of the upload process",
1045
- inputSchema: z3.object({
1046
- pieceCid: z3.string(),
1047
- txHash: z3.string().optional(),
1048
- fileName: z3.string(),
1049
- fileSize: z3.number(),
1050
- 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()
1051
1697
  }),
1052
- outputSchema: z3.object({
1053
- success: z3.boolean(),
1054
- summary: z3.object({
1055
- balance: z3.any(),
1056
- payment: z3.any(),
1057
- 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()
1058
1704
  })
1059
1705
  }),
1060
1706
  execute: async ({ getStepResult }) => {
@@ -1092,12 +1738,12 @@ var e2eFileUploadWorkflow = createWorkflow({
1092
1738
  id: "e2eFileUpload",
1093
1739
  description: "Upload a file to Filecoin storage",
1094
1740
  inputSchema: e2eFileUploadInputSchema,
1095
- outputSchema: z3.object({
1096
- success: z3.boolean(),
1097
- summary: z3.object({
1098
- balance: z3.any(),
1099
- payment: z3.any(),
1100
- 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()
1101
1747
  })
1102
1748
  })
1103
1749
  }).then(checkBalanceStep).then(processPaymentStep).then(uploadFileStep).then(summaryStep).commit();
@@ -1132,6 +1778,7 @@ DATASET MANAGEMENT:
1132
1778
 
1133
1779
  - Returns: Datasets with piece CIDs, file sizes, provider details, retrieval URLs, blockchain storage proofs
1134
1780
  - Parameters: includeAllDatasets (boolean), filterByCDN (boolean)
1781
+ - Progress Tracking: Returns progressLog showing blockchain fetch and metadata processing steps
1135
1782
  - Use when: User wants to inventory files, check storage status, or locate specific uploads
1136
1783
 
1137
1784
  \u2022 getDataset: Retrieve detailed information about a specific dataset by its ID
@@ -1142,18 +1789,24 @@ DATASET MANAGEMENT:
1142
1789
 
1143
1790
  \u2022 createDataset: Create a new dataset container on Filecoin for organizing related files
1144
1791
 
1145
- - 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)
1146
1793
  - Purpose: Define storage parameters (CDN, provider selection) that apply to all files added
1147
1794
  - Benefits: Better file organization, consistent retrieval performance
1148
- - 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
1149
1797
  - Use when: User wants dedicated dataset or specific storage configuration
1798
+ - Important: providerId is required - use getProviders to list available providers first
1150
1799
 
1151
1800
  BALANCE & PAYMENT:
1152
1801
  \u2022 getBalances: Check wallet balances (FIL and USDFC tokens) and comprehensive storage metrics
1153
1802
 
1154
1803
  - Returns: Available funds, required deposits, days of storage remaining, allowance status
1155
- - Output: Both human-readable formatted values and raw data
1156
- - 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
1157
1810
  - Use when: Before upload operations to verify sufficient balance, or to monitor storage budget and plan deposits
1158
1811
 
1159
1812
  \u2022 processPayment: Deposit USDFC tokens and configure storage service allowances in a single transaction
@@ -1162,8 +1815,17 @@ BALANCE & PAYMENT:
1162
1815
  - Parameters: depositAmount (optional, default: 0)
1163
1816
  - Actions: Sets both rate allowance (per-epoch spending) and lockup allowance (total committed funds) to unlimited
1164
1817
  - Validation: Checks wallet balance before processing to prevent failed transactions
1818
+ - Progress Tracking: Returns progressLog showing conversion, transaction initiation, and confirmation steps
1165
1819
  - Use when: User needs to fund storage account before uploads or when balance is insufficient
1166
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
+
1167
1829
  PROVIDER MANAGEMENT:
1168
1830
  \u2022 getProviders: List storage providers available on the Filecoin network
1169
1831
 
@@ -1192,7 +1854,9 @@ PROVIDER MANAGEMENT:
1192
1854
 
1193
1855
  5. MONITOR STORAGE METRICS AND PERSISTENCE:
1194
1856
  - Check persistence days remaining regularly
1195
- - 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
1196
1860
 
1197
1861
  6. VALIDATE FILE PATHS:
1198
1862
  - Ensure filePath is absolute path
@@ -1218,10 +1882,11 @@ FOR DATASET MANAGEMENT:
1218
1882
 
1219
1883
  FOR BALANCE MANAGEMENT:
1220
1884
 
1221
- 1. Check Current State: getBalances with includeMetrics
1222
- 2. Calculate Needs: Estimate storage requirements
1223
- 3. Process Payment: processPayment with appropriate amounts
1224
- 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
1225
1890
 
1226
1891
  \u{1F4A1} STRATEGIC CONSIDERATIONS:
1227
1892
 
@@ -1243,6 +1908,8 @@ COST MANAGEMENT:
1243
1908
  \u2022 Rate allowance: Controls per-epoch spending
1244
1909
  \u2022 Lockup allowance: Total committed for long-term storage
1245
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
1246
1913
 
1247
1914
  \u{1F6A8} ERROR HANDLING:
1248
1915
 
@@ -1259,7 +1926,8 @@ DURING UPLOAD:
1259
1926
  \u2022 Each phase has status updates
1260
1927
 
1261
1928
  COMMON ERRORS:
1262
- \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
1263
1931
  \u2022 "Signer not found": Wallet not connected properly \u2192 check PRIVATE_KEY env var
1264
1932
  \u2022 "Transaction failed": User rejected signature or gas issue \u2192 explain and retry
1265
1933
  \u2022 "Provider connection failed": Try different provider or retry
@@ -1294,12 +1962,14 @@ ERROR RESPONSES:
1294
1962
  \u{1F3AF} AGENT BEHAVIOR GUIDELINES:
1295
1963
 
1296
1964
  1. BE PROACTIVE: Suggest checking balances before uploads
1297
- 2. BE CLEAR: Explain blockchain concepts simply
1298
- 3. BE PATIENT: Uploads take time (30-60 seconds typical)
1299
- 4. BE HELPFUL: Guide users through wallet signatures
1300
- 5. BE ACCURATE: Provide precise pieceCids and txHashes
1301
- 6. BE EFFICIENT: Reuse datasets when appropriate
1302
- 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
1303
1973
 
1304
1974
  \u{1F510} SECURITY CONSIDERATIONS:
1305
1975
  \u2022 Never expose private keys or wallet seeds
@@ -1352,7 +2022,7 @@ var focStorageResources = {
1352
2022
  // src/mastra/index.ts
1353
2023
  var mcpServer = new MCPServer({
1354
2024
  name: "FOC Storage MCP",
1355
- version: "0.1.0",
2025
+ version: "0.1.3",
1356
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.",
1357
2027
  tools: focStorageTools,
1358
2028
  repository: {