diff --git a/apps/backend/src/checkout-session/checkout-session.controller.ts b/apps/backend/src/checkout-session/checkout-session.controller.ts index 0806ef4..31330c4 100644 --- a/apps/backend/src/checkout-session/checkout-session.controller.ts +++ b/apps/backend/src/checkout-session/checkout-session.controller.ts @@ -50,9 +50,9 @@ export class CheckoutSessionController { const internalApiKey = configService.get('INTERNAL_API_KEY'); const stripeSecretKey = configService.getOrThrow('STRIPE_SECRET_KEY'); if (internalApiKey) { - this.apiKeys = [internalApiKey, stripeSecretKey] + this.apiKeys = [internalApiKey, stripeSecretKey]; } else { - this.apiKeys = [stripeSecretKey] + this.apiKeys = [stripeSecretKey]; } } diff --git a/apps/backend/src/checkout-session/checkout-session.processer.ts b/apps/backend/src/checkout-session/checkout-session.processer.ts index c2ecd06..0d2f072 100644 --- a/apps/backend/src/checkout-session/checkout-session.processer.ts +++ b/apps/backend/src/checkout-session/checkout-session.processer.ts @@ -61,7 +61,7 @@ export class CheckoutSessionProcesser extends WorkerHost { constructor( @InjectQueue(QUEUE_NAME) private readonly queue: Queue, @InjectStripeClient() private readonly stripeClient: Stripe, - private readonly converterService: SuperfluidStripeConverterService + private readonly converterService: SuperfluidStripeConverterService, ) { super(); } diff --git a/apps/backend/src/queue-dashboard/basic-auth.middleware.ts b/apps/backend/src/queue-dashboard/basic-auth.middleware.ts index 297c021..f268874 100644 --- a/apps/backend/src/queue-dashboard/basic-auth.middleware.ts +++ b/apps/backend/src/queue-dashboard/basic-auth.middleware.ts @@ -7,16 +7,16 @@ export class BasicAuthMiddleware implements NestMiddleware { private readonly encodedCredentials: ReadonlyArray; constructor(configService: ConfigService) { - const stripeSecretKey = configService.getOrThrow('STRIPE_SECRET_KEY') + const stripeSecretKey = configService.getOrThrow('STRIPE_SECRET_KEY'); const stripeSecretKeyEncodedCredentials = base64Encode(`${stripeSecretKey}:`); if (configService.get('QUEUE_DASHBOARD_USER')) { const username = configService.getOrThrow('QUEUE_DASHBOARD_USER'); const password = configService.getOrThrow('QUEUE_DASHBOARD_PASSWORD'); const encodedCredentials = base64Encode(username + ':' + password); - this.encodedCredentials = [ encodedCredentials, stripeSecretKeyEncodedCredentials] + this.encodedCredentials = [encodedCredentials, stripeSecretKeyEncodedCredentials]; } else { - this.encodedCredentials = [ stripeSecretKeyEncodedCredentials ] + this.encodedCredentials = [stripeSecretKeyEncodedCredentials]; } } @@ -31,6 +31,4 @@ export class BasicAuthMiddleware implements NestMiddleware { } } -const base64Encode = (value: string) => Buffer.from(value).toString( - 'base64', -) \ No newline at end of file +const base64Encode = (value: string) => Buffer.from(value).toString('base64'); diff --git a/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/chain-to-super-token-receiver-map.ts b/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/chain-to-super-token-receiver-map.ts index 0c47a10..245f417 100644 --- a/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/chain-to-super-token-receiver-map.ts +++ b/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/chain-to-super-token-receiver-map.ts @@ -1,3 +1,3 @@ import { Address, ChainId } from './basic-types'; -export type ChainToSuperTokenReceiverMap = Map; \ No newline at end of file +export type ChainToSuperTokenReceiverMap = Map; diff --git a/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/superfluid-stripe-config.service.ts b/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/superfluid-stripe-config.service.ts index 8de36ed..512ae9f 100644 --- a/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/superfluid-stripe-config.service.ts +++ b/apps/backend/src/superfluid-stripe-converter/superfluid-stripe-config/superfluid-stripe-config.service.ts @@ -5,50 +5,69 @@ import Stripe from 'stripe'; import { Address, ChainId, StripeCurrencyKey } from './basic-types'; import { isAddress } from 'viem'; -const CUSTOMER_EMAIL = "stripe@superfluid.finance"; // This is the key for finding the customer. -const LOOK_AND_FEEL_CUSTOMER_NAME = "Superfluid ♥ Stripe: Look and Feel"; -const BLOCKCHAIN_CUSTOMER_NAME = "Superfluid ♥ Stripe: Blockchain"; +const CUSTOMER_EMAIL = 'stripe@superfluid.finance'; // This is the key for finding the customer. +const LOOK_AND_FEEL_CUSTOMER_NAME = 'Superfluid ♥ Stripe: Look and Feel'; +const BLOCKCHAIN_CUSTOMER_NAME = 'Superfluid ♥ Stripe: Blockchain'; const DEFAULT_LOOK_AND_FEEL_CUSTOMER = { email: CUSTOMER_EMAIL, name: LOOK_AND_FEEL_CUSTOMER_NAME, description: 'Auto-generated fake customer for Superfluid integration.', - metadata: - { - theme: `{"palette":{"mode":"light","primary":{"main":"#3f51b5"},"secondary":{"main":"#f50057"}}}`, - } + metadata: { + theme: `{"palette":{"mode":"light","primary":{"main":"#3f51b5"},"secondary":{"main":"#f50057"}}}`, + }, } as const satisfies Stripe.CustomerCreateParams; const DEFAULT_BLOCKCHAIN_CUSTOMER = { email: CUSTOMER_EMAIL, name: BLOCKCHAIN_CUSTOMER_NAME, description: 'Auto-generated fake customer for Superfluid integration.', // TODO(KK): Add documentation here. - metadata: - { - "chain_43114_usd_token": "0x288398f314d472b82c44855f3f6ff20b633c2a97", - "chain_43114_receiver": "0x...", - "chain_42161_usd_token": "0x1dbc1809486460dcd189b8a15990bca3272ee04e", - "chain_42161_receiver": "0x...", - "chain_100_usd_token": "0x1234756ccf0660e866305289267211823ae86eec", - "chain_100_receiver": "0x...", - "chain_1_usd_token": "0x1ba8603da702602a75a25fca122bb6898b8b1282", - "chain_1_receiver": "0x...", - "chain_10_usd_token": "0x8430f084b939208e2eded1584889c9a66b90562f", - "chain_10_receiver": "0x...", - "chain_137_usd_token": "0xcaa7349cea390f89641fe306d93591f87595dc1f", - "chain_137_receiver": "0x...", - "chain_5_usd_token": "0x8ae68021f6170e5a766be613cea0d75236ecca9a", - "chain_5_receiver": "0x...", - "default_receiver": "", - } + metadata: { + chain_43114_usd_token: '0x288398f314d472b82c44855f3f6ff20b633c2a97', + chain_43114_receiver: '0x...', + chain_42161_usd_token: '0x1dbc1809486460dcd189b8a15990bca3272ee04e', + chain_42161_receiver: '0x...', + chain_100_usd_token: '0x1234756ccf0660e866305289267211823ae86eec', + chain_100_receiver: '0x...', + chain_1_usd_token: '0x1ba8603da702602a75a25fca122bb6898b8b1282', + chain_1_receiver: '0x...', + chain_10_usd_token: '0x8430f084b939208e2eded1584889c9a66b90562f', + chain_10_receiver: '0x...', + chain_137_usd_token: '0xcaa7349cea390f89641fe306d93591f87595dc1f', + chain_137_receiver: '0x...', + chain_5_usd_token: '0x8ae68021f6170e5a766be613cea0d75236ecca9a', + chain_5_receiver: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045', // vitalik.eth + default_receiver: '', + }, } as const satisfies Stripe.CustomerCreateParams; +const FIRST_TIME_EXAMPLE_PRODUCT: Stripe.ProductCreateParams = { + name: 'Example Superfluid Integration Product', + features: [ + { name: 'decentralized' }, + { name: 'pseudoanonymous' }, + { name: 'pay and get paid every second' }, + { name: 'complete control of your money streams' }, + ], + default_price_data: { + currency: 'USD', + recurring: { + interval: 'month', + }, + unit_amount: 500, + }, + + // metadata: { + // superfluid: `The value here does not matter. When "superfluid" metadata key is specified then it is valid for the Superfluid integration.` + // } +}; + export type IntegrationConfig = { version: string; chains: ReadonlyArray; - theme: any // TODO(KK): any + theme: any; // TODO(KK): any // lookAndFeel: Record; -} +}; interface GlobalConfigCustomerManager { loadConfig(): Promise; @@ -56,79 +75,96 @@ interface GlobalConfigCustomerManager { @Injectable() export class SuperfluidStripeConfigService implements GlobalConfigCustomerManager { - constructor(@InjectStripeClient() private readonly stripeClient: Stripe) {} - - async loadConfig(): Promise { - // TODO: caching - // TODO: use better constants - - let configurationCustomer: Stripe.Customer; - - const customers = await this.stripeClient.customers - .list({ - email: CUSTOMER_EMAIL, - }) - .autoPagingToArray(DEFAULT_PAGING); + constructor(@InjectStripeClient() private readonly stripeClient: Stripe) {} - let blockchainCustomer = customers.find(x => x.name === BLOCKCHAIN_CUSTOMER_NAME); - let lookAndFeelCustomer = customers.find(x => x.name === LOOK_AND_FEEL_CUSTOMER_NAME); + async loadConfig(): Promise { + // TODO: caching + // TODO: use better constants - if (!blockchainCustomer) { - blockchainCustomer = await this.stripeClient.customers.create(DEFAULT_BLOCKCHAIN_CUSTOMER); - } + let configurationCustomer: Stripe.Customer; - if (!lookAndFeelCustomer) { - lookAndFeelCustomer = await this.stripeClient.customers.create(DEFAULT_LOOK_AND_FEEL_CUSTOMER); - } + const customers = await this.stripeClient.customers + .list({ + email: CUSTOMER_EMAIL, + }) + .autoPagingToArray(DEFAULT_PAGING); - const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata); + let blockchainCustomer = customers.find((x) => x.name === BLOCKCHAIN_CUSTOMER_NAME); + let lookAndFeelCustomer = customers.find((x) => x.name === LOOK_AND_FEEL_CUSTOMER_NAME); - // TODO: use Zod for validation? - // TODO: get rid of any - let theme: any; - try { - theme = JSON.parse(lookAndFeelCustomer.metadata['theme']); - } catch (e) { - logger.error(e); - } + const isFirstTimeUsage = !blockchainCustomer && !lookAndFeelCustomer; + if (isFirstTimeUsage) { + await this.stripeClient.products.create(FIRST_TIME_EXAMPLE_PRODUCT); + } - const mappedResult: IntegrationConfig = { - version: "1.0.0", - chains: chainConfigs, - theme - } - - return mappedResult; + if (!blockchainCustomer) { + blockchainCustomer = await this.stripeClient.customers.create(DEFAULT_BLOCKCHAIN_CUSTOMER); + } + + if (!lookAndFeelCustomer) { + lookAndFeelCustomer = await this.stripeClient.customers.create( + DEFAULT_LOOK_AND_FEEL_CUSTOMER, + ); } + + const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata); + + // TODO: use Zod for validation? + // TODO: get rid of any + let theme: any; + try { + theme = JSON.parse(lookAndFeelCustomer.metadata['theme']); + } catch (e) { + logger.error(e); + } + + const mappedResult: IntegrationConfig = { + version: '1.0.0', + chains: chainConfigs, + theme, + }; + + return mappedResult; + } } export type ChainConfig = { - chainId: ChainId, - receiverAddress: Address, - currency: StripeCurrencyKey, + chainId: ChainId; + receiverAddress: Address; + currency: StripeCurrencyKey; superTokenAddress: Address; -} +}; -const mapBlockchainCustomerMetadataIntoChainConfigs = (metadata: Record): ChainConfig[] => { +const mapBlockchainCustomerMetadataIntoChainConfigs = ( + metadata: Record, +): ChainConfig[] => { const defaultReceiverAddress = metadata.default_receiver; const chainConfigs: ChainConfig[] = []; - const chainIds = [...new Set(Object.keys(metadata).map(key => { - const match = key.match(/chain_(\d+)/); - return match ? Number(match[1]) : undefined; - }).filter((chainId): chainId is number => chainId !== undefined))]; + const chainIds = [ + ...new Set( + Object.keys(metadata) + .map((key) => { + const match = key.match(/chain_(\d+)/); + return match ? Number(match[1]) : undefined; + }) + .filter((chainId): chainId is number => chainId !== undefined), + ), + ]; for (const chainId of chainIds) { const receiverAddress = metadata[`chain_${chainId}_receiver`] || defaultReceiverAddress; if (!isAddress(receiverAddress)) { continue; } - + // Filter currency-token entries for this chainId - const chainSpecificKeys = Object.keys(metadata).filter(key => key.startsWith(`chain_${chainId}_`)); + const chainSpecificKeys = Object.keys(metadata).filter((key) => + key.startsWith(`chain_${chainId}_`), + ); - for(const key of chainSpecificKeys) { + for (const key of chainSpecificKeys) { const match = key.match(/chain_\d+_(.+)_token/); const currency = match ? match[1] : undefined; // TODO(KK): Validate if Stripe currency @@ -137,11 +173,11 @@ const mapBlockchainCustomerMetadataIntoChainConfigs = (metadata: Record { const stripeConfig = await this.stripeConfigService.loadConfig(); - + const addressLowerCased = superToken.address.toLowerCase(); - const configEntry = stripeConfig.chains.find(x => x.chainId === superToken.chainId && x.superTokenAddress.toLowerCase() === addressLowerCased); + const configEntry = stripeConfig.chains.find( + (x) => + x.chainId === superToken.chainId && x.superTokenAddress.toLowerCase() === addressLowerCased, + ); if (configEntry) { return configEntry.currency; @@ -69,12 +73,11 @@ export class SuperfluidStripeConverterService // Anything else regarding recurring to check here? } - const matchingCurrencyConfigs = stripeConfig.chains.filter(x => x.currency === p.currency); + const matchingCurrencyConfigs = stripeConfig.chains.filter((x) => x.currency === p.currency); if (!matchingCurrencyConfigs) { return; } - const currencyDecimals = currencyDecimalMapping.get(p.currency.toUpperCase()); const amount = formatUnits(BigInt(p.unit_amount!), currencyDecimals!) as `${number}`; // TODO: bangs diff --git a/apps/frontend/src/components/WagmiProvider.tsx b/apps/frontend/src/components/WagmiProvider.tsx index b0df807..d2ff0f8 100644 --- a/apps/frontend/src/components/WagmiProvider.tsx +++ b/apps/frontend/src/components/WagmiProvider.tsx @@ -42,6 +42,8 @@ const wagmiConfig = createConfig( // alchemyId: process.env.ALCHEMY_ID, // or infuraId walletConnectProjectId: publicConfig.walletConnectProjectId, + // TODO(KK): Take these as inputs? + // Required appName: "Your App Name", diff --git a/apps/frontend/src/internalConfig.ts b/apps/frontend/src/internalConfig.ts index 9800e84..031c54e 100644 --- a/apps/frontend/src/internalConfig.ts +++ b/apps/frontend/src/internalConfig.ts @@ -12,7 +12,7 @@ const internalConfig: InternalConfig = { }, getBackendBaseUrl() { const host = ensureDefined(process.env.BACKEND_HOST, 'BACKEND_HOST'); - const protocol = process.env.BACKEND_PROTOCOL ?? "https"; + const protocol = process.env.BACKEND_PROTOCOL ?? 'https'; const port = Number(process.env.BACKEND_PORT); if (port) { return new URL(`${protocol}://${host}:${port}`);