Skip to content

Commit

Permalink
Continued initial development, 2... (#14)
Browse files Browse the repository at this point in the history
* add adr

* remove demo project

* init stripe customer configs on backend boot-up

* fetch theme from stripe for all pages
  • Loading branch information
kasparkallas authored Nov 2, 2023
1 parent d62edcb commit 20e2c6a
Show file tree
Hide file tree
Showing 41 changed files with 823 additions and 3,574 deletions.
1 change: 1 addition & 0 deletions apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ QUEUE_DASHBOARD_USER=user
QUEUE_DASHBOARD_PASSWORD=password
INTERNAL_API_KEY=
STRIPE_SECRET_KEY=
STRIPE_TEST_MODE=true
PORT=3001
2 changes: 2 additions & 0 deletions apps/backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
"clean": "npm-run-all -s clean:*",
"clean:dist": "rimraf \"./dist\"",
"clean:node-modules": "rimraf \"./node_modules\"",
"generate": "npm-run-all -s generate:*",
"generate:accounting-openapi-client": "pnpm exec openapi-typescript https://accounting.superfluid.dev/static/api-docs.yaml -o ./src/super-token-accounting/client/types.d.ts"
},
"jest": {
Expand Down Expand Up @@ -55,6 +56,7 @@
"@nestjs/platform-express": "^10.2.7",
"@nestjs/swagger": "^7.1.14",
"@nestjs/terminus": "^10.1.1",
"@superfluid-finance/tokenlist": "^3.3.2",
"@superfluid-finance/widget": "^0.4.7",
"@types/lodash": "^4.14.200",
"bullmq": "^4.12.6",
Expand Down
3 changes: 3 additions & 0 deletions apps/backend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
import { tierASuperTokenList } from '@superfluid-finance/tokenlist';

import Stripe from 'stripe';

export const CUSTOMER_EMAIL = '[email protected]'; // This is always the key for finding the customers.

// The customer names are used for finding the configuration with right type.
export const LOOK_AND_FEEL_CUSTOMER_NAME = 'Superfluid ♥ Stripe: Look and Feel';
export const BLOCKCHAIN_CUSTOMER_NAME = 'Superfluid ♥ Stripe: Blockchain';

// This is the default customer for look and feel to bootstrap the integration.
export 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"}}}`,
},
} as const satisfies Stripe.CustomerCreateParams;

const liveCurrencyTokenSymbols = {
usd: ['USDCx', 'USDTx', 'DAIx', 'cUSDx', 'G$', 'mUSDx'],
eur: ['cEURx', 'EUROex', 'EURSx', 'agEURx', 'jEURx', 'EURex'],
cad: ['jCADx'],
bgn: ['jBGNx'],
chf: ['jXOFx'],
php: ['jPHPx'],
xaf: ['jXAFx'],
sgd: ['jSGDx'],
jpy: ['JPYCx'],
};

// This is the default customer for on-chain settings.
export const createDefaultBlockChainCustomer = (testMode: boolean): Stripe.CustomerCreateParams => {
return {
email: CUSTOMER_EMAIL,
name: BLOCKCHAIN_CUSTOMER_NAME,
description: 'Auto-generated fake customer for Superfluid integration.',
metadata: {
chain_5_usd_token: '0x8ae68021f6170e5a766be613cea0d75236ecca9a',
chain_5_receiver: '0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045',
default_receiver: '',
},
};
};

// This is the default customer for on-chain settings.
export const DEFAULT_LIVE_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...',
default_receiver: '',
},
} as const satisfies Stripe.CustomerCreateParams;

export 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.`
// }
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,128 +3,111 @@ import { Injectable, Logger } from '@nestjs/common';
import { DEFAULT_PAGING } from 'src/stripe-module-config';
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 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"}}}`,
},
} 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: '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.`
// }
import { Block, isAddress } from 'viem';
import {
BLOCKCHAIN_CUSTOMER_NAME,
CUSTOMER_EMAIL,
DEFAULT_LOOK_AND_FEEL_CUSTOMER,
FIRST_TIME_EXAMPLE_PRODUCT,
LOOK_AND_FEEL_CUSTOMER_NAME,
createDefaultBlockChainCustomer,
} from './stripe-entities';
import { ConfigService } from '@nestjs/config';

export type LookAndFeelConfig = {
theme: any; // TODO(KK): any
};

