Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export class PermissionlessValidatorTxBuilder extends TransactionBuilder {
protected _endTime: bigint;
protected _stakeAmount: bigint;
protected _delegationFeeRate: number;
protected recoverSigner = false;

constructor(coinConfig: Readonly<CoinConfig>) {
super(coinConfig);
Expand Down
33 changes: 29 additions & 4 deletions modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,23 @@ interface Codec {
}

export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
protected recoverSigner = false;

constructor(_coinConfig: Readonly<CoinConfig>) {
super(_coinConfig);
}

/**
* Enables recovery mode for transaction building.
* When enabled, uses backup key (index 2) instead of user key (index 0) for signing.
* @param recoverSigner - Whether to use recovery signing (default: true)
* @returns this factory for chaining
*/
recoverMode(recoverSigner = true): this {
this.recoverSigner = recoverSigner;
return this;
}

/**
* Extract credentials from remaining bytes after transaction using FlareJS codec.
* This is the proper way to parse credentials - using the codec's UnpackPrefix method.
Expand Down Expand Up @@ -73,6 +86,18 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
}
}

/**
* Apply recovery mode setting to a builder if enabled on the factory.
* @param builder The transaction builder to configure
* @returns The configured builder
*/
private applyRecoverMode<T extends TransactionBuilder>(builder: T): T {
if (this.recoverSigner) {
builder.recoverMode(true);
}
return builder;
}

/**
* Create the appropriate transaction builder based on transaction type.
* @param tx The parsed transaction
Expand All @@ -91,23 +116,23 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory {
if (ExportInCTxBuilder.verifyTxType(tx._type)) {
const builder = this.getExportInCBuilder();
builder.initBuilder(tx as evmSerial.ExportTx, rawBuffer, credentials);
return builder;
return this.applyRecoverMode(builder);
}
if (ImportInCTxBuilder.verifyTxType(tx._type)) {
const builder = this.getImportInCBuilder();
builder.initBuilder(tx as evmSerial.ImportTx, rawBuffer, credentials);
return builder;
return this.applyRecoverMode(builder);
}
} else {
if (ImportInPTxBuilder.verifyTxType(tx._type)) {
const builder = this.getImportInPBuilder();
builder.initBuilder(tx as pvmSerial.ImportTx, rawBuffer, credentials);
return builder;
return this.applyRecoverMode(builder);
}
if (ExportInPTxBuilder.verifyTxType(tx._type)) {
const builder = this.getExportInPBuilder();
builder.initBuilder(tx as pvmSerial.ExportTx, rawBuffer, credentials);
return builder;
return this.applyRecoverMode(builder);
}
}
throw new NotSupported('Transaction type not supported');
Expand Down
21 changes: 21 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/exportInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
import { TransactionBuilderFactory, DecodedUtxoObj, Transaction } from '../../../src/lib';
import { coins, FlareNetwork } from '@bitgo/statics';
import signFlowTest from './signFlowTestSuit';
import recoverModeTestSuit from './recoverModeTestSuit';

describe('Flrp Export In P Tx Builder', () => {
const coinConfig = coins.get('tflrp');
Expand Down Expand Up @@ -189,4 +190,24 @@ describe('Flrp Export In P Tx Builder', () => {
err.message.should.be.equal('Private key cannot sign the transaction');
});
});

recoverModeTestSuit({
transactionType: 'Export P (Recovery Mode)',
newTxFactory: () => new TransactionBuilderFactory(coins.get('tflrp')),
newTxBuilder: () =>
new TransactionBuilderFactory(coins.get('tflrp'))
.getExportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.amount(testData.amount)
.externalChainId(testData.sourceChainId)
.fee(testData.fee)
.utxos(testData.outputs),
privateKey: {
prv1: testData.privateKeys[0],
prv2: testData.privateKeys[1],
prv3: testData.privateKeys[2],
},
});
});
19 changes: 19 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/importInCTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { TransactionBuilderFactory, DecodedUtxoObj } from '../../../src/lib';
import { coins } from '@bitgo/statics';
import { IMPORT_IN_C as testData } from '../../resources/transactionData/importInC';
import signFlowTest from './signFlowTestSuit';
import recoverModeTestSuit from './recoverModeTestSuit';

describe('Flrp Import In C Tx Builder', () => {
const factory = new TransactionBuilderFactory(coins.get('tflrp'));
Expand Down Expand Up @@ -49,4 +50,22 @@ describe('Flrp Import In C Tx Builder', () => {
},
txHash: testData.txhash,
});

recoverModeTestSuit({
transactionType: 'Import C (Recovery Mode)',
newTxFactory: () => new TransactionBuilderFactory(coins.get('tflrp')),
newTxBuilder: () =>
new TransactionBuilderFactory(coins.get('tflrp'))
.getImportInCBuilder()
.threshold(testData.threshold)
.fromPubKey(testData.pAddresses)
.utxos(testData.outputs)
.to(testData.to)
.feeRate(testData.fee),
privateKey: {
prv1: testData.privateKeys[0],
prv2: testData.privateKeys[1],
prv3: testData.privateKeys[2],
},
});
});
20 changes: 20 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { IMPORT_IN_P as testData } from '../../resources/transactionData/importI
import { TransactionBuilderFactory, DecodedUtxoObj, Transaction } from '../../../src/lib';
import { coins, FlareNetwork } from '@bitgo/statics';
import signFlowTest from './signFlowTestSuit';
import recoverModeTestSuit from './recoverModeTestSuit';

describe('Flrp Import In P Tx Builder', () => {
const coinConfig = coins.get('tflrp');
Expand Down Expand Up @@ -129,4 +130,23 @@ describe('Flrp Import In P Tx Builder', () => {
err.message.should.be.equal('Private key cannot sign the transaction');
});
});

recoverModeTestSuit({
transactionType: 'Import P (Recovery Mode)',
newTxFactory: () => new TransactionBuilderFactory(coins.get('tflrp')),
newTxBuilder: () =>
new TransactionBuilderFactory(coins.get('tflrp'))
.getImportInPBuilder()
.threshold(testData.threshold)
.locktime(testData.locktime)
.fromPubKey(testData.pAddresses)
.externalChainId(testData.sourceChainId)
.fee(testData.fee)
.utxos(testData.outputs),
privateKey: {
prv1: testData.privateKeys[0],
prv2: testData.privateKeys[1],
prv3: testData.privateKeys[2],
},
});
});
126 changes: 126 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/recoverModeTestSuit.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
import { BaseTransactionBuilder, BaseTransactionBuilderFactory } from '@bitgo/sdk-core';

export interface RecoverModeTestSuitArgs {
transactionType: string;
newTxFactory: () => BaseTransactionBuilderFactory;
newTxBuilder: () => BaseTransactionBuilder;
privateKey: { prv1: string; prv2: string; prv3: string };
}

/**
* Test suite focusing on recovery mode signing.
* In recovery mode, the backup key (prv3) is used instead of user key (prv1) along with BitGo key (prv2).
* @param {RecoverModeTestSuitArgs} data with required info.
*/
export default function recoverModeTestSuit(data: RecoverModeTestSuitArgs): void {
describe(`should test recovery mode for ${data.transactionType}`, () => {
it('Should set recoverMode flag on builder', async () => {
const txBuilder = data.newTxBuilder();
// @ts-expect-error - accessing protected property for testing
txBuilder.recoverSigner.should.equal(false);

// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode(true);
// @ts-expect-error - accessing protected property for testing
txBuilder.recoverSigner.should.equal(true);

// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode(false);
// @ts-expect-error - accessing protected property for testing
txBuilder.recoverSigner.should.equal(false);
});

it('Should default recoverMode to true when called without argument', async () => {
const txBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode();
// @ts-expect-error - accessing protected property for testing
txBuilder.recoverSigner.should.equal(true);
});

it('Should build unsigned tx in recovery mode', async () => {
const txBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode(true);
const tx = await txBuilder.build();
tx.toBroadcastFormat().should.be.a.String();
});

it('Should half sign tx in recovery mode using backup key (prv3)', async () => {
const txBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode(true);

// In recovery mode, sign with backup key (prv3) instead of user key (prv1)
txBuilder.sign({ key: data.privateKey.prv3 });
const tx = await txBuilder.build();
const halfSignedHex = tx.toBroadcastFormat();
halfSignedHex.should.be.a.String();
halfSignedHex.length.should.be.greaterThan(0);
});

it('Should full sign tx in recovery mode using backup key (prv3) and bitgo key (prv2)', async () => {
const txBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
txBuilder.recoverMode(true);

// In recovery mode: backup key (prv3) + bitgo key (prv2)
txBuilder.sign({ key: data.privateKey.prv3 });
txBuilder.sign({ key: data.privateKey.prv2 });
const tx = await txBuilder.build();
const fullSignedHex = tx.toBroadcastFormat();
fullSignedHex.should.be.a.String();
fullSignedHex.length.should.be.greaterThan(0);
});

it('Should produce different signed tx in recovery mode vs regular mode', async () => {
// Build and sign in regular mode (user key prv1 + bitgo key prv2)
const regularTxBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
regularTxBuilder.recoverMode(false);
regularTxBuilder.sign({ key: data.privateKey.prv1 });
regularTxBuilder.sign({ key: data.privateKey.prv2 });
const regularTx = await regularTxBuilder.build();
const regularHex = regularTx.toBroadcastFormat();

// Build and sign in recovery mode (backup key prv3 + bitgo key prv2)
const recoveryTxBuilder = data.newTxBuilder();
// @ts-expect-error - method exists on flrp TransactionBuilder
recoveryTxBuilder.recoverMode(true);
recoveryTxBuilder.sign({ key: data.privateKey.prv3 });
recoveryTxBuilder.sign({ key: data.privateKey.prv2 });
const recoveryTx = await recoveryTxBuilder.build();
const recoveryHex = recoveryTx.toBroadcastFormat();

// Both should be valid hex strings
regularHex.should.be.a.String();
recoveryHex.should.be.a.String();

// The signed transactions should be different because different keys are used
regularHex.should.not.equal(recoveryHex);

// Signatures should also be different
const regularSignatures = regularTx.signature;
const recoverySignatures = recoveryTx.signature;
regularSignatures.should.not.eql(recoverySignatures);
});

it('Should set recoverMode via factory and pass to builder from raw tx', async () => {
// First build an unsigned transaction
const txBuilder = data.newTxBuilder();
const tx = await txBuilder.build();
const unsignedHex = tx.toBroadcastFormat();

// Parse from raw with recovery mode enabled on factory
const factory = data.newTxFactory();
// @ts-expect-error - accessing the method which may not be on base type
if (typeof factory.recoverMode === 'function') {
// @ts-expect-error - calling recoverMode on factory
factory.recoverMode(true);
const recoveredBuilder = factory.from(unsignedHex);
// Cast to any to access protected property for testing
(recoveredBuilder as any).recoverSigner.should.equal(true);
}
});
});
}