Skip to content

Commit c126887

Browse files
authored
feat: add token minimum balance calculation and response type (#7269)
## Explanation <!-- Thanks for your contribution! Take a moment to answer these questions so that reviewers have the information they need to properly understand your changes: * What is the current state of things and why does it need to change? * What is the solution your changes offer and how does it work? * Are there any changes whose purpose might not obvious to those unfamiliar with the domain? * If your primary goal was to update one package but you found you had to update another one along the way, why did you do so? * If you had to upgrade a dependency, why did you do so? --> This PR - Add `minBillingCyclesForBalance` to `ProductPrice` type - Add `getTokenMinimumBalanceAmount` method to `SubscriptionController` ## References <!-- Are there any issues that this pull request is tied to? Are there other links that reviewers should consult to understand these changes better? Are there client or consumer pull requests to adopt any breaking changes? For example: * Fixes #12345 * Related to #67890 --> ## Checklist - [x] I've updated the test suite for new or updated code as appropriate - [x] I've updated documentation (JSDoc, Markdown, etc.) for new or updated code as appropriate - [x] I've communicated my changes to consumers by [updating changelogs for packages I've changed](https://github.com/MetaMask/core/tree/main/docs/contributing.md#updating-changelogs) - [x] I've introduced [breaking changes](https://github.com/MetaMask/core/tree/main/docs/breaking-changes.md) in this PR and have prepared draft pull requests for clients and consumer packages to resolve them <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Add `minBillingCyclesForBalance` and `getTokenMinimumBalanceAmount` to compute minimum token balance, with tests and minor docs updates. > > - **Subscription Controller**: > - Add `#getSubscriptionBalanceAmount(price)` and public `getTokenMinimumBalanceAmount(price, tokenPaymentInfo)` to compute minimum token balance using conversion rates; throws on missing rate. > - Clarify approval amount JSDoc for `#getSubscriptionPriceAmount`. > - **Types**: > - Extend `ProductPrice` with `minBillingCyclesForBalance`. > - **Tests**: > - Update mock pricing to include `minBillingCyclesForBalance`. > - Add unit tests for `getTokenMinimumBalanceAmount`, including error on missing conversion rate. > - Adjust existing tests to pass new pricing field where applicable. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 6e482ee. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
1 parent c8fc0e1 commit c126887

File tree

4 files changed

+95
-1
lines changed

4 files changed

+95
-1
lines changed

packages/subscription-controller/CHANGELOG.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
### Added
11+
12+
- Added `minBillingCyclesForBalance` property to `ProductPrice` type ([#7269](https://github.com/MetaMask/core/pull/7269))
13+
- Added `getTokenMinimumBalanceAmount` method to `SubscriptonController` ([#7269](https://github.com/MetaMask/core/pull/7269))
14+
1015
### Changed
1116

1217
- Bump `@metamask/transaction-controller` from `^62.3.0` to `^62.3.1` ([#7257](https://github.com/MetaMask/core/pull/7257))

packages/subscription-controller/src/SubscriptionController.test.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ const MOCK_PRODUCT_PRICE: ProductPricing = {
9494
unitDecimals: 2,
9595
trialPeriodDays: 0,
9696
minBillingCycles: 12,
97+
minBillingCyclesForBalance: 1,
9798
},
9899
{
99100
interval: 'year',
@@ -102,6 +103,7 @@ const MOCK_PRODUCT_PRICE: ProductPricing = {
102103
currency: 'usd',
103104
trialPeriodDays: 14,
104105
minBillingCycles: 1,
106+
minBillingCyclesForBalance: 1,
105107
},
106108
],
107109
};
@@ -1114,6 +1116,7 @@ describe('SubscriptionController', () => {
11141116
unitDecimals: 18,
11151117
trialPeriodDays: 0,
11161118
minBillingCycles: 1,
1119+
minBillingCyclesForBalance: 1,
11171120
},
11181121
],
11191122
},
@@ -1258,6 +1261,45 @@ describe('SubscriptionController', () => {
12581261
});
12591262
});
12601263

1264+
describe('getTokenMinimumBalanceAmount', () => {
1265+
it('returns correct minimum balance amount for token', async () => {
1266+
await withController(async ({ controller }) => {
1267+
const [price] = MOCK_PRODUCT_PRICE.prices;
1268+
const { chains } = MOCK_PRICING_PAYMENT_METHOD;
1269+
if (!chains || chains.length === 0) {
1270+
throw new Error('Mock chains not found');
1271+
}
1272+
const [tokenPaymentInfo] = chains[0].tokens;
1273+
1274+
const result = controller.getTokenMinimumBalanceAmount(
1275+
price,
1276+
tokenPaymentInfo,
1277+
);
1278+
1279+
expect(result).toBe('9000000000000000000');
1280+
});
1281+
});
1282+
1283+
it('throws when conversion rate not found', async () => {
1284+
await withController(async ({ controller }) => {
1285+
const price = MOCK_PRODUCT_PRICE.prices[0];
1286+
const tokenPaymentInfoWithoutRate = {
1287+
address: '0xtoken' as const,
1288+
decimals: 18,
1289+
symbol: 'USDT',
1290+
conversionRate: {} as { usd: string },
1291+
};
1292+
1293+
expect(() =>
1294+
controller.getTokenMinimumBalanceAmount(
1295+
price,
1296+
tokenPaymentInfoWithoutRate,
1297+
),
1298+
).toThrow('Conversion rate not found');
1299+
});
1300+
});
1301+
});
1302+
12611303
describe('triggerAuthTokenRefresh', () => {
12621304
it('should trigger auth token refresh', async () => {
12631305
await withController(async ({ controller, mockPerformSignOut }) => {

packages/subscription-controller/src/SubscriptionController.ts

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,7 @@ export class SubscriptionController extends StaticIntervalPollingController()<
757757
}
758758

759759
/**
760-
* Calculate total subscription price amount from price info
760+
* Calculate total subscription price amount (approval amount) from price info
761761
* e.g: $8 per month * 12 months min billing cycles = $96
762762
*
763763
* @param price - The price info
@@ -772,6 +772,21 @@ export class SubscriptionController extends StaticIntervalPollingController()<
772772
return amount;
773773
}
774774

775+
/**
776+
* Calculate minimum subscription balance amount from price info
777+
*
778+
* @param price - The price info
779+
* @returns The balance amount
780+
*/
781+
#getSubscriptionBalanceAmount(price: ProductPrice) {
782+
// no need to use BigInt since max unitDecimals are always 2 for price
783+
const amount = new BigNumber(price.unitAmount)
784+
.div(10 ** price.unitDecimals)
785+
.multipliedBy(price.minBillingCyclesForBalance)
786+
.toString();
787+
return amount;
788+
}
789+
775790
/**
776791
* Calculate token approve amount from price info
777792
*
@@ -800,6 +815,35 @@ export class SubscriptionController extends StaticIntervalPollingController()<
800815
return tokenAmount.toFixed(0);
801816
}
802817

818+
/**
819+
* Calculate token minimum balance amount from price info
820+
*
821+
* @param price - The price info
822+
* @param tokenPaymentInfo - The token price info
823+
* @returns The token balance amount
824+
*/
825+
getTokenMinimumBalanceAmount(
826+
price: ProductPrice,
827+
tokenPaymentInfo: TokenPaymentInfo,
828+
): string {
829+
const conversionRate =
830+
tokenPaymentInfo.conversionRate[
831+
price.currency as keyof typeof tokenPaymentInfo.conversionRate
832+
];
833+
if (!conversionRate) {
834+
throw new Error('Conversion rate not found');
835+
}
836+
const balanceAmount = new BigNumber(
837+
this.#getSubscriptionBalanceAmount(price),
838+
);
839+
840+
const tokenDecimal = new BigNumber(10).pow(tokenPaymentInfo.decimals);
841+
const tokenAmount = balanceAmount
842+
.multipliedBy(tokenDecimal)
843+
.div(conversionRate);
844+
return tokenAmount.toFixed(0);
845+
}
846+
803847
/**
804848
* Triggers an access token refresh.
805849
*/

packages/subscription-controller/src/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,10 @@ export type ProductPrice = {
171171
/** only usd for now */
172172
currency: Currency;
173173
trialPeriodDays: number;
174+
/** min billing cycles for approval */
174175
minBillingCycles: number;
176+
/** min billing cycles for account balance check */
177+
minBillingCyclesForBalance: number;
175178
};
176179

177180
export type ProductPricing = {

0 commit comments

Comments
 (0)