diff --git a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts index c5e68fffc0..d7c4c5c42c 100644 --- a/modules/sdk-core/src/bitgo/utils/mpcUtils.ts +++ b/modules/sdk-core/src/bitgo/utils/mpcUtils.ts @@ -428,6 +428,11 @@ export abstract class MpcUtils { memo: params.memo?.value, token: params.tokenName, enableTokens: params.enableTokens, + // enableToken's configureAccount opt-in (Solana Token-2022 confidential transfers) + // must survive the generic tail: the Solana enableToken prebuild supplies no + // feeOptions, and gating the flag on that branch silently routed explicit + // opt-ins to plain ATA-init. Scoped to enableToken so other intents gain no field. + ...(params.intentType === 'enableToken' ? { configureAccount: params.configureAccount } : {}), feeToken: params.feeToken, }; } diff --git a/modules/sdk-core/test/unit/bitgo/utils/mpcUtils.enableToken.ts b/modules/sdk-core/test/unit/bitgo/utils/mpcUtils.enableToken.ts new file mode 100644 index 0000000000..7cfdafa82d --- /dev/null +++ b/modules/sdk-core/test/unit/bitgo/utils/mpcUtils.enableToken.ts @@ -0,0 +1,90 @@ +import assert from 'assert'; +import * as sinon from 'sinon'; +import { IBaseCoin, KeychainsTriplet } from '../../../../src/bitgo/baseCoin'; +import { BitGoBase } from '../../../../src/bitgo/bitgoBase'; +import { MpcUtils } from '../../../../src/bitgo/utils/mpcUtils'; +import { RequestTracer } from '../../../../src'; + +class TestMpcUtils extends MpcUtils { + createKeychains(): Promise { + return Promise.reject(new Error('unused')); + } +} + +/** + * enableToken's `configureAccount` opt-in (the Solana Token-2022 confidential-transfer lane) + * must survive `populateIntent` for the generic (non-EVM) coin path. The generic tail used to + * copy the flag only inside the `feeOptions !== undefined` branch, and the Solana enableToken + * prebuild supplies no feeOptions — so `wallet.enableToken({ configureAccount: true })` + * reached Wallet Platform with the flag silently dropped and routed to plain ATA-init. + */ +describe('populateIntent enableToken configureAccount', function () { + const reqId = new RequestTracer(); + let mpcUtils: TestMpcUtils; + let solCoin: IBaseCoin; + + beforeEach(function () { + const mockBitgo = { getEnv: sinon.stub().returns('test') } as unknown as BitGoBase; + solCoin = { + getChain: () => 'tsol', + getFamily: () => 'sol', + isEVM: () => false, + supportsTss: () => true, + // The generic tail runs setCoinSpecificFieldsInIntent before returning; the Solana + // coin adds nothing for enableToken, so a no-op is the faithful mock. + setCoinSpecificFieldsInIntent: () => undefined, + } as unknown as IBaseCoin; + mpcUtils = new TestMpcUtils(mockBitgo, solCoin); + }); + + afterEach(function () { + sinon.restore(); + }); + + it('keeps the configureAccount opt-in on the generic path without feeOptions', function () { + const intent = mpcUtils.populateIntent(solCoin, { + reqId, + intentType: 'enableToken', + recipients: [], + enableTokens: [{ name: 'tsol:t22mint' }], + configureAccount: true, + }) as { configureAccount?: boolean }; + + assert.strictEqual(intent.configureAccount, true); + }); + + it('keeps the opt-out explicit when configureAccount is false', function () { + const intent = mpcUtils.populateIntent(solCoin, { + reqId, + intentType: 'enableToken', + recipients: [], + enableTokens: [{ name: 'tsol:t22mint' }], + configureAccount: false, + }) as { configureAccount?: boolean }; + + assert.strictEqual(intent.configureAccount, false); + }); + + it('still carries the flag when feeOptions are present', function () { + const intent = mpcUtils.populateIntent(solCoin, { + reqId, + intentType: 'enableToken', + recipients: [], + enableTokens: [{ name: 'tsol:t22mint' }], + configureAccount: true, + feeOptions: { maxFeePerGas: 1000, maxPriorityFeePerGas: 1000 }, + }) as { configureAccount?: boolean }; + + assert.strictEqual(intent.configureAccount, true); + }); + + it('does not add the field to non-enableToken intents', function () { + const intent = mpcUtils.populateIntent(solCoin, { + reqId, + intentType: 'wrap-native', + wrapParams: { tokenName: 'tsol', amount: '1000' }, + }) as unknown as Record; + + assert.strictEqual('configureAccount' in intent, false); + }); +});