diff --git a/modules/sdk-coin-flr/src/flr.ts b/modules/sdk-coin-flr/src/flr.ts index 8ac281eace..b5b3e7eef3 100644 --- a/modules/sdk-coin-flr/src/flr.ts +++ b/modules/sdk-coin-flr/src/flr.ts @@ -19,10 +19,12 @@ import { common, FeeEstimateOptions, IWallet, + isAvalancheAtomicTx, MPCAlgorithm, MultisigType, multisigTypes, Recipient, + SignableTransaction, TransactionExplanation, Entry, } from '@bitgo/sdk-core'; @@ -85,6 +87,16 @@ export class Flr extends AbstractEthLikeNewCoins { return 'ecdsa'; } + /** + * Returns true when the signableHex for this transaction is already the + * final signing hash. Cross-chain export atomic transactions from FLR + * C-chain to FLRP P-chain are pre-hashed with SHA-256(txBody); the MPC + * signing flow must use that digest directly without applying keccak256. + */ + isSignablePreHashed(unsignedTx: SignableTransaction): boolean { + return isAvalancheAtomicTx(unsignedTx); + } + 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 8e964e3fe3..5aebc74a43 100644 --- a/modules/sdk-coin-flr/test/unit/flr.ts +++ b/modules/sdk-coin-flr/test/unit/flr.ts @@ -97,6 +97,26 @@ describe('flr', function () { }); }); + describe('isSignablePreHashed', function () { + it('returns true for Avalanche atomic txs (codec prefix 0000)', function () { + // C->P cross-chain export atomic tx — signableHex is already SHA-256(txBody). + // The MPC layer must use this digest directly, not re-hash with keccak256, + // otherwise the user and BitGo signature shares will not combine. + const signableHex = 'a'.repeat(64); + flrCoin.isSignablePreHashed({ serializedTxHex: '0000' + 'b'.repeat(20), signableHex }).should.equal(true); + }); + + it('returns false for standard EVM RLP transactions', function () { + // EIP-1559 / RLP serialized transactions start with 0x02 or 0xf8 — never 0x0000. + flrCoin + .isSignablePreHashed({ + serializedTxHex: '02f17281d902850ba43b740283061a80', + signableHex: '02f17281d902850ba43b740283061a80', + }) + .should.equal(false); + }); + }); + describe('Address Validation', function () { it('should validate valid eth address', function () { const address = '0x1374a2046661f914d1687d85dbbceb9ac7910a29'; diff --git a/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts b/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts index e83728151b..226797f319 100644 --- a/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts @@ -57,6 +57,13 @@ export const NO_RECIPIENT_TX_TYPES = new Set([ 'transferOfferWithdrawn', 'cantonCommand', 'pledge', + + // Avalanche / Flare cross-chain atomic imports — recipients are not supplied + // by the client because the import consumes UTXOs already owned by the + // wallet; the destination address is the wallet itself. WP issues these + // with intentType 'import' (P-chain) or 'importtoc' (C-chain). + 'import', + 'importtoc', ]); /** diff --git a/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts b/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts index 1df5ccfd95..68ed006d4e 100644 --- a/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts +++ b/modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts @@ -49,6 +49,9 @@ describe('recipientUtils', function () { 'transferOfferWithdrawn', 'cantonCommand', 'pledge', + // Avalanche / Flare cross-chain atomic imports + 'import', + 'importtoc', ]; expected.forEach((t) => assert.ok(NO_RECIPIENT_TX_TYPES.has(t), `${t} should be in NO_RECIPIENT_TX_TYPES`)); assert.strictEqual(NO_RECIPIENT_TX_TYPES.size, expected.length); @@ -104,12 +107,25 @@ describe('recipientUtils', function () { 'stake', 'createAccount', 'pledge', + 'import', + 'importtoc', ]) { const txRequest = makeTxRequest(); assert.doesNotThrow(() => resolveEffectiveTxParams(txRequest, { type: txType })); } }); + it('does not throw for Avalanche cross-chain imports resolved from intent.intentType', function () { + // P-chain and C-chain import intents legitimately carry no recipients — + // the wallet imports its own UTXOs and the destination address is the + // wallet itself. The intentType lives only on the intent (txParams.type + // is unset on the MPC signing call) so the guard must read it from there. + for (const intentType of ['import', 'importtoc']) { + const txRequest = makeTxRequest({ intent: { intentType } as any }); + assert.doesNotThrow(() => resolveEffectiveTxParams(txRequest, {})); + } + }); + it('throws InvalidTransactionError for unknown types with no recipients', function () { const txRequest = makeTxRequest(); assert.throws(