Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sdk-coin-etc): unsigned sweep support and fullsign support for etc via ovc #4853

Merged
merged 1 commit into from
Aug 28, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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));
};
Loading