diff --git a/examples/swap-board-ml-btc/HTLC_FLOW_FIX.md b/examples/swap-board-ml-btc/HTLC_FLOW_FIX.md new file mode 100644 index 0000000..5f14767 --- /dev/null +++ b/examples/swap-board-ml-btc/HTLC_FLOW_FIX.md @@ -0,0 +1,182 @@ +# HTLC Creation Flow Fix - ML/BTC Atomic Swaps + +## Problem Summary + +The original implementation assumed that ML HTLC must always be created first, which blocked the flow where the creator offers BTC and needs to create a BTC HTLC as the first HTLC in the atomic swap. + +### Original Issues: +1. **BTC HTLC creation always required ML HTLC to exist** - This prevented creator from offering BTC first +2. **Secret hash was not saved to database** - Required blockchain API calls to extract secret hash every time +3. **No distinction between first and second HTLC creation** - Code didn't handle different flows properly + +## Solution Overview + +The fix implements proper handling for both HTLC creation orders: + +### Scenario A: Creator offers ML, Taker offers BTC +1. Creator creates **ML HTLC** (FIRST) → Secret hash generated by wallet, extracted and saved +2. Taker creates **BTC HTLC** (SECOND) → Uses saved secret hash from ML HTLC +3. Creator claims BTC HTLC (reveals secret) +4. Taker claims ML HTLC (using revealed secret) + +### Scenario B: Creator offers BTC, Taker offers ML +1. Creator creates **BTC HTLC** (FIRST) → Secret hash generated by wallet, returned and saved +2. Taker creates **ML HTLC** (SECOND) → Uses saved secret hash from BTC HTLC +3. Taker claims BTC HTLC (reveals secret) +4. Creator claims ML HTLC (using revealed secret) + +## Changes Made + +### 1. Fixed `createBTCHTLC()` Function + +**Key Changes:** +- Detects if BTC HTLC is first or second in the swap flow +- Handles secret hash generation (first HTLC) vs extraction (second HTLC) +- Saves secret hash from BTC wallet response when BTC is first HTLC +- Uses saved secret hash when available (optimization) + +### 2. Fixed `validateSwapForBTCHTLC()` Function + +**Key Changes:** +- Removed `secretHash` validation requirement +- Added comment explaining why: wallet generates secret hash for first HTLC +- Secret hash validation now happens in the calling function for second HTLC only + +**Logic Flow:** +```javascript +// Determine HTLC order +const isUserCreator = swap.offer.creatorMLAddress === userAddress +const creatorOfferedBTC = isCreatorOfferingBTC(swap.offer) +const isBTCFirstHTLC = isUserCreator && creatorOfferedBTC +const isBTCSecondHTLC = !isUserCreator && !creatorOfferedBTC + +if (isBTCFirstHTLC) { + // Use placeholder - wallet generates secret hash + secretHashHex = '0000000000000000000000000000000000000000' + // Wallet returns: { htlcAddress, secretHashHex, transactionId, signedTxHex, redeemScript } + // Save response.secretHashHex to database +} else if (isBTCSecondHTLC) { + // Extract from existing ML HTLC (use saved or fetch from blockchain) + secretHashHex = swap.secretHash || extractFromBlockchain() +} +``` + +### 3. Updated `createHtlc()` Function (ML HTLC) + +**Key Changes:** +- Extracts secret hash from broadcasted ML HTLC transaction +- Saves secret hash to database for later use +- Includes 2-second delay to allow transaction indexing + +**Logic Flow:** +```javascript +// Create and broadcast ML HTLC +const signedTxHex = await client.createHtlc(htlcParams) +const broadcastResult = await client.broadcastTx(signedTxHex) +const txId = broadcastResult.tx_id + +// Wait for indexing and extract secret hash +await new Promise(resolve => setTimeout(resolve, 2000)) +const txData = await fetch(`${apiServer}/transaction/${txId}`) +const secretHashHex = extractSecretHashFromTxData(txData) + +// Save with secret hash +await updateSwap({ creatorHtlcTxHash: txId, secretHash: secretHashHex }) +``` + +### 4. Updated `createCounterpartyHtlc()` Function + +**Key Changes:** +- Checks for both ML and BTC creator HTLCs +- Prioritizes using saved secret hash (faster, no API call) +- Falls back to blockchain extraction if secret hash not saved +- Handles case where creator created BTC HTLC first + +**Logic Flow:** +```javascript +let secretHashHex: string + +if (swap.secretHash) { + // Use saved secret hash (fast path) + secretHashHex = swap.secretHash +} else if (swap.creatorHtlcTxHash) { + // Extract from ML HTLC on blockchain (fallback) + secretHashHex = extractFromMLHTLC() +} else { + // Creator created BTC HTLC first but secret hash not saved + throw new Error('Secret hash not found') +} +``` + +## Database Schema + +The `secretHash` field already exists in the Swap model: + +```prisma +model Swap { + secretHash String? // Stores the secret hash for the atomic swap + // ... other fields +} +``` + +## Wallet Behavior + +### ML HTLC Creation +- Input: Placeholder secret hash `{ hex: '0000000000000000000000000000000000000000' }` +- Wallet: Generates actual secret hash internally +- Output: `signedTxHex` (string) +- Secret hash: Embedded in transaction, must be extracted from blockchain + +### BTC HTLC Creation +- Input: Secret hash (placeholder for first HTLC, actual for second HTLC) +- Wallet: Generates secret hash if placeholder provided +- Output: + ```javascript + { + htlcAddress: string, + secretHashHex: string, // ✅ Returned directly + transactionId: string, + signedTxHex: string, + redeemScript: string, + } + ``` +- Secret hash: Returned directly in response + +## Status Flow + +### When Creator Offers BTC: +1. `pending` → Creator creates BTC HTLC → `btc_htlc_created` +2. `btc_htlc_created` → Taker creates ML HTLC → `both_htlcs_created` or `in_progress` +3. Claims proceed as normal + +### When Creator Offers ML: +1. `pending` → Creator creates ML HTLC → `htlc_created` +2. `htlc_created` → Taker creates BTC HTLC → `both_htlcs_created` +3. Claims proceed as normal + +## Benefits + +1. **Supports both HTLC creation orders** - Creator can offer either BTC or ML first +2. **Optimized secret hash handling** - Saved to database, reduces blockchain API calls +3. **Faster counterparty HTLC creation** - Uses saved secret hash instead of fetching from blockchain +4. **Better error handling** - Clear messages for different failure scenarios +5. **Maintains backward compatibility** - Falls back to blockchain extraction if secret hash not saved + +## Testing Checklist + +- [ ] Creator offers BTC → Creates BTC HTLC first (no ML HTLC exists) +- [ ] Creator offers BTC → Taker creates ML HTLC second (uses secret hash from BTC) +- [ ] Creator offers ML → Creates ML HTLC first (secret hash saved) +- [ ] Creator offers ML → Taker creates BTC HTLC second (uses saved secret hash) +- [ ] Secret hash properly shared between both HTLCs +- [ ] Claims work correctly in both directions +- [ ] Fallback to blockchain extraction works if secret hash not saved +- [ ] Status updates correctly based on HTLC creation order + +## Notes + +- The 2-second delay in ML HTLC creation allows the transaction to be indexed by the blockchain API +- BTC wallet returns secret hash directly, eliminating need for extraction +- Secret hash is stored as hex string in database +- Both ML and BTC secret hashes are compatible (same format) + diff --git a/examples/swap-board-ml-btc/TESTING_GUIDE.md b/examples/swap-board-ml-btc/TESTING_GUIDE.md new file mode 100644 index 0000000..d8421b1 --- /dev/null +++ b/examples/swap-board-ml-btc/TESTING_GUIDE.md @@ -0,0 +1,334 @@ +# Testing Guide: ML/BTC Atomic Swap HTLC Flow + +## Overview +This guide covers testing both HTLC creation flows after the fix that allows BTC HTLC to be created first. + +## Test Scenarios + +### Scenario 1: Creator Offers ML, Taker Offers BTC (Original Flow) + +**Setup:** +1. Creator creates offer: 100 ML → 0.001 BTC +2. Taker accepts offer with BTC address and public key + +**Expected Flow:** + +#### Step 1: Creator Creates ML HTLC +- **Action:** Creator clicks "Create ML HTLC" +- **Expected:** + - ML HTLC transaction created and broadcasted + - Status changes to `htlc_created` + - Secret hash extracted from transaction and saved to database + - Console log: "Extracted secret hash from ML HTLC: [hash]" + - Alert: "HTLC created and broadcasted successfully! TX ID: [txId]" + +**Verification:** +```javascript +// Check database +swap.status === 'htlc_created' +swap.creatorHtlcTxHash !== null +swap.creatorHtlcTxHex !== null +swap.secretHash !== null // ✅ NEW: Should be saved +``` + +#### Step 2: Taker Creates BTC HTLC +- **Action:** Taker clicks "Create BTC HTLC" (in BTC section) +- **Expected:** + - Uses saved secret hash from ML HTLC + - Console log: "Creating BTC HTLC as SECOND HTLC (taker offers BTC)" + - Console log: "Using saved secret hash: [hash]" + - BTC HTLC transaction created and broadcasted + - Status changes to `both_htlcs_created` + - Alert: "BTC HTLC created successfully! TX ID: [txId]" + +**Verification:** +```javascript +// Check database +swap.status === 'both_htlcs_created' +swap.btcHtlcTxId !== null +swap.btcHtlcTxHex !== null +swap.btcRedeemScript !== null +swap.btcHtlcAddress !== null +``` + +#### Step 3: Claims +- Creator claims BTC HTLC (reveals secret) +- Taker claims ML HTLC (using revealed secret) +- Status: `fully_completed` + +--- + +### Scenario 2: Creator Offers BTC, Taker Offers ML (NEW Flow) + +**Setup:** +1. Creator creates offer: 0.001 BTC → 100 ML +2. Taker accepts offer with ML address + +**Expected Flow:** + +#### Step 1: Creator Creates BTC HTLC +- **Action:** Creator clicks "Create BTC HTLC" +- **Expected:** + - Console log: "Creating BTC HTLC as FIRST HTLC (creator offers BTC)" + - Placeholder secret hash used: `0000000000000000000000000000000000000000` + - Wallet generates actual secret hash + - BTC HTLC transaction created and broadcasted + - Status changes to `btc_htlc_created` + - Secret hash from wallet response saved to database + - Console log: "Saved secret hash from BTC wallet: [hash]" + - Alert: "BTC HTLC created successfully! TX ID: [txId]" + +**Verification:** +```javascript +// Check database +swap.status === 'btc_htlc_created' +swap.btcHtlcTxId !== null +swap.btcHtlcTxHex !== null +swap.btcRedeemScript !== null +swap.btcHtlcAddress !== null +swap.secretHash !== null // ✅ NEW: Should be saved from BTC wallet +``` + +**Critical Check:** +```javascript +// Verify wallet response structure +const response = { + htlcAddress: string, + secretHashHex: string, // ✅ Must be present + transactionId: string, + signedTxHex: string, + redeemScript: string, +} +``` + +#### Step 2: Taker Creates ML HTLC +- **Action:** Taker clicks "Create Counterparty HTLC" +- **Expected:** + - Uses saved secret hash from BTC HTLC + - Console log: "Using saved secret hash: [hash]" + - ML HTLC transaction created and broadcasted + - Status changes to `in_progress` or `both_htlcs_created` + - Alert: "Counterparty HTLC created and broadcasted successfully! TX ID: [txId]" + +**Verification:** +```javascript +// Check database +swap.status === 'in_progress' || swap.status === 'both_htlcs_created' +swap.takerHtlcTxHash !== null +swap.takerHtlcTxHex !== null + +// Verify same secret hash used +// Extract secret hash from ML HTLC transaction +const mlSecretHash = extractFromMLHTLC(swap.takerHtlcTxHash) +mlSecretHash === swap.secretHash // ✅ Must match +``` + +#### Step 3: Claims +- Taker claims BTC HTLC (reveals secret) +- Creator claims ML HTLC (using revealed secret) +- Status: `fully_completed` + +--- + +## Edge Cases to Test + +### Test 3: Fallback to Blockchain Extraction (ML First) + +**Scenario:** Secret hash not saved for some reason + +**Setup:** +1. Manually clear `swap.secretHash` in database after ML HTLC creation +2. Taker tries to create BTC HTLC + +**Expected:** +- Console log: "Secret hash not saved, fetching from blockchain..." +- Fetches ML HTLC transaction from blockchain API +- Extracts secret hash from transaction +- Console log: "Extracted secret hash from ML HTLC: [hash]" +- BTC HTLC created successfully + +### Test 4: Error Handling - No Creator HTLC + +**Scenario:** Taker tries to create HTLC before creator + +**Setup:** +1. Taker tries to create counterparty HTLC immediately after accepting offer + +**Expected:** +- Alert: "Creator HTLC must be created first" +- No HTLC created + +### Test 5: Error Handling - Wrong User + +**Scenario:** Wrong user tries to create HTLC + +**Setup:** +1. Taker tries to create first HTLC +2. Creator tries to create second HTLC + +**Expected:** +- Alert: "You are not the one who should create the BTC HTLC" +- No HTLC created + +--- + +## Console Log Checklist + +### ML HTLC Creation (First): +``` +✅ "HTLC signed: [hex]" +✅ "HTLC broadcast result: [result]" +✅ "Extracted secret hash from ML HTLC: [hash]" +``` + +### BTC HTLC Creation (First): +``` +✅ "Creating BTC HTLC as FIRST HTLC (creator offers BTC)" +✅ "Saved secret hash from BTC wallet: [hash]" +``` + +### BTC HTLC Creation (Second): +``` +✅ "Creating BTC HTLC as SECOND HTLC (taker offers BTC)" +✅ "Using saved secret hash: [hash]" +OR +✅ "Secret hash not saved, fetching from blockchain..." +✅ "ML HTLC transaction data: [data]" +✅ "Extracted secret hash from ML HTLC: [hash]" +``` + +### ML HTLC Creation (Second): +``` +✅ "Using saved secret hash: [hash]" +OR +✅ "Secret hash not saved, fetching from ML HTLC blockchain..." +✅ "Creator ML HTLC transaction data: [data]" +✅ "Extracted secret hash from creator ML HTLC: [hash]" +``` + +--- + +## Database State Verification + +### After First HTLC (ML): +```sql +SELECT + status, -- 'htlc_created' + creatorHtlcTxHash, -- NOT NULL + creatorHtlcTxHex, -- NOT NULL + secretHash, -- NOT NULL ✅ NEW + btcHtlcTxId -- NULL +FROM Swap WHERE id = ? +``` + +### After First HTLC (BTC): +```sql +SELECT + status, -- 'btc_htlc_created' + btcHtlcTxId, -- NOT NULL + btcHtlcTxHex, -- NOT NULL + btcRedeemScript, -- NOT NULL + btcHtlcAddress, -- NOT NULL + secretHash, -- NOT NULL ✅ NEW + creatorHtlcTxHash -- NULL +FROM Swap WHERE id = ? +``` + +### After Second HTLC: +```sql +SELECT + status, -- 'both_htlcs_created' or 'in_progress' + creatorHtlcTxHash, -- NOT NULL (if ML first) OR NULL (if BTC first) + btcHtlcTxId, -- NOT NULL (if BTC first) OR NOT NULL (if BTC second) + takerHtlcTxHash, -- NOT NULL (if ML second) OR NULL + secretHash -- NOT NULL +FROM Swap WHERE id = ? +``` + +--- + +## Performance Verification + +### With Secret Hash Saved (Optimized): +- **ML HTLC Creation:** ~2-3 seconds (includes 2s wait for indexing) +- **BTC HTLC Creation (Second):** ~1-2 seconds (no blockchain API call) +- **ML HTLC Creation (Second):** ~1-2 seconds (no blockchain API call) + +### Without Secret Hash (Fallback): +- **BTC HTLC Creation (Second):** ~3-5 seconds (includes blockchain API call) +- **ML HTLC Creation (Second):** ~3-5 seconds (includes blockchain API call) + +--- + +## Wallet Integration Tests + +### Test BTC Wallet Secret Hash Generation: +```javascript +// When creating BTC HTLC with placeholder +const request = { + amount: '100000', + secretHash: '0000000000000000000000000000000000000000', + recipientPublicKey: '...', + refundPublicKey: '...', + timeoutBlocks: 144 +} + +const response = await wallet.signTransaction({ + chain: 'bitcoin', + txData: { JSONRepresentation: request } +}) + +// Verify response structure +console.assert(response.htlcAddress, 'htlcAddress missing') +console.assert(response.secretHashHex, 'secretHashHex missing') // ✅ CRITICAL +console.assert(response.transactionId, 'transactionId missing') +console.assert(response.signedTxHex, 'signedTxHex missing') +console.assert(response.redeemScript, 'redeemScript missing') + +// Verify secret hash format +console.assert(response.secretHashHex.length === 40, 'Invalid secret hash length') +console.assert(/^[0-9a-f]+$/i.test(response.secretHashHex), 'Invalid secret hash format') +``` + +--- + +## Success Criteria + +✅ **Scenario 1 (ML First):** Complete atomic swap with ML HTLC created first +✅ **Scenario 2 (BTC First):** Complete atomic swap with BTC HTLC created first +✅ **Secret Hash Saved:** Secret hash saved to database in both scenarios +✅ **Optimization Works:** Saved secret hash used when available (no API call) +✅ **Fallback Works:** Blockchain extraction works when secret hash not saved +✅ **Error Handling:** Appropriate errors for invalid flows +✅ **Status Updates:** Correct status transitions in both flows +✅ **Claims Work:** Both parties can claim HTLCs successfully +✅ **Secret Revealed:** Secret properly revealed and extracted during claims + +--- + +## Troubleshooting + +### Issue: Secret hash not saved from BTC wallet +**Check:** +- Wallet response includes `secretHashHex` field +- `response.secretHashHex` is not undefined or null +- Database update includes `secretHash` field + +### Issue: ML HTLC secret hash extraction fails +**Check:** +- 2-second delay is sufficient for transaction indexing +- Blockchain API is accessible +- Transaction has HTLC output with secret_hash field + +### Issue: Taker cannot create second HTLC +**Check:** +- Creator's first HTLC exists in database +- Secret hash is saved or can be extracted +- User role detection is correct (isUserCreator logic) + +### Issue: Wrong status after HTLC creation +**Check:** +- `isBTCFirstHTLC` and `isBTCSecondHTLC` logic is correct +- Status update uses correct value based on HTLC order +- Both HTLCs exist before setting `both_htlcs_created` + diff --git a/examples/swap-board-ml-btc/src/app/create/page.tsx b/examples/swap-board-ml-btc/src/app/create/page.tsx index af2b249..aa2e45d 100644 --- a/examples/swap-board-ml-btc/src/app/create/page.tsx +++ b/examples/swap-board-ml-btc/src/app/create/page.tsx @@ -19,6 +19,8 @@ export default function CreateOfferPage() { }) const [loading, setLoading] = useState(false) const [userAddress, setUserAddress] = useState('') + const [userBTCAddress, setUserBTCAddress] = useState('') + const [userBTCPublicKey, setUserBTCPublicKey] = useState('') const [client, setClient] = useState(null) const [tokens, setTokens] = useState([]) const [loadingTokens, setLoadingTokens] = useState(true) @@ -70,6 +72,15 @@ export default function CreateOfferPage() { const connect = await client.connect() const address = connect.address.testnet.receiving[0] setUserAddress(address) + + // Get BTC address and public key from wallet connection + if (connect.addressesByChain?.bitcoin) { + const btcAddress = connect.addressesByChain.bitcoin.receiving?.[0] + const btcPublicKey = connect.addressesByChain.bitcoin.publicKeys?.receiving?.[0] + + if (btcAddress) setUserBTCAddress(btcAddress) + if (btcPublicKey) setUserBTCPublicKey(btcPublicKey) + } } } catch (error) { console.error('Error connecting wallet:', error) @@ -125,24 +136,15 @@ export default function CreateOfferPage() { try { let creatorBTCAddress, creatorBTCPublicKey; - // If offering BTC or requesting BTC, get BTC credentials + // If offering BTC or requesting BTC, use BTC credentials from wallet connection if (formData.tokenA === 'BTC' || formData.tokenB === 'BTC') { - if (!client) { - alert('Wallet client not initialized') + if (!userBTCAddress || !userBTCPublicKey) { + alert('BTC credentials not available. Please reconnect your wallet.') return } - try { - // Get BTC credentials from wallet - const BTCData = await (client as any).request({ method: 'getData', params: { items: ['btcAddress', 'btcPublicKey']} }) - - creatorBTCAddress = BTCData.btcAddress - creatorBTCPublicKey = BTCData.btcPublicKey - } catch (error) { - console.error('Error getting BTC credentials:', error) - alert('Failed to get BTC credentials from wallet. Please make sure your wallet supports BTC.') - return - } + creatorBTCAddress = userBTCAddress + creatorBTCPublicKey = userBTCPublicKey } const response = await fetch('/api/offers', { diff --git a/examples/swap-board-ml-btc/src/app/offers/page.tsx b/examples/swap-board-ml-btc/src/app/offers/page.tsx index 1fb9508..77e4208 100644 --- a/examples/swap-board-ml-btc/src/app/offers/page.tsx +++ b/examples/swap-board-ml-btc/src/app/offers/page.tsx @@ -16,6 +16,8 @@ export default function OffersPage() { const [accepting, setAccepting] = useState(null) const [client, setClient] = useState(null) const [userAddress, setUserAddress] = useState('') + const [userBTCAddress, setUserBTCAddress] = useState('') + const [userBTCPublicKey, setUserBTCPublicKey] = useState('') const [tokens, setTokens] = useState([]) useEffect(() => { @@ -74,6 +76,15 @@ export default function OffersPage() { const connect = await client.connect() const address = connect.address.testnet.receiving[0] setUserAddress(address) + + // Get BTC address and public key from wallet connection + if (connect.addressesByChain?.bitcoin) { + const btcAddress = connect.addressesByChain.bitcoin.receiving?.[0] + const btcPublicKey = connect.addressesByChain.bitcoin.publicKeys?.receiving?.[0] + + if (btcAddress) setUserBTCAddress(btcAddress) + if (btcPublicKey) setUserBTCPublicKey(btcPublicKey) + } } } catch (error) { console.error('Error connecting wallet:', error) @@ -100,24 +111,15 @@ export default function OffersPage() { const offer = offers.find(o => o.id === offerId) let takerBTCAddress, takerBTCPublicKey; - // If offer involves BTC, get BTC credentials + // If offer involves BTC, use BTC credentials from wallet connection if (offer && (offer.tokenA === 'BTC' || offer.tokenB === 'BTC')) { - if (!client) { - alert('Wallet client not initialized') + if (!userBTCAddress || !userBTCPublicKey) { + alert('BTC credentials not available. Please reconnect your wallet.') return } - try { - // Get BTC credentials from wallet - const BTCData = await (client as any).request({ method: 'getData', params: { items: ['btcAddress', 'btcPublicKey']} }) - - takerBTCAddress = BTCData.btcAddress - takerBTCPublicKey = BTCData.btcPublicKey - } catch (error) { - console.error('Error getting BTC credentials:', error) - alert('Failed to get BTC credentials from wallet. Please make sure your wallet supports BTC.') - return - } + takerBTCAddress = userBTCAddress + takerBTCPublicKey = userBTCPublicKey } const response = await fetch('/api/swaps', { diff --git a/examples/swap-board-ml-btc/src/app/swap/[id]/page.tsx b/examples/swap-board-ml-btc/src/app/swap/[id]/page.tsx index 2a31f22..86e5c77 100644 --- a/examples/swap-board-ml-btc/src/app/swap/[id]/page.tsx +++ b/examples/swap-board-ml-btc/src/app/swap/[id]/page.tsx @@ -301,17 +301,49 @@ export default function SwapPage({ params }: { params: { id: string } }) { const txId = broadcastResult.tx_id || broadcastResult.transaction_id || broadcastResult.id - // Step 4: Update swap status with transaction ID and hex + // Step 4: Extract secret hash from the broadcasted transaction + const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' + const apiServer = network === 'mainnet' + ? 'https://api-server.mintlayer.org/api/v2' + : 'https://api-server-lovelace.mintlayer.org/api/v2' + + let secretHashHex: string | undefined + + try { + // Wait a moment for transaction to be indexed + await new Promise(resolve => setTimeout(resolve, 2000)) + + const txResponse = await fetch(`${apiServer}/transaction/${txId}`) + if (txResponse.ok) { + const txData = await txResponse.json() + const htlcOutput = txData.outputs?.find((output: any) => output.type === 'Htlc') + if (htlcOutput?.htlc?.secret_hash?.hex) { + secretHashHex = htlcOutput.htlc.secret_hash.hex + console.log('Extracted secret hash from ML HTLC:', secretHashHex) + } + } + } catch (error) { + console.error('Error extracting secret hash from ML HTLC:', error) + // Continue without secret hash - it can be extracted later if needed + } + + // Step 5: Update swap status with transaction ID, hex, and secret hash // Note: We save the signed transaction hex because it's needed later // to extract the secret when someone claims the HTLC using extractHtlcSecret() + const updateData: any = { + status: 'htlc_created', + creatorHtlcTxHash: txId, + creatorHtlcTxHex: signedTxHex + } + + if (secretHashHex) { + updateData.secretHash = secretHashHex + } + await fetch(`/api/swaps/${swap.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - status: 'htlc_created', - creatorHtlcTxHash: txId, - creatorHtlcTxHex: signedTxHex - }) + body: JSON.stringify(updateData) }) // Refresh swap data @@ -331,36 +363,50 @@ export default function SwapPage({ params }: { params: { id: string } }) { return } - // Check if creator's HTLC exists to extract secret hash from - if (!swap.creatorHtlcTxHash) { + // Check if creator's HTLC exists (could be ML or BTC) + const creatorMLHTLCExists = !!swap.creatorHtlcTxHash + const creatorBTCHTLCExists = !!swap.btcHtlcTxId + + if (!creatorMLHTLCExists && !creatorBTCHTLCExists) { alert('Creator HTLC must be created first') return } setCreatingCounterpartyHtlc(true) try { - // Fetch the creator's ML HTLC transaction to extract the secret hash const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' const apiServer = network === 'mainnet' ? 'https://api-server.mintlayer.org/api/v2' : 'https://api-server-lovelace.mintlayer.org/api/v2' - const txResponse = await fetch(`${apiServer}/transaction/${swap.creatorHtlcTxHash}`) - if (!txResponse.ok) { - throw new Error('Failed to fetch creator HTLC transaction') - } + let secretHashHex: string + + // Try to use saved secret hash first (faster, no API call needed) + if (swap.secretHash) { + secretHashHex = swap.secretHash + console.log('Using saved secret hash:', secretHashHex) + } else if (creatorMLHTLCExists) { + // Fallback: Extract from creator's ML HTLC transaction on blockchain + console.log('Secret hash not saved, fetching from ML HTLC blockchain...') + const txResponse = await fetch(`${apiServer}/transaction/${swap.creatorHtlcTxHash}`) + if (!txResponse.ok) { + throw new Error('Failed to fetch creator ML HTLC transaction') + } - const txData = await txResponse.json() - console.log('Creator HTLC transaction data:', txData) + const txData = await txResponse.json() + console.log('Creator ML HTLC transaction data:', txData) - // Find the HTLC output and extract the secret hash - const htlcOutput = txData.outputs?.find((output: any) => output.type === 'Htlc') - if (!htlcOutput || !htlcOutput.htlc?.secret_hash?.hex) { - throw new Error('Could not find secret hash in creator HTLC transaction') - } + const htlcOutput = txData.outputs?.find((output: any) => output.type === 'Htlc') + if (!htlcOutput || !htlcOutput.htlc?.secret_hash?.hex) { + throw new Error('Could not find secret hash in creator ML HTLC transaction') + } - const secretHashHex = htlcOutput.htlc.secret_hash.hex - console.log('Extracted secret hash from creator HTLC:', secretHashHex) + secretHashHex = htlcOutput.htlc.secret_hash.hex + console.log('Extracted secret hash from creator ML HTLC:', secretHashHex) + } else { + // Creator created BTC HTLC first, but secret hash not saved + throw new Error('Secret hash not found. Creator must have created BTC HTLC with secret hash saved.') + } const htlcParams = { amount: swap.offer.amountB, // Taker gives amountB @@ -668,49 +714,72 @@ export default function SwapPage({ params }: { params: { id: string } }) { return } - // Check if ML HTLC exists to extract secret hash from - const mlHtlcTxHash = swap.creatorHtlcTxHash || swap.takerHtlcTxHash - if (!mlHtlcTxHash) { - alert('ML HTLC must be created first to generate the secret hash') - return - } - setCreatingBTCHtlc(true) try { validateSwapForBTCHTLC(swap, swap.offer) - // Fetch the ML HTLC transaction to extract the secret hash const network = (process.env.NEXT_PUBLIC_MINTLAYER_NETWORK as 'testnet' | 'mainnet') || 'testnet' const apiServer = network === 'mainnet' ? 'https://api-server.mintlayer.org/api/v2' : 'https://api-server-lovelace.mintlayer.org/api/v2' - const txResponse = await fetch(`${apiServer}/transaction/${mlHtlcTxHash}`) - if (!txResponse.ok) { - throw new Error('Failed to fetch ML HTLC transaction') - } + // Determine if BTC HTLC is first or second in the swap + const isUserCreator = swap.offer.creatorMLAddress === userAddress + const creatorOfferedBTC = isCreatorOfferingBTC(swap.offer) - const txData = await txResponse.json() - console.log('ML HTLC transaction data:', txData) + // BTC is FIRST HTLC if creator offers BTC and user is creator + const isBTCFirstHTLC = isUserCreator && creatorOfferedBTC - // Find the HTLC output and extract the secret hash - const htlcOutput = txData.outputs?.find((output: any) => output.type === 'Htlc') - if (!htlcOutput || !htlcOutput.htlc?.secret_hash?.hex) { - throw new Error('Could not find secret hash in ML HTLC transaction') - } + // BTC is SECOND HTLC if taker offers BTC (creator wants BTC) + const isBTCSecondHTLC = !isUserCreator && !creatorOfferedBTC - const secretHashHex = htlcOutput.htlc.secret_hash.hex - console.log('Extracted secret hash from ML HTLC:', secretHashHex) + let secretHashHex: string + let request: any - const isUserCreator = swap.offer.creatorMLAddress === userAddress - let request; + if (isBTCFirstHTLC) { + // BTC is the FIRST HTLC - wallet will generate secret hash + console.log('Creating BTC HTLC as FIRST HTLC (creator offers BTC)') - if (isUserCreator && isCreatorOfferingBTC(swap.offer)) { - // Creator is offering BTC + // Use placeholder - wallet will generate actual secret hash + secretHashHex = '0000000000000000000000000000000000000000' request = buildCreatorBTCHTLCRequest(swap, swap.offer, secretHashHex) - } else if (!isUserCreator && !isCreatorOfferingBTC(swap.offer)) { - // Taker is offering BTC (creator wants BTC) + + } else if (isBTCSecondHTLC) { + // BTC is the SECOND HTLC - extract secret hash from existing ML HTLC + console.log('Creating BTC HTLC as SECOND HTLC (taker offers BTC)') + + const mlHtlcTxHash = swap.creatorHtlcTxHash + if (!mlHtlcTxHash) { + alert('Creator must create ML HTLC first') + return + } + + // Try to use saved secret hash first (faster) + if (swap.secretHash) { + secretHashHex = swap.secretHash + console.log('Using saved secret hash:', secretHashHex) + } else { + // Fallback: Extract from blockchain + console.log('Secret hash not saved, fetching from blockchain...') + const txResponse = await fetch(`${apiServer}/transaction/${mlHtlcTxHash}`) + if (!txResponse.ok) { + throw new Error('Failed to fetch ML HTLC transaction') + } + + const txData = await txResponse.json() + console.log('ML HTLC transaction data:', txData) + + const htlcOutput = txData.outputs?.find((output: any) => output.type === 'Htlc') + if (!htlcOutput || !htlcOutput.htlc?.secret_hash?.hex) { + throw new Error('Could not find secret hash in ML HTLC transaction') + } + + secretHashHex = htlcOutput.htlc.secret_hash.hex + console.log('Extracted secret hash from ML HTLC:', secretHashHex) + } + request = buildTakerBTCHTLCRequest(swap, swap.offer, secretHashHex) + } else { alert('You are not the one who should create the BTC HTLC') return @@ -722,17 +791,28 @@ export default function SwapPage({ params }: { params: { id: string } }) { // Broadcast the transaction using Blockstream API const txId = await broadcastBTCTransaction(response.signedTxHex, network === 'testnet') + // Determine status based on whether this is first or second HTLC + const newStatus = isBTCFirstHTLC ? 'btc_htlc_created' : 'both_htlcs_created' + // Update swap with BTC HTLC details + const updateData: any = { + btcHtlcAddress: response.htlcAddress, + btcHtlcTxId: txId, + btcHtlcTxHex: response.signedTxHex, + btcRedeemScript: response.redeemScript, + status: newStatus + } + + // If BTC is first HTLC, save the secret hash returned by wallet + if (isBTCFirstHTLC && response.secretHashHex) { + updateData.secretHash = response.secretHashHex + console.log('Saved secret hash from BTC wallet:', response.secretHashHex) + } + await fetch(`/api/swaps/${swap.id}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - btcHtlcAddress: response.htlcAddress, - btcHtlcTxId: txId, - btcHtlcTxHex: response.signedTxHex, - btcRedeemScript: response.redeemScript, - status: swap.creatorHtlcTxHash || swap.takerHtlcTxHash ? 'both_htlcs_created' : 'btc_htlc_created' - }) + body: JSON.stringify(updateData) }) fetchSwap() diff --git a/examples/swap-board-ml-btc/src/lib/btc-request-builder.ts b/examples/swap-board-ml-btc/src/lib/btc-request-builder.ts index a779343..f6c4a7e 100644 --- a/examples/swap-board-ml-btc/src/lib/btc-request-builder.ts +++ b/examples/swap-board-ml-btc/src/lib/btc-request-builder.ts @@ -188,12 +188,13 @@ export function getBTCTimeoutBlocks(isCreator: boolean): number { /** * Validate swap has required BTC data for HTLC creation + * Note: secretHash is NOT required for first HTLC creation - wallet will generate it */ export function validateSwapForBTCHTLC(swap: Swap, offer: Offer): void { if (!offerInvolvesBTC(offer)) { throw new Error('Offer does not involve BTC') } - + if (isCreatorOfferingBTC(offer)) { if (!offer.creatorBTCAddress || !offer.creatorBTCPublicKey) { throw new Error('Missing creator BTC credentials') @@ -209,10 +210,10 @@ export function validateSwapForBTCHTLC(swap: Swap, offer: Offer): void { throw new Error('Missing taker BTC credentials') } } - - if (!swap.secretHash) { - throw new Error('Missing secret hash') - } + + // Note: We don't validate secretHash here because: + // - For FIRST BTC HTLC: wallet generates the secret hash + // - For SECOND BTC HTLC: secret hash is validated in the calling function } /**