Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Release latest changes to production #142

Merged
merged 4 commits into from
Feb 1, 2024
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
9 changes: 9 additions & 0 deletions .editorconfig
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
# Editor configuration, see http://editorconfig.org
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
6 changes: 3 additions & 3 deletions .eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
"no-shadow": "off",
"import/no-duplicates": "error",
"import/no-self-import": "error",
"import/no-cycle": "error",
"import/order": [
"error",
{
Expand All @@ -36,12 +37,11 @@
"newlines-between": "always"
}
],
"@typescript-eslint/strict-boolean-expressions": "error",
"@typescript-eslint/strict-boolean-expressions": "warn",
"@typescript-eslint/no-unused-vars": "error",
"@typescript-eslint/explicit-module-boundary-types": "off",
"@typescript-eslint/no-unnecessary-type-constraint": "off",
"@typescript-eslint/ban-ts-comment": "off",
"quotes": ["error", "single"]
"@typescript-eslint/ban-ts-comment": "off"
},
"globals": {
"localStorage": true
Expand Down
7 changes: 7 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
"author": "Inokentii Mazhara <[email protected]>",
"license": "MIT",
"dependencies": {
"@ethersproject/address": "^5.7.0",
"@ethersproject/hash": "^5.7.0",
"@ethersproject/strings": "^5.7.0",
"@ethersproject/transactions": "^5.7.0",
"@stablelib/random": "^1.0.2",
"@taquito/rpc": "14.0.0",
"@taquito/taquito": "14.0.0",
"@taquito/tzip12": "14.0.0",
Expand All @@ -18,6 +23,7 @@
"dotenv": "^9.0.2",
"express": "^4.18.2",
"firebase-admin": "^10.0.2",
"http-status-codes": "^2.3.0",
"ioredis": "^5.3.2",
"lodash": "^4.17.21",
"memoizee": "^0.4.15",
Expand All @@ -37,6 +43,7 @@
"ts": "tsc --pretty",
"lint": "eslint ./src --ext .js,.ts",
"lint:fix": "eslint ./src --ext .js,.ts --fix",
"lint:rules": "eslint --print-config",
"clean": "rimraf dist/",
"db-migration": "cd migrations/notifications && npx ts-node index.ts"
},
Expand Down
58 changes: 32 additions & 26 deletions src/advertising/slise.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { redisClient } from '../redis';
import { isDefined } from '../utils/helpers';
import { objectStorageMethodsFactory, redisClient } from '../redis';

/** Style properties names that are likely to be unnecessary for banners are skipped */
export const stylePropsNames = [
Expand Down Expand Up @@ -90,6 +89,29 @@ export interface SliseAdPlacesRule {
stylesOverrides?: SliseAdStylesOverrides[];
}

export interface PermanentSliseAdPlacesRule {
urlRegexes: string[];
adSelector: {
isMultiple: boolean;
cssString: string;
parentDepth: number;
};
parentSelector: {
isMultiple: boolean;
cssString: string;
parentDepth: number;
};
insertionIndex?: number;
insertBeforeSelector?: string;
insertAfterSelector?: string;
insertionsCount?: number;
shouldUseDivWrapper: boolean;
divWrapperStyle?: Record<StylePropName, string>;
elementToMeasureSelector?: string;
stylesOverrides?: SliseAdStylesOverrides[];
shouldHideOriginal?: boolean;
}

export interface SliseAdProvidersByDomainRule {
urlRegexes: string[];
providers: string[];
Expand All @@ -99,30 +121,7 @@ const SLISE_AD_PLACES_RULES_KEY = 'slise_ad_places_rules';
const SLISE_AD_PROVIDERS_BY_SITES_KEY = 'slise_ad_providers_by_sites';
const SLISE_AD_PROVIDERS_ALL_SITES_KEY = 'slise_ad_providers_all_sites';
const SLISE_AD_PROVIDERS_LIST_KEY = 'slise_ad_providers_list';

const objectStorageMethodsFactory = <V>(storageKey: string, fallbackValue: V) => ({
getByKey: async (key: string): Promise<V> => {
const value = await redisClient.hget(storageKey, key);

return isDefined(value) ? JSON.parse(value) : fallbackValue;
},
getAllValues: async (): Promise<Record<string, V>> => {
const values = await redisClient.hgetall(storageKey);

const parsedValues: Record<string, V> = {};
for (const key in values) {
parsedValues[key] = JSON.parse(values[key]);
}

return parsedValues;
},
upsertValues: (newValues: Record<string, V>) =>
redisClient.hmset(
storageKey,
Object.fromEntries(Object.entries(newValues).map(([domain, value]) => [domain, JSON.stringify(value)]))
),
removeValues: (keys: string[]) => redisClient.hdel(storageKey, ...keys)
});
const PERMANENT_SLISE_AD_PLACES_RULES_KEY = 'permanent_slise_ad_places_rules';

export const {
getByKey: getSliseAdPlacesRulesByDomain,
Expand All @@ -145,6 +144,13 @@ export const {
removeValues: removeProviders
} = objectStorageMethodsFactory<string[]>(SLISE_AD_PROVIDERS_LIST_KEY, []);

export const {
getByKey: getPermanentSliseAdPlacesRulesByDomain,
getAllValues: getAllPermanentSliseAdPlacesRules,
upsertValues: upsertPermanentSliseAdPlacesRules,
removeValues: removePermanentSliseAdPlacesRules
} = objectStorageMethodsFactory<PermanentSliseAdPlacesRule[]>(PERMANENT_SLISE_AD_PLACES_RULES_KEY, []);

export const getSliseAdProvidersForAllSites = async () => redisClient.smembers(SLISE_AD_PROVIDERS_ALL_SITES_KEY);

export const addSliseAdProvidersForAllSites = async (providers: string[]) =>
Expand Down
4 changes: 2 additions & 2 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { getEnv } from './utils/env';
import { isDefined } from './utils/helpers';

export const MIN_IOS_APP_VERSION = '1.20.1027';
export const MIN_ANDROID_APP_VERSION = '1.20.1027';
export const MIN_IOS_APP_VERSION = '1.10.445';
export const MIN_ANDROID_APP_VERSION = '1.10.445';

export const EnvVars = {
MOONPAY_SECRET_KEY: getEnv('MOONPAY_SECRET_KEY'),
Expand Down
50 changes: 50 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
import { getAdvertisingInfo } from './advertising/advertising';
import { MIN_ANDROID_APP_VERSION, MIN_IOS_APP_VERSION } from './config';
import getDAppsStats from './getDAppsStats';
import { getMagicSquareQuestParticipants, startMagicSquareQuest } from './magic-square';
import { basicAuth } from './middlewares/basic-auth.middleware';
import { Notification, PlatformType } from './notifications/notification.interface';
import { getImageFallback } from './notifications/utils/get-image-fallback.util';
Expand All @@ -28,10 +29,12 @@
import { getAliceBobOrderInfo } from './utils/alice-bob/get-alice-bob-order-info';
import { getAliceBobPairInfo } from './utils/alice-bob/get-alice-bob-pair-info';
import { getAliceBobPairsInfo } from './utils/alice-bob/get-alice-bob-pairs-info';
import { CodedError } from './utils/errors';
import { coinGeckoTokens } from './utils/gecko-tokens';
import { getExternalApiErrorPayload, isDefined, isNonEmptyString } from './utils/helpers';
import logger from './utils/logger';
import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url';
import { getSigningNonce } from './utils/signing-nonce';
import SingleQueryDataProvider from './utils/SingleQueryDataProvider';
import { tezExchangeRateProvider } from './utils/tezos';
import { getExchangeRatesFromDB } from './utils/tokens';
Expand Down Expand Up @@ -154,7 +157,7 @@
await redisClient.lpush('notifications', JSON.stringify(newNotification));

res.status(200).send({ message: 'Notification added successfully', notification: newNotification });
} catch (error: any) {

Check warning on line 160 in src/index.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected any. Specify a different type
res.status(500).send({ error: error.message });
}
});
Expand Down Expand Up @@ -327,6 +330,53 @@

app.use('/api/slise-ad-rules', sliseRulesRouter);

app.post('/api/magic-square-quest/start', async (req, res) => {
try {
await startMagicSquareQuest(req.body);

res.status(200).send({ message: 'Quest successfully started' });
} catch (error: any) {

Check warning on line 338 in src/index.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected any. Specify a different type
console.error(error);

if (error instanceof CodedError) {
res.status(error.code).send(error.buildResponse());
} else {
res.status(500).send({ message: error?.message });
}
}
});

app.get('/api/magic-square-quest/participants', basicAuth, async (req, res) => {
try {
res.status(200).send(await getMagicSquareQuestParticipants());
} catch (error: any) {

Check warning on line 352 in src/index.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected any. Specify a different type
console.error(error);

if (error instanceof CodedError) {
res.status(error.code).send(error.buildResponse());
} else {
res.status(500).send({ message: error?.message });
}
}
});

app.get('/api/signing-nonce', (req, res) => {
try {
const pkh = req.query.pkh;
if (!pkh || typeof pkh !== 'string') throw new Error('PKH is not a string');

Check warning on line 366 in src/index.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected value in conditional. A boolean expression is required

res.status(200).send(getSigningNonce(pkh));
} catch (error: any) {

Check warning on line 369 in src/index.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected any. Specify a different type
console.error(error);

if (error instanceof CodedError) {
res.status(error.code).send(error.buildResponse());
} else {
res.status(500).send({ message: error?.message });
}
}
});

const swaggerOptions = {
swaggerDefinition: {
openapi: '3.0.0',
Expand Down
104 changes: 104 additions & 0 deletions src/magic-square.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import * as ethersAddressUtils from '@ethersproject/address';
import * as ethersHashUtils from '@ethersproject/hash';
import * as ethersStringsUtils from '@ethersproject/strings';
import * as ethersTxUtils from '@ethersproject/transactions';
import { verifySignature, getPkhfromPk } from '@taquito/utils';
import { StatusCodes } from 'http-status-codes';

import { objectStorageMethodsFactory } from './redis';
import { CodedError } from './utils/errors';
import { safeCheck } from './utils/helpers';
import { getSigningNonce, removeSigningNonce } from './utils/signing-nonce';

interface Participant {
pkh: string;
evmPkh: string;
ts: string;
}

const REDIS_DB_KEY = 'magic_square_quest';

const redisStorage = objectStorageMethodsFactory<Participant, null>(REDIS_DB_KEY, null);

export function getMagicSquareQuestParticipants() {
return redisStorage.getAllValues().then(records => Object.values(records));
}

interface StartQuestPayload {
publicKey: string;
messageBytes: string;
signature: string;
evm: {
pkh: string;
messageBytes: string;
signature: string;
};
}

export async function startMagicSquareQuest({ publicKey, messageBytes, signature, evm }: StartQuestPayload) {
// Public Key Hashes

let pkh: string;
try {
pkh = getPkhfromPk(publicKey);
} catch (err) {
console.error(err);
throw new CodedError(StatusCodes.BAD_REQUEST, 'Invalid Tezos public key');
}

let evmPkh: string;
try {
// Corrects lower-cased addresses. Throws if invalid.
evmPkh = ethersAddressUtils.getAddress(evm.pkh);
} catch (err) {
console.error(err);
throw new CodedError(StatusCodes.BAD_REQUEST, 'Invalid EVM public key hash');
}

// Nonce
const { value: nonce } = getSigningNonce(pkh);
const nonceBytes = Buffer.from(nonce, 'utf-8').toString('hex');

if (!messageBytes.includes(nonceBytes))
throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid Tezos message nonce', 'INVALID_NONCE_TEZ');

if (!evm.messageBytes.includes(nonceBytes))
throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid EVM message nonce', 'INVALID_NONCE_EVM');

// Signatures

if (!safeCheck(() => verifySignature(messageBytes, publicKey, signature)))
throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid Tezos signature or message');

if (
!safeCheck(() => {
const messageBytes = ethersStringsUtils.toUtf8String(evm.messageBytes);
const messageHash = ethersHashUtils.hashMessage(messageBytes);

return ethersTxUtils.recoverAddress(messageHash, evm.signature) === evmPkh;
})
)
throw new CodedError(StatusCodes.UNAUTHORIZED, 'Invalid EVM signature or message');

// Presence check

const storageKey = `${pkh}+${evmPkh}`;

const existingValue = await redisStorage.getByKey(storageKey);

if (existingValue)
throw new CodedError(StatusCodes.CONFLICT, 'Your quest was already started before', 'QUEST_IS_STARTED');

// Invalidating nonce
removeSigningNonce(pkh);

// Registering

const participant: Participant = {
pkh,
evmPkh,
ts: new Date().toISOString()
};

await redisStorage.upsertValues({ [storageKey]: participant });
}
25 changes: 25 additions & 0 deletions src/redis.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
import { Redis } from 'ioredis';

import { EnvVars } from './config';
import { isDefined } from './utils/helpers';
import logger from './utils/logger';

export const redisClient = new Redis(EnvVars.REDIS_URL);
redisClient.on('error', err => logger.error(err));

export const objectStorageMethodsFactory = <V, F = V>(storageKey: string, fallbackValue: F) => ({
getByKey: async (key: string): Promise<V | F> => {
const value = await redisClient.hget(storageKey, key);

return isDefined(value) ? JSON.parse(value) : fallbackValue;
},
getAllValues: async (): Promise<Record<string, V>> => {
const values = await redisClient.hgetall(storageKey);

const parsedValues: Record<string, V> = {};
for (const key in values) {
parsedValues[key] = JSON.parse(values[key]);
}

return parsedValues;
},
upsertValues: (newValues: Record<string, V>) =>
redisClient.hmset(
storageKey,
Object.fromEntries(Object.entries(newValues).map(([domain, value]) => [domain, JSON.stringify(value)]))
),
removeValues: (keys: string[]) => redisClient.hdel(storageKey, ...keys)
});
Loading
Loading