Skip to content
Merged
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
47 changes: 46 additions & 1 deletion modules/abstract-utxo/src/recovery/crossChainRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,40 @@ export async function isWalletAddress(wallet: IWallet | WalletV1, address: strin
}
}

/**
* Convert a Litecoin P2SH address from M... format (scriptHash 0x32) to the legacy 3... format (scriptHash 0x05).
* This is needed for cross-chain recovery when LTC was sent to a BTC address, because the BTC wallet
* stores addresses in the 3... format while the LTC blockchain returns addresses in M... format.
*
* @param address - LTC address to convert
* @param network - The Litecoin network
* @returns The address in legacy 3... format, or the original address if it's not a P2SH address
*/
export function convertLtcAddressToLegacyFormat(address: string, network: utxolib.Network): string {
try {
// Try to decode as bech32 - these don't need conversion
utxolib.address.fromBech32(address);
return address;
} catch (e) {
// Not bech32, continue to base58
}

try {
const decoded = utxolib.address.fromBase58Check(address, network);
// Only convert P2SH addresses (scriptHash), not P2PKH (pubKeyHash)
if (decoded.version === network.scriptHash) {
// Convert to legacy format using Bitcoin's scriptHash (0x05)
const legacyScriptHash = utxolib.networks.bitcoin.scriptHash;
return utxolib.address.toBase58Check(decoded.hash, legacyScriptHash, network);
}
// P2PKH or other - return unchanged
return address;
} catch (e) {
// If decoding fails, return the original address
return address;
}
}

/**
* @param coin
* @param txid
Expand All @@ -137,7 +171,18 @@ async function getAllRecoveryOutputs<TNumber extends number | bigint = number>(
// in non legacy format. However, we want to keep the address in the same format as the response since we
// are going to hit the API again to fetch address unspents.
const canonicalAddress = coin.canonicalAddress(output.address);
const isWalletOwned = await isWalletAddress(wallet, canonicalAddress);
let isWalletOwned = await isWalletAddress(wallet, canonicalAddress);

// For LTC cross-chain recovery: if the address isn't found, try the legacy format.
// When LTC is sent to a BTC address, the LTC blockchain returns M... addresses
// but the BTC wallet stores addresses in 3... format.
if (!isWalletOwned && coin.getFamily() === 'ltc') {
const legacyAddress = convertLtcAddressToLegacyFormat(output.address, coin.network);
if (legacyAddress !== output.address) {
isWalletOwned = await isWalletAddress(wallet, legacyAddress);
}
}

return isWalletOwned ? output.address : null;
})
)
Expand Down
30 changes: 30 additions & 0 deletions modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getWallet,
supportedCrossChainRecoveries,
generateAddress,
convertLtcAddressToLegacyFormat,
} from '../../../src';
import {
getFixture,
Expand Down Expand Up @@ -327,3 +328,32 @@ describe(`Cross-Chain Recovery getWallet`, async function () {
}
});
});

describe('convertLtcAddressToLegacyFormat', function () {
const ltcNetwork = utxolib.networks.litecoin;

it('should convert M... P2SH address to 3... legacy format', function () {
// These two addresses represent the same underlying script hash:
// - MNQ7zkgMsaV67rsjA3JuP59RC5wxRXpwgE is the LTC format (scriptHash 0x32)
// - 3GBygsGPvTdfKMbq4AKZZRu1sPMWPEsBfd is the BTC format (scriptHash 0x05)
const ltcAddress = 'MNQ7zkgMsaV67rsjA3JuP59RC5wxRXpwgE';
const expectedLegacyAddress = '3GBygsGPvTdfKMbq4AKZZRu1sPMWPEsBfd';

const legacyAddress = convertLtcAddressToLegacyFormat(ltcAddress, ltcNetwork);
assert.strictEqual(legacyAddress, expectedLegacyAddress);
});

it('should convert MD68PsdheKxcYsrVLyZRXgoSDLnB1MdVtE to legacy format', function () {
const address = 'MD68PsdheKxcYsrVLyZRXgoSDLnB1MdVtE';
const legacyAddress = convertLtcAddressToLegacyFormat(address, ltcNetwork);

// Should start with '3' (legacy BTC P2SH format)
assert.ok(legacyAddress.startsWith('3'), `Expected address to start with '3', got: ${legacyAddress}`);
});

it('should not modify bech32 addresses', function () {
const bech32Address = 'ltc1qgrl8zpndsklaa9swgd5vevyxmx5x63vcrl7dk4';
const result = convertLtcAddressToLegacyFormat(bech32Address, ltcNetwork);
assert.strictEqual(result, bech32Address);
});
});