-
Notifications
You must be signed in to change notification settings - Fork 66
Add getSubscriptionStatus method #110
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
9 commits
Select commit
Hold shift + click to select a range
89cddb5
getSubscriptionStatus
spencerstock 981fb71
getSubscriptionStatus
spencerstock 47e57f9
fix: lint errors - remove unnecessary try-catch and fix unused variables
spencerstock 1cf0146
fix: apply formatting
spencerstock 9bda9bb
fix: update calculateCurrentPeriod logic and test mocks
spencerstock 4dd60ed
adjust new behavior
spencerstock 3f64a95
remove unnecessary try catch
spencerstock f558d4b
self review
spencerstock 44f34ab
prepareCharge
spencerstock File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
169 changes: 169 additions & 0 deletions
169
packages/account-sdk/src/interface/payment/getSubscriptionStatus.ts
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,169 @@ | ||
| import { formatUnits } from 'viem'; | ||
| import { readContract } from 'viem/actions'; | ||
| import { | ||
| spendPermissionManagerAbi, | ||
| spendPermissionManagerAddress, | ||
| } from '../../sign/base-account/utils/constants.js'; | ||
| import { createClients, FALLBACK_CHAINS, getClient } from '../../store/chain-clients/utils.js'; | ||
| import { | ||
| fetchPermission, | ||
| getPermissionStatus, | ||
| } from '../public-utilities/spend-permission/index.js'; | ||
| import { | ||
| calculateCurrentPeriod, | ||
| timestampInSecondsToDate, | ||
| toSpendPermissionArgs, | ||
| } from '../public-utilities/spend-permission/utils.js'; | ||
| import { CHAIN_IDS, TOKENS } from './constants.js'; | ||
| import type { SubscriptionStatus, SubscriptionStatusOptions } from './types.js'; | ||
|
|
||
| /** | ||
| * Gets the current status and details of a subscription. | ||
| * | ||
| * This function fetches the subscription (spend permission) details using its ID (permission hash) | ||
| * and returns status information about the subscription. If there's no on-chain state for the | ||
| * subscription (e.g., it has never been used), the function will infer that the subscription | ||
| * is unrevoked and the full recurring amount is available to spend. | ||
| * | ||
| * @param options - Options for checking subscription status | ||
| * @param options.id - The subscription ID (permission hash) returned from subscribe() | ||
| * @param options.testnet - Whether to check on testnet (Base Sepolia). Defaults to false (mainnet) | ||
| * @returns Promise<SubscriptionStatus> - Subscription status information | ||
| * @throws Error if the subscription cannot be found or if fetching fails | ||
| * | ||
| * @example | ||
| * ```typescript | ||
| * import { getSubscriptionStatus } from '@base-org/account/payment'; | ||
| * | ||
| * // Check status of a subscription using its ID | ||
| * const status = await getSubscriptionStatus({ | ||
| * id: '0x71319cd488f8e4f24687711ec5c95d9e0c1bacbf5c1064942937eba4c7cf2984', | ||
| * testnet: false | ||
| * }); | ||
| * | ||
| * console.log(`Subscribed: ${status.isSubscribed}`); | ||
| * console.log(`Next payment: ${status.nextPeriodStart}`); | ||
| * console.log(`Recurring amount: $${status.recurringAmount}`); | ||
| * ``` | ||
| */ | ||
| export async function getSubscriptionStatus( | ||
| options: SubscriptionStatusOptions | ||
| ): Promise<SubscriptionStatus> { | ||
| const { id, testnet = false } = options; | ||
|
|
||
| // First, try to fetch the permission details using the hash | ||
| const permission = await fetchPermission({ | ||
| permissionHash: id, | ||
| }); | ||
|
|
||
| // If no permission found in the indexer, return as not subscribed | ||
| if (!permission) { | ||
| // No permission found - the subscription doesn't exist or cannot be found | ||
| return { | ||
| isSubscribed: false, | ||
| recurringCharge: '0', | ||
| }; | ||
| } | ||
|
|
||
| // Validate this is a USDC permission on Base/Base Sepolia | ||
| const expectedChainId = testnet ? CHAIN_IDS.baseSepolia : CHAIN_IDS.base; | ||
| const expectedTokenAddress = testnet | ||
| ? TOKENS.USDC.addresses.baseSepolia.toLowerCase() | ||
| : TOKENS.USDC.addresses.base.toLowerCase(); | ||
|
|
||
| if (permission.chainId !== expectedChainId) { | ||
| // Determine if the subscription is on mainnet or testnet | ||
| const isSubscriptionOnMainnet = permission.chainId === CHAIN_IDS.base; | ||
| const isSubscriptionOnTestnet = permission.chainId === CHAIN_IDS.baseSepolia; | ||
|
|
||
| let errorMessage: string; | ||
| if (testnet && isSubscriptionOnMainnet) { | ||
| errorMessage = | ||
| 'The subscription was requested on testnet but is actually a mainnet subscription'; | ||
| } else if (!testnet && isSubscriptionOnTestnet) { | ||
| errorMessage = | ||
| 'The subscription was requested on mainnet but is actually a testnet subscription'; | ||
| } else { | ||
| // Fallback for unexpected chain IDs | ||
| errorMessage = `Subscription is on chain ${permission.chainId}, expected ${expectedChainId} (${testnet ? 'Base Sepolia' : 'Base'})`; | ||
| } | ||
|
|
||
| throw new Error(errorMessage); | ||
| } | ||
|
|
||
| if (permission.permission.token.toLowerCase() !== expectedTokenAddress) { | ||
| throw new Error( | ||
| `Subscription is not for USDC token. Got ${permission.permission.token}, expected ${expectedTokenAddress}` | ||
| ); | ||
| } | ||
|
|
||
| // Ensure chain client is initialized for the permission's chain | ||
| if (permission.chainId && !getClient(permission.chainId)) { | ||
| const fallbackChain = FALLBACK_CHAINS.find((chain) => chain.id === permission.chainId); | ||
| if (fallbackChain) { | ||
| createClients([fallbackChain]); | ||
| } | ||
| } | ||
|
|
||
| // Get the current permission status (includes period info and active state) | ||
| const status = await getPermissionStatus(permission); | ||
|
|
||
| // Get the current period info directly to get spend amount | ||
| let currentPeriod: { start: number; end: number; spend: bigint }; | ||
|
|
||
| const client = getClient(permission.chainId!); | ||
| if (client) { | ||
| try { | ||
| const spendPermissionArgs = toSpendPermissionArgs(permission); | ||
| currentPeriod = (await readContract(client, { | ||
| address: spendPermissionManagerAddress, | ||
| abi: spendPermissionManagerAbi, | ||
| functionName: 'getCurrentPeriod', | ||
| args: [spendPermissionArgs], | ||
| })) as { start: number; end: number; spend: bigint }; | ||
| } catch { | ||
| // If we can't read on-chain state, calculate from permission parameters | ||
| currentPeriod = calculateCurrentPeriod(permission); | ||
| } | ||
| } else { | ||
| // No client available, calculate from permission parameters | ||
| currentPeriod = calculateCurrentPeriod(permission); | ||
| } | ||
|
|
||
| // Format the allowance amount from wei to USD string (USDC has 6 decimals) | ||
| const recurringCharge = formatUnits(BigInt(permission.permission.allowance), 6); | ||
|
|
||
| // Calculate period in days from the period duration in seconds | ||
| const periodInDays = Number(permission.permission.period) / 86400; | ||
|
|
||
| // Check if the subscription period has started | ||
| const currentTime = Math.floor(Date.now() / 1000); | ||
| const permissionStart = Number(permission.permission.start); | ||
| const permissionEnd = Number(permission.permission.end); | ||
|
|
||
| if (currentTime < permissionStart) { | ||
| throw new Error( | ||
| `Subscription has not started yet. It will begin at ${new Date(permissionStart * 1000).toISOString()}` | ||
| ); | ||
| } | ||
|
|
||
| // Check if the subscription has expired | ||
| const hasNotExpired = currentTime <= permissionEnd; | ||
|
|
||
| // A subscription is considered active if we're within the valid time bounds | ||
| // and the permission hasn't been revoked. | ||
| const hasNoOnChainState = currentPeriod.spend === BigInt(0); | ||
| const isSubscribed = hasNotExpired && (status.isActive || hasNoOnChainState); | ||
|
|
||
| // Build the result with data from getCurrentPeriod and other on-chain functions | ||
| const result: SubscriptionStatus = { | ||
| isSubscribed, | ||
| recurringCharge, | ||
| remainingChargeInPeriod: formatUnits(status.remainingSpend, 6), | ||
| currentPeriodStart: timestampInSecondsToDate(currentPeriod.start), | ||
| nextPeriodStart: status.nextPeriodStart, | ||
| periodInDays, | ||
| }; | ||
|
|
||
| return result; | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I noticed there's no tests for this, are there plans to add any?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
will add!