@originals/sdk 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/utils/hash.js +1 -0
  2. package/package.json +6 -5
  3. package/src/adapters/FeeOracleMock.ts +9 -0
  4. package/src/adapters/index.ts +5 -0
  5. package/src/adapters/providers/OrdHttpProvider.ts +126 -0
  6. package/src/adapters/providers/OrdMockProvider.ts +101 -0
  7. package/src/adapters/types.ts +66 -0
  8. package/src/bitcoin/BitcoinManager.ts +329 -0
  9. package/src/bitcoin/BroadcastClient.ts +54 -0
  10. package/src/bitcoin/OrdinalsClient.ts +120 -0
  11. package/src/bitcoin/PSBTBuilder.ts +106 -0
  12. package/src/bitcoin/fee-calculation.ts +38 -0
  13. package/src/bitcoin/providers/OrdNodeProvider.ts +92 -0
  14. package/src/bitcoin/providers/OrdinalsProvider.ts +56 -0
  15. package/src/bitcoin/providers/types.ts +59 -0
  16. package/src/bitcoin/transactions/commit.ts +465 -0
  17. package/src/bitcoin/transactions/index.ts +13 -0
  18. package/src/bitcoin/transfer.ts +43 -0
  19. package/src/bitcoin/utxo-selection.ts +322 -0
  20. package/src/bitcoin/utxo.ts +113 -0
  21. package/src/cel/ExternalReferenceManager.ts +87 -0
  22. package/src/cel/OriginalsCel.ts +460 -0
  23. package/src/cel/algorithms/createEventLog.ts +68 -0
  24. package/src/cel/algorithms/deactivateEventLog.ts +109 -0
  25. package/src/cel/algorithms/index.ts +11 -0
  26. package/src/cel/algorithms/updateEventLog.ts +99 -0
  27. package/src/cel/algorithms/verifyEventLog.ts +306 -0
  28. package/src/cel/algorithms/witnessEvent.ts +87 -0
  29. package/src/cel/cli/create.ts +330 -0
  30. package/src/cel/cli/index.ts +383 -0
  31. package/src/cel/cli/inspect.ts +549 -0
  32. package/src/cel/cli/migrate.ts +473 -0
  33. package/src/cel/cli/verify.ts +249 -0
  34. package/src/cel/hash.ts +71 -0
  35. package/src/cel/index.ts +16 -0
  36. package/src/cel/layers/BtcoCelManager.ts +408 -0
  37. package/src/cel/layers/PeerCelManager.ts +371 -0
  38. package/src/cel/layers/WebVHCelManager.ts +361 -0
  39. package/src/cel/layers/index.ts +27 -0
  40. package/src/cel/serialization/cbor.ts +189 -0
  41. package/src/cel/serialization/index.ts +10 -0
  42. package/src/cel/serialization/json.ts +209 -0
  43. package/src/cel/types.ts +160 -0
  44. package/src/cel/witnesses/BitcoinWitness.ts +184 -0
  45. package/src/cel/witnesses/HttpWitness.ts +241 -0
  46. package/src/cel/witnesses/WitnessService.ts +51 -0
  47. package/src/cel/witnesses/index.ts +11 -0
  48. package/src/contexts/credentials-v1.json +237 -0
  49. package/src/contexts/credentials-v2-examples.json +5 -0
  50. package/src/contexts/credentials-v2.json +340 -0
  51. package/src/contexts/credentials.json +237 -0
  52. package/src/contexts/data-integrity-v2.json +81 -0
  53. package/src/contexts/dids.json +58 -0
  54. package/src/contexts/ed255192020.json +93 -0
  55. package/src/contexts/ordinals-plus.json +23 -0
  56. package/src/contexts/originals.json +22 -0
  57. package/src/core/OriginalsSDK.ts +420 -0
  58. package/src/crypto/Multikey.ts +194 -0
  59. package/src/crypto/Signer.ts +262 -0
  60. package/src/crypto/noble-init.ts +138 -0
  61. package/src/did/BtcoDidResolver.ts +231 -0
  62. package/src/did/DIDManager.ts +705 -0
  63. package/src/did/Ed25519Verifier.ts +68 -0
  64. package/src/did/KeyManager.ts +239 -0
  65. package/src/did/WebVHManager.ts +499 -0
  66. package/src/did/createBtcoDidDocument.ts +60 -0
  67. package/src/did/providers/OrdinalsClientProviderAdapter.ts +68 -0
  68. package/src/events/EventEmitter.ts +222 -0
  69. package/src/events/index.ts +19 -0
  70. package/src/events/types.ts +331 -0
  71. package/src/examples/basic-usage.ts +78 -0
  72. package/src/examples/create-module-original.ts +435 -0
  73. package/src/examples/full-lifecycle-flow.ts +514 -0
  74. package/src/examples/run.ts +60 -0
  75. package/src/index.ts +204 -0
  76. package/src/kinds/KindRegistry.ts +320 -0
  77. package/src/kinds/index.ts +74 -0
  78. package/src/kinds/types.ts +470 -0
  79. package/src/kinds/validators/AgentValidator.ts +257 -0
  80. package/src/kinds/validators/AppValidator.ts +211 -0
  81. package/src/kinds/validators/DatasetValidator.ts +242 -0
  82. package/src/kinds/validators/DocumentValidator.ts +311 -0
  83. package/src/kinds/validators/MediaValidator.ts +269 -0
  84. package/src/kinds/validators/ModuleValidator.ts +225 -0
  85. package/src/kinds/validators/base.ts +276 -0
  86. package/src/kinds/validators/index.ts +12 -0
  87. package/src/lifecycle/BatchOperations.ts +381 -0
  88. package/src/lifecycle/LifecycleManager.ts +2156 -0
  89. package/src/lifecycle/OriginalsAsset.ts +524 -0
  90. package/src/lifecycle/ProvenanceQuery.ts +280 -0
  91. package/src/lifecycle/ResourceVersioning.ts +163 -0
  92. package/src/migration/MigrationManager.ts +587 -0
  93. package/src/migration/audit/AuditLogger.ts +176 -0
  94. package/src/migration/checkpoint/CheckpointManager.ts +112 -0
  95. package/src/migration/checkpoint/CheckpointStorage.ts +101 -0
  96. package/src/migration/index.ts +33 -0
  97. package/src/migration/operations/BaseMigration.ts +126 -0
  98. package/src/migration/operations/PeerToBtcoMigration.ts +105 -0
  99. package/src/migration/operations/PeerToWebvhMigration.ts +62 -0
  100. package/src/migration/operations/WebvhToBtcoMigration.ts +105 -0
  101. package/src/migration/rollback/RollbackManager.ts +170 -0
  102. package/src/migration/state/StateMachine.ts +92 -0
  103. package/src/migration/state/StateTracker.ts +156 -0
  104. package/src/migration/types.ts +356 -0
  105. package/src/migration/validation/BitcoinValidator.ts +107 -0
  106. package/src/migration/validation/CredentialValidator.ts +62 -0
  107. package/src/migration/validation/DIDCompatibilityValidator.ts +151 -0
  108. package/src/migration/validation/LifecycleValidator.ts +64 -0
  109. package/src/migration/validation/StorageValidator.ts +79 -0
  110. package/src/migration/validation/ValidationPipeline.ts +213 -0
  111. package/src/resources/ResourceManager.ts +655 -0
  112. package/src/resources/index.ts +21 -0
  113. package/src/resources/types.ts +202 -0
  114. package/src/storage/LocalStorageAdapter.ts +64 -0
  115. package/src/storage/MemoryStorageAdapter.ts +29 -0
  116. package/src/storage/StorageAdapter.ts +25 -0
  117. package/src/storage/index.ts +3 -0
  118. package/src/types/bitcoin.ts +98 -0
  119. package/src/types/common.ts +92 -0
  120. package/src/types/credentials.ts +89 -0
  121. package/src/types/did.ts +31 -0
  122. package/src/types/external-shims.d.ts +53 -0
  123. package/src/types/index.ts +7 -0
  124. package/src/types/network.ts +178 -0
  125. package/src/utils/EventLogger.ts +298 -0
  126. package/src/utils/Logger.ts +324 -0
  127. package/src/utils/MetricsCollector.ts +358 -0
  128. package/src/utils/bitcoin-address.ts +132 -0
  129. package/src/utils/cbor.ts +31 -0
  130. package/src/utils/encoding.ts +135 -0
  131. package/src/utils/hash.ts +12 -0
  132. package/src/utils/retry.ts +46 -0
  133. package/src/utils/satoshi-validation.ts +196 -0
  134. package/src/utils/serialization.ts +102 -0
  135. package/src/utils/telemetry.ts +44 -0
  136. package/src/utils/validation.ts +123 -0
  137. package/src/vc/CredentialManager.ts +955 -0
  138. package/src/vc/Issuer.ts +105 -0
  139. package/src/vc/Verifier.ts +54 -0
  140. package/src/vc/cryptosuites/bbs.ts +253 -0
  141. package/src/vc/cryptosuites/bbsSimple.ts +21 -0
  142. package/src/vc/cryptosuites/eddsa.ts +99 -0
  143. package/src/vc/documentLoader.ts +81 -0
  144. package/src/vc/proofs/data-integrity.ts +33 -0
  145. package/src/vc/utils/jsonld.ts +18 -0
