Skip to content

Commit

Permalink
TW-1240: Magic Square campaign
Browse files Browse the repository at this point in the history
  • Loading branch information
alex-tsx committed Jan 12, 2024
1 parent 04dc8fe commit de6e712
Show file tree
Hide file tree
Showing 9 changed files with 380 additions and 9 deletions.
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
6 changes: 6 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 Down Expand Up @@ -37,6 +42,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
52 changes: 52 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import swaggerUi from 'swagger-ui-express';
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 { getAliceBobEstimationPayload } from './utils/alice-bob/get-alice-bob-es
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, SIGNING_NONCE_TTL } from './utils/signing-nonce';
import SingleQueryDataProvider from './utils/SingleQueryDataProvider';
import { tezExchangeRateProvider } from './utils/tezos';
import { getExchangeRatesFromDB } from './utils/tokens';
Expand Down Expand Up @@ -327,6 +330,55 @@ app.get('/api/advertising-info', (_req, res) => {

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/auth-nonce', async (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

const nonce = getSigningNonce(pkh);

res.status(200).send({ nonce, ttl: SIGNING_NONCE_TTL });
} catch (error: any) {

Check warning on line 371 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
82 changes: 82 additions & 0 deletions src/magic-square.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
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 { STATUS_CODE } from '@taquito/http-utils';
import { validateAddress, ValidationResult, verifySignature, getPkhfromPk } from '@taquito/utils';

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

const REDIS_DB_KEY = 'magic_square_quest';

export function getMagicSquareQuestParticipants() {
return redisClient.lrange(REDIS_DB_KEY, 0, -1).then(records => records.map(r => JSON.parse(r)));
}

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

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

if (!safeCheck(() => validateAddress(pkh) === ValidationResult.VALID && getPkhfromPk(publicKey) === pkh))
throw new CodedError(STATUS_CODE.BAD_REQUEST, 'Invalid Tezos public key (hash)');

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

// Signatures

if (!safeCheck(() => verifySignature(messageBytes, publicKey, signature)))
throw new CodedError(STATUS_CODE.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(STATUS_CODE.UNAUTHORIZED, 'Invalid EVM signature or message');

// Presence check

const exists = await redisClient
.lrange(REDIS_DB_KEY, 0, -1)
.then(items => items.some(item => item.includes(pkh) && item.includes(evmPkh)));

if (exists)
throw new CodedError(STATUS_CODE.CONFLICT, 'Quest already started for the given credentials', 'QUEST_STARTED');

// Auth nonce
getSigningNonce.delete(pkh);

// Registering

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

await redisClient.lpush(REDIS_DB_KEY, JSON.stringify(item));
}
17 changes: 17 additions & 0 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
interface CodedErrorForResponse {
message: string;
code?: string;
}

export class CodedError extends Error {
constructor(public code: number, message: string, public errorCode?: string) {
super(message);
}

buildResponse() {
const res: CodedErrorForResponse = { message: this.message };
if (this.errorCode) res.code = this.errorCode;

Check warning on line 13 in src/utils/errors.ts

View workflow job for this annotation

GitHub Actions / Checks if ts and lint works

Unexpected nullable string value in conditional. Please handle the nullish/empty cases explicitly

return res;
}
}
10 changes: 10 additions & 0 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,3 +45,13 @@ export const getExternalApiErrorPayload = (error: unknown) => {

return { status, data };
};

export function safeCheck(check: () => boolean, def = false) {
try {
return check();
} catch (error) {
console.error();

return def;
}
}
23 changes: 23 additions & 0 deletions src/utils/signing-nonce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { randomStringForEntropy } from '@stablelib/random';
import { validateAddress, ValidationResult } from '@taquito/utils';
import memoizee from 'memoizee';

import { CodedError } from './errors';

export const SIGNING_NONCE_TTL = 5 * 60_000;

const MEMOIZE_OPTIONS = {
max: 500,
maxAge: SIGNING_NONCE_TTL
};

export const getSigningNonce = memoizee((pkh: string) => {
if (validateAddress(pkh) !== ValidationResult.VALID) throw new CodedError(400, 'Invalid address');

return buildNonce();
}, MEMOIZE_OPTIONS);

function buildNonce() {
// The way it is done in SIWE.generateNonce()
return randomStringForEntropy(96);
}
Loading

0 comments on commit de6e712

Please sign in to comment.