@originals/sdk 1.8.1 → 1.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (145) hide show
  1. package/dist/utils/hash.js +1 -0
  2. package/package.json +6 -5
  3. package/src/adapters/FeeOracleMock.ts +9 -0
  4. package/src/adapters/index.ts +5 -0
  5. package/src/adapters/providers/OrdHttpProvider.ts +126 -0
  6. package/src/adapters/providers/OrdMockProvider.ts +101 -0
  7. package/src/adapters/types.ts +66 -0
  8. package/src/bitcoin/BitcoinManager.ts +329 -0
  9. package/src/bitcoin/BroadcastClient.ts +54 -0
  10. package/src/bitcoin/OrdinalsClient.ts +120 -0
  11. package/src/bitcoin/PSBTBuilder.ts +106 -0
  12. package/src/bitcoin/fee-calculation.ts +38 -0
  13. package/src/bitcoin/providers/OrdNodeProvider.ts +92 -0
  14. package/src/bitcoin/providers/OrdinalsProvider.ts +56 -0
  15. package/src/bitcoin/providers/types.ts +59 -0
  16. package/src/bitcoin/transactions/commit.ts +465 -0
  17. package/src/bitcoin/transactions/index.ts +13 -0
  18. package/src/bitcoin/transfer.ts +43 -0
  19. package/src/bitcoin/utxo-selection.ts +322 -0
  20. package/src/bitcoin/utxo.ts +113 -0
  21. package/src/cel/ExternalReferenceManager.ts +87 -0
  22. package/src/cel/OriginalsCel.ts +460 -0
  23. package/src/cel/algorithms/createEventLog.ts +68 -0
  24. package/src/cel/algorithms/deactivateEventLog.ts +109 -0
  25. package/src/cel/algorithms/index.ts +11 -0
  26. package/src/cel/algorithms/updateEventLog.ts +99 -0
  27. package/src/cel/algorithms/verifyEventLog.ts +306 -0
  28. package/src/cel/algorithms/witnessEvent.ts +87 -0
  29. package/src/cel/cli/create.ts +330 -0
  30. package/src/cel/cli/index.ts +383 -0
  31. package/src/cel/cli/inspect.ts +549 -0
  32. package/src/cel/cli/migrate.ts +473 -0
  33. package/src/cel/cli/verify.ts +249 -0
  34. package/src/cel/hash.ts +71 -0
  35. package/src/cel/index.ts +16 -0
  36. package/src/cel/layers/BtcoCelManager.ts +408 -0
  37. package/src/cel/layers/PeerCelManager.ts +371 -0
  38. package/src/cel/layers/WebVHCelManager.ts +361 -0
  39. package/src/cel/layers/index.ts +27 -0
  40. package/src/cel/serialization/cbor.ts +189 -0
  41. package/src/cel/serialization/index.ts +10 -0
  42. package/src/cel/serialization/json.ts +209 -0
  43. package/src/cel/types.ts +160 -0
  44. package/src/cel/witnesses/BitcoinWitness.ts +184 -0
  45. package/src/cel/witnesses/HttpWitness.ts +241 -0
  46. package/src/cel/witnesses/WitnessService.ts +51 -0
  47. package/src/cel/witnesses/index.ts +11 -0
  48. package/src/contexts/credentials-v1.json +237 -0
  49. package/src/contexts/credentials-v2-examples.json +5 -0
  50. package/src/contexts/credentials-v2.json +340 -0
  51. package/src/contexts/credentials.json +237 -0
  52. package/src/contexts/data-integrity-v2.json +81 -0
  53. package/src/contexts/dids.json +58 -0
  54. package/src/contexts/ed255192020.json +93 -0
  55. package/src/contexts/ordinals-plus.json +23 -0
  56. package/src/contexts/originals.json +22 -0
  57. package/src/core/OriginalsSDK.ts +420 -0
  58. package/src/crypto/Multikey.ts +194 -0
  59. package/src/crypto/Signer.ts +262 -0
  60. package/src/crypto/noble-init.ts +138 -0
  61. package/src/did/BtcoDidResolver.ts +231 -0
  62. package/src/did/DIDManager.ts +705 -0
  63. package/src/did/Ed25519Verifier.ts +68 -0
  64. package/src/did/KeyManager.ts +239 -0
  65. package/src/did/WebVHManager.ts +499 -0
  66. package/src/did/createBtcoDidDocument.ts +60 -0
  67. package/src/did/providers/OrdinalsClientProviderAdapter.ts +68 -0
  68. package/src/events/EventEmitter.ts +222 -0
  69. package/src/events/index.ts +19 -0
  70. package/src/events/types.ts +331 -0
  71. package/src/examples/basic-usage.ts +78 -0
  72. package/src/examples/create-module-original.ts +435 -0
  73. package/src/examples/full-lifecycle-flow.ts +514 -0
  74. package/src/examples/run.ts +60 -0
  75. package/src/index.ts +204 -0
  76. package/src/kinds/KindRegistry.ts +320 -0
  77. package/src/kinds/index.ts +74 -0
  78. package/src/kinds/types.ts +470 -0
  79. package/src/kinds/validators/AgentValidator.ts +257 -0
  80. package/src/kinds/validators/AppValidator.ts +211 -0
  81. package/src/kinds/validators/DatasetValidator.ts +242 -0
  82. package/src/kinds/validators/DocumentValidator.ts +311 -0
  83. package/src/kinds/validators/MediaValidator.ts +269 -0
  84. package/src/kinds/validators/ModuleValidator.ts +225 -0
  85. package/src/kinds/validators/base.ts +276 -0
  86. package/src/kinds/validators/index.ts +12 -0
  87. package/src/lifecycle/BatchOperations.ts +381 -0
  88. package/src/lifecycle/LifecycleManager.ts +2156 -0
  89. package/src/lifecycle/OriginalsAsset.ts +524 -0
  90. package/src/lifecycle/ProvenanceQuery.ts +280 -0
  91. package/src/lifecycle/ResourceVersioning.ts +163 -0
  92. package/src/migration/MigrationManager.ts +587 -0
  93. package/src/migration/audit/AuditLogger.ts +176 -0
  94. package/src/migration/checkpoint/CheckpointManager.ts +112 -0
  95. package/src/migration/checkpoint/CheckpointStorage.ts +101 -0
  96. package/src/migration/index.ts +33 -0
  97. package/src/migration/operations/BaseMigration.ts +126 -0
  98. package/src/migration/operations/PeerToBtcoMigration.ts +105 -0
  99. package/src/migration/operations/PeerToWebvhMigration.ts +62 -0
  100. package/src/migration/operations/WebvhToBtcoMigration.ts +105 -0
  101. package/src/migration/rollback/RollbackManager.ts +170 -0
  102. package/src/migration/state/StateMachine.ts +92 -0
  103. package/src/migration/state/StateTracker.ts +156 -0
  104. package/src/migration/types.ts +356 -0
  105. package/src/migration/validation/BitcoinValidator.ts +107 -0
  106. package/src/migration/validation/CredentialValidator.ts +62 -0
  107. package/src/migration/validation/DIDCompatibilityValidator.ts +151 -0
  108. package/src/migration/validation/LifecycleValidator.ts +64 -0
  109. package/src/migration/validation/StorageValidator.ts +79 -0
  110. package/src/migration/validation/ValidationPipeline.ts +213 -0
  111. package/src/resources/ResourceManager.ts +655 -0
  112. package/src/resources/index.ts +21 -0
  113. package/src/resources/types.ts +202 -0
  114. package/src/storage/LocalStorageAdapter.ts +64 -0
  115. package/src/storage/MemoryStorageAdapter.ts +29 -0
  116. package/src/storage/StorageAdapter.ts +25 -0
  117. package/src/storage/index.ts +3 -0
  118. package/src/types/bitcoin.ts +98 -0
  119. package/src/types/common.ts +92 -0
  120. package/src/types/credentials.ts +89 -0
  121. package/src/types/did.ts +31 -0
  122. package/src/types/external-shims.d.ts +53 -0
  123. package/src/types/index.ts +7 -0
  124. package/src/types/network.ts +178 -0
  125. package/src/utils/EventLogger.ts +298 -0
  126. package/src/utils/Logger.ts +324 -0
  127. package/src/utils/MetricsCollector.ts +358 -0
  128. package/src/utils/bitcoin-address.ts +132 -0
  129. package/src/utils/cbor.ts +31 -0
  130. package/src/utils/encoding.ts +135 -0
  131. package/src/utils/hash.ts +12 -0
  132. package/src/utils/retry.ts +46 -0
  133. package/src/utils/satoshi-validation.ts +196 -0
  134. package/src/utils/serialization.ts +102 -0
  135. package/src/utils/telemetry.ts +44 -0
  136. package/src/utils/validation.ts +123 -0
  137. package/src/vc/CredentialManager.ts +955 -0
  138. package/src/vc/Issuer.ts +105 -0
  139. package/src/vc/Verifier.ts +54 -0
  140. package/src/vc/cryptosuites/bbs.ts +253 -0
  141. package/src/vc/cryptosuites/bbsSimple.ts +21 -0
  142. package/src/vc/cryptosuites/eddsa.ts +99 -0
  143. package/src/vc/documentLoader.ts +81 -0
  144. package/src/vc/proofs/data-integrity.ts +33 -0
  145. package/src/vc/utils/jsonld.ts +18 -0
