Skip to content

Commit 326b0dc

Browse files
authored
Merge pull request #7725 from BitGo/WIN-8240
feat(sdk-coin-flrp): add transaction validation utility
2 parents b0c2a65 + 8d09223 commit 326b0dc

File tree

3 files changed

+211
-1
lines changed

3 files changed

+211
-1
lines changed

modules/sdk-coin-flrp/src/lib/transaction.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -261,6 +261,14 @@ export class Transaction extends BaseTransaction {
261261
this._flareTransaction = tx as UnsignedTx;
262262
}
263263

264+
/**
265+
* Get the underlying Flare transaction
266+
* @returns The Flare transaction object
267+
*/
268+
getFlareTransaction(): Tx {
269+
return this._flareTransaction;
270+
}
271+
264272
setTransactionType(transactionType: TransactionType): void {
265273
if (![TransactionType.AddPermissionlessValidator].includes(transactionType)) {
266274
throw new Error(`Transaction type ${transactionType} is not supported`);

modules/sdk-coin-flrp/src/lib/utils.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { FlareNetwork } from '@bitgo/statics';
1111
import { Buffer } from 'buffer';
1212
import { createHash } from 'crypto';
1313
import { ecc } from '@bitgo/secp256k1';
14-
import { ADDRESS_SEPARATOR, Output } from './iface';
14+
import { ADDRESS_SEPARATOR, Output, Tx } from './iface';
1515
import bs58 from 'bs58';
1616
import { bech32 } from 'bech32';
1717

@@ -390,6 +390,45 @@ export class Utils implements BaseUtils {
390390
throw new Error(`Failed to recover signature: ${error.message}`);
391391
}
392392
}
393+
394+
/**
395+
* Check if tx is for the blockchainId
396+
*
397+
* @param {Tx} tx
398+
* @param {string} blockchainId - blockchain ID in hex format
399+
* @returns true if tx is for blockchainId
400+
*/
401+
isTransactionOf(tx: Tx, blockchainId: string): boolean {
402+
// Note: getBlockchainId() and BlockchainId.value() return CB58-encoded strings,
403+
// but we need hex format, so we use toBytes() and convert to hex
404+
const extractBlockchainId = (txObj: any): string | null => {
405+
if (typeof txObj.getTx === 'function') {
406+
const innerTx = txObj.getTx();
407+
if (innerTx.baseTx?.BlockchainId?.toBytes) {
408+
return Buffer.from(innerTx.baseTx.BlockchainId.toBytes()).toString('hex');
409+
}
410+
if (innerTx.blockchainId?.toBytes) {
411+
return Buffer.from(innerTx.blockchainId.toBytes()).toString('hex');
412+
}
413+
}
414+
415+
if (txObj.tx?.baseTx?.BlockchainId?.toBytes) {
416+
return Buffer.from(txObj.tx.baseTx.BlockchainId.toBytes()).toString('hex');
417+
}
418+
419+
if (txObj.baseTx?.BlockchainId?.toBytes) {
420+
return Buffer.from(txObj.baseTx.BlockchainId.toBytes()).toString('hex');
421+
}
422+
if (txObj.blockchainId?.toBytes) {
423+
return Buffer.from(txObj.blockchainId.toBytes()).toString('hex');
424+
}
425+
426+
return null;
427+
};
428+
429+
const txBlockchainId = extractBlockchainId(tx);
430+
return txBlockchainId === blockchainId;
431+
}
393432
}
394433

