Skip to content

Commit e13869c

Browse files
feat: recovery for eth and other musig2 coins wip
1 parent 0cee0c1 commit e13869c

5 files changed

Lines changed: 253 additions & 36 deletions

File tree

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"halfSigned": {
3+
"recipients": [
4+
{
5+
"address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20",
6+
"amount": "1000000000000000000"
7+
}
8+
],
9+
"expireTime": 1750182870,
10+
"contractSequenceId": 1,
11+
"operationHash": "0x92d3a28bd75dfa559c60e679b98fddfcb7dcaeb579c25cab3f9442b25fd270e2",
12+
"signature": "0x62c594b62ce2fc9f1d2e82a105668ed53528eb02635b8ad73206fe75ed26b26923450ed54b87980296c362fe03c7bc8e156d1ab38bfe9a682ba585e7d92d88e31b",
13+
"backupKeyNonce": 0
14+
}
15+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"txHex": "f901c380808094223fe2adcc8f28d8a46f72f7f355117d2727554d80b9016439125215000000000000000000000000e7d07af8e3e7472ea8391a3372ab98d04ac4df200000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006851b0bf000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000041917bcebd0b1f43a25b72b161dbd4db539c282af9f3856fc60f701471f0df22e22b5428593f5affc1a88944a5c5255c6c2d0df87a0668864d551a5409ec9f82ca1c000000000000000000000000000000000000000000000000000000000000001ca0c1e0750ac2c3c1cc98997dd1a14f96f1ba65929503bbdbd96ffa00fdeea2e51fa05cca504d869dcf2935c46dc5a733c0c3d8483ce2b452cae2bf89b7417d7f80b9"
3+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
{
2+
"tx": "f9012b808504a817c8008307a12094223fe2adcc8f28d8a46f72f7f355117d2727554d80b9010439125215000000000000000000000000e7d07af8e3e7472ea8391a3372ab98d04ac4df200000000000000000000000000000000000000000000000000de0b6b3a764000000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000006851a693000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000808080",
3+
"userKey": "xpub661MyMwAqRbcFigezGWEYSbCPVuaUmvnp1u7iEpH9YsKU6uYQtPANvudjgAo82QRHXsUieMqKeB1xEj89VUKU1ugtmyAZ3xzNEbHPexxgKK",
4+
"backupKey": "xpub661MyMwAqRbcGbCirzmQsUJT2eidt9tFLw2m77w6FiKco6TKu49CP3GkHF88xGCpvqkP93SYMAarfyWAn8UWevQtNT6pDo8xH7xmf6GqK6e",
5+
"coin": "hteth",
6+
"gasPrice": "20000000000",
7+
"gasLimit": "500000",
8+
"recipients": [
9+
{
10+
"address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20",
11+
"amount": "1000000000000000000"
12+
}
13+
],
14+
"walletContractAddress": "0x223fe2adcc8f28d8a46f72f7f355117d2727554d",
15+
"amount": "1000000000000000000",
16+
"backupKeyNonce": 0,
17+
"recipient": {
18+
"address": "0xe7d07af8e3e7472ea8391a3372ab98d04ac4df20",
19+
"amount": "1000000000000000000"
20+
},
21+
"expireTime": 1750181523,
22+
"contractSequenceId": 1,
23+
"nextContractSequenceId": 1
24+
}
Lines changed: 145 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -1,67 +1,176 @@
1+
// TODO: type the handler with something like this
12
// export async function handleRecoveryWalletOnPrem(
23
// req: MasterApiSpecRouteRequest<'v1.wallet.recovery', 'post'>,
34
// ) {
4-
// console.log(req);
55
// }
66

7-
import { AbstractEthLikeNewCoins, RecoverOptions } from '@bitgo/abstract-eth';
7+
import { SignFinalOptions } from '@bitgo/abstract-eth';
8+
import { isEosCoin, isEthCoin, isStxCoin, isUtxoCoin, isXtzCoin } from '../shared/coinUtils';
89
import { BitGoRequest } from '../types/request';
910
import { createEnclavedExpressClient } from './enclavedExpressClient';
1011

