Skip to content

Commit 0cd31bb

Browse files
davidkaplanbitgoArunBala-Bitgo
authored andcommitted
Merge pull request #7679 from BitGo/BTC-2829
feat: add LTC cross-chain recovery support TICKET: WIN-8147
2 parents 71f892a + 0e6a03a commit 0cd31bb

6 files changed

Lines changed: 118 additions & 7 deletions

File tree

modules/abstract-utxo/src/recovery/crossChainRecovery.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,40 @@ export async function isWalletAddress(wallet: IWallet | WalletV1, address: strin
113113
}
114114
}
115115

116+
/**
117+
* Convert a Litecoin P2SH address from M... format (scriptHash 0x32) to the legacy 3... format (scriptHash 0x05).
118+
* This is needed for cross-chain recovery when LTC was sent to a BTC address, because the BTC wallet
119+
* stores addresses in the 3... format while the LTC blockchain returns addresses in M... format.
120+
*
121+
* @param address - LTC address to convert
122+
* @param network - The Litecoin network
123+
* @returns The address in legacy 3... format, or the original address if it's not a P2SH address
124+
*/
125+
export function convertLtcAddressToLegacyFormat(address: string, network: utxolib.Network): string {
126+
try {
127+
// Try to decode as bech32 - these don't need conversion
128+
utxolib.address.fromBech32(address);
129+
return address;
130+
} catch (e) {
131+
// Not bech32, continue to base58
132+
}
133+
134+
try {
135+
const decoded = utxolib.address.fromBase58Check(address, network);
136+
// Only convert P2SH addresses (scriptHash), not P2PKH (pubKeyHash)
137+
if (decoded.version === network.scriptHash) {
138+
// Convert to legacy format using Bitcoin's scriptHash (0x05)
139+
const legacyScriptHash = utxolib.networks.bitcoin.scriptHash;
140+
return utxolib.address.toBase58Check(decoded.hash, legacyScriptHash, network);
141+
}
142+
// P2PKH or other - return unchanged
143+
return address;
144+
} catch (e) {
145+
// If decoding fails, return the original address
146+
return address;
147+
}
148+
}
149+
116150
/**
117151
* @param coin
118152
* @param txid
@@ -137,7 +171,18 @@ async function getAllRecoveryOutputs<TNumber extends number | bigint = number>(
137171
// in non legacy format. However, we want to keep the address in the same format as the response since we
138172
// are going to hit the API again to fetch address unspents.
139173
const canonicalAddress = coin.canonicalAddress(output.address);
140-
const isWalletOwned = await isWalletAddress(wallet, canonicalAddress);
174+
let isWalletOwned = await isWalletAddress(wallet, canonicalAddress);
175+
176+
// For LTC cross-chain recovery: if the address isn't found, try the legacy format.
177+
// When LTC is sent to a BTC address, the LTC blockchain returns M... addresses
178+
// but the BTC wallet stores addresses in 3... format.
179+
if (!isWalletOwned && coin.getFamily() === 'ltc') {
180+
const legacyAddress = convertLtcAddressToLegacyFormat(output.address, coin.network);
181+
if (legacyAddress !== output.address) {
182+
isWalletOwned = await isWalletAddress(wallet, legacyAddress);
183+
}
184+
}
185+
141186
return isWalletOwned ? output.address : null;
142187
})
143188
)

modules/abstract-utxo/test/unit/recovery/crossChainRecovery.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
getWallet,
1515
supportedCrossChainRecoveries,
1616
generateAddress,
17+
convertLtcAddressToLegacyFormat,
1718
} from '../../../src';
1819
import {
1920
getFixture,
@@ -327,3 +328,32 @@ describe(`Cross-Chain Recovery getWallet`, async function () {
327328
}
328329
});
329330
});
331+
332+
describe('convertLtcAddressToLegacyFormat', function () {
333+
const ltcNetwork = utxolib.networks.litecoin;
334+
335+
it('should convert M... P2SH address to 3... legacy format', function () {
336+
// These two addresses represent the same underlying script hash:
337+
// - MNQ7zkgMsaV67rsjA3JuP59RC5wxRXpwgE is the LTC format (scriptHash 0x32)
338+
// - 3GBygsGPvTdfKMbq4AKZZRu1sPMWPEsBfd is the BTC format (scriptHash 0x05)
339+
const ltcAddress = 'MNQ7zkgMsaV67rsjA3JuP59RC5wxRXpwgE';
340+
const expectedLegacyAddress = '3GBygsGPvTdfKMbq4AKZZRu1sPMWPEsBfd';
341+
342+
const legacyAddress = convertLtcAddressToLegacyFormat(ltcAddress, ltcNetwork);
343+
assert.strictEqual(legacyAddress, expectedLegacyAddress);
344+
});
345+
346+
it('should convert MD68PsdheKxcYsrVLyZRXgoSDLnB1MdVtE to legacy format', function () {
347+
const address = 'MD68PsdheKxcYsrVLyZRXgoSDLnB1MdVtE';
348+
const legacyAddress = convertLtcAddressToLegacyFormat(address, ltcNetwork);
349+
350+
// Should start with '3' (legacy BTC P2SH format)
351+
assert.ok(legacyAddress.startsWith('3'), `Expected address to start with '3', got: ${legacyAddress}`);
352+
});
353+
354+
it('should not modify bech32 addresses', function () {
355+
const bech32Address = 'ltc1qgrl8zpndsklaa9swgd5vevyxmx5x63vcrl7dk4';
356+
const result = convertLtcAddressToLegacyFormat(bech32Address, ltcNetwork);
357+
assert.strictEqual(result, bech32Address);
358+
});
359+
});

modules/sdk-core/package.json

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,32 @@
22
"name": "@bitgo/sdk-core",
33
"version": "36.22.0",
44
"description": "core library functions for BitGoJS",
5-
"main": "./dist/src/index.js",
6-
"types": "./dist/src/index.d.ts",
5+
"main": "./dist/cjs/src/index.js",
6+
"module": "./dist/esm/index.js",
7+
"browser": "./dist/esm/index.js",
8+
"types": "./dist/cjs/src/index.d.ts",
79
"files": [
8-
"dist"
10+
"dist/cjs",
11+
"dist/esm"
912
],
13+
"exports": {
14+
".": {
15+
"import": {
16+
"types": "./dist/esm/index.d.ts",
17+
"default": "./dist/esm/index.js"
18+
},
19+
"require": {
20+
"types": "./dist/cjs/src/index.d.ts",
21+
"default": "./dist/cjs/src/index.js"
22+
}
23+
}
24+
},
1025
"scripts": {
1126
"test": "yarn unit-test",
1227
"unit-test": "nyc -- mocha --recursive test",
13-
"build": "yarn tsc --build --incremental --verbose .",
28+
"build": "yarn build:cjs && yarn build:esm",
29+
"build:cjs": "yarn tsc --build --incremental --verbose .",
30+
"build:esm": "yarn tsc --project tsconfig.esm.json",
1431
"fmt": "prettier --write .",
1532
"check-fmt": "prettier --check '**/*.{ts,js,json}'",
1633
"clean": "rm -r ./dist",

