Skip to content

Commit 08ebe9c

Browse files
committed
feat(sdk-coin-flrp): add MPC/TSS coin-level methods and transaction verification tests
Ticket: CECHO-687
1 parent c98a831 commit 08ebe9c

1 file changed

Lines changed: 274 additions & 1 deletion

File tree

  • modules/sdk-coin-flrp/test/unit

modules/sdk-coin-flrp/test/unit/flrp.ts

Lines changed: 274 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ import { TestBitGo, TestBitGoAPI } from '@bitgo/sdk-test';
33
import { Flrp, TflrP } from '../../src/';
44
import { randomBytes } from 'crypto';
55
import { BitGoAPI } from '@bitgo/sdk-api';
6-
import { SEED_ACCOUNT, ACCOUNT_1, ACCOUNT_2 } from '../resources/account';
6+
import { coins } from '@bitgo/statics';
7+
import { SEED_ACCOUNT, ACCOUNT_1, ACCOUNT_2, ON_CHAIN_TEST_WALLET, CONTEXT } from '../resources/account';
78
import { EXPORT_IN_C } from '../resources/transactionData/exportInC';
89
import { EXPORT_IN_P } from '../resources/transactionData/exportInP';
910
import { IMPORT_IN_P } from '../resources/transactionData/importInP';
1011
import { IMPORT_IN_C } from '../resources/transactionData/importInC';
1112
import { HalfSignedAccountTransaction, TransactionType, MPCAlgorithm } from '@bitgo/sdk-core';
13+
import { secp256k1 } from '@flarenetwork/flarejs';
14+
import { FlrpContext } from '@bitgo/public-types';
1215
import assert from 'assert';
1316

