Skip to content

Commit c141817

Browse files
Merge pull request #5490 from BitGo/COIN-2884-send-recovery-txn
feat(abstract-eth): add method to build and send ccr recovery txn
2 parents 3aca764 + 0221896 commit c141817

File tree

5 files changed

+174
-1
lines changed

5 files changed

+174
-1
lines changed

modules/abstract-eth/src/abstractEthLikeNewCoins.ts

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import {
6868
TransactionBuilder,
6969
TransferBuilder,
7070
} from './lib';
71+
import { SendCrossChainRecoveryOptions } from './types';
7172

7273
/**
7374
* The prebuilt hop transaction returned from the HSM
@@ -1321,6 +1322,58 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
13211322
};
13221323
}
13231324

1325+
async sendCrossChainRecoveryTransaction(
1326+
params: SendCrossChainRecoveryOptions
1327+
): Promise<{ coin: string; txHex?: string; txid: string }> {
1328+
const buildResponse = await this.buildCrossChainRecoveryTransaction(params.recoveryId);
1329+
if (params.walletType === 'cold') {
1330+
return buildResponse;
1331+
}
1332+
if (!params.encryptedPrv) {
1333+
throw new Error('missing encryptedPrv');
1334+
}
1335+
1336+
let userKeyPrv;
1337+
try {
1338+
userKeyPrv = this.bitgo.decrypt({
1339+
input: params.encryptedPrv,
1340+
password: params.walletPassphrase,
1341+
});
1342+
} catch (e) {
1343+
throw new Error(`Error decrypting user keychain: ${e.message}`);
1344+
}
1345+
const keyPair = new KeyPairLib({ prv: userKeyPrv });
1346+
const userSigningKey = keyPair.getKeys().prv;
1347+
if (!userSigningKey) {
1348+
throw new Error('no private key');
1349+
}
1350+
1351+
const txBuilder = this.getTransactionBuilder(params.common) as TransactionBuilder;
1352+
const txHex = buildResponse.txHex;
1353+
txBuilder.from(txHex);
1354+
txBuilder
1355+
.transfer()
1356+
.coin(this.staticsCoin?.name as string)
1357+
.key(userSigningKey);
1358+
const tx = await txBuilder.build();
1359+
const res = await this.bitgo
1360+
.post(this.bitgo.microservicesUrl(`/api/recovery/v1/crosschain/${params.recoveryId}/sign`))
1361+
.send({ txHex: tx.toBroadcastFormat() });
1362+
return {
1363+
coin: this.staticsCoin?.name as string,
1364+
txid: res.body.txid,
1365+
};
1366+
}
1367+
1368+
async buildCrossChainRecoveryTransaction(recoveryId: string): Promise<{ coin: string; txHex: string; txid: string }> {
1369+
const res = await this.bitgo.get(this.bitgo.microservicesUrl(`/api/recovery/v1/crosschain/${recoveryId}/buildtx`));
1370+
return {
1371+
coin: res.body.coin,
1372+
txHex: res.body.txHex,
1373+
txid: res.body.txid,
1374+
};
1375+
}
1376+
13241377
/**
13251378
* Builds a unsigned (for cold, custody wallet) or
13261379
* half-signed (for hot wallet) evm cross chain recovery transaction with

modules/abstract-eth/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import EthereumCommon from '@ethereumjs/common';
2+
3+
export type SendCrossChainRecoveryOptions = {
4+
recoveryId: string;
5+
walletPassphrase?: string;
6+
encryptedPrv?: string;
7+
walletType: 'hot' | 'cold';
8+
common?: EthereumCommon;
9+
};

modules/sdk-coin-ethlike/test/fixtures/ethlikeCoin.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,3 +49,21 @@ export const getContractCallResponse = {
4949
result: '0x0000000000000000000000000000000000000000000000000000000000002a7f',
5050
id: 1,
5151
};
52+
53+
export const ccr = {
54+
hteth: {
55+
txHex:
56+
'0xf9012c02843b9aca0083b8a1a0948f977e912ef500548a0c3be6ddde9899f1199b8180b901043912521500000000000000000000000019645032c7f1533395d44a629462e751084d3e4c000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000005ec67e28000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008284f38080',
57+
txid: '0x62261142553d7dd3c574a93628391d58fe52b4005db1a230d8b6b99ce3584638',
58+
},
59+
tarbeth: {
60+
txHex:
61+
'0xf9012e018541314cf000836acfc0948ce59c2d1702844f8eded451aa103961bc37b4e880b9010439125215000000000000000000000000eeaf0f05f37891ab4a21208b105a0687d12c5af7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830cddff8080',
62+
txid: '0xc6cff6f06e0452f85886c1e4f212a3fe444fe981939f80d8aad98e9a8507e526',
63+
},
64+
};
65+
export const encryptedUserKey =
66+
'{"iv":"VFZ3jvXhxo1Z+Yaf2MtZnA==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' +
67+
':"ccm","adata":"","cipher":"aes","salt":"p+fkHuLa/8k=","ct":"hYG7pvljLIgCjZ\n' +
68+
'53PBlCde5KZRmlUKKHLtDMk+HJfuU46hW+x+C9WsIAO4gFPnTCvFVmQ8x7czCtcNFub5AO2otOG\n' +
69+
'OsX4GE2gXOEmCl1TpWwwNhm7yMUjGJUpgW6ZZgXSXdDitSKi4V/hk78SGSzjFOBSPYRa6I="}\n';

modules/sdk-coin-ethlike/test/resources.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import EthereumCommon from '@ethereumjs/common';
2+
import { coins, EthereumNetwork } from '@bitgo/statics';
23
import { BN } from 'ethereumjs-util';
34

45
export const baseChainCommon = EthereumCommon.custom({
@@ -8,6 +9,15 @@ export const baseChainCommon = EthereumCommon.custom({
89
defaultHardfork: 'london',
910
});
1011

12+
export function getCommon(coin: string): EthereumCommon {
13+
return EthereumCommon.custom({
14+
name: coins.get(coin).name,
15+
networkId: (coins.get(coin).network as EthereumNetwork).chainId,
16+
chainId: (coins.get(coin).network as EthereumNetwork).chainId,
17+
defaultHardfork: 'london',
18+
});
19+
}
20+
1121
export const WALLET_FACTORY_ADDRESS = '0x809ee567e413543af1caebcdb247f6a67eafc8dd';
1222

1323
export const PRIVATE_KEY_1 =

modules/sdk-coin-ethlike/test/unit/ethlikeCoin.ts

Lines changed: 84 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,94 @@ import nock from 'nock';
88

99
import { EthLikeCoin, TethLikeCoin, EthLikeTransactionBuilder } from '../../src';
1010
import { getBuilder } from '../getBuilder';
11-
import { baseChainCommon } from '../resources';
11+
import { baseChainCommon, getCommon } from '../resources';
1212
import * as mockData from '../fixtures/ethlikeCoin';
1313

1414
nock.enableNetConnect();
1515

16+
const coins = [
17+
{
18+
name: 'hteth',
19+
common: getCommon('hteth'),
20+
},
21+
{
22+
name: 'tarbeth',
23+
common: getCommon('tarbeth'),
24+
},
25+
];
26+
27+
describe('EthLike coin tests', function () {
28+
let bitgo: TestBitGoAPI;
29+
let basecoin: TethLikeCoin;
30+
coins.forEach((coin) => {
31+
describe(coin.name, function () {
32+
before(function () {
33+
const env = 'test';
34+
bitgo = TestBitGo.decorate(BitGoAPI, { env });
35+
bitgo.safeRegister(coin.name, TethLikeCoin.createInstance);
36+
bitgo.initializeTestVars();
37+
basecoin = bitgo.coin(coin.name) as TethLikeCoin;
38+
});
39+
40+
after(function () {
41+
nock.cleanAll();
42+
});
43+
44+
it('should instantiate a coin', function () {
45+
basecoin.should.be.an.instanceof(TethLikeCoin);
46+
});
47+
it('should reject for missing encryptedPrv for hot wallet', async function () {
48+
const recoveryId = '0x1234567890abcdef';
49+
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
50+
txHex: mockData.ccr[coin.name].txHex,
51+
});
52+
const walletPassphrase = TestBitGo.V2.TEST_RECOVERY_PASSCODE as string;
53+
const params = {
54+
recoveryId,
55+
walletPassphrase,
56+
common: coin.common,
57+
};
58+
await basecoin
59+
.sendCrossChainRecoveryTransaction({ ...params, walletType: 'hot' })
60+
.should.be.rejectedWith('missing encryptedPrv');
61+
});
62+
it('should send cross chain recovery transaction for hot wallet', async function () {
63+
const recoveryId = '0x1234567890abcdef';
64+
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
65+
txHex: mockData.ccr[coin.name].txHex,
66+
});
67+
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).post(`/${recoveryId}/sign`).reply(200, {
68+
coin: coin.name,
69+
txid: mockData.ccr[coin.name].txid,
70+
});
71+
const walletPassphrase = TestBitGo.V2.TEST_RECOVERY_PASSCODE as string;
72+
const params = {
73+
recoveryId,
74+
walletPassphrase,
75+
encryptedPrv: mockData.encryptedUserKey,
76+
common: coin.common,
77+
};
78+
const result = await basecoin.sendCrossChainRecoveryTransaction({ ...params, walletType: 'hot' });
79+
result.coin.should.equal(coin.name);
80+
result.txid.should.equal(mockData.ccr[coin.name].txid);
81+
});
82+
83+
it('should build txn for cross chain recovery for cold wallet', async function () {
84+
const recoveryId = '0x1234567890abcdef';
85+
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
86+
txHex: mockData.ccr[coin.name].txHex,
87+
});
88+
const params = {
89+
recoveryId,
90+
common: coin.common,
91+
};
92+
const result = await basecoin.sendCrossChainRecoveryTransaction({ ...params, walletType: 'cold' });
93+
assert(result.txHex);
94+
result.txHex.should.equal(mockData.ccr[coin.name].txHex);
95+
});
96+
});
97+
});
98+
});
1699
describe('EthLikeCoin', function () {
17100
let bitgo: TestBitGoAPI;
18101
const coinName = 'tbaseeth';

0 commit comments

Comments
 (0)