Skip to content
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
8 changes: 8 additions & 0 deletions modules/sdk-coin-flrp/src/lib/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -261,6 +261,14 @@ export class Transaction extends BaseTransaction {
this._flareTransaction = tx as UnsignedTx;
}

/**
* Get the underlying Flare transaction
* @returns The Flare transaction object
*/
getFlareTransaction(): Tx {
return this._flareTransaction;
}

setTransactionType(transactionType: TransactionType): void {
if (![TransactionType.AddPermissionlessValidator].includes(transactionType)) {
throw new Error(`Transaction type ${transactionType} is not supported`);
Expand Down
41 changes: 40 additions & 1 deletion modules/sdk-coin-flrp/src/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
import { Buffer } from 'buffer';
import { createHash } from 'crypto';
import { ecc } from '@bitgo/secp256k1';
import { ADDRESS_SEPARATOR, Output } from './iface';
import { ADDRESS_SEPARATOR, Output, Tx } from './iface';
import bs58 from 'bs58';
import { bech32 } from 'bech32';

Expand Down Expand Up @@ -225,7 +225,7 @@
/**
* Maps outputs to entry format
*/
mapOutputToEntry(network: FlareNetwork): (Output) => Entry {

Check warning

Code scanning / CodeQL

Ineffective parameter type Warning

The parameter 'Output' has type 'any', but its name coincides with the
imported type Output
.
return (output: Output) => {
if (this.isTransferableOutput(output)) {
const outputAmount = output.amount();
Expand Down Expand Up @@ -390,6 +390,45 @@
throw new Error(`Failed to recover signature: ${error.message}`);
}
}

/**
* Check if tx is for the blockchainId
*
* @param {Tx} tx
* @param {string} blockchainId - blockchain ID in hex format
* @returns true if tx is for blockchainId
*/
isTransactionOf(tx: Tx, blockchainId: string): boolean {
// Note: getBlockchainId() and BlockchainId.value() return CB58-encoded strings,
// but we need hex format, so we use toBytes() and convert to hex
const extractBlockchainId = (txObj: any): string | null => {
if (typeof txObj.getTx === 'function') {
const innerTx = txObj.getTx();
if (innerTx.baseTx?.BlockchainId?.toBytes) {
return Buffer.from(innerTx.baseTx.BlockchainId.toBytes()).toString('hex');
}
if (innerTx.blockchainId?.toBytes) {
return Buffer.from(innerTx.blockchainId.toBytes()).toString('hex');
}
}

if (txObj.tx?.baseTx?.BlockchainId?.toBytes) {
return Buffer.from(txObj.tx.baseTx.BlockchainId.toBytes()).toString('hex');
}

if (txObj.baseTx?.BlockchainId?.toBytes) {
return Buffer.from(txObj.baseTx.BlockchainId.toBytes()).toString('hex');
}
if (txObj.blockchainId?.toBytes) {
return Buffer.from(txObj.blockchainId.toBytes()).toString('hex');
}

return null;
};

const txBlockchainId = extractBlockchainId(tx);
return txBlockchainId === blockchainId;
}
}

const utils = new Utils();
Expand Down
163 changes: 163 additions & 0 deletions modules/sdk-coin-flrp/test/unit/lib/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ import {
import { ecc } from '@bitgo/secp256k1';
import { EXPORT_IN_C } from '../../resources/transactionData/exportInC';
import { IMPORT_IN_P } from '../../resources/transactionData/importInP';
import { EXPORT_IN_P } from '../../resources/transactionData/exportInP';
import { IMPORT_IN_C } from '../../resources/transactionData/importInC';
import { TransactionBuilderFactory, Transaction } from '../../../src/lib';

describe('Utils', function () {
let utils: Utils;
Expand Down Expand Up @@ -539,4 +542,164 @@ describe('Utils', function () {
assert.throws(() => utils.recoverySignature(network, message, signature), /Failed to recover signature/);
});
});

describe('isTransactionOf', function () {
const factory = new TransactionBuilderFactory(coins.get('tflrp'));
const utilsInstance = new Utils();
const testnetNetwork = coins.get('tflrp').network as FlareNetwork;
const pChainBlockchainIdHex = Buffer.from(utilsInstance.cb58Decode(testnetNetwork.blockchainID)).toString('hex');
const cChainBlockchainIdHex = Buffer.from(utilsInstance.cb58Decode(testnetNetwork.cChainBlockchainID)).toString(
'hex'
);

it('should return true for Import in P transaction with matching P-chain blockchain ID', async function () {
const txBuilder = factory
.getImportInPBuilder()
.threshold(IMPORT_IN_P.threshold)
.locktime(IMPORT_IN_P.locktime)
.fromPubKey(IMPORT_IN_P.pAddresses)
.externalChainId(IMPORT_IN_P.sourceChainId)
.fee(IMPORT_IN_P.fee)
.utxos(IMPORT_IN_P.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), true);
});

it('should return false for Import in P transaction with non-matching C-chain blockchain ID', async function () {
const txBuilder = factory
.getImportInPBuilder()
.threshold(IMPORT_IN_P.threshold)
.locktime(IMPORT_IN_P.locktime)
.fromPubKey(IMPORT_IN_P.pAddresses)
.externalChainId(IMPORT_IN_P.sourceChainId)
.fee(IMPORT_IN_P.fee)
.utxos(IMPORT_IN_P.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), false);
});

it('should return true for Export in P transaction with matching P-chain blockchain ID', async function () {
const txBuilder = factory
.getExportInPBuilder()
.threshold(EXPORT_IN_P.threshold)
.locktime(EXPORT_IN_P.locktime)
.fromPubKey(EXPORT_IN_P.pAddresses)
.externalChainId(EXPORT_IN_P.sourceChainId)
.fee(EXPORT_IN_P.fee)
.amount(EXPORT_IN_P.amount)
.utxos(EXPORT_IN_P.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), true);
});

it('should return false for Export in P transaction with non-matching C-chain blockchain ID', async function () {
const txBuilder = factory
.getExportInPBuilder()
.threshold(EXPORT_IN_P.threshold)
.locktime(EXPORT_IN_P.locktime)
.fromPubKey(EXPORT_IN_P.pAddresses)
.externalChainId(EXPORT_IN_P.sourceChainId)
.fee(EXPORT_IN_P.fee)
.amount(EXPORT_IN_P.amount)
.utxos(EXPORT_IN_P.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), false);
});

it('should return true for Import in C transaction with matching C-chain blockchain ID', async function () {
const txBuilder = factory
.getImportInCBuilder()
.threshold(IMPORT_IN_C.threshold)
.locktime(IMPORT_IN_C.locktime)
.fromPubKey(IMPORT_IN_C.pAddresses)
.externalChainId(IMPORT_IN_C.sourceChainId)
.feeRate(IMPORT_IN_C.fee)
.to(IMPORT_IN_C.to)
.utxos(IMPORT_IN_C.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), true);
});