1417
describe('Flrp test cases', function () {
@@ -687,4 +690,274 @@ describe('Flrp test cases', function () {
687690
});
688691
});
689692
});
693+
694+
describe('MPC/TSS Coin-Level Methods', () => {
695+
const coinConfig = coins.get('tflrp');
696+
const factory = new FlrpLib.TransactionBuilderFactory(coinConfig);
697+
698+
// Helper: build unsigned MPC ExportInC tx and return its hex
699+
async function buildUnsignedExportInC(): Promise<string> {
700+
const txBuilder = factory
701+
.getExportInCBuilder()
702+
.fromPubKey(EXPORT_IN_C.cHexAddress)
703+
.nonce(EXPORT_IN_C.nonce)
704+
.amount(EXPORT_IN_C.amount)
705+
.threshold(1)
706+
.locktime(0)
707+
.to(ON_CHAIN_TEST_WALLET.user.pChainAddress)
708+
.fee(EXPORT_IN_C.fee)
709+
.context(CONTEXT as FlrpContext);
710+
711+
const tx = await txBuilder.build();
712+
return tx.toBroadcastFormat();
713+
}
714+
715+
// Helper: build unsigned MPC ImportInP tx and return its hex
716+
async function buildUnsignedImportInP(): Promise<string> {
717+
const mpcUtxo = {
718+
outputID: 7,
719+
amount: '50000000',
720+
txid: 'aLwVQequmbhhjfhL6SvfM6MGWAB8wHwQfJ67eowEbAEUpkueN',
721+
threshold: 1,
722+
addresses: [ON_CHAIN_TEST_WALLET.user.pChainAddress],
723+
outputidx: '0',
724+
locktime: '0',
725+
};
726+
const txBuilder = factory
727+
.getImportInPBuilder()
728+
.threshold(1)
729+
.locktime(0)
730+
.fromPubKey([ON_CHAIN_TEST_WALLET.user.corethAddress])
731+
.to([ON_CHAIN_TEST_WALLET.user.pChainAddress])
732+
.externalChainId(IMPORT_IN_P.sourceChainId)
733+
.decodedUtxos([mpcUtxo])
734+
.context(IMPORT_IN_P.context as FlrpContext)
735+
.feeState(IMPORT_IN_P.feeState as any);
736+
737+
const tx = await txBuilder.build();
738+
return tx.toBroadcastFormat();
739+
}
740+
741+
// Helper: build unsigned MPC ExportInP tx and return its hex
742+
async function buildUnsignedExportInP(): Promise<string> {
743+
const mpcUtxo = {
744+
outputID: 7,
745+
amount: '50000000',
746+
txid: 'bgHnEJ64td8u31aZrGDaWcDqxZ8vDV5qGd7bmSifgvUnUW8v2',
747+
threshold: 1,
748+
addresses: [ON_CHAIN_TEST_WALLET.user.pChainAddress],
749+
outputidx: '0',
750+
locktime: '0',
751+
};
752+
const txBuilder = factory
753+
.getExportInPBuilder()
754+
.threshold(1)
755+
.locktime(0)
756+
.fromPubKey([ON_CHAIN_TEST_WALLET.user.pChainAddress])
757+
.amount('30000000')
758+
.externalChainId(EXPORT_IN_P.sourceChainId)
759+
.decodedUtxos([mpcUtxo])
760+
.context(EXPORT_IN_P.context as FlrpContext)
761+
.feeState(EXPORT_IN_P.feeState as any);
762+
763+
const tx = await txBuilder.build();
764+
return tx.toBroadcastFormat();
765+
}
766+
767+
// Helper: build unsigned MPC ImportInC tx and return its hex
768+
async function buildUnsignedImportInC(): Promise<string> {
769+
const mpcUtxo = {
770+
outputID: 7,
771+
amount: '30000000',
772+
txid: 'nSBwNcgfLbk5S425b1qaYaqTTCiMCV75KU4Fbnq8SPUUqLq2',
773+
threshold: 1,
774+
addresses: [ON_CHAIN_TEST_WALLET.user.pChainAddress],
775+
outputidx: '1',
776+
locktime: '0',
777+
};
778+
const txBuilder = factory
779+
.getImportInCBuilder()
780+
.threshold(1)
781+
.locktime(0)
782+
.fromPubKey([ON_CHAIN_TEST_WALLET.user.pChainAddress])
783+
.to('0x96993BAEb6AaE2e06BF95F144e2775D4f8efbD35')
784+
.fee('1000000')
785+
.decodedUtxos([mpcUtxo])
786+
.context(IMPORT_IN_C.context as FlrpContext);
787+
788+
const tx = await txBuilder.build();
789+
return tx.toBroadcastFormat();
790+
}
791+
792+
describe('getSignablePayload', () => {
793+
it('should return signable payload for ExportInC', async () => {
794+
const txHex = await buildUnsignedExportInC();
795+
const payload = await basecoin.getSignablePayload(txHex);
796+
797+
payload.should.be.instanceOf(Buffer);
798+
payload.length.should.be.greaterThan(0);
799+
});
800+
801+
it('should return signable payload for ImportInP', async () => {
802+
const txHex = await buildUnsignedImportInP();
803+
const payload = await basecoin.getSignablePayload(txHex);
804+
805+
payload.should.be.instanceOf(Buffer);
806+
payload.length.should.be.greaterThan(0);
807+
});
808+
809+
it('should return signable payload for ExportInP', async () => {
810+
const txHex = await buildUnsignedExportInP();
811+
const payload = await basecoin.getSignablePayload(txHex);
812+
813+
payload.should.be.instanceOf(Buffer);
814+
payload.length.should.be.greaterThan(0);
815+
});
816+
817+
it('should return signable payload for ImportInC', async () => {
818+
const txHex = await buildUnsignedImportInC();
819+
const payload = await basecoin.getSignablePayload(txHex);
820+
821+
payload.should.be.instanceOf(Buffer);
822+
payload.length.should.be.greaterThan(0);
823+
});
824+
});
825+
826+
describe('addSignatureToTransaction', () => {
827+
it('should complete getSignablePayload → sign → addSignatureToTransaction round-trip for ExportInC', async () => {
828+
const txHex = await buildUnsignedExportInC();
829+
830+
// Get signable payload (what MPC ceremony would receive)
831+
const payload = await basecoin.getSignablePayload(txHex);
832+
833+
// Simulate MPC: sign externally
834+
const signature = await secp256k1.sign(payload, Buffer.from(ON_CHAIN_TEST_WALLET.user.privateKey, 'hex'));
835+
836+
// Inject signature
837+
const signedHex = await basecoin.addSignatureToTransaction(txHex, Buffer.from(signature));
838+
signedHex.should.not.equal(txHex);
839+
840+
// Verify signed tx can be parsed
841+
const txBuilder = factory.from(signedHex);
842+
const tx = await txBuilder.build();
843+
tx.signature.length.should.equal(1);
844+
tx.toJson().type.should.equal(TransactionType.Export);
845+
});
846+
847+
it('should complete round-trip for ImportInP', async () => {
848+
const txHex = await buildUnsignedImportInP();
849+
const payload = await basecoin.getSignablePayload(txHex);
850+
const signature = await secp256k1.sign(payload, Buffer.from(ON_CHAIN_TEST_WALLET.user.privateKey, 'hex'));
851+
const signedHex = await basecoin.addSignatureToTransaction(txHex, Buffer.from(signature));
852+
853+
signedHex.should.not.equal(txHex);
854+
const tx = await factory.from(signedHex).build();
855+
tx.signature.length.should.equal(1);
856+
tx.toJson().type.should.equal(TransactionType.Import);
857+
});
858+
859+
it('should complete round-trip for ExportInP', async () => {
860+
const txHex = await buildUnsignedExportInP();
861+
const payload = await basecoin.getSignablePayload(txHex);
862+
const signature = await secp256k1.sign(payload, Buffer.from(ON_CHAIN_TEST_WALLET.user.privateKey, 'hex'));
863+
const signedHex = await basecoin.addSignatureToTransaction(txHex, Buffer.from(signature));
864+
865+
signedHex.should.not.equal(txHex);
866+
const tx = await factory.from(signedHex).build();
867+
tx.signature.length.should.equal(1);
868+
tx.toJson().type.should.equal(TransactionType.Export);
869+
});
870+
871+
it('should complete round-trip for ImportInC', async () => {
872+
const txHex = await buildUnsignedImportInC();
873+
const payload = await basecoin.getSignablePayload(txHex);
874+
const signature = await secp256k1.sign(payload, Buffer.from(ON_CHAIN_TEST_WALLET.user.privateKey, 'hex'));
875+
const signedHex = await basecoin.addSignatureToTransaction(txHex, Buffer.from(signature));
876+
877+
signedHex.should.not.equal(txHex);
878+
const tx = await factory.from(signedHex).build();
879+
tx.signature.length.should.equal(1);
880+
tx.toJson().type.should.equal(TransactionType.Import);
881+
});
882+
883+
it('should produce valid signed tx via both sign() and addSignatureToTransaction() for ExportInC', async () => {
884+
const privateKey = ON_CHAIN_TEST_WALLET.user.privateKey;
885+
886+
// Path 1: sign() via signTransaction
887+
const signResult = await basecoin.signTransaction({
888+
txPrebuild: { txHex: await buildUnsignedExportInC() },
889+
prv: privateKey,
890+
});
891+
const signedHex1 = (signResult as HalfSignedAccountTransaction).halfSigned!.txHex!;
892+
893+
// Path 2: getSignablePayload → external sign → addSignatureToTransaction
894+
const unsignedHex = await buildUnsignedExportInC();
895+
const payload = await basecoin.getSignablePayload(unsignedHex);
896+
const signature = await secp256k1.sign(payload, Buffer.from(privateKey, 'hex'));
897+
const signedHex2 = await basecoin.addSignatureToTransaction(unsignedHex, Buffer.from(signature));
898+
899+
// Both paths should produce valid signed transactions with 1 signature
900+
const tx1 = await factory.from(signedHex1).build();
901+
const tx2 = await factory.from(signedHex2).build();
902+
tx1.signature.length.should.equal(1);
903+
tx2.signature.length.should.equal(1);
904+
tx1.toJson().type.should.equal(TransactionType.Export);
905+
tx2.toJson().type.should.equal(TransactionType.Export);
906+
});
907+
});
908+
909+
describe('verifyTransaction with MPC params', () => {
910+
it('should verify MPC ExportInC transaction', async () => {
911+
const txHex = await buildUnsignedExportInC();
912+
const txPrebuild = { txHex, txInfo: {} };
913+
const txParams = {
914+
recipients: [{ address: ON_CHAIN_TEST_WALLET.user.pChainAddress, amount: EXPORT_IN_C.amount }],
915+
type: 'Export',
916+
locktime: 0,
917+
};
918+
919+
const isVerified = await basecoin.verifyTransaction({ txParams, txPrebuild });
920+
isVerified.should.equal(true);
921+
});
922+
923+
it('should verify MPC ImportInP transaction', async () => {
924+
const txHex = await buildUnsignedImportInP();
925+
const txPrebuild = { txHex, txInfo: {} };
926+
const txParams = {
927+
recipients: [],
928+
type: 'Import',
929+
locktime: 0,
930+
};
931+
932+
const isVerified = await basecoin.verifyTransaction({ txParams, txPrebuild });
933+
isVerified.should.equal(true);
934+
});
935+
936+
it('should verify MPC ExportInP transaction', async () => {
937+
const txHex = await buildUnsignedExportInP();
938+
const txPrebuild = { txHex, txInfo: {} };
939+
const txParams = {
940+
recipients: [{ address: ON_CHAIN_TEST_WALLET.user.pChainAddress, amount: '30000000' }],
941+
type: 'Export',
942+
locktime: 0,
943+
};
944+
945+
const isVerified = await basecoin.verifyTransaction({ txParams, txPrebuild });
946+
isVerified.should.equal(true);
947+
});
948+
949+
it('should verify MPC ImportInC transaction', async () => {
950+
const txHex = await buildUnsignedImportInC();
951+
const txPrebuild = { txHex, txInfo: {} };
952+
const txParams = {
953+
recipients: [{ address: '0x96993BAEb6AaE2e06BF95F144e2775D4f8efbD35', amount: '1' }],
954+
type: 'ImportToC',
955+
locktime: 0,
956+
};
957+
958+
const isVerified = await basecoin.verifyTransaction({ txParams, txPrebuild });
959+
isVerified.should.equal(true);
960+
});
961+
});
962+
});
690963
});

0 commit comments

Comments
 (0)