export type IntegrationConfig = {
export type BlockchainConfig = {
version: string;
chains: ReadonlyArray<ChainConfig>;
theme: any; // TODO(KK): any
// lookAndFeel: Record<string, any>;
};

export type CompleteConfig = LookAndFeelConfig & BlockchainConfig;

interface GlobalConfigCustomerManager {
loadConfig(): Promise<IntegrationConfig>;
loadOrInitializeCompleteConfig(): Promise<CompleteConfig>;
loadOrInitializeLookAndFeelConfig(): Promise<LookAndFeelConfig>;
loadOrInitializeBlockchainConfig(): Promise<BlockchainConfig>;
}

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

async loadConfig(): Promise<IntegrationConfig> {
// TODO: caching
// TODO: use better constants
constructor(
@InjectStripeClient() private readonly stripeClient: Stripe,
private readonly configService: ConfigService,
) {
this.stripeTestMode = this.configService.get('STRIPE_TEST_MODE') === 'true';
}

let configurationCustomer: Stripe.Customer;
async loadOrInitializeLookAndFeelConfig(): Promise<LookAndFeelConfig> {
const { lookAndFeelCustomer: lookAndFeelCustomer_ } = await this.loadCustomers();
const 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);
// TODO: use Zod for validation?
// TODO: get rid of any
const theme = JSON.parse(lookAndFeelCustomer.metadata.theme);

let blockchainCustomer = customers.find((x) => x.name === BLOCKCHAIN_CUSTOMER_NAME);
let lookAndFeelCustomer = customers.find((x) => x.name === LOOK_AND_FEEL_CUSTOMER_NAME);
return {
theme,
};
}

const isFirstTimeUsage = !blockchainCustomer && !lookAndFeelCustomer;
if (isFirstTimeUsage) {
await this.stripeClient.products.create(FIRST_TIME_EXAMPLE_PRODUCT);
}
async loadOrInitializeBlockchainConfig(): Promise<BlockchainConfig> {
const { blockchainCustomer: blockchainCustomer_ } = await this.loadCustomers();
const blockchainCustomer =
blockchainCustomer_ ??
(await this.stripeClient.customers.create(
createDefaultBlockChainCustomer(this.stripeTestMode),
));

if (!blockchainCustomer) {
blockchainCustomer = await this.stripeClient.customers.create(DEFAULT_BLOCKCHAIN_CUSTOMER);
}
const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata);

if (!lookAndFeelCustomer) {
lookAndFeelCustomer = await this.stripeClient.customers.create(
DEFAULT_LOOK_AND_FEEL_CUSTOMER,
);
}
return {
version: '1.0.0',
chains: chainConfigs,
};
}

const chainConfigs = mapBlockchainCustomerMetadataIntoChainConfigs(blockchainCustomer.metadata);
async loadOrInitializeCompleteConfig(): Promise<CompleteConfig> {
const { lookAndFeelCustomer, blockchainCustomer } = await this.loadCustomers();

// 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 = !lookAndFeelCustomer && !blockchainCustomer;
if (this.stripeTestMode) {
if (isFirstTimeUsage) {
await this.stripeClient.products.create(FIRST_TIME_EXAMPLE_PRODUCT);
}
}

const mappedResult: IntegrationConfig = {
version: '1.0.0',
chains: chainConfigs,
theme,
const lookAndFeelConfig = await this.loadOrInitializeLookAndFeelConfig();
const blockchainConfig = await this.loadOrInitializeBlockchainConfig();

return {
...lookAndFeelConfig,
...blockchainConfig,
};
}

private async loadCustomers(): Promise<{
blockchainCustomer: Stripe.Customer | undefined;
lookAndFeelCustomer: Stripe.Customer | undefined;
}> {
// TODO: cache

return mappedResult;
const customers = await this.stripeClient.customers
.list({
email: CUSTOMER_EMAIL,
})
.autoPagingToArray(DEFAULT_PAGING);

return {
blockchainCustomer: customers.find((x) => x.name === BLOCKCHAIN_CUSTOMER_NAME),
lookAndFeelCustomer: customers.find((x) => x.name === LOOK_AND_FEEL_CUSTOMER_NAME),
};
}
}

Expand Down
Loading

0 comments on commit 20e2c6a

Please sign in to comment.