diff --git a/modules/sdk-coin-flr/src/flr.ts b/modules/sdk-coin-flr/src/flr.ts index 908700b291..0ed1fac09a 100644 --- a/modules/sdk-coin-flr/src/flr.ts +++ b/modules/sdk-coin-flr/src/flr.ts @@ -97,6 +97,37 @@ export class Flr extends AbstractEthLikeNewCoins { return isAvalancheAtomicTx(unsignedTx); } + /** + * Get the signable payload for a transaction. For atomic cross-chain + * transactions (codec prefix 0x0000), route to the FLRP atomic builder + * which knows how to parse PVM/EVM atomic format and compute SHA-256(txBody). + * For regular EVM transactions, use the inherited EVM path. + */ + async getSignablePayload(serializedTx: string): Promise { + const hexWithout0x = serializedTx.startsWith('0x') ? serializedTx.slice(2) : serializedTx; + if (hexWithout0x.startsWith('0000')) { + const txBuilder = this.getAtomicBuilder().from(serializedTx); + const tx = (await txBuilder.build()) as FlrPLib.Transaction; + return Buffer.from(tx.signablePayload); + } + return super.getSignablePayload(serializedTx); + } + + /** + * Inject an MPC-produced signature into an unsigned atomic transaction. + * Called by the server after TSS signing to assemble the final signed tx. + * Only applies to cross-chain export atomic transactions (codec prefix 0x0000). + * @param txHex - Unsigned atomic transaction hex + * @param signature - 65-byte ECDSA signature (r || s || recovery) + * @returns Signed transaction hex + */ + async addSignatureToTransaction(txHex: string, signature: Buffer): Promise { + const txBuilder = this.getAtomicBuilder().from(txHex); + const tx = await txBuilder.build(); + (tx as FlrPLib.Transaction).addExternalSignature(new Uint8Array(signature)); + return tx.toBroadcastFormat(); + } + protected async buildUnsignedSweepTxnTSS(params: RecoverOptions): Promise { return this.buildUnsignedSweepTxnMPCv2(params); } diff --git a/modules/sdk-coin-flr/test/unit/flr.ts b/modules/sdk-coin-flr/test/unit/flr.ts index 602fd28d2b..7fe18adf3c 100644 --- a/modules/sdk-coin-flr/test/unit/flr.ts +++ b/modules/sdk-coin-flr/test/unit/flr.ts @@ -117,6 +117,55 @@ describe('flr', function () { }); }); + describe('getSignablePayload and addSignatureToTransaction (atomic C→P export)', function () { + it('should return 32-byte SHA-256 hash for atomic tx', async function () { + const signablePayload = await tflrCoin.getSignablePayload(EXPORT_C_TEST_DATA.unsignedTxHex); + signablePayload.should.be.instanceof(Buffer); + signablePayload.length.should.equal(32); + }); + + it('should produce the same signablePayload as the FLRP builder', async function () { + // FLR getSignablePayload must agree with the FLRP builder's signablePayload + // for the same atomic tx hex — both parties of TSS must hash identically. + const flrpModule = await import('@bitgo/sdk-coin-flrp'); + const flrpFactory = new flrpModule.FlrPLib.TransactionBuilderFactory(coins.get('tflrp')); + const txBuilder = flrpFactory.from(EXPORT_C_TEST_DATA.unsignedTxHex); + const tx = await txBuilder.build(); + const expectedPayload = Buffer.from((tx as any).signablePayload); + + const actualPayload = await tflrCoin.getSignablePayload(EXPORT_C_TEST_DATA.unsignedTxHex); + actualPayload.should.deepEqual(expectedPayload); + }); + + it('addSignatureToTransaction should produce identical result to FLRP builder sign()', async function () { + // Simulate TSS post-ceremony: sign the payload externally, then inject + // via addSignatureToTransaction. This must equal the result of signing + // via the FLRP builder directly (the reference path). + const flrpModule = await import('@bitgo/sdk-coin-flrp'); + const flrpFactory = new flrpModule.FlrPLib.TransactionBuilderFactory(coins.get('tflrp')); + + // Reference: sign via FLRP builder + const txBuilderForSign = flrpFactory.from(EXPORT_C_TEST_DATA.unsignedTxHex); + txBuilderForSign.sign({ key: EXPORT_C_TEST_DATA.privKey }); + const signedTx = await txBuilderForSign.build(); + const signedHexViaBuilder = signedTx.toBroadcastFormat(); + + // Extract the signature from the builder-signed tx, then re-inject it + // via FLR's addSignatureToTransaction (the TSS server path). + const parsedBuilder = flrpFactory.from(signedHexViaBuilder); + const parsedTx = await parsedBuilder.build(); + const sigHex = parsedTx.signature[0]; + const sigBuffer = Buffer.from(sigHex.startsWith('0x') ? sigHex.slice(2) : sigHex, 'hex'); + + const signedHexViaInject = await tflrCoin.addSignatureToTransaction( + EXPORT_C_TEST_DATA.unsignedTxHex, + sigBuffer + ); + + signedHexViaInject.should.equal(signedHexViaBuilder); + }); + }); + describe('Address Validation', function () { it('should validate valid eth address', function () { const address = '0x1374a2046661f914d1687d85dbbceb9ac7910a29';