12+
// TODO: this is gonna be present on eve so we can remove this
13+
const userEncryptedPrv = '';
14+
const backupEncryptedPrv = '';
15+
const passphrase = '';
16+
// TODO: ---end remove vars
17+
1118
export async function handleRecoveryWalletOnPrem(req: BitGoRequest) {
1219
const bitgo = req.bitgo;
1320
const coin = req.params.coin;
14-
// const { rootAddress, recoveryDestinationAddress, userPubKey, backupPubKey, coinSpecificParams } =
15-
// req.body;
1621

17-
//TODO: delete this part
18-
const userPubKey = '';
19-
const backupPubKey = '';
20-
const apiKey = '';
21-
const walletContractAddress = '';
22-
const recoveryDestinationAddress = '';
22+
const {
23+
userPub,
24+
backupPub,
25+
walletContractAddress,
26+
recoveryDestinationAddress,
27+
coinSpecificParams,
28+
} = req.body;
2329

2430
const baseCoin = bitgo.coin(coin);
25-
2631
const enclavedExpressClient = createEnclavedExpressClient(req.config, coin);
2732
if (!enclavedExpressClient) {
2833
throw new Error(
2934
'Enclaved express client not configured - enclaved express features will be disabled',
3035
);
3136
}
3237

33-
// what's this? isEVM
38+
const sdkCoin = baseCoin;
39+
const commonRecoverParams = {
40+
userKey: userPub,
41+
backupKey: backupPub,
42+
walletContractAddress,
43+
recoveryDestination: recoveryDestinationAddress,
44+
// TODO: add api key here, currently configured on bitgo obj
45+
// apiKey,
46+
};
3447
if (baseCoin.isEVM()) {
35-
let sdkCoin;
36-
//TODO: do we need this cast to call recover?
37-
if (true) {
38-
sdkCoin = baseCoin as unknown as AbstractEthLikeNewCoins;
39-
// } else if (isStxCoin(baseCoin)) {
40-
// //TODO: what's the abstract coin class for stx, eos, btc, etc?
41-
// sdkCoin = baseCoin as unknown as AbstractStxCoin;
48+
if (isEthCoin(sdkCoin)) {
49+
try {
50+
// TODO: populate coinSpecificParams with things like replayProtectionOptions
51+
// coinSpecificParams type could be "recoverOptions"
52+
const unsignedTx = await sdkCoin.recover({
53+
...commonRecoverParams,
54+
walletPassphrase: passphrase,
55+
});
56+
57+
const halfSignedTx = await sdkCoin.signTransaction({
58+
txPrebuild: { ...unsignedTx } as unknown as SignFinalOptions,
59+
prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }),
60+
});
61+
62+
const { halfSigned } = halfSignedTx as any;
63+
const fullSignedTx = await sdkCoin.signTransaction({
64+
isLastSignature: true,
65+
signingKeyNonce: halfSigned.signingKeyNonce ?? 0,
66+
backupKeyNonce: halfSigned.backupKeyNonce ?? 0,
67+
txPrebuild: {
68+
...halfSigned,
69+
txHex: halfSigned.signatures,
70+
halfSigned,
71+
} as unknown as SignFinalOptions,
72+
prv: bitgo.decrypt({ password: passphrase, input: backupEncryptedPrv }),
73+
recipients: halfSigned.recipients ?? [],
74+
walletContractAddress: walletContractAddress,
75+
});
76+
} catch (err) {
77+
console.log(err);
78+
throw err;
79+
}
4280
} else {
4381
throw new Error('Unsupported coin type for recovery: ' + coin);
4482
}
83+
} else {
84+
// TODO (can't advance): XTZ throws a method not implemented on recover.
85+
if (isXtzCoin(sdkCoin)) {
86+
try {
87+
const unsignedTx = await sdkCoin.recover({
88+
...commonRecoverParams,
89+
});
90+
91+
//TODO: fill this fields, check output from recover when recover implemented on sdk for xtz
92+
const txHex = '';
93+
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
94+
const addressInfo = 'addressInfo' in unsignedTx ? unsignedTx.addressInfo : undefined;
95+
const feeInfo = 'feeInfo' in unsignedTx ? unsignedTx.feeInfo : undefined;
96+
const source = '';
97+
const dataToSign = '';
4598

46-
// Is the other class for xtz, eos, btc ==> AbstractUtxoCoin or do we have more specialization than that?
99+
const halfSignedTx = await sdkCoin.signTransaction({
100+
txPrebuild: {
101+
txHex,
102+
txInfo,
103+
addressInfo,
104+
feeInfo,
105+
source,
106+
dataToSign,
107+
},
108+
prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }),
109+
});
110+
} catch (err) {
111+
console.log(err);
112+
throw err;
113+
}
114+
} else if (isStxCoin(sdkCoin)) {
115+
//TODO: (implementation untested): prioritize eth and btc instead of stc, when the other couple finished, go back to STX
116+
try {
117+
const unsignedTx = await sdkCoin.recover({
118+
...commonRecoverParams,
119+
rootAddress: walletContractAddress, // TODO: is a root address the same as wallet contract address? where does root address comes from if not?
120+
});
121+
} catch (err) {
122+
console.log(err);
123+
throw err;
124+
}
125+
} else if (isEosCoin(sdkCoin)) {
126+
// TODO (implementation untested): we need some funds but faucets not working
127+
try {
128+
const unsignedTx = await sdkCoin.recover({
129+
...commonRecoverParams,
130+
});
131+
} catch (err) {
132+
console.log(err);
133+
throw err;
134+
}
135+
} else if (isUtxoCoin(sdkCoin)) {
136+
//TODO (implementation untested): we need an API key to complete/test btc flow
137+
//TODO: do we need a special case for BTC or is another UTXO-based coin?
47138

48-
try {
49-
// const { apiKey, walletContractAddress } = coinSpecificParams;
139+
const { bitgoPub } = coinSpecificParams || '';
140+
try {
141+
const unsignedTx = await sdkCoin.recover({
142+
...commonRecoverParams,
143+
bitgoKey: bitgoPub,
144+
ignoreAddressTypes: coinSpecificParams?.ignoreAddressTypes || [],
145+
});
50146

51-
// recover also ask for gasPrice, gasLimit, replayProtectionOptions, etc
52-
// should we bring those from the coinSpecificParams or just let them empty?
53-
const unsignedTx = await sdkCoin.recover({
54-
userKey: userPubKey,
55-
backupKey: backupPubKey,
56-
walletContractAddress,
57-
recoveryDestination: recoveryDestinationAddress,
58-
apiKey,
59-
walletPassphrase: '^.u0UWaTI;cIx!xi9Ya1',
60-
} as any as RecoverOptions);
61-
console.log('unsigned tx payload');
62-
console.log(JSON.stringify(unsignedTx));
63-
} catch (err) {
64-
console.log(err);
147+
// some guards as the types have some imcompatibilities issues
148+
const txInfo = 'txInfo' in unsignedTx ? unsignedTx.txInfo : undefined;
149+
const txHex = 'txHex' in unsignedTx ? unsignedTx.txHex : '';
150+
151+
const halfSignedTx = await sdkCoin.signTransaction({
152+
txPrebuild: {
153+
txHex,
154+
txInfo,
155+
},
156+
prv: bitgo.decrypt({ password: passphrase, input: userEncryptedPrv }),
157+
});
158+
159+
const fullSignedTx = await sdkCoin.signTransaction({
160+
//TODO: check the body of this based on halfSignedTx output
161+
isLastSignature: true,
162+
txPrebuild: {
163+
txHex,
164+
txInfo,
165+
},
166+
signingStep: 'cosignerNonce',
167+
});
168+
} catch (err) {
169+
console.log(err);
170+
throw err;
171+
}
172+
} else {
173+
throw new Error('Unsupported coin type for recovery: ' + coin);
65174
}
66175
}
67176
}

src/shared/coinUtils.ts

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { AbstractEthLikeNewCoins } from '@bitgo/abstract-eth';
2+
import { BaseCoin } from 'bitgo';
3+
import { AbstractUtxoCoin, Eos, Stx, Xtz } from 'bitgo/dist/types/src/v2/coins';
4+
5+
export function isEthCoin(coin: BaseCoin): coin is AbstractEthLikeNewCoins {
6+
const isEthPure =
7+
isFamily(coin, 'eth', 'gteth') ||
8+
isFamily(coin, 'eth', 'hteth') ||
9+
isFamily(coin, 'ethw', 'tethw');
10+
11+
const isEthLike =
12+
isFamily(coin, 'rbtc', 'trbtc') ||
13+
isFamily(coin, 'etc', 'tetc') ||
14+
isFamily(coin, 'avaxc', 'tavaxc') ||
15+
isFamily(coin, 'polygon', 'tpolygon') ||
16+
isFamily(coin, 'arbeth', 'tarbeth') ||
17+
isFamily(coin, 'opeth', 'topeth') ||
18+
isFamily(coin, 'bsc', 'tbsc') ||
19+
isFamily(coin, 'baseeth', 'tbaseeth') ||
20+
isFamily(coin, 'coredao', 'tcoredao') ||
21+
isFamily(coin, 'oas', 'toas') ||
22+
isFamily(coin, 'flr', 'tflr') ||
23+
isFamily(coin, 'sgb', 'tsgb') ||
24+
isFamily(coin, 'wemix', 'twemix') ||
25+
isFamily(coin, 'xdc', 'txdc');
26+
27+
return isEthPure || isEthLike;
28+
}
29+
30+
export function isUtxoCoin(coin: BaseCoin): coin is AbstractUtxoCoin {
31+
// how to check if coin is UTXO? so many families
32+
const isBtc = isFamily(coin, 'btc', 'tbtc');
33+
34+
const isBtcLike =
35+
isFamily(coin, 'ltc', 'tltc') ||
36+
isFamily(coin, 'bch', 'tbch') ||
37+
isFamily(coin, 'zec', 'tzec') ||
38+
isFamily(coin, 'dash', 'tdash') ||
39+
isFamily(coin, 'doge', 'tdoge') ||
40+
isFamily(coin, 'btg', 'tbtg');
41+
42+
return isBtc || isBtcLike;
43+
}
44+
45+
//look for those on OVC repo
46+
//https://github.com/BitGo/offline-vault-console/blob/7f850cdd10c89ceb850c69759349b9e0bbfb56db/frontend/src/pkg/bitgo/transaction-utils.ts#L595
47+
export function isEosCoin(coin: BaseCoin): coin is Eos {
48+
return isFamily(coin, 'eos', 'teos');
49+
}
50+
51+
export function isStxCoin(coin: BaseCoin): coin is Stx {
52+
return isFamily(coin, 'stx', 'tstx');
53+
}
54+
55+
export function isXtzCoin(coin: BaseCoin): coin is Xtz {
56+
// Tezos faucet: https://faucet.ghostnet.teztnets.com/
57+
return isFamily(coin, 'xtz', 'txtz');
58+
}
59+
60+
function isFamily(coin: BaseCoin, coinFamily: string, testFamily: string) {
61+
if (!coin) {
62+
return false;
63+
}
64+
const family = coin.getFamily();
65+
return family === coinFamily || family === testFamily;
66+
}

0 commit comments

Comments
 (0)