Skip to content

Commit

Permalink
feat(sdk-coin-etc): unsigned sweep support and fullsign support for e…
Browse files Browse the repository at this point in the history
…tc via ovc

TICKET: COIN-1595
  • Loading branch information
bhupendra11 committed Aug 27, 2024
1 parent 4d69ed8 commit 206cd9b
Show file tree
Hide file tree
Showing 3 changed files with 153 additions and 4 deletions.
63 changes: 63 additions & 0 deletions modules/sdk-coin-etc/src/etc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {
optionalDeps,
RecoverOptions,
RecoveryInfo,
SignedTransaction,
SignTransactionOptions,
} from '@bitgo/abstract-eth';
import { BaseCoin, BitGoBase, common, getIsUnsignedSweep, Util, Recipient } from '@bitgo/sdk-core';
import { BaseCoin as StaticsBaseCoin, coins, EthereumNetwork as EthLikeNetwork, ethGasConfigs } from '@bitgo/statics';
Expand Down Expand Up @@ -452,4 +454,65 @@ export class Etc extends AbstractEthLikeCoin {
getNetwork(): EthLikeNetwork | undefined {
return this.staticsCoin?.network as EthLikeNetwork;
}

/**
* Assemble half-sign prebuilt transaction
* @param {SignTransactionOptions} params
*/
async signTransaction(params: SignTransactionOptions): Promise<SignedTransaction> {
// Normally the SDK provides the first signature for an EthLike tx, but occasionally it provides the second and final one.
if (params.isLastSignature) {
// In this case when we're doing the second (final) signature, the logic is different.
return await this.signFinal(params);
}
const txBuilder = this.getTransactionBuilder();
txBuilder.from(params.txPrebuild.txHex);
txBuilder
.transfer()
.coin(this.staticsCoin?.name as string)
.key(new KeyPairLib({ prv: params.prv }).getKeys().prv!);
const transaction = await txBuilder.build();

const recipients = transaction.outputs.map((output) => ({ address: output.address, amount: output.value }));

const txParams = {
eip1559: params.txPrebuild.eip1559,
txHex: transaction.toBroadcastFormat(),
recipients: recipients,
expiration: params.txPrebuild.expireTime,
hopTransaction: params.txPrebuild.hopTransaction,
custodianTransactionId: params.custodianTransactionId,
expireTime: params.expireTime,
contractSequenceId: params.txPrebuild.nextContractSequenceId as number,
sequenceId: params.sequenceId,
};

return { halfSigned: txParams };
}

/**
* Helper function for signTransaction for the rare case that SDK is doing the second signature
* Note: we are expecting this to be called from the offline vault
* @param params.txPrebuild
* @param params.prv
* @returns {{txHex: string}}
*/
async signFinal(params) {
const keyPair = new KeyPairLib({ prv: params.prv });
const signingKey = keyPair.getKeys().prv;
if (_.isUndefined(signingKey)) {
throw new Error('missing private key');
}
const txBuilder = this.getTransactionBuilder();
try {
txBuilder.from(params.txPrebuild.halfSigned.txHex);
} catch (e) {
throw new Error('invalid half-signed transaction');
}
txBuilder.sign({ key: signingKey });
const tx = await txBuilder.build();
return {
txHex: tx.toBroadcastFormat(),
};
}
}
88 changes: 84 additions & 4 deletions modules/sdk-coin-etc/test/unit/etc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@ import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
import { BitGoAPI } from '@bitgo/sdk-api';
import { Etc, Tetc, TransactionBuilder } from '../../src';
import sinon from 'sinon';
import { OfflineVaultTxInfo } from '@bitgo/abstract-eth';
import { OfflineVaultTxInfo, SignTransactionOptions } from '@bitgo/abstract-eth';

import { BN } from 'ethereumjs-util';
import { getBuilder } from './getBuilder';
import { FullySignedTransaction } from '@bitgo/sdk-core';
import * as should from 'should';

