From 2f117267d1b9d16895275a4b864c61819ece9155 Mon Sep 17 00:00:00 2001 From: Kashif Jamil Date: Sun, 30 Nov 2025 01:11:43 +0530 Subject: [PATCH] feat(sdk-coin-flrp): enhance ImportInPTxBuilder for P-chain transactions Ticket: WIN-8043 --- .../src/lib/ImportInPTxBuilder.ts | 222 ++++++++++++++---- .../src/lib/atomicTransactionBuilder.ts | 35 ++- modules/sdk-coin-flrp/src/lib/transaction.ts | 20 +- .../src/lib/transactionBuilderFactory.ts | 42 ++-- modules/sdk-coin-flrp/src/lib/utils.ts | 62 +++++ .../resources/transactionData/importInP.ts | 63 +++++ .../test/unit/lib/importInPTxBuilder.ts | 91 +++++++ .../test/unit/lib/signFlowTestSuit.ts | 110 +++++++++ 8 files changed, 576 insertions(+), 69 deletions(-) create mode 100644 modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts create mode 100644 modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts create mode 100644 modules/sdk-coin-flrp/test/unit/lib/signFlowTestSuit.ts diff --git a/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts index 5122fbcd92..96aa54c3f9 100644 --- a/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/ImportInPTxBuilder.ts @@ -2,14 +2,21 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { BuildTransactionError, NotSupported, TransactionType } from '@bitgo/sdk-core'; import { AtomicTransactionBuilder } from './atomicTransactionBuilder'; import { - evmSerial, + pvmSerial, + avaxSerial, UnsignedTx, Int, Id, TransferableInput, + TransferableOutput, + TransferOutput, + TransferInput, + OutputOwners, utils as FlareUtils, Address, BigIntPr, + Credential, + Bytes, } from '@flarenetwork/flarejs'; import utils from './utils'; import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } from './iface'; @@ -17,53 +24,105 @@ import { DecodedUtxoObj, FlareTransactionType, SECP256K1_Transfer_Output, Tx } f export class ImportInPTxBuilder extends AtomicTransactionBuilder { constructor(_coinConfig: Readonly) { super(_coinConfig); - // external chain id is P - this._externalChainId = utils.cb58Decode(this.transaction._network.blockchainID); - // chain id is C - this.transaction._blockchainID = Buffer.from( - utils.cb58Decode(this.transaction._network.cChainBlockchainID) - ).toString('hex'); + // For Import INTO P-chain: + // - external chain (source) is C-chain + // - blockchain ID (destination) is P-chain + this._externalChainId = utils.cb58Decode(this.transaction._network.cChainBlockchainID); + // P-chain blockchain ID (from network config - typically all zeros for primary network) + this.transaction._blockchainID = Buffer.from(utils.cb58Decode(this.transaction._network.blockchainID)).toString( + 'hex' + ); } protected get transactionType(): TransactionType { return TransactionType.Import; } - initBuilder(tx: Tx): this { - const baseTx = tx as evmSerial.ImportTx; - if (!this.verifyTxType(baseTx._type)) { + initBuilder(tx: Tx, rawBytes?: Buffer): this { + const importTx = tx as pvmSerial.ImportTx; + + if (!this.verifyTxType(importTx._type)) { throw new NotSupported('Transaction cannot be parsed or has an unsupported transaction type'); } // The regular change output is the tx output in Import tx. - // createInputOutput results in a single item array. // It's expected to have only one output with the addresses of the sender. - const outputs = baseTx.Outs; + const outputs = importTx.baseTx.outputs; if (outputs.length !== 1) { throw new BuildTransactionError('Transaction can have one external output'); } const output = outputs[0]; const assetId = output.assetId.toBytes(); - if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId)) !== 0) { + if (Buffer.compare(assetId, Buffer.from(this.transaction._assetId, 'hex')) !== 0) { throw new Error('The Asset ID of the output does not match the transaction'); } - // Set locktime to 0 since it's not used in EVM outputs - this.transaction._locktime = BigInt(0); + const transferOutput = output.output as TransferOutput; + const outputOwners = transferOutput.outputOwners; + + // Set locktime from output + this.transaction._locktime = outputOwners.locktime.value(); - // Set threshold to 1 since EVM outputs only have one address - this.transaction._threshold = 1; + // Set threshold from output + this.transaction._threshold = outputOwners.threshold.value(); - // Convert output address to buffer and set as fromAddress - this.transaction._fromAddresses = [Buffer.from(output.address.toBytes())]; + // Convert output addresses to buffers and set as fromAddresses + this.transaction._fromAddresses = outputOwners.addrs.map((addr) => Buffer.from(addr.toBytes())); // Set external chain ID from the source chain - this._externalChainId = Buffer.from(baseTx.sourceChain.toString()); + this._externalChainId = Buffer.from(importTx.sourceChain.toBytes()); // Recover UTXOs from imported inputs - this.transaction._utxos = this.recoverUtxos(baseTx.importedInputs); + this.transaction._utxos = this.recoverUtxos(importTx.ins); + + // Calculate and set fee from input/output difference + const totalInputAmount = importTx.ins.reduce((sum, input) => sum + input.amount(), BigInt(0)); + const outputAmount = transferOutput.amount(); + const fee = totalInputAmount - outputAmount; + this.transaction._fee.fee = fee.toString(); + + // Check if raw bytes contain credentials + // For PVM transactions, credentials start after the unsigned tx bytes + let hasCredentials = false; + let credentials: Credential[] = []; + + if (rawBytes) { + // Try standard extraction first + const result = utils.extractCredentialsFromRawBytes(rawBytes, importTx, 'PVM'); + hasCredentials = result.hasCredentials; + credentials = result.credentials; + + // If extraction failed but raw bytes are longer, try parsing credentials at known offset + // For ImportTx, the unsigned tx is typically 302 bytes + if ((!hasCredentials || credentials.length === 0) && rawBytes.length > 350) { + hasCredentials = true; + // Try to extract credentials at the standard position (302 bytes) + const credResult = utils.parseCredentialsAtOffset(rawBytes, 302); + if (credResult.length > 0) { + credentials = credResult; + } + } + } + + // If there are credentials in raw bytes, store the original bytes to preserve exact format + if (rawBytes && hasCredentials) { + this.transaction._rawSignedBytes = rawBytes; + } + // Create proper UnsignedTx wrapper with credentials + const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b)); + const addressMaps = sortedAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]])); + + // Create credentials if none exist + const txCredentials = + credentials.length > 0 + ? credentials + : [new Credential(sortedAddresses.slice(0, this.transaction._threshold).map(() => utils.createNewSig('')))]; + + const unsignedTx = new UnsignedTx(importTx, [], new FlareUtils.AddressMaps(addressMaps), txCredentials); + + this.transaction.setTransaction(unsignedTx); return this; } @@ -76,39 +135,56 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { } /** - * Build the import transaction + * Build the import transaction for P-chain * @protected */ protected buildFlareTransaction(): void { // if tx has credentials, tx shouldn't change if (this.transaction.hasCredentials) return; - const { inputs, credentials } = this.createInputOutput(BigInt(this.transaction.fee.fee)); + const { inputs, credentials, totalAmount } = this.createImportInputs(); - // Convert TransferableInput to evmSerial.Output - const evmOutputs = inputs.map((input) => { - return new evmSerial.Output( - new Address(this.transaction._fromAddresses[0]), - new BigIntPr(input.input.amount()), - new Id(input.assetId.toBytes()) - ); - }); + // Calculate fee from transaction fee settings + const fee = BigInt(this.transaction.fee.fee); + const outputAmount = totalAmount - fee; + + // Create the output for P-chain (TransferableOutput with TransferOutput) + const assetIdBytes = new Uint8Array(Buffer.from(this.transaction._assetId, 'hex')); + + // Create OutputOwners with the P-chain addresses (sorted by byte value as per AVAX protocol) + const sortedAddresses = [...this.transaction._fromAddresses].sort((a, b) => Buffer.compare(a, b)); + const outputOwners = new OutputOwners( + new BigIntPr(this.transaction._locktime), + new Int(this.transaction._threshold), + sortedAddresses.map((addr) => new Address(addr)) + ); + + const transferOutput = new TransferOutput(new BigIntPr(outputAmount), outputOwners); + const output = new TransferableOutput(new Id(assetIdBytes), transferOutput); - // Create the import transaction - const importTx = new evmSerial.ImportTx( + // Create the BaseTx for the P-chain import transaction + const baseTx = new avaxSerial.BaseTx( new Int(this.transaction._networkID), - Id.fromString(this.transaction._blockchainID.toString()), - Id.fromString(this._externalChainId.toString()), - inputs, - evmOutputs + new Id(Buffer.from(this.transaction._blockchainID, 'hex')), + [output], // outputs + [], // inputs (empty for import - inputs come from importedInputs) + new Bytes(new Uint8Array(0)) // empty memo + ); + + // Create the P-chain import transaction using pvmSerial.ImportTx + const importTx = new pvmSerial.ImportTx( + baseTx, + new Id(this._externalChainId), // sourceChain (C-chain) + inputs // importedInputs (ins) ); - const addressMaps = this.transaction._fromAddresses.map((a) => new FlareUtils.AddressMap([[new Address(a), 0]])); + // Create address maps for signing + const addressMaps = this.transaction._fromAddresses.map((a, i) => new FlareUtils.AddressMap([[new Address(a), i]])); // Create unsigned transaction const unsignedTx = new UnsignedTx( importTx, - [], // Empty UTXOs array, will be filled during processing + [], // Empty UTXOs array new FlareUtils.AddressMaps(addressMaps), credentials ); @@ -116,6 +192,66 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { this.transaction.setTransaction(unsignedTx); } + /** + * Create inputs from UTXOs for P-chain import + * @returns inputs, credentials, and total amount + */ + protected createImportInputs(): { + inputs: TransferableInput[]; + credentials: Credential[]; + totalAmount: bigint; + } { + const sender = this.transaction._fromAddresses.slice(); + if (this.recoverSigner) { + // switch first and last signer + const tmp = sender.pop(); + sender.push(sender[0]); + if (tmp) { + sender[0] = tmp; + } + } + + let totalAmount = BigInt(0); + const inputs: TransferableInput[] = []; + const credentials: Credential[] = []; + + this.transaction._utxos.forEach((utxo: DecodedUtxoObj) => { + const amount = BigInt(utxo.amount); + totalAmount += amount; + + // Create signature indices for threshold + const sigIndices: number[] = []; + for (let i = 0; i < this.transaction._threshold; i++) { + sigIndices.push(i); + } + + // Use fromNative to create TransferableInput + // fromNative expects cb58-encoded strings for txId and assetId + const txIdCb58 = utxo.txid; // Already cb58 encoded + const assetIdCb58 = utils.cb58Encode(Buffer.from(this.transaction._assetId, 'hex')); + + const transferableInput = TransferableInput.fromNative( + txIdCb58, + Number(utxo.outputidx), + assetIdCb58, + amount, + sigIndices + ); + + inputs.push(transferableInput); + + // Create credential with empty signatures for threshold signers + const emptySignatures = sigIndices.map(() => utils.createNewSig('')); + credentials.push(new Credential(emptySignatures)); + }); + + return { + inputs, + credentials, + totalAmount, + }; + } + /** * Recover UTXOs from imported inputs * @param importedInputs Array of transferable inputs @@ -124,12 +260,12 @@ export class ImportInPTxBuilder extends AtomicTransactionBuilder { private recoverUtxos(importedInputs: TransferableInput[]): DecodedUtxoObj[] { return importedInputs.map((input) => { const utxoId = input.utxoID; - const transferInput = input.input; + const transferInput = input.input as TransferInput; const utxo: DecodedUtxoObj = { outputID: SECP256K1_Transfer_Output, - amount: transferInput.amount.toString(), - txid: utils.cb58Encode(Buffer.from(utxoId.ID.toString())), - outputidx: utxoId.outputIdx.toBytes().toString(), + amount: transferInput.amount().toString(), + txid: utils.cb58Encode(Buffer.from(utxoId.txID.toBytes())), + outputidx: utxoId.outputIdx.value().toString(), threshold: this.transaction._threshold, addresses: this.transaction._fromAddresses.map((addr) => utils.addressToString(this.transaction._network.hrp, this.transaction._network.alias, Buffer.from(addr)) diff --git a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts index b516762b10..470206f1eb 100644 --- a/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts +++ b/modules/sdk-coin-flrp/src/lib/atomicTransactionBuilder.ts @@ -2,8 +2,9 @@ import { BaseCoin as CoinConfig } from '@bitgo/statics'; import { TransactionType } from '@bitgo/sdk-core'; import { TransactionBuilder } from './transactionBuilder'; import { Transaction } from './transaction'; -import { TransferableInput, Int, Id, TypeSymbols } from '@flarenetwork/flarejs'; +import { TransferableInput, Int, Id, TypeSymbols, Credential } from '@flarenetwork/flarejs'; import { DecodedUtxoObj } from './iface'; +import utils from './utils'; // Interface for objects that can provide an amount interface Amounter { @@ -34,7 +35,7 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { protected createInputOutput(amount: bigint): { inputs: TransferableInput[]; outputs: TransferableInput[]; - credentials: any[]; + credentials: Credential[]; } { const sender = (this.transaction as Transaction)._fromAddresses.slice(); if (this.recoverSigner) { @@ -49,7 +50,7 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { let totalAmount = BigInt(0); const inputs: TransferableInput[] = []; const outputs: TransferableInput[] = []; - const credentials: any[] = []; + const credentials: Credential[] = []; (this.transaction as Transaction)._utxos.forEach((utxo: DecodedUtxoObj) => { const utxoAmount = BigInt(utxo.amount); @@ -96,8 +97,8 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { inputs.push(transferableInput); // Create empty credential for each input - const emptySignatures = sender.map(() => Buffer.alloc(0)); - credentials.push({ signatures: emptySignatures }); + const emptySignatures = sender.map(() => utils.createNewSig('')); + credentials.push(new Credential(emptySignatures)); }); // Create output if there is change @@ -189,4 +190,28 @@ export abstract class AtomicTransactionBuilder extends TransactionBuilder { setTransactionType(transactionType: TransactionType): void { this.transaction._type = transactionType; } + + /** + * The internal chain is the one set for the coin in coinConfig.network. The external chain is the other chain involved. + * The external chain id is the source on import and the destination on export. + * + * @param {string} chainId - id of the external chain + */ + externalChainId(chainId: string | Buffer): this { + const newTargetChainId = typeof chainId === 'string' ? utils.cb58Decode(chainId) : Buffer.from(chainId); + this.validateChainId(newTargetChainId); + this._externalChainId = newTargetChainId; + return this; + } + + /** + * Set the transaction fee + * + * @param {string | bigint} feeValue - the fee value + */ + fee(feeValue: string | bigint): this { + const fee = typeof feeValue === 'string' ? feeValue : feeValue.toString(); + (this.transaction as Transaction)._fee.fee = fee; + return this; + } } diff --git a/modules/sdk-coin-flrp/src/lib/transaction.ts b/modules/sdk-coin-flrp/src/lib/transaction.ts index 0b66772cad..839d280869 100644 --- a/modules/sdk-coin-flrp/src/lib/transaction.ts +++ b/modules/sdk-coin-flrp/src/lib/transaction.ts @@ -18,6 +18,7 @@ import { Address, } from '@flarenetwork/flarejs'; import { Buffer } from 'buffer'; +import { createHash } from 'crypto'; import { DecodedUtxoObj, TransactionExplanation, Tx, TxData } from './iface'; import { KeyPair } from './keyPair'; import utils from './utils'; @@ -93,18 +94,27 @@ export class Transaction extends BaseTransaction { throw new InvalidTransactionError('empty credentials to sign'); } - //TODO: need to check for type of transaction and handle accordingly const unsignedTx = this._flareTransaction as EVMUnsignedTx; const unsignedBytes = unsignedTx.toBytes(); const publicKey = secp256k1.getPublicKey(prv); - const EVMAddressHex = new Address(secp256k1.publicKeyToEthAddress(publicKey)).toHex(); + // Derive both EVM and P-chain addresses from the public key + const evmAddressHex = new Address(secp256k1.publicKeyToEthAddress(publicKey)).toHex(); + + // P-chain address derivation: ripemd160(sha256(publicKey)) + const sha256Hash = createHash('sha256').update(Buffer.from(publicKey)).digest(); + const pChainAddressBuffer = createHash('ripemd160').update(sha256Hash).digest(); + const pChainAddressHex = pChainAddressBuffer.toString('hex'); const addressMap = unsignedTx.getAddresses(); - const hasMatchingAddress = addressMap.some( - (addr) => Buffer.from(addr).toString('hex').toLowerCase() === utils.removeHexPrefix(EVMAddressHex).toLowerCase() - ); + // Check for both EVM and P-chain address formats + const hasMatchingAddress = addressMap.some((addr) => { + const addrHex = Buffer.from(addr).toString('hex').toLowerCase(); + return ( + addrHex === utils.removeHexPrefix(evmAddressHex).toLowerCase() || addrHex === pChainAddressHex.toLowerCase() + ); + }); if (hasMatchingAddress) { const signature = await secp256k1.sign(unsignedBytes, prv); diff --git a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts index 3322b1315e..4c3129c8ce 100644 --- a/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts +++ b/modules/sdk-coin-flrp/src/lib/transactionBuilderFactory.ts @@ -1,4 +1,4 @@ -import { utils as FlareUtils, evmSerial } from '@flarenetwork/flarejs'; +import { utils as FlareUtils, evmSerial, pvmSerial } from '@flarenetwork/flarejs'; import { BaseTransactionBuilderFactory, NotSupported } from '@bitgo/sdk-core'; import { FlareNetwork, BaseCoin as CoinConfig } from '@bitgo/statics'; import { TransactionBuilder } from './transactionBuilder'; @@ -22,30 +22,40 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { const rawBuffer = Buffer.from(rawNoHex, 'hex'); let txSource: 'EVM' | 'PVM'; - // const manager = FlareUtils.getManagerForVM("EVM") - // const parsedTx = manager.unpackTransaction(rawBuffer) - - // Get network IDs const network = this._coinConfig.network as FlareNetwork; + let tx: any; try { txSource = 'EVM'; const evmManager = FlareUtils.getManagerForVM('EVM'); - const tx = evmManager.unpackTransaction(rawBuffer); + tx = evmManager.unpackTransaction(rawBuffer); const blockchainId = tx.getBlockchainId(); if (blockchainId === network.cChainBlockchainID) { console.log('Parsed as EVM transaction on C-Chain'); } + } catch (e) { + txSource = 'PVM'; + const pvmManager = FlareUtils.getManagerForVM('PVM'); + tx = pvmManager.unpackTransaction(rawBuffer); + const blockchainId = tx.getBlockchainId(); - if (txSource === 'EVM') { - if (ExportInCTxBuilder.verifyTxType(tx._type)) { - const exportBuilder = this.getExportInCBuilder(); - exportBuilder.initBuilder(tx as evmSerial.ExportTx, rawBuffer); - return exportBuilder; - } + if (blockchainId === network.blockchainID) { + console.log('Parsed as PVM transaction on P-Chain'); + } + } + + if (txSource === 'EVM') { + if (ExportInCTxBuilder.verifyTxType(tx._type)) { + const exportBuilder = this.getExportInCBuilder(); + exportBuilder.initBuilder(tx as evmSerial.ExportTx, rawBuffer); + return exportBuilder; + } + } else if (txSource === 'PVM') { + if (ImportInPTxBuilder.verifyTxType(tx._type)) { + const importBuilder = this.getImportInPBuilder(); + importBuilder.initBuilder(tx as pvmSerial.ImportTx, rawBuffer); + return importBuilder; } - } catch (e) { - console.log('error while parsing tx: ', e.message); } throw new NotSupported('Transaction type not supported'); } @@ -58,14 +68,14 @@ export class TransactionBuilderFactory extends BaseTransactionBuilderFactory { /** * Export Cross chain transfer */ - getExportBuilder(): ExportInPTxBuilder { + getExportInPBuilder(): ExportInPTxBuilder { return new ExportInPTxBuilder(this._coinConfig); } /** * Import Cross chain transfer */ - getImportBuilder(): ImportInPTxBuilder { + getImportInPBuilder(): ImportInPTxBuilder { return new ImportInPTxBuilder(this._coinConfig); } diff --git a/modules/sdk-coin-flrp/src/lib/utils.ts b/modules/sdk-coin-flrp/src/lib/utils.ts index 9e9e238518..504b81055e 100644 --- a/modules/sdk-coin-flrp/src/lib/utils.ts +++ b/modules/sdk-coin-flrp/src/lib/utils.ts @@ -460,6 +460,68 @@ export class Utils implements BaseUtils { } } + /** + * Parse credentials from raw bytes at a specific offset + * This is useful when the standard extraction fails due to serialization differences + * @param rawBytes Raw transaction bytes including credentials + * @param offset Byte offset where credentials start + * @returns Array of parsed credentials + */ + parseCredentialsAtOffset(rawBytes: Buffer, offset: number): Credential[] { + try { + if (rawBytes.length <= offset + 4) { + return []; + } + + const credentialBytes = rawBytes.slice(offset); + const numCredentials = credentialBytes.readUInt32BE(0); + + if (numCredentials === 0) { + return []; + } + + const credentials: Credential[] = []; + let pos = 4; + + for (let i = 0; i < numCredentials; i++) { + if (pos + 8 > credentialBytes.length) { + break; + } + + // Read type ID (4 bytes) - Type ID 9 = secp256k1 credential + const typeId = credentialBytes.readUInt32BE(pos); + pos += 4; + + if (typeId !== 9) { + continue; + } + + // Read number of signatures (4 bytes) + const numSigs = credentialBytes.readUInt32BE(pos); + pos += 4; + + // Parse all signatures for this credential + const signatures: Signature[] = []; + for (let j = 0; j < numSigs; j++) { + if (pos + 65 > credentialBytes.length) { + break; + } + const sigBytes = Buffer.from(credentialBytes.slice(pos, pos + 65)); + signatures.push(new Signature(sigBytes)); + pos += 65; + } + + if (signatures.length > 0) { + credentials.push(new Credential(signatures)); + } + } + + return credentials; + } catch { + return []; + } + } + /** * FlareJS wrapper to recover signature * @param network diff --git a/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts b/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts new file mode 100644 index 0000000000..26b15290db --- /dev/null +++ b/modules/sdk-coin-flrp/test/resources/transactionData/importInP.ts @@ -0,0 +1,63 @@ +export const IMPORT_IN_P = { + txhash: 'E9zZjFzTshfrZZv17n17gFKwj9ijyRaj6nQ1cJed3gYxSBUaX', + unsignedHex: + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b80000000000000000000000020000000312cb32eaf92553064db98d271b56cba079ec78f5a6e0c1abd0132f70efb77e2274637ff336a29a57c386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da555247900000001836b0141f34b3f855b69a0837e8ac0ede628333a4fbb389fb6a939709b0dbfa90000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001', + signedHex: + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b80000000000000000000000020000000312cb32eaf92553064db98d271b56cba079ec78f5a6e0c1abd0132f70efb77e2274637ff336a29a57c386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da555247900000001836b0141f34b3f855b69a0837e8ac0ede628333a4fbb389fb6a939709b0dbfa90000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001000000010000000900000002ef08753ef72f04e7f55ed806de709ebac9dae71f152c4d9dc63f4d33caaac7380ea00017b948172268ff47955dccb3812772b63c9fc0a6d6f135a968eebb2e9d01b9c7e056bac529f03cf05e2f1d3f18884546b19e59baeb9d87fe297b9fa2f6813dda416a2d19a5b13aa0b4850f0082c5cfdfd15b20069ecda47e1b5bf611c89e00', + xPrivateKey: + 'xprv9s21ZrQH143K2DW9jvDoAkVpRKi5V9XhZaVdoUcqoYPPQ9wRrLNT6VGgWBbRoSYB39Lak6kXgdTM9T3QokEi5n2JJ8EdggHLkZPX8eDiBu1', + signature: [ + '0x33f126dee90108c473af9513ebd9eb1591a701b5dfc69041075b303b858fee0609ca9a60208b46f6836f0baf1a9fba740d97b65d45caae10470b5fa707eb45c900', + ], + + halfSignedSignature: + '0xef08753ef72f04e7f55ed806de709ebac9dae71f152c4d9dc63f4d33caaac7380ea00017b948172268ff47955dccb3812772b63c9fc0a6d6f135a968eebb2e9d01', + halfSigntxHex: + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b80000000000000000000000020000000312cb32eaf92553064db98d271b56cba079ec78f5a6e0c1abd0132f70efb77e2274637ff336a29a57c386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da555247900000001836b0141f34b3f855b69a0837e8ac0ede628333a4fbb389fb6a939709b0dbfa90000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001000000010000000900000002ef08753ef72f04e7f55ed806de709ebac9dae71f152c4d9dc63f4d33caaac7380ea00017b948172268ff47955dccb3812772b63c9fc0a6d6f135a968eebb2e9d010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000', + fullSigntxHex: + '0x0000000000110000007200000000000000000000000000000000000000000000000000000000000000000000000158734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000070000000002e7b2b80000000000000000000000020000000312cb32eaf92553064db98d271b56cba079ec78f5a6e0c1abd0132f70efb77e2274637ff336a29a57c386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3000000000000000078db5c30bed04c05ce209179812850bbb3fe6d46d7eef3744d814c0da555247900000001836b0141f34b3f855b69a0837e8ac0ede628333a4fbb389fb6a939709b0dbfa90000000058734f94af871c3d131b56131b6fb7a0291eacadd261e69dfb42a9cdf6f7fddd000000050000000002faf080000000020000000000000001000000010000000900000002ef08753ef72f04e7f55ed806de709ebac9dae71f152c4d9dc63f4d33caaac7380ea00017b948172268ff47955dccb3812772b63c9fc0a6d6f135a968eebb2e9d01b9c7e056bac529f03cf05e2f1d3f18884546b19e59baeb9d87fe297b9fa2f6813dda416a2d19a5b13aa0b4850f0082c5cfdfd15b20069ecda47e1b5bf611c89e00', + fullSignedSignature: + '0xb9c7e056bac529f03cf05e2f1d3f18884546b19e59baeb9d87fe297b9fa2f6813dda416a2d19a5b13aa0b4850f0082c5cfdfd15b20069ecda47e1b5bf611c89e00', + + outputs: [ + { + outputID: 0, + amount: '50000000', + txid: 'zstyYq5riDKYDSR3fUYKKkuXKJ1aJCe8WNrXKqEBJD4CGwzFw', + outputidx: '0', + addresses: [ + '0x12cb32eaf92553064db98d271b56cba079ec78f5', + '0xa6e0c1abd0132f70efb77e2274637ff336a29a57', + '0xc386d58d09a9ae77cf1cf07bf1c9de44ebb0c9f3', + ], + threshold: 2, + }, + ], + cAddressPrivateKey: '14977929a4e00e4af1c33545240a6a5a08ca3034214618f6b04b72b80883be3a', + cAddressPublicKey: '033ca1801f51484063f3bce093413ca06f7d91c44c3883f642eb103eda5e0eaed3', + amount: '50000000', // 0.00005 FLR + cHexAddress: '0x28A05933dC76e4e6c25f35D5c9b2A58769700E76', + pAddresses: [ + 'P-costwo1zt9n96hey4fsvnde35n3k4kt5pu7c784dzewzd', + 'P-costwo1cwrdtrgf4xh80ncu7palrjw7gn4mpj0n4dxghh', + 'P-costwo15msvr27szvhhpmah0c38gcml7vm29xjh7tcek8', + ], + mainAddress: 'P-costwo1q0ssshmwz3k77k3v0wkfr0j64dvhzzaaf9wdhq', + corethAddress: [ + 'C-costwo1zt9n96hey4fsvnde35n3k4kt5pu7c784dzewzd', + 'C-costwo1cwrdtrgf4xh80ncu7palrjw7gn4mpj0n4dxghh', + 'C-costwo15msvr27szvhhpmah0c38gcml7vm29xjh7tcek8', + ], + privateKeys: [ + '14977929a4e00e4af1c33545240a6a5a08ca3034214618f6b04b72b80883be3a', + '7cf4f2c6ba02376bd586217f4a7cd4061e1908e38cf1614278606548d7eb6f7a', + '002939e9312351e9e23c58015d7ef977ef9f5eaa290e8375b1c4b7f071e0ac1a', + ], + sourceChainId: 'vE8M98mEQH6wk56sStD1ML8HApTgSqfJZLk9gQ3Fsd4i6m3Bi', + nonce: 9, + threshold: 2, + fee: '1261000', // Fee derived from expected hex (input - output = 50000000 - 48739000) + locktime: 0, + INVALID_CHAIN_ID: 'wrong chain id', + VALID_C_CHAIN_ID: 'yH8D7ThNJkxmtkuv2jgBa4P1Rn3Qpr4pPr7QYNfcdoS6k6HWp', +}; diff --git a/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts b/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts new file mode 100644 index 0000000000..23400364d0 --- /dev/null +++ b/modules/sdk-coin-flrp/test/unit/lib/importInPTxBuilder.ts @@ -0,0 +1,91 @@ +import assert from 'assert'; +import 'should'; +import { IMPORT_IN_P as testData } from '../../resources/transactionData/importInP'; +import { TransactionBuilderFactory, DecodedUtxoObj } from '../../../src/lib'; +import { coins } from '@bitgo/statics'; +import signFlowTest from './signFlowTestSuit'; + +describe('Flrp Import In P Tx Builder', () => { + const factory = new TransactionBuilderFactory(coins.get('tflrp')); + describe('validate txBuilder fields', () => { + const txBuilder = factory.getImportInPBuilder(); + + it('should fail target chain id length incorrect', () => { + assert.throws( + () => { + txBuilder.externalChainId(Buffer.from(testData.INVALID_CHAIN_ID)); + }, + (e: any) => e.message === 'Chain id are 32 byte size' + ); + }); + + it('should fail target chain id not a valid base58 string', () => { + assert.throws( + () => { + txBuilder.externalChainId(testData.INVALID_CHAIN_ID); + }, + (e: any) => e.message === 'Non-base58 character' + ); + }); + + it('should fail target chain id cb58 invalid checksum', () => { + assert.throws( + () => { + txBuilder.externalChainId(testData.VALID_C_CHAIN_ID.slice(2)); + }, + (e: any) => e.message === 'Invalid checksum' + ); + }); + + it('should fail validate Utxos empty string', () => { + assert.throws( + () => { + txBuilder.validateUtxos([]); + }, + (e: any) => e.message === 'UTXOs array cannot be empty' + ); + }); + + it('should fail validate Utxos without amount field', () => { + assert.throws( + () => { + txBuilder.validateUtxos([{ outputID: '' } as any as DecodedUtxoObj]); + }, + (e: any) => e.message === 'UTXO missing required field: amount' + ); + }); + }); + + signFlowTest({ + transactionType: 'Import P2C', + 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), + unsignedTxHex: testData.unsignedHex, + halfSignedTxHex: testData.halfSigntxHex, + fullSignedTxHex: testData.fullSigntxHex, + privateKey: { + prv1: testData.privateKeys[0], + prv2: testData.privateKeys[1], + }, + txHash: testData.txhash, + }); + + it('Should full sign a import tx from unsigned raw tx', () => { + const txBuilder = new TransactionBuilderFactory(coins.get('tflrp')).from(testData.unsignedHex); + txBuilder.sign({ key: testData.privateKeys[0] }); + txBuilder + .build() + .then(() => assert.fail('it can sign')) + .catch((err) => { + err.message.should.be.equal('Private key cannot sign the transaction'); + }); + }); +}); diff --git a/modules/sdk-coin-flrp/test/unit/lib/signFlowTestSuit.ts b/modules/sdk-coin-flrp/test/unit/lib/signFlowTestSuit.ts new file mode 100644 index 0000000000..b5f9d2b565 --- /dev/null +++ b/modules/sdk-coin-flrp/test/unit/lib/signFlowTestSuit.ts @@ -0,0 +1,110 @@ +import { BaseTransactionBuilder, BaseTransactionBuilderFactory } from '@bitgo/sdk-core'; + +export interface signFlowTestSuitArgs { + transactionType: string; + newTxFactory: () => BaseTransactionBuilderFactory; + newTxBuilder: () => BaseTransactionBuilder; + unsignedTxHex: string; + halfSignedTxHex: string; + fullSignedTxHex: string; + privateKey: { prv1: string; prv2: string }; + txHash: string; +} + +/** + * Test suit focus in raw tx signing changes. + * TODO(BG-54381): Coin Agnostic Testing + * @param {signFlowTestSuitArgs} data with require info. + */ +export default function signFlowTestSuit(data: signFlowTestSuitArgs): void { + describe(`should sign ${data.transactionType} in full flow `, () => { + it('Should create tx for same values', async () => { + const txBuilder = data.newTxBuilder(); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.unsignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should recover tx from raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.unsignedTxHex); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.unsignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should create half signed tx for same values', async () => { + const txBuilder = data.newTxBuilder(); + + txBuilder.sign({ key: data.privateKey.prv1 }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.halfSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should recover half signed tx from raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.halfSignedTxHex); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.halfSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should half sign tx from unsigned raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.unsignedTxHex); + txBuilder.sign({ key: data.privateKey.prv1 }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.halfSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should recover half signed tx from half signed raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.halfSignedTxHex); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.halfSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should recover signed tx from signed raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.fullSignedTxHex); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.fullSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should full sign a tx for same values', async () => { + const txBuilder = data.newTxBuilder(); + + txBuilder.sign({ key: data.privateKey.prv1 }); + txBuilder.sign({ key: data.privateKey.prv2 }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.fullSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should full sign a tx from half signed raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.halfSignedTxHex); + txBuilder.sign({ key: data.privateKey.prv2 }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.fullSignedTxHex); + tx.id.should.equal(data.txHash); + }); + + it('Should full sign a tx from unsigned raw tx', async () => { + const txBuilder = data.newTxFactory().from(data.unsignedTxHex); + txBuilder.sign({ key: data.privateKey.prv1 }); + txBuilder.sign({ key: data.privateKey.prv2 }); + const tx = await txBuilder.build(); + const rawTx = tx.toBroadcastFormat(); + rawTx.should.equal(data.fullSignedTxHex); + tx.id.should.equal(data.txHash); + }); + }); +}