it('should return false for Import in C transaction with non-matching P-chain blockchain ID', async function () {
const txBuilder = factory
.getImportInCBuilder()
.threshold(IMPORT_IN_C.threshold)
.locktime(IMPORT_IN_C.locktime)
.fromPubKey(IMPORT_IN_C.pAddresses)
.externalChainId(IMPORT_IN_C.sourceChainId)
.feeRate(IMPORT_IN_C.fee)
.to(IMPORT_IN_C.to)
.utxos(IMPORT_IN_C.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), false);
});

it('should return true for Export in C transaction with matching C-chain blockchain ID', async function () {
const txBuilder = factory
.getExportInCBuilder()
.fromPubKey(EXPORT_IN_C.cHexAddress)
.nonce(EXPORT_IN_C.nonce)
.amount(EXPORT_IN_C.amount)
.threshold(EXPORT_IN_C.threshold)
.locktime(EXPORT_IN_C.locktime)
.to(EXPORT_IN_C.pAddresses)
.feeRate(EXPORT_IN_C.fee);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, cChainBlockchainIdHex), true);
});

it('should return false for Export in C transaction with non-matching P-chain blockchain ID', async function () {
const txBuilder = factory
.getExportInCBuilder()
.fromPubKey(EXPORT_IN_C.cHexAddress)
.nonce(EXPORT_IN_C.nonce)
.amount(EXPORT_IN_C.amount)
.threshold(EXPORT_IN_C.threshold)
.locktime(EXPORT_IN_C.locktime)
.to(EXPORT_IN_C.pAddresses)
.feeRate(EXPORT_IN_C.fee);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, pChainBlockchainIdHex), false);
});

it('should return false for invalid blockchain ID', async function () {
const txBuilder = factory
.getImportInPBuilder()
.threshold(IMPORT_IN_P.threshold)
.locktime(IMPORT_IN_P.locktime)
.fromPubKey(IMPORT_IN_P.pAddresses)
.externalChainId(IMPORT_IN_P.sourceChainId)
.fee(IMPORT_IN_P.fee)
.utxos(IMPORT_IN_P.outputs);

const tx = (await txBuilder.build()) as Transaction;
const flareTransaction = tx.getFlareTransaction();

assert.strictEqual(utilsInstance.isTransactionOf(flareTransaction, 'invalidblockchainid'), false);
});
});
});