describe('Ethereum Classic', function () {
let bitgo: TestBitGoAPI;
Expand Down Expand Up @@ -41,6 +44,8 @@ describe('Wallet Recovery Wizard', function () {
callBack.withArgs(sourceRootAddress).resolves(new BN('2190000000000000000'));
callBack.withArgs(backupKeyAddress).resolves(new BN('190000000000000000'));
callBack.withArgs('0x5273e0d869226ccf579a81b6d291fb3702ba9dec').resolves(new BN('0'));
callBack.withArgs('0x1b9af47cc3048fe1d31ad72299611d3df3926755').resolves(new BN('190000000000000000'));
callBack.withArgs('0x7fcf95a9106a0ed3bd09e653c8ea3d5e489bfb23').resolves(new BN('2190000000000000000'));
});

afterEach(function () {
Expand Down Expand Up @@ -74,7 +79,7 @@ describe('Wallet Recovery Wizard', function () {
recovery.should.have.property('id');
recovery.should.have.property('tx');

const txBuilder = tetcCoin.getTransactionBuilder() as TransactionBuilder;
const txBuilder = getBuilder('tetc') as TransactionBuilder;
txBuilder.from(recovery.tx);
const tx = await txBuilder.build();
tx.toBroadcastFormat().should.not.be.empty();
Expand Down Expand Up @@ -104,10 +109,85 @@ describe('Wallet Recovery Wizard', function () {
});
});

// Add tests related to unsigned sweep here if any
describe('Unsigned sweep', function () {
describe('Unsigned sweep for cold wallet', function () {
const userXprv =
'xprv9s21ZrQH143K38Cfd5PyKGajVbA1sZYwAKQif8qvJMfMmSY85spqTnd4taexRHc9F92QCgBzHosCauYcnJWT9eWxfFKvSjAKoSgQkf74DoM';
const userXpub =
'xpub661MyMwAqRbcFcH8j6vygQXU3czWH2GnXYLKTXFXrhCLeEsGdR961awYjr3yC8eUj9rqhgFWHVbQJWqZS7kXpLBDzvoCKDLaBujsCH12Zfj';
const backupXprv =
'xprv9s21ZrQH143K3WkGc7rUw4NU5ZZTPczbMk9GajGxpJYhJXtfnYUL4j1x6vAGcxUg9XFzEHpQWPy3aYyJZcuGnYbc2eNzrsyNn3SRNdQa1PC';
const backupXpub =
'xpub661MyMwAqRbcGYaF52itktGhGDfiL9CBBTh4TSXV6QqGgXRbhSS5DAaTbdCPJA425XwkvwyCKtTmoxcUTAUgKUf7Qr5Ks9gJP9DTfiV2PhU';

const walletContractAddress = '0x7fcf95a9106a0ed3bd09e653c8ea3d5e489bfb23';
// tetc wallet 1 receiveAddress 4
const recoveryDestination = '0x321cbe223ff1c3d0c03b73b8c648ef2d91e4aaa1';
const gasPrice = 25000000000;

beforeEach(function () {
tetcCoin = bitgo.coin('tetc') as Tetc;
});

afterEach(function () {
sandbox.restore();
});

it('should generate an ETH unsigned sweep', async function () {
const transaction: OfflineVaultTxInfo = (await tetcCoin.recover({
userKey: userXpub,
backupKey: backupXpub,
walletContractAddress,
recoveryDestination,
gasPrice,
})) as OfflineVaultTxInfo;
should.exist(transaction);
transaction.should.have.property('txHex');
transaction.should.have.property('userKey');
transaction.should.have.property('backupKey');
transaction.should.have.property('gasLimit');
transaction.gasLimit.should.equal('500000');
transaction.should.have.property('gasPrice');
transaction.gasPrice.should.equal('25000000000');
transaction.should.have.property('walletContractAddress');
transaction.walletContractAddress.should.equal('0x7fcf95a9106a0ed3bd09e653c8ea3d5e489bfb23');
transaction.should.have.property('recipient');
});

it('should add a second signature', async function () {
const transaction = (await tetcCoin.recover({
userKey: userXpub,
backupKey: backupXpub,
walletContractAddress,
recoveryDestination,
gasPrice,
})) as OfflineVaultTxInfo;

const txPrebuild = {
txHex: transaction.txHex,
};

const params = {
txPrebuild,
prv: userXprv,
};
// sign transaction once
const halfSigned = await tetcCoin.signTransaction(params as SignTransactionOptions);
const halfSignedParams = {
txPrebuild: halfSigned,
isLastSignature: true,
walletContractAddress: walletContractAddress,
prv: backupXprv,
};
// sign transaction twice with the "isLastSignature" flag
const finalSignedTx = (await tetcCoin.signTransaction(
halfSignedParams as SignTransactionOptions
)) as FullySignedTransaction;
finalSignedTx.should.have.property('txHex');
const txBuilder = tetcCoin.getTransactionBuilder() as TransactionBuilder;
txBuilder.from(finalSignedTx.txHex);
const rebuiltTx = await txBuilder.build();
rebuiltTx.signature.length.should.equal(2);
rebuiltTx.outputs.length.should.equal(1);
});
});
});
6 changes: 6 additions & 0 deletions modules/sdk-coin-etc/test/unit/getBuilder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { TransactionBuilder } from '../../src';
import { coins } from '@bitgo/statics';

export const getBuilder = (coin: string): TransactionBuilder => {
return new TransactionBuilder(coins.get(coin));
};

0 comments on commit 206cd9b

Please sign in to comment.