modules/sdk-core/src/bitgo/tss/eddsa/eddsa.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
} from '../../utils';
2424
import { BaseTransaction } from '../../../account-lib';
2525
import { Ed25519Bip32HdTree } from '@bitgo/sdk-lib-mpc';
26-
import _ = require('lodash');
26+
import _ from 'lodash';
2727
import { commonVerifyWalletSignature, getTxRequest, sendSignatureShare } from '../common';
2828
import { IRequestTracer } from '../../../api';
2929

modules/sdk-core/tsconfig.esm.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"outDir": "./dist/esm",
5+
"rootDir": "./src",
6+
"module": "ES2020",
7+
"target": "ES2020",
8+
"moduleResolution": "bundler",
9+
"lib": ["ES2020", "DOM"],
10+
"declaration": true,
11+
"declarationMap": true,
12+
"skipLibCheck": true
13+
},
14+
"include": ["src/**/*"],
15+
"exclude": ["node_modules", "test", "dist"],
16+
"references": []
17+
}

modules/sdk-core/tsconfig.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
{
22
"extends": "../../tsconfig.json",
33
"compilerOptions": {
4-
"outDir": "./dist",
4+
"outDir": "./dist/cjs",
55
"rootDir": "./",
6+
"module": "node16",
7+
"moduleResolution": "node16",
68
"strictPropertyInitialization": false,
79
"esModuleInterop": true,
810
"typeRoots": ["../../types", "./node_modules/@types", "../../node_modules/@types"]

0 commit comments

Comments
 (0)