395434
const utils = new Utils();

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

Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ import {
1212
import { ecc } from '@bitgo/secp256k1';
1313
import { EXPORT_IN_C } from '../../resources/transactionData/exportInC';
1414
import { IMPORT_IN_P } from '../../resources/transactionData/importInP';
15+
import { EXPORT_IN_P } from '../../resources/transactionData/exportInP';
16+
import { IMPORT_IN_C } from '../../resources/transactionData/importInC';
17+
import { TransactionBuilderFactory, Transaction } from '../../../src/lib';
1518

1619
describe('Utils', function () {
1720
let utils: Utils;
@@ -539,4 +542,164 @@ describe('Utils', function () {
539542
assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/);
540543
});
541544
});
545+
546+
describe('isTransactionOf', function () {
547+
const factory = new TransactionBuilderFactory(coins.get('tflrp'));
548+
const utilsInstance = new Utils();
549+
const testnetNetwork = coins.get('tflrp').network as FlareNetwork;
550+
const pChainBlockchainIdHex = Buffer.from(utilsInstance.cb58Decode(testnetNetwork.blockchainID)).toString('hex');
551+
const cChainBlockchainIdHex = Buffer.from(utilsInstance.cb58Decode(testnetNetwork.cChainBlockchainID)).toString(
552+
'hex'
553+
);
554+
555+
it('should return true for Import in P transaction with matching P-chain blockchain ID', async function () {
556+
const txBuilder = factory
557+
.getImportInPBuilder()
558+
.threshold(IMPORT_IN_P.threshold)
559+
.locktime(IMPORT_IN_P.locktime)
560+
.fromPubKey(IMPORT_IN_P.pAddresses)
561+
.externalChainId(IMPORT_IN_P.sourceChainId)
562+
.fee(IMPORT_IN_P.fee)
563+
.utxos(IMPORT_IN_P.outputs);
564+
565+
const tx = (await txBuilder.build()) as Transaction;
566+
const flareTransaction = tx.getFlareTransaction();
567+
568+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), true);
569+
});
570+
571+
it('should return false for Import in P transaction with non-matching C-chain blockchain ID', async function () {
572+
const txBuilder = factory
573+
.getImportInPBuilder()
574+
.threshold(IMPORT_IN_P.threshold)
575+
.locktime(IMPORT_IN_P.locktime)
576+
.fromPubKey(IMPORT_IN_P.pAddresses)
577+
.externalChainId(IMPORT_IN_P.sourceChainId)
578+
.fee(IMPORT_IN_P.fee)
579+
.utxos(IMPORT_IN_P.outputs);
580+
581+
const tx = (await txBuilder.build()) as Transaction;
582+
const flareTransaction = tx.getFlareTransaction();
583+
584+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), false);
585+
});
586+
587+
it('should return true for Export in P transaction with matching P-chain blockchain ID', async function () {
588+
const txBuilder = factory
589+
.getExportInPBuilder()
590+
.threshold(EXPORT_IN_P.threshold)
591+
.locktime(EXPORT_IN_P.locktime)
592+
.fromPubKey(EXPORT_IN_P.pAddresses)
593+
.externalChainId(EXPORT_IN_P.sourceChainId)
594+
.fee(EXPORT_IN_P.fee)
595+
.amount(EXPORT_IN_P.amount)
596+
.utxos(EXPORT_IN_P.outputs);
597+
598+
const tx = (await txBuilder.build()) as Transaction;
599+
const flareTransaction = tx.getFlareTransaction();
600+
601+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), true);
602+
});
603+
604+
it('should return false for Export in P transaction with non-matching C-chain blockchain ID', async function () {
605+
const txBuilder = factory
606+
.getExportInPBuilder()
607+
.threshold(EXPORT_IN_P.threshold)
608+
.locktime(EXPORT_IN_P.locktime)
609+
.fromPubKey(EXPORT_IN_P.pAddresses)
610+
.externalChainId(EXPORT_IN_P.sourceChainId)
611+
.fee(EXPORT_IN_P.fee)
612+
.amount(EXPORT_IN_P.amount)
613+
.utxos(EXPORT_IN_P.outputs);
614+
615+
const tx = (await txBuilder.build()) as Transaction;
616+
const flareTransaction = tx.getFlareTransaction();
617+
618+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), false);
619+
});
620+
621+
it('should return true for Import in C transaction with matching C-chain blockchain ID', async function () {
622+
const txBuilder = factory
623+
.getImportInCBuilder()
624+
.threshold(IMPORT_IN_C.threshold)
625+
.locktime(IMPORT_IN_C.locktime)
626+
.fromPubKey(IMPORT_IN_C.pAddresses)
627+
.externalChainId(IMPORT_IN_C.sourceChainId)
628+
.feeRate(IMPORT_IN_C.fee)
629+
.to(IMPORT_IN_C.to)
630+
.utxos(IMPORT_IN_C.outputs);
631+
632+
const tx = (await txBuilder.build()) as Transaction;
633+
const flareTransaction = tx.getFlareTransaction();
634+
635+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), true);
636+
});
637+
638+
it('should return false for Import in C transaction with non-matching P-chain blockchain ID', async function () {
639+
const txBuilder = factory
640+
.getImportInCBuilder()
641+
.threshold(IMPORT_IN_C.threshold)
642+
.locktime(IMPORT_IN_C.locktime)
643+
.fromPubKey(IMPORT_IN_C.pAddresses)
644+
.externalChainId(IMPORT_IN_C.sourceChainId)
645+
.feeRate(IMPORT_IN_C.fee)
646+
.to(IMPORT_IN_C.to)
647+
.utxos(IMPORT_IN_C.outputs);
648+
649+
const tx = (await txBuilder.build()) as Transaction;
650+
const flareTransaction = tx.getFlareTransaction();
651+
652+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), false);
653+
});
654+
655+
it('should return true for Export in C transaction with matching C-chain blockchain ID', async function () {
656+
const txBuilder = factory
657+
.getExportInCBuilder()
658+
.fromPubKey(EXPORT_IN_C.cHexAddress)
659+
.nonce(EXPORT_IN_C.nonce)
660+
.amount(EXPORT_IN_C.amount)
661+
.threshold(EXPORT_IN_C.threshold)
662+
.locktime(EXPORT_IN_C.locktime)
663+
.to(EXPORT_IN_C.pAddresses)
664+
.feeRate(EXPORT_IN_C.fee);
665+
666+
const tx = (await txBuilder.build()) as Transaction;
667+
const flareTransaction = tx.getFlareTransaction();
668+
669+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), true);
670+
});
671+
672+
it('should return false for Export in C transaction with non-matching P-chain blockchain ID', async function () {
673+
const txBuilder = factory
674+
.getExportInCBuilder()
675+
.fromPubKey(EXPORT_IN_C.cHexAddress)
676+
.nonce(EXPORT_IN_C.nonce)
677+
.amount(EXPORT_IN_C.amount)
678+
.threshold(EXPORT_IN_C.threshold)
679+
.locktime(EXPORT_IN_C.locktime)
680+
.to(EXPORT_IN_C.pAddresses)
681+
.feeRate(EXPORT_IN_C.fee);
682+
683+
const tx = (await txBuilder.build()) as Transaction;
684+
const flareTransaction = tx.getFlareTransaction();
685+
686+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), false);
687+
});
688+
689+
it('should return false for invalid blockchain ID', async function () {
690+
const txBuilder = factory
691+
.getImportInPBuilder()
692+
.threshold(IMPORT_IN_P.threshold)
693+
.locktime(IMPORT_IN_P.locktime)
694+
.fromPubKey(IMPORT_IN_P.pAddresses)
695+
.externalChainId(IMPORT_IN_P.sourceChainId)
696+
.fee(IMPORT_IN_P.fee)
697+
.utxos(IMPORT_IN_P.outputs);
698+
699+
const tx = (await txBuilder.build()) as Transaction;
700+
const flareTransaction = tx.getFlareTransaction();
701+
702+
assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, 'invalidblockchainid'), false);
703+
});
704+
});
542705
});

0 commit comments

Comments
 (0)