@@ -0,0 +1,2156 @@
1
+ import {
2
+ OriginalsConfig,
3
+ AssetResource,
4
+ BitcoinTransaction,
5
+ KeyStore,
6
+ ExternalSigner,
7
+ VerifiableCredential,
8
+ LayerType
9
+ } from '../types';
10
+ import { BitcoinManager } from '../bitcoin/BitcoinManager';
11
+ import { DIDManager } from '../did/DIDManager';
12
+ import { CredentialManager } from '../vc/CredentialManager';
13
+ import { OriginalsAsset } from './OriginalsAsset';
14
+ import { MemoryStorageAdapter } from '../storage/MemoryStorageAdapter';
15
+ import { encodeBase64UrlMultibase, hexToBytes } from '../utils/encoding';
16
+ import { validateBitcoinAddress } from '../utils/bitcoin-address';
17
+ import { multikey } from '../crypto/Multikey';
18
+ import { EventEmitter } from '../events/EventEmitter';
19
+ import type { EventHandler, EventTypeMap } from '../events/types';
20
+ import { Logger } from '../utils/Logger';
21
+ import { MetricsCollector } from '../utils/MetricsCollector';
22
+ import {
23
+ BatchOperationExecutor,
24
+ BatchValidator,
25
+ BatchError,
26
+ type BatchResult,
27
+ type BatchOperationOptions,
28
+ type BatchInscriptionOptions,
29
+ } from './BatchOperations';
30
+ import {
31
+ type OriginalKind,
32
+ type OriginalManifest,
33
+ type CreateTypedOriginalOptions,
34
+ KindRegistry,
35
+ } from '../kinds';
36
+
37
+ /**
38
+ * Cost estimation result for migration operations
39
+ */
40
+ export interface CostEstimate {
41
+ /** Total estimated cost in satoshis */
42
+ totalSats: number;
43
+ /** Breakdown of costs */
44
+ breakdown: {
45
+ /** Network fee in satoshis */
46
+ networkFee: number;
47
+ /** Data cost for inscription (sat/vB * size) */
48
+ dataCost: number;
49
+ /** Dust output value */
50
+ dustValue: number;
51
+ };
52
+ /** Fee rate used for estimation (sat/vB) */
53
+ feeRate: number;
54
+ /** Data size in bytes */
55
+ dataSize: number;
56
+ /** Target layer for the migration */
57
+ targetLayer: LayerType;
58
+ /** Confidence level of estimate */
59
+ confidence: 'low' | 'medium' | 'high';
60
+ }
61
+
62
+ /**
63
+ * Migration validation result
64
+ */
65
+ export interface MigrationValidation {
66
+ /** Whether the migration is valid */
67
+ valid: boolean;
68
+ /** List of validation errors */
69
+ errors: string[];
70
+ /** List of warnings (non-blocking) */
71
+ warnings: string[];
72
+ /** Current layer of the asset */
73
+ currentLayer: LayerType;
74
+ /** Target layer for migration */
75
+ targetLayer: LayerType;
76
+ /** Checks performed */
77
+ checks: {
78
+ layerTransition: boolean;
79
+ resourcesValid: boolean;
80
+ credentialsValid: boolean;
81
+ didDocumentValid: boolean;
82
+ bitcoinReadiness?: boolean;
83
+ };
84
+ }
85
+
86
+ /**
87
+ * Progress callback for long-running operations
88
+ */
89
+ export type ProgressCallback = (progress: LifecycleProgress) => void;
90
+
91
+ /**
92
+ * Progress information for lifecycle operations
93
+ */
94
+ export interface LifecycleProgress {
95
+ /** Current operation phase */
96
+ phase: 'preparing' | 'validating' | 'processing' | 'committing' | 'confirming' | 'complete' | 'failed';
97
+ /** Progress percentage (0-100) */
98
+ percentage: number;
99
+ /** Human-readable message */
100
+ message: string;
101
+ /** Current operation details */
102
+ details?: {
103
+ currentStep?: number;
104
+ totalSteps?: number;
105
+ transactionId?: string;
106
+ confirmations?: number;
107
+ };
108
+ }
109
+
110
+ /**
111
+ * Options for lifecycle operations with progress tracking
112
+ */
113
+ export interface LifecycleOperationOptions {
114
+ /** Fee rate for Bitcoin operations (sat/vB) */
115
+ feeRate?: number;
116
+ /** Progress callback for operation updates */
117
+ onProgress?: ProgressCallback;
118
+ /** Enable atomic rollback on failure (default: true) */
119
+ atomicRollback?: boolean;
120
+ }
121
+
122
+ export class LifecycleManager {
123
+ private eventEmitter: EventEmitter;
124
+ private batchExecutor: BatchOperationExecutor;
125
+ private batchValidator: BatchValidator;
126
+ private logger: Logger;
127
+ private metrics: MetricsCollector;
128
+
129
+ constructor(
130
+ private config: OriginalsConfig,
131
+ private didManager: DIDManager,
132
+ private credentialManager: CredentialManager,
133
+ private deps?: { bitcoinManager?: BitcoinManager },
134
+ private keyStore?: KeyStore
135
+ ) {
136
+ this.eventEmitter = new EventEmitter();
137
+ this.batchExecutor = new BatchOperationExecutor();
138
+ this.batchValidator = new BatchValidator();
139
+ this.logger = new Logger('LifecycleManager', config);
140
+ this.metrics = new MetricsCollector();
141
+ }
142
+
143
+ /**
144
+ * Subscribe to a lifecycle event
145
+ * @param eventType - The type of event to subscribe to
146
+ * @param handler - The handler function to call when the event is emitted
147
+ * @returns A function to unsubscribe from the event
148
+ */
149
+ on<K extends keyof EventTypeMap>(eventType: K, handler: EventHandler<EventTypeMap[K]>): () => void {
150
+ return this.eventEmitter.on(eventType, handler);
151
+ }
152
+
153
+ /**
154
+ * Subscribe to a lifecycle event once
155
+ * @param eventType - The type of event to subscribe to
156
+ * @param handler - The handler function to call when the event is emitted (will only fire once)
157
+ * @returns A function to unsubscribe from the event
158
+ */
159
+ once<K extends keyof EventTypeMap>(eventType: K, handler: EventHandler<EventTypeMap[K]>): () => void {
160
+ return this.eventEmitter.once(eventType, handler);
161
+ }
162
+
163
+ /**
164
+ * Unsubscribe from a lifecycle event
165
+ * @param eventType - The type of event to unsubscribe from
166
+ * @param handler - The handler function to remove
167
+ */
168
+ off<K extends keyof EventTypeMap>(eventType: K, handler: EventHandler<EventTypeMap[K]>): void {
169
+ this.eventEmitter.off(eventType, handler);
170
+ }
171
+
172
+ async registerKey(verificationMethodId: string, privateKey: string): Promise<void> {
173
+ if (!this.keyStore) {
174
+ throw new Error('KeyStore not configured. Provide keyStore to LifecycleManager constructor.');
175
+ }
176
+
177
+ // Validate verification method ID format
178
+ if (!verificationMethodId || typeof verificationMethodId !== 'string') {
179
+ throw new Error('Invalid verificationMethodId: must be a non-empty string');
180
+ }
181
+
182
+ // Validate private key format (should be multibase encoded)
183
+ if (!privateKey || typeof privateKey !== 'string') {
184
+ throw new Error('Invalid privateKey: must be a non-empty string');
185
+ }
186
+
187
+ // Validate that it's a valid multibase-encoded private key
188
+ try {
189
+ multikey.decodePrivateKey(privateKey);
190
+ } catch (err) {
191
+ throw new Error('Invalid privateKey format: must be a valid multibase-encoded private key');
192
+ }
193
+
194
+ await this.keyStore.setPrivateKey(verificationMethodId, privateKey);
195
+ }
196
+
197
+ async createAsset(resources: AssetResource[]): Promise<OriginalsAsset> {
198
+ const stopTimer = this.logger.startTimer('createAsset');
199
+ this.logger.info('Creating asset', { resourceCount: resources.length });
200
+
201
+ try {
202
+ // Input validation
203
+ if (!Array.isArray(resources)) {
204
+ throw new Error('Resources must be an array');
205
+ }
206
+ if (resources.length === 0) {
207
+ throw new Error('At least one resource is required');
208
+ }
209
+
210
+ // Validate each resource
211
+ for (const resource of resources) {
212
+ if (!resource || typeof resource !== 'object') {
213
+ throw new Error('Invalid resource: must be an object');
214
+ }
215
+ if (!resource.id || typeof resource.id !== 'string') {
216
+ throw new Error('Invalid resource: missing or invalid id');
217
+ }
218
+ if (!resource.type || typeof resource.type !== 'string') {
219
+ throw new Error('Invalid resource: missing or invalid type');
220
+ }
221
+ if (!resource.contentType || typeof resource.contentType !== 'string') {
222
+ throw new Error('Invalid resource: missing or invalid contentType');
223
+ }
224
+ if (!resource.hash || typeof resource.hash !== 'string' || !/^[0-9a-fA-F]+$/.test(resource.hash)) {
225
+ throw new Error('Invalid resource: missing or invalid hash (must be hex string)');
226
+ }
227
+ // Validate contentType is a valid MIME type
228
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$/.test(resource.contentType)) {
229
+ throw new Error(`Invalid resource: invalid contentType MIME format: ${resource.contentType}`);
230
+ }
231
+ }
232
+
233
+ // Create a proper DID:peer document with verification methods
234
+ // If keyStore is provided, request the key pair to be returned
235
+ if (this.keyStore) {
236
+ const result = await this.didManager.createDIDPeer(resources, true);
237
+ const didDoc = result.didDocument;
238
+ const keyPair = result.keyPair;
239
+
240
+ // Register the private key in the keyStore
241
+ if (didDoc.verificationMethod && didDoc.verificationMethod.length > 0) {
242
+ let verificationMethodId = didDoc.verificationMethod[0].id;
243
+
244
+ // Ensure VM ID is absolute (not just a fragment like #key-0)
245
+ if (verificationMethodId.startsWith('#')) {
246
+ verificationMethodId = `${didDoc.id}${verificationMethodId}`;
247
+ }
248
+
249
+ await this.keyStore.setPrivateKey(verificationMethodId, keyPair.privateKey);
250
+ }
251
+
252
+ const asset = new OriginalsAsset(resources, didDoc, []);
253
+
254
+ // Defer asset:created event emission to next microtask so callers can subscribe first
255
+ queueMicrotask(() => {
256
+ const event = {
257
+ type: 'asset:created' as const,
258
+ timestamp: new Date().toISOString(),
259
+ asset: {
260
+ id: asset.id,
261
+ layer: asset.currentLayer,
262
+ resourceCount: resources.length,
263
+ createdAt: asset.getProvenance().createdAt
264
+ }
265
+ };
266
+
267
+ // Emit from both LifecycleManager and asset emitters
268
+ void this.eventEmitter.emit(event);
269
+ void (asset as unknown as { eventEmitter: EventEmitter }).eventEmitter.emit(event);
270
+ });
271
+
272
+ stopTimer();
273
+ this.logger.info('Asset created successfully', { assetId: asset.id });
274
+ this.metrics.recordAssetCreated();
275
+
276
+ return asset;
277
+ } else {
278
+ // No keyStore, just create the DID document
279
+ const didDoc = await this.didManager.createDIDPeer(resources);
280
+ const asset = new OriginalsAsset(resources, didDoc, []);
281
+
282
+ // Defer asset:created event emission to next microtask so callers can subscribe first
283
+ queueMicrotask(() => {
284
+ const event = {
285
+ type: 'asset:created' as const,
286
+ timestamp: new Date().toISOString(),
287
+ asset: {
288
+ id: asset.id,
289
+ layer: asset.currentLayer,
290
+ resourceCount: resources.length,
291
+ createdAt: asset.getProvenance().createdAt
292
+ }
293
+ };
294
+
295
+ // Emit from both LifecycleManager and asset emitters
296
+ void this.eventEmitter.emit(event);
297
+ void (asset as unknown as { eventEmitter: EventEmitter }).eventEmitter.emit(event);
298
+ });
299
+
300
+ stopTimer();
301
+ this.logger.info('Asset created successfully', { assetId: asset.id });
302
+ this.metrics.recordAssetCreated();
303
+
304
+ return asset;
305
+ }
306
+ } catch (error) {
307
+ stopTimer();
308
+ this.logger.error('Asset creation failed', error as Error, { resourceCount: resources.length });
309
+ this.metrics.recordError('ASSET_CREATION_FAILED', 'createAsset');
310
+ throw error;
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Create a typed Original with kind-specific validation
316
+ *
317
+ * This is the recommended way to create Originals with proper typing and validation.
318
+ * Each kind (App, Agent, Module, Dataset, Media, Document) has specific metadata
319
+ * requirements that are validated before creation.
320
+ *
321
+ * @param kind - The kind of Original to create
322
+ * @param manifest - The manifest containing name, version, resources, and kind-specific metadata
323
+ * @param options - Optional creation options (skipValidation, strictMode)
324
+ * @returns The created OriginalsAsset
325
+ * @throws Error if validation fails (unless skipValidation is true)
326
+ *
327
+ * @example
328
+ * ```typescript
329
+ * // Create a Module Original
330
+ * const moduleAsset = await sdk.lifecycle.createTypedOriginal(
331
+ * OriginalKind.Module,
332
+ * {
333
+ * kind: OriginalKind.Module,
334
+ * name: 'my-utility',
335
+ * version: '1.0.0',
336
+ * resources: [{ id: 'index.js', type: 'code', hash: '...', contentType: 'application/javascript' }],
337
+ * metadata: {
338
+ * format: 'esm',
339
+ * main: 'index.js',
340
+ * }
341
+ * }
342
+ * );
343
+ * ```
344
+ */
345
+ async createTypedOriginal<K extends OriginalKind>(
346
+ kind: K,
347
+ manifest: OriginalManifest<K>,
348
+ options?: CreateTypedOriginalOptions
349
+ ): Promise<OriginalsAsset> {
350
+ const stopTimer = this.logger.startTimer('createTypedOriginal');
351
+ this.logger.info('Creating typed Original', { kind, name: manifest.name, version: manifest.version });
352
+
353
+ try {
354
+ // Verify kind matches
355
+ if (manifest.kind !== kind) {
356
+ throw new Error(`Manifest kind "${manifest.kind}" does not match requested kind "${kind}"`);
357
+ }
358
+
359
+ // Validate manifest using KindRegistry
360
+ const registry = KindRegistry.getInstance();
361
+ registry.validateOrThrow(manifest, options);
362
+
363
+ // Log warnings if any
364
+ if (!options?.skipValidation) {
365
+ const validationResult = registry.validate(manifest, options);
366
+ if (validationResult.warnings.length > 0) {
367
+ for (const warning of validationResult.warnings) {
368
+ this.logger.warn(`[${warning.code}] ${warning.message}`, { path: warning.path });
369
+ }
370
+ }
371
+ }
372
+
373
+ // Create the asset using existing createAsset method
374
+ const asset = await this.createAsset(manifest.resources);
375
+
376
+ // Store the manifest metadata on the asset for future reference
377
+ // We attach it as a non-enumerable property to avoid serialization issues
378
+ Object.defineProperty(asset, '_manifest', {
379
+ value: manifest,
380
+ writable: false,
381
+ enumerable: false,
382
+ configurable: false,
383
+ });
384
+
385
+ // Emit typed:created event
386
+ queueMicrotask(() => {
387
+ const event = {
388
+ type: 'asset:created' as const,
389
+ timestamp: new Date().toISOString(),
390
+ asset: {
391
+ id: asset.id,
392
+ layer: asset.currentLayer,
393
+ resourceCount: manifest.resources.length,
394
+ createdAt: asset.getProvenance().createdAt,
395
+ kind: kind,
396
+ name: manifest.name,
397
+ version: manifest.version,
398
+ }
399
+ };
400
+
401
+ // Emit from LifecycleManager
402
+ void this.eventEmitter.emit(event);
403
+ });
404
+
405
+ stopTimer();
406
+ this.logger.info('Typed Original created successfully', {
407
+ assetId: asset.id,
408
+ kind,
409
+ name: manifest.name,
410
+ version: manifest.version,
411
+ });
412
+ this.metrics.recordAssetCreated();
413
+
414
+ return asset;
415
+ } catch (error) {
416
+ stopTimer();
417
+ this.logger.error('Typed Original creation failed', error as Error, {
418
+ kind,
419
+ name: manifest.name,
420
+ version: manifest.version,
421
+ });
422
+ this.metrics.recordError('TYPED_ASSET_CREATION_FAILED', 'createTypedOriginal');
423
+ throw error;
424
+ }
425
+ }
426
+
427
+ /**
428
+ * Get the manifest from a typed Original asset
429
+ * Returns undefined if the asset was not created with createTypedOriginal
430
+ *
431
+ * @param asset - The OriginalsAsset to get manifest from
432
+ * @returns The manifest or undefined
433
+ */
434
+ getManifest<K extends OriginalKind>(asset: OriginalsAsset): OriginalManifest<K> | undefined {
435
+ return (asset as { _manifest?: OriginalManifest<K> })._manifest;
436
+ }
437
+
438
+ /**
439
+ * Estimate the cost of creating a typed Original
440
+ * Useful for showing users estimated fees before creation
441
+ *
442
+ * @param manifest - The manifest to estimate
443
+ * @param targetLayer - The target layer (did:webvh or did:btco)
444
+ * @param feeRate - Optional fee rate override (sat/vB)
445
+ * @returns Cost estimate including fees
446
+ */
447
+ async estimateTypedOriginalCost<K extends OriginalKind>(
448
+ manifest: OriginalManifest<K>,
449
+ targetLayer: LayerType,
450
+ feeRate?: number
451
+ ): Promise<CostEstimate> {
452
+ // For webvh, costs are minimal
453
+ if (targetLayer === 'did:webvh') {
454
+ return {
455
+ totalSats: 0,
456
+ breakdown: {
457
+ networkFee: 0,
458
+ dataCost: 0,
459
+ dustValue: 0
460
+ },
461
+ feeRate: 0,
462
+ dataSize: 0,
463
+ targetLayer,
464
+ confidence: 'high'
465
+ };
466
+ }
467
+
468
+ // Calculate total data size including manifest metadata
469
+ let dataSize = 0;
470
+ for (const resource of manifest.resources) {
471
+ if (resource.size) {
472
+ dataSize += resource.size;
473
+ } else if (resource.content) {
474
+ dataSize += Buffer.from(resource.content).length;
475
+ } else {
476
+ // Estimate based on hash length (assume average resource size)
477
+ dataSize += 1000;
478
+ }
479
+ }
480
+
481
+ // Add inscription manifest overhead
482
+ const inscriptionManifest = {
483
+ assetId: `did:peer:placeholder`,
484
+ kind: manifest.kind,
485
+ name: manifest.name,
486
+ version: manifest.version,
487
+ resources: manifest.resources.map(r => ({
488
+ id: r.id,
489
+ hash: r.hash,
490
+ contentType: r.contentType,
491
+ })),
492
+ metadata: manifest.metadata,
493
+ timestamp: new Date().toISOString()
494
+ };
495
+ dataSize += Buffer.from(JSON.stringify(inscriptionManifest)).length;
496
+
497
+ // Get fee rate from oracle or use provided/default
498
+ let effectiveFeeRate = feeRate;
499
+ let confidence: 'low' | 'medium' | 'high' = 'medium';
500
+
501
+ if (!effectiveFeeRate) {
502
+ if (this.config.feeOracle) {
503
+ try {
504
+ effectiveFeeRate = await this.config.feeOracle.estimateFeeRate(1);
505
+ confidence = 'high';
506
+ } catch {
507
+ // Fallback to default
508
+ }
509
+ }
510
+
511
+ if (!effectiveFeeRate && this.config.ordinalsProvider) {
512
+ try {
513
+ effectiveFeeRate = await this.config.ordinalsProvider.estimateFee(1);
514
+ confidence = 'medium';
515
+ } catch {
516
+ // Fallback to default
517
+ }
518
+ }
519
+
520
+ if (!effectiveFeeRate) {
521
+ effectiveFeeRate = 10;
522
+ confidence = 'low';
523
+ }
524
+ }
525
+
526
+ // Transaction overhead (commit + reveal structure)
527
+ const txOverhead = 200 + 122; // Base tx + inscription overhead
528
+ const totalVbytes = txOverhead + Math.ceil(dataSize / 4); // Witness data is ~1/4 weight
529
+
530
+ const networkFee = totalVbytes * effectiveFeeRate;
531
+ const dustValue = 330; // Minimum output value
532
+
533
+ return {
534
+ totalSats: networkFee + dustValue,
535
+ breakdown: {
536
+ networkFee,
537
+ dataCost: Math.ceil(dataSize / 4) * effectiveFeeRate,
538
+ dustValue
539
+ },
540
+ feeRate: effectiveFeeRate,
541
+ dataSize,
542
+ targetLayer,
543
+ confidence
544
+ };
545
+ }
546
+
547
+ async publishToWeb(
548
+ asset: OriginalsAsset,
549
+ publisherDidOrSigner: string | ExternalSigner
550
+ ): Promise<OriginalsAsset> {
551
+ const stopTimer = this.logger.startTimer('publishToWeb');
552
+
553
+ try {
554
+ if (asset.currentLayer !== 'did:peer') {
555
+ throw new Error('Asset must be in did:peer layer to publish to web');
556
+ }
557
+
558
+ const { publisherDid, signer } = this.extractPublisherInfo(publisherDidOrSigner);
559
+ const { domain, userPath } = this.parseWebVHDid(publisherDid);
560
+
561
+ this.logger.info('Publishing asset to web', { assetId: asset.id, publisherDid });
562
+
563
+ // Publish resources to storage
564
+ await this.publishResources(asset, publisherDid, domain, userPath);
565
+
566
+ // Store the original did:peer ID before migration
567
+ const originalPeerDid = asset.id;
568
+
569
+ // Migrate asset to did:webvh layer
570
+ await asset.migrate('did:webvh');
571
+ asset.bindings = { ...(asset.bindings || {}), 'did:peer': originalPeerDid, 'did:webvh': publisherDid };
572
+
573
+ // Issue publication credential (best-effort)
574
+ await this.issuePublicationCredential(asset, publisherDid, signer);
575
+
576
+ stopTimer();
577
+ this.logger.info('Asset published to web successfully', {
578
+ assetId: asset.id,
579
+ publisherDid,
580
+ resourceCount: asset.resources.length
581
+ });
582
+ this.metrics.recordMigration('did:peer', 'did:webvh');
583
+
584
+ return asset;
585
+ } catch (error) {
586
+ stopTimer();
587
+ this.logger.error('Publish to web failed', error as Error, { assetId: asset.id });
588
+ this.metrics.recordError('PUBLISH_FAILED', 'publishToWeb');
589
+ throw error;
590
+ }
591
+ }
592
+
593
+ private extractPublisherInfo(publisherDidOrSigner: string | ExternalSigner): {
594
+ publisherDid: string;
595
+ signer?: ExternalSigner;
596
+ } {
597
+ if (typeof publisherDidOrSigner === 'string') {
598
+ // If it's already a did:webvh DID, use it as-is
599
+ if (publisherDidOrSigner.startsWith('did:webvh:')) {
600
+ return { publisherDid: publisherDidOrSigner };
601
+ }
602
+
603
+ // Otherwise, treat it as a domain and construct a did:webvh DID
604
+ // Format: did:webvh:domain:user (use 'user' as default user path)
605
+ // Encode the domain to handle ports (e.g., localhost:5000 -> localhost%3A5000)
606
+ const domain = publisherDidOrSigner;
607
+ const encodedDomain = encodeURIComponent(domain);
608
+ const publisherDid = `did:webvh:${encodedDomain}:user`;
609
+ return { publisherDid };
610
+ }
611
+
612
+ const signer = publisherDidOrSigner;
613
+ const resolvedVmId = signer.getVerificationMethodId();
614
+ const publisherDid = resolvedVmId.includes('#') ? resolvedVmId.split('#')[0] : resolvedVmId;
615
+
616
+ if (!publisherDid.startsWith('did:webvh:')) {
617
+ throw new Error('Signer must be associated with a did:webvh identifier');
618
+ }
619
+
620
+ return { publisherDid, signer };
621
+ }
622
+
623
+ private parseWebVHDid(did: string): { domain: string; userPath: string } {
624
+ const parts = did.split(':');
625
+ if (parts.length < 4) {
626
+ throw new Error('Invalid did:webvh format: must include domain and user path');
627
+ }
628
+
629
+ const domain = decodeURIComponent(parts[2]);
630
+ const userPath = parts.slice(3).join('/');
631
+
632
+ return { domain, userPath };
633
+ }
634
+
635
+ private async publishResources(
636
+ asset: OriginalsAsset,
637
+ publisherDid: string,
638
+ domain: string,
639
+ userPath: string
640
+ ): Promise<void> {
641
+ const storage = (this.config as { storageAdapter?: unknown }).storageAdapter || new MemoryStorageAdapter();
642
+
643
+ for (const resource of asset.resources) {
644
+ const hashBytes = hexToBytes(resource.hash);
645
+ const multibase = encodeBase64UrlMultibase(hashBytes);
646
+ const resourceUrl = `${publisherDid}/resources/${multibase}`;
647
+ const relativePath = `${userPath}/resources/${multibase}`;
648
+
649
+ // Store resource content
650
+ const data = resource.content
651
+ ? Buffer.from(resource.content)
652
+ : Buffer.from(resource.hash);
653
+
654
+ const storageWithPut = storage as { put?: (key: string, data: Buffer, options: { contentType: string }) => Promise<void> };
655
+ const storageWithPutObject = storage as { putObject?: (domain: string, path: string, data: Uint8Array) => Promise<void> };
656
+
657
+ if (typeof storageWithPut.put === 'function') {
658
+ await storageWithPut.put(`${domain}/${relativePath}`, data, { contentType: resource.contentType });
659
+ } else if (typeof storageWithPutObject.putObject === 'function') {
660
+ const encoded = new TextEncoder().encode(resource.content || resource.hash);
661
+ await storageWithPutObject.putObject(domain, relativePath, encoded);
662
+ }
663
+
664
+ (resource as { url?: string }).url = resourceUrl;
665
+
666
+ await this.emitResourcePublishedEvent(asset, resource, resourceUrl, publisherDid, domain);
667
+ }
668
+ }
669
+
670
+ private async emitResourcePublishedEvent(
671
+ asset: OriginalsAsset,
672
+ resource: AssetResource,
673
+ resourceUrl: string,
674
+ publisherDid: string,
675
+ domain: string
676
+ ): Promise<void> {
677
+ const event = {
678
+ type: 'resource:published' as const,
679
+ timestamp: new Date().toISOString(),
680
+ asset: { id: asset.id },
681
+ resource: {
682
+ id: resource.id,
683
+ url: resourceUrl,
684
+ contentType: resource.contentType,
685
+ hash: resource.hash
686
+ },
687
+ publisherDid,
688
+ domain
689
+ };
690
+
691
+ try {
692
+ // Emit from both LifecycleManager and asset emitters
693
+ await this.eventEmitter.emit(event);
694
+ await (asset as unknown as { eventEmitter: EventEmitter }).eventEmitter.emit(event);
695
+ } catch (err) {
696
+ this.logger.error('Event handler error', err as Error, { event: event.type });
697
+ }
698
+ }
699
+
700
+ private async issuePublicationCredential(
701
+ asset: OriginalsAsset,
702
+ publisherDid: string,
703
+ signer?: ExternalSigner
704
+ ): Promise<void> {
705
+ try {
706
+ const subject = {
707
+ id: asset.id,
708
+ publishedAs: publisherDid,
709
+ resourceId: asset.resources[0]?.id,
710
+ fromLayer: 'did:peer' as const,
711
+ toLayer: 'did:webvh' as const,
712
+ migratedAt: new Date().toISOString()
713
+ };
714
+
715
+ const unsigned = this.credentialManager.createResourceCredential(
716
+ 'ResourceMigrated',
717
+ subject,
718
+ publisherDid
719
+ );
720
+
721
+ const signed = signer
722
+ ? await this.credentialManager.signCredentialWithExternalSigner(unsigned, signer)
723
+ : await this.signWithKeyStore(unsigned, publisherDid);
724
+
725
+ asset.credentials.push(signed);
726
+
727
+ const event = {
728
+ type: 'credential:issued' as const,
729
+ timestamp: new Date().toISOString(),
730
+ asset: { id: asset.id },
731
+ credential: {
732
+ type: signed.type,
733
+ issuer: typeof signed.issuer === 'string' ? signed.issuer : signed.issuer.id
734
+ }
735
+ };
736
+
737
+ // Emit from both LifecycleManager and asset emitters
738
+ await this.eventEmitter.emit(event);
739
+ await (asset as unknown as { eventEmitter: EventEmitter }).eventEmitter.emit(event);
740
+ } catch (err) {
741
+ this.logger.error('Failed to issue credential during publish', err as Error);
742
+ }
743
+ }
744
+
745
+ private async signWithKeyStore(
746
+ credential: VerifiableCredential,
747
+ issuer: string
748
+ ): Promise<VerifiableCredential> {
749
+ if (!this.keyStore) {
750
+ throw new Error('KeyStore required for signing. Provide keyStore or external signer.');
751
+ }
752
+
753
+ // Try to find a key in the keyStore for this DID
754
+ // First try common verification method patterns: #key-0, #keys-1, etc.
755
+ const commonVmIds = [
756
+ `${issuer}#key-0`,
757
+ `${issuer}#keys-1`,
758
+ `${issuer}#authentication`,
759
+ ];
760
+
761
+ let privateKey: string | null = null;
762
+ let vmId: string | null = null;
763
+
764
+ for (const testVmId of commonVmIds) {
765
+ const key = await this.keyStore.getPrivateKey(testVmId);
766
+ if (key) {
767
+ privateKey = key;
768
+ vmId = testVmId;
769
+ break;
770
+ }
771
+ }
772
+
773
+ // If not found, try to find ANY key that starts with the issuer DID
774
+ const keyStoreWithGetAll = this.keyStore as { getAllVerificationMethodIds?: () => string[] };
775
+ if (!privateKey && typeof keyStoreWithGetAll.getAllVerificationMethodIds === 'function') {
776
+ const allVmIds = keyStoreWithGetAll.getAllVerificationMethodIds();
777
+ for (const testVmId of allVmIds) {
778
+ if (testVmId.startsWith(issuer)) {
779
+ const key = await this.keyStore.getPrivateKey(testVmId);
780
+ if (key) {
781
+ privateKey = key;
782
+ vmId = testVmId;
783
+ break;
784
+ }
785
+ }
786
+ }
787
+ }
788
+
789
+ // If no key found in common patterns, try resolving the DID
790
+ if (!privateKey) {
791
+ const didDoc = await this.didManager.resolveDID(issuer);
792
+ if (!didDoc?.verificationMethod?.[0]) {
793
+ throw new Error('No verification method found in publisher DID document');
794
+ }
795
+
796
+ vmId = didDoc.verificationMethod[0].id;
797
+ if (vmId.startsWith('#')) {
798
+ vmId = `${issuer}${vmId}`;
799
+ }
800
+
801
+ privateKey = await this.keyStore.getPrivateKey(vmId);
802
+ if (!privateKey) {
803
+ throw new Error('Private key not found in keyStore');
804
+ }
805
+ }
806
+
807
+ if (!vmId) {
808
+ throw new Error('Verification method ID could not be determined');
809
+ }
810
+
811
+ return this.credentialManager.signCredential(credential, privateKey, vmId);
812
+ }
813
+
814
+ async inscribeOnBitcoin(
815
+ asset: OriginalsAsset,
816
+ feeRate?: number
817
+ ): Promise<OriginalsAsset> {
818
+ const stopTimer = this.logger.startTimer('inscribeOnBitcoin');
819
+ this.logger.info('Inscribing asset on Bitcoin', { assetId: asset.id, feeRate });
820
+
821
+ try {
822
+ // Input validation
823
+ if (!asset || typeof asset !== 'object') {
824
+ throw new Error('Invalid asset: must be a valid OriginalsAsset');
825
+ }
826
+ if (feeRate !== undefined) {
827
+ if (typeof feeRate !== 'number' || feeRate <= 0 || !Number.isFinite(feeRate)) {
828
+ throw new Error('Invalid feeRate: must be a positive number');
829
+ }
830
+ if (feeRate < 1 || feeRate > 1000000) {
831
+ throw new Error('Invalid feeRate: must be between 1 and 1000000 sat/vB');
832
+ }
833
+ }
834
+
835
+ if (typeof asset.migrate !== 'function') {
836
+ throw new Error('Not implemented');
837
+ }
838
+ if (asset.currentLayer !== 'did:webvh' && asset.currentLayer !== 'did:peer') {
839
+ throw new Error('Not implemented');
840
+ }
841
+ const bitcoinManager = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
842
+ const manifest = {
843
+ assetId: asset.id,
844
+ resources: asset.resources.map(res => ({ id: res.id, hash: res.hash, contentType: res.contentType, url: res.url })),
845
+ timestamp: new Date().toISOString()
846
+ };
847
+ const payload = Buffer.from(JSON.stringify(manifest));
848
+ const inscription = await bitcoinManager.inscribeData(payload, 'application/json', feeRate) as {
849
+ revealTxId?: string;
850
+ txid: string;
851
+ commitTxId?: string;
852
+ inscriptionId: string;
853
+ satoshi?: string;
854
+ feeRate?: number;
855
+ };
856
+ const revealTxId = inscription.revealTxId ?? inscription.txid;
857
+ const commitTxId = inscription.commitTxId;
858
+ const usedFeeRate = typeof inscription.feeRate === 'number' ? inscription.feeRate : feeRate;
859
+
860
+ // Capture the layer before migration for accurate metrics
861
+ const fromLayer = asset.currentLayer;
862
+
863
+ await asset.migrate('did:btco', {
864
+ transactionId: revealTxId,
865
+ inscriptionId: inscription.inscriptionId,
866
+ satoshi: inscription.satoshi,
867
+ commitTxId,
868
+ revealTxId,
869
+ feeRate: usedFeeRate
870
+ });
871
+
872
+ const bindingValue = inscription.satoshi
873
+ ? `did:btco:${inscription.satoshi}`
874
+ : `did:btco:${inscription.inscriptionId}`;
875
+ asset.bindings = Object.assign({}, asset.bindings || {}, { 'did:btco': bindingValue });
876
+
877
+ stopTimer();
878
+ this.logger.info('Asset inscribed on Bitcoin successfully', {
879
+ assetId: asset.id,
880
+ inscriptionId: inscription.inscriptionId,
881
+ transactionId: revealTxId
882
+ });
883
+ this.metrics.recordMigration(fromLayer, 'did:btco');
884
+
885
+ return asset;
886
+ } catch (error) {
887
+ stopTimer();
888
+ this.logger.error('Bitcoin inscription failed', error as Error, { assetId: asset.id, feeRate });
889
+ this.metrics.recordError('INSCRIPTION_FAILED', 'inscribeOnBitcoin');
890
+ throw error;
891
+ }
892
+ }
893
+
894
+ async transferOwnership(
895
+ asset: OriginalsAsset,
896
+ newOwner: string
897
+ ): Promise<BitcoinTransaction> {
898
+ const stopTimer = this.logger.startTimer('transferOwnership');
899
+ this.logger.info('Transferring asset ownership', { assetId: asset.id, newOwner });
900
+
901
+ try {
902
+ // Input validation
903
+ if (!asset || typeof asset !== 'object') {
904
+ throw new Error('Invalid asset: must be a valid OriginalsAsset');
905
+ }
906
+ if (!newOwner || typeof newOwner !== 'string') {
907
+ throw new Error('Invalid newOwner: must be a non-empty string');
908
+ }
909
+
910
+ // Validate Bitcoin address format and checksum
911
+ try {
912
+ validateBitcoinAddress(newOwner, this.config.network);
913
+ } catch (error) {
914
+ const message = error instanceof Error ? error.message : 'Invalid Bitcoin address';
915
+ throw new Error(`Invalid Bitcoin address for ownership transfer: ${message}`);
916
+ }
917
+
918
+ // Transfer Bitcoin-anchored asset ownership
919
+ // Only works for assets in did:btco layer
920
+ if (asset.currentLayer !== 'did:btco') {
921
+ throw new Error('Asset must be inscribed on Bitcoin before transfer');
922
+ }
923
+ const bm = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
924
+ const provenance = asset.getProvenance();
925
+ const latestMigration = provenance.migrations[provenance.migrations.length - 1];
926
+ const satoshi = latestMigration?.satoshi ?? (asset.id.startsWith('did:btco:') ? asset.id.split(':')[2] : '');
927
+ const inscription = {
928
+ satoshi,
929
+ inscriptionId: latestMigration?.inscriptionId ?? `insc-${satoshi || 'unknown'}`,
930
+ content: Buffer.alloc(0),
931
+ contentType: 'application/octet-stream',
932
+ txid: latestMigration?.transactionId ?? 'unknown-tx',
933
+ vout: 0
934
+ } as const;
935
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
936
+ const tx = await bm.transferInscription(inscription as never, newOwner);
937
+ await asset.recordTransfer(asset.id, newOwner, tx.txid);
938
+
939
+ stopTimer();
940
+ this.logger.info('Asset ownership transferred successfully', {
941
+ assetId: asset.id,
942
+ newOwner,
943
+ transactionId: tx.txid
944
+ });
945
+ this.metrics.recordTransfer();
946
+
947
+ return tx;
948
+ } catch (error) {
949
+ stopTimer();
950
+ this.logger.error('Ownership transfer failed', error as Error, { assetId: asset.id, newOwner });
951
+ this.metrics.recordError('TRANSFER_FAILED', 'transferOwnership');
952
+ throw error;
953
+ }
954
+ }
955
+
956
+ /**
957
+ * Create multiple assets in batch
958
+ *
959
+ * @param resourcesList - Array of resource arrays, one per asset to create
960
+ * @param options - Batch operation options
961
+ * @returns BatchResult with created assets
962
+ */
963
+ async batchCreateAssets(
964
+ resourcesList: AssetResource[][],
965
+ options?: BatchOperationOptions
966
+ ): Promise<BatchResult<OriginalsAsset>> {
967
+ const batchId = this.batchExecutor.generateBatchId();
968
+
969
+ // Validate first if requested
970
+ if (options?.validateFirst !== false) {
971
+ const validationResults = this.batchValidator.validateBatchCreate(resourcesList);
972
+ const invalid = validationResults.filter(r => !r.isValid);
973
+ if (invalid.length > 0) {
974
+ const errors = invalid.flatMap(r => r.errors).join('; ');
975
+ throw new Error(`Batch validation failed: ${errors}`);
976
+ }
977
+ }
978
+
979
+ // Emit batch:started event
980
+ await this.eventEmitter.emit({
981
+ type: 'batch:started',
982
+ timestamp: new Date().toISOString(),
983
+ operation: 'create',
984
+ batchId,
985
+ itemCount: resourcesList.length
986
+ });
987
+
988
+ try {
989
+ // Use batch executor to process all asset creations
990
+ const result = await this.batchExecutor.execute(
991
+ resourcesList,
992
+ async (resources, _index) => {
993
+ const asset = await this.createAsset(resources);
994
+ return asset;
995
+ },
996
+ options,
997
+ batchId // Pass the pre-generated batchId for event correlation
998
+ );
999
+
1000
+ // Emit batch:completed event
1001
+ await this.eventEmitter.emit({
1002
+ type: 'batch:completed',
1003
+ timestamp: new Date().toISOString(),
1004
+ batchId,
1005
+ operation: 'create',
1006
+ results: {
1007
+ successful: result.successful.length,
1008
+ failed: result.failed.length,
1009
+ totalDuration: result.totalDuration
1010
+ }
1011
+ });
1012
+
1013
+ return result;
1014
+ } catch (error) {
1015
+ // Emit batch:failed event
1016
+ await this.eventEmitter.emit({
1017
+ type: 'batch:failed',
1018
+ timestamp: new Date().toISOString(),
1019
+ batchId,
1020
+ operation: 'create',
1021
+ error: error instanceof Error ? error.message : String(error)
1022
+ });
1023
+
1024
+ throw error;
1025
+ }
1026
+ }
1027
+
1028
+ /**
1029
+ * Publish multiple assets to web storage in batch
1030
+ *
1031
+ * @param assets - Array of assets to publish
1032
+ * @param domain - Domain to publish to
1033
+ * @param options - Batch operation options
1034
+ * @returns BatchResult with published assets
1035
+ */
1036
+ async batchPublishToWeb(
1037
+ assets: OriginalsAsset[],
1038
+ domain: string,
1039
+ options?: BatchOperationOptions
1040
+ ): Promise<BatchResult<OriginalsAsset>> {
1041
+ const batchId = this.batchExecutor.generateBatchId();
1042
+
1043
+ // Validate domain once
1044
+ if (!domain || typeof domain !== 'string') {
1045
+ throw new Error('Invalid domain: must be a non-empty string');
1046
+ }
1047
+
1048
+ const normalized = domain.trim().toLowerCase();
1049
+
1050
+ // Split domain and port if present
1051
+ const [domainPart, portPart] = normalized.split(':');
1052
+
1053
+ // Validate port if present
1054
+ if (portPart && (!/^\d+$/.test(portPart) || parseInt(portPart) < 1 || parseInt(portPart) > 65535)) {
1055
+ throw new Error(`Invalid domain format: ${domain} - invalid port`);
1056
+ }
1057
+
1058
+ // Allow localhost and IP addresses for development
1059
+ const isLocalhost = domainPart === 'localhost';
1060
+ const isIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(domainPart);
1061
+
1062
+ if (!isLocalhost && !isIP) {
1063
+ // For non-localhost domains, require proper domain format
1064
+ const label = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
1065
+ const domainRegex = new RegExp(`^(?=.{1,253}$)(?:${label})(?:\\.(?:${label}))+?$`, 'i');
1066
+ if (!domainRegex.test(domainPart)) {
1067
+ throw new Error(`Invalid domain format: ${domain}`);
1068
+ }
1069
+ }
1070
+
1071
+ // Emit batch:started event
1072
+ await this.eventEmitter.emit({
1073
+ type: 'batch:started',
1074
+ timestamp: new Date().toISOString(),
1075
+ operation: 'publish',
1076
+ batchId,
1077
+ itemCount: assets.length
1078
+ });
1079
+
1080
+ try {
1081
+ const result = await this.batchExecutor.execute(
1082
+ assets,
1083
+ async (asset, _index) => {
1084
+ return await this.publishToWeb(asset, domain);
1085
+ },
1086
+ options,
1087
+ batchId // Pass the pre-generated batchId for event correlation
1088
+ );
1089
+
1090
+ // Emit batch:completed event
1091
+ await this.eventEmitter.emit({
1092
+ type: 'batch:completed',
1093
+ timestamp: new Date().toISOString(),
1094
+ batchId,
1095
+ operation: 'publish',
1096
+ results: {
1097
+ successful: result.successful.length,
1098
+ failed: result.failed.length,
1099
+ totalDuration: result.totalDuration
1100
+ }
1101
+ });
1102
+
1103
+ return result;
1104
+ } catch (error) {
1105
+ // Emit batch:failed event
1106
+ await this.eventEmitter.emit({
1107
+ type: 'batch:failed',
1108
+ timestamp: new Date().toISOString(),
1109
+ batchId,
1110
+ operation: 'publish',
1111
+ error: error instanceof Error ? error.message : String(error)
1112
+ });
1113
+
1114
+ throw error;
1115
+ }
1116
+ }
1117
+
1118
+ /**
1119
+ * Inscribe multiple assets on Bitcoin with cost optimization
1120
+ * KEY FEATURE: singleTransaction option for 30%+ cost savings
1121
+ *
1122
+ * @param assets - Array of assets to inscribe
1123
+ * @param options - Batch inscription options
1124
+ * @returns BatchResult with inscribed assets
1125
+ */
1126
+ async batchInscribeOnBitcoin(
1127
+ assets: OriginalsAsset[],
1128
+ options?: BatchInscriptionOptions
1129
+ ): Promise<BatchResult<OriginalsAsset>> {
1130
+ // Validate first if requested
1131
+ if (options?.validateFirst !== false) {
1132
+ const validationResults = this.batchValidator.validateBatchInscription(assets);
1133
+ const invalid = validationResults.filter(r => !r.isValid);
1134
+ if (invalid.length > 0) {
1135
+ const errors = invalid.flatMap(r => r.errors).join('; ');
1136
+ throw new Error(`Batch validation failed: ${errors}`);
1137
+ }
1138
+ }
1139
+
1140
+ if (options?.singleTransaction) {
1141
+ return this.batchInscribeSingleTransaction(assets, options);
1142
+ } else {
1143
+ return this.batchInscribeIndividualTransactions(assets, options);
1144
+ }
1145
+ }
1146
+
1147
+ /**
1148
+ * CORE INNOVATION: Single-transaction batch inscription
1149
+ * Combines multiple assets into one Bitcoin transaction for 30%+ cost savings
1150
+ *
1151
+ * @param assets - Array of assets to inscribe
1152
+ * @param options - Batch inscription options
1153
+ * @returns BatchResult with inscribed assets and cost savings data
1154
+ */
1155
+ private async batchInscribeSingleTransaction(
1156
+ assets: OriginalsAsset[],
1157
+ options?: BatchInscriptionOptions
1158
+ ): Promise<BatchResult<OriginalsAsset>> {
1159
+ const batchId = this.batchExecutor.generateBatchId();
1160
+ const startTime = Date.now();
1161
+ const startedAt = new Date().toISOString();
1162
+
1163
+ // Emit batch:started event
1164
+ await this.eventEmitter.emit({
1165
+ type: 'batch:started',
1166
+ timestamp: startedAt,
1167
+ operation: 'inscribe',
1168
+ batchId,
1169
+ itemCount: assets.length
1170
+ });
1171
+
1172
+ try {
1173
+ // Calculate total data size for all assets
1174
+ const totalDataSize = this.calculateTotalDataSize(assets);
1175
+
1176
+ // Estimate savings from batch inscription
1177
+ const estimatedSavings = this.estimateBatchSavings(assets, options?.feeRate);
1178
+
1179
+ // Create manifests for all assets
1180
+ const manifests = assets.map(asset => ({
1181
+ assetId: asset.id,
1182
+ resources: asset.resources.map(res => ({
1183
+ id: res.id,
1184
+ hash: res.hash,
1185
+ contentType: res.contentType,
1186
+ url: res.url
1187
+ })),
1188
+ timestamp: new Date().toISOString()
1189
+ }));
1190
+
1191
+ // Combine all manifests into a single batch payload
1192
+ const batchManifest = {
1193
+ batchId,
1194
+ assets: manifests,
1195
+ timestamp: new Date().toISOString()
1196
+ };
1197
+
1198
+ const payload = Buffer.from(JSON.stringify(batchManifest));
1199
+
1200
+ // Inscribe the batch manifest as a single transaction
1201
+ const bitcoinManager = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
1202
+ const inscription = await bitcoinManager.inscribeData(
1203
+ payload,
1204
+ 'application/json',
1205
+ options?.feeRate
1206
+ ) as {
1207
+ revealTxId?: string;
1208
+ txid: string;
1209
+ commitTxId?: string;
1210
+ inscriptionId: string;
1211
+ satoshi?: string;
1212
+ feeRate?: number;
1213
+ };
1214
+
1215
+ const revealTxId = inscription.revealTxId ?? inscription.txid;
1216
+ const commitTxId = inscription.commitTxId;
1217
+ const usedFeeRate = typeof inscription.feeRate === 'number' ? inscription.feeRate : options?.feeRate;
1218
+
1219
+ // Calculate fee per asset (split proportionally by data size)
1220
+ // Include both metadata and resource content size for accurate fee distribution
1221
+ const assetSizes = assets.map(asset => {
1222
+ // Calculate metadata size
1223
+ const metadataSize = JSON.stringify({
1224
+ assetId: asset.id,
1225
+ resources: asset.resources.map(r => ({
1226
+ id: r.id,
1227
+ hash: r.hash,
1228
+ contentType: r.contentType,
1229
+ url: r.url
1230
+ }))
1231
+ }).length;
1232
+
1233
+ // Add resource content sizes
1234
+ const contentSize = asset.resources.reduce((sum, r) => {
1235
+ const content = (r as { content?: string | Buffer }).content;
1236
+ if (content) {
1237
+ const length = typeof content === 'string' ? Buffer.byteLength(content) : content.length;
1238
+ return sum + (length || 0);
1239
+ }
1240
+ return sum;
1241
+ }, 0);
1242
+
1243
+ return metadataSize + contentSize;
1244
+ });
1245
+ const totalSize = assetSizes.reduce((sum, size) => sum + size, 0);
1246
+
1247
+ // Calculate total fee from batch transaction size and fee rate
1248
+ // Estimate transaction size: base overhead (200 bytes) + batch payload size
1249
+ const batchTxSize = 200 + totalDataSize;
1250
+ const effectiveFeeRate = usedFeeRate ?? 10;
1251
+ const totalFee = batchTxSize * effectiveFeeRate;
1252
+
1253
+ // Split fees proportionally by asset data size
1254
+ const feePerAsset = assetSizes.map(size =>
1255
+ Math.floor(totalFee * (size / totalSize))
1256
+ );
1257
+
1258
+ // Update all assets with batch inscription data
1259
+ const individualInscriptionIds: string[] = [];
1260
+ const successful: BatchResult<OriginalsAsset>['successful'] = [];
1261
+
1262
+ for (let i = 0; i < assets.length; i++) {
1263
+ const asset = assets[i];
1264
+ // For batch inscriptions, use the base inscription ID for all assets
1265
+ // The batch index is stored as metadata, not in the ID
1266
+ const individualInscriptionId = inscription.inscriptionId;
1267
+ individualInscriptionIds.push(individualInscriptionId);
1268
+
1269
+ await asset.migrate('did:btco', {
1270
+ transactionId: revealTxId,
1271
+ inscriptionId: individualInscriptionId,
1272
+ satoshi: inscription.satoshi,
1273
+ commitTxId,
1274
+ revealTxId,
1275
+ feeRate: usedFeeRate
1276
+ });
1277
+
1278
+ // Add batch metadata to provenance
1279
+ const provenance = asset.getProvenance();
1280
+ const latestMigration = provenance.migrations[provenance.migrations.length - 1];
1281
+ const migrationWithBatchData = latestMigration as typeof latestMigration & {
1282
+ batchId?: string;
1283
+ batchInscription?: boolean;
1284
+ batchIndex?: number;
1285
+ feePaid?: number;
1286
+ };
1287
+ migrationWithBatchData.batchId = batchId;
1288
+ migrationWithBatchData.batchInscription = true;
1289
+ migrationWithBatchData.batchIndex = i; // Store index as metadata
1290
+ migrationWithBatchData.feePaid = feePerAsset[i];
1291
+
1292
+ const bindingValue = inscription.satoshi
1293
+ ? `did:btco:${inscription.satoshi}`
1294
+ : `did:btco:${individualInscriptionId}`;
1295
+ asset.bindings = Object.assign({}, asset.bindings || {}, { 'did:btco': bindingValue });
1296
+
1297
+ successful.push({
1298
+ index: i,
1299
+ result: asset,
1300
+ duration: Date.now() - startTime
1301
+ });
1302
+ }
1303
+
1304
+ const totalDuration = Date.now() - startTime;
1305
+ const completedAt = new Date().toISOString();
1306
+
1307
+ // Emit batch:completed event with cost savings
1308
+ await this.eventEmitter.emit({
1309
+ type: 'batch:completed',
1310
+ timestamp: completedAt,
1311
+ batchId,
1312
+ operation: 'inscribe',
1313
+ results: {
1314
+ successful: successful.length,
1315
+ failed: 0,
1316
+ totalDuration,
1317
+ costSavings: {
1318
+ amount: estimatedSavings.savings,
1319
+ percentage: estimatedSavings.savingsPercentage
1320
+ }
1321
+ }
1322
+ });
1323
+
1324
+ return {
1325
+ successful,
1326
+ failed: [],
1327
+ totalProcessed: assets.length,
1328
+ totalDuration,
1329
+ batchId,
1330
+ startedAt,
1331
+ completedAt
1332
+ };
1333
+ } catch (error) {
1334
+ // Emit batch:failed event
1335
+ await this.eventEmitter.emit({
1336
+ type: 'batch:failed',
1337
+ timestamp: new Date().toISOString(),
1338
+ batchId,
1339
+ operation: 'inscribe',
1340
+ error: error instanceof Error ? error.message : String(error)
1341
+ });
1342
+
1343
+ throw new BatchError(
1344
+ batchId,
1345
+ 'inscribe',
1346
+ { successful: 0, failed: assets.length },
1347
+ error instanceof Error ? error.message : String(error)
1348
+ );
1349
+ }
1350
+ }
1351
+
1352
+ /**
1353
+ * Individual transaction batch inscription (fallback mode)
1354
+ * Each asset is inscribed in its own transaction
1355
+ *
1356
+ * @param assets - Array of assets to inscribe
1357
+ * @param options - Batch inscription options
1358
+ * @returns BatchResult with inscribed assets
1359
+ */
1360
+ private async batchInscribeIndividualTransactions(
1361
+ assets: OriginalsAsset[],
1362
+ options?: BatchInscriptionOptions
1363
+ ): Promise<BatchResult<OriginalsAsset>> {
1364
+ const batchId = this.batchExecutor.generateBatchId();
1365
+
1366
+ // Emit batch:started event
1367
+ await this.eventEmitter.emit({
1368
+ type: 'batch:started',
1369
+ timestamp: new Date().toISOString(),
1370
+ operation: 'inscribe',
1371
+ batchId,
1372
+ itemCount: assets.length
1373
+ });
1374
+
1375
+ try {
1376
+ const result = await this.batchExecutor.execute(
1377
+ assets,
1378
+ async (asset, _index) => {
1379
+ return await this.inscribeOnBitcoin(asset, options?.feeRate);
1380
+ },
1381
+ options,
1382
+ batchId // Pass the pre-generated batchId for event correlation
1383
+ );
1384
+
1385
+ // Emit batch:completed event
1386
+ await this.eventEmitter.emit({
1387
+ type: 'batch:completed',
1388
+ timestamp: new Date().toISOString(),
1389
+ batchId,
1390
+ operation: 'inscribe',
1391
+ results: {
1392
+ successful: result.successful.length,
1393
+ failed: result.failed.length,
1394
+ totalDuration: result.totalDuration
1395
+ }
1396
+ });
1397
+
1398
+ return result;
1399
+ } catch (error) {
1400
+ // Emit batch:failed event
1401
+ await this.eventEmitter.emit({
1402
+ type: 'batch:failed',
1403
+ timestamp: new Date().toISOString(),
1404
+ batchId,
1405
+ operation: 'inscribe',
1406
+ error: error instanceof Error ? error.message : String(error)
1407
+ });
1408
+
1409
+ throw error;
1410
+ }
1411
+ }
1412
+
1413
+ /**
1414
+ * Transfer ownership of multiple assets in batch
1415
+ *
1416
+ * @param transfers - Array of transfer operations
1417
+ * @param options - Batch operation options
1418
+ * @returns BatchResult with transaction results
1419
+ */
1420
+ async batchTransferOwnership(
1421
+ transfers: Array<{ asset: OriginalsAsset; to: string }>,
1422
+ options?: BatchOperationOptions
1423
+ ): Promise<BatchResult<BitcoinTransaction>> {
1424
+ const batchId = this.batchExecutor.generateBatchId();
1425
+
1426
+ // Validate first if requested
1427
+ if (options?.validateFirst !== false) {
1428
+ const validationResults = this.batchValidator.validateBatchTransfer(transfers);
1429
+ const invalid = validationResults.filter(r => !r.isValid);
1430
+ if (invalid.length > 0) {
1431
+ const errors = invalid.flatMap(r => r.errors).join('; ');
1432
+ throw new Error(`Batch validation failed: ${errors}`);
1433
+ }
1434
+
1435
+ // Validate all Bitcoin addresses
1436
+ for (let i = 0; i < transfers.length; i++) {
1437
+ try {
1438
+ validateBitcoinAddress(transfers[i].to, this.config.network);
1439
+ } catch (error) {
1440
+ const message = error instanceof Error ? error.message : 'Invalid Bitcoin address';
1441
+ throw new Error(`Transfer ${i}: Invalid Bitcoin address: ${message}`);
1442
+ }
1443
+ }
1444
+ }
1445
+
1446
+ // Emit batch:started event
1447
+ await this.eventEmitter.emit({
1448
+ type: 'batch:started',
1449
+ timestamp: new Date().toISOString(),
1450
+ operation: 'transfer',
1451
+ batchId,
1452
+ itemCount: transfers.length
1453
+ });
1454
+
1455
+ try {
1456
+ const result = await this.batchExecutor.execute(
1457
+ transfers,
1458
+ async (transfer, _index) => {
1459
+ return await this.transferOwnership(transfer.asset, transfer.to);
1460
+ },
1461
+ options,
1462
+ batchId // Pass the pre-generated batchId for event correlation
1463
+ );
1464
+
1465
+ // Emit batch:completed event
1466
+ await this.eventEmitter.emit({
1467
+ type: 'batch:completed',
1468
+ timestamp: new Date().toISOString(),
1469
+ batchId,
1470
+ operation: 'transfer',
1471
+ results: {
1472
+ successful: result.successful.length,
1473
+ failed: result.failed.length,
1474
+ totalDuration: result.totalDuration
1475
+ }
1476
+ });
1477
+
1478
+ return result;
1479
+ } catch (error) {
1480
+ // Emit batch:failed event
1481
+ await this.eventEmitter.emit({
1482
+ type: 'batch:failed',
1483
+ timestamp: new Date().toISOString(),
1484
+ batchId,
1485
+ operation: 'transfer',
1486
+ error: error instanceof Error ? error.message : String(error)
1487
+ });
1488
+
1489
+ throw error;
1490
+ }
1491
+ }
1492
+
1493
+ /**
1494
+ * Calculate total data size for all assets in a batch
1495
+ */
1496
+ private calculateTotalDataSize(assets: OriginalsAsset[]): number {
1497
+ return assets.reduce((total, asset) => {
1498
+ const manifest = {
1499
+ assetId: asset.id,
1500
+ resources: asset.resources.map(res => ({
1501
+ id: res.id,
1502
+ hash: res.hash,
1503
+ contentType: res.contentType,
1504
+ url: res.url
1505
+ })),
1506
+ timestamp: new Date().toISOString()
1507
+ };
1508
+ return total + JSON.stringify(manifest).length;
1509
+ }, 0);
1510
+ }
1511
+
1512
+ /**
1513
+ * Estimate cost savings from batch inscription vs individual inscriptions
1514
+ */
1515
+ private estimateBatchSavings(
1516
+ assets: OriginalsAsset[],
1517
+ feeRate?: number
1518
+ ): {
1519
+ batchFee: number;
1520
+ individualFees: number;
1521
+ savings: number;
1522
+ savingsPercentage: number;
1523
+ } {
1524
+ // Calculate total size for batch
1525
+ const batchSize = this.calculateTotalDataSize(assets);
1526
+
1527
+ // Estimate individual sizes
1528
+ const individualSizes = assets.map(asset =>
1529
+ JSON.stringify({
1530
+ assetId: asset.id,
1531
+ resources: asset.resources.map(r => ({
1532
+ id: r.id,
1533
+ hash: r.hash,
1534
+ contentType: r.contentType,
1535
+ url: r.url
1536
+ }))
1537
+ }).length
1538
+ );
1539
+
1540
+ // Realistic fee estimation based on Bitcoin transaction structure
1541
+ // Base transaction overhead: ~200 bytes (inputs, outputs, etc.)
1542
+ // Per inscription witness overhead: ~120 bytes (script, envelope, etc.)
1543
+ // In batch mode: shared transaction overhead + minimal per-asset overhead
1544
+ const effectiveFeeRate = feeRate ?? 10; // default 10 sat/vB
1545
+
1546
+ // Batch: one transaction overhead + batch data + minimal per-asset overhead
1547
+ // The batch manifest is more efficient as it shares structure
1548
+ const batchTxSize = 200 + batchSize + (assets.length * 5); // 5 bytes per asset for array/object overhead
1549
+ const batchFee = batchTxSize * effectiveFeeRate;
1550
+
1551
+ // Individual: each inscription needs full transaction overhead + witness overhead + data
1552
+ const individualFees = individualSizes.reduce((total, size) => {
1553
+ // Each individual inscription has:
1554
+ // - Full transaction overhead: 200 bytes
1555
+ // - Witness/inscription overhead: 122 bytes
1556
+ // - Asset data: size bytes
1557
+ const txSize = 200 + 122 + size;
1558
+ return total + (txSize * effectiveFeeRate);
1559
+ }, 0);
1560
+
1561
+ const savings = individualFees - batchFee;
1562
+ const savingsPercentage = (savings / individualFees) * 100;
1563
+
1564
+ return {
1565
+ batchFee,
1566
+ individualFees,
1567
+ savings,
1568
+ savingsPercentage
1569
+ };
1570
+ }
1571
+
1572
+ // ===== Clean Lifecycle API =====
1573
+ // These methods provide a cleaner, more intuitive API while maintaining
1574
+ // backward compatibility with the existing methods.
1575
+
1576
+ /**
1577
+ * Create a draft asset (did:peer layer)
1578
+ *
1579
+ * This is the entry point for creating new Originals. Draft assets are
1580
+ * stored locally and can be published or inscribed later.
1581
+ *
1582
+ * @param resources - Array of resources to include in the asset
1583
+ * @param options - Optional configuration including progress callback
1584
+ * @returns The newly created OriginalsAsset in did:peer layer
1585
+ *
1586
+ * @example
1587
+ * ```typescript
1588
+ * const draft = await sdk.lifecycle.createDraft([
1589
+ * { id: 'main', type: 'code', contentType: 'text/javascript', hash: '...' }
1590
+ * ], {
1591
+ * onProgress: (p) => console.log(p.message)
1592
+ * });
1593
+ * ```
1594
+ */
1595
+ async createDraft(
1596
+ resources: AssetResource[],
1597
+ options?: LifecycleOperationOptions
1598
+ ): Promise<OriginalsAsset> {
1599
+ const onProgress = options?.onProgress;
1600
+
1601
+ onProgress?.({
1602
+ phase: 'preparing',
1603
+ percentage: 0,
1604
+ message: 'Preparing draft asset...'
1605
+ });
1606
+
1607
+ onProgress?.({
1608
+ phase: 'validating',
1609
+ percentage: 20,
1610
+ message: 'Validating resources...'
1611
+ });
1612
+
1613
+ try {
1614
+ onProgress?.({
1615
+ phase: 'processing',
1616
+ percentage: 50,
1617
+ message: 'Creating DID document...'
1618
+ });
1619
+
1620
+ const asset = await this.createAsset(resources);
1621
+
1622
+ onProgress?.({
1623
+ phase: 'complete',
1624
+ percentage: 100,
1625
+ message: 'Draft asset created successfully'
1626
+ });
1627
+
1628
+ return asset;
1629
+ } catch (error) {
1630
+ onProgress?.({
1631
+ phase: 'failed',
1632
+ percentage: 0,
1633
+ message: `Failed to create draft: ${error instanceof Error ? error.message : String(error)}`
1634
+ });
1635
+ throw error;
1636
+ }
1637
+ }
1638
+
1639
+ /**
1640
+ * Publish an asset to the web (did:webvh layer)
1641
+ *
1642
+ * Migrates a draft asset from did:peer to did:webvh, making it publicly
1643
+ * discoverable via HTTPS.
1644
+ *
1645
+ * @param asset - The asset to publish (must be in did:peer layer)
1646
+ * @param publisherDidOrSigner - Publisher's DID or external signer
1647
+ * @param options - Optional configuration including progress callback
1648
+ * @returns The published OriginalsAsset in did:webvh layer
1649
+ *
1650
+ * @example
1651
+ * ```typescript
1652
+ * const published = await sdk.lifecycle.publish(draft, 'did:webvh:example.com:user');
1653
+ * ```
1654
+ */
1655
+ async publish(
1656
+ asset: OriginalsAsset,
1657
+ publisherDidOrSigner: string | ExternalSigner,
1658
+ options?: LifecycleOperationOptions
1659
+ ): Promise<OriginalsAsset> {
1660
+ const onProgress = options?.onProgress;
1661
+
1662
+ onProgress?.({
1663
+ phase: 'preparing',
1664
+ percentage: 0,
1665
+ message: 'Preparing to publish...'
1666
+ });
1667
+
1668
+ onProgress?.({
1669
+ phase: 'validating',
1670
+ percentage: 10,
1671
+ message: 'Validating migration...'
1672
+ });
1673
+
1674
+ // Pre-flight validation
1675
+ const validation = this.validateMigration(asset, 'did:webvh');
1676
+ if (!validation.valid) {
1677
+ onProgress?.({
1678
+ phase: 'failed',
1679
+ percentage: 0,
1680
+ message: `Validation failed: ${validation.errors.join(', ')}`
1681
+ });
1682
+ throw new Error(`Migration validation failed: ${validation.errors.join(', ')}`);
1683
+ }
1684
+
1685
+ try {
1686
+ onProgress?.({
1687
+ phase: 'processing',
1688
+ percentage: 30,
1689
+ message: 'Publishing resources...'
1690
+ });
1691
+
1692
+ onProgress?.({
1693
+ phase: 'committing',
1694
+ percentage: 70,
1695
+ message: 'Finalizing publication...'
1696
+ });
1697
+
1698
+ const published = await this.publishToWeb(asset, publisherDidOrSigner);
1699
+
1700
+ onProgress?.({
1701
+ phase: 'complete',
1702
+ percentage: 100,
1703
+ message: 'Asset published successfully'
1704
+ });
1705
+
1706
+ return published;
1707
+ } catch (error) {
1708
+ onProgress?.({
1709
+ phase: 'failed',
1710
+ percentage: 0,
1711
+ message: `Failed to publish: ${error instanceof Error ? error.message : String(error)}`
1712
+ });
1713
+ throw error;
1714
+ }
1715
+ }
1716
+
1717
+ /**
1718
+ * Inscribe an asset on Bitcoin (did:btco layer)
1719
+ *
1720
+ * Permanently anchors an asset on the Bitcoin blockchain via Ordinals inscription.
1721
+ * This is an irreversible operation.
1722
+ *
1723
+ * @param asset - The asset to inscribe (must be in did:peer or did:webvh layer)
1724
+ * @param options - Optional configuration including fee rate and progress callback
1725
+ * @returns The inscribed OriginalsAsset in did:btco layer
1726
+ *
1727
+ * @example
1728
+ * ```typescript
1729
+ * const inscribed = await sdk.lifecycle.inscribe(published, {
1730
+ * feeRate: 15,
1731
+ * onProgress: (p) => console.log(`${p.percentage}%: ${p.message}`)
1732
+ * });
1733
+ * ```
1734
+ */
1735
+ async inscribe(
1736
+ asset: OriginalsAsset,
1737
+ options?: LifecycleOperationOptions
1738
+ ): Promise<OriginalsAsset> {
1739
+ const onProgress = options?.onProgress;
1740
+ const feeRate = options?.feeRate;
1741
+
1742
+ onProgress?.({
1743
+ phase: 'preparing',
1744
+ percentage: 0,
1745
+ message: 'Preparing inscription...'
1746
+ });
1747
+
1748
+ onProgress?.({
1749
+ phase: 'validating',
1750
+ percentage: 10,
1751
+ message: 'Validating migration...'
1752
+ });
1753
+
1754
+ // Pre-flight validation
1755
+ const validation = this.validateMigration(asset, 'did:btco');
1756
+ if (!validation.valid) {
1757
+ onProgress?.({
1758
+ phase: 'failed',
1759
+ percentage: 0,
1760
+ message: `Validation failed: ${validation.errors.join(', ')}`
1761
+ });
1762
+ throw new Error(`Migration validation failed: ${validation.errors.join(', ')}`);
1763
+ }
1764
+
1765
+ // Show cost estimate
1766
+ if (onProgress) {
1767
+ const estimate = await this.estimateCost(asset, 'did:btco', feeRate);
1768
+ onProgress({
1769
+ phase: 'preparing',
1770
+ percentage: 20,
1771
+ message: `Estimated cost: ${estimate.totalSats} sats (${estimate.feeRate} sat/vB)`
1772
+ });
1773
+ }
1774
+
1775
+ try {
1776
+ onProgress?.({
1777
+ phase: 'processing',
1778
+ percentage: 30,
1779
+ message: 'Creating commit transaction...',
1780
+ details: { currentStep: 1, totalSteps: 3 }
1781
+ });
1782
+
1783
+ onProgress?.({
1784
+ phase: 'committing',
1785
+ percentage: 60,
1786
+ message: 'Broadcasting reveal transaction...',
1787
+ details: { currentStep: 2, totalSteps: 3 }
1788
+ });
1789
+
1790
+ const inscribed = await this.inscribeOnBitcoin(asset, feeRate);
1791
+
1792
+ onProgress?.({
1793
+ phase: 'confirming',
1794
+ percentage: 90,
1795
+ message: 'Waiting for confirmation...',
1796
+ details: { currentStep: 3, totalSteps: 3 }
1797
+ });
1798
+
1799
+ onProgress?.({
1800
+ phase: 'complete',
1801
+ percentage: 100,
1802
+ message: 'Asset inscribed successfully'
1803
+ });
1804
+
1805
+ return inscribed;
1806
+ } catch (error) {
1807
+ onProgress?.({
1808
+ phase: 'failed',
1809
+ percentage: 0,
1810
+ message: `Failed to inscribe: ${error instanceof Error ? error.message : String(error)}`
1811
+ });
1812
+ throw error;
1813
+ }
1814
+ }
1815
+
1816
+ /**
1817
+ * Transfer ownership of a Bitcoin-inscribed asset
1818
+ *
1819
+ * Transfers an inscribed asset to a new owner. Only works for assets
1820
+ * in the did:btco layer.
1821
+ *
1822
+ * @param asset - The asset to transfer (must be in did:btco layer)
1823
+ * @param newOwnerAddress - Bitcoin address of the new owner
1824
+ * @param options - Optional configuration including progress callback
1825
+ * @returns The Bitcoin transaction for the transfer
1826
+ *
1827
+ * @example
1828
+ * ```typescript
1829
+ * const tx = await sdk.lifecycle.transfer(inscribed, 'bc1q...newowner');
1830
+ * console.log('Transfer txid:', tx.txid);
1831
+ * ```
1832
+ */
1833
+ async transfer(
1834
+ asset: OriginalsAsset,
1835
+ newOwnerAddress: string,
1836
+ options?: LifecycleOperationOptions
1837
+ ): Promise<BitcoinTransaction> {
1838
+ const onProgress = options?.onProgress;
1839
+
1840
+ onProgress?.({
1841
+ phase: 'preparing',
1842
+ percentage: 0,
1843
+ message: 'Preparing transfer...'
1844
+ });
1845
+
1846
+ onProgress?.({
1847
+ phase: 'validating',
1848
+ percentage: 10,
1849
+ message: 'Validating transfer...'
1850
+ });
1851
+
1852
+ // Validate asset is in correct layer
1853
+ if (asset.currentLayer !== 'did:btco') {
1854
+ onProgress?.({
1855
+ phase: 'failed',
1856
+ percentage: 0,
1857
+ message: 'Asset must be inscribed on Bitcoin before transfer'
1858
+ });
1859
+ throw new Error('Asset must be inscribed on Bitcoin before transfer');
1860
+ }
1861
+
1862
+ try {
1863
+ onProgress?.({
1864
+ phase: 'processing',
1865
+ percentage: 30,
1866
+ message: 'Creating transfer transaction...'
1867
+ });
1868
+
1869
+ onProgress?.({
1870
+ phase: 'committing',
1871
+ percentage: 60,
1872
+ message: 'Broadcasting transaction...'
1873
+ });
1874
+
1875
+ const tx = await this.transferOwnership(asset, newOwnerAddress);
1876
+
1877
+ onProgress?.({
1878
+ phase: 'confirming',
1879
+ percentage: 90,
1880
+ message: 'Waiting for confirmation...',
1881
+ details: { transactionId: tx.txid }
1882
+ });
1883
+
1884
+ onProgress?.({
1885
+ phase: 'complete',
1886
+ percentage: 100,
1887
+ message: 'Transfer complete',
1888
+ details: { transactionId: tx.txid }
1889
+ });
1890
+
1891
+ return tx;
1892
+ } catch (error) {
1893
+ onProgress?.({
1894
+ phase: 'failed',
1895
+ percentage: 0,
1896
+ message: `Failed to transfer: ${error instanceof Error ? error.message : String(error)}`
1897
+ });
1898
+ throw error;
1899
+ }
1900
+ }
1901
+
1902
+ // ===== Cost Estimation =====
1903
+
1904
+ /**
1905
+ * Estimate the cost of migrating an asset to a target layer
1906
+ *
1907
+ * Returns a detailed breakdown of expected costs for Bitcoin operations.
1908
+ * For did:webvh migrations, costs are minimal (only hosting).
1909
+ *
1910
+ * @param asset - The asset to estimate costs for
1911
+ * @param targetLayer - The target layer for migration
1912
+ * @param feeRate - Optional fee rate override (sat/vB)
1913
+ * @returns Detailed cost estimate
1914
+ *
1915
+ * @example
1916
+ * ```typescript
1917
+ * const cost = await sdk.lifecycle.estimateCost(draft, 'did:btco', 10);
1918
+ * console.log(`Estimated cost: ${cost.totalSats} sats`);
1919
+ * ```
1920
+ */
1921
+ async estimateCost(
1922
+ asset: OriginalsAsset,
1923
+ targetLayer: LayerType,
1924
+ feeRate?: number
1925
+ ): Promise<CostEstimate> {
1926
+ // For webvh, costs are minimal (just hosting costs not applicable here)
1927
+ if (targetLayer === 'did:webvh') {
1928
+ return {
1929
+ totalSats: 0,
1930
+ breakdown: {
1931
+ networkFee: 0,
1932
+ dataCost: 0,
1933
+ dustValue: 0
1934
+ },
1935
+ feeRate: 0,
1936
+ dataSize: 0,
1937
+ targetLayer,
1938
+ confidence: 'high'
1939
+ };
1940
+ }
1941
+
1942
+ // For btco, calculate inscription costs
1943
+ if (targetLayer === 'did:btco') {
1944
+ // Calculate manifest size
1945
+ const manifest = {
1946
+ assetId: asset.id,
1947
+ resources: asset.resources.map(res => ({
1948
+ id: res.id,
1949
+ hash: res.hash,
1950
+ contentType: res.contentType,
1951
+ url: res.url
1952
+ })),
1953
+ timestamp: new Date().toISOString()
1954
+ };
1955
+ const dataSize = Buffer.from(JSON.stringify(manifest)).length;
1956
+
1957
+ // Get fee rate from oracle or use provided/default
1958
+ let effectiveFeeRate = feeRate;
1959
+ let confidence: 'low' | 'medium' | 'high' = 'medium';
1960
+
1961
+ if (!effectiveFeeRate) {
1962
+ // Try to get from fee oracle
1963
+ if (this.config.feeOracle) {
1964
+ try {
1965
+ effectiveFeeRate = await this.config.feeOracle.estimateFeeRate(1);
1966
+ confidence = 'high';
1967
+ } catch {
1968
+ // Fallback to default
1969
+ }
1970
+ }
1971
+
1972
+ // Try ordinals provider
1973
+ if (!effectiveFeeRate && this.config.ordinalsProvider) {
1974
+ try {
1975
+ effectiveFeeRate = await this.config.ordinalsProvider.estimateFee(1);
1976
+ confidence = 'medium';
1977
+ } catch {
1978
+ // Fallback to default
1979
+ }
1980
+ }
1981
+
1982
+ // Use default if no oracle available
1983
+ if (!effectiveFeeRate) {
1984
+ effectiveFeeRate = 10; // Conservative default
1985
+ confidence = 'low';
1986
+ }
1987
+ }
1988
+
1989
+ // Transaction structure estimation:
1990
+ // - Commit transaction: ~200 vB base + input overhead
1991
+ // - Reveal transaction: ~200 vB base + inscription envelope + data
1992
+ // Inscription envelope overhead: ~122 bytes
1993
+ const commitTxSize = 200;
1994
+ const revealTxSize = 200 + 122 + dataSize;
1995
+ const totalSize = commitTxSize + revealTxSize;
1996
+
1997
+ const networkFee = totalSize * effectiveFeeRate;
1998
+ const dustValue = 546; // Standard dust limit for P2TR
1999
+ const totalSats = networkFee + dustValue;
2000
+
2001
+ return {
2002
+ totalSats,
2003
+ breakdown: {
2004
+ networkFee,
2005
+ dataCost: dataSize * effectiveFeeRate,
2006
+ dustValue
2007
+ },
2008
+ feeRate: effectiveFeeRate,
2009
+ dataSize,
2010
+ targetLayer,
2011
+ confidence
2012
+ };
2013
+ }
2014
+
2015
+ // For peer layer (no migration needed)
2016
+ return {
2017
+ totalSats: 0,
2018
+ breakdown: {
2019
+ networkFee: 0,
2020
+ dataCost: 0,
2021
+ dustValue: 0
2022
+ },
2023
+ feeRate: 0,
2024
+ dataSize: 0,
2025
+ targetLayer,
2026
+ confidence: 'high'
2027
+ };
2028
+ }
2029
+
2030
+ // ===== Migration Validation =====
2031
+
2032
+ /**
2033
+ * Validate whether an asset can be migrated to a target layer
2034
+ *
2035
+ * Performs comprehensive pre-flight checks including:
2036
+ * - Valid layer transition
2037
+ * - Resource integrity
2038
+ * - Credential validity
2039
+ * - DID document structure
2040
+ * - Bitcoin readiness (for did:btco)
2041
+ *
2042
+ * @param asset - The asset to validate
2043
+ * @param targetLayer - The target layer for migration
2044
+ * @returns Detailed validation result
2045
+ *
2046
+ * @example
2047
+ * ```typescript
2048
+ * const validation = await sdk.lifecycle.validateMigration(draft, 'did:webvh');
2049
+ * if (!validation.valid) {
2050
+ * console.error('Cannot migrate:', validation.errors);
2051
+ * }
2052
+ * ```
2053
+ */
2054
+ validateMigration(
2055
+ asset: OriginalsAsset,
2056
+ targetLayer: LayerType
2057
+ ): MigrationValidation {
2058
+ const errors: string[] = [];
2059
+ const warnings: string[] = [];
2060
+ const checks = {
2061
+ layerTransition: false,
2062
+ resourcesValid: false,
2063
+ credentialsValid: false,
2064
+ didDocumentValid: false,
2065
+ bitcoinReadiness: undefined as boolean | undefined
2066
+ };
2067
+
2068
+ // Check layer transition validity
2069
+ const validTransitions: Record<LayerType, LayerType[]> = {
2070
+ 'did:peer': ['did:webvh', 'did:btco'],
2071
+ 'did:webvh': ['did:btco'],
2072
+ 'did:btco': []
2073
+ };
2074
+
2075
+ if (validTransitions[asset.currentLayer].includes(targetLayer)) {
2076
+ checks.layerTransition = true;
2077
+ } else {
2078
+ errors.push(`Invalid migration from ${asset.currentLayer} to ${targetLayer}`);
2079
+ }
2080
+
2081
+ // Validate resources
2082
+ if (asset.resources.length === 0) {
2083
+ errors.push('Asset must have at least one resource');
2084
+ } else {
2085
+ let resourcesValid = true;
2086
+ for (const resource of asset.resources) {
2087
+ if (!resource.id || !resource.type || !resource.contentType || !resource.hash) {
2088
+ resourcesValid = false;
2089
+ errors.push(`Resource ${resource.id || 'unknown'} is missing required fields`);
2090
+ }
2091
+ if (resource.hash && !/^[0-9a-fA-F]+$/.test(resource.hash)) {
2092
+ resourcesValid = false;
2093
+ errors.push(`Resource ${resource.id} has invalid hash format`);
2094
+ }
2095
+ }
2096
+ checks.resourcesValid = resourcesValid;
2097
+ }
2098
+
2099
+ // Validate DID document
2100
+ if (asset.did && asset.did.id) {
2101
+ checks.didDocumentValid = true;
2102
+ } else {
2103
+ errors.push('Asset has invalid or missing DID document');
2104
+ }
2105
+
2106
+ // Validate credentials (structural check)
2107
+ if (asset.credentials.length > 0) {
2108
+ let credentialsValid = true;
2109
+ for (const cred of asset.credentials) {
2110
+ if (!cred.type || !cred.issuer || !cred.issuanceDate) {
2111
+ credentialsValid = false;
2112
+ warnings.push('Asset has credentials with missing fields');
2113
+ }
2114
+ }
2115
+ checks.credentialsValid = credentialsValid;
2116
+ } else {
2117
+ checks.credentialsValid = true; // No credentials is valid
2118
+ }
2119
+
2120
+ // Bitcoin-specific checks
2121
+ if (targetLayer === 'did:btco') {
2122
+ checks.bitcoinReadiness = true;
2123
+
2124
+ // Check if ordinals provider is configured
2125
+ if (!this.config.ordinalsProvider) {
2126
+ checks.bitcoinReadiness = false;
2127
+ errors.push('Bitcoin inscription requires an ordinalsProvider to be configured');
2128
+ }
2129
+
2130
+ // Warn about large data sizes
2131
+ const manifestSize = JSON.stringify({
2132
+ assetId: asset.id,
2133
+ resources: asset.resources.map(r => ({
2134
+ id: r.id,
2135
+ hash: r.hash,
2136
+ contentType: r.contentType
2137
+ }))
2138
+ }).length;
2139
+
2140
+ if (manifestSize > 100000) {
2141
+ warnings.push(`Large manifest size (${manifestSize} bytes) may result in high inscription costs`);
2142
+ }
2143
+ }
2144
+
2145
+ return {
2146
+ valid: errors.length === 0,
2147
+ errors,
2148
+ warnings,
2149
+ currentLayer: asset.currentLayer,
2150
+ targetLayer,
2151
+ checks
2152
+ };
2153
+ }
2154
+ }
2155
+
2156
+