Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { describe, it, expect, beforeEach, afterEach, spyOn } from 'bun:test';
import { getTransactionStatus, broadcastTransaction } from '../blockchainService';

let fetchSpy: any;

beforeEach(() => {
// default network for tests
process.env.BITCOIN_NETWORK = 'testnet';
fetchSpy = spyOn(globalThis, 'fetch');
});

afterEach(() => {
fetchSpy.mockRestore();
});

describe('getTransactionStatus', () => {
it('returns confirmed status when API confirms', async () => {
const txid = 'abc';
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ confirmed: true, block_height: 100 }), { status: 200 }));

const result = await getTransactionStatus(txid);
expect(result).toEqual({ status: 'confirmed', blockHeight: 100 });
expect(fetchSpy).toHaveBeenCalled();
});

it('returns pending status when API not confirmed', async () => {
const txid = 'def';
fetchSpy.mockResolvedValue(new Response(JSON.stringify({ confirmed: false }), { status: 200 }));

const result = await getTransactionStatus(txid);
expect(result).toEqual({ status: 'pending' });
});

it('returns not_found when API responds 404', async () => {
const txid = 'ghi';
fetchSpy.mockResolvedValue(new Response('', { status: 404 }));

const result = await getTransactionStatus(txid);
expect(result).toEqual({ status: 'not_found' });
});

it('throws on network error', async () => {
const txid = 'err';
fetchSpy.mockRejectedValue(new Error('network fail'));
await expect(getTransactionStatus(txid)).rejects.toThrow('Failed to fetch transaction status');
});
});

describe('broadcastTransaction', () => {
it('returns txid when broadcast succeeds', async () => {
const txHex = 'deadbeef';
fetchSpy.mockResolvedValue(new Response('txid123', { status: 200 }));
const txid = await broadcastTransaction(txHex);
expect(txid).toBe('txid123');
expect(fetchSpy).toHaveBeenCalled();
});

it('throws when broadcast fails', async () => {
const txHex = 'deadbeef';
fetchSpy.mockResolvedValue(new Response('error', { status: 500 }));
await expect(broadcastTransaction(txHex)).rejects.toThrow('Failed to broadcast transaction');
});
});
99 changes: 78 additions & 21 deletions packages/ordinals-plus-api/src/services/blockchainService.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,81 @@
// Placeholder for blockchain interaction service

import type { TransactionStatusResponse } from '../types';

export async function getTransactionStatus(txid: string): Promise<TransactionStatusResponse> {
// TODO: Implement transaction status check (e.g., call node RPC or block explorer API)
console.warn(`getTransactionStatus: Placeholder implementation for ${txid}`);
// Simulate different statuses based on txid for testing
if (txid.endsWith('pending')) {
return { status: 'pending' };
} else if (txid.endsWith('confirmed')) {
return { status: 'confirmed', blockHeight: 800000, inscriptionId: `i${txid.substring(0, 10)}0` };
} else if (txid.endsWith('failed')) {
return { status: 'failed' };
import type { TransactionStatusResponse, NetworkType } from '../types';
import fetchClient from '../utils/fetchUtils';

// Environment controlled network selection
const DEFAULT_NETWORK: NetworkType =
(process.env.BITCOIN_NETWORK as NetworkType) || 'mainnet';

const MEMPOOL_MAINNET_API_URL =
process.env.MEMPOOL_MAINNET_API_URL || 'https://mempool.space/api';
const MEMPOOL_TESTNET_API_URL =
process.env.MEMPOOL_TESTNET_API_URL || 'https://mempool.space/testnet/api';
const MEMPOOL_SIGNET_API_URL =
process.env.MEMPOOL_SIGNET_API_URL || 'https://mempool.space/signet/api';

const getMempoolApiUrl = (network: NetworkType): string => {
switch (network) {
case 'testnet':
return MEMPOOL_TESTNET_API_URL;
case 'signet':
return MEMPOOL_SIGNET_API_URL;
case 'mainnet':
default:
return MEMPOOL_MAINNET_API_URL;
}
};

export async function getTransactionStatus(
txid: string,
network: NetworkType = DEFAULT_NETWORK
): Promise<TransactionStatusResponse> {
const statusUrl = `${getMempoolApiUrl(network)}/tx/${txid}/status`;
try {
const response = await fetchClient.get(statusUrl);

if (response.status === 404) {
return { status: 'not_found' };
}

const data = response.data as {
confirmed: boolean;
block_height?: number;
};

if (data.confirmed) {
return { status: 'confirmed', blockHeight: data.block_height };
}
return { status: 'not_found' };

return { status: 'pending' };
} catch (error) {
console.error(
`[blockchainService] Failed to fetch transaction status from ${statusUrl}:`,
error
);
throw new Error('Failed to fetch transaction status');
}
}

export async function broadcastTransaction(signedTxHex: string): Promise<string> {
// TODO: Implement transaction broadcasting (e.g., via node RPC or API)
console.warn('broadcastTransaction: Placeholder implementation');
// Return a dummy txid
return `tx_${Date.now()}_broadcasted`;
}
export async function broadcastTransaction(
signedTxHex: string,
network: NetworkType = DEFAULT_NETWORK
): Promise<string> {
const broadcastUrl = `${getMempoolApiUrl(network)}/tx`;

try {
const response = await fetchClient.post<string>(broadcastUrl, signedTxHex, {
headers: { 'Content-Type': 'text/plain' },
responseType: 'text'
});

if (response.status >= 400 || typeof response.data !== 'string') {
throw new Error(
`Broadcast failed with status ${response.status}: ${response.data}`
);
}

return response.data.trim();
} catch (error) {
console.error('[blockchainService] Transaction broadcast failed:', error);
throw new Error('Failed to broadcast transaction');
}
}