@originals/sdk 1.4.2 → 1.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (212) hide show
  1. package/package.json +4 -1
  2. package/.eslintrc.json +0 -33
  3. package/src/adapters/FeeOracleMock.ts +0 -9
  4. package/src/adapters/index.ts +0 -5
  5. package/src/adapters/providers/OrdHttpProvider.ts +0 -126
  6. package/src/adapters/providers/OrdMockProvider.ts +0 -101
  7. package/src/adapters/types.ts +0 -66
  8. package/src/bitcoin/BitcoinManager.ts +0 -330
  9. package/src/bitcoin/BroadcastClient.ts +0 -54
  10. package/src/bitcoin/OrdinalsClient.ts +0 -119
  11. package/src/bitcoin/PSBTBuilder.ts +0 -106
  12. package/src/bitcoin/fee-calculation.ts +0 -38
  13. package/src/bitcoin/providers/OrdNodeProvider.ts +0 -92
  14. package/src/bitcoin/providers/OrdinalsProvider.ts +0 -56
  15. package/src/bitcoin/providers/types.ts +0 -59
  16. package/src/bitcoin/transactions/commit.ts +0 -465
  17. package/src/bitcoin/transactions/index.ts +0 -13
  18. package/src/bitcoin/transfer.ts +0 -43
  19. package/src/bitcoin/utxo-selection.ts +0 -322
  20. package/src/bitcoin/utxo.ts +0 -113
  21. package/src/contexts/credentials-v1.json +0 -237
  22. package/src/contexts/credentials-v2-examples.json +0 -5
  23. package/src/contexts/credentials-v2.json +0 -340
  24. package/src/contexts/credentials.json +0 -237
  25. package/src/contexts/data-integrity-v2.json +0 -81
  26. package/src/contexts/dids.json +0 -58
  27. package/src/contexts/ed255192020.json +0 -93
  28. package/src/contexts/ordinals-plus.json +0 -23
  29. package/src/contexts/originals.json +0 -22
  30. package/src/core/OriginalsSDK.ts +0 -416
  31. package/src/crypto/Multikey.ts +0 -194
  32. package/src/crypto/Signer.ts +0 -254
  33. package/src/crypto/noble-init.ts +0 -121
  34. package/src/did/BtcoDidResolver.ts +0 -227
  35. package/src/did/DIDManager.ts +0 -694
  36. package/src/did/Ed25519Verifier.ts +0 -68
  37. package/src/did/KeyManager.ts +0 -236
  38. package/src/did/WebVHManager.ts +0 -498
  39. package/src/did/createBtcoDidDocument.ts +0 -59
  40. package/src/did/providers/OrdinalsClientProviderAdapter.ts +0 -68
  41. package/src/events/EventEmitter.ts +0 -222
  42. package/src/events/index.ts +0 -19
  43. package/src/events/types.ts +0 -331
  44. package/src/examples/basic-usage.ts +0 -78
  45. package/src/examples/create-module-original.ts +0 -435
  46. package/src/examples/full-lifecycle-flow.ts +0 -514
  47. package/src/examples/run.ts +0 -60
  48. package/src/index.ts +0 -150
  49. package/src/kinds/KindRegistry.ts +0 -290
  50. package/src/kinds/index.ts +0 -74
  51. package/src/kinds/types.ts +0 -470
  52. package/src/kinds/validators/AgentValidator.ts +0 -257
  53. package/src/kinds/validators/AppValidator.ts +0 -211
  54. package/src/kinds/validators/DatasetValidator.ts +0 -242
  55. package/src/kinds/validators/DocumentValidator.ts +0 -311
  56. package/src/kinds/validators/MediaValidator.ts +0 -269
  57. package/src/kinds/validators/ModuleValidator.ts +0 -225
  58. package/src/kinds/validators/base.ts +0 -276
  59. package/src/kinds/validators/index.ts +0 -12
  60. package/src/lifecycle/BatchOperations.ts +0 -373
  61. package/src/lifecycle/LifecycleManager.ts +0 -2126
  62. package/src/lifecycle/OriginalsAsset.ts +0 -524
  63. package/src/lifecycle/ProvenanceQuery.ts +0 -280
  64. package/src/lifecycle/ResourceVersioning.ts +0 -163
  65. package/src/migration/MigrationManager.ts +0 -527
  66. package/src/migration/audit/AuditLogger.ts +0 -176
  67. package/src/migration/checkpoint/CheckpointManager.ts +0 -112
  68. package/src/migration/checkpoint/CheckpointStorage.ts +0 -101
  69. package/src/migration/index.ts +0 -33
  70. package/src/migration/operations/BaseMigration.ts +0 -126
  71. package/src/migration/operations/PeerToBtcoMigration.ts +0 -105
  72. package/src/migration/operations/PeerToWebvhMigration.ts +0 -62
  73. package/src/migration/operations/WebvhToBtcoMigration.ts +0 -105
  74. package/src/migration/rollback/RollbackManager.ts +0 -170
  75. package/src/migration/state/StateMachine.ts +0 -92
  76. package/src/migration/state/StateTracker.ts +0 -156
  77. package/src/migration/types.ts +0 -344
  78. package/src/migration/validation/BitcoinValidator.ts +0 -107
  79. package/src/migration/validation/CredentialValidator.ts +0 -62
  80. package/src/migration/validation/DIDCompatibilityValidator.ts +0 -151
  81. package/src/migration/validation/LifecycleValidator.ts +0 -64
  82. package/src/migration/validation/StorageValidator.ts +0 -79
  83. package/src/migration/validation/ValidationPipeline.ts +0 -213
  84. package/src/resources/ResourceManager.ts +0 -655
  85. package/src/resources/index.ts +0 -21
  86. package/src/resources/types.ts +0 -202
  87. package/src/storage/LocalStorageAdapter.ts +0 -61
  88. package/src/storage/MemoryStorageAdapter.ts +0 -29
  89. package/src/storage/StorageAdapter.ts +0 -25
  90. package/src/storage/index.ts +0 -3
  91. package/src/types/bitcoin.ts +0 -98
  92. package/src/types/common.ts +0 -92
  93. package/src/types/credentials.ts +0 -88
  94. package/src/types/did.ts +0 -31
  95. package/src/types/external-shims.d.ts +0 -53
  96. package/src/types/index.ts +0 -7
  97. package/src/types/network.ts +0 -175
  98. package/src/utils/EventLogger.ts +0 -298
  99. package/src/utils/Logger.ts +0 -322
  100. package/src/utils/MetricsCollector.ts +0 -358
  101. package/src/utils/bitcoin-address.ts +0 -130
  102. package/src/utils/cbor.ts +0 -12
  103. package/src/utils/encoding.ts +0 -127
  104. package/src/utils/hash.ts +0 -6
  105. package/src/utils/retry.ts +0 -46
  106. package/src/utils/satoshi-validation.ts +0 -196
  107. package/src/utils/serialization.ts +0 -96
  108. package/src/utils/telemetry.ts +0 -40
  109. package/src/utils/validation.ts +0 -119
  110. package/src/vc/CredentialManager.ts +0 -918
  111. package/src/vc/Issuer.ts +0 -100
  112. package/src/vc/Verifier.ts +0 -47
  113. package/src/vc/cryptosuites/bbs.ts +0 -253
  114. package/src/vc/cryptosuites/bbsSimple.ts +0 -21
  115. package/src/vc/cryptosuites/eddsa.ts +0 -99
  116. package/src/vc/documentLoader.ts +0 -67
  117. package/src/vc/proofs/data-integrity.ts +0 -33
  118. package/src/vc/utils/jsonld.ts +0 -18
  119. package/tests/__mocks__/bbs-signatures.js +0 -17
  120. package/tests/__mocks__/mf-base58.js +0 -24
  121. package/tests/fixtures/did-documents.ts +0 -247
  122. package/tests/index.test.ts +0 -21
  123. package/tests/integration/BatchOperations.test.ts +0 -531
  124. package/tests/integration/CompleteLifecycle.e2e.test.ts +0 -735
  125. package/tests/integration/CredentialManager.test.ts +0 -42
  126. package/tests/integration/DIDManager.test.ts +0 -41
  127. package/tests/integration/DidPeerToWebVhFlow.test.ts +0 -351
  128. package/tests/integration/Events.test.ts +0 -435
  129. package/tests/integration/Lifecycle.transfer.btco.integration.test.ts +0 -25
  130. package/tests/integration/LifecycleManager.test.ts +0 -21
  131. package/tests/integration/MultikeyFlow.test.ts +0 -52
  132. package/tests/integration/TelemetryIntegration.test.ts +0 -395
  133. package/tests/integration/WebVhPublish.test.ts +0 -48
  134. package/tests/integration/createTypedOriginal.test.ts +0 -379
  135. package/tests/integration/migration/peer-to-webvh.test.ts +0 -172
  136. package/tests/manual/test-commit-creation.ts +0 -323
  137. package/tests/mocks/MockKeyStore.ts +0 -38
  138. package/tests/mocks/adapters/MemoryStorageAdapter.ts +0 -24
  139. package/tests/mocks/adapters/MockFeeOracle.ts +0 -11
  140. package/tests/mocks/adapters/MockOrdinalsProvider.ts +0 -76
  141. package/tests/mocks/adapters/OrdMockProvider.test.ts +0 -176
  142. package/tests/mocks/adapters/index.ts +0 -6
  143. package/tests/performance/BatchOperations.perf.test.ts +0 -403
  144. package/tests/performance/logging.perf.test.ts +0 -336
  145. package/tests/sdk.test.ts +0 -43
  146. package/tests/security/bitcoin-penetration-tests.test.ts +0 -622
  147. package/tests/setup.bun.ts +0 -69
  148. package/tests/setup.jest.ts +0 -23
  149. package/tests/stress/batch-operations-stress.test.ts +0 -571
  150. package/tests/unit/adapters/FeeOracleMock.test.ts +0 -40
  151. package/tests/unit/bitcoin/BitcoinManager.test.ts +0 -293
  152. package/tests/unit/bitcoin/BroadcastClient.test.ts +0 -52
  153. package/tests/unit/bitcoin/OrdNodeProvider.test.ts +0 -53
  154. package/tests/unit/bitcoin/OrdinalsClient.test.ts +0 -381
  155. package/tests/unit/bitcoin/OrdinalsClientProvider.test.ts +0 -102
  156. package/tests/unit/bitcoin/PSBTBuilder.test.ts +0 -84
  157. package/tests/unit/bitcoin/fee-calculation.test.ts +0 -261
  158. package/tests/unit/bitcoin/transactions/commit.test.ts +0 -649
  159. package/tests/unit/bitcoin/transfer.test.ts +0 -31
  160. package/tests/unit/bitcoin/utxo-selection-new.test.ts +0 -502
  161. package/tests/unit/bitcoin/utxo.more.test.ts +0 -39
  162. package/tests/unit/bitcoin/utxo.selection.test.ts +0 -38
  163. package/tests/unit/core/OriginalsSDK.test.ts +0 -152
  164. package/tests/unit/crypto/Multikey.test.ts +0 -206
  165. package/tests/unit/crypto/Signer.test.ts +0 -408
  166. package/tests/unit/did/BtcoDidResolver.test.ts +0 -611
  167. package/tests/unit/did/DIDManager.more.test.ts +0 -43
  168. package/tests/unit/did/DIDManager.test.ts +0 -185
  169. package/tests/unit/did/Ed25519Verifier.test.ts +0 -160
  170. package/tests/unit/did/KeyManager.test.ts +0 -452
  171. package/tests/unit/did/OrdinalsClientProviderAdapter.test.ts +0 -45
  172. package/tests/unit/did/WebVHManager.test.ts +0 -435
  173. package/tests/unit/did/createBtcoDidDocument.test.ts +0 -67
  174. package/tests/unit/did/providers/OrdinalsClientProviderAdapter.test.ts +0 -159
  175. package/tests/unit/events/EventEmitter.test.ts +0 -407
  176. package/tests/unit/kinds/KindRegistry.test.ts +0 -329
  177. package/tests/unit/kinds/types.test.ts +0 -409
  178. package/tests/unit/kinds/validators.test.ts +0 -651
  179. package/tests/unit/lifecycle/BatchOperations.test.ts +0 -527
  180. package/tests/unit/lifecycle/LifecycleManager.cleanapi.test.ts +0 -441
  181. package/tests/unit/lifecycle/LifecycleManager.keymanagement.test.ts +0 -312
  182. package/tests/unit/lifecycle/LifecycleManager.prov.test.ts +0 -18
  183. package/tests/unit/lifecycle/LifecycleManager.test.ts +0 -213
  184. package/tests/unit/lifecycle/LifecycleManager.transfer.unit.test.ts +0 -30
  185. package/tests/unit/lifecycle/OriginalsAsset.test.ts +0 -176
  186. package/tests/unit/lifecycle/ProvenanceQuery.test.ts +0 -577
  187. package/tests/unit/lifecycle/ResourceVersioning.test.ts +0 -651
  188. package/tests/unit/resources/ResourceManager.test.ts +0 -740
  189. package/tests/unit/storage/MemoryStorageAdapter.test.ts +0 -93
  190. package/tests/unit/types/network.test.ts +0 -255
  191. package/tests/unit/utils/EventIntegration.test.ts +0 -384
  192. package/tests/unit/utils/Logger.test.ts +0 -473
  193. package/tests/unit/utils/MetricsCollector.test.ts +0 -358
  194. package/tests/unit/utils/bitcoin-address.test.ts +0 -250
  195. package/tests/unit/utils/cbor.test.ts +0 -35
  196. package/tests/unit/utils/encoding.test.ts +0 -318
  197. package/tests/unit/utils/hash.test.ts +0 -12
  198. package/tests/unit/utils/retry.test.ts +0 -100
  199. package/tests/unit/utils/satoshi-validation.test.ts +0 -354
  200. package/tests/unit/utils/serialization.test.ts +0 -124
  201. package/tests/unit/utils/telemetry.test.ts +0 -52
  202. package/tests/unit/utils/validation.test.ts +0 -141
  203. package/tests/unit/vc/CredentialManager.helpers.test.ts +0 -527
  204. package/tests/unit/vc/CredentialManager.test.ts +0 -487
  205. package/tests/unit/vc/Issuer.test.ts +0 -107
  206. package/tests/unit/vc/Verifier.test.ts +0 -525
  207. package/tests/unit/vc/bbs.test.ts +0 -282
  208. package/tests/unit/vc/cryptosuites/eddsa.test.ts +0 -398
  209. package/tests/unit/vc/documentLoader.test.ts +0 -121
  210. package/tests/unit/vc/proofs/data-integrity.test.ts +0 -24
  211. package/tsconfig.json +0 -31
  212. package/tsconfig.test.json +0 -15