@@ -5,6 +5,7 @@ export async function sha256Bytes(input) {
5
5
  if (!subtle) {
6
6
  throw new Error('SubtleCrypto not available in this environment');
7
7
  }
8
+ // Use type assertion to handle Uint8Array<ArrayBufferLike> compatibility with SubtleCrypto
8
9
  const digest = await subtle.digest('SHA-256', data);
9
10
  return new Uint8Array(digest);
10
11
  }
package/package.json CHANGED
@@ -1,11 +1,12 @@
1
1
  {
2
2
  "name": "@originals/sdk",
3
- "version": "1.8.1",
3
+ "version": "1.8.2",
4
4
  "description": "TypeScript SDK for the Originals Protocol - creating, discovering, and transferring digital assets with cryptographically verifiable provenance",
5
5
  "type": "module",
6
- "main": "dist/index.js",
7
- "types": "dist/index.d.ts",
6
+ "main": "src/index.ts",
7
+ "types": "src/index.ts",
8
8
  "files": [
9
+ "src",
9
10
  "dist"
10
11
  ],
11
12
  "bin": {
@@ -13,8 +14,8 @@
13
14
  },
14
15
  "exports": {
15
16
  ".": {
16
- "types": "./dist/index.d.ts",
17
- "import": "./dist/index.js"
17
+ "types": "./src/index.ts",
18
+ "import": "./src/index.ts"
18
19
  }
19
20
  },
20
21
  "scripts": {
@@ -0,0 +1,9 @@
1
+ import { FeeOracleAdapter } from './types';
2
+
3
+ export class FeeOracleMock implements FeeOracleAdapter {
4
+ constructor(private feeRate = 7) {}
5
+ estimateFeeRate(targetBlocks = 1): Promise<number> {
6
+ return Promise.resolve(Math.max(1, this.feeRate - (targetBlocks - 1)));
7
+ }
8
+ }
9
+
@@ -0,0 +1,5 @@
1
+ export * from './types';
2
+ export * from './FeeOracleMock';
3
+ export * from './providers/OrdMockProvider';
4
+ export * from './providers/OrdHttpProvider';
5
+
@@ -0,0 +1,126 @@
1
+ /* istanbul ignore file */
2
+ import type { OrdinalsProvider } from '../types';
3
+
4
+ interface HttpProviderOptions {
5
+ baseUrl: string;
6
+ }
7
+
8
+ function buildUrl(baseUrl: string, path: string): string {
9
+ const base = baseUrl.replace(/\/$/, '');
10
+ return `${base}${path.startsWith('/') ? '' : '/'}${path}`;
11
+ }
12
+
13
+ async function fetchJson<T>(url: string): Promise<T | null> {
14
+ const res = await (globalThis as any).fetch(url, {
15
+ headers: {
16
+ 'Accept': 'application/json'
17
+ }
18
+ });
19
+ if (!res.ok) return null;
20
+ return (await res.json()) as T;
21
+ }
22
+
23
+ export class OrdHttpProvider implements OrdinalsProvider {
24
+ private readonly baseUrl: string;
25
+
26
+ constructor(options: HttpProviderOptions) {
27
+ if (!options?.baseUrl) {
28
+ throw new Error('OrdHttpProvider requires baseUrl');
29
+ }
30
+ this.baseUrl = options.baseUrl;
31
+ }
32
+
33
+ async getInscriptionById(id: string) {
34
+ if (!id) return null;
35
+ const data = await fetchJson<any>(buildUrl(this.baseUrl, `/inscription/${id}`));
36
+ if (!data) return null;
37
+ // Expecting a shape similar to Ordinals indexers; adapt minimally
38
+ const ownerOutput: string | undefined = data.owner_output;
39
+ let txid = data.txid || 'unknown';
40
+ let vout = typeof data.vout === 'number' ? data.vout : 0;
41
+ if (ownerOutput && ownerOutput.includes(':')) {
42
+ const [tid, v] = ownerOutput.split(':');
43
+ txid = tid;
44
+ vout = Number(v) || 0;
45
+ }
46
+
47
+ const contentType = data.content_type || 'application/octet-stream';
48
+ const contentUrl = data.content_url || buildUrl(this.baseUrl, `/content/${id}`);
49
+ const contentRes = await (globalThis as any).fetch(contentUrl);
50
+ if (!contentRes.ok) return null;
51
+ const buf = (globalThis as any).Buffer
52
+ ? (globalThis as any).Buffer.from(new Uint8Array(await contentRes.arrayBuffer()))
53
+ : new Uint8Array(await contentRes.arrayBuffer()) as any;
54
+
55
+ return {
56
+ inscriptionId: data.inscription_id || id,
57
+ content: buf,
58
+ contentType,
59
+ txid,
60
+ vout,
61
+ satoshi: String(data.sat ?? ''),
62
+ blockHeight: data.block_height
63
+ };
64
+ }
65
+
66
+ async getInscriptionsBySatoshi(satoshi: string) {
67
+ if (!satoshi) return [];
68
+ const data = await fetchJson<any>(buildUrl(this.baseUrl, `/sat/${satoshi}`));
69
+ const ids: string[] = Array.isArray(data?.inscription_ids) ? data.inscription_ids : [];
70
+ return ids.map((inscriptionId) => ({ inscriptionId }));
71
+ }
72
+
73
+ async broadcastTransaction(_txHexOrObj: unknown): Promise<string> {
74
+ // For example purposes only, return a placeholder
75
+ return 'broadcast-txid';
76
+ }
77
+
78
+ async getTransactionStatus(_txid: string) {
79
+ return { confirmed: false };
80
+ }
81
+
82
+ async estimateFee(blocks: number = 1): Promise<number> {
83
+ // Basic fallback: some providers expose fee estimates; for example purposes, return linear estimate
84
+ return 5 * Math.max(1, blocks);
85
+ }
86
+
87
+ async createInscription(params: { data: any; contentType: string; feeRate?: number; }) {
88
+ // Example placeholder: a real implementation would POST to a service
89
+ // Here we return a deterministic mock-like result to avoid network coupling in code
90
+ const inscriptionId = `insc-${Math.random().toString(36).slice(2)}`;
91
+ const txid = `tx-${Math.random().toString(36).slice(2)}`;
92
+ return {
93
+ inscriptionId,
94
+ revealTxId: txid,
95
+ txid,
96
+ vout: 0,
97
+ blockHeight: undefined,
98
+ content: params.data,
99
+ contentType: params.contentType,
100
+ feeRate: params.feeRate
101
+ };
102
+ }
103
+
104
+ async transferInscription(inscriptionId: string, _toAddress: string, _options?: { feeRate?: number }) {
105
+ if (!inscriptionId) throw new Error('inscriptionId required');
106
+ const txid = `tx-${Math.random().toString(36).slice(2)}`;
107
+ return {
108
+ txid,
109
+ vin: [{ txid: 'prev', vout: 0 }],
110
+ vout: [{ value: 546, scriptPubKey: 'script' }],
111
+ fee: 100,
112
+ confirmations: 0
113
+ };
114
+ }
115
+ }
116
+
117
+ export async function createOrdinalsProviderFromEnv(): Promise<OrdinalsProvider> {
118
+ const useLive = String(((globalThis as any).process?.env?.USE_LIVE_ORD_PROVIDER) || '').toLowerCase() === 'true';
119
+ if (useLive) {
120
+ const baseUrl = ((globalThis as any).process?.env?.ORD_PROVIDER_BASE_URL) || 'https://ord.example.com/api';
121
+ return new OrdHttpProvider({ baseUrl });
122
+ }
123
+ const mod = await import('./OrdMockProvider');
124
+ return new mod.OrdMockProvider();
125
+ }
126
+
@@ -0,0 +1,101 @@
1
+ import { OrdinalsProvider } from '../types';
2
+
3
+ export interface OrdMockState {
4
+ inscriptionsById: Map<string, {
5
+ inscriptionId: string;
6
+ content: Buffer;
7
+ contentType: string;
8
+ txid: string;
9
+ vout: number;
10
+ satoshi?: string;
11
+ blockHeight?: number;
12
+ }>;
13
+ inscriptionsBySatoshi: Map<string, string[]>;
14
+ feeRate: number;
15
+ }
16
+
17
+ export class OrdMockProvider implements OrdinalsProvider {
18
+ private state: OrdMockState;
19
+
20
+ constructor(state?: Partial<OrdMockState>) {
21
+ this.state = {
22
+ inscriptionsById: new Map(),
23
+ inscriptionsBySatoshi: new Map(),
24
+ feeRate: 5,
25
+ ...state
26
+ } as OrdMockState;
27
+ }
28
+
29
+ async getInscriptionById(id: string) {
30
+ const rec = this.state.inscriptionsById.get(id);
31
+ return rec ? { ...rec } : null;
32
+ }
33
+
34
+ async getInscriptionsBySatoshi(satoshi: string) {
35
+ const list = this.state.inscriptionsBySatoshi.get(satoshi) || [];
36
+ return list.map((inscriptionId) => ({ inscriptionId }));
37
+ }
38
+
39
+ async broadcastTransaction(_txHexOrObj: unknown): Promise<string> {
40
+ return 'mock-broadcast-txid';
41
+ }
42
+
43
+ async getTransactionStatus(txid: string) {
44
+ return { confirmed: true, blockHeight: 1, confirmations: 1 };
45
+ }
46
+
47
+ async estimateFee(blocks = 1): Promise<number> {
48
+ return Math.max(1, this.state.feeRate - (blocks - 1));
49
+ }
50
+
51
+ async createInscription(params: { data: Buffer; contentType: string; feeRate?: number; }) {
52
+ const inscriptionId = `insc-${Math.random().toString(36).slice(2)}`;
53
+ const txid = `tx-${Math.random().toString(36).slice(2)}`;
54
+ // Generate a valid numeric satoshi identifier (not sat-123 format)
55
+ const satoshi = `${Math.floor(Math.random() * 1e12)}`;
56
+ const vout = 0;
57
+ const record = {
58
+ inscriptionId,
59
+ content: params.data,
60
+ contentType: params.contentType,
61
+ txid,
62
+ vout,
63
+ satoshi,
64
+ blockHeight: 1
65
+ };
66
+ this.state.inscriptionsById.set(inscriptionId, record);
67
+ const list = this.state.inscriptionsBySatoshi.get(satoshi) || [];
68
+ list.push(inscriptionId);
69
+ this.state.inscriptionsBySatoshi.set(satoshi, list);
70
+ return {
71
+ inscriptionId,
72
+ revealTxId: txid,
73
+ commitTxId: undefined,
74
+ satoshi,
75
+ txid,
76
+ vout,
77
+ blockHeight: 1,
78
+ content: params.data,
79
+ contentType: params.contentType,
80
+ feeRate: params.feeRate
81
+ };
82
+ }
83
+
84
+ async transferInscription(inscriptionId: string, _toAddress: string, _options?: { feeRate?: number }) {
85
+ const rec = this.state.inscriptionsById.get(inscriptionId);
86
+ if (!rec) {
87
+ return Promise.reject(new Error('inscription not found'));
88
+ }
89
+ const txid = `tx-${Math.random().toString(36).slice(2)}`;
90
+ return {
91
+ txid,
92
+ vin: [{ txid: rec.txid, vout: rec.vout }],
93
+ vout: [{ value: 546, scriptPubKey: 'script' }],
94
+ fee: 100,
95
+ blockHeight: 1,
96
+ confirmations: 0,
97
+ satoshi: rec.satoshi
98
+ };
99
+ }
100
+ }
101
+
@@ -0,0 +1,66 @@
1
+ export interface StoragePutOptions {
2
+ contentType?: string;
3
+ cacheControl?: string;
4
+ }
5
+
6
+ export interface StorageGetResult {
7
+ content: Buffer;
8
+ contentType: string;
9
+ }
10
+
11
+ export interface StorageAdapter {
12
+ put(objectKey: string, data: Buffer | string, options?: StoragePutOptions): Promise<string>;
13
+ get(objectKey: string): Promise<StorageGetResult | null>;
14
+ delete?(objectKey: string): Promise<boolean>;
15
+ }
16
+
17
+ export interface FeeOracleAdapter {
18
+ // Returns sats/vB (or feerate unit appropriate to the network) for the given target blocks
19
+ estimateFeeRate(targetBlocks?: number): Promise<number>;
20
+ }
21
+
22
+ export interface OrdinalsProvider {
23
+ getInscriptionById(id: string): Promise<{
24
+ inscriptionId: string;
25
+ content: Buffer;
26
+ contentType: string;
27
+ txid: string;
28
+ vout: number;
29
+ satoshi?: string;
30
+ blockHeight?: number;
31
+ } | null>;
32
+ getInscriptionsBySatoshi(satoshi: string): Promise<Array<{ inscriptionId: string }>>;
33
+ broadcastTransaction(txHexOrObj: unknown): Promise<string>;
34
+ getTransactionStatus(txid: string): Promise<{ confirmed: boolean; blockHeight?: number; confirmations?: number }>;
35
+ estimateFee(blocks?: number): Promise<number>;
36
+ createInscription(params: {
37
+ data: Buffer;
38
+ contentType: string;
39
+ feeRate?: number;
40
+ }): Promise<{
41
+ inscriptionId: string;
42
+ revealTxId: string;
43
+ commitTxId?: string;
44
+ satoshi?: string;
45
+ txid?: string;
46
+ vout?: number;
47
+ blockHeight?: number;
48
+ content?: Buffer;
49
+ contentType?: string;
50
+ feeRate?: number;
51
+ }>;
52
+ transferInscription(
53
+ inscriptionId: string,
54
+ toAddress: string,
55
+ options?: { feeRate?: number }
56
+ ): Promise<{
57
+ txid: string;
58
+ vin: Array<{ txid: string; vout: number }>;
59
+ vout: Array<{ value: number; scriptPubKey: string; address?: string }>;
60
+ fee: number;
61
+ blockHeight?: number;
62
+ confirmations?: number;
63
+ satoshi?: string;
64
+ }>;
65
+ }
66
+
@@ -0,0 +1,329 @@
1
+ import {
2
+ OriginalsConfig,
3
+ OrdinalsInscription,
4
+ BitcoinTransaction,
5
+ DUST_LIMIT_SATS
6
+ } from '../types';
7
+ import type { FeeOracleAdapter, OrdinalsProvider } from '../adapters';
8
+ import { emitTelemetry, StructuredError } from '../utils/telemetry';
9
+ import { validateBitcoinAddress } from '../utils/bitcoin-address';
10
+ import { validateSatoshiNumber } from '../utils/satoshi-validation';
11
+
12
+ export class BitcoinManager {
13
+ private readonly feeOracle?: FeeOracleAdapter;
14
+ private readonly ord?: OrdinalsProvider;
15
+
16
+ constructor(private config: OriginalsConfig) {
17
+ this.feeOracle = config.feeOracle;
18
+ this.ord = config.ordinalsProvider;
19
+ }
20
+
21
+ private async resolveFeeRate(targetBlocks = 1, provided?: number): Promise<number | undefined> {
22
+ // 1) Prefer external fee oracle
23
+ if (this.feeOracle) {
24
+ try {
25
+ const estimated = await this.feeOracle.estimateFeeRate(targetBlocks);
26
+ if (typeof estimated === 'number' && Number.isFinite(estimated) && estimated > 0) {
27
+ emitTelemetry(this.config.telemetry, {
28
+ name: 'bitcoin.fee.estimated',
29
+ attributes: { feeRate: estimated, source: 'feeOracle' }
30
+ });
31
+ return estimated;
32
+ }
33
+ } catch (error) {
34
+ emitTelemetry(this.config.telemetry, {
35
+ name: 'bitcoin.fee.error',
36
+ level: 'warn',
37
+ attributes: { error: String(error), source: 'feeOracle' }
38
+ });
39
+ }
40
+ }
41
+
42
+ // 2) Fallback to ordinals provider if present
43
+ if (this.ord) {
44
+ try {
45
+ const estimated = await this.ord.estimateFee(targetBlocks);
46
+ if (typeof estimated === 'number' && Number.isFinite(estimated) && estimated > 0) {
47
+ emitTelemetry(this.config.telemetry, {
48
+ name: 'bitcoin.fee.estimated',
49
+ attributes: { feeRate: estimated, source: 'ordinalsProvider' }
50
+ });
51
+ return estimated;
52
+ }
53
+ } catch (error) {
54
+ emitTelemetry(this.config.telemetry, {
55
+ name: 'bitcoin.fee.error',
56
+ level: 'warn',
57
+ attributes: { error: String(error), source: 'ordinalsProvider' }
58
+ });
59
+ }
60
+ }
61
+
62
+ // 3) If caller provided a valid non-zero fee rate, use it as last resort
63
+ if (typeof provided === 'number' && Number.isFinite(provided) && provided > 0) {
64
+ return provided;
65
+ }
66
+
67
+ return undefined;
68
+ }
69
+
70
+ async inscribeData(
71
+ data: any,
72
+ contentType: string,
73
+ feeRate?: number
74
+ ): Promise<OrdinalsInscription> {
75
+ // Input validation
76
+ if (!data) {
77
+ throw new StructuredError('INVALID_INPUT', 'Data to inscribe cannot be null or undefined');
78
+ }
79
+ if (!contentType || typeof contentType !== 'string') {
80
+ throw new StructuredError('INVALID_INPUT', 'Content type must be a non-empty string');
81
+ }
82
+ // Validate contentType is a valid MIME type
83
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}\/[a-zA-Z0-9][a-zA-Z0-9!#$&^_.+-]{0,126}$/.test(contentType)) {
84
+ throw new StructuredError('INVALID_INPUT', `Invalid MIME type format: ${contentType}`);
85
+ }
86
+ if (feeRate !== undefined && (typeof feeRate !== 'number' || feeRate <= 0 || !Number.isFinite(feeRate))) {
87
+ throw new StructuredError('INVALID_INPUT', 'Fee rate must be a positive number');
88
+ }
89
+ // Security: Reject extremely high fee rates to prevent accidental fund drainage
90
+ const MAX_REASONABLE_FEE_RATE = 10_000; // sat/vB
91
+ if (feeRate !== undefined && feeRate > MAX_REASONABLE_FEE_RATE) {
92
+ throw new StructuredError('INVALID_INPUT', `Fee rate ${feeRate} exceeds maximum reasonable fee rate of ${MAX_REASONABLE_FEE_RATE} sat/vB`);
93
+ }
94
+
95
+ const effectiveFeeRate = await this.resolveFeeRate(1, feeRate);
96
+
97
+ if (!this.ord) {
98
+ throw new StructuredError(
99
+ 'ORD_PROVIDER_REQUIRED',
100
+ 'Ordinals provider must be configured to inscribe data on Bitcoin. ' +
101
+ 'Please provide an ordinalsProvider in your SDK configuration. ' +
102
+ 'For testing, use: import { OrdMockProvider } from \'@originals/sdk\';'
103
+ );
104
+ }
105
+
106
+ if (typeof this.ord.createInscription !== 'function') {
107
+ throw new StructuredError(
108
+ 'ORD_PROVIDER_UNSUPPORTED',
109
+ 'Configured ordinals provider does not support inscription creation'
110
+ );
111
+ }
112
+
113
+ const creation = await this.ord.createInscription({ data, contentType, feeRate: effectiveFeeRate });
114
+ const txid = creation.txid ?? creation.revealTxId;
115
+ if (!creation.inscriptionId || !txid) {
116
+ throw new StructuredError(
117
+ 'ORD_PROVIDER_INVALID_RESPONSE',
118
+ 'Ordinals provider did not return a valid inscription identifier or transaction id'
119
+ );
120
+ }
121
+
122
+ let satoshi = creation.satoshi ?? '';
123
+ if (!satoshi) {
124
+ satoshi = (await this.getSatoshiFromInscription(creation.inscriptionId)) ?? '';
125
+ }
126
+
127
+ // Validate satoshi before using it
128
+ if (satoshi) {
129
+ const validation = validateSatoshiNumber(satoshi);
130
+ if (!validation.valid) {
131
+ throw new StructuredError(
132
+ 'INVALID_SATOSHI',
133
+ `Ordinals provider returned invalid satoshi identifier: ${validation.error}`
134
+ );
135
+ }
136
+ }
137
+
138
+ let recordedFeeRate: number | undefined;
139
+ if (this.feeOracle) {
140
+ recordedFeeRate = effectiveFeeRate;
141
+ } else if (typeof feeRate === 'number' && Number.isFinite(feeRate) && feeRate > 0) {
142
+ recordedFeeRate = feeRate;
143
+ } else {
144
+ recordedFeeRate = creation.feeRate ?? effectiveFeeRate;
145
+ }
146
+
147
+ const inscription: OrdinalsInscription & {
148
+ revealTxId?: string;
149
+ commitTxId?: string;
150
+ feeRate?: number;
151
+ } = {
152
+ satoshi,
153
+ inscriptionId: creation.inscriptionId,
154
+ content: creation.content ?? data,
155
+ contentType: creation.contentType ?? contentType,
156
+ txid,
157
+ vout: typeof creation.vout === 'number' ? creation.vout : 0,
158
+ blockHeight: creation.blockHeight,
159
+ revealTxId: creation.revealTxId,
160
+ commitTxId: creation.commitTxId,
161
+ feeRate: recordedFeeRate
162
+ };
163
+
164
+ return inscription;
165
+ }
166
+
167
+ async trackInscription(inscriptionId: string): Promise<OrdinalsInscription | null> {
168
+ if (this.ord) {
169
+ const info = await this.ord.getInscriptionById(inscriptionId);
170
+ if (!info) return null;
171
+ return {
172
+ satoshi: info.satoshi ?? '',
173
+ inscriptionId: info.inscriptionId,
174
+ content: info.content,
175
+ contentType: info.contentType,
176
+ txid: info.txid,
177
+ vout: info.vout,
178
+ blockHeight: info.blockHeight
179
+ };
180
+ }
181
+ return null;
182
+ }
183
+
184
+ async transferInscription(
185
+ inscription: OrdinalsInscription,
186
+ toAddress: string
187
+ ): Promise<BitcoinTransaction> {
188
+ // Input validation
189
+ if (!inscription || typeof inscription !== 'object') {
190
+ throw new StructuredError('INVALID_INPUT', 'Inscription must be a valid OrdinalsInscription object');
191
+ }
192
+ if (!inscription.inscriptionId || typeof inscription.inscriptionId !== 'string') {
193
+ throw new StructuredError('INVALID_INPUT', 'Inscription must have a valid inscriptionId');
194
+ }
195
+ if (!toAddress || typeof toAddress !== 'string') {
196
+ throw new StructuredError('INVALID_INPUT', 'Destination address must be a non-empty string');
197
+ }
198
+
199
+ // Validate Bitcoin address format and checksum
200
+ try {
201
+ validateBitcoinAddress(toAddress, this.config.network);
202
+ } catch (error) {
203
+ const message = error instanceof Error ? error.message : 'Invalid Bitcoin address';
204
+ throw new StructuredError('INVALID_ADDRESS', message);
205
+ }
206
+
207
+ const effectiveFeeRate = await this.resolveFeeRate(1);
208
+
209
+ if (!this.ord) {
210
+ throw new StructuredError(
211
+ 'ORD_PROVIDER_REQUIRED',
212
+ 'Ordinals provider must be configured to transfer inscriptions on Bitcoin. ' +
213
+ 'Please provide an ordinalsProvider in your SDK configuration. ' +
214
+ 'For testing, use: import { OrdMockProvider } from \'@originals/sdk\';'
215
+ );
216
+ }
217
+
218
+ if (typeof this.ord.transferInscription !== 'function') {
219
+ throw new StructuredError(
220
+ 'ORD_PROVIDER_UNSUPPORTED',
221
+ 'Configured ordinals provider does not support inscription transfers'
222
+ );
223
+ }
224
+
225
+ const response = await this.ord.transferInscription(inscription.inscriptionId, toAddress, {
226
+ feeRate: effectiveFeeRate
227
+ });
228
+
229
+ if (!response || !response.txid) {
230
+ throw new StructuredError(
231
+ 'ORD_PROVIDER_INVALID_RESPONSE',
232
+ 'Ordinals provider did not return a valid transfer transaction'
233
+ );
234
+ }
235
+
236
+ if (response.satoshi) {
237
+ inscription.satoshi = response.satoshi;
238
+ }
239
+
240
+ return {
241
+ txid: response.txid,
242
+ vin: response.vin ?? [{ txid: inscription.txid, vout: inscription.vout }],
243
+ vout:
244
+ response.vout?.length
245
+ ? response.vout
246
+ : [{ value: DUST_LIMIT_SATS, scriptPubKey: 'script', address: toAddress }],
247
+ fee: response.fee,
248
+ blockHeight: response.blockHeight,
249
+ confirmations: response.confirmations
250
+ };
251
+ }
252
+
253
+ async preventFrontRunning(satoshi: string): Promise<boolean> {
254
+ if (!satoshi) throw new StructuredError('SATOSHI_REQUIRED', 'Satoshi identifier is required');
255
+ // Naive implementation: check for multiple inscriptions on same satoshi via provider
256
+ if (this.ord) {
257
+ const list = await this.ord.getInscriptionsBySatoshi(satoshi);
258
+ return list.length <= 1;
259
+ }
260
+ return true;
261
+ }
262
+
263
+ async getSatoshiFromInscription(inscriptionId: string): Promise<string | null> {
264
+ if (this.ord) {
265
+ const info = await this.ord.getInscriptionById(inscriptionId);
266
+ const satoshi = info?.satoshi;
267
+
268
+ // Validate satoshi before returning
269
+ if (satoshi) {
270
+ const validation = validateSatoshiNumber(satoshi);
271
+ if (!validation.valid) {
272
+ // Return null if validation fails (don't return empty or invalid string)
273
+ return null;
274
+ }
275
+ return satoshi;
276
+ }
277
+ return null;
278
+ }
279
+ return null;
280
+ }
281
+
282
+ async validateBTCODID(didId: string): Promise<boolean> {
283
+ // Validate that a did:btco DID exists on Bitcoin
284
+ const satoshi = this.extractSatoshiFromBTCODID(didId);
285
+ if (!satoshi) return false;
286
+
287
+ // Validate the extracted satoshi number
288
+ const validation = validateSatoshiNumber(satoshi);
289
+ if (!validation.valid) return false;
290
+
291
+ if (!this.ord) return false;
292
+ const inscriptions = await this.ord.getInscriptionsBySatoshi(satoshi);
293
+ return inscriptions.length > 0;
294
+ }
295
+
296
+ private extractSatoshiFromBTCODID(didId: string): string | null {
297
+ // Extract satoshi identifier from did:btco DID
298
+ if (!didId.startsWith('did:btco:')) return null;
299
+
300
+ const parts = didId.split(':');
301
+ let satoshi: string | null = null;
302
+
303
+ // Handle different network prefixes:
304
+ // did:btco:123456 (mainnet) - 3 parts
305
+ // did:btco:test:123456 or did:btco:sig:123456 - 4 parts
306
+ if (parts.length === 3) {
307
+ satoshi = parts[2];
308
+ } else if (parts.length === 4) {
309
+ // Validate network prefix - only 'test' and 'sig' are allowed
310
+ const network = parts[2];
311
+ if (network !== 'test' && network !== 'sig') {
312
+ return null;
313
+ }
314
+ satoshi = parts[3];
315
+ }
316
+
317
+ // Validate the extracted satoshi format
318
+ if (satoshi) {
319
+ const validation = validateSatoshiNumber(satoshi);
320
+ if (!validation.valid) {
321
+ return null;
322
+ }
323
+ }
324
+
325
+ return satoshi;
326
+ }
327
+ }
328
+
329
+