Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
  • Loading branch information
kasparkallas authored Oct 31, 2023
2 parents 9dfcbed + 1d77d20 commit cf8c8ea
Show file tree
Hide file tree
Showing 26 changed files with 302 additions and 217 deletions.
2 changes: 1 addition & 1 deletion .devcontainer/devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": [
"pnpm install",
"pnpm add -g @nestjs/cli", // You can now globally use "nest" commands.
"npm install -g @nestjs/cli", // You can now globally use "nest" commands.
"pnpm docker:redis"
],
"postStartCommand": [
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ To work on this integration, ensure that you have a Stripe account set up with S
#Stripe Secret Key
STRIPE_SECRET_KEY=""
# Strip API Key
API_KEY=""
INTERNAL_API_KEY=""
#Redis queue user
QUEUE_DASHBOARD_USER=
#Redis queue password
Expand Down
2 changes: 1 addition & 1 deletion apps/backend/.env.example
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,6 @@ REDIS_USER=
REDIS_PASSWORD=
QUEUE_DASHBOARD_USER=user
QUEUE_DASHBOARD_PASSWORD=password
API_KEY=
INTERNAL_API_KEY=
STRIPE_SECRET_KEY=
PORT=3001
12 changes: 9 additions & 3 deletions apps/backend/src/checkout-session/checkout-session.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,21 +41,27 @@ export class CreateSessionData {

@Controller('checkout-session')
export class CheckoutSessionController {
private readonly apiKey: string;
private readonly apiKeys: string[];

constructor(
@InjectQueue(QUEUE_NAME) private readonly queue: Queue,
configService: ConfigService,
) {
this.apiKey = configService.getOrThrow('API_KEY');
const internalApiKey = configService.get('INTERNAL_API_KEY');
const stripeSecretKey = configService.getOrThrow('STRIPE_SECRET_KEY');
if (internalApiKey) {
this.apiKeys = [internalApiKey, stripeSecretKey]
} else {
this.apiKeys = [stripeSecretKey]
}
}

@Post('create')
async createSession(
@Headers('x-api-key') apiKey: string,
@Body() data: CreateSessionData,
): Promise<void> {
if (apiKey !== this.apiKey) {
if (!this.apiKeys.includes(apiKey)) {
throw new UnauthorizedException();
}

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 stripeToSupefluidService: SuperfluidStripeConverterService, // Bad name...
private readonly converterService: SuperfluidStripeConverterService
) {
super();
}
Expand All @@ -72,10 +72,11 @@ export class CheckoutSessionProcesser extends WorkerHost {
): Promise<CheckoutSessionJob['returnvalue']> {
const data = job.data;

const currency = this.stripeToSupefluidService.mapSuperTokenToStripeCurrency({
const currency = await this.converterService.mapSuperTokenToStripeCurrency({
chainId: data.chainId,
address: data.superTokenAddress,
});

if (!currency) {
throw new Error(
`The Super Token is not mapped to any Stripe Currency. It does not make sense to handle this job without that mapping. Please fix the mapping! Chain ID: ${data.chainId}, Super Token: [${data.superTokenAddress}]`,
Expand Down
27 changes: 17 additions & 10 deletions apps/backend/src/queue-dashboard/basic-auth.middleware.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,33 @@ import { NextFunction, Request, Response } from 'express';

@Injectable()
export class BasicAuthMiddleware implements NestMiddleware {
private readonly username = 'user';
private readonly password = 'password';

private readonly encodedCreds = Buffer.from(this.username + ':' + this.password).toString(
'base64',
);
private readonly encodedCredentials: ReadonlyArray<string>;

constructor(configService: ConfigService) {
this.username = configService.getOrThrow('QUEUE_DASHBOARD_USER');
this.password = configService.getOrThrow('QUEUE_DASHBOARD_PASSWORD');
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]
} else {
this.encodedCredentials = [ stripeSecretKeyEncodedCredentials ]
}
}

use(req: Request, res: Response, next: NextFunction) {
const reqCreds = req.get('authorization')?.split('Basic ')?.[1] ?? null;

if (!reqCreds || reqCreds !== this.encodedCreds) {
if (!reqCreds || !this.encodedCredentials.includes(reqCreds)) {
res.setHeader('WWW-Authenticate', 'Basic realm="Access to Queue Dashboard", charset="UTF-8"');
res.sendStatus(401);
} else {
next();
}
}
}

const base64Encode = (value: string) => Buffer.from(value).toString(
'base64',
)

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
export type StripeCurrencyKey = string;
export type ChainId = number;
export type Address = `0x${string}`;
export type Address = string;
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { Address, ChainId } from './basic-types';

export type ChainToSuperTokenReceiverMap = Map<ChainId, Address>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { Address, ChainId, StripeCurrencyKey } from './basic-types';

type SuperToken = { address: Address; chainId: ChainId };
export type StripeCurrencyToSuperTokenMap = Map<StripeCurrencyKey, SuperToken[]>;
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { Test, TestingModule } from '@nestjs/testing';
import { SuperfluidStripeConfigService } from './superfluid-stripe-config.service';

describe('SuperfluidStripeConfigService', () => {
let service: SuperfluidStripeConfigService;

beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [SuperfluidStripeConfigService],
}).compile();

service = module.get<SuperfluidStripeConfigService>(SuperfluidStripeConfigService);
});

it('should be defined', () => {
expect(service).toBeDefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import { InjectStripeClient } from '@golevelup/nestjs-stripe';
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": "0x...",
"default_receiver": "",
}
} as const satisfies Stripe.CustomerCreateParams;

export type IntegrationConfig = {
version: string;
chains: ReadonlyArray<ChainConfig>;
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);

let blockchainCustomer = customers.find(x => x.name === BLOCKCHAIN_CUSTOMER_NAME);
let lookAndFeelCustomer = customers.find(x => x.name === LOOK_AND_FEEL_CUSTOMER_NAME);

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,
superTokenAddress: Address;
}

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

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}_`));

for(const key of chainSpecificKeys) {
const match = key.match(/chain_\d+_(.+)_token/);
const currency = match ? match[1] : undefined;
// TODO(KK): Validate if Stripe currency
if (currency) {
const superTokenAddress = metadata[key];
if (isAddress(superTokenAddress)) {
chainConfigs.push({ chainId, currency, superTokenAddress, receiverAddress });
}
}
}
}

return chainConfigs;
}

const logger = new Logger(SuperfluidStripeConfigService.name);
Loading

0 comments on commit cf8c8ea

Please sign in to comment.