@@ -1,2126 +0,0 @@
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
- this.eventEmitter.emit(event);
269
- (asset as any).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
- this.eventEmitter.emit(event);
297
- (asset as any).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
- 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 any)._manifest as OriginalManifest<K> | undefined;
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 } = await 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 as any).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 async extractPublisherInfo(publisherDidOrSigner: string | ExternalSigner): Promise<{
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 vmId = await signer.getVerificationMethodId();
614
- const publisherDid = vmId.includes('#') ? vmId.split('#')[0] : vmId;
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 any).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
- if (typeof storage.put === 'function') {
655
- await storage.put(`${domain}/${relativePath}`, data, { contentType: resource.contentType });
656
- } else {
657
- const encoded = new TextEncoder().encode(resource.content || resource.hash);
658
- await storage.putObject(domain, relativePath, encoded);
659
- }
660
-
661
- (resource as any).url = resourceUrl;
662
-
663
- await this.emitResourcePublishedEvent(asset, resource, resourceUrl, publisherDid, domain);
664
- }
665
- }
666
-
667
- private async emitResourcePublishedEvent(
668
- asset: OriginalsAsset,
669
- resource: AssetResource,
670
- resourceUrl: string,
671
- publisherDid: string,
672
- domain: string
673
- ): Promise<void> {
674
- const event = {
675
- type: 'resource:published' as const,
676
- timestamp: new Date().toISOString(),
677
- asset: { id: asset.id },
678
- resource: {
679
- id: resource.id,
680
- url: resourceUrl,
681
- contentType: resource.contentType,
682
- hash: resource.hash
683
- },
684
- publisherDid,
685
- domain
686
- };
687
-
688
- try {
689
- // Emit from both LifecycleManager and asset emitters
690
- await this.eventEmitter.emit(event);
691
- await (asset as any).eventEmitter.emit(event);
692
- } catch (err) {
693
- this.logger.error('Event handler error', err as Error, { event: event.type });
694
- }
695
- }
696
-
697
- private async issuePublicationCredential(
698
- asset: OriginalsAsset,
699
- publisherDid: string,
700
- signer?: ExternalSigner
701
- ): Promise<void> {
702
- try {
703
- const subject = {
704
- id: asset.id,
705
- publishedAs: publisherDid,
706
- resourceId: asset.resources[0]?.id,
707
- fromLayer: 'did:peer' as const,
708
- toLayer: 'did:webvh' as const,
709
- migratedAt: new Date().toISOString()
710
- };
711
-
712
- const unsigned = await this.credentialManager.createResourceCredential(
713
- 'ResourceMigrated',
714
- subject,
715
- publisherDid
716
- );
717
-
718
- const signed = signer
719
- ? await this.credentialManager.signCredentialWithExternalSigner(unsigned, signer)
720
- : await this.signWithKeyStore(unsigned, publisherDid);
721
-
722
- asset.credentials.push(signed);
723
-
724
- const event = {
725
- type: 'credential:issued' as const,
726
- timestamp: new Date().toISOString(),
727
- asset: { id: asset.id },
728
- credential: {
729
- type: signed.type,
730
- issuer: typeof signed.issuer === 'string' ? signed.issuer : signed.issuer.id
731
- }
732
- };
733
-
734
- // Emit from both LifecycleManager and asset emitters
735
- await this.eventEmitter.emit(event);
736
- await (asset as any).eventEmitter.emit(event);
737
- } catch (err) {
738
- this.logger.error('Failed to issue credential during publish', err as Error);
739
- }
740
- }
741
-
742
- private async signWithKeyStore(
743
- credential: VerifiableCredential,
744
- issuer: string
745
- ): Promise<VerifiableCredential> {
746
- if (!this.keyStore) {
747
- throw new Error('KeyStore required for signing. Provide keyStore or external signer.');
748
- }
749
-
750
- // Try to find a key in the keyStore for this DID
751
- // First try common verification method patterns: #key-0, #keys-1, etc.
752
- const commonVmIds = [
753
- `${issuer}#key-0`,
754
- `${issuer}#keys-1`,
755
- `${issuer}#authentication`,
756
- ];
757
-
758
- let privateKey: string | null = null;
759
- let vmId: string | null = null;
760
-
761
- for (const testVmId of commonVmIds) {
762
- const key = await this.keyStore.getPrivateKey(testVmId);
763
- if (key) {
764
- privateKey = key;
765
- vmId = testVmId;
766
- break;
767
- }
768
- }
769
-
770
- // If not found, try to find ANY key that starts with the issuer DID
771
- if (!privateKey && typeof (this.keyStore as any).getAllVerificationMethodIds === 'function') {
772
- const allVmIds = (this.keyStore as any).getAllVerificationMethodIds();
773
- for (const testVmId of allVmIds) {
774
- if (testVmId.startsWith(issuer)) {
775
- const key = await this.keyStore.getPrivateKey(testVmId);
776
- if (key) {
777
- privateKey = key;
778
- vmId = testVmId;
779
- break;
780
- }
781
- }
782
- }
783
- }
784
-
785
- // If no key found in common patterns, try resolving the DID
786
- if (!privateKey) {
787
- const didDoc = await this.didManager.resolveDID(issuer);
788
- if (!didDoc?.verificationMethod?.[0]) {
789
- throw new Error('No verification method found in publisher DID document');
790
- }
791
-
792
- vmId = didDoc.verificationMethod[0].id;
793
- if (vmId.startsWith('#')) {
794
- vmId = `${issuer}${vmId}`;
795
- }
796
-
797
- privateKey = await this.keyStore.getPrivateKey(vmId);
798
- if (!privateKey) {
799
- throw new Error('Private key not found in keyStore');
800
- }
801
- }
802
-
803
- return this.credentialManager.signCredential(credential, privateKey!, vmId!);
804
- }
805
-
806
- async inscribeOnBitcoin(
807
- asset: OriginalsAsset,
808
- feeRate?: number
809
- ): Promise<OriginalsAsset> {
810
- const stopTimer = this.logger.startTimer('inscribeOnBitcoin');
811
- this.logger.info('Inscribing asset on Bitcoin', { assetId: asset.id, feeRate });
812
-
813
- try {
814
- // Input validation
815
- if (!asset || typeof asset !== 'object') {
816
- throw new Error('Invalid asset: must be a valid OriginalsAsset');
817
- }
818
- if (feeRate !== undefined) {
819
- if (typeof feeRate !== 'number' || feeRate <= 0 || !Number.isFinite(feeRate)) {
820
- throw new Error('Invalid feeRate: must be a positive number');
821
- }
822
- if (feeRate < 1 || feeRate > 1000000) {
823
- throw new Error('Invalid feeRate: must be between 1 and 1000000 sat/vB');
824
- }
825
- }
826
-
827
- if (typeof (asset as any).migrate !== 'function') {
828
- throw new Error('Not implemented');
829
- }
830
- if (asset.currentLayer !== 'did:webvh' && asset.currentLayer !== 'did:peer') {
831
- throw new Error('Not implemented');
832
- }
833
- const bitcoinManager = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
834
- const manifest = {
835
- assetId: asset.id,
836
- resources: asset.resources.map(res => ({ id: res.id, hash: res.hash, contentType: res.contentType, url: res.url })),
837
- timestamp: new Date().toISOString()
838
- };
839
- const payload = Buffer.from(JSON.stringify(manifest));
840
- const inscription: any = await bitcoinManager.inscribeData(payload, 'application/json', feeRate);
841
- const revealTxId = inscription.revealTxId ?? inscription.txid;
842
- const commitTxId = inscription.commitTxId;
843
- const usedFeeRate = typeof inscription.feeRate === 'number' ? inscription.feeRate : feeRate;
844
-
845
- // Capture the layer before migration for accurate metrics
846
- const fromLayer = asset.currentLayer;
847
-
848
- await asset.migrate('did:btco', {
849
- transactionId: revealTxId,
850
- inscriptionId: inscription.inscriptionId,
851
- satoshi: inscription.satoshi,
852
- commitTxId,
853
- revealTxId,
854
- feeRate: usedFeeRate
855
- });
856
-
857
- const bindingValue = inscription.satoshi
858
- ? `did:btco:${inscription.satoshi}`
859
- : `did:btco:${inscription.inscriptionId}`;
860
- (asset as any).bindings = Object.assign({}, (asset as any).bindings, { 'did:btco': bindingValue });
861
-
862
- stopTimer();
863
- this.logger.info('Asset inscribed on Bitcoin successfully', {
864
- assetId: asset.id,
865
- inscriptionId: inscription.inscriptionId,
866
- transactionId: revealTxId
867
- });
868
- this.metrics.recordMigration(fromLayer, 'did:btco');
869
-
870
- return asset;
871
- } catch (error) {
872
- stopTimer();
873
- this.logger.error('Bitcoin inscription failed', error as Error, { assetId: asset.id, feeRate });
874
- this.metrics.recordError('INSCRIPTION_FAILED', 'inscribeOnBitcoin');
875
- throw error;
876
- }
877
- }
878
-
879
- async transferOwnership(
880
- asset: OriginalsAsset,
881
- newOwner: string
882
- ): Promise<BitcoinTransaction> {
883
- const stopTimer = this.logger.startTimer('transferOwnership');
884
- this.logger.info('Transferring asset ownership', { assetId: asset.id, newOwner });
885
-
886
- try {
887
- // Input validation
888
- if (!asset || typeof asset !== 'object') {
889
- throw new Error('Invalid asset: must be a valid OriginalsAsset');
890
- }
891
- if (!newOwner || typeof newOwner !== 'string') {
892
- throw new Error('Invalid newOwner: must be a non-empty string');
893
- }
894
-
895
- // Validate Bitcoin address format and checksum
896
- try {
897
- validateBitcoinAddress(newOwner, this.config.network);
898
- } catch (error) {
899
- const message = error instanceof Error ? error.message : 'Invalid Bitcoin address';
900
- throw new Error(`Invalid Bitcoin address for ownership transfer: ${message}`);
901
- }
902
-
903
- // Transfer Bitcoin-anchored asset ownership
904
- // Only works for assets in did:btco layer
905
- if (asset.currentLayer !== 'did:btco') {
906
- throw new Error('Asset must be inscribed on Bitcoin before transfer');
907
- }
908
- const bm = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
909
- const provenance = asset.getProvenance();
910
- const latestMigration = provenance.migrations[provenance.migrations.length - 1];
911
- const satoshi = latestMigration?.satoshi ?? (asset.id.startsWith('did:btco:') ? asset.id.split(':')[2] : '');
912
- const inscription = {
913
- satoshi,
914
- inscriptionId: latestMigration?.inscriptionId ?? `insc-${satoshi || 'unknown'}`,
915
- content: Buffer.alloc(0),
916
- contentType: 'application/octet-stream',
917
- txid: latestMigration?.transactionId ?? 'unknown-tx',
918
- vout: 0
919
- };
920
- const tx = await bm.transferInscription(inscription as any, newOwner);
921
- await asset.recordTransfer(asset.id, newOwner, tx.txid);
922
-
923
- stopTimer();
924
- this.logger.info('Asset ownership transferred successfully', {
925
- assetId: asset.id,
926
- newOwner,
927
- transactionId: tx.txid
928
- });
929
- this.metrics.recordTransfer();
930
-
931
- return tx;
932
- } catch (error) {
933
- stopTimer();
934
- this.logger.error('Ownership transfer failed', error as Error, { assetId: asset.id, newOwner });
935
- this.metrics.recordError('TRANSFER_FAILED', 'transferOwnership');
936
- throw error;
937
- }
938
- }
939
-
940
- /**
941
- * Create multiple assets in batch
942
- *
943
- * @param resourcesList - Array of resource arrays, one per asset to create
944
- * @param options - Batch operation options
945
- * @returns BatchResult with created assets
946
- */
947
- async batchCreateAssets(
948
- resourcesList: AssetResource[][],
949
- options?: BatchOperationOptions
950
- ): Promise<BatchResult<OriginalsAsset>> {
951
- const batchId = this.batchExecutor.generateBatchId();
952
-
953
- // Validate first if requested
954
- if (options?.validateFirst !== false) {
955
- const validationResults = this.batchValidator.validateBatchCreate(resourcesList);
956
- const invalid = validationResults.filter(r => !r.isValid);
957
- if (invalid.length > 0) {
958
- const errors = invalid.flatMap(r => r.errors).join('; ');
959
- throw new Error(`Batch validation failed: ${errors}`);
960
- }
961
- }
962
-
963
- // Emit batch:started event
964
- await this.eventEmitter.emit({
965
- type: 'batch:started',
966
- timestamp: new Date().toISOString(),
967
- operation: 'create',
968
- batchId,
969
- itemCount: resourcesList.length
970
- });
971
-
972
- try {
973
- // Use batch executor to process all asset creations
974
- const result = await this.batchExecutor.execute(
975
- resourcesList,
976
- async (resources, index) => {
977
- const asset = await this.createAsset(resources);
978
- return asset;
979
- },
980
- options,
981
- batchId // Pass the pre-generated batchId for event correlation
982
- );
983
-
984
- // Emit batch:completed event
985
- await this.eventEmitter.emit({
986
- type: 'batch:completed',
987
- timestamp: new Date().toISOString(),
988
- batchId,
989
- operation: 'create',
990
- results: {
991
- successful: result.successful.length,
992
- failed: result.failed.length,
993
- totalDuration: result.totalDuration
994
- }
995
- });
996
-
997
- return result;
998
- } catch (error) {
999
- // Emit batch:failed event
1000
- await this.eventEmitter.emit({
1001
- type: 'batch:failed',
1002
- timestamp: new Date().toISOString(),
1003
- batchId,
1004
- operation: 'create',
1005
- error: error instanceof Error ? error.message : String(error)
1006
- });
1007
-
1008
- throw error;
1009
- }
1010
- }
1011
-
1012
- /**
1013
- * Publish multiple assets to web storage in batch
1014
- *
1015
- * @param assets - Array of assets to publish
1016
- * @param domain - Domain to publish to
1017
- * @param options - Batch operation options
1018
- * @returns BatchResult with published assets
1019
- */
1020
- async batchPublishToWeb(
1021
- assets: OriginalsAsset[],
1022
- domain: string,
1023
- options?: BatchOperationOptions
1024
- ): Promise<BatchResult<OriginalsAsset>> {
1025
- const batchId = this.batchExecutor.generateBatchId();
1026
-
1027
- // Validate domain once
1028
- if (!domain || typeof domain !== 'string') {
1029
- throw new Error('Invalid domain: must be a non-empty string');
1030
- }
1031
-
1032
- const normalized = domain.trim().toLowerCase();
1033
-
1034
- // Split domain and port if present
1035
- const [domainPart, portPart] = normalized.split(':');
1036
-
1037
- // Validate port if present
1038
- if (portPart && (!/^\d+$/.test(portPart) || parseInt(portPart) < 1 || parseInt(portPart) > 65535)) {
1039
- throw new Error(`Invalid domain format: ${domain} - invalid port`);
1040
- }
1041
-
1042
- // Allow localhost and IP addresses for development
1043
- const isLocalhost = domainPart === 'localhost';
1044
- const isIP = /^(\d{1,3}\.){3}\d{1,3}$/.test(domainPart);
1045
-
1046
- if (!isLocalhost && !isIP) {
1047
- // For non-localhost domains, require proper domain format
1048
- const label = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
1049
- const domainRegex = new RegExp(`^(?=.{1,253}$)(?:${label})(?:\\.(?:${label}))+?$`, 'i');
1050
- if (!domainRegex.test(domainPart)) {
1051
- throw new Error(`Invalid domain format: ${domain}`);
1052
- }
1053
- }
1054
-
1055
- // Emit batch:started event
1056
- await this.eventEmitter.emit({
1057
- type: 'batch:started',
1058
- timestamp: new Date().toISOString(),
1059
- operation: 'publish',
1060
- batchId,
1061
- itemCount: assets.length
1062
- });
1063
-
1064
- try {
1065
- const result = await this.batchExecutor.execute(
1066
- assets,
1067
- async (asset, index) => {
1068
- return await this.publishToWeb(asset, domain);
1069
- },
1070
- options,
1071
- batchId // Pass the pre-generated batchId for event correlation
1072
- );
1073
-
1074
- // Emit batch:completed event
1075
- await this.eventEmitter.emit({
1076
- type: 'batch:completed',
1077
- timestamp: new Date().toISOString(),
1078
- batchId,
1079
- operation: 'publish',
1080
- results: {
1081
- successful: result.successful.length,
1082
- failed: result.failed.length,
1083
- totalDuration: result.totalDuration
1084
- }
1085
- });
1086
-
1087
- return result;
1088
- } catch (error) {
1089
- // Emit batch:failed event
1090
- await this.eventEmitter.emit({
1091
- type: 'batch:failed',
1092
- timestamp: new Date().toISOString(),
1093
- batchId,
1094
- operation: 'publish',
1095
- error: error instanceof Error ? error.message : String(error)
1096
- });
1097
-
1098
- throw error;
1099
- }
1100
- }
1101
-
1102
- /**
1103
- * Inscribe multiple assets on Bitcoin with cost optimization
1104
- * KEY FEATURE: singleTransaction option for 30%+ cost savings
1105
- *
1106
- * @param assets - Array of assets to inscribe
1107
- * @param options - Batch inscription options
1108
- * @returns BatchResult with inscribed assets
1109
- */
1110
- async batchInscribeOnBitcoin(
1111
- assets: OriginalsAsset[],
1112
- options?: BatchInscriptionOptions
1113
- ): Promise<BatchResult<OriginalsAsset>> {
1114
- // Validate first if requested
1115
- if (options?.validateFirst !== false) {
1116
- const validationResults = this.batchValidator.validateBatchInscription(assets);
1117
- const invalid = validationResults.filter(r => !r.isValid);
1118
- if (invalid.length > 0) {
1119
- const errors = invalid.flatMap(r => r.errors).join('; ');
1120
- throw new Error(`Batch validation failed: ${errors}`);
1121
- }
1122
- }
1123
-
1124
- if (options?.singleTransaction) {
1125
- return this.batchInscribeSingleTransaction(assets, options);
1126
- } else {
1127
- return this.batchInscribeIndividualTransactions(assets, options);
1128
- }
1129
- }
1130
-
1131
- /**
1132
- * CORE INNOVATION: Single-transaction batch inscription
1133
- * Combines multiple assets into one Bitcoin transaction for 30%+ cost savings
1134
- *
1135
- * @param assets - Array of assets to inscribe
1136
- * @param options - Batch inscription options
1137
- * @returns BatchResult with inscribed assets and cost savings data
1138
- */
1139
- private async batchInscribeSingleTransaction(
1140
- assets: OriginalsAsset[],
1141
- options?: BatchInscriptionOptions
1142
- ): Promise<BatchResult<OriginalsAsset>> {
1143
- const batchId = this.batchExecutor.generateBatchId();
1144
- const startTime = Date.now();
1145
- const startedAt = new Date().toISOString();
1146
-
1147
- // Emit batch:started event
1148
- await this.eventEmitter.emit({
1149
- type: 'batch:started',
1150
- timestamp: startedAt,
1151
- operation: 'inscribe',
1152
- batchId,
1153
- itemCount: assets.length
1154
- });
1155
-
1156
- try {
1157
- // Calculate total data size for all assets
1158
- const totalDataSize = this.calculateTotalDataSize(assets);
1159
-
1160
- // Estimate savings from batch inscription
1161
- const estimatedSavings = await this.estimateBatchSavings(assets, options?.feeRate);
1162
-
1163
- // Create manifests for all assets
1164
- const manifests = assets.map(asset => ({
1165
- assetId: asset.id,
1166
- resources: asset.resources.map(res => ({
1167
- id: res.id,
1168
- hash: res.hash,
1169
- contentType: res.contentType,
1170
- url: res.url
1171
- })),
1172
- timestamp: new Date().toISOString()
1173
- }));
1174
-
1175
- // Combine all manifests into a single batch payload
1176
- const batchManifest = {
1177
- batchId,
1178
- assets: manifests,
1179
- timestamp: new Date().toISOString()
1180
- };
1181
-
1182
- const payload = Buffer.from(JSON.stringify(batchManifest));
1183
-
1184
- // Inscribe the batch manifest as a single transaction
1185
- const bitcoinManager = this.deps?.bitcoinManager ?? new BitcoinManager(this.config);
1186
- const inscription: any = await bitcoinManager.inscribeData(
1187
- payload,
1188
- 'application/json',
1189
- options?.feeRate
1190
- );
1191
-
1192
- const revealTxId = inscription.revealTxId ?? inscription.txid;
1193
- const commitTxId = inscription.commitTxId;
1194
- const usedFeeRate = typeof inscription.feeRate === 'number' ? inscription.feeRate : options?.feeRate;
1195
-
1196
- // Calculate fee per asset (split proportionally by data size)
1197
- // Include both metadata and resource content size for accurate fee distribution
1198
- const assetSizes = assets.map(asset => {
1199
- // Calculate metadata size
1200
- const metadataSize = JSON.stringify({
1201
- assetId: asset.id,
1202
- resources: asset.resources.map(r => ({
1203
- id: r.id,
1204
- hash: r.hash,
1205
- contentType: r.contentType,
1206
- url: r.url
1207
- }))
1208
- }).length;
1209
-
1210
- // Add resource content sizes
1211
- const contentSize = asset.resources.reduce((sum, r) => {
1212
- const content = (r as any).content;
1213
- if (content) {
1214
- return sum + (typeof content === 'string' ? Buffer.byteLength(content) : content.length || 0);
1215
- }
1216
- return sum;
1217
- }, 0);
1218
-
1219
- return metadataSize + contentSize;
1220
- });
1221
- const totalSize = assetSizes.reduce((sum, size) => sum + size, 0);
1222
-
1223
- // Calculate total fee from batch transaction size and fee rate
1224
- // Estimate transaction size: base overhead (200 bytes) + batch payload size
1225
- const batchTxSize = 200 + totalDataSize;
1226
- const effectiveFeeRate = usedFeeRate ?? 10;
1227
- const totalFee = batchTxSize * effectiveFeeRate;
1228
-
1229
- // Split fees proportionally by asset data size
1230
- const feePerAsset = assetSizes.map(size =>
1231
- Math.floor(totalFee * (size / totalSize))
1232
- );
1233
-
1234
- // Update all assets with batch inscription data
1235
- const individualInscriptionIds: string[] = [];
1236
- const successful: BatchResult<OriginalsAsset>['successful'] = [];
1237
-
1238
- for (let i = 0; i < assets.length; i++) {
1239
- const asset = assets[i];
1240
- // For batch inscriptions, use the base inscription ID for all assets
1241
- // The batch index is stored as metadata, not in the ID
1242
- const individualInscriptionId = inscription.inscriptionId;
1243
- individualInscriptionIds.push(individualInscriptionId);
1244
-
1245
- await asset.migrate('did:btco', {
1246
- transactionId: revealTxId,
1247
- inscriptionId: individualInscriptionId,
1248
- satoshi: inscription.satoshi,
1249
- commitTxId,
1250
- revealTxId,
1251
- feeRate: usedFeeRate
1252
- });
1253
-
1254
- // Add batch metadata to provenance
1255
- const provenance = asset.getProvenance();
1256
- const latestMigration = provenance.migrations[provenance.migrations.length - 1];
1257
- (latestMigration as any).batchId = batchId;
1258
- (latestMigration as any).batchInscription = true;
1259
- (latestMigration as any).batchIndex = i; // Store index as metadata
1260
- (latestMigration as any).feePaid = feePerAsset[i];
1261
-
1262
- const bindingValue = inscription.satoshi
1263
- ? `did:btco:${inscription.satoshi}`
1264
- : `did:btco:${individualInscriptionId}`;
1265
- (asset as any).bindings = Object.assign({}, (asset as any).bindings, { 'did:btco': bindingValue });
1266
-
1267
- successful.push({
1268
- index: i,
1269
- result: asset,
1270
- duration: Date.now() - startTime
1271
- });
1272
- }
1273
-
1274
- const totalDuration = Date.now() - startTime;
1275
- const completedAt = new Date().toISOString();
1276
-
1277
- // Emit batch:completed event with cost savings
1278
- await this.eventEmitter.emit({
1279
- type: 'batch:completed',
1280
- timestamp: completedAt,
1281
- batchId,
1282
- operation: 'inscribe',
1283
- results: {
1284
- successful: successful.length,
1285
- failed: 0,
1286
- totalDuration,
1287
- costSavings: {
1288
- amount: estimatedSavings.savings,
1289
- percentage: estimatedSavings.savingsPercentage
1290
- }
1291
- }
1292
- });
1293
-
1294
- return {
1295
- successful,
1296
- failed: [],
1297
- totalProcessed: assets.length,
1298
- totalDuration,
1299
- batchId,
1300
- startedAt,
1301
- completedAt
1302
- };
1303
- } catch (error) {
1304
- // Emit batch:failed event
1305
- await this.eventEmitter.emit({
1306
- type: 'batch:failed',
1307
- timestamp: new Date().toISOString(),
1308
- batchId,
1309
- operation: 'inscribe',
1310
- error: error instanceof Error ? error.message : String(error)
1311
- });
1312
-
1313
- throw new BatchError(
1314
- batchId,
1315
- 'inscribe',
1316
- { successful: 0, failed: assets.length },
1317
- error instanceof Error ? error.message : String(error)
1318
- );
1319
- }
1320
- }
1321
-
1322
- /**
1323
- * Individual transaction batch inscription (fallback mode)
1324
- * Each asset is inscribed in its own transaction
1325
- *
1326
- * @param assets - Array of assets to inscribe
1327
- * @param options - Batch inscription options
1328
- * @returns BatchResult with inscribed assets
1329
- */
1330
- private async batchInscribeIndividualTransactions(
1331
- assets: OriginalsAsset[],
1332
- options?: BatchInscriptionOptions
1333
- ): Promise<BatchResult<OriginalsAsset>> {
1334
- const batchId = this.batchExecutor.generateBatchId();
1335
-
1336
- // Emit batch:started event
1337
- await this.eventEmitter.emit({
1338
- type: 'batch:started',
1339
- timestamp: new Date().toISOString(),
1340
- operation: 'inscribe',
1341
- batchId,
1342
- itemCount: assets.length
1343
- });
1344
-
1345
- try {
1346
- const result = await this.batchExecutor.execute(
1347
- assets,
1348
- async (asset, index) => {
1349
- return await this.inscribeOnBitcoin(asset, options?.feeRate);
1350
- },
1351
- options,
1352
- batchId // Pass the pre-generated batchId for event correlation
1353
- );
1354
-
1355
- // Emit batch:completed event
1356
- await this.eventEmitter.emit({
1357
- type: 'batch:completed',
1358
- timestamp: new Date().toISOString(),
1359
- batchId,
1360
- operation: 'inscribe',
1361
- results: {
1362
- successful: result.successful.length,
1363
- failed: result.failed.length,
1364
- totalDuration: result.totalDuration
1365
- }
1366
- });
1367
-
1368
- return result;
1369
- } catch (error) {
1370
- // Emit batch:failed event
1371
- await this.eventEmitter.emit({
1372
- type: 'batch:failed',
1373
- timestamp: new Date().toISOString(),
1374
- batchId,
1375
- operation: 'inscribe',
1376
- error: error instanceof Error ? error.message : String(error)
1377
- });
1378
-
1379
- throw error;
1380
- }
1381
- }
1382
-
1383
- /**
1384
- * Transfer ownership of multiple assets in batch
1385
- *
1386
- * @param transfers - Array of transfer operations
1387
- * @param options - Batch operation options
1388
- * @returns BatchResult with transaction results
1389
- */
1390
- async batchTransferOwnership(
1391
- transfers: Array<{ asset: OriginalsAsset; to: string }>,
1392
- options?: BatchOperationOptions
1393
- ): Promise<BatchResult<BitcoinTransaction>> {
1394
- const batchId = this.batchExecutor.generateBatchId();
1395
-
1396
- // Validate first if requested
1397
- if (options?.validateFirst !== false) {
1398
- const validationResults = this.batchValidator.validateBatchTransfer(transfers);
1399
- const invalid = validationResults.filter(r => !r.isValid);
1400
- if (invalid.length > 0) {
1401
- const errors = invalid.flatMap(r => r.errors).join('; ');
1402
- throw new Error(`Batch validation failed: ${errors}`);
1403
- }
1404
-
1405
- // Validate all Bitcoin addresses
1406
- for (let i = 0; i < transfers.length; i++) {
1407
- try {
1408
- validateBitcoinAddress(transfers[i].to, this.config.network);
1409
- } catch (error) {
1410
- const message = error instanceof Error ? error.message : 'Invalid Bitcoin address';
1411
- throw new Error(`Transfer ${i}: Invalid Bitcoin address: ${message}`);
1412
- }
1413
- }
1414
- }
1415
-
1416
- // Emit batch:started event
1417
- await this.eventEmitter.emit({
1418
- type: 'batch:started',
1419
- timestamp: new Date().toISOString(),
1420
- operation: 'transfer',
1421
- batchId,
1422
- itemCount: transfers.length
1423
- });
1424
-
1425
- try {
1426
- const result = await this.batchExecutor.execute(
1427
- transfers,
1428
- async (transfer, index) => {
1429
- return await this.transferOwnership(transfer.asset, transfer.to);
1430
- },
1431
- options,
1432
- batchId // Pass the pre-generated batchId for event correlation
1433
- );
1434
-
1435
- // Emit batch:completed event
1436
- await this.eventEmitter.emit({
1437
- type: 'batch:completed',
1438
- timestamp: new Date().toISOString(),
1439
- batchId,
1440
- operation: 'transfer',
1441
- results: {
1442
- successful: result.successful.length,
1443
- failed: result.failed.length,
1444
- totalDuration: result.totalDuration
1445
- }
1446
- });
1447
-
1448
- return result;
1449
- } catch (error) {
1450
- // Emit batch:failed event
1451
- await this.eventEmitter.emit({
1452
- type: 'batch:failed',
1453
- timestamp: new Date().toISOString(),
1454
- batchId,
1455
- operation: 'transfer',
1456
- error: error instanceof Error ? error.message : String(error)
1457
- });
1458
-
1459
- throw error;
1460
- }
1461
- }
1462
-
1463
- /**
1464
- * Calculate total data size for all assets in a batch
1465
- */
1466
- private calculateTotalDataSize(assets: OriginalsAsset[]): number {
1467
- return assets.reduce((total, asset) => {
1468
- const manifest = {
1469
- assetId: asset.id,
1470
- resources: asset.resources.map(res => ({
1471
- id: res.id,
1472
- hash: res.hash,
1473
- contentType: res.contentType,
1474
- url: res.url
1475
- })),
1476
- timestamp: new Date().toISOString()
1477
- };
1478
- return total + JSON.stringify(manifest).length;
1479
- }, 0);
1480
- }
1481
-
1482
- /**
1483
- * Estimate cost savings from batch inscription vs individual inscriptions
1484
- */
1485
- private async estimateBatchSavings(
1486
- assets: OriginalsAsset[],
1487
- feeRate?: number
1488
- ): Promise<{
1489
- batchFee: number;
1490
- individualFees: number;
1491
- savings: number;
1492
- savingsPercentage: number;
1493
- }> {
1494
- // Calculate total size for batch
1495
- const batchSize = this.calculateTotalDataSize(assets);
1496
-
1497
- // Estimate individual sizes
1498
- const individualSizes = assets.map(asset =>
1499
- JSON.stringify({
1500
- assetId: asset.id,
1501
- resources: asset.resources.map(r => ({
1502
- id: r.id,
1503
- hash: r.hash,
1504
- contentType: r.contentType,
1505
- url: r.url
1506
- }))
1507
- }).length
1508
- );
1509
-
1510
- // Realistic fee estimation based on Bitcoin transaction structure
1511
- // Base transaction overhead: ~200 bytes (inputs, outputs, etc.)
1512
- // Per inscription witness overhead: ~120 bytes (script, envelope, etc.)
1513
- // In batch mode: shared transaction overhead + minimal per-asset overhead
1514
- const effectiveFeeRate = feeRate ?? 10; // default 10 sat/vB
1515
-
1516
- // Batch: one transaction overhead + batch data + minimal per-asset overhead
1517
- // The batch manifest is more efficient as it shares structure
1518
- const batchTxSize = 200 + batchSize + (assets.length * 5); // 5 bytes per asset for array/object overhead
1519
- const batchFee = batchTxSize * effectiveFeeRate;
1520
-
1521
- // Individual: each inscription needs full transaction overhead + witness overhead + data
1522
- const individualFees = individualSizes.reduce((total, size) => {
1523
- // Each individual inscription has:
1524
- // - Full transaction overhead: 200 bytes
1525
- // - Witness/inscription overhead: 122 bytes
1526
- // - Asset data: size bytes
1527
- const txSize = 200 + 122 + size;
1528
- return total + (txSize * effectiveFeeRate);
1529
- }, 0);
1530
-
1531
- const savings = individualFees - batchFee;
1532
- const savingsPercentage = (savings / individualFees) * 100;
1533
-
1534
- return {
1535
- batchFee,
1536
- individualFees,
1537
- savings,
1538
- savingsPercentage
1539
- };
1540
- }
1541
-
1542
- // ===== Clean Lifecycle API =====
1543
- // These methods provide a cleaner, more intuitive API while maintaining
1544
- // backward compatibility with the existing methods.
1545
-
1546
- /**
1547
- * Create a draft asset (did:peer layer)
1548
- *
1549
- * This is the entry point for creating new Originals. Draft assets are
1550
- * stored locally and can be published or inscribed later.
1551
- *
1552
- * @param resources - Array of resources to include in the asset
1553
- * @param options - Optional configuration including progress callback
1554
- * @returns The newly created OriginalsAsset in did:peer layer
1555
- *
1556
- * @example
1557
- * ```typescript
1558
- * const draft = await sdk.lifecycle.createDraft([
1559
- * { id: 'main', type: 'code', contentType: 'text/javascript', hash: '...' }
1560
- * ], {
1561
- * onProgress: (p) => console.log(p.message)
1562
- * });
1563
- * ```
1564
- */
1565
- async createDraft(
1566
- resources: AssetResource[],
1567
- options?: LifecycleOperationOptions
1568
- ): Promise<OriginalsAsset> {
1569
- const onProgress = options?.onProgress;
1570
-
1571
- onProgress?.({
1572
- phase: 'preparing',
1573
- percentage: 0,
1574
- message: 'Preparing draft asset...'
1575
- });
1576
-
1577
- onProgress?.({
1578
- phase: 'validating',
1579
- percentage: 20,
1580
- message: 'Validating resources...'
1581
- });
1582
-
1583
- try {
1584
- onProgress?.({
1585
- phase: 'processing',
1586
- percentage: 50,
1587
- message: 'Creating DID document...'
1588
- });
1589
-
1590
- const asset = await this.createAsset(resources);
1591
-
1592
- onProgress?.({
1593
- phase: 'complete',
1594
- percentage: 100,
1595
- message: 'Draft asset created successfully'
1596
- });
1597
-
1598
- return asset;
1599
- } catch (error) {
1600
- onProgress?.({
1601
- phase: 'failed',
1602
- percentage: 0,
1603
- message: `Failed to create draft: ${error instanceof Error ? error.message : String(error)}`
1604
- });
1605
- throw error;
1606
- }
1607
- }
1608
-
1609
- /**
1610
- * Publish an asset to the web (did:webvh layer)
1611
- *
1612
- * Migrates a draft asset from did:peer to did:webvh, making it publicly
1613
- * discoverable via HTTPS.
1614
- *
1615
- * @param asset - The asset to publish (must be in did:peer layer)
1616
- * @param publisherDidOrSigner - Publisher's DID or external signer
1617
- * @param options - Optional configuration including progress callback
1618
- * @returns The published OriginalsAsset in did:webvh layer
1619
- *
1620
- * @example
1621
- * ```typescript
1622
- * const published = await sdk.lifecycle.publish(draft, 'did:webvh:example.com:user');
1623
- * ```
1624
- */
1625
- async publish(
1626
- asset: OriginalsAsset,
1627
- publisherDidOrSigner: string | ExternalSigner,
1628
- options?: LifecycleOperationOptions
1629
- ): Promise<OriginalsAsset> {
1630
- const onProgress = options?.onProgress;
1631
-
1632
- onProgress?.({
1633
- phase: 'preparing',
1634
- percentage: 0,
1635
- message: 'Preparing to publish...'
1636
- });
1637
-
1638
- onProgress?.({
1639
- phase: 'validating',
1640
- percentage: 10,
1641
- message: 'Validating migration...'
1642
- });
1643
-
1644
- // Pre-flight validation
1645
- const validation = await this.validateMigration(asset, 'did:webvh');
1646
- if (!validation.valid) {
1647
- onProgress?.({
1648
- phase: 'failed',
1649
- percentage: 0,
1650
- message: `Validation failed: ${validation.errors.join(', ')}`
1651
- });
1652
- throw new Error(`Migration validation failed: ${validation.errors.join(', ')}`);
1653
- }
1654
-
1655
- try {
1656
- onProgress?.({
1657
- phase: 'processing',
1658
- percentage: 30,
1659
- message: 'Publishing resources...'
1660
- });
1661
-
1662
- onProgress?.({
1663
- phase: 'committing',
1664
- percentage: 70,
1665
- message: 'Finalizing publication...'
1666
- });
1667
-
1668
- const published = await this.publishToWeb(asset, publisherDidOrSigner);
1669
-
1670
- onProgress?.({
1671
- phase: 'complete',
1672
- percentage: 100,
1673
- message: 'Asset published successfully'
1674
- });
1675
-
1676
- return published;
1677
- } catch (error) {
1678
- onProgress?.({
1679
- phase: 'failed',
1680
- percentage: 0,
1681
- message: `Failed to publish: ${error instanceof Error ? error.message : String(error)}`
1682
- });
1683
- throw error;
1684
- }
1685
- }
1686
-
1687
- /**
1688
- * Inscribe an asset on Bitcoin (did:btco layer)
1689
- *
1690
- * Permanently anchors an asset on the Bitcoin blockchain via Ordinals inscription.
1691
- * This is an irreversible operation.
1692
- *
1693
- * @param asset - The asset to inscribe (must be in did:peer or did:webvh layer)
1694
- * @param options - Optional configuration including fee rate and progress callback
1695
- * @returns The inscribed OriginalsAsset in did:btco layer
1696
- *
1697
- * @example
1698
- * ```typescript
1699
- * const inscribed = await sdk.lifecycle.inscribe(published, {
1700
- * feeRate: 15,
1701
- * onProgress: (p) => console.log(`${p.percentage}%: ${p.message}`)
1702
- * });
1703
- * ```
1704
- */
1705
- async inscribe(
1706
- asset: OriginalsAsset,
1707
- options?: LifecycleOperationOptions
1708
- ): Promise<OriginalsAsset> {
1709
- const onProgress = options?.onProgress;
1710
- const feeRate = options?.feeRate;
1711
-
1712
- onProgress?.({
1713
- phase: 'preparing',
1714
- percentage: 0,
1715
- message: 'Preparing inscription...'
1716
- });
1717
-
1718
- onProgress?.({
1719
- phase: 'validating',
1720
- percentage: 10,
1721
- message: 'Validating migration...'
1722
- });
1723
-
1724
- // Pre-flight validation
1725
- const validation = await this.validateMigration(asset, 'did:btco');
1726
- if (!validation.valid) {
1727
- onProgress?.({
1728
- phase: 'failed',
1729
- percentage: 0,
1730
- message: `Validation failed: ${validation.errors.join(', ')}`
1731
- });
1732
- throw new Error(`Migration validation failed: ${validation.errors.join(', ')}`);
1733
- }
1734
-
1735
- // Show cost estimate
1736
- if (onProgress) {
1737
- const estimate = await this.estimateCost(asset, 'did:btco', feeRate);
1738
- onProgress({
1739
- phase: 'preparing',
1740
- percentage: 20,
1741
- message: `Estimated cost: ${estimate.totalSats} sats (${estimate.feeRate} sat/vB)`
1742
- });
1743
- }
1744
-
1745
- try {
1746
- onProgress?.({
1747
- phase: 'processing',
1748
- percentage: 30,
1749
- message: 'Creating commit transaction...',
1750
- details: { currentStep: 1, totalSteps: 3 }
1751
- });
1752
-
1753
- onProgress?.({
1754
- phase: 'committing',
1755
- percentage: 60,
1756
- message: 'Broadcasting reveal transaction...',
1757
- details: { currentStep: 2, totalSteps: 3 }
1758
- });
1759
-
1760
- const inscribed = await this.inscribeOnBitcoin(asset, feeRate);
1761
-
1762
- onProgress?.({
1763
- phase: 'confirming',
1764
- percentage: 90,
1765
- message: 'Waiting for confirmation...',
1766
- details: { currentStep: 3, totalSteps: 3 }
1767
- });
1768
-
1769
- onProgress?.({
1770
- phase: 'complete',
1771
- percentage: 100,
1772
- message: 'Asset inscribed successfully'
1773
- });
1774
-
1775
- return inscribed;
1776
- } catch (error) {
1777
- onProgress?.({
1778
- phase: 'failed',
1779
- percentage: 0,
1780
- message: `Failed to inscribe: ${error instanceof Error ? error.message : String(error)}`
1781
- });
1782
- throw error;
1783
- }
1784
- }
1785
-
1786
- /**
1787
- * Transfer ownership of a Bitcoin-inscribed asset
1788
- *
1789
- * Transfers an inscribed asset to a new owner. Only works for assets
1790
- * in the did:btco layer.
1791
- *
1792
- * @param asset - The asset to transfer (must be in did:btco layer)
1793
- * @param newOwnerAddress - Bitcoin address of the new owner
1794
- * @param options - Optional configuration including progress callback
1795
- * @returns The Bitcoin transaction for the transfer
1796
- *
1797
- * @example
1798
- * ```typescript
1799
- * const tx = await sdk.lifecycle.transfer(inscribed, 'bc1q...newowner');
1800
- * console.log('Transfer txid:', tx.txid);
1801
- * ```
1802
- */
1803
- async transfer(
1804
- asset: OriginalsAsset,
1805
- newOwnerAddress: string,
1806
- options?: LifecycleOperationOptions
1807
- ): Promise<BitcoinTransaction> {
1808
- const onProgress = options?.onProgress;
1809
-
1810
- onProgress?.({
1811
- phase: 'preparing',
1812
- percentage: 0,
1813
- message: 'Preparing transfer...'
1814
- });
1815
-
1816
- onProgress?.({
1817
- phase: 'validating',
1818
- percentage: 10,
1819
- message: 'Validating transfer...'
1820
- });
1821
-
1822
- // Validate asset is in correct layer
1823
- if (asset.currentLayer !== 'did:btco') {
1824
- onProgress?.({
1825
- phase: 'failed',
1826
- percentage: 0,
1827
- message: 'Asset must be inscribed on Bitcoin before transfer'
1828
- });
1829
- throw new Error('Asset must be inscribed on Bitcoin before transfer');
1830
- }
1831
-
1832
- try {
1833
- onProgress?.({
1834
- phase: 'processing',
1835
- percentage: 30,
1836
- message: 'Creating transfer transaction...'
1837
- });
1838
-
1839
- onProgress?.({
1840
- phase: 'committing',
1841
- percentage: 60,
1842
- message: 'Broadcasting transaction...'
1843
- });
1844
-
1845
- const tx = await this.transferOwnership(asset, newOwnerAddress);
1846
-
1847
- onProgress?.({
1848
- phase: 'confirming',
1849
- percentage: 90,
1850
- message: 'Waiting for confirmation...',
1851
- details: { transactionId: tx.txid }
1852
- });
1853
-
1854
- onProgress?.({
1855
- phase: 'complete',
1856
- percentage: 100,
1857
- message: 'Transfer complete',
1858
- details: { transactionId: tx.txid }
1859
- });
1860
-
1861
- return tx;
1862
- } catch (error) {
1863
- onProgress?.({
1864
- phase: 'failed',
1865
- percentage: 0,
1866
- message: `Failed to transfer: ${error instanceof Error ? error.message : String(error)}`
1867
- });
1868
- throw error;
1869
- }
1870
- }
1871
-
1872
- // ===== Cost Estimation =====
1873
-
1874
- /**
1875
- * Estimate the cost of migrating an asset to a target layer
1876
- *
1877
- * Returns a detailed breakdown of expected costs for Bitcoin operations.
1878
- * For did:webvh migrations, costs are minimal (only hosting).
1879
- *
1880
- * @param asset - The asset to estimate costs for
1881
- * @param targetLayer - The target layer for migration
1882
- * @param feeRate - Optional fee rate override (sat/vB)
1883
- * @returns Detailed cost estimate
1884
- *
1885
- * @example
1886
- * ```typescript
1887
- * const cost = await sdk.lifecycle.estimateCost(draft, 'did:btco', 10);
1888
- * console.log(`Estimated cost: ${cost.totalSats} sats`);
1889
- * ```
1890
- */
1891
- async estimateCost(
1892
- asset: OriginalsAsset,
1893
- targetLayer: LayerType,
1894
- feeRate?: number
1895
- ): Promise<CostEstimate> {
1896
- // For webvh, costs are minimal (just hosting costs not applicable here)
1897
- if (targetLayer === 'did:webvh') {
1898
- return {
1899
- totalSats: 0,
1900
- breakdown: {
1901
- networkFee: 0,
1902
- dataCost: 0,
1903
- dustValue: 0
1904
- },
1905
- feeRate: 0,
1906
- dataSize: 0,
1907
- targetLayer,
1908
- confidence: 'high'
1909
- };
1910
- }
1911
-
1912
- // For btco, calculate inscription costs
1913
- if (targetLayer === 'did:btco') {
1914
- // Calculate manifest size
1915
- const manifest = {
1916
- assetId: asset.id,
1917
- resources: asset.resources.map(res => ({
1918
- id: res.id,
1919
- hash: res.hash,
1920
- contentType: res.contentType,
1921
- url: res.url
1922
- })),
1923
- timestamp: new Date().toISOString()
1924
- };
1925
- const dataSize = Buffer.from(JSON.stringify(manifest)).length;
1926
-
1927
- // Get fee rate from oracle or use provided/default
1928
- let effectiveFeeRate = feeRate;
1929
- let confidence: 'low' | 'medium' | 'high' = 'medium';
1930
-
1931
- if (!effectiveFeeRate) {
1932
- // Try to get from fee oracle
1933
- if (this.config.feeOracle) {
1934
- try {
1935
- effectiveFeeRate = await this.config.feeOracle.estimateFeeRate(1);
1936
- confidence = 'high';
1937
- } catch {
1938
- // Fallback to default
1939
- }
1940
- }
1941
-
1942
- // Try ordinals provider
1943
- if (!effectiveFeeRate && this.config.ordinalsProvider) {
1944
- try {
1945
- effectiveFeeRate = await this.config.ordinalsProvider.estimateFee(1);
1946
- confidence = 'medium';
1947
- } catch {
1948
- // Fallback to default
1949
- }
1950
- }
1951
-
1952
- // Use default if no oracle available
1953
- if (!effectiveFeeRate) {
1954
- effectiveFeeRate = 10; // Conservative default
1955
- confidence = 'low';
1956
- }
1957
- }
1958
-
1959
- // Transaction structure estimation:
1960
- // - Commit transaction: ~200 vB base + input overhead
1961
- // - Reveal transaction: ~200 vB base + inscription envelope + data
1962
- // Inscription envelope overhead: ~122 bytes
1963
- const commitTxSize = 200;
1964
- const revealTxSize = 200 + 122 + dataSize;
1965
- const totalSize = commitTxSize + revealTxSize;
1966
-
1967
- const networkFee = totalSize * effectiveFeeRate;
1968
- const dustValue = 546; // Standard dust limit for P2TR
1969
- const totalSats = networkFee + dustValue;
1970
-
1971
- return {
1972
- totalSats,
1973
- breakdown: {
1974
- networkFee,
1975
- dataCost: dataSize * effectiveFeeRate,
1976
- dustValue
1977
- },
1978
- feeRate: effectiveFeeRate,
1979
- dataSize,
1980
- targetLayer,
1981
- confidence
1982
- };
1983
- }
1984
-
1985
- // For peer layer (no migration needed)
1986
- return {
1987
- totalSats: 0,
1988
- breakdown: {
1989
- networkFee: 0,
1990
- dataCost: 0,
1991
- dustValue: 0
1992
- },
1993
- feeRate: 0,
1994
- dataSize: 0,
1995
- targetLayer,
1996
- confidence: 'high'
1997
- };
1998
- }
1999
-
2000
- // ===== Migration Validation =====
2001
-
2002
- /**
2003
- * Validate whether an asset can be migrated to a target layer
2004
- *
2005
- * Performs comprehensive pre-flight checks including:
2006
- * - Valid layer transition
2007
- * - Resource integrity
2008
- * - Credential validity
2009
- * - DID document structure
2010
- * - Bitcoin readiness (for did:btco)
2011
- *
2012
- * @param asset - The asset to validate
2013
- * @param targetLayer - The target layer for migration
2014
- * @returns Detailed validation result
2015
- *
2016
- * @example
2017
- * ```typescript
2018
- * const validation = await sdk.lifecycle.validateMigration(draft, 'did:webvh');
2019
- * if (!validation.valid) {
2020
- * console.error('Cannot migrate:', validation.errors);
2021
- * }
2022
- * ```
2023
- */
2024
- async validateMigration(
2025
- asset: OriginalsAsset,
2026
- targetLayer: LayerType
2027
- ): Promise<MigrationValidation> {
2028
- const errors: string[] = [];
2029
- const warnings: string[] = [];
2030
- const checks = {
2031
- layerTransition: false,
2032
- resourcesValid: false,
2033
- credentialsValid: false,
2034
- didDocumentValid: false,
2035
- bitcoinReadiness: undefined as boolean | undefined
2036
- };
2037
-
2038
- // Check layer transition validity
2039
- const validTransitions: Record<LayerType, LayerType[]> = {
2040
- 'did:peer': ['did:webvh', 'did:btco'],
2041
- 'did:webvh': ['did:btco'],
2042
- 'did:btco': []
2043
- };
2044
-
2045
- if (validTransitions[asset.currentLayer].includes(targetLayer)) {
2046
- checks.layerTransition = true;
2047
- } else {
2048
- errors.push(`Invalid migration from ${asset.currentLayer} to ${targetLayer}`);
2049
- }
2050
-
2051
- // Validate resources
2052
- if (asset.resources.length === 0) {
2053
- errors.push('Asset must have at least one resource');
2054
- } else {
2055
- let resourcesValid = true;
2056
- for (const resource of asset.resources) {
2057
- if (!resource.id || !resource.type || !resource.contentType || !resource.hash) {
2058
- resourcesValid = false;
2059
- errors.push(`Resource ${resource.id || 'unknown'} is missing required fields`);
2060
- }
2061
- if (resource.hash && !/^[0-9a-fA-F]+$/.test(resource.hash)) {
2062
- resourcesValid = false;
2063
- errors.push(`Resource ${resource.id} has invalid hash format`);
2064
- }
2065
- }
2066
- checks.resourcesValid = resourcesValid;
2067
- }
2068
-
2069
- // Validate DID document
2070
- if (asset.did && asset.did.id) {
2071
- checks.didDocumentValid = true;
2072
- } else {
2073
- errors.push('Asset has invalid or missing DID document');
2074
- }
2075
-
2076
- // Validate credentials (structural check)
2077
- if (asset.credentials.length > 0) {
2078
- let credentialsValid = true;
2079
- for (const cred of asset.credentials) {
2080
- if (!cred.type || !cred.issuer || !cred.issuanceDate) {
2081
- credentialsValid = false;
2082
- warnings.push('Asset has credentials with missing fields');
2083
- }
2084
- }
2085
- checks.credentialsValid = credentialsValid;
2086
- } else {
2087
- checks.credentialsValid = true; // No credentials is valid
2088
- }
2089
-
2090
- // Bitcoin-specific checks
2091
- if (targetLayer === 'did:btco') {
2092
- checks.bitcoinReadiness = true;
2093
-
2094
- // Check if ordinals provider is configured
2095
- if (!this.config.ordinalsProvider) {
2096
- checks.bitcoinReadiness = false;
2097
- errors.push('Bitcoin inscription requires an ordinalsProvider to be configured');
2098
- }
2099
-
2100
- // Warn about large data sizes
2101
- const manifestSize = JSON.stringify({
2102
- assetId: asset.id,
2103
- resources: asset.resources.map(r => ({
2104
- id: r.id,
2105
- hash: r.hash,
2106
- contentType: r.contentType
2107
- }))
2108
- }).length;
2109
-
2110
- if (manifestSize > 100000) {
2111
- warnings.push(`Large manifest size (${manifestSize} bytes) may result in high inscription costs`);
2112
- }
2113
- }
2114
-
2115
- return {
2116
- valid: errors.length === 0,
2117
- errors,
2118
- warnings,
2119
- currentLayer: asset.currentLayer,
2120
- targetLayer,
2121
- checks
2122
- };
2123
- }
2124
- }
2125
-
2126
-