@toon-protocol/sdk 0.2.0 → 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/README.md +6 -4
- package/dist/chunk-UP2VWCW5.js +38 -0
- package/dist/chunk-UP2VWCW5.js.map +1 -0
- package/dist/index.d.ts +646 -6
- package/dist/index.js +1046 -17
- package/dist/index.js.map +1 -1
- package/dist/node-WPA2UDEH.js +8221 -0
- package/dist/node-WPA2UDEH.js.map +1 -0
- package/package.json +6 -3
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,
|
|
@@ -140,7 +142,6 @@ function toChecksumAddress(addressHex) {
|
|
|
140
142
|
}
|
|
141
143
|
|
|
142
144
|
// src/handler-context.ts
|
|
143
|
-
import { createHash } from "crypto";
|
|
144
145
|
function createHandlerContext(options) {
|
|
145
146
|
let cachedEvent;
|
|
146
147
|
return {
|
|
@@ -166,11 +167,8 @@ function createHandlerContext(options) {
|
|
|
166
167
|
return cachedEvent;
|
|
167
168
|
},
|
|
168
169
|
accept(metadata) {
|
|
169
|
-
const toonBytes = Buffer.from(options.toon, "base64");
|
|
170
|
-
const fulfillment = createHash("sha256").update(toonBytes).digest().toString("base64");
|
|
171
170
|
return {
|
|
172
171
|
accept: true,
|
|
173
|
-
fulfillment,
|
|
174
172
|
...metadata ? { metadata } : {}
|
|
175
173
|
};
|
|
176
174
|
},
|
|
@@ -381,7 +379,15 @@ import {
|
|
|
381
379
|
buildIlpPrepare,
|
|
382
380
|
buildJobFeedbackEvent,
|
|
383
381
|
buildJobResultEvent,
|
|
384
|
-
parseJobResult
|
|
382
|
+
parseJobResult,
|
|
383
|
+
deriveChildAddress,
|
|
384
|
+
checkAddressCollision,
|
|
385
|
+
AddressRegistry,
|
|
386
|
+
ILP_ROOT_PREFIX,
|
|
387
|
+
calculateRouteAmount,
|
|
388
|
+
resolveRouteFees,
|
|
389
|
+
buildPrefixClaimEvent,
|
|
390
|
+
validatePrefix
|
|
385
391
|
} from "@toon-protocol/core";
|
|
386
392
|
import {
|
|
387
393
|
shallowParseToon,
|
|
@@ -390,11 +396,21 @@ import {
|
|
|
390
396
|
} from "@toon-protocol/core/toon";
|
|
391
397
|
|
|
392
398
|
// src/skill-descriptor.ts
|
|
399
|
+
import { ToonError as ToonError2 } from "@toon-protocol/core";
|
|
400
|
+
var HEX_64_REGEX = /^[0-9a-f]{64}$/;
|
|
393
401
|
function buildSkillDescriptor(registry, config = {}) {
|
|
394
402
|
const dvmKinds = registry.getDvmKinds();
|
|
395
403
|
if (dvmKinds.length === 0) {
|
|
396
404
|
return void 0;
|
|
397
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
|
+
}
|
|
398
414
|
const basePricePerByte = config.basePricePerByte ?? 10n;
|
|
399
415
|
const kindPricing = config.kindPricing ?? {};
|
|
400
416
|
const pricing = {};
|
|
@@ -416,6 +432,15 @@ function buildSkillDescriptor(registry, config = {}) {
|
|
|
416
432
|
if (config.models !== void 0) {
|
|
417
433
|
descriptor.models = config.models;
|
|
418
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
|
+
}
|
|
419
444
|
return descriptor;
|
|
420
445
|
}
|
|
421
446
|
|
|
@@ -578,11 +603,54 @@ function createNode(config) {
|
|
|
578
603
|
return { accept: false, code: "T00", message: "Internal error" };
|
|
579
604
|
}
|
|
580
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
|
+
}
|
|
581
647
|
const ilpInfo = {
|
|
582
|
-
ilpAddress:
|
|
648
|
+
ilpAddress: resolvedIlpAddress,
|
|
649
|
+
ilpAddresses: resolvedIlpAddresses,
|
|
583
650
|
btpEndpoint: config.btpEndpoint ?? "",
|
|
584
651
|
assetCode: config.assetCode ?? "USD",
|
|
585
|
-
assetScale: config.assetScale ?? 6
|
|
652
|
+
assetScale: config.assetScale ?? 6,
|
|
653
|
+
feePerByte: String(config.feePerByte ?? 0n)
|
|
586
654
|
};
|
|
587
655
|
let ilpClient;
|
|
588
656
|
let adminClient;
|
|
@@ -667,6 +735,7 @@ function createNode(config) {
|
|
|
667
735
|
keyBuffer.fill(0);
|
|
668
736
|
}
|
|
669
737
|
const hasSettlementAddresses = chainConfig.registryAddress && chainConfig.tokenNetworkAddress;
|
|
738
|
+
const hasChainProviders = config.chainProviders !== void 0 && config.chainProviders.length > 0;
|
|
670
739
|
autoCreatedConnector = new ConnectorNodeClass(
|
|
671
740
|
{
|
|
672
741
|
nodeId,
|
|
@@ -676,7 +745,12 @@ function createNode(config) {
|
|
|
676
745
|
peers: [],
|
|
677
746
|
routes: [],
|
|
678
747
|
localDelivery: { enabled: false },
|
|
679
|
-
|
|
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 && {
|
|
680
754
|
settlementInfra: {
|
|
681
755
|
enabled: true,
|
|
682
756
|
rpcUrl: chainConfig.rpcUrl,
|
|
@@ -684,7 +758,9 @@ function createNode(config) {
|
|
|
684
758
|
tokenAddress: chainConfig.usdcAddress,
|
|
685
759
|
privateKey: settlementPrivateKey
|
|
686
760
|
}
|
|
687
|
-
}
|
|
761
|
+
},
|
|
762
|
+
// NIP-59 transport privacy
|
|
763
|
+
...config.nip59 && { nip59: config.nip59 }
|
|
688
764
|
},
|
|
689
765
|
connectorLogger
|
|
690
766
|
);
|
|
@@ -721,11 +797,13 @@ function createNode(config) {
|
|
|
721
797
|
discoveryTrackerInstance.setChannelClient(toonNode.channelClient);
|
|
722
798
|
}
|
|
723
799
|
trackerRef.current = discoveryTrackerInstance;
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
800
|
+
for (const addr of ilpInfo.ilpAddresses) {
|
|
801
|
+
autoCreatedConnector.addRoute({
|
|
802
|
+
prefix: addr,
|
|
803
|
+
nextHop: nodeId,
|
|
804
|
+
priority: 100
|
|
805
|
+
});
|
|
806
|
+
}
|
|
729
807
|
const result = await toonNode.start();
|
|
730
808
|
return result;
|
|
731
809
|
};
|
|
@@ -842,6 +920,7 @@ function createNode(config) {
|
|
|
842
920
|
};
|
|
843
921
|
}
|
|
844
922
|
let started = false;
|
|
923
|
+
let lastBootstrapResults = [];
|
|
845
924
|
const node = {
|
|
846
925
|
get pubkey() {
|
|
847
926
|
return pubkey;
|
|
@@ -893,6 +972,7 @@ function createNode(config) {
|
|
|
893
972
|
try {
|
|
894
973
|
const result = await doStart();
|
|
895
974
|
started = true;
|
|
975
|
+
lastBootstrapResults = result.bootstrapResults;
|
|
896
976
|
return {
|
|
897
977
|
peerCount: result.peerCount,
|
|
898
978
|
channelCount: result.channelCount,
|
|
@@ -941,7 +1021,35 @@ function createNode(config) {
|
|
|
941
1021
|
}
|
|
942
1022
|
try {
|
|
943
1023
|
const toonData = encoder(event);
|
|
944
|
-
const
|
|
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
|
+
}
|
|
945
1053
|
const packet = buildIlpPrepare({
|
|
946
1054
|
destination: options.destination,
|
|
947
1055
|
amount,
|
|
@@ -952,7 +1060,7 @@ function createNode(config) {
|
|
|
952
1060
|
return {
|
|
953
1061
|
success: true,
|
|
954
1062
|
eventId: event.id,
|
|
955
|
-
|
|
1063
|
+
...result.data !== void 0 ? { data: result.data } : {}
|
|
956
1064
|
};
|
|
957
1065
|
}
|
|
958
1066
|
return {
|
|
@@ -987,6 +1095,9 @@ function createNode(config) {
|
|
|
987
1095
|
return node.publishEvent(resultEvent, options);
|
|
988
1096
|
},
|
|
989
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
|
+
);
|
|
990
1101
|
if (!started) {
|
|
991
1102
|
throw new NodeError(
|
|
992
1103
|
"Cannot settle compute: node not started. Call start() first."
|
|
@@ -1037,26 +1148,944 @@ function createNode(config) {
|
|
|
1037
1148
|
amount: computeAmount,
|
|
1038
1149
|
data: ""
|
|
1039
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
|
+
});
|
|
1040
1227
|
}
|
|
1041
1228
|
};
|
|
1042
1229
|
return node;
|
|
1043
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
|
+
}
|
|
1044
2065
|
export {
|
|
2066
|
+
ChunkManager,
|
|
1045
2067
|
HandlerError,
|
|
1046
2068
|
HandlerRegistry,
|
|
1047
2069
|
IdentityError,
|
|
1048
2070
|
NodeError,
|
|
1049
2071
|
PricingError,
|
|
2072
|
+
SwarmCoordinator,
|
|
2073
|
+
TurboUploadAdapter,
|
|
1050
2074
|
VerificationError,
|
|
2075
|
+
WorkflowOrchestrator,
|
|
1051
2076
|
buildSkillDescriptor,
|
|
2077
|
+
createArweaveDvmHandler,
|
|
1052
2078
|
createEventStorageHandler,
|
|
1053
2079
|
createHandlerContext,
|
|
1054
2080
|
createNode,
|
|
1055
2081
|
createPaymentHandlerBridge,
|
|
2082
|
+
createPrefixClaimHandler,
|
|
1056
2083
|
createPricingValidator,
|
|
1057
2084
|
createVerificationPipeline,
|
|
1058
2085
|
fromMnemonic,
|
|
1059
2086
|
fromSecretKey,
|
|
1060
|
-
generateMnemonic
|
|
2087
|
+
generateMnemonic,
|
|
2088
|
+
uploadBlob,
|
|
2089
|
+
uploadBlobChunked
|
|
1061
2090
|
};
|
|
1062
2091
|
//# sourceMappingURL=index.js.map
|