Skip to content

Commit

Permalink
create example project on first-time initialization
Browse files Browse the repository at this point in the history
  • Loading branch information
kasparkallas authored Nov 1, 2023
1 parent 28663cf commit d62edcb
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 99 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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];
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
}
Expand Down
10 changes: 4 additions & 6 deletions apps/backend/src/queue-dashboard/basic-auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ export class BasicAuthMiddleware implements NestMiddleware {
private readonly encodedCredentials: ReadonlyArray<string>;

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];
}
}

Expand All @@ -31,6 +31,4 @@ export class BasicAuthMiddleware implements NestMiddleware {
}
}

const base64Encode = (value: string) => Buffer.from(value).toString(
'base64',
)
const base64Encode = (value: string) => Buffer.from(value).toString('base64');
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { Address, ChainId } from './basic-types';

export type ChainToSuperTokenReceiverMap = Map<ChainId, Address>;
export type ChainToSuperTokenReceiverMap = Map<ChainId, Address>;
Original file line number Diff line number Diff line change
Expand Up @@ -5,130 +5,166 @@ import Stripe from 'stripe';
import { Address, ChainId, StripeCurrencyKey } from './basic-types';
import { isAddress } from 'viem';

const CUSTOMER_EMAIL = "[email protected]"; // 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 = '[email protected]'; // 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<ChainConfig>;
theme: any // TODO(KK): any
theme: any; // TODO(KK): any
// lookAndFeel: Record<string, any>;
}
};

interface GlobalConfigCustomerManager {
loadConfig(): Promise<IntegrationConfig>;
}

@Injectable()
export class SuperfluidStripeConfigService implements GlobalConfigCustomerManager {
constructor(@InjectStripeClient() private readonly stripeClient: Stripe) {}

async loadConfig(): Promise<IntegrationConfig> {
// 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<IntegrationConfig> {
// 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<string, string>): ChainConfig[] => {
const mapBlockchainCustomerMetadataIntoChainConfigs = (
metadata: Record<string, string>,
): 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
Expand All @@ -137,11 +173,11 @@ const mapBlockchainCustomerMetadataIntoChainConfigs = (metadata: Record<string,
if (isAddress(superTokenAddress)) {
chainConfigs.push({ chainId, currency, superTokenAddress, receiverAddress });
}
}
}
}
}

return chainConfigs;
}
};

const logger = new Logger(SuperfluidStripeConfigService.name);
const logger = new Logger(SuperfluidStripeConfigService.name);
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ export class SuperfluidStripeConverterController {
const config = await this.superfluidStripeConverterService.mapStripeProductToWidgetConfig({
product: stripeProduct,
prices: pricesForProduct,
integrationConfig
integrationConfig,
});

return { ...config, stripeProduct };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ import Stripe from 'stripe';
import { ChainId, PaymentOption, ProductDetails, WidgetProps } from '@superfluid-finance/widget';
import { currencyDecimalMapping } from 'src/stripe-currencies';
import { Address, formatUnits } from 'viem';
import { IntegrationConfig, SuperfluidStripeConfigService } from './superfluid-stripe-config/superfluid-stripe-config.service';
import {
IntegrationConfig,
SuperfluidStripeConfigService,
} from './superfluid-stripe-config/superfluid-stripe-config.service';
import { StripeCurrencyKey } from './superfluid-stripe-config/basic-types';

type Input = {
Expand Down Expand Up @@ -31,9 +34,7 @@ interface SuperTokenToStripeCurrencyMapper {

@Injectable()
export class SuperfluidStripeConverterService
implements
StripeProductToWidgetConfigMapper,
SuperTokenToStripeCurrencyMapper
implements StripeProductToWidgetConfigMapper, SuperTokenToStripeCurrencyMapper
{
constructor(private readonly stripeConfigService: SuperfluidStripeConfigService) {}

Expand All @@ -42,9 +43,12 @@ export class SuperfluidStripeConverterService
address: string;
}): Promise<StripeCurrencyKey | undefined> {
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;
Expand All @@ -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

Expand Down
2 changes: 2 additions & 0 deletions apps/frontend/src/components/WagmiProvider.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",

Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/internalConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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}`);
Expand Down

0 comments on commit d62edcb

Please sign in to comment.