@originals/sdk 1.4.3 → 1.4.5

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