diff --git a/.gitignore b/.gitignore
index afb9f7b..41beeca 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,9 @@
# windsurf rules
.windsurfrules
-
+./backend
+backend
+mempool
# Logs
logs
*.log
diff --git a/docs/api-method-protorunesbyoutpoint-implementation-plan.md b/docs/api-method-protorunesbyoutpoint-implementation-plan.md
new file mode 100644
index 0000000..c440d59
--- /dev/null
+++ b/docs/api-method-protorunesbyoutpoint-implementation-plan.md
@@ -0,0 +1,352 @@
+# Implementation Plan: alkanes_protorunesbyoutpoint API Method
+
+## Overview
+
+This document outlines the implementation plan for the `alkanes_protorunesbyoutpoint` JSON-RPC method in the METAGRAPH application. This method retrieves Protorunes by outpoint (txid, vout) at a specific block height.
+
+## Key Requirements
+
+1. Implement a direct fetch method instead of using the SDK
+2. Document the function well, making explicit the fact that the txid is reversed
+3. Default the protocol ID but make it clear that it can change
+4. Ensure the method works across different network providers (oylnet, mainnet, regtest)
+
+## Implementation Details
+
+### 1. SDK Function Implementation
+
+Create a new function in `src/sdk/alkanes.js` that makes a direct fetch request to the API endpoint:
+
+```javascript
+/**
+ * Gets Protorunes by outpoint at a specific block height using direct JSON-RPC call
+ * @param {Object} params - Parameters for the query
+ * @param {string} params.txid - Transaction ID (will be reversed for the API call)
+ * @param {number} params.vout - Output index
+ * @param {string} params.protocolTag - Protocol tag (default: "1")
+ * @param {number} height - Block height to query
+ * @param {string} endpoint - API endpoint to use ('regtest', 'mainnet', 'oylnet')
+ * @returns {Promise} - Protorunes at the specified outpoint and height
+ */
+export const getProtorunesByOutpoint = async (params, height, endpoint = 'regtest') => {
+ try {
+ console.log(`Getting Protorunes by outpoint ${params.txid}:${params.vout} at height ${height} with ${endpoint} endpoint`);
+
+ // Validate inputs
+ if (!params.txid || typeof params.txid !== 'string') {
+ throw new Error('Invalid txid: must be a non-empty string');
+ }
+
+ if (params.vout === undefined || params.vout === null || isNaN(parseInt(params.vout, 10))) {
+ throw new Error('Invalid vout: must be a number');
+ }
+
+ if (!height || isNaN(parseInt(height, 10))) {
+ throw new Error('Invalid height: must be a number');
+ }
+
+ // Reverse the txid for the API call
+ // This converts from the standard display format to the internal byte order
+ const reversedTxid = params.txid
+ .match(/.{2}/g)
+ ?.reverse()
+ .join('') || params.txid;
+
+ // Determine the API URL based on the endpoint
+ const url = endpoint === 'mainnet' ? 'https://mainnet.sandshrew.io/v2/lasereyes' :
+ endpoint === 'oylnet' ? 'https://oylnet.oyl.gg/v2/lasereyes' :
+ 'http://localhost:18888/v1/lasereyes';
+
+ // Make the JSON-RPC request
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 0,
+ method: 'alkanes_protorunesbyoutpoint',
+ params: [
+ {
+ protocolTag: params.protocolTag || '1',
+ txid: reversedTxid,
+ vout: parseInt(params.vout, 10)
+ },
+ parseInt(height, 10)
+ ]
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message || 'Error fetching Protorunes by outpoint');
+ }
+
+ return {
+ status: "success",
+ message: "Protorunes retrieved",
+ outpoint: `${params.txid}:${params.vout}`,
+ height,
+ protorunes: data.result
+ };
+ } catch (error) {
+ console.error('Error getting Protorunes by outpoint:', error);
+ return {
+ status: "error",
+ message: error.message || "Unknown error",
+ outpoint: `${params.txid}:${params.vout}`,
+ height
+ };
+ }
+};
+```
+
+### 2. Form Component Implementation
+
+Create a new form component in `src/components/methods/ProtorunesByOutpointForm.jsx`:
+
+```jsx
+import React from 'react';
+import { Link } from 'react-router-dom';
+import APIForm from '../shared/APIForm';
+import { getProtorunesByOutpoint } from '../../sdk';
+
+/**
+ * ProtorunesByOutpointForm Component
+ *
+ * Form specific to the protorunesByOutpoint API method.
+ * Gets Protorunes by outpoint at a specific block height.
+ *
+ * @param {Object} props
+ * @param {string} props.endpoint - Current endpoint (regtest, mainnet, oylnet)
+ */
+const ProtorunesByOutpointForm = ({ endpoint = 'mainnet' }) => {
+ // Define method details
+ const methodDetails = {
+ 'Method Type': 'View Function',
+ 'JSON-RPC Method': 'alkanes_protorunesbyoutpoint',
+ 'Required Parameters': 'txid (transaction ID), vout (output index), height (block height)'
+ };
+
+ // Define parameters for the form
+ const parameters = [
+ {
+ name: 'txid',
+ label: 'Transaction ID',
+ placeholder: '64c85a06ca2ac0e2d3bb7dc3f1a69a83c0fb23f11638c45d250633f20bb0dc06',
+ description: () => (
+
+ The transaction ID to query. Important: The txid will be reversed internally when making the API call. The txid you enter should be in the standard display format as seen on block explorers. View example transaction on
+ mempool.space
+
+
+ ),
+ required: true
+ },
+ {
+ name: 'vout',
+ label: 'Output Index',
+ placeholder: '0',
+ description: () => (
+
+ The output index (vout) to query. For protostones, vout indexing begins at tx.output.length + 1. Protostones are laid out in the " shadow vout" range.
+
+ ),
+ required: true
+ },
+ {
+ name: 'height',
+ label: 'Block Height',
+ placeholder: '890550',
+ description: 'The block height at which to query the outpoint.',
+ required: true
+ },
+ {
+ name: 'protocolTag',
+ label: 'Protocol Tag',
+ placeholder: '1',
+ description: 'The protocol tag to use. Defaults to "1" if not specified, but can be changed for different protocols.',
+ required: false
+ }
+ ];
+
+ // Handle form submission
+ const handleSubmit = async (values) => {
+ const { txid, vout, height, protocolTag = '1' } = values;
+
+ // Call the SDK function
+ return await getProtorunesByOutpoint(
+ {
+ txid,
+ vout: parseInt(vout, 10),
+ protocolTag
+ },
+ parseInt(height, 10),
+ endpoint
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default ProtorunesByOutpointForm;
+```
+
+### 3. Update Routes
+
+Update `src/routes.jsx` to include the new method page:
+
+```jsx
+// Add import for the new form component
+import ProtorunesByOutpointForm from './components/methods/ProtorunesByOutpointForm';
+
+// Add to the routes array
+{
+ path: 'api-methods/protorunesbyoutpoint',
+ element:
+}
+```
+
+### 4. Update APIMethodPage
+
+Update the method components map in `src/pages/APIMethodPage.jsx`:
+
+```jsx
+// Add import for the new form component
+import ProtorunesByOutpointForm from '../components/methods/ProtorunesByOutpointForm';
+
+// Update the methodComponents object
+const methodComponents = {
+ 'trace': TraceForm,
+ 'simulate': SimulateForm,
+ 'traceblockstatus': TraceBlockStatusForm,
+ 'protorunesbyoutpoint': ProtorunesByOutpointForm,
+ // Add other methods as they are implemented
+};
+```
+
+### 5. Update SDK Index
+
+Ensure the new function is exported from the SDK index file:
+
+```jsx
+// In src/sdk/index.js
+export {
+ // ... other exports
+ getProtorunesByOutpoint
+} from './alkanes';
+```
+
+## Implementation Considerations
+
+### 1. Txid Reversal
+
+The txid reversal is a critical aspect of this implementation. Bitcoin transaction IDs are typically displayed in a reversed format in block explorers compared to how they are used internally in the Bitcoin protocol. The implementation handles this reversal internally, so users can enter the txid as they see it in block explorers.
+
+The reversal logic is:
+```javascript
+const reversedTxid = txid
+ .match(/.{2}/g)
+ ?.reverse()
+ .join('') || txid;
+```
+
+This splits the txid into pairs of characters (bytes), reverses the order, and joins them back together.
+
+### 2. Error Handling
+
+The implementation includes comprehensive error handling:
+- Input validation to ensure txid, vout, and height are valid
+- Error handling for API request failures
+- Structured error responses with meaningful messages
+
+### 3. Documentation
+
+The form component includes detailed documentation:
+- Clear explanation of the txid reversal behavior
+- Description of the protocol tag parameter and its default value
+- Links to relevant documentation (shadow vout)
+- Example request and response
+
+### 4. Network Support
+
+The implementation supports all three network environments:
+- Mainnet: https://mainnet.sandshrew.io/v2/lasereyes
+- Oylnet: https://oylnet.oyl.gg/v2/lasereyes
+- Regtest: http://localhost:18888/v1/lasereyes
+
+## Testing Plan
+
+1. Test with valid inputs on each network environment
+2. Test with invalid txid format
+3. Test with non-existent txid
+4. Test with invalid vout
+5. Test with invalid height
+6. Test with different protocol tags
+7. Verify txid reversal works correctly
+
+## Next Steps
+
+1. Switch to Code mode to implement the changes
+2. Test the implementation with real data
+3. Update the progress.md file to reflect the completion of this task
+4. Consider adding additional validation or features as needed
\ No newline at end of file
diff --git a/src/components/methods/ProtorunesByOutpointForm.jsx b/src/components/methods/ProtorunesByOutpointForm.jsx
new file mode 100644
index 0000000..26a45b3
--- /dev/null
+++ b/src/components/methods/ProtorunesByOutpointForm.jsx
@@ -0,0 +1,129 @@
+import React from 'react';
+import { Link } from 'react-router-dom';
+import APIForm from '../shared/APIForm';
+import { getProtorunesByOutpoint } from '../../sdk';
+
+/**
+ * ProtorunesByOutpointForm Component
+ *
+ * Form specific to the protorunesByOutpoint API method.
+ * Gets Protorunes by outpoint at a specific block height.
+ *
+ * @param {Object} props
+ * @param {string} props.endpoint - Current endpoint (regtest, mainnet, oylnet)
+ */
+const ProtorunesByOutpointForm = ({ endpoint = 'mainnet' }) => {
+ // Define method details
+ const methodDetails = {
+ 'Method Type': 'View Function',
+ 'JSON-RPC Method': 'alkanes_protorunesbyoutpoint',
+ 'Required Parameters': 'txid (transaction ID), vout (output index), height (block height)',
+ 'Response Format': 'Returns token information with ID in hex format (converted to decimal for readability) and value in hex format with 8 decimals'
+ };
+
+ // Define parameters for the form
+ const parameters = [
+ {
+ name: 'txid',
+ label: 'Transaction ID',
+ placeholder: '64c85a06ca2ac0e2d3bb7dc3f1a69a83c0fb23f11638c45d250633f20bb0dc06',
+ description: () => (
+
+ The transaction ID to query. Important: The txid will be reversed internally when making the API call. The txid you enter should be in the standard display format as seen on block explorers. View example transaction on
+ mempool.space
+
+
+ ),
+ required: true
+ },
+ {
+ name: 'vout',
+ label: 'Output Index',
+ placeholder: '0',
+ description: 'The output index (vout) to query.',
+ required: true
+ },
+ {
+ name: 'height',
+ label: 'Block Height',
+ placeholder: '890550',
+ description: 'The block height at which to query the outpoint.',
+ required: true
+ },
+ {
+ name: 'protocolTag',
+ label: 'Protocol Tag',
+ placeholder: '1',
+ description: 'The protocol tag to use. Defaults to "1" if not specified, but can be changed for different protocols.',
+ required: false
+ }
+ ];
+
+ // Handle form submission
+ const handleSubmit = async (values) => {
+ const { txid, vout, height, protocolTag = '1' } = values;
+
+ // Call the SDK function
+ return await getProtorunesByOutpoint(
+ {
+ txid,
+ vout: parseInt(vout, 10),
+ protocolTag
+ },
+ parseInt(height, 10),
+ endpoint
+ );
+ };
+
+ return (
+
+ );
+};
+
+export default ProtorunesByOutpointForm;
\ No newline at end of file
diff --git a/src/pages/APIMethodPage.jsx b/src/pages/APIMethodPage.jsx
index b98035f..8eb3500 100644
--- a/src/pages/APIMethodPage.jsx
+++ b/src/pages/APIMethodPage.jsx
@@ -3,6 +3,7 @@ import { useParams, useNavigate, useOutletContext } from 'react-router-dom';
import TraceBlockStatusForm from '../components/methods/TraceBlockStatusForm';
import SimulateForm from '../components/methods/SimulateForm';
import TraceForm from '../components/methods/TraceForm';
+import ProtorunesByOutpointForm from '../components/methods/ProtorunesByOutpointForm';
/**
* APIMethodPage Component
@@ -20,6 +21,7 @@ const APIMethodPage = ({ methodComponent: ProvidedMethodComponent, methodName: p
'trace': TraceForm,
'simulate': SimulateForm,
'traceblockstatus': TraceBlockStatusForm,
+ 'protorunesbyoutpoint': ProtorunesByOutpointForm,
// Add other methods as they are implemented
};
diff --git a/src/pages/BitcoinAddressExplorer.jsx b/src/pages/BitcoinAddressExplorer.jsx
index d1dacdf..f4d5528 100644
--- a/src/pages/BitcoinAddressExplorer.jsx
+++ b/src/pages/BitcoinAddressExplorer.jsx
@@ -1,10 +1,11 @@
import React, { useState, useEffect } from 'react';
import { useOutletContext } from 'react-router-dom';
import { useLaserEyes } from '@omnisat/lasereyes';
-import {
- getAddressTransactions,
- getTransactionInfo,
- getAddressTransactionsWithTrace
+import {
+ getAddressInfo,
+ getAddressTransactionsChain,
+ getTransactionInfo,
+ getAddressTransactionsWithTrace
} from '../sdk/esplora';
import { traceTransaction } from '../sdk/alkanes';
@@ -33,6 +34,7 @@ const BitcoinAddressExplorer = () => {
const [totalPages, setTotalPages] = useState(1);
const [totalTransactions, setTotalTransactions] = useState(0);
const [pageLoading, setPageLoading] = useState(false);
+ const [lastSeenTxid, setLastSeenTxid] = useState(null);
const transactionsPerPage = 10;
// Helper function to shorten txids
@@ -120,12 +122,18 @@ const BitcoinAddressExplorer = () => {
try {
console.log(`Searching for transactions on network ${endpoint} for address ${addressToUse}`);
- // Fetch first page of transactions with pagination
- const result = await getAddressTransactions(
+ // First get the address info to get the total transaction count
+ const addressInfoResult = await getAddressInfo(addressToUse, endpoint);
+
+ if (addressInfoResult.status === "error") {
+ throw new Error(addressInfoResult.message);
+ }
+
+ // Fetch first page of transactions with cursor-based pagination
+ const result = await getAddressTransactionsChain(
addressToUse,
endpoint,
- transactionsPerPage,
- 0
+ null // null for first page
);
if (result.status === "error") {
@@ -138,15 +146,17 @@ const BitcoinAddressExplorer = () => {
setAddress(addressToUse);
// Set pagination data
- if (result.pagination) {
- setTotalTransactions(result.pagination.total);
- setTotalPages(Math.max(1, Math.ceil(result.pagination.total / transactionsPerPage)));
- } else {
- // Fallback if pagination info is not available
- setTotalTransactions(txs.length);
- setTotalPages(Math.max(1, Math.ceil(txs.length / transactionsPerPage)));
+ const totalTxCount = addressInfoResult.totalTxCount;
+ setTotalTransactions(totalTxCount);
+ setTotalPages(Math.max(1, Math.ceil(totalTxCount / transactionsPerPage)));
+
+ // Store the last seen txid for pagination
+ if (txs.length > 0) {
+ setLastSeenTxid(txs[txs.length - 1].txid);
}
+ console.log(`Total transactions: ${totalTxCount}, Pages: ${Math.ceil(totalTxCount / transactionsPerPage)}`);
+
} catch (err) {
console.error("Error fetching transactions data:", err);
setError(err.message || "Failed to fetch transactions data");
@@ -208,27 +218,86 @@ const BitcoinAddressExplorer = () => {
setPageLoading(true);
try {
- const offset = (pageNumber - 1) * transactionsPerPage;
-
- // Fetch transactions for the specified page
- const result = await getAddressTransactions(
- address,
- endpoint,
- transactionsPerPage,
- offset
- );
-
- if (result.status === "error") {
- throw new Error(result.message);
- }
-
- // Update transactions
- setTransactions(result.transactions || []);
-
- // Update pagination data if available
- if (result.pagination) {
- setTotalTransactions(result.pagination.total);
- setTotalPages(Math.max(1, Math.ceil(result.pagination.total / transactionsPerPage)));
+ // For page 1, we don't need a lastSeenTxid
+ if (pageNumber === 1) {
+ // Get address info first to get total transaction count
+ const addressInfoResult = await getAddressInfo(address, endpoint);
+
+ if (addressInfoResult.status === "error") {
+ throw new Error(addressInfoResult.message);
+ }
+
+ // Fetch first page of transactions
+ const result = await getAddressTransactionsChain(
+ address,
+ endpoint,
+ null // null for first page
+ );
+
+ if (result.status === "error") {
+ throw new Error(result.message);
+ }
+
+ // Update transactions
+ const txs = result.transactions || [];
+ setTransactions(txs);
+
+ // Update pagination data
+ setTotalTransactions(addressInfoResult.totalTxCount);
+
+ // Store the last seen txid for pagination
+ if (txs.length > 0) {
+ setLastSeenTxid(txs[txs.length - 1].txid);
+ }
+ } else {
+ // For pages > 1, we need to implement a different approach
+ // since we're using cursor-based pagination
+
+ // This is a simplified implementation - in a real app, you would
+ // need to keep track of the lastSeenTxid for each page
+
+ // For now, we'll just fetch the first page and then fetch additional
+ // pages one by one until we reach the requested page
+
+ let currentPage = 1;
+ let currentLastSeenTxid = null;
+ let currentTxs = [];
+
+ while (currentPage < pageNumber) {
+ const result = await getAddressTransactionsChain(
+ address,
+ endpoint,
+ currentLastSeenTxid
+ );
+
+ if (result.status === "error" || !result.transactions || !result.transactions.length) {
+ break;
+ }
+
+ currentTxs = result.transactions;
+ currentLastSeenTxid = result.pagination.lastSeenTxid;
+ currentPage++;
+ }
+
+ // Now fetch the actual page we want
+ const result = await getAddressTransactionsChain(
+ address,
+ endpoint,
+ currentLastSeenTxid
+ );
+
+ if (result.status === "error") {
+ throw new Error(result.message);
+ }
+
+ // Update transactions
+ const txs = result.transactions || [];
+ setTransactions(txs);
+
+ // Store the last seen txid for pagination
+ if (txs.length > 0) {
+ setLastSeenTxid(txs[txs.length - 1].txid);
+ }
}
// Scroll to top of results
diff --git a/src/pages/Home.jsx b/src/pages/Home.jsx
index 77b0914..d275965 100644
--- a/src/pages/Home.jsx
+++ b/src/pages/Home.jsx
@@ -164,6 +164,13 @@ const Home = () => {
Simulate Alkanes operations to preview outcomes without broadcasting to the network
+
+
+
/api-methods/protorunesbyoutpoint
+
+ Query Protorunes by outpoint (txid, vout) at a specific block height
+
+
@@ -205,6 +212,13 @@ const Home = () => {
Explore transactions for an address
+
+
+
/explorer/transaction-io
+
+ Explore transaction inputs and outputs
+
+
diff --git a/src/pages/TransactionInputsOutputsExplorer.jsx b/src/pages/TransactionInputsOutputsExplorer.jsx
new file mode 100644
index 0000000..490a155
--- /dev/null
+++ b/src/pages/TransactionInputsOutputsExplorer.jsx
@@ -0,0 +1,962 @@
+import React, { useState, useEffect } from 'react';
+import { useOutletContext } from 'react-router-dom';
+import { useLaserEyes } from '@omnisat/lasereyes';
+import {
+ getAddressInfo,
+ getAddressTransactionsChain,
+ getTransactionInfo,
+ getTransactionOutspends
+} from '../sdk/esplora';
+import { getProtorunesByOutpoint } from '../sdk/alkanes';
+import getProvider from '../sdk/provider';
+
+/**
+ * TransactionInputsOutputsExplorer Component
+ *
+ * Page for exploring Bitcoin transaction inputs and outputs
+ * Allows users to search for an address and view all its transactions
+ * with detailed inputs and outputs visualization
+ */
+const TransactionInputsOutputsExplorer = () => {
+ const { endpoint = 'mainnet' } = useOutletContext() || {};
+ const { connected, address: walletAddress } = useLaserEyes();
+
+ // State for address input
+ const [address, setAddress] = useState('');
+ const [manualAddress, setManualAddress] = useState('');
+
+ // State for transaction data
+ const [transactions, setTransactions] = useState([]);
+ const [processedTransactions, setProcessedTransactions] = useState([]);
+ const [loading, setLoading] = useState(false);
+ const [error, setError] = useState(null);
+
+ // State for pagination
+ const [page, setPage] = useState(1);
+ const [totalPages, setTotalPages] = useState(1);
+ const [totalTransactions, setTotalTransactions] = useState(0);
+ const [pageLoading, setPageLoading] = useState(false);
+ const [fetchingAllTransactions, setFetchingAllTransactions] = useState(false);
+ const [fetchProgress, setFetchProgress] = useState(0);
+ const [maxTransactionsToFetch, setMaxTransactionsToFetch] = useState(10000);
+ const transactionsPerPage = 25; // Default page size from Esplora API
+
+ // Reset component state when endpoint/network changes
+ useEffect(() => {
+ // Reset the state to prevent issues when switching networks
+ setTransactions([]);
+ setProcessedTransactions([]);
+ setError(null);
+ setPage(1);
+ setTotalPages(1);
+
+ console.log(`Network switched to ${endpoint}`);
+ }, [endpoint]);
+
+ // Only populate the address field when both wallet connects and no address is already entered
+ useEffect(() => {
+ if (connected && walletAddress && !address && !manualAddress) {
+ // Only set the wallet address when both address and manualAddress are empty
+ // This prevents overriding any user input
+ setAddress(walletAddress);
+ }
+ }, [connected, walletAddress, address, manualAddress]);
+
+ // Helper function to shorten txids and addresses
+ const shortenTxid = (txid) => {
+ if (!txid) return 'N/A';
+ if (txid.length <= 13) return txid;
+ return `${txid.substring(0, 6)}...${txid.substring(txid.length - 6)}`;
+ };
+
+ const shortenAddress = (address) => {
+ if (!address) return 'Unknown';
+ if (address === 'OP_RETURN') return address;
+ if (address.length <= 15) return address;
+ return `${address.substring(0, 6)}...${address.substring(address.length - 6)}`;
+ };
+
+ // Function to copy text to clipboard
+ const copyToClipboard = (text) => {
+ if (!text) return;
+ navigator.clipboard.writeText(text)
+ .then(() => {
+ console.log('Copied to clipboard:', text);
+ })
+ .catch(err => {
+ console.error('Failed to copy text: ', err);
+ });
+ };
+
+ // Validate Bitcoin address (basic validation)
+ const isValidBitcoinAddress = (addr) => {
+ // Basic validation - check if it starts with valid prefixes
+ return addr && (
+ addr.startsWith('bc1') ||
+ addr.startsWith('1') ||
+ addr.startsWith('3') ||
+ addr.startsWith('bcr') || //regtest
+ addr.startsWith('tb1') || // testnet
+ addr.startsWith('m') || // testnet
+ addr.startsWith('n') || // testnet
+ addr.startsWith('2') // testnet
+ );
+ };
+
+ // Format date from timestamp
+ const formatDate = (timestamp) => {
+ if (!timestamp) return 'N/A';
+ const date = new Date(timestamp * 1000);
+ return date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
+ };
+
+ // Helper function to convert hex to decimal
+ const hexToDec = (hexString) => {
+ if (!hexString || typeof hexString !== 'string') return 'N/A';
+ // Remove '0x' prefix if present
+ const cleanHex = hexString.startsWith('0x') ? hexString.slice(2) : hexString;
+ try {
+ return BigInt(`0x${cleanHex}`).toString(10);
+ } catch (error) {
+ console.error("Error converting hex to decimal:", error);
+ return hexString; // Return original if conversion fails
+ }
+ };
+
+ // Note: We now use the getAddressInfo function from esplora.js
+
+ // Fetch all transactions for an address up to the maximum limit
+ const fetchAllTransactions = async (address) => {
+ try {
+ setFetchingAllTransactions(true);
+ setFetchProgress(0);
+
+ // First get the total transaction count
+ const addressInfoResult = await getAddressInfo(address, endpoint);
+
+ if (addressInfoResult.status === "error") {
+ throw new Error(addressInfoResult.message);
+ }
+
+ const totalTxCount = Math.min(addressInfoResult.totalTxCount, maxTransactionsToFetch);
+ setTotalTransactions(totalTxCount);
+
+ // Start with an empty array of transactions
+ let allTransactions = [];
+ let lastSeenTxid = null;
+
+ // Update progress
+ let progress = 0;
+ setFetchProgress(progress);
+
+ // Fetch transactions in batches using cursor-based pagination
+ while (allTransactions.length < totalTxCount && allTransactions.length < maxTransactionsToFetch) {
+ // Fetch the next batch
+ const result = await getAddressTransactionsChain(
+ address,
+ endpoint,
+ lastSeenTxid
+ );
+
+ if (result.status === "error" || !result.transactions || !result.transactions.length) {
+ break; // No more transactions or error
+ }
+
+ // Add transactions to our collection
+ allTransactions = [...allTransactions, ...result.transactions];
+
+ // Update progress
+ progress = Math.min(100, Math.round((allTransactions.length / totalTxCount) * 100));
+ setFetchProgress(progress);
+
+ // Update the last seen txid for the next batch
+ lastSeenTxid = result.pagination.lastSeenTxid;
+
+ // If we don't have more transactions or we've reached the maximum, stop fetching
+ if (!result.pagination.hasMore || allTransactions.length >= maxTransactionsToFetch) {
+ break;
+ }
+ }
+
+ return {
+ status: "success",
+ message: "All transactions retrieved",
+ address,
+ transactions: allTransactions,
+ pagination: {
+ total: totalTxCount,
+ fetched: allTransactions.length
+ }
+ };
+ } catch (error) {
+ console.error('Error fetching all transactions:', error);
+ return {
+ status: "error",
+ message: error.message || "Unknown error",
+ address,
+ transactions: []
+ };
+ } finally {
+ setFetchingAllTransactions(false);
+ }
+ };
+
+ // Handle form submission
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+
+ // Reset all states before making a new request
+ setError(null);
+ setPage(1);
+
+ // Validate address
+ const addressToUse = manualAddress || address;
+ if (!addressToUse) {
+ setError("Please enter an address");
+ return;
+ }
+
+ if (!isValidBitcoinAddress(addressToUse)) {
+ setError("Please enter a valid Bitcoin address");
+ return;
+ }
+
+ // Set loading state
+ setLoading(true);
+ setTransactions([]); // Clear previous results
+ setProcessedTransactions([]);
+
+ try {
+ console.log(`Searching for transactions on network ${endpoint} for address ${addressToUse}`);
+
+ // Fetch all transactions for the address (up to maxTransactionsToFetch)
+ const result = await fetchAllTransactions(addressToUse);
+
+ if (result.status === "error") {
+ throw new Error(result.message);
+ }
+
+ // Set data
+ const txs = result.transactions || [];
+ setTransactions(txs);
+ setAddress(addressToUse);
+
+ // Set pagination data for UI display
+ if (result.pagination) {
+ setTotalTransactions(result.pagination.total);
+ // Calculate total pages based on the number of transactions we actually fetched
+ const calculatedPages = Math.max(1, Math.ceil(txs.length / transactionsPerPage));
+ setTotalPages(calculatedPages);
+
+ console.log(`Total transactions: ${result.pagination.total}, Fetched: ${txs.length}, Pages: ${calculatedPages}`);
+ } else {
+ // Fallback if pagination info is not available
+ setTotalTransactions(txs.length);
+ setTotalPages(Math.max(1, Math.ceil(txs.length / transactionsPerPage)));
+
+ console.log(`Total transactions: ${txs.length}, Pages: ${Math.ceil(txs.length / transactionsPerPage)}`);
+ }
+
+ // Process transactions to get inputs and outputs
+ await processTransactions(txs);
+
+ } catch (err) {
+ console.error("Error fetching transactions data:", err);
+ setError(err.message || "Failed to fetch transactions data");
+ setTransactions([]);
+ setProcessedTransactions([]);
+ setTotalTransactions(0);
+ setTotalPages(1);
+ } finally {
+ setLoading(false);
+ }
+ };
+
+ // Process transactions to get detailed input and output information
+ const processTransactions = async (txs) => {
+ const processed = [];
+
+ // Process transactions in batches to avoid overwhelming the browser
+ const batchSize = 25;
+ const totalBatches = Math.ceil(txs.length / batchSize);
+
+ for (let batchIndex = 0; batchIndex < totalBatches; batchIndex++) {
+ const startIndex = batchIndex * batchSize;
+ const endIndex = Math.min(startIndex + batchSize, txs.length);
+ const batch = txs.slice(startIndex, endIndex);
+
+ console.log(`Processing batch ${batchIndex + 1}/${totalBatches} (${startIndex}-${endIndex} of ${txs.length} transactions)`);
+
+ // Process each transaction in the batch
+ for (const tx of batch) {
+ try {
+ // Get detailed transaction info
+ const txInfo = await getTransactionInfo(tx.txid, endpoint);
+
+ if (txInfo.status === "error") {
+ processed.push({
+ ...tx,
+ error: txInfo.message,
+ inputs: [],
+ outputs: []
+ });
+ continue;
+ }
+
+ const transaction = txInfo.transaction;
+
+ // Get outspends to determine if outputs have been spent
+ const outspends = await getTransactionOutspends(tx.txid, endpoint);
+
+ // Process inputs
+ const inputs = transaction.vin.map(input => ({
+ txid: input.txid,
+ vout: input.vout,
+ address: input.prevout?.scriptpubkey_address || 'Unknown',
+ value: input.prevout?.value || 0,
+ valueBTC: (input.prevout?.value || 0) / 100000000, // Convert satoshis to BTC
+ isCoinbase: input.is_coinbase || false
+ }));
+
+ // Process outputs
+ const outputs = await Promise.all(transaction.vout.map(async (output, index) => {
+ const isSpent = outspends.status === "success" &&
+ outspends.outspends &&
+ outspends.outspends[index] &&
+ outspends.outspends[index].spent;
+
+ // Check for Alkanes at this outpoint
+ let alkanes = null;
+ try {
+ // Use the transaction's block height instead of current height
+ const blockHeight = tx.status?.block_height;
+
+ // If block height is not available, skip Alkanes check
+ if (!blockHeight) {
+ console.log(`Skipping Alkanes check for ${tx.txid}: No block height available`);
+ return null;
+ }
+
+ // Use the correct vout index
+ const voutIndex = index; // Use the array index to ensure we check all vouts
+
+ console.log(`Checking for Alkanes at outpoint ${tx.txid}:${voutIndex} (output.n: ${output.n}) at block height ${blockHeight}`);
+
+ // Call the alkanes_protorunesbyoutpoint function
+ const alkanesResult = await getProtorunesByOutpoint(
+ {
+ txid: tx.txid,
+ vout: voutIndex,
+ protocolTag: '1'
+ },
+ blockHeight,
+ endpoint
+ );
+
+ console.log(`Alkanes result for ${tx.txid}:${voutIndex}:`, alkanesResult);
+
+ // Check if we got a valid result with Alkanes
+ if (alkanesResult.result && alkanesResult.result.length > 0) {
+ console.log(`Found Alkanes at outpoint ${tx.txid}:${voutIndex}:`, alkanesResult.result);
+
+ alkanes = alkanesResult.result.map(item => {
+ const token = item.token || {};
+ const id = token.id || {};
+
+ // Convert hex block and tx values to decimal
+ const blockHex = id.block || "0x0";
+ const txHex = id.tx || "0x0";
+ const blockDecimal = parseInt(blockHex, 16);
+ const txDecimal = parseInt(txHex, 16);
+
+ // Convert hex value to decimal and divide by 10^8 (8 decimals)
+ const valueHex = item.value || "0x0";
+ const valueDecimal = parseInt(valueHex, 16);
+ const valueFormatted = valueDecimal / 100000000; // Divide by 10^8
+
+ return {
+ token: {
+ ...token,
+ id: {
+ ...id,
+ blockDecimal,
+ txDecimal,
+ formatted: `[${blockDecimal},${txDecimal}]`
+ }
+ },
+ valueHex: item.value,
+ valueDecimal,
+ valueFormatted
+ };
+ });
+ }
+ } catch (error) {
+ console.error(`Error checking for Alkanes at outpoint ${tx.txid}:${output.n}:`, error);
+ }
+
+ return {
+ n: output.n,
+ address: output.scriptpubkey_address || 'OP_RETURN',
+ value: output.value || 0,
+ valueBTC: (output.value || 0) / 100000000, // Convert satoshis to BTC
+ type: output.scriptpubkey_type,
+ isOpReturn: output.scriptpubkey_type === 'op_return',
+ spent: isSpent,
+ alkanes: alkanes // Add Alkanes information if found
+ };
+ }));
+
+ // Calculate total input and output values
+ const totalInput = inputs.reduce((sum, input) => sum + input.value, 0);
+ const totalOutput = outputs.reduce((sum, output) => sum + output.value, 0);
+
+ processed.push({
+ ...tx,
+ inputs,
+ outputs,
+ totalInput,
+ totalOutput,
+ totalInputBTC: totalInput / 100000000,
+ totalOutputBTC: totalOutput / 100000000,
+ fee: totalInput - totalOutput,
+ feeBTC: (totalInput - totalOutput) / 100000000
+ });
+
+ } catch (error) {
+ console.error(`Error processing transaction ${tx.txid}:`, error);
+ processed.push({
+ ...tx,
+ error: error.message,
+ inputs: [],
+ outputs: []
+ });
+ }
+ }
+
+ // Update the processed transactions after each batch
+ setProcessedTransactions([...processed]);
+
+ // Update progress
+ const progress = Math.min(100, Math.round((processed.length / txs.length) * 100));
+ setFetchProgress(progress);
+ }
+
+ console.log(`Processed ${processed.length} transactions in total`);
+ setProcessedTransactions(processed);
+ };
+
+ // Get current page transactions - paginate locally since we've fetched all transactions
+ const getCurrentPageTransactions = () => {
+ const startIndex = (page - 1) * transactionsPerPage;
+ const endIndex = startIndex + transactionsPerPage;
+ const pageTransactions = processedTransactions.slice(startIndex, endIndex);
+
+ console.log(`Getting page ${page} transactions: ${startIndex}-${endIndex} of ${processedTransactions.length}`);
+ return pageTransactions;
+ };
+
+ // Handle pagination - now just updates the page state for local pagination
+ const handlePreviousPage = () => {
+ if (page > 1) {
+ const newPage = page - 1;
+ setPage(newPage);
+ // Scroll to top of results
+ window.scrollTo(0, 0);
+ }
+ };
+
+ const handleNextPage = () => {
+ if (page < totalPages) {
+ const newPage = page + 1;
+ setPage(newPage);
+ // Scroll to top of results
+ window.scrollTo(0, 0);
+ }
+ };
+
+ // Handle using connected wallet
+ const useConnectedWallet = () => {
+ if (connected && walletAddress) {
+ setManualAddress('');
+ setAddress(walletAddress);
+ }
+ };
+
+ // CSS for inline styling according to design guidelines
+ const styles = {
+ container: {
+ width: '100%',
+ maxWidth: '1200px',
+ margin: '0 auto',
+ backgroundColor: '#FFFFFF',
+ padding: '20px',
+ border: '1px solid #E0E0E0',
+ },
+ title: {
+ fontSize: '24px',
+ fontWeight: 'bold',
+ marginBottom: '16px',
+ textAlign: 'left',
+ fontFamily: 'Roboto Mono, monospace',
+ },
+ subtitle: {
+ fontSize: '20px',
+ fontWeight: 'bold',
+ marginBottom: '12px',
+ textAlign: 'left',
+ fontFamily: 'Roboto Mono, monospace',
+ },
+ description: {
+ fontSize: '14px',
+ marginBottom: '20px',
+ textAlign: 'left',
+ fontFamily: 'Roboto Mono, monospace',
+ },
+ section: {
+ marginBottom: '20px',
+ padding: '20px',
+ backgroundColor: '#FFFFFF',
+ border: '1px solid #E0E0E0',
+ },
+ form: {
+ display: 'flex',
+ flexDirection: 'column',
+ gap: '16px',
+ },
+ formRow: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '10px',
+ flexWrap: 'wrap',
+ },
+ label: {
+ fontWeight: 'bold',
+ marginBottom: '8px',
+ display: 'block',
+ fontFamily: 'Roboto Mono, monospace',
+ fontSize: '14px',
+ },
+ input: {
+ padding: '8px',
+ border: '1px solid #E0E0E0',
+ borderRadius: '4px',
+ width: '100%',
+ fontFamily: 'Roboto Mono, monospace',
+ fontSize: '14px',
+ },
+ button: {
+ backgroundColor: '#000000',
+ color: '#FFFFFF',
+ border: 'none',
+ padding: '8px 16px',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontFamily: 'Roboto Mono, monospace',
+ fontSize: '14px',
+ fontWeight: 'bold',
+ },
+ secondaryButton: {
+ backgroundColor: '#FFFFFF',
+ color: '#000000',
+ border: '1px solid #000000',
+ padding: '8px 16px',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontFamily: 'Roboto Mono, monospace',
+ fontSize: '14px',
+ },
+ disabledButton: {
+ backgroundColor: '#CCCCCC',
+ color: '#666666',
+ border: 'none',
+ padding: '8px 16px',
+ borderRadius: '4px',
+ cursor: 'not-allowed',
+ fontFamily: 'Roboto Mono, monospace',
+ fontSize: '14px',
+ },
+ transactionCard: {
+ marginBottom: '20px',
+ padding: '15px',
+ border: '1px solid #E0E0E0',
+ borderRadius: '4px',
+ backgroundColor: '#FFFFFF',
+ },
+ transactionHeader: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ marginBottom: '10px',
+ padding: '5px 0',
+ borderBottom: '1px solid #E0E0E0',
+ },
+ txid: {
+ fontFamily: 'monospace',
+ cursor: 'pointer',
+ },
+ inputOutputContainer: {
+ display: 'flex',
+ gap: '20px',
+ },
+ column: {
+ flex: 1,
+ padding: '10px',
+ backgroundColor: '#F5F5F5',
+ borderRadius: '4px',
+ },
+ columnHeader: {
+ fontSize: '16px',
+ fontWeight: 'bold',
+ marginBottom: '10px',
+ textAlign: 'center',
+ },
+ item: {
+ display: 'flex',
+ justifyContent: 'space-between',
+ alignItems: 'center',
+ padding: '8px',
+ marginBottom: '5px',
+ backgroundColor: '#FFFFFF',
+ borderRadius: '4px',
+ border: '1px solid #E0E0E0',
+ },
+ itemAddress: {
+ display: 'flex',
+ alignItems: 'center',
+ gap: '5px',
+ },
+ address: {
+ fontFamily: 'monospace',
+ cursor: 'pointer',
+ },
+ itemValue: {
+ fontWeight: 'bold',
+ },
+ redCircle: {
+ color: '#F44336',
+ fontSize: '12px',
+ },
+ greenCircle: {
+ color: '#4CAF50',
+ fontSize: '12px',
+ },
+ totalRow: {
+ padding: '10px',
+ textAlign: 'right',
+ fontWeight: 'bold',
+ borderTop: '1px solid #E0E0E0',
+ marginTop: '10px',
+ },
+ feeRow: {
+ padding: '10px',
+ textAlign: 'right',
+ fontWeight: 'bold',
+ borderTop: '1px solid #E0E0E0',
+ marginTop: '10px',
+ },
+ pagination: {
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ gap: '10px',
+ marginTop: '20px',
+ },
+ paginationButton: {
+ padding: '5px 10px',
+ backgroundColor: '#000000',
+ color: '#FFFFFF',
+ border: 'none',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ },
+ pageInfo: {
+ fontFamily: 'Roboto Mono, monospace',
+ },
+ opReturn: {
+ fontFamily: 'monospace',
+ backgroundColor: '#E0E0E0',
+ padding: '2px 5px',
+ borderRadius: '4px',
+ },
+ runestone: {
+ backgroundColor: '#9C27B0',
+ color: '#FFFFFF',
+ padding: '2px 5px',
+ borderRadius: '4px',
+ marginLeft: '5px',
+ fontSize: '12px',
+ },
+ alkanes: {
+ backgroundColor: '#FF9800',
+ color: '#FFFFFF',
+ padding: '2px 5px',
+ borderRadius: '4px',
+ marginLeft: '5px',
+ fontSize: '12px',
+ },
+ alkanesDetails: {
+ marginTop: '5px',
+ padding: '5px',
+ backgroundColor: '#FFF3E0',
+ borderRadius: '4px',
+ fontSize: '12px',
+ fontFamily: 'monospace',
+ },
+ detailsButton: {
+ backgroundColor: '#2196F3',
+ color: '#FFFFFF',
+ border: 'none',
+ padding: '5px 10px',
+ borderRadius: '4px',
+ cursor: 'pointer',
+ fontSize: '12px',
+ },
+ progressContainer: {
+ marginTop: '20px',
+ marginBottom: '20px',
+ textAlign: 'center',
+ },
+ progressBar: {
+ width: '100%',
+ height: '20px',
+ backgroundColor: '#E0E0E0',
+ borderRadius: '4px',
+ margin: '10px 0',
+ overflow: 'hidden',
+ },
+ progressBarFill: {
+ height: '100%',
+ backgroundColor: '#4CAF50',
+ borderRadius: '4px',
+ transition: 'width 0.3s ease',
+ },
+ transactionStats: {
+ marginBottom: '15px',
+ padding: '10px',
+ backgroundColor: '#F5F5F5',
+ borderRadius: '4px',
+ textAlign: 'center',
+ },
+ };
+
+ return (
+
+
+
Transaction Inputs & Outputs Explorer
+
+ Search for a Bitcoin address to view all its transactions with inputs and outputs.
+
+
+
+
+ {error && (
+
+ Error: {error}
+
+ )}
+
+
+ {address && (
+
+
Transactions for {address}
+
+ {loading ? (
+
Loading transactions...
+ ) : fetchingAllTransactions ? (
+
+
Fetching transactions... {fetchProgress}% complete
+
+
This may take a while for addresses with many transactions.
+
+ ) : processedTransactions.length > 0 ? (
+
+
+
Showing {getCurrentPageTransactions().length} of {processedTransactions.length} transactions (Total: {totalTransactions})
+
+
+ {getCurrentPageTransactions().map((tx, index) => (
+
+
+
+ Transaction:
+ copyToClipboard(tx.txid)}
+ title="Click to copy"
+ >
+ {tx.txid}
+
+
+
+ Date: {formatDate(tx.status?.block_time)}
+
+
+
+
+
Inputs & Outputs
+ Details
+
+
+
+ {/* Inputs Column */}
+
+
Inputs
+ {tx.inputs.map((input, i) => (
+
+
+ ●
+ {input.isCoinbase ? (
+ Coinbase (New Coins)
+ ) : (
+ copyToClipboard(input.address)}
+ title="Click to copy"
+ >
+ {shortenAddress(input.address)}
+
+ )}
+
+
+ {input.valueBTC.toFixed(8)} BTC
+
+
+ ))}
+
+ Total: {tx.totalInputBTC.toFixed(8)} BTC
+
+
+
+ {/* Outputs Column */}
+
+
Outputs
+ {tx.outputs.map((output, i) => (
+
+
+
●
+ {output.isOpReturn ? (
+
+ OP_RETURN
+ Runestone
+
+ ) : (
+
+ copyToClipboard(output.address)}
+ title="Click to copy"
+ >
+ {shortenAddress(output.address)}
+
+ {output.alkanes && (
+ Alkanes
+ )}
+
+ )}
+
+
+ {output.valueBTC.toFixed(8)} BTC
+
+ {output.alkanes && (
+
+ {output.alkanes.map((alkane, j) => (
+
+
+ Token: {alkane.token.name} ({alkane.token.symbol})
+
+
+ ID: {alkane.token.id.formatted}
+
+
+ Value: {alkane.valueFormatted.toFixed(8)}
+
+
+ ))}
+
+ )}
+
+ ))}
+
+ Total: {tx.totalOutputBTC.toFixed(8)} BTC
+
+
+
+
+
+ Fee: {tx.feeBTC.toFixed(8)} BTC
+
+
+ ))}
+
+ {/* Pagination */}
+ {totalPages > 1 && (
+
+
+ Previous
+
+
+ Page {page} of {totalPages} ({processedTransactions.length} of {totalTransactions} transactions)
+
+
+ Next
+
+
+ )}
+
+ ) : (
+
No transactions found for this address.
+ )}
+
+ )}
+
+ );
+};
+
+export default TransactionInputsOutputsExplorer;
\ No newline at end of file
diff --git a/src/routes.jsx b/src/routes.jsx
index 3e4506c..6f9549d 100644
--- a/src/routes.jsx
+++ b/src/routes.jsx
@@ -9,9 +9,11 @@ import AlkanesBalanceExplorer from './pages/AlkanesBalanceExplorer';
import AlkanesTokensExplorer from './pages/AlkanesTokensExplorer';
import AlkanesTemplatesExplorer from './pages/AlkanesTemplatesExplorer';
import BitcoinAddressExplorer from './pages/BitcoinAddressExplorer';
+import TransactionInputsOutputsExplorer from './pages/TransactionInputsOutputsExplorer';
import TraceBlockStatusForm from './components/methods/TraceBlockStatusForm';
import SimulateForm from './components/methods/SimulateForm';
import TraceForm from './components/methods/TraceForm';
+import ProtorunesByOutpointForm from './components/methods/ProtorunesByOutpointForm';
/**
* Application Routes
@@ -50,6 +52,10 @@ const router = createBrowserRouter([
path: 'api-methods/simulate',
element:
},
+ {
+ path: 'api-methods/protorunesbyoutpoint',
+ element:
+ },
// Explorer routes
{
path: 'explorer/alkanes-tokens',
@@ -67,6 +73,10 @@ const router = createBrowserRouter([
path: 'explorer/address',
element:
},
+ {
+ path: 'explorer/transaction-io',
+ element:
+ },
// Not found route
{
path: '*',
diff --git a/src/sdk/alkanes.js b/src/sdk/alkanes.js
index fdd16cb..a9dfb9f 100644
--- a/src/sdk/alkanes.js
+++ b/src/sdk/alkanes.js
@@ -473,3 +473,83 @@ export const getAlkanesByHeight = async (height, endpoint = 'regtest') => {
};
}
};
+
+/**
+ * Gets Protorunes by outpoint at a specific block height using direct JSON-RPC call
+ * @param {Object} params - Parameters for the query
+ * @param {string} params.txid - Transaction ID (will be reversed for the API call)
+ * @param {number} params.vout - Output index
+ * @param {string} params.protocolTag - Protocol tag (default: "1")
+ * @param {number} height - Block height to query
+ * @param {string} endpoint - API endpoint to use ('regtest', 'mainnet', 'oylnet')
+ * @returns {Promise} - Protorunes at the specified outpoint and height
+ */
+export const getProtorunesByOutpoint = async (params, height, endpoint = 'regtest') => {
+ try {
+ console.log(`Getting Protorunes by outpoint ${params.txid}:${params.vout} at height ${height} with ${endpoint} endpoint`);
+
+ // Validate inputs
+ if (!params.txid || typeof params.txid !== 'string') {
+ throw new Error('Invalid txid: must be a non-empty string');
+ }
+
+ if (params.vout === undefined || params.vout === null || isNaN(parseInt(params.vout, 10))) {
+ throw new Error('Invalid vout: must be a number');
+ }
+
+ if (!height || isNaN(parseInt(height, 10))) {
+ throw new Error('Invalid height: must be a number');
+ }
+
+ // Reverse the txid for the API call
+ // This converts from the standard display format to the internal byte order
+ const reversedTxid = params.txid
+ .match(/.{2}/g)
+ ?.reverse()
+ .join('') || params.txid;
+
+ // Determine the API URL based on the endpoint
+ const url = endpoint === 'mainnet' ? 'https://mainnet.sandshrew.io/v2/lasereyes' :
+ endpoint === 'oylnet' ? 'https://oylnet.oyl.gg/v2/lasereyes' :
+ 'http://localhost:18888/v1/lasereyes';
+
+ // Make the JSON-RPC request
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 0,
+ method: 'alkanes_protorunesbyoutpoint',
+ params: [
+ {
+ protocolTag: params.protocolTag || '1',
+ txid: reversedTxid,
+ vout: parseInt(params.vout, 10)
+ },
+ parseInt(height, 10)
+ ]
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message || 'Error fetching Protorunes by outpoint');
+ }
+
+ // Return the exact API response format
+ return data;
+ } catch (error) {
+ console.error('Error getting Protorunes by outpoint:', error);
+ return {
+ error: {
+ message: error.message || "Unknown error"
+ },
+ id: 0,
+ jsonrpc: "2.0"
+ };
+ }
+};
diff --git a/src/sdk/esplora.js b/src/sdk/esplora.js
index c3f2ac1..b6d26aa 100644
--- a/src/sdk/esplora.js
+++ b/src/sdk/esplora.js
@@ -143,44 +143,111 @@ export const getTransactionOutspends = async (txid, endpoint = 'regtest') => {
};
/**
- * Gets transactions for a specific Bitcoin address with pagination support
+ * Gets address information including transaction count using the Esplora API
* @param {string} address - Bitcoin address to query
* @param {string} endpoint - API endpoint to use ('regtest', 'mainnet', 'oylnet')
- * @param {number} limit - Maximum number of transactions to return (default: 10)
- * @param {number} offset - Number of transactions to skip (default: 0)
- * @returns {Promise} - Transactions for the address with pagination info
+ * @returns {Promise} - Address information including transaction count
*/
-export const getAddressTransactions = async (address, endpoint = 'regtest', limit = 10, offset = 0) => {
+export const getAddressInfo = async (address, endpoint = 'regtest') => {
try {
const provider = getProvider(endpoint);
- console.log(`Getting transactions for address ${address} with ${endpoint} endpoint (limit: ${limit}, offset: ${offset})`);
-
- // Ensure provider.esplora exists
- if (!provider.esplora || typeof provider.esplora.getAddressTx !== 'function') {
- throw new Error('Esplora getAddressTx method not available');
+ console.log(`Getting address info for ${address} with ${endpoint} endpoint`);
+
+ // Make a direct fetch request to the Esplora API
+ const url = endpoint === 'mainnet' ? 'https://mainnet.sandshrew.io/v2/lasereyes' :
+ endpoint === 'oylnet' ? 'https://oylnet.oyl.gg/v2/lasereyes' :
+ 'http://localhost:18888/v1/lasereyes';
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'esplora_address',
+ params: [address]
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message || 'Error fetching address info');
}
- // Call the getAddressTx method
- // Note: The underlying API might not support pagination directly,
- // so we'll get all transactions and then slice them
- const allTransactions = await provider.esplora.getAddressTx(address);
+ const result = data.result;
- // Get the total count of transactions
- const totalCount = allTransactions.length;
+ // Calculate total transaction count (chain + mempool)
+ const totalTxCount = result.chain_stats.tx_count + result.mempool_stats.tx_count;
- // Slice the transactions based on limit and offset
- const paginatedTransactions = allTransactions.slice(offset, offset + limit);
+ return {
+ status: "success",
+ message: "Address info retrieved",
+ address,
+ info: result,
+ totalTxCount
+ };
+ } catch (error) {
+ console.error('Error getting address info:', error);
+ return {
+ status: "error",
+ message: error.message || "Unknown error",
+ address,
+ totalTxCount: 0
+ };
+ }
+};
+
+/**
+ * Gets transactions for a specific Bitcoin address using cursor-based pagination
+ * @param {string} address - Bitcoin address to query
+ * @param {string} endpoint - API endpoint to use ('regtest', 'mainnet', 'oylnet')
+ * @param {string|null} lastSeenTxid - Last transaction ID seen for pagination (null for first page)
+ * @returns {Promise} - Transactions for the address with pagination info
+ */
+export const getAddressTransactionsChain = async (address, endpoint = 'regtest', lastSeenTxid = null) => {
+ try {
+ const provider = getProvider(endpoint);
+ console.log(`Getting transactions for address ${address} with ${endpoint} endpoint (lastSeenTxid: ${lastSeenTxid || 'null'})`);
+
+ // Make a direct fetch request to the Esplora API
+ const url = endpoint === 'mainnet' ? 'https://mainnet.sandshrew.io/v2/lasereyes' :
+ endpoint === 'oylnet' ? 'https://oylnet.oyl.gg/v2/lasereyes' :
+ 'http://localhost:18888/v1/lasereyes';
+
+ const params = lastSeenTxid ? [address, lastSeenTxid] : [address];
+
+ const response = await fetch(url, {
+ method: 'POST',
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ body: JSON.stringify({
+ jsonrpc: '2.0',
+ id: 1,
+ method: 'esplora_address::txs:chain',
+ params: params
+ })
+ });
+
+ const data = await response.json();
+
+ if (data.error) {
+ throw new Error(data.error.message || 'Error fetching address transactions');
+ }
+
+ const result = data.result;
return {
status: "success",
message: "Transactions retrieved",
address,
- transactions: paginatedTransactions,
+ transactions: result,
pagination: {
- total: totalCount,
- limit: limit,
- offset: offset,
- hasMore: offset + limit < totalCount
+ lastSeenTxid: result.length > 0 ? result[result.length - 1].txid : null,
+ hasMore: result.length === 25 // CHAIN_TXS_PER_PAGE is 25 in rest.rs
}
};
} catch (error) {
@@ -188,7 +255,8 @@ export const getAddressTransactions = async (address, endpoint = 'regtest', limi
return {
status: "error",
message: error.message || "Unknown error",
- address
+ address,
+ transactions: []
};
}
};
@@ -306,8 +374,8 @@ export const getAddressTransactionsWithTrace = async (address, endpoint = 'regte
const provider = getProvider(endpoint);
console.log(`Getting and tracing transactions for address ${address} with ${endpoint} endpoint`);
- // Get all transactions for the address
- const txResult = await getAddressTransactions(address, endpoint);
+ // Get all transactions for the address using the new chain method
+ const txResult = await getAddressTransactionsChain(address, endpoint);
if (txResult.status === "error") {
throw new Error(txResult.message);