openclaw-overlay-plugin 0.8.18 → 0.8.20

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 (84) hide show
  1. package/dist/index.js +607 -853
  2. package/dist/index.js.map +4 -4
  3. package/dist/src/cli.js +30 -568
  4. package/dist/src/cli.js.map +4 -4
  5. package/package.json +5 -5
  6. package/index.ts +0 -379
  7. package/src/ambient.d.ts +0 -1
  8. package/src/cli-main.ts +0 -240
  9. package/src/cli.ts +0 -16
  10. package/src/compatibility.test.ts +0 -46
  11. package/src/scripts/baemail/commands.ts +0 -311
  12. package/src/scripts/baemail/handler.ts +0 -338
  13. package/src/scripts/baemail/index.ts +0 -6
  14. package/src/scripts/config.ts +0 -89
  15. package/src/scripts/index.ts +0 -8
  16. package/src/scripts/messaging/connect.ts +0 -162
  17. package/src/scripts/messaging/handlers.ts +0 -394
  18. package/src/scripts/messaging/inbox.ts +0 -64
  19. package/src/scripts/messaging/index.ts +0 -9
  20. package/src/scripts/messaging/poll.ts +0 -59
  21. package/src/scripts/messaging/send.ts +0 -54
  22. package/src/scripts/output.ts +0 -30
  23. package/src/scripts/overlay/advertisement.ts +0 -138
  24. package/src/scripts/overlay/discover.ts +0 -83
  25. package/src/scripts/overlay/index.ts +0 -8
  26. package/src/scripts/overlay/registration.ts +0 -199
  27. package/src/scripts/overlay/services.ts +0 -199
  28. package/src/scripts/overlay/transaction.ts +0 -124
  29. package/src/scripts/payment/build.ts +0 -65
  30. package/src/scripts/payment/commands.ts +0 -92
  31. package/src/scripts/payment/index.ts +0 -7
  32. package/src/scripts/payment/types.ts +0 -62
  33. package/src/scripts/services/index.ts +0 -7
  34. package/src/scripts/services/queue.ts +0 -35
  35. package/src/scripts/services/request.ts +0 -98
  36. package/src/scripts/services/respond.ts +0 -149
  37. package/src/scripts/types.ts +0 -121
  38. package/src/scripts/utils/index.ts +0 -7
  39. package/src/scripts/utils/merkle.ts +0 -57
  40. package/src/scripts/utils/storage.ts +0 -231
  41. package/src/scripts/utils/woc.ts +0 -106
  42. package/src/scripts/wallet/balance.ts +0 -277
  43. package/src/scripts/wallet/identity.ts +0 -204
  44. package/src/scripts/wallet/index.ts +0 -7
  45. package/src/scripts/wallet/setup.ts +0 -137
  46. package/src/scripts/x-verification/commands.ts +0 -261
  47. package/src/scripts/x-verification/index.ts +0 -5
  48. package/src/services/built-in/api-proxy/index.ts +0 -26
  49. package/src/services/built-in/api-proxy/prompt.md +0 -26
  50. package/src/services/built-in/code-develop/index.ts +0 -26
  51. package/src/services/built-in/code-develop/prompt.md +0 -35
  52. package/src/services/built-in/code-review/index.ts +0 -54
  53. package/src/services/built-in/code-review/prompt.md +0 -105
  54. package/src/services/built-in/image-analysis/index.ts +0 -36
  55. package/src/services/built-in/image-analysis/prompt.md +0 -42
  56. package/src/services/built-in/memory-store/index.ts +0 -25
  57. package/src/services/built-in/memory-store/prompt.md +0 -45
  58. package/src/services/built-in/roulette/index.ts +0 -30
  59. package/src/services/built-in/roulette/prompt.md +0 -35
  60. package/src/services/built-in/summarize/index.ts +0 -24
  61. package/src/services/built-in/summarize/prompt.md +0 -27
  62. package/src/services/built-in/tell-joke/handler.ts +0 -134
  63. package/src/services/built-in/tell-joke/index.ts +0 -34
  64. package/src/services/built-in/tell-joke/prompt.md +0 -59
  65. package/src/services/built-in/translate/index.ts +0 -24
  66. package/src/services/built-in/translate/prompt.md +0 -23
  67. package/src/services/built-in/web-research/index.ts +0 -54
  68. package/src/services/built-in/web-research/prompt.md +0 -110
  69. package/src/services/index.ts +0 -16
  70. package/src/services/loader.ts +0 -344
  71. package/src/services/manager.ts +0 -304
  72. package/src/services/registry.ts +0 -246
  73. package/src/services/types.ts +0 -259
  74. package/src/test/cli.test.ts +0 -353
  75. package/src/test/comprehensive-overlay.test.ts +0 -729
  76. package/src/test/identity-consistency.test.ts +0 -68
  77. package/src/test/key-derivation.test.ts +0 -102
  78. package/src/test/network-address.test.ts +0 -46
  79. package/src/test/overlay-submit.test.ts +0 -570
  80. package/src/test/request-response-flow.test.ts +0 -253
  81. package/src/test/service-system.test.ts +0 -241
  82. package/src/test/taskflow.test.ts +0 -95
  83. package/src/test/utils/server-logic.ts +0 -368
  84. package/src/test/wallet.test.ts +0 -165
@@ -1,149 +0,0 @@
1
- /**
2
- * Service response commands.
3
- */
4
-
5
- import fs from 'node:fs';
6
- import { OVERLAY_URL, PATHS } from '../config.js';
7
- import { ok, fail } from '../output.js';
8
- import { loadIdentity, signRelayMessage } from '../wallet/identity.js';
9
- import { updateServiceQueueStatus } from '../utils/storage.js';
10
-
11
- /**
12
- * Respond to a service request.
13
- */
14
- export async function cmdRespondService(
15
- requestId: string | undefined,
16
- recipientKey: string | undefined,
17
- serviceId: string | undefined,
18
- resultJson: string | undefined
19
- ): Promise<any> {
20
- if (!requestId || !recipientKey || !serviceId || !resultJson) {
21
- return fail('Usage: respond-service <requestId> <recipientKey> <serviceId> <resultJson>');
22
- }
23
-
24
- let result: unknown;
25
- try {
26
- result = JSON.parse(resultJson);
27
- } catch {
28
- return fail('resultJson must be valid JSON');
29
- }
30
-
31
- const { identityKey, privKey } = await loadIdentity();
32
-
33
- // Check if already processed before sending response (idempotency)
34
- if (fs.existsSync(PATHS.serviceQueue)) {
35
- const lines = fs.readFileSync(PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
36
- const finalStatuses = ['fulfilled', 'rejected', 'delivery_failed', 'failed', 'error'];
37
-
38
- for (const line of lines) {
39
- try {
40
- const entry = JSON.parse(line);
41
- if (entry.requestId === requestId && finalStatuses.includes(entry.status)) {
42
- return ok({
43
- sent: false,
44
- requestId,
45
- serviceId,
46
- to: recipientKey,
47
- message: `Request already processed with status: ${entry.status}`,
48
- alreadyProcessed: true,
49
- previousStatus: entry.status
50
- });
51
- }
52
- } catch {}
53
- }
54
- }
55
-
56
- const responsePayload = {
57
- requestId,
58
- serviceId,
59
- status: 'fulfilled',
60
- result,
61
- };
62
-
63
- const sig = await signRelayMessage(privKey, recipientKey, 'service-response', responsePayload);
64
- const resp = await fetch(`${OVERLAY_URL}/relay/send`, {
65
- method: 'POST',
66
- headers: { 'Content-Type': 'application/json' },
67
- body: JSON.stringify({
68
- from: identityKey,
69
- to: recipientKey,
70
- type: 'service-response',
71
- payload: responsePayload,
72
- signature: sig,
73
- }),
74
- });
75
-
76
- if (!resp.ok) {
77
- // Mark as failed in queue using atomic update
78
- updateServiceQueueStatus(requestId, 'failed', {
79
- failedAt: Date.now(),
80
- error: `Relay send failed: ${resp.status}`
81
- });
82
- return fail(`Relay send failed: ${resp.status}`);
83
- }
84
-
85
- // Mark as fulfilled in queue using atomic update
86
- updateServiceQueueStatus(requestId, 'fulfilled', {
87
- fulfilledAt: Date.now()
88
- });
89
-
90
- return ok({ sent: true, requestId, serviceId, to: recipientKey });
91
- }
92
-
93
- /**
94
- * Respond to a research request with results.
95
- */
96
- export async function cmdResearchRespond(resultJsonPath: string | undefined): Promise<any> {
97
- if (!resultJsonPath) return fail('Usage: research-respond <resultJsonFile>');
98
- if (!fs.existsSync(resultJsonPath)) return fail(`File not found: ${resultJsonPath}`);
99
-
100
- const result = JSON.parse(fs.readFileSync(resultJsonPath, 'utf-8'));
101
- const { requestId, from: recipientKey, query, research } = result;
102
-
103
- if (!requestId || !recipientKey || !research) {
104
- return fail('Result JSON must have: requestId, from, query, research');
105
- }
106
-
107
- const { identityKey, privKey } = await loadIdentity();
108
-
109
- const responsePayload = {
110
- requestId,
111
- serviceId: 'web-research',
112
- status: 'fulfilled',
113
- result: research,
114
- paymentAccepted: true,
115
- paymentTxid: result.paymentTxid || null,
116
- satoshisReceived: result.satoshisReceived || 0,
117
- walletAccepted: result.walletAccepted ?? true,
118
- };
119
-
120
- const sig = await signRelayMessage(privKey, recipientKey, 'service-response', responsePayload);
121
- const sendResp = await fetch(`${OVERLAY_URL}/relay/send`, {
122
- method: 'POST',
123
- headers: { 'Content-Type': 'application/json' },
124
- body: JSON.stringify({
125
- from: identityKey,
126
- to: recipientKey,
127
- type: 'service-response',
128
- payload: responsePayload,
129
- signature: sig,
130
- }),
131
- });
132
-
133
- if (!sendResp.ok) {
134
- return fail(`Failed to send response: ${await sendResp.text()}`);
135
- }
136
-
137
- const sendResult = await sendResp.json();
138
-
139
- // Remove from queue
140
- if (fs.existsSync(PATHS.researchQueue)) {
141
- const lines = fs.readFileSync(PATHS.researchQueue, 'utf-8').trim().split('\n').filter(Boolean);
142
- const remaining = lines.filter((l: string) => {
143
- try { return JSON.parse(l).requestId !== requestId; } catch { return true; }
144
- });
145
- fs.writeFileSync(PATHS.researchQueue, remaining.length ? remaining.join('\n') + '\n' : '');
146
- }
147
-
148
- return ok({ responded: true, requestId, to: recipientKey, query, pushed: sendResult.pushed });
149
- }
@@ -1,121 +0,0 @@
1
- /**
2
- * Shared TypeScript interfaces for the overlay CLI.
3
- */
4
-
5
- export interface WalletIdentity {
6
- rootKeyHex: string;
7
- identityKey: string;
8
- network: 'mainnet' | 'testnet';
9
- }
10
-
11
- export interface PaymentResult {
12
- beef: string;
13
- txid: string;
14
- satoshis: number;
15
- derivationPrefix: string;
16
- derivationSuffix: string;
17
- senderIdentityKey: string;
18
- }
19
-
20
- export interface PaymentParams {
21
- to: string;
22
- satoshis: number;
23
- description?: string;
24
- }
25
-
26
- export interface ServiceAdvertisement {
27
- serviceId: string;
28
- name: string;
29
- description: string;
30
- priceSats: number;
31
- txid?: string;
32
- registeredAt?: string;
33
- }
34
-
35
- export interface Message {
36
- id: string;
37
- from: string;
38
- to: string;
39
- type: string;
40
- payload: unknown;
41
- signature?: string;
42
- timestamp?: number;
43
- }
44
-
45
- export interface CommandResult<T = unknown> {
46
- success: boolean;
47
- data?: T;
48
- error?: string;
49
- }
50
-
51
- export interface Registration {
52
- identityKey: string;
53
- agentName: string;
54
- agentDescription: string;
55
- overlayUrl: string;
56
- identityTxid: string;
57
- serviceTxid: string | null;
58
- funded: string;
59
- registeredAt: string;
60
- }
61
-
62
- export interface OverlayPayload {
63
- protocol: string;
64
- type: string;
65
- identityKey?: string;
66
- [key: string]: unknown;
67
- }
68
-
69
- export interface VerifyAndAcceptResult {
70
- accepted: boolean;
71
- txid: string | null;
72
- satoshis: number;
73
- outputIndex: number;
74
- walletAccepted: boolean;
75
- error: string | null;
76
- }
77
-
78
- export interface ProcessMessageResult {
79
- id: string;
80
- type: string;
81
- action: string;
82
- from: string;
83
- ack: boolean;
84
- [key: string]: unknown;
85
- }
86
-
87
- export interface RelayMessage {
88
- id: string;
89
- from: string;
90
- to: string;
91
- type: string;
92
- payload: Record<string, unknown>;
93
- signature?: string;
94
- }
95
-
96
- export interface XVerification {
97
- identityKey: string;
98
- xHandle: string;
99
- xUserId: string;
100
- tweetId: string;
101
- tweetUrl: string;
102
- signature: string;
103
- verifiedAt: string;
104
- txid?: string | null;
105
- }
106
-
107
- export interface StoredChange {
108
- txHex: string;
109
- txid: string;
110
- vout: number;
111
- satoshis: number;
112
- sourceChain?: SourceChainEntry[];
113
- savedAt: string;
114
- }
115
-
116
- export interface SourceChainEntry {
117
- txHex: string;
118
- txid: string;
119
- merklePathHex?: string;
120
- blockHeight?: number;
121
- }
@@ -1,7 +0,0 @@
1
- /**
2
- * Utility module exports.
3
- */
4
-
5
- export * from './woc.js';
6
- export * from './storage.js';
7
- export * from './merkle.js';
@@ -1,57 +0,0 @@
1
- /**
2
- * Merkle path utilities for SPV proofs.
3
- */
4
-
5
- import type { MerklePath as MerklePathType } from '@bsv/sdk';
6
-
7
- // We'll import MerklePath dynamically to avoid issues with ESM resolution
8
- let _MerklePath: typeof MerklePathType | null = null;
9
-
10
- async function getMerklePath(): Promise<typeof MerklePathType> {
11
- if (_MerklePath) return _MerklePath;
12
- const sdk = await import('@bsv/sdk');
13
- _MerklePath = sdk.MerklePath;
14
- return _MerklePath;
15
- }
16
-
17
- /**
18
- * Build a MerklePath from TSC (Transaction Status Check) proof data.
19
- * @param txid - Transaction ID
20
- * @param txIndex - Transaction's index in the block
21
- * @param nodes - Array of sibling hashes (or '*' for duplicate)
22
- * @param blockHeight - Block height
23
- */
24
- export async function buildMerklePathFromTSC(
25
- txid: string,
26
- txIndex: number,
27
- nodes: string[],
28
- blockHeight: number
29
- ): Promise<MerklePathType> {
30
- const MerklePath = await getMerklePath();
31
- const treeHeight = nodes.length;
32
- const mpPath: Array<Array<{ offset: number; hash?: string; txid?: boolean; duplicate?: boolean }>> = [];
33
-
34
- // Level 0
35
- const level0: Array<{ offset: number; hash?: string; txid?: boolean; duplicate?: boolean }> = [
36
- { offset: txIndex, hash: txid, txid: true }
37
- ];
38
- if (nodes[0] === '*') {
39
- level0.push({ offset: txIndex ^ 1, duplicate: true });
40
- } else {
41
- level0.push({ offset: txIndex ^ 1, hash: nodes[0] });
42
- }
43
- level0.sort((a, b) => a.offset - b.offset);
44
- mpPath.push(level0);
45
-
46
- // Higher levels
47
- for (let i = 1; i < treeHeight; i++) {
48
- const siblingOffset = (txIndex >> i) ^ 1;
49
- if (nodes[i] === '*') {
50
- mpPath.push([{ offset: siblingOffset, duplicate: true }]);
51
- } else {
52
- mpPath.push([{ offset: siblingOffset, hash: nodes[i] }]);
53
- }
54
- }
55
-
56
- return new MerklePath(blockHeight, mpPath);
57
- }
@@ -1,231 +0,0 @@
1
- /**
2
- * File-based storage helpers for registration, services, and queues.
3
- */
4
-
5
- import fs from 'node:fs';
6
- import { OVERLAY_STATE_DIR, PATHS } from '../config.js';
7
- import type { Registration, ServiceAdvertisement, XVerification, StoredChange } from '../types.js';
8
-
9
- /**
10
- * Ensure the overlay state directory exists.
11
- */
12
- export function ensureStateDir(): void {
13
- fs.mkdirSync(OVERLAY_STATE_DIR, { recursive: true });
14
- }
15
-
16
- /**
17
- * Load registration data from disk.
18
- */
19
- export function loadRegistration(): Registration | null {
20
- try {
21
- if (fs.existsSync(PATHS.registration)) {
22
- return JSON.parse(fs.readFileSync(PATHS.registration, 'utf-8'));
23
- }
24
- } catch {
25
- // Ignore parse errors
26
- }
27
- return null;
28
- }
29
-
30
- /**
31
- * Save registration data to disk.
32
- */
33
- export function saveRegistration(data: Registration): void {
34
- ensureStateDir();
35
- fs.writeFileSync(PATHS.registration, JSON.stringify(data, null, 2), 'utf-8');
36
- }
37
-
38
- /**
39
- * Delete registration file.
40
- */
41
- export function deleteRegistration(): void {
42
- try {
43
- fs.unlinkSync(PATHS.registration);
44
- } catch {
45
- // Ignore if file doesn't exist
46
- }
47
- }
48
-
49
- /**
50
- * Load services list from disk.
51
- */
52
- export function loadServices(): ServiceAdvertisement[] {
53
- try {
54
- if (fs.existsSync(PATHS.services)) {
55
- return JSON.parse(fs.readFileSync(PATHS.services, 'utf-8'));
56
- }
57
- } catch {
58
- // Ignore parse errors
59
- }
60
- return [];
61
- }
62
-
63
- /**
64
- * Save services list to disk.
65
- */
66
- export function saveServices(services: ServiceAdvertisement[]): void {
67
- ensureStateDir();
68
- fs.writeFileSync(PATHS.services, JSON.stringify(services, null, 2), 'utf-8');
69
- }
70
-
71
- /**
72
- * Load X verifications from disk.
73
- */
74
- export function loadXVerifications(): XVerification[] {
75
- try {
76
- if (fs.existsSync(PATHS.xVerifications)) {
77
- return JSON.parse(fs.readFileSync(PATHS.xVerifications, 'utf-8'));
78
- }
79
- } catch {
80
- // Ignore parse errors
81
- }
82
- return [];
83
- }
84
-
85
- /**
86
- * Save X verifications to disk.
87
- */
88
- export function saveXVerifications(verifications: XVerification[]): void {
89
- ensureStateDir();
90
- fs.writeFileSync(PATHS.xVerifications, JSON.stringify(verifications, null, 2), 'utf-8');
91
- }
92
-
93
- /**
94
- * Append a line to a JSONL file.
95
- */
96
- export function appendToJsonl(filePath: string, entry: Record<string, unknown>): void {
97
- ensureStateDir();
98
- fs.appendFileSync(filePath, JSON.stringify(entry) + '\n');
99
- }
100
-
101
- /**
102
- * Read and parse a JSONL file.
103
- */
104
- export function readJsonl<T>(filePath: string): T[] {
105
- if (!fs.existsSync(filePath)) return [];
106
- const lines = fs.readFileSync(filePath, 'utf-8').trim().split('\n').filter(Boolean);
107
- return lines.map((line: string) => {
108
- try {
109
- return JSON.parse(line);
110
- } catch {
111
- return null;
112
- }
113
- }).filter(Boolean) as T[];
114
- }
115
-
116
- /**
117
- * Load stored change BEEF data.
118
- */
119
- export function loadStoredChange(): StoredChange | null {
120
- try {
121
- if (fs.existsSync(PATHS.latestChange)) {
122
- return JSON.parse(fs.readFileSync(PATHS.latestChange, 'utf-8'));
123
- }
124
- } catch {
125
- // Ignore parse errors
126
- }
127
- return null;
128
- }
129
-
130
- /**
131
- * Save stored change BEEF data.
132
- */
133
- export function saveStoredChange(data: StoredChange): void {
134
- ensureStateDir();
135
- fs.writeFileSync(PATHS.latestChange, JSON.stringify(data));
136
- }
137
-
138
- /**
139
- * Delete stored change file.
140
- */
141
- export function deleteStoredChange(): void {
142
- try {
143
- fs.unlinkSync(PATHS.latestChange);
144
- } catch {
145
- // Ignore if file doesn't exist
146
- }
147
- }
148
-
149
- /**
150
- * Clean up old entries from service queue.
151
- * Removes entries older than maxAgeMs or entries with final statuses older than finalStatusMaxAgeMs.
152
- */
153
- export function cleanupServiceQueue(maxAgeMs: number = 24 * 60 * 60 * 1000, finalStatusMaxAgeMs: number = 2 * 60 * 60 * 1000): void {
154
- if (!fs.existsSync(PATHS.serviceQueue)) return;
155
-
156
- const now = Date.now();
157
- const finalStatuses = ['fulfilled', 'rejected', 'delivery_failed', 'failed', 'error'];
158
-
159
- const lines = fs.readFileSync(PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
160
- const keptLines: string[] = [];
161
- let removedCount = 0;
162
-
163
- for (const line of lines) {
164
- try {
165
- const entry = JSON.parse(line);
166
- const entryAge = now - (entry._ts || 0);
167
-
168
- // Always keep pending entries that aren't too old
169
- if (entry.status === 'pending' && entryAge < maxAgeMs) {
170
- keptLines.push(line);
171
- continue;
172
- }
173
-
174
- // Keep final status entries only if they're recent
175
- if (finalStatuses.includes(entry.status) && entryAge < finalStatusMaxAgeMs) {
176
- keptLines.push(line);
177
- continue;
178
- }
179
-
180
- // Remove this entry
181
- removedCount++;
182
- } catch {
183
- // Keep malformed entries to avoid data loss
184
- keptLines.push(line);
185
- }
186
- }
187
-
188
- if (removedCount > 0) {
189
- fs.writeFileSync(PATHS.serviceQueue, keptLines.join('\n') + (keptLines.length ? '\n' : ''));
190
- console.error(JSON.stringify({ event: 'queue-cleanup', removed: removedCount, kept: keptLines.length }));
191
- }
192
- }
193
-
194
- /**
195
- * Atomically update a service queue entry status.
196
- * Returns true if the entry was found and updated, false otherwise.
197
- */
198
- export function updateServiceQueueStatus(
199
- requestId: string,
200
- newStatus: string,
201
- additionalFields: Record<string, any> = {}
202
- ): boolean {
203
- if (!fs.existsSync(PATHS.serviceQueue)) return false;
204
-
205
- const lines = fs.readFileSync(PATHS.serviceQueue, 'utf-8').trim().split('\n').filter(Boolean);
206
- let updated = false;
207
-
208
- const updatedLines = lines.map(line => {
209
- try {
210
- const entry = JSON.parse(line);
211
- if (entry.requestId === requestId) {
212
- updated = true;
213
- return JSON.stringify({
214
- ...entry,
215
- status: newStatus,
216
- ...additionalFields,
217
- updatedAt: Date.now()
218
- });
219
- }
220
- return line;
221
- } catch {
222
- return line;
223
- }
224
- });
225
-
226
- if (updated) {
227
- fs.writeFileSync(PATHS.serviceQueue, updatedLines.join('\n') + '\n');
228
- }
229
-
230
- return updated;
231
- }
@@ -1,106 +0,0 @@
1
- /**
2
- * WhatsOnChain API helpers with retry logic and rate limiting.
3
- */
4
-
5
- import { NETWORK, WOC_API_KEY } from '../config.js';
6
-
7
- /**
8
- * Fetch from WhatsonChain with optional API key auth and retry logic.
9
- * Retries on 429 (rate limit) and 5xx errors with exponential backoff.
10
- * Includes timeout to prevent hanging indefinitely.
11
- */
12
- export async function wocFetch(
13
- urlPath: string,
14
- options: RequestInit = {},
15
- maxRetries = 3,
16
- timeoutMs = 30000
17
- ): Promise<Response> {
18
- const wocNet = NETWORK === 'mainnet' ? 'main' : 'test';
19
- const base = `https://api.whatsonchain.com/v1/bsv/${wocNet}`;
20
- const url = urlPath.startsWith('http') ? urlPath : `${base}${urlPath}`;
21
- const headers: Record<string, string> = { ...(options.headers as Record<string, string> || {}) };
22
- if (WOC_API_KEY) {
23
- headers['Authorization'] = `Bearer ${WOC_API_KEY}`;
24
- }
25
-
26
- let lastError: Error | undefined;
27
- for (let attempt = 0; attempt <= maxRetries; attempt++) {
28
- try {
29
- const controller = new AbortController();
30
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
31
-
32
- const resp = await fetch(url, { ...options, headers, signal: controller.signal });
33
- clearTimeout(timeout);
34
-
35
- // Retry on 429 (rate limit) or 5xx (server error)
36
- if ((resp.status === 429 || resp.status >= 500) && attempt < maxRetries) {
37
- const delayMs = Math.min(1000 * Math.pow(2, attempt), 8000);
38
- await new Promise(r => setTimeout(r, delayMs));
39
- continue;
40
- }
41
-
42
- return resp;
43
- } catch (err) {
44
- lastError = err as Error;
45
- if (attempt < maxRetries) {
46
- const delayMs = Math.min(1000 * Math.pow(2, attempt), 8000);
47
- await new Promise(r => setTimeout(r, delayMs));
48
- continue;
49
- }
50
- }
51
- }
52
-
53
- throw lastError || new Error('WoC fetch failed after retries');
54
- }
55
-
56
- /**
57
- * Fetch with timeout using AbortController.
58
- */
59
- export async function fetchWithTimeout(
60
- url: string,
61
- options: RequestInit = {},
62
- timeoutMs = 15000
63
- ): Promise<Response> {
64
- const controller = new AbortController();
65
- const timeout = setTimeout(() => controller.abort(), timeoutMs);
66
- try {
67
- const resp = await fetch(url, { ...options, signal: controller.signal });
68
- return resp;
69
- } finally {
70
- clearTimeout(timeout);
71
- }
72
- }
73
-
74
- /**
75
- * Fetch a pre-built BEEF from WhatsonChain for a given txid.
76
- * WoC returns raw binary BEEF that includes the full source chain and merkle proofs.
77
- */
78
- export async function fetchBeefFromWoC(txid: string): Promise<Uint8Array | null> {
79
- try {
80
- const resp = await wocFetch(`/tx/${txid}/beef`);
81
- if (!resp.ok) return null;
82
- const hexStr = (await resp.text()).trim();
83
- if (!hexStr || hexStr.length < 8) return null;
84
- const bytes = hexStr.match(/.{2}/g)!.map(h => parseInt(h, 16));
85
- return new Uint8Array(bytes);
86
- } catch {
87
- return null;
88
- }
89
- }
90
-
91
- /**
92
- * Get the WoC base URL for the current network.
93
- */
94
- export function getWocBaseUrl(): string {
95
- const wocNet = NETWORK === 'mainnet' ? 'main' : 'test';
96
- return `https://api.whatsonchain.com/v1/bsv/${wocNet}`;
97
- }
98
-
99
- /**
100
- * Get the explorer base URL for the current network.
101
- */
102
- export function getExplorerBaseUrl(): string {
103
- return NETWORK === 'mainnet'
104
- ? 'https://whatsonchain.com'
105
- : 'https://test.whatsonchain.com';
106
- }