@toon-protocol/sdk 0.1.5 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,3 +1,5 @@
1
+ import "./chunk-UP2VWCW5.js";
2
+
1
3
  // src/identity.ts
2
4
  import {
3
5
  generateMnemonic as _generateMnemonic,
@@ -167,7 +169,6 @@ function createHandlerContext(options) {
167
169
  accept(metadata) {
168
170
  return {
169
171
  accept: true,
170
- fulfillment: "default-fulfillment",
171
172
  ...metadata ? { metadata } : {}
172
173
  };
173
174
  },
@@ -378,7 +379,15 @@ import {
378
379
  buildIlpPrepare,
379
380
  buildJobFeedbackEvent,
380
381
  buildJobResultEvent,
381
- parseJobResult
382
+ parseJobResult,
383
+ deriveChildAddress,
384
+ checkAddressCollision,
385
+ AddressRegistry,
386
+ ILP_ROOT_PREFIX,
387
+ calculateRouteAmount,
388
+ resolveRouteFees,
389
+ buildPrefixClaimEvent,
390
+ validatePrefix
382
391
  } from "@toon-protocol/core";
383
392
  import {
384
393
  shallowParseToon,
@@ -387,11 +396,21 @@ import {
387
396
  } from "@toon-protocol/core/toon";
388
397
 
389
398
  // src/skill-descriptor.ts
399
+ import { ToonError as ToonError2 } from "@toon-protocol/core";
400
+ var HEX_64_REGEX = /^[0-9a-f]{64}$/;
390
401
  function buildSkillDescriptor(registry, config = {}) {
391
402
  const dvmKinds = registry.getDvmKinds();
392
403
  if (dvmKinds.length === 0) {
393
404
  return void 0;
394
405
  }
406
+ if (config.attestation !== void 0) {
407
+ if (!HEX_64_REGEX.test(config.attestation.eventId)) {
408
+ throw new ToonError2(
409
+ "Skill descriptor attestation eventId must be a 64-character lowercase hex string",
410
+ "DVM_SKILL_INVALID_ATTESTATION_EVENT_ID"
411
+ );
412
+ }
413
+ }
395
414
  const basePricePerByte = config.basePricePerByte ?? 10n;
396
415
  const kindPricing = config.kindPricing ?? {};
397
416
  const pricing = {};
@@ -413,6 +432,15 @@ function buildSkillDescriptor(registry, config = {}) {
413
432
  if (config.models !== void 0) {
414
433
  descriptor.models = config.models;
415
434
  }
435
+ if (config.reputation !== void 0) {
436
+ descriptor.reputation = config.reputation;
437
+ }
438
+ if (config.attestation !== void 0) {
439
+ descriptor.attestation = {
440
+ eventId: config.attestation.eventId,
441
+ enclaveImageHash: config.attestation.enclaveImageHash
442
+ };
443
+ }
416
444
  return descriptor;
417
445
  }
418
446
 
@@ -426,17 +454,13 @@ function createNode(config) {
426
454
  "NodeConfig: provide either connector or connectorUrl, not both"
427
455
  );
428
456
  }
429
- if (!hasConnector && !hasConnectorUrl) {
430
- throw new NodeError(
431
- "NodeConfig: one of connector or connectorUrl is required"
432
- );
433
- }
434
457
  if (hasConnectorUrl && config.handlerPort === void 0) {
435
458
  throw new NodeError(
436
459
  "NodeConfig: handlerPort is required when using connectorUrl (standalone mode)"
437
460
  );
438
461
  }
439
- const embeddedMode = hasConnector;
462
+ const autoCreateConnector = !hasConnector && !hasConnectorUrl;
463
+ const embeddedMode = hasConnector || autoCreateConnector;
440
464
  const chainConfig = resolveChainConfig(config.chain);
441
465
  let effectiveSettlementInfo = config.settlementInfo;
442
466
  if (!effectiveSettlementInfo) {
@@ -579,11 +603,54 @@ function createNode(config) {
579
603
  return { accept: false, code: "T00", message: "Internal error" };
580
604
  }
581
605
  };
606
+ let resolvedIlpAddress;
607
+ let resolvedIlpAddresses;
608
+ if (config.upstreamPrefixes !== void 0 && config.upstreamPrefixes.length > 0) {
609
+ if (config.upstreamPrefix !== void 0) {
610
+ console.warn(
611
+ "[toon:warn] Both upstreamPrefixes and upstreamPrefix set; upstreamPrefixes takes priority"
612
+ );
613
+ }
614
+ resolvedIlpAddresses = config.upstreamPrefixes.map(
615
+ (p) => deriveChildAddress(p, pubkey)
616
+ );
617
+ for (let i = 0; i < resolvedIlpAddresses.length; i++) {
618
+ const others = resolvedIlpAddresses.filter((_, j) => j !== i);
619
+ checkAddressCollision(resolvedIlpAddresses[i], others);
620
+ }
621
+ resolvedIlpAddress = resolvedIlpAddresses[0];
622
+ } else if (config.upstreamPrefix !== void 0) {
623
+ resolvedIlpAddress = deriveChildAddress(config.upstreamPrefix, pubkey);
624
+ resolvedIlpAddresses = [resolvedIlpAddress];
625
+ } else if (config.ilpAddress !== void 0) {
626
+ resolvedIlpAddress = config.ilpAddress;
627
+ resolvedIlpAddresses = [resolvedIlpAddress];
628
+ } else {
629
+ resolvedIlpAddress = deriveChildAddress(ILP_ROOT_PREFIX, pubkey);
630
+ resolvedIlpAddresses = [resolvedIlpAddress];
631
+ }
632
+ const addressRegistry = new AddressRegistry();
633
+ if (config.upstreamPrefixes !== void 0 && config.upstreamPrefixes.length > 0) {
634
+ for (let i = 0; i < config.upstreamPrefixes.length; i++) {
635
+ addressRegistry.addAddress(
636
+ config.upstreamPrefixes[i],
637
+ resolvedIlpAddresses[i]
638
+ );
639
+ }
640
+ } else if (config.upstreamPrefix !== void 0) {
641
+ addressRegistry.addAddress(config.upstreamPrefix, resolvedIlpAddress);
642
+ } else if (config.ilpAddress !== void 0) {
643
+ addressRegistry.addAddress("explicit", resolvedIlpAddress);
644
+ } else {
645
+ addressRegistry.addAddress(ILP_ROOT_PREFIX, resolvedIlpAddress);
646
+ }
582
647
  const ilpInfo = {
583
- ilpAddress: config.ilpAddress ?? "g.toon.local",
648
+ ilpAddress: resolvedIlpAddress,
649
+ ilpAddresses: resolvedIlpAddresses,
584
650
  btpEndpoint: config.btpEndpoint ?? "",
585
651
  assetCode: config.assetCode ?? "USD",
586
- assetScale: config.assetScale ?? 6
652
+ assetScale: config.assetScale ?? 6,
653
+ feePerByte: String(config.feePerByte ?? 0n)
587
654
  };
588
655
  let ilpClient;
589
656
  let adminClient;
@@ -593,7 +660,8 @@ function createNode(config) {
593
660
  let httpServer = null;
594
661
  let doStart;
595
662
  let doStop;
596
- if (embeddedMode) {
663
+ let autoCreatedConnector = null;
664
+ if (embeddedMode && hasConnector) {
597
665
  const toonNode = createToonNode({
598
666
  connector: config.connector,
599
667
  handlePacket: pipelinedHandler,
@@ -618,6 +686,133 @@ function createNode(config) {
618
686
  trackerRef.current = discoveryTrackerInstance;
619
687
  doStart = async () => toonNode.start();
620
688
  doStop = async () => toonNode.stop();
689
+ } else if (autoCreateConnector) {
690
+ bootstrapServiceInstance = new BootstrapService(
691
+ {
692
+ knownPeers: config.knownPeers ?? [],
693
+ ardriveEnabled: config.ardriveEnabled ?? false,
694
+ defaultRelayUrl: config.relayUrl ?? "",
695
+ settlementInfo: effectiveSettlementInfo,
696
+ ownIlpAddress: ilpInfo.ilpAddress,
697
+ toonEncoder: encoder,
698
+ toonDecoder: decoder,
699
+ basePricePerByte: config.basePricePerByte ?? 10n
700
+ },
701
+ config.secretKey,
702
+ ilpInfo
703
+ );
704
+ discoveryTrackerInstance = createDiscoveryTracker({
705
+ secretKey: config.secretKey,
706
+ settlementInfo: effectiveSettlementInfo
707
+ });
708
+ ilpClient = {
709
+ sendIlpPacket: () => Promise.reject(new NodeError("Node not started. Call start() first."))
710
+ };
711
+ adminClient = {
712
+ addPeer: () => Promise.resolve(),
713
+ removePeer: () => Promise.resolve()
714
+ };
715
+ trackerRef.current = discoveryTrackerInstance;
716
+ doStart = async () => {
717
+ let ConnectorNodeClass;
718
+ let createConnectorLogger;
719
+ try {
720
+ const mod = await import("@toon-protocol/connector");
721
+ ConnectorNodeClass = mod.ConnectorNode;
722
+ createConnectorLogger = mod.createLogger;
723
+ } catch {
724
+ throw new NodeError(
725
+ "Auto-create connector requires @toon-protocol/connector as a peer dependency. Install it with: pnpm add @toon-protocol/connector"
726
+ );
727
+ }
728
+ const nodeId = `toon-${pubkey.slice(0, 16)}`;
729
+ const btpServerPort = config.btpServerPort ?? 3e3;
730
+ const connectorLogger = createConnectorLogger(nodeId, "warn");
731
+ let settlementPrivateKey = config.settlementPrivateKey;
732
+ if (!settlementPrivateKey) {
733
+ const keyBuffer = Buffer.from(config.secretKey);
734
+ settlementPrivateKey = `0x${keyBuffer.toString("hex")}`;
735
+ keyBuffer.fill(0);
736
+ }
737
+ const hasSettlementAddresses = chainConfig.registryAddress && chainConfig.tokenNetworkAddress;
738
+ const hasChainProviders = config.chainProviders !== void 0 && config.chainProviders.length > 0;
739
+ autoCreatedConnector = new ConnectorNodeClass(
740
+ {
741
+ nodeId,
742
+ btpServerPort,
743
+ environment: "development",
744
+ deploymentMode: "embedded",
745
+ peers: [],
746
+ routes: [],
747
+ localDelivery: { enabled: false },
748
+ // Multi-chain: use chainProviders when provided
749
+ ...hasChainProviders && {
750
+ chainProviders: config.chainProviders
751
+ },
752
+ // Legacy: fall back to settlementInfra when chainProviders absent
753
+ ...!hasChainProviders && hasSettlementAddresses && {
754
+ settlementInfra: {
755
+ enabled: true,
756
+ rpcUrl: chainConfig.rpcUrl,
757
+ registryAddress: chainConfig.registryAddress,
758
+ tokenAddress: chainConfig.usdcAddress,
759
+ privateKey: settlementPrivateKey
760
+ }
761
+ },
762
+ // NIP-59 transport privacy
763
+ ...config.nip59 && { nip59: config.nip59 }
764
+ },
765
+ connectorLogger
766
+ );
767
+ await autoCreatedConnector.start();
768
+ const connector = autoCreatedConnector;
769
+ const toonNode = createToonNode({
770
+ connector,
771
+ handlePacket: pipelinedHandler,
772
+ secretKey: config.secretKey,
773
+ ilpInfo,
774
+ toonEncoder: encoder,
775
+ toonDecoder: decoder,
776
+ relayUrl: config.relayUrl,
777
+ knownPeers: config.knownPeers,
778
+ settlementInfo: effectiveSettlementInfo,
779
+ basePricePerByte: config.basePricePerByte,
780
+ ardriveEnabled: config.ardriveEnabled
781
+ });
782
+ ilpClient = toonNode.ilpClient;
783
+ channelClient = toonNode.channelClient;
784
+ bootstrapServiceInstance.setIlpClient(toonNode.ilpClient);
785
+ bootstrapServiceInstance.setConnectorAdmin({
786
+ addPeer: () => Promise.resolve(),
787
+ removePeer: () => Promise.resolve()
788
+ });
789
+ if (toonNode.channelClient) {
790
+ bootstrapServiceInstance.setChannelClient(toonNode.channelClient);
791
+ }
792
+ discoveryTrackerInstance.setConnectorAdmin({
793
+ addPeer: () => Promise.resolve(),
794
+ removePeer: () => Promise.resolve()
795
+ });
796
+ if (toonNode.channelClient) {
797
+ discoveryTrackerInstance.setChannelClient(toonNode.channelClient);
798
+ }
799
+ trackerRef.current = discoveryTrackerInstance;
800
+ for (const addr of ilpInfo.ilpAddresses) {
801
+ autoCreatedConnector.addRoute({
802
+ prefix: addr,
803
+ nextHop: nodeId,
804
+ priority: 100
805
+ });
806
+ }
807
+ const result = await toonNode.start();
808
+ return result;
809
+ };
810
+ doStop = async () => {
811
+ if (autoCreatedConnector) {
812
+ await autoCreatedConnector.stop();
813
+ autoCreatedConnector = null;
814
+ }
815
+ };
621
816
  } else {
622
817
  const connectorUrl = config.connectorUrl;
623
818
  const handlerPort = config.handlerPort;
@@ -725,6 +920,7 @@ function createNode(config) {
725
920
  };
726
921
  }
727
922
  let started = false;
923
+ let lastBootstrapResults = [];
728
924
  const node = {
729
925
  get pubkey() {
730
926
  return pubkey;
@@ -733,7 +929,7 @@ function createNode(config) {
733
929
  return evmAddress;
734
930
  },
735
931
  get connector() {
736
- return config.connector ?? null;
932
+ return config.connector ?? autoCreatedConnector ?? null;
737
933
  },
738
934
  get channelClient() {
739
935
  return channelClient;
@@ -776,6 +972,7 @@ function createNode(config) {
776
972
  try {
777
973
  const result = await doStart();
778
974
  started = true;
975
+ lastBootstrapResults = result.bootstrapResults;
779
976
  return {
780
977
  peerCount: result.peerCount,
781
978
  channelCount: result.channelCount,
@@ -824,7 +1021,35 @@ function createNode(config) {
824
1021
  }
825
1022
  try {
826
1023
  const toonData = encoder(event);
827
- const amount = (config.basePricePerByte ?? 10n) * BigInt(toonData.length);
1024
+ const { hopFees, warnings } = resolveRouteFees({
1025
+ destination: options.destination,
1026
+ ownIlpAddress: ilpInfo.ilpAddress,
1027
+ discoveredPeers: discoveryTrackerInstance.getAllDiscoveredPeers()
1028
+ });
1029
+ for (const warning of warnings) {
1030
+ console.warn(`[publishEvent] ${warning}`);
1031
+ }
1032
+ const destinationAmount = options.amount ?? (config.basePricePerByte ?? 10n) * BigInt(toonData.length);
1033
+ if (options.bid !== void 0 && destinationAmount > options.bid) {
1034
+ throw new NodeError(
1035
+ `Cannot publish: destination amount ${destinationAmount} exceeds bid safety cap ${options.bid}`
1036
+ );
1037
+ }
1038
+ let amount;
1039
+ if (options.amount !== void 0) {
1040
+ const hopFeesTotal = calculateRouteAmount({
1041
+ basePricePerByte: 0n,
1042
+ packetByteLength: toonData.length,
1043
+ hopFees
1044
+ });
1045
+ amount = options.amount + hopFeesTotal;
1046
+ } else {
1047
+ amount = calculateRouteAmount({
1048
+ basePricePerByte: config.basePricePerByte ?? 10n,
1049
+ packetByteLength: toonData.length,
1050
+ hopFees
1051
+ });
1052
+ }
828
1053
  const packet = buildIlpPrepare({
829
1054
  destination: options.destination,
830
1055
  amount,
@@ -835,7 +1060,7 @@ function createNode(config) {
835
1060
  return {
836
1061
  success: true,
837
1062
  eventId: event.id,
838
- fulfillment: result.fulfillment ?? ""
1063
+ ...result.data !== void 0 ? { data: result.data } : {}
839
1064
  };
840
1065
  }
841
1066
  return {
@@ -870,6 +1095,9 @@ function createNode(config) {
870
1095
  return node.publishEvent(resultEvent, options);
871
1096
  },
872
1097
  async settleCompute(resultEvent, providerIlpAddress, options) {
1098
+ console.warn(
1099
+ "[settleCompute] DEPRECATED: Use publishEvent() with { amount } option instead. The prepaid model sends job request + payment in one packet."
1100
+ );
873
1101
  if (!started) {
874
1102
  throw new NodeError(
875
1103
  "Cannot settle compute: node not started. Call start() first."
@@ -920,26 +1148,944 @@ function createNode(config) {
920
1148
  amount: computeAmount,
921
1149
  data: ""
922
1150
  });
1151
+ },
1152
+ // Task 6.3: Address lifecycle management for multi-peered nodes
1153
+ addUpstreamPeer(upstreamPrefix) {
1154
+ const derivedAddress = deriveChildAddress(upstreamPrefix, pubkey);
1155
+ checkAddressCollision(derivedAddress, addressRegistry.getAddresses());
1156
+ addressRegistry.addAddress(upstreamPrefix, derivedAddress);
1157
+ ilpInfo.ilpAddresses = addressRegistry.getAddresses();
1158
+ ilpInfo.ilpAddress = addressRegistry.getPrimaryAddress() ?? ilpInfo.ilpAddress;
1159
+ if (autoCreatedConnector) {
1160
+ const nodeId = `toon-${pubkey.slice(0, 16)}`;
1161
+ autoCreatedConnector.addRoute({
1162
+ prefix: derivedAddress,
1163
+ nextHop: nodeId,
1164
+ priority: 100
1165
+ });
1166
+ }
1167
+ if (started && lastBootstrapResults.length > 0) {
1168
+ void bootstrapServiceInstance.republish(lastBootstrapResults);
1169
+ }
1170
+ },
1171
+ removeUpstreamPeer(upstreamPrefix) {
1172
+ if (addressRegistry.hasPrefix(upstreamPrefix) && addressRegistry.size <= 1) {
1173
+ throw new NodeError(
1174
+ "Cannot remove last upstream peer: a node must have at least one ILP address"
1175
+ );
1176
+ }
1177
+ const removedAddress = addressRegistry.removeAddress(upstreamPrefix);
1178
+ if (removedAddress === void 0) {
1179
+ return;
1180
+ }
1181
+ ilpInfo.ilpAddresses = addressRegistry.getAddresses();
1182
+ const newPrimary = addressRegistry.getPrimaryAddress();
1183
+ if (newPrimary !== void 0) {
1184
+ ilpInfo.ilpAddress = newPrimary;
1185
+ }
1186
+ if (autoCreatedConnector && autoCreatedConnector.removeRoute) {
1187
+ autoCreatedConnector.removeRoute(removedAddress);
1188
+ }
1189
+ if (started && lastBootstrapResults.length > 0) {
1190
+ void bootstrapServiceInstance.republish(lastBootstrapResults);
1191
+ }
1192
+ },
1193
+ async claimPrefix(prefix, upstreamDestination, options) {
1194
+ if (!started) {
1195
+ throw new NodeError(
1196
+ "Cannot claim prefix: node not started. Call start() first."
1197
+ );
1198
+ }
1199
+ const prefixValidation = validatePrefix(prefix);
1200
+ if (!prefixValidation.valid) {
1201
+ throw new NodeError(
1202
+ `Cannot claim prefix: ${prefixValidation.reason ?? "invalid prefix"}`
1203
+ );
1204
+ }
1205
+ let claimAmount = options?.prefixPrice;
1206
+ if (claimAmount === void 0) {
1207
+ const peers = discoveryTrackerInstance.getAllDiscoveredPeers();
1208
+ const upstreamPeer = peers.find(
1209
+ (p) => p.peerInfo.ilpAddress === upstreamDestination || p.peerInfo.ilpAddresses && p.peerInfo.ilpAddresses.includes(upstreamDestination)
1210
+ );
1211
+ if (upstreamPeer?.peerInfo.prefixPricing?.basePrice) {
1212
+ claimAmount = BigInt(upstreamPeer.peerInfo.prefixPricing.basePrice);
1213
+ } else {
1214
+ throw new NodeError(
1215
+ "Cannot claim prefix: no amount provided and upstream peer prefix pricing not found in discovery"
1216
+ );
1217
+ }
1218
+ }
1219
+ const claimEvent = buildPrefixClaimEvent(
1220
+ { requestedPrefix: prefix },
1221
+ config.secretKey
1222
+ );
1223
+ return node.publishEvent(claimEvent, {
1224
+ destination: upstreamDestination,
1225
+ amount: claimAmount
1226
+ });
923
1227
  }
924
1228
  };
925
1229
  return node;
926
1230
  }
1231
+
1232
+ // src/workflow-orchestrator.ts
1233
+ import { buildJobRequestEvent, parseJobFeedback } from "@toon-protocol/core";
1234
+ var WorkflowOrchestrator = class {
1235
+ node;
1236
+ options;
1237
+ state = "pending";
1238
+ definition = null;
1239
+ workflowEventId = "0".repeat(64);
1240
+ customerPubkey = "0".repeat(64);
1241
+ currentStepIndex = 0;
1242
+ stepStates = [];
1243
+ timeoutHandle = null;
1244
+ processedResultIds = /* @__PURE__ */ new Set();
1245
+ constructor(node, options) {
1246
+ this.node = node;
1247
+ this.options = {
1248
+ stepTimeoutMs: 3e5,
1249
+ // 5 minutes default
1250
+ ...options
1251
+ };
1252
+ if (options?.workflowEventId) {
1253
+ this.workflowEventId = options.workflowEventId;
1254
+ }
1255
+ if (options?.customerPubkey) {
1256
+ this.customerPubkey = options.customerPubkey;
1257
+ }
1258
+ }
1259
+ /**
1260
+ * Returns the current workflow state.
1261
+ */
1262
+ getState() {
1263
+ return this.state;
1264
+ }
1265
+ /**
1266
+ * Returns per-step state tracking data (for testing/debugging).
1267
+ */
1268
+ getStepStates() {
1269
+ return this.stepStates;
1270
+ }
1271
+ /**
1272
+ * Starts a workflow: parses the definition, creates and publishes
1273
+ * the Kind 5xxx job request for step 1, sets state to step_1_running.
1274
+ */
1275
+ async startWorkflow(definition) {
1276
+ if (this.state !== "pending") {
1277
+ throw new Error(
1278
+ "WorkflowOrchestrator.startWorkflow() called on a non-pending orchestrator. Create a new WorkflowOrchestrator instance for each workflow."
1279
+ );
1280
+ }
1281
+ if (!definition.steps || definition.steps.length === 0) {
1282
+ throw new Error(
1283
+ "WorkflowOrchestrator.startWorkflow() requires at least one step in the definition."
1284
+ );
1285
+ }
1286
+ this.definition = definition;
1287
+ this.stepStates = definition.steps.map((_, index) => ({
1288
+ index,
1289
+ settled: false
1290
+ }));
1291
+ if (this.options.eventStore) {
1292
+ const now = this.options.now ? this.options.now() : Date.now();
1293
+ const syntheticId = `wf${now.toString(16).padStart(16, "0")}${this.node.pubkey.slice(0, 48)}`;
1294
+ const stateEvent = {
1295
+ id: syntheticId.padEnd(64, "0").slice(0, 64),
1296
+ pubkey: this.node.pubkey,
1297
+ created_at: Math.floor(now / 1e3),
1298
+ kind: 10040,
1299
+ content: JSON.stringify({
1300
+ state: "started",
1301
+ definition
1302
+ }),
1303
+ tags: [],
1304
+ sig: "0".repeat(128)
1305
+ };
1306
+ await this.options.eventStore.store(stateEvent);
1307
+ }
1308
+ await this.publishStepRequest(
1309
+ 0,
1310
+ definition.initialInput.data,
1311
+ definition.initialInput.type
1312
+ );
1313
+ this.state = "step_1_running";
1314
+ this.currentStepIndex = 0;
1315
+ this.startStepTimeout();
1316
+ }
1317
+ /**
1318
+ * Handles a Kind 6xxx result event for the current step.
1319
+ *
1320
+ * If the current step matches, extracts the result content and either:
1321
+ * - Advances to the next step (publishes Kind 5xxx for step N+1)
1322
+ * - Marks workflow as completed (if this was the final step)
1323
+ *
1324
+ * Idempotent: re-processing a result for an already-advanced step is a no-op.
1325
+ */
1326
+ async handleStepResult(resultEvent) {
1327
+ if (!this.definition) return;
1328
+ if (resultEvent.kind < 6e3 || resultEvent.kind > 6999) return;
1329
+ if (this.processedResultIds.has(resultEvent.id)) return;
1330
+ const currentStep = this.stepStates[this.currentStepIndex];
1331
+ if (!currentStep) return;
1332
+ if (currentStep.resultEvent) return;
1333
+ if (this.state === "completed" || this.state.endsWith("_failed")) return;
1334
+ this.processedResultIds.add(resultEvent.id);
1335
+ this.clearStepTimeout();
1336
+ currentStep.resultEvent = resultEvent;
1337
+ await this.settleStep(this.currentStepIndex, resultEvent);
1338
+ if (this.options.eventStore) {
1339
+ await this.options.eventStore.store(resultEvent);
1340
+ }
1341
+ const isLastStep = this.currentStepIndex === this.definition.steps.length - 1;
1342
+ if (isLastStep) {
1343
+ this.state = "completed";
1344
+ await this.publishWorkflowNotification(
1345
+ "success",
1346
+ "Workflow completed successfully"
1347
+ );
1348
+ } else {
1349
+ const nextIndex = this.currentStepIndex + 1;
1350
+ const resultContent = resultEvent.content;
1351
+ await this.publishStepRequest(nextIndex, resultContent, "text");
1352
+ this.currentStepIndex = nextIndex;
1353
+ this.state = `step_${nextIndex + 1}_running`;
1354
+ this.startStepTimeout();
1355
+ }
1356
+ }
1357
+ /**
1358
+ * Handles a Kind 7000 feedback event for the current step.
1359
+ *
1360
+ * If the feedback status is 'error', marks the workflow as failed
1361
+ * and notifies the customer.
1362
+ */
1363
+ async handleStepFeedback(feedbackEvent) {
1364
+ if (!this.definition) return;
1365
+ if (this.state === "completed" || this.state.endsWith("_failed")) return;
1366
+ const parsed = parseJobFeedback(feedbackEvent);
1367
+ if (!parsed) return;
1368
+ if (parsed.status === "error") {
1369
+ this.clearStepTimeout();
1370
+ this.state = `step_${this.currentStepIndex + 1}_failed`;
1371
+ if (this.options.eventStore) {
1372
+ await this.options.eventStore.store(feedbackEvent);
1373
+ }
1374
+ await this.publishWorkflowNotification(
1375
+ "error",
1376
+ `Workflow failed at step ${this.currentStepIndex + 1}: ${parsed.content || "Unknown error"}`
1377
+ );
1378
+ }
1379
+ }
1380
+ /**
1381
+ * Cleans up resources (timeout handles).
1382
+ */
1383
+ destroy() {
1384
+ this.clearStepTimeout();
1385
+ }
1386
+ // ---------- Private Methods ----------
1387
+ /**
1388
+ * Publishes a Kind 5xxx job request for the given step.
1389
+ */
1390
+ async publishStepRequest(stepIndex, inputData, inputType) {
1391
+ if (!this.definition) return;
1392
+ const step = this.definition.steps[stepIndex];
1393
+ if (!step) return;
1394
+ const stepBid = this.getStepBid(stepIndex);
1395
+ const jobRequest = buildJobRequestEvent(
1396
+ {
1397
+ kind: step.kind,
1398
+ input: { data: inputData, type: inputType },
1399
+ bid: stepBid,
1400
+ output: "text/plain",
1401
+ content: step.description,
1402
+ targetProvider: step.targetProvider
1403
+ },
1404
+ this.getSecretKey()
1405
+ );
1406
+ const destination = this.options.destination ?? "g.toon.local";
1407
+ await this.node.publishEvent(jobRequest, { destination });
1408
+ const stepState = this.stepStates[stepIndex];
1409
+ if (stepState) {
1410
+ stepState.requestEventId = jobRequest.id;
1411
+ }
1412
+ }
1413
+ /**
1414
+ * Settles compute payment for a completed step.
1415
+ */
1416
+ async settleStep(stepIndex, resultEvent) {
1417
+ if (!this.definition) return;
1418
+ const stepState = this.stepStates[stepIndex];
1419
+ if (!stepState || stepState.settled) return;
1420
+ const step = this.definition.steps[stepIndex];
1421
+ if (!step) return;
1422
+ const providerIlpAddress = this.options.destination ?? "g.toon.provider";
1423
+ try {
1424
+ await this.node.settleCompute(resultEvent, providerIlpAddress, {
1425
+ originalBid: this.getStepBid(stepIndex)
1426
+ });
1427
+ stepState.settled = true;
1428
+ } catch (_err) {
1429
+ }
1430
+ }
1431
+ /**
1432
+ * Returns the bid allocation for a specific step.
1433
+ * Uses explicit bidAllocation if set, otherwise proportional split.
1434
+ */
1435
+ getStepBid(stepIndex) {
1436
+ if (!this.definition) return "0";
1437
+ const step = this.definition.steps[stepIndex];
1438
+ if (!step) return "0";
1439
+ if (step.bidAllocation !== void 0) {
1440
+ return step.bidAllocation;
1441
+ }
1442
+ try {
1443
+ const totalBid = BigInt(this.definition.totalBid);
1444
+ const stepCount = BigInt(this.definition.steps.length);
1445
+ const perStep = totalBid / stepCount;
1446
+ return perStep.toString();
1447
+ } catch {
1448
+ return "0";
1449
+ }
1450
+ }
1451
+ /**
1452
+ * Publishes a Kind 7000 workflow notification to the customer.
1453
+ */
1454
+ async publishWorkflowNotification(status, content) {
1455
+ const destination = this.options.destination ?? "g.toon.local";
1456
+ try {
1457
+ await this.node.publishFeedback(
1458
+ this.workflowEventId,
1459
+ this.customerPubkey,
1460
+ status,
1461
+ content,
1462
+ { destination }
1463
+ );
1464
+ } catch (_err) {
1465
+ }
1466
+ }
1467
+ /**
1468
+ * Starts a timeout timer for the current step.
1469
+ */
1470
+ startStepTimeout() {
1471
+ this.clearStepTimeout();
1472
+ const timeoutMs = this.options.stepTimeoutMs;
1473
+ const timerFn = this.options.setTimer ?? setTimeout;
1474
+ this.timeoutHandle = timerFn(() => {
1475
+ this.state = `step_${this.currentStepIndex + 1}_failed`;
1476
+ void this.publishWorkflowNotification(
1477
+ "error",
1478
+ `Workflow timed out at step ${this.currentStepIndex + 1} after ${timeoutMs}ms`
1479
+ );
1480
+ }, timeoutMs);
1481
+ }
1482
+ /**
1483
+ * Returns the secret key for signing step events.
1484
+ * Falls back to a deterministic key derived from the node pubkey
1485
+ * (for testing -- production should always pass secretKey in options).
1486
+ */
1487
+ getSecretKey() {
1488
+ if (this.options.secretKey) {
1489
+ return this.options.secretKey;
1490
+ }
1491
+ throw new Error(
1492
+ "WorkflowOrchestrator requires secretKey in options to sign step events"
1493
+ );
1494
+ }
1495
+ /**
1496
+ * Clears the current step timeout.
1497
+ */
1498
+ clearStepTimeout() {
1499
+ if (this.timeoutHandle !== null) {
1500
+ const cancelFn = this.options.clearTimer ?? clearTimeout;
1501
+ cancelFn(this.timeoutHandle);
1502
+ this.timeoutHandle = null;
1503
+ }
1504
+ }
1505
+ };
1506
+
1507
+ // src/swarm-coordinator.ts
1508
+ import {
1509
+ ToonError as ToonError3,
1510
+ parseSwarmRequest,
1511
+ parseSwarmSelection
1512
+ } from "@toon-protocol/core";
1513
+ var SwarmCoordinator = class {
1514
+ node;
1515
+ options;
1516
+ state = "collecting";
1517
+ swarmRequestId = "";
1518
+ customerPubkey = "";
1519
+ maxProviders = 0;
1520
+ submissions = [];
1521
+ timeoutHandle = null;
1522
+ started = false;
1523
+ settlementSucceeded = false;
1524
+ submissionIds = /* @__PURE__ */ new Set();
1525
+ constructor(node, options) {
1526
+ this.node = node;
1527
+ this.options = {
1528
+ timeoutMs: 6e5,
1529
+ // 10 minutes default
1530
+ ...options
1531
+ };
1532
+ }
1533
+ /**
1534
+ * Returns the current swarm state.
1535
+ */
1536
+ getState() {
1537
+ return this.state;
1538
+ }
1539
+ /**
1540
+ * Returns whether settlement was successfully completed.
1541
+ * Only meaningful when state is 'settled' (returns true).
1542
+ * If settlement fails, the state remains 'judging' and this returns false.
1543
+ */
1544
+ isSettlementSucceeded() {
1545
+ return this.settlementSucceeded;
1546
+ }
1547
+ /**
1548
+ * Returns the collected eligible submissions.
1549
+ */
1550
+ getSubmissions() {
1551
+ return this.submissions;
1552
+ }
1553
+ /**
1554
+ * Starts a swarm: parses the swarm request, initializes collection,
1555
+ * and starts the timeout timer.
1556
+ */
1557
+ async startSwarm(swarmRequest) {
1558
+ const parsed = parseSwarmRequest(swarmRequest);
1559
+ if (!parsed) {
1560
+ throw new ToonError3(
1561
+ "Invalid swarm request event: missing swarm tag or invalid Kind 5xxx",
1562
+ "DVM_INVALID_KIND"
1563
+ );
1564
+ }
1565
+ this.swarmRequestId = swarmRequest.id;
1566
+ this.customerPubkey = swarmRequest.pubkey;
1567
+ this.maxProviders = parsed.maxProviders;
1568
+ this.state = "collecting";
1569
+ this.submissions = [];
1570
+ this.submissionIds = /* @__PURE__ */ new Set();
1571
+ this.started = true;
1572
+ if (this.options.eventStore) {
1573
+ await this.options.eventStore.store(swarmRequest);
1574
+ }
1575
+ this.startTimeout();
1576
+ }
1577
+ /**
1578
+ * Handles a Kind 6xxx result event (provider submission).
1579
+ *
1580
+ * Validates the `e` tag references the swarm request, adds to the
1581
+ * submissions list, and checks if max providers is reached.
1582
+ *
1583
+ * Late submissions (after timeout or max reached) are ignored.
1584
+ * Submissions before startSwarm() are silently ignored.
1585
+ */
1586
+ async handleSubmission(resultEvent) {
1587
+ if (!this.started) return;
1588
+ if (this.state !== "collecting") return;
1589
+ if (resultEvent.kind < 6e3 || resultEvent.kind > 6999) return;
1590
+ const eTag = resultEvent.tags.find((t) => t[0] === "e");
1591
+ if (!eTag) return;
1592
+ const requestEventId = eTag[1];
1593
+ if (requestEventId === void 0 || requestEventId !== this.swarmRequestId)
1594
+ return;
1595
+ if (this.submissionIds.has(resultEvent.id)) return;
1596
+ this.submissionIds.add(resultEvent.id);
1597
+ this.submissions.push(resultEvent);
1598
+ if (this.options.eventStore) {
1599
+ await this.options.eventStore.store(resultEvent);
1600
+ }
1601
+ if (this.submissions.length >= this.maxProviders) {
1602
+ this.clearTimeout();
1603
+ this.state = "judging";
1604
+ }
1605
+ }
1606
+ /**
1607
+ * Selects the winner and settles compute payment.
1608
+ *
1609
+ * Validates:
1610
+ * - Swarm is in `judging` state (not `collecting`, `settled`, or `failed`)
1611
+ * - Selection references a submission in the collected set
1612
+ * - Swarm has not already been settled (idempotency guard)
1613
+ *
1614
+ * @throws ToonError with code DVM_SWARM_ALREADY_SETTLED if already settled
1615
+ * @throws ToonError with code DVM_SWARM_INVALID_SELECTION if winner not in submissions
1616
+ */
1617
+ async selectWinner(selectionEvent) {
1618
+ if (this.state === "settled") {
1619
+ throw new ToonError3(
1620
+ "Swarm has already been settled; duplicate selection rejected",
1621
+ "DVM_SWARM_ALREADY_SETTLED"
1622
+ );
1623
+ }
1624
+ if (this.state !== "judging") {
1625
+ throw new ToonError3(
1626
+ `Cannot select winner in state '${this.state}'; swarm must be in 'judging' state`,
1627
+ "DVM_SWARM_INVALID_SELECTION"
1628
+ );
1629
+ }
1630
+ if (selectionEvent.pubkey !== this.customerPubkey) {
1631
+ throw new ToonError3(
1632
+ "Selection event pubkey does not match the swarm request customer pubkey",
1633
+ "DVM_SWARM_INVALID_SELECTION"
1634
+ );
1635
+ }
1636
+ const parsed = parseSwarmSelection(selectionEvent);
1637
+ if (!parsed) {
1638
+ throw new ToonError3(
1639
+ "Invalid swarm selection event: missing winner tag or invalid Kind 7000",
1640
+ "DVM_SWARM_INVALID_SELECTION"
1641
+ );
1642
+ }
1643
+ if (parsed.swarmRequestEventId !== this.swarmRequestId) {
1644
+ throw new ToonError3(
1645
+ `Selection event references swarm '${parsed.swarmRequestEventId}' but this swarm is '${this.swarmRequestId}'`,
1646
+ "DVM_SWARM_INVALID_SELECTION"
1647
+ );
1648
+ }
1649
+ const winnerSubmission = this.submissions.find(
1650
+ (s) => s.id === parsed.winnerResultEventId
1651
+ );
1652
+ if (!winnerSubmission) {
1653
+ throw new ToonError3(
1654
+ `Winner result event ID '${parsed.winnerResultEventId}' not found in collected submissions`,
1655
+ "DVM_SWARM_INVALID_SELECTION"
1656
+ );
1657
+ }
1658
+ const providerIlpAddress = this.options.destination ?? "g.toon.provider";
1659
+ try {
1660
+ await this.node.settleCompute(winnerSubmission, providerIlpAddress);
1661
+ this.settlementSucceeded = true;
1662
+ } catch (_err) {
1663
+ this.settlementSucceeded = false;
1664
+ throw new ToonError3(
1665
+ "Settlement failed for winning provider; swarm remains in judging state for retry",
1666
+ "DVM_SWARM_SETTLEMENT_FAILED"
1667
+ );
1668
+ }
1669
+ if (this.options.eventStore) {
1670
+ await this.options.eventStore.store(selectionEvent);
1671
+ }
1672
+ this.state = "settled";
1673
+ }
1674
+ /**
1675
+ * Cleans up resources (timeout handles).
1676
+ */
1677
+ destroy() {
1678
+ this.clearTimeout();
1679
+ }
1680
+ // ---------- Private Methods ----------
1681
+ /**
1682
+ * Starts the collection timeout timer.
1683
+ */
1684
+ startTimeout() {
1685
+ this.clearTimeout();
1686
+ const timeoutMs = this.options.timeoutMs;
1687
+ const timerFn = this.options.setTimer ?? setTimeout;
1688
+ this.timeoutHandle = timerFn(() => {
1689
+ if (this.state !== "collecting") return;
1690
+ if (this.submissions.length === 0) {
1691
+ this.state = "failed";
1692
+ void this.publishNoSubmissionsFeedback();
1693
+ } else {
1694
+ this.state = "judging";
1695
+ }
1696
+ }, timeoutMs);
1697
+ }
1698
+ /**
1699
+ * Publishes a Kind 7000 feedback event indicating no submissions were received.
1700
+ */
1701
+ async publishNoSubmissionsFeedback() {
1702
+ const destination = this.options.destination ?? "g.toon.local";
1703
+ try {
1704
+ await this.node.publishFeedback(
1705
+ this.swarmRequestId,
1706
+ this.customerPubkey,
1707
+ "error",
1708
+ "Swarm timed out with no submissions",
1709
+ { destination }
1710
+ );
1711
+ } catch (_err) {
1712
+ }
1713
+ }
1714
+ /**
1715
+ * Clears the current timeout timer.
1716
+ */
1717
+ clearTimeout() {
1718
+ if (this.timeoutHandle !== null) {
1719
+ const cancelFn = this.options.clearTimer ?? clearTimeout;
1720
+ cancelFn(this.timeoutHandle);
1721
+ this.timeoutHandle = null;
1722
+ }
1723
+ }
1724
+ };
1725
+
1726
+ // src/prefix-claim-handler.ts
1727
+ import {
1728
+ parsePrefixClaimEvent,
1729
+ buildPrefixGrantEvent,
1730
+ validatePrefix as validatePrefix2
1731
+ } from "@toon-protocol/core";
1732
+ function createPrefixClaimHandler(options) {
1733
+ return async (ctx) => {
1734
+ const event = ctx.decode();
1735
+ const claim = parsePrefixClaimEvent(event);
1736
+ if (!claim) {
1737
+ return ctx.reject("F06", "Invalid prefix claim event: malformed content");
1738
+ }
1739
+ const validation = validatePrefix2(claim.requestedPrefix);
1740
+ if (!validation.valid) {
1741
+ return ctx.reject(
1742
+ "F06",
1743
+ `Invalid prefix: ${validation.reason ?? "unknown validation error"}`
1744
+ );
1745
+ }
1746
+ if (ctx.amount < options.prefixPricing.basePrice) {
1747
+ return ctx.reject(
1748
+ "F06",
1749
+ `Insufficient payment: received ${ctx.amount}, required ${options.prefixPricing.basePrice}`
1750
+ );
1751
+ }
1752
+ const claimedPrefixes = options.getClaimedPrefixes();
1753
+ if (claimedPrefixes.has(claim.requestedPrefix)) {
1754
+ return ctx.reject(
1755
+ "F06",
1756
+ `PREFIX_TAKEN: prefix "${claim.requestedPrefix}" is already claimed`
1757
+ );
1758
+ }
1759
+ const claimed = options.claimPrefix(claim.requestedPrefix, event.pubkey);
1760
+ if (!claimed) {
1761
+ return ctx.reject(
1762
+ "F06",
1763
+ `PREFIX_TAKEN: prefix "${claim.requestedPrefix}" is already claimed`
1764
+ );
1765
+ }
1766
+ const prefix = options.ilpAddressPrefix ?? "g.toon";
1767
+ const ilpAddress = `${prefix}.${claim.requestedPrefix}`;
1768
+ const grantEvent = buildPrefixGrantEvent(
1769
+ {
1770
+ grantedPrefix: claim.requestedPrefix,
1771
+ claimerPubkey: event.pubkey,
1772
+ ilpAddress
1773
+ },
1774
+ options.secretKey
1775
+ );
1776
+ await options.publishGrant(grantEvent);
1777
+ return ctx.accept();
1778
+ };
1779
+ }
1780
+
1781
+ // src/arweave/arweave-dvm-handler.ts
1782
+ import { parseBlobStorageRequest } from "@toon-protocol/core";
1783
+ var MIME_REGEX = /^[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*\/[a-zA-Z0-9][a-zA-Z0-9!#$&\-^_.+]*$/;
1784
+ function sanitizeContentType(contentType) {
1785
+ const base = (contentType.split(";")[0] ?? "").trim();
1786
+ if (base.length > 0 && base.length <= 255 && MIME_REGEX.test(base)) {
1787
+ return base;
1788
+ }
1789
+ return "application/octet-stream";
1790
+ }
1791
+ function createArweaveDvmHandler(config) {
1792
+ const { turboAdapter, chunkManager, arweaveTags } = config;
1793
+ return async (ctx) => {
1794
+ const event = ctx.decode();
1795
+ const parsed = parseBlobStorageRequest(event);
1796
+ if (!parsed) {
1797
+ return {
1798
+ accept: false,
1799
+ code: "F00",
1800
+ message: "Malformed kind:5094 blob storage request"
1801
+ };
1802
+ }
1803
+ const uploadTags = {
1804
+ ...arweaveTags,
1805
+ "Content-Type": sanitizeContentType(parsed.contentType)
1806
+ };
1807
+ if (parsed.uploadId !== void 0 && parsed.chunkIndex !== void 0 && parsed.totalChunks !== void 0) {
1808
+ try {
1809
+ const result = chunkManager.addChunk(
1810
+ parsed.uploadId,
1811
+ parsed.chunkIndex,
1812
+ parsed.totalChunks,
1813
+ parsed.blobData
1814
+ );
1815
+ if (!result.complete) {
1816
+ return {
1817
+ accept: true,
1818
+ data: Buffer.from(`ack:${parsed.chunkIndex}`).toString("base64")
1819
+ };
1820
+ }
1821
+ const assembled = result.assembled ?? Buffer.alloc(0);
1822
+ const { txId } = await turboAdapter.upload(assembled, uploadTags);
1823
+ return {
1824
+ accept: true,
1825
+ data: Buffer.from(txId).toString("base64")
1826
+ };
1827
+ } catch (error) {
1828
+ const internalMsg = error instanceof Error ? error.message : "Unknown chunk error";
1829
+ const safeMsg = internalMsg.includes("Duplicate chunkIndex") ? "Duplicate chunk rejected" : internalMsg.includes("Max active uploads") ? "Server busy, too many concurrent uploads" : internalMsg.includes("exceeds max bytes") ? "Upload size limit exceeded" : "Chunk processing error";
1830
+ return {
1831
+ accept: false,
1832
+ code: "F00",
1833
+ message: safeMsg
1834
+ };
1835
+ }
1836
+ }
1837
+ try {
1838
+ const { txId } = await turboAdapter.upload(parsed.blobData, uploadTags);
1839
+ return {
1840
+ accept: true,
1841
+ data: Buffer.from(txId).toString("base64")
1842
+ };
1843
+ } catch {
1844
+ return {
1845
+ accept: false,
1846
+ code: "T00",
1847
+ message: "Arweave upload failed"
1848
+ };
1849
+ }
1850
+ };
1851
+ }
1852
+
1853
+ // src/arweave/turbo-adapter.ts
1854
+ import { Readable } from "stream";
1855
+ var TurboUploadAdapter = class {
1856
+ turboClient;
1857
+ initialized = false;
1858
+ constructor(turboClient) {
1859
+ if (turboClient) {
1860
+ this.turboClient = turboClient;
1861
+ this.initialized = true;
1862
+ }
1863
+ }
1864
+ async getClient() {
1865
+ if (!this.initialized) {
1866
+ const { TurboFactory } = await import("@ardrive/turbo-sdk/node");
1867
+ const Arweave = (await import("./node-WPA2UDEH.js")).default;
1868
+ const arweave = Arweave.init({});
1869
+ const jwk = await arweave.crypto.generateJWK();
1870
+ this.turboClient = TurboFactory.authenticated({ privateKey: jwk });
1871
+ this.initialized = true;
1872
+ }
1873
+ return this.turboClient;
1874
+ }
1875
+ async upload(data, tags) {
1876
+ const client = await this.getClient();
1877
+ const tagArray = [];
1878
+ if (tags) {
1879
+ for (const [name, value] of Object.entries(tags)) {
1880
+ tagArray.push({ name, value });
1881
+ }
1882
+ }
1883
+ const result = await client.uploadFile({
1884
+ fileStreamFactory: () => Readable.from(data),
1885
+ fileSizeFactory: () => data.length,
1886
+ ...tagArray.length > 0 ? { dataItemOpts: { tags: tagArray } } : {}
1887
+ });
1888
+ return { txId: result.id };
1889
+ }
1890
+ };
1891
+
1892
+ // src/arweave/chunk-manager.ts
1893
+ var ChunkManager = class {
1894
+ uploads = /* @__PURE__ */ new Map();
1895
+ timeoutMs;
1896
+ maxActiveUploads;
1897
+ maxBytesPerUpload;
1898
+ constructor(config = {}) {
1899
+ this.timeoutMs = config.timeoutMs ?? 3e5;
1900
+ this.maxActiveUploads = config.maxActiveUploads ?? 100;
1901
+ this.maxBytesPerUpload = config.maxBytesPerUpload ?? 50 * 1024 * 1024;
1902
+ }
1903
+ /**
1904
+ * Add a chunk for a given uploadId.
1905
+ *
1906
+ * @param uploadId - Unique identifier for the upload session.
1907
+ * @param chunkIndex - Zero-based index of this chunk.
1908
+ * @param totalChunks - Total number of chunks expected.
1909
+ * @param data - The chunk data.
1910
+ * @returns Result indicating completion status or error.
1911
+ * @throws Error if memory cap reached or duplicate chunk.
1912
+ */
1913
+ addChunk(uploadId, chunkIndex, totalChunks, data) {
1914
+ let state = this.uploads.get(uploadId);
1915
+ if (!state) {
1916
+ if (this.uploads.size >= this.maxActiveUploads) {
1917
+ throw new Error(
1918
+ `Max active uploads reached (${this.maxActiveUploads}). Cannot accept new uploadId.`
1919
+ );
1920
+ }
1921
+ const timer = setTimeout(() => {
1922
+ this.cleanup(uploadId);
1923
+ }, this.timeoutMs);
1924
+ state = {
1925
+ chunks: /* @__PURE__ */ new Map(),
1926
+ totalChunks,
1927
+ timer
1928
+ };
1929
+ this.uploads.set(uploadId, state);
1930
+ }
1931
+ if (chunkIndex < 0 || chunkIndex >= state.totalChunks) {
1932
+ throw new Error(
1933
+ `chunkIndex ${chunkIndex} out of bounds for totalChunks ${state.totalChunks} (uploadId ${uploadId})`
1934
+ );
1935
+ }
1936
+ if (state.chunks.has(chunkIndex)) {
1937
+ throw new Error(
1938
+ `Duplicate chunkIndex ${chunkIndex} for uploadId ${uploadId}`
1939
+ );
1940
+ }
1941
+ let currentBytes = 0;
1942
+ for (const chunk of state.chunks.values()) {
1943
+ currentBytes += chunk.length;
1944
+ }
1945
+ if (currentBytes + data.length > this.maxBytesPerUpload) {
1946
+ this.cleanup(uploadId);
1947
+ throw new Error(
1948
+ `Upload ${uploadId} exceeds max bytes per upload (${this.maxBytesPerUpload})`
1949
+ );
1950
+ }
1951
+ state.chunks.set(chunkIndex, data);
1952
+ if (state.chunks.size === state.totalChunks) {
1953
+ const orderedChunks = [];
1954
+ for (let i = 0; i < state.totalChunks; i++) {
1955
+ const chunk = state.chunks.get(i);
1956
+ if (!chunk) {
1957
+ throw new Error(`Missing chunk ${i} for upload`);
1958
+ }
1959
+ orderedChunks.push(chunk);
1960
+ }
1961
+ const assembled = Buffer.concat(orderedChunks);
1962
+ this.cleanup(uploadId);
1963
+ return { complete: true, assembled };
1964
+ }
1965
+ return { complete: false };
1966
+ }
1967
+ /**
1968
+ * Check if an upload is complete (all chunks received).
1969
+ * Returns false if the uploadId is unknown (cleaned up or never started).
1970
+ */
1971
+ isComplete(uploadId) {
1972
+ const state = this.uploads.get(uploadId);
1973
+ if (!state) return false;
1974
+ return state.chunks.size === state.totalChunks;
1975
+ }
1976
+ /**
1977
+ * Clean up state for a given uploadId, clearing the timeout timer.
1978
+ */
1979
+ cleanup(uploadId) {
1980
+ const state = this.uploads.get(uploadId);
1981
+ if (state) {
1982
+ clearTimeout(state.timer);
1983
+ this.uploads.delete(uploadId);
1984
+ }
1985
+ }
1986
+ /**
1987
+ * Clean up all active uploads and their timers.
1988
+ * Call this on node shutdown to prevent timer leaks.
1989
+ */
1990
+ destroyAll() {
1991
+ for (const [, state] of this.uploads) {
1992
+ clearTimeout(state.timer);
1993
+ }
1994
+ this.uploads.clear();
1995
+ }
1996
+ };
1997
+
1998
+ // src/arweave/chunked-upload.ts
1999
+ import { randomUUID } from "crypto";
2000
+ import { buildBlobStorageRequest } from "@toon-protocol/core";
2001
+ async function uploadBlob(node, blob, destination, options) {
2002
+ const contentType = options.contentType ?? "application/octet-stream";
2003
+ const pricePerByte = options.pricePerByte ?? 10n;
2004
+ const amount = BigInt(blob.length) * pricePerByte;
2005
+ const event = buildBlobStorageRequest(
2006
+ {
2007
+ blobData: blob,
2008
+ contentType,
2009
+ bid: amount.toString()
2010
+ },
2011
+ options.secretKey
2012
+ );
2013
+ const result = await node.publishEvent(event, { destination, amount });
2014
+ if (!result.success) {
2015
+ throw new Error(`Blob upload failed: ${result.message ?? "unknown error"}`);
2016
+ }
2017
+ if (result.data) {
2018
+ return Buffer.from(result.data, "base64").toString("utf-8");
2019
+ }
2020
+ return result.eventId;
2021
+ }
2022
+ async function uploadBlobChunked(node, blob, destination, options) {
2023
+ if (blob.length === 0) {
2024
+ throw new Error("Cannot upload empty blob via chunked upload");
2025
+ }
2026
+ const chunkSize = options.chunkSize ?? 5e5;
2027
+ const contentType = options.contentType ?? "application/octet-stream";
2028
+ const pricePerByte = options.pricePerByte ?? 10n;
2029
+ const uploadId = randomUUID();
2030
+ const totalChunks = Math.ceil(blob.length / chunkSize);
2031
+ let lastResult;
2032
+ for (let i = 0; i < totalChunks; i++) {
2033
+ const start = i * chunkSize;
2034
+ const end = Math.min(start + chunkSize, blob.length);
2035
+ const chunkData = blob.subarray(start, end);
2036
+ const amount = BigInt(chunkData.length) * pricePerByte;
2037
+ const event = buildBlobStorageRequest(
2038
+ {
2039
+ blobData: Buffer.from(chunkData),
2040
+ contentType,
2041
+ bid: amount.toString(),
2042
+ params: [
2043
+ { key: "uploadId", value: uploadId },
2044
+ { key: "chunkIndex", value: String(i) },
2045
+ { key: "totalChunks", value: String(totalChunks) }
2046
+ ]
2047
+ },
2048
+ options.secretKey
2049
+ );
2050
+ lastResult = await node.publishEvent(event, { destination, amount });
2051
+ if (!lastResult.success) {
2052
+ throw new Error(
2053
+ `Chunk ${i}/${totalChunks} upload failed: ${lastResult.message ?? "unknown error"}`
2054
+ );
2055
+ }
2056
+ }
2057
+ if (!lastResult) {
2058
+ throw new Error("No chunks were uploaded");
2059
+ }
2060
+ if (lastResult.data) {
2061
+ return Buffer.from(lastResult.data, "base64").toString("utf-8");
2062
+ }
2063
+ return lastResult.eventId;
2064
+ }
927
2065
  export {
2066
+ ChunkManager,
928
2067
  HandlerError,
929
2068
  HandlerRegistry,
930
2069
  IdentityError,
931
2070
  NodeError,
932
2071
  PricingError,
2072
+ SwarmCoordinator,
2073
+ TurboUploadAdapter,
933
2074
  VerificationError,
2075
+ WorkflowOrchestrator,
934
2076
  buildSkillDescriptor,
2077
+ createArweaveDvmHandler,
935
2078
  createEventStorageHandler,
936
2079
  createHandlerContext,
937
2080
  createNode,
938
2081
  createPaymentHandlerBridge,
2082
+ createPrefixClaimHandler,
939
2083
  createPricingValidator,
940
2084
  createVerificationPipeline,
941
2085
  fromMnemonic,
942
2086
  fromSecretKey,
943
- generateMnemonic
2087
+ generateMnemonic,
2088
+ uploadBlob,
2089
+ uploadBlobChunked
944
2090
  };
945
2091
  //# sourceMappingURL=index.js.map