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
12 changes: 12 additions & 0 deletions modules/sdk-coin-flr/src/flr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,12 @@ import {
common,
FeeEstimateOptions,
IWallet,
isAvalancheAtomicTx,
MPCAlgorithm,
MultisigType,
multisigTypes,
Recipient,
SignableTransaction,
TransactionExplanation,
Entry,
} from '@bitgo/sdk-core';
Expand Down Expand Up @@ -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<OfflineVaultTxInfo | UnsignedSweepTxMPCv2> {
return this.buildUnsignedSweepTxnMPCv2(params);
}
Expand Down
20 changes: 20 additions & 0 deletions modules/sdk-coin-flr/test/unit/flr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
7 changes: 7 additions & 0 deletions modules/sdk-core/src/bitgo/utils/tss/recipientUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
]);

/**
Expand Down
16 changes: 16 additions & 0 deletions modules/sdk-core/test/unit/bitgo/utils/tss/recipientUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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(
Expand Down
Loading