Skip to content
Draft
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
31 changes: 31 additions & 0 deletions modules/sdk-coin-flr/src/flr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Buffer> {
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<string> {
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<OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
return this.buildUnsignedSweepTxnMPCv2(params);
}
Expand Down
49 changes: 49 additions & 0 deletions modules/sdk-coin-flr/test/unit/flr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
Loading