Skip to content

Commit

Permalink
Merge pull request #5490 from BitGo/COIN-2884-send-recovery-txn
Browse files Browse the repository at this point in the history
feat(abstract-eth): add method to build and send ccr recovery txn
  • Loading branch information
mullapudipruthvik authored Feb 7, 2025
2 parents 3aca764 + 0221896 commit c141817
Show file tree
Hide file tree
Showing 5 changed files with 174 additions and 1 deletion.
53 changes: 53 additions & 0 deletions modules/abstract-eth/src/abstractEthLikeNewCoins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ import {
TransactionBuilder,
TransferBuilder,
} from './lib';
import { SendCrossChainRecoveryOptions } from './types';

/**
* The prebuilt hop transaction returned from the HSM
Expand Down Expand Up @@ -1321,6 +1322,58 @@ export abstract class AbstractEthLikeNewCoins extends AbstractEthLikeCoin {
};
}

async sendCrossChainRecoveryTransaction(
params: SendCrossChainRecoveryOptions
): Promise<{ coin: string; txHex?: string; txid: string }> {
const buildResponse = await this.buildCrossChainRecoveryTransaction(params.recoveryId);
if (params.walletType === 'cold') {
return buildResponse;
}
if (!params.encryptedPrv) {
throw new Error('missing encryptedPrv');
}

let userKeyPrv;
try {
userKeyPrv = this.bitgo.decrypt({
input: params.encryptedPrv,
password: params.walletPassphrase,
});
} catch (e) {
throw new Error(`Error decrypting user keychain: ${e.message}`);
}
const keyPair = new KeyPairLib({ prv: userKeyPrv });
const userSigningKey = keyPair.getKeys().prv;
if (!userSigningKey) {
throw new Error('no private key');
}

const txBuilder = this.getTransactionBuilder(params.common) as TransactionBuilder;
const txHex = buildResponse.txHex;
txBuilder.from(txHex);
txBuilder
.transfer()
.coin(this.staticsCoin?.name as string)
.key(userSigningKey);
const tx = await txBuilder.build();
const res = await this.bitgo
.post(this.bitgo.microservicesUrl(`/api/recovery/v1/crosschain/${params.recoveryId}/sign`))
.send({ txHex: tx.toBroadcastFormat() });
return {
coin: this.staticsCoin?.name as string,
txid: res.body.txid,
};
}

async buildCrossChainRecoveryTransaction(recoveryId: string): Promise<{ coin: string; txHex: string; txid: string }> {
const res = await this.bitgo.get(this.bitgo.microservicesUrl(`/api/recovery/v1/crosschain/${recoveryId}/buildtx`));
return {
coin: res.body.coin,
txHex: res.body.txHex,
txid: res.body.txid,
};
}

/**
* Builds a unsigned (for cold, custody wallet) or
* half-signed (for hot wallet) evm cross chain recovery transaction with
Expand Down
9 changes: 9 additions & 0 deletions modules/abstract-eth/src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import EthereumCommon from '@ethereumjs/common';

export type SendCrossChainRecoveryOptions = {
recoveryId: string;
walletPassphrase?: string;
encryptedPrv?: string;
walletType: 'hot' | 'cold';
common?: EthereumCommon;
};
18 changes: 18 additions & 0 deletions modules/sdk-coin-ethlike/test/fixtures/ethlikeCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,3 +49,21 @@ export const getContractCallResponse = {
result: '0x0000000000000000000000000000000000000000000000000000000000002a7f',
id: 1,
};

export const ccr = {
hteth: {
txHex:
'0xf9012c02843b9aca0083b8a1a0948f977e912ef500548a0c3be6ddde9899f1199b8180b901043912521500000000000000000000000019645032c7f1533395d44a629462e751084d3e4c000000000000000000000000000000000000000000000000000000003b9aca0000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000005ec67e28000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000e0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008284f38080',
txid: '0x62261142553d7dd3c574a93628391d58fe52b4005db1a230d8b6b99ce3584638',
},
tarbeth: {
txHex:
'0xf9012e018541314cf000836acfc0948ce59c2d1702844f8eded451aa103961bc37b4e880b9010439125215000000000000000000000000eeaf0f05f37891ab4a21208b105a0687d12c5af7000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000c00000000000000000000000000000000000000000000000000000000000002710000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000e000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000830cddff8080',
txid: '0xc6cff6f06e0452f85886c1e4f212a3fe444fe981939f80d8aad98e9a8507e526',
},
};
export const encryptedUserKey =
'{"iv":"VFZ3jvXhxo1Z+Yaf2MtZnA==","v":1,"iter":10000,"ks":256,"ts":64,"mode"\n' +
':"ccm","adata":"","cipher":"aes","salt":"p+fkHuLa/8k=","ct":"hYG7pvljLIgCjZ\n' +
'53PBlCde5KZRmlUKKHLtDMk+HJfuU46hW+x+C9WsIAO4gFPnTCvFVmQ8x7czCtcNFub5AO2otOG\n' +
'OsX4GE2gXOEmCl1TpWwwNhm7yMUjGJUpgW6ZZgXSXdDitSKi4V/hk78SGSzjFOBSPYRa6I="}\n';
10 changes: 10 additions & 0 deletions modules/sdk-coin-ethlike/test/resources.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import EthereumCommon from '@ethereumjs/common';
import { coins, EthereumNetwork } from '@bitgo/statics';
import { BN } from 'ethereumjs-util';

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

export function getCommon(coin: string): EthereumCommon {
return EthereumCommon.custom({
name: coins.get(coin).name,
networkId: (coins.get(coin).network as EthereumNetwork).chainId,
chainId: (coins.get(coin).network as EthereumNetwork).chainId,
defaultHardfork: 'london',
});
}

export const WALLET_FACTORY_ADDRESS = '0x809ee567e413543af1caebcdb247f6a67eafc8dd';

export const PRIVATE_KEY_1 =
Expand Down
85 changes: 84 additions & 1 deletion modules/sdk-coin-ethlike/test/unit/ethlikeCoin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,94 @@ import nock from 'nock';

import { EthLikeCoin, TethLikeCoin, EthLikeTransactionBuilder } from '../../src';
import { getBuilder } from '../getBuilder';
import { baseChainCommon } from '../resources';
import { baseChainCommon, getCommon } from '../resources';
import * as mockData from '../fixtures/ethlikeCoin';

nock.enableNetConnect();

const coins = [
{
name: 'hteth',
common: getCommon('hteth'),
},
{
name: 'tarbeth',
common: getCommon('tarbeth'),
},
];

describe('EthLike coin tests', function () {
let bitgo: TestBitGoAPI;
let basecoin: TethLikeCoin;
coins.forEach((coin) => {
describe(coin.name, function () {
before(function () {
const env = 'test';
bitgo = TestBitGo.decorate(BitGoAPI, { env });
bitgo.safeRegister(coin.name, TethLikeCoin.createInstance);
bitgo.initializeTestVars();
basecoin = bitgo.coin(coin.name) as TethLikeCoin;
});

after(function () {
nock.cleanAll();
});

it('should instantiate a coin', function () {
basecoin.should.be.an.instanceof(TethLikeCoin);
});
it('should reject for missing encryptedPrv for hot wallet', async function () {
const recoveryId = '0x1234567890abcdef';
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
txHex: mockData.ccr[coin.name].txHex,
});
const walletPassphrase = TestBitGo.V2.TEST_RECOVERY_PASSCODE as string;
const params = {
recoveryId,
walletPassphrase,
common: coin.common,
};
await basecoin
.sendCrossChainRecoveryTransaction({ ...params, walletType: 'hot' })
.should.be.rejectedWith('missing encryptedPrv');
});
it('should send cross chain recovery transaction for hot wallet', async function () {
const recoveryId = '0x1234567890abcdef';
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
txHex: mockData.ccr[coin.name].txHex,
});
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).post(`/${recoveryId}/sign`).reply(200, {
coin: coin.name,
txid: mockData.ccr[coin.name].txid,
});
const walletPassphrase = TestBitGo.V2.TEST_RECOVERY_PASSCODE as string;
const params = {
recoveryId,
walletPassphrase,
encryptedPrv: mockData.encryptedUserKey,
common: coin.common,
};
const result = await basecoin.sendCrossChainRecoveryTransaction({ ...params, walletType: 'hot' });
result.coin.should.equal(coin.name);
result.txid.should.equal(mockData.ccr[coin.name].txid);
});

it('should build txn for cross chain recovery for cold wallet', async function () {
const recoveryId = '0x1234567890abcdef';
nock(bitgo.microservicesUrl(`/api/recovery/v1/crosschain`)).get(`/${recoveryId}/buildtx`).reply(200, {
txHex: mockData.ccr[coin.name].txHex,
});
const params = {
recoveryId,
common: coin.common,
};
const result = await basecoin.sendCrossChainRecoveryTransaction({ ...params, walletType: 'cold' });
assert(result.txHex);
result.txHex.should.equal(mockData.ccr[coin.name].txHex);
});
});
});
});
describe('EthLikeCoin', function () {
let bitgo: TestBitGoAPI;
const coinName = 'tbaseeth';
Expand Down

0 comments on commit c141817

Please sign in to comment.