Skip to content

Commit 2bdfd7d

Browse files
Merge pull request #9550 from BitGo/ryaan/sol-consolidation-passphrase-fix
fix(sdk-coin-sol): validate bitgoKey in recoverConsolidations, add co…
2 parents 917d39f + 684728c commit 2bdfd7d

2 files changed

Lines changed: 83 additions & 0 deletions

File tree

modules/sdk-coin-sol/src/sol.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,6 +1916,10 @@ export class Sol extends BaseCoin {
19161916
* @param {string} [params.endingScanIndex] - receive address index to end scanning at. default to startingScanIndex + 20 (exclusive).
19171917
*/
19181918
async recoverConsolidations(params: SolConsolidationRecoveryOptions): Promise<MPCTxs | MPCSweepTxs> {
1919+
if (!params.bitgoKey) {
1920+
throw new Error('missing bitgoKey');
1921+
}
1922+
19191923
const isUnsignedSweep = !params.walletPassphrase;
19201924
const startIdx = params.startingScanIndex || 1;
19211925
const endIdx = params.endingScanIndex || startIdx + DEFAULT_SCAN_FACTOR;

modules/sdk-coin-sol/test/unit/sol.ts

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {
3131
getAmountBasedOnEndianness,
3232
KeyPair,
3333
Sol,
34+
SolConsolidationRecoveryOptions,
3435
SolRecoveryOptions,
3536
SolVerifyTransactionOptions,
3637
Tsol,
@@ -2592,6 +2593,37 @@ describe('SOL:', function () {
25922593
sandBox.assert.callCount(solCoin.getDataFromNode, 4);
25932594
});
25942595

2596+
it('should recover a txn for unsigned sweep recoveries when userKey/backupKey are empty strings', async function () {
2597+
// Regression test: WRW's cold-wallet sweep form sends userKey/backupKey as empty strings
2598+
// (rather than omitting them) to satisfy TS typing. Empty string and undefined must be
2599+
// handled identically by the unsigned-sweep gate (`!params.walletPassphrase`) and by
2600+
// isMpcv2SigningMaterial's early `!walletPassphrase` short-circuit.
2601+
const unsignedSweepTxn = (await basecoin.recover({
2602+
userKey: '',
2603+
backupKey: '',
2604+
bitgoKey: testData.keys.bitgoKey,
2605+
recoveryDestination: testData.keys.destinationPubKey,
2606+
durableNonce: {
2607+
publicKey: testData.keys.durableNoncePubKey,
2608+
secretKey: testData.keys.durableNoncePrivKey,
2609+
},
2610+
})) as MPCSweepTxs;
2611+
2612+
unsignedSweepTxn.should.not.be.empty();
2613+
unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.should.hasOwnProperty('serializedTx');
2614+
should.equal(unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.scanIndex, 0);
2615+
2616+
const unsignedSweepTxnDeserialize = new Transaction(coin);
2617+
unsignedSweepTxnDeserialize.fromRawTransaction(
2618+
unsignedSweepTxn.txRequests[0].transactions[0].unsignedTx.serializedTx
2619+
);
2620+
const unsignedSweepTxnJson = unsignedSweepTxnDeserialize.toJson();
2621+
2622+
should.equal(unsignedSweepTxnJson.nonce, testData.SolInputData.durableNonceBlockhash);
2623+
should.equal(unsignedSweepTxnJson.feePayer, testData.accountInfo.bs58EncodedPublicKey);
2624+
should.equal(unsignedSweepTxnJson.numSignatures, testData.SolInputData.unsignedSweepSignatures);
2625+
});
2626+
25952627
it('should handle error in recover function if a required field is missing/incorrect', async function () {
25962628
// missing userkey
25972629
await basecoin
@@ -3889,6 +3921,41 @@ describe('SOL:', function () {
38893921
should.equal(latestBlockhashTxnJson1.numSignatures, testData.SolInputData.unsignedSweepSignatures);
38903922
});
38913923

3924+
it('should build signed token consolidation recoveries (hot wallet)', async function () {
3925+
// Regression test: hot-wallet SPL-token consolidation must produce a *signed* transaction
3926+
// when userKey/backupKey/walletPassphrase are supplied — mirrors the WRW hot-wallet flow.
3927+
const res = (await basecoin.recoverConsolidations({
3928+
userKey: testData.wrwUser.userKey,
3929+
backupKey: testData.wrwUser.backupKey,
3930+
bitgoKey: testData.wrwUser.bitgoKey,
3931+
walletPassphrase: testData.wrwUser.walletPassphrase,
3932+
startingScanIndex: 3,
3933+
endingScanIndex: 5,
3934+
tokenContractAddress: usdtMintAddress,
3935+
durableNonces: durableNonces,
3936+
})) as MPCTxs;
3937+
3938+
res.should.not.be.empty();
3939+
res.transactions.length.should.equal(1);
3940+
3941+
const txn1 = res.transactions[0] as MPCTx;
3942+
txn1.should.hasOwnProperty('serializedTx');
3943+
txn1.should.hasOwnProperty('scanIndex');
3944+
(txn1.scanIndex ?? 0).should.equal(4);
3945+
3946+
const tokenConsolidationTxnDeserialize = new Transaction(coin);
3947+
tokenConsolidationTxnDeserialize.fromRawTransaction(txn1.serializedTx);
3948+
const tokenConsolidationTxnJson = tokenConsolidationTxnDeserialize.toJson();
3949+
3950+
const nonce1 = testData.SolResponses.getAccountInfoResponse.body.result.value.data.parsed.info.blockhash;
3951+
should.equal(tokenConsolidationTxnJson.nonce, nonce1);
3952+
should.equal(tokenConsolidationTxnJson.feePayer, testData.wrwUser.walletAddress5);
3953+
// Signed (durable nonce + recovery signature) must have MORE signatures than an unsigned sweep
3954+
// of the same transaction shape — proves walletPassphrase actually drove a signed path.
3955+
should.equal(tokenConsolidationTxnJson.numSignatures, testData.SolInputData.durableNonceSignatures);
3956+
tokenConsolidationTxnJson.numSignatures.should.be.above(testData.SolInputData.unsignedSweepSignatures);
3957+
});
3958+
38923959
it('should skip building consolidate transaction if balance is equal to zero', async function () {
38933960
await basecoin
38943961
.recoverConsolidations({
@@ -3903,6 +3970,18 @@ describe('SOL:', function () {
39033970
.should.rejectedWith('Did not find an address with funds to recover');
39043971
});
39053972

3973+
it('should throw if bitgoKey is missing', async () => {
3974+
await basecoin
3975+
.recoverConsolidations({
3976+
userKey: testData.wrwUser.userKey,
3977+
backupKey: testData.wrwUser.backupKey,
3978+
startingScanIndex: 1,
3979+
endingScanIndex: 2,
3980+
durableNonces: durableNonces,
3981+
} as SolConsolidationRecoveryOptions)
3982+
.should.be.rejectedWith('missing bitgoKey');
3983+
});
3984+
39063985
it('should throw if startingScanIndex is not ge to 1', async () => {
39073986
await basecoin
39083987
.recoverConsolidations({

0 commit comments

Comments
 (0)