Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ export {
DEFAULT_GET_SUBSCRIPTION_STATUS_CODE,
DEFAULT_SUBSCRIBE_CODE,
GET_SUBSCRIPTION_STATUS_QUICK_TIPS,
SUBSCRIBE_CODE_WITH_MINIMUM_BALANCE_FALSE,
SUBSCRIBE_CODE_WITH_TEST_PERIOD,
SUBSCRIBE_QUICK_TIPS,
} from './playground';
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,23 @@ try {
throw error;
}`;

export const SUBSCRIBE_CODE_WITH_MINIMUM_BALANCE_FALSE = `import { base } from '@base-org/account'

try {
const subscription = await base.subscription.subscribe({
recurringCharge: "10.50",
subscriptionOwner: "0xFe21034794A5a574B94fE4fDfD16e005F1C96e51", // Your app's address
periodInDays: 30,
minimumBalance: false, // Don't require minimum balance check
testnet: true
})

return subscription;
} catch (error) {
console.error('Subscription failed:', error.message);
throw error;
}`;

export const DEFAULT_GET_SUBSCRIPTION_STATUS_CODE = `import { base } from '@base-org/account'

try {
Expand Down
19 changes: 17 additions & 2 deletions examples/testapp/src/pages/subscribe-playground/index.page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,17 @@ import {
DEFAULT_GET_SUBSCRIPTION_STATUS_CODE,
DEFAULT_SUBSCRIBE_CODE,
GET_SUBSCRIPTION_STATUS_QUICK_TIPS,
SUBSCRIBE_CODE_WITH_MINIMUM_BALANCE_FALSE,
SUBSCRIBE_CODE_WITH_TEST_PERIOD,
SUBSCRIBE_QUICK_TIPS,
} from './constants';
import { useCodeExecution } from './hooks';
import styles from './styles/Home.module.css';

function SubscribePlayground() {
const [subscribeVariant, setSubscribeVariant] = useState<'default' | 'test'>('default');
const [subscribeVariant, setSubscribeVariant] = useState<'default' | 'test' | 'minimumBalance'>(
'default'
);
const [subscribeCode, setSubscribeCode] = useState(DEFAULT_SUBSCRIBE_CODE);
const [getSubscriptionStatusCode, setGetSubscriptionStatusCode] = useState(
DEFAULT_GET_SUBSCRIPTION_STATUS_CODE
Expand All @@ -30,11 +33,13 @@ function SubscribePlayground() {
subscribeExecution.reset();
};

const handleSubscribeVariantChange = (variant: 'default' | 'test') => {
const handleSubscribeVariantChange = (variant: 'default' | 'test' | 'minimumBalance') => {
setSubscribeVariant(variant);
let newCode = DEFAULT_SUBSCRIBE_CODE;
if (variant === 'test') {
newCode = SUBSCRIBE_CODE_WITH_TEST_PERIOD;
} else if (variant === 'minimumBalance') {
newCode = SUBSCRIBE_CODE_WITH_MINIMUM_BALANCE_FALSE;
}
setSubscribeCode(newCode);
subscribeExecution.reset();
Expand Down Expand Up @@ -105,6 +110,16 @@ try {
/>
Test Mode (5-minute period)
</label>
<label>
<input
type="radio"
name="subscribeVariant"
value="minimumBalance"
checked={subscribeVariant === 'minimumBalance'}
onChange={() => handleSubscribeVariantChange('minimumBalance')}
/>
No Balance Check
</label>
</div>

<div className={styles.playground}>
Expand Down
160 changes: 160 additions & 0 deletions packages/account-sdk/src/interface/payment/subscribe.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,166 @@ vi.mock('../public-utilities/spend-permission/index.js', () => ({
getHash: vi.fn(() => Promise.resolve('0xmockhash')),
}));

describe('subscribe with requireBalance capability', () => {
it('should include capabilities when requireBalance is true', async () => {
const options: SubscriptionOptions = {
recurringCharge: '10.00',
subscriptionOwner: '0x1234567890123456789012345678901234567890',
periodInDays: 30,
testnet: true,
requireBalance: true, // Enable balance check
};

// Mock the provider response
const mockProvider = {
request: vi.fn().mockResolvedValue({
signature: '0xsignature',
signedData: {
message: {
account: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
spender: '0x1234567890123456789012345678901234567890',
token: '0xtoken',
allowance: '10000000',
period: 2592000, // 30 days in seconds
start: 1234567890,
end: 999999999,
salt: '0xsalt',
extraData: '0x',
},
},
}),
disconnect: vi.fn(),
};

const { createEphemeralSDK } = await import('./utils/sdkManager.js');
vi.mocked(createEphemeralSDK).mockReturnValue({
getProvider: () => mockProvider as any,
} as any);

await subscribe(options);

// Verify wallet_sign was called with capabilities
expect(mockProvider.request).toHaveBeenCalledWith({
method: 'wallet_sign',
params: [
expect.objectContaining({
version: '1.0',
request: expect.any(Object),
mutableData: expect.any(Object),
capabilities: {
spendPermissions: {
requireBalance: true,
},
},
}),
],
});
});

it('should not include capabilities when requireBalance is false', async () => {
const options: SubscriptionOptions = {
recurringCharge: '10.00',
subscriptionOwner: '0x1234567890123456789012345678901234567890',
periodInDays: 30,
testnet: true,
requireBalance: false, // Explicitly disable
};

// Mock the provider response
const mockProvider = {
request: vi.fn().mockResolvedValue({
signature: '0xsignature',
signedData: {
message: {
account: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
spender: '0x1234567890123456789012345678901234567890',
token: '0xtoken',
allowance: '10000000',
period: 2592000, // 30 days in seconds
start: 1234567890,
end: 999999999,
salt: '0xsalt',
extraData: '0x',
},
},
}),
disconnect: vi.fn(),
};

const { createEphemeralSDK } = await import('./utils/sdkManager.js');
vi.mocked(createEphemeralSDK).mockReturnValue({
getProvider: () => mockProvider as any,
} as any);

await subscribe(options);

// Verify wallet_sign was called without capabilities
expect(mockProvider.request).toHaveBeenCalledWith({
method: 'wallet_sign',
params: [
expect.not.objectContaining({
capabilities: expect.anything(),
}),
],
});
});

it('should include capabilities by default when requireBalance is undefined', async () => {
const options: SubscriptionOptions = {
recurringCharge: '10.00',
subscriptionOwner: '0x1234567890123456789012345678901234567890',
periodInDays: 30,
testnet: true,
// requireBalance not specified - should default to true
};

// Mock the provider response
const mockProvider = {
request: vi.fn().mockResolvedValue({
signature: '0xsignature',
signedData: {
message: {
account: '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA',
spender: '0x1234567890123456789012345678901234567890',
token: '0xtoken',
allowance: '10000000',
period: 2592000, // 30 days in seconds
start: 1234567890,
end: 999999999,
salt: '0xsalt',
extraData: '0x',
},
},
}),
disconnect: vi.fn(),
};

const { createEphemeralSDK } = await import('./utils/sdkManager.js');
vi.mocked(createEphemeralSDK).mockReturnValue({
getProvider: () => mockProvider as any,
} as any);

await subscribe(options);

// Verify wallet_sign was called with capabilities (default behavior)
expect(mockProvider.request).toHaveBeenCalledWith({
method: 'wallet_sign',
params: [
expect.objectContaining({
version: '1.0',
request: expect.any(Object),
mutableData: expect.any(Object),
capabilities: {
spendPermissions: {
requireBalance: true,
},
},
}),
],
});
});
});

describe('subscribe with overridePeriodInSecondsForTestnet', () => {
it('should throw error when overridePeriodInSecondsForTestnet is used without testnet', async () => {
const options = {
Expand Down
8 changes: 8 additions & 0 deletions packages/account-sdk/src/interface/payment/subscribe.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ const PLACEHOLDER_ADDRESS = '0xAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' as cons
* @param options.testnet - Whether to use Base Sepolia testnet (default: false)
* @param options.walletUrl - Optional wallet URL to use
* @param options.telemetry - Whether to enable telemetry logging (default: true)
* @param options.requireBalance - Whether to require the user has sufficient balance before creating the subscription (default: true)
* @returns Promise<SubscriptionResult> - Simplified result with subscription details
* @throws Error if the subscription fails
*
Expand Down Expand Up @@ -78,6 +79,7 @@ export async function subscribe(options: SubscriptionOptions): Promise<Subscript
testnet = false,
walletUrl,
telemetry = true,
requireBalance = true,
} = options;

// Check if overridePeriodInSecondsForTestnet is present in options
Expand Down Expand Up @@ -155,6 +157,11 @@ export async function subscribe(options: SubscriptionOptions): Promise<Subscript
const provider = sdk.getProvider();

try {
// Build capabilities if requireBalance is set
const capabilities = requireBalance
? { spendPermissions: { requireBalance: true } }
: undefined;

// Define the wallet_sign parameters with mutable data
// This allows the wallet to replace PLACEHOLDER_ADDRESS with the actual account
const signParams = {
Expand All @@ -166,6 +173,7 @@ export async function subscribe(options: SubscriptionOptions): Promise<Subscript
mutableData: {
fields: ['message.account'],
},
...(capabilities && { capabilities }),
};

// Request signature from wallet
Expand Down
2 changes: 2 additions & 0 deletions packages/account-sdk/src/interface/payment/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,8 @@ interface BaseSubscriptionOptions {
walletUrl?: string;
/** Whether to enable telemetry logging. Defaults to true */
telemetry?: boolean;
/** Whether to require the user has sufficient balance before creating the subscription. Defaults to true */
requireBalance?: boolean;
}

/**
Expand Down
Loading