Skip to content

Commit

Permalink
Merge pull request #108 from madfish-solutions/development
Browse files Browse the repository at this point in the history
Release latest changes to production
  • Loading branch information
lourenc authored Jul 11, 2023
2 parents 795be5f + 26053f3 commit e75f0ae
Show file tree
Hide file tree
Showing 18 changed files with 828 additions and 493 deletions.
4 changes: 2 additions & 2 deletions .env.dist
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
PORT=3000
QUIPUSWAP_FA12_FACTORIES=KT1K7whn5yHucGXMN7ymfKiX5r534QeaJM29,KT1Lw8hCoaBrHeTeMXbqHPG4sS4K1xn7yKcD,KT1FWHLMk5tHbwuSsp31S4Jum4dTVmkXpfJw
QUIPUSWAP_FA2_FACTORIES=KT1MMLb2FVrrE9Do74J3FH1RNNc4QhDuVCNX,KT1SwH9P1Tx8a58Mm6qBExQFTcy2rwZyZiXS,KT1PvEyN1xCFCgorN92QCfYjw3axS6jawCiJ
SHOULD_APP_CHECK_BLOCK_THE_APP=false
IOS_APP_ID=
ANDROID_APP_ID=
MOONPAY_SECRET_KEY=
ALICE_BOB_PUBLIC_KEY=
ALICE_BOB_PRIVATE_KEY=
THREE_ROUTE_API_URL=
THREE_ROUTE_API_AUTH_TOKEN=
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
{
"typescript.tsdk": "node_modules/typescript/lib"
}
12 changes: 7 additions & 5 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"cors": "^2.8.5",
"cross-fetch": "^3.1.5",
"dotenv": "^9.0.2",
"express": "^4.17.3",
"express": "^4.18.2",
"firebase-admin": "^10.0.2",
"memoizee": "^0.4.15",
"pino": "^6.11.2",
Expand All @@ -35,9 +35,11 @@
"clean": "rimraf dist/"
},
"devDependencies": {
"@types/express": "^4.17.11",
"@types/express": "^4.17.17",
"@types/express-jwt": "^7.4.2",
"@types/express-unless": "^2.0.1",
"@types/memoizee": "^0.4.5",
"@types/node": "^15.3.0",
"@types/node": "^18.14.6",
"@types/pino": "^6.3.8",
"@types/pino-http": "^5.4.1",
"@types/semaphore": "^1.1.1",
Expand All @@ -52,7 +54,7 @@
"npm-run-all": "^4.1.5",
"prettier": "^2.8.2",
"rimraf": "^3.0.2",
"ts-node-dev": "^1.1.8",
"typescript": "^4.2.4"
"ts-node-dev": "^2.0.0",
"typescript": "^4.9.5"
}
}
22 changes: 15 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,24 @@
import { getEnv } from './utils/env';
import { isDefined } from './utils/helpers';

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

export const QUIPUSWAP_FA12_FACTORIES = getEnv('QUIPUSWAP_FA12_FACTORIES');
export const QUIPUSWAP_FA2_FACTORIES = getEnv('QUIPUSWAP_FA2_FACTORIES');
export const MOONPAY_SECRET_KEY = getEnv('MOONPAY_SECRET_KEY');
export const ALICE_BOB_PRIVATE_KEY = getEnv('ALICE_BOB_PRIVATE_KEY');
export const ALICE_BOB_PUBLIC_KEY = getEnv('ALICE_BOB_PUBLIC_KEY');
export const THREE_ROUTE_API_URL = getEnv('THREE_ROUTE_API_URL');
export const THREE_ROUTE_API_AUTH_TOKEN = getEnv('THREE_ROUTE_API_AUTH_TOKEN');

if (!Boolean(QUIPUSWAP_FA12_FACTORIES)) throw new Error('process.env.QUIPUSWAP_FA12_FACTORIES not found.');
if (!Boolean(QUIPUSWAP_FA2_FACTORIES)) throw new Error('process.env.QUIPUSWAP_FA2_FACTORIES not found.');
if (!Boolean(MOONPAY_SECRET_KEY)) throw new Error('process.env.MOONPAY_SECRET_KEY not found.');
if (!Boolean(ALICE_BOB_PRIVATE_KEY)) throw new Error('process.env.ALICE_BOB_PRIVATE_KEY not found.');
if (!Boolean(ALICE_BOB_PUBLIC_KEY)) throw new Error('process.env.ALICE_BOB_PUBLIC_KEY not found.');
const variablesToAssert = [
{ name: 'MOONPAY_SECRET_KEY', value: MOONPAY_SECRET_KEY },
{ name: 'ALICE_BOB_PRIVATE_KEY', value: ALICE_BOB_PRIVATE_KEY },
{ name: 'ALICE_BOB_PUBLIC_KEY', value: ALICE_BOB_PUBLIC_KEY },
{ name: 'THREE_ROUTE_API_URL', value: THREE_ROUTE_API_URL },
{ name: 'THREE_ROUTE_API_AUTH_TOKEN', value: THREE_ROUTE_API_AUTH_TOKEN }
];
variablesToAssert.forEach(({ name, value }) => {
if (!isDefined(value)) {
throw new Error(`process.env.${name} not found.`);
}
});
16 changes: 11 additions & 5 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import { estimateAliceBobOutput } from './utils/alice-bob/estimate-alice-bob-out
import { getAliceBobOrderInfo } from './utils/alice-bob/get-alice-bob-order-info';
import { getAliceBobPairInfo } from './utils/alice-bob/get-alice-bob-pair-info';
import { coinGeckoTokens } from './utils/gecko-tokens';
import { getExternalApiErrorPayload } from './utils/helpers';
import logger from './utils/logger';
import { getSignedMoonPayUrl } from './utils/moonpay/get-signed-moonpay-url';
import SingleQueryDataProvider from './utils/SingleQueryDataProvider';
Expand Down Expand Up @@ -174,7 +175,8 @@ app.post('/api/alice-bob/create-order', async (_req, res) => {

res.status(200).send({ orderInfo });
} catch (error) {
res.status(error.response.status).send(error.response.data);
const { status, data } = getExternalApiErrorPayload(error);
res.status(status).send(data);
}
});

Expand All @@ -186,7 +188,8 @@ app.post('/api/alice-bob/cancel-order', async (_req, res) => {

res.status(200);
} catch (error) {
res.status(error.response.status).send(error.response.data);
const { status, data } = getExternalApiErrorPayload(error);
res.status(status).send(data);
}
});

Expand All @@ -198,7 +201,8 @@ app.get('/api/alice-bob/get-pair-info', async (_req, res) => {

res.status(200).send({ pairInfo });
} catch (error) {
res.status(error.response.status).send({ error: error.response.data });
const { status, data } = getExternalApiErrorPayload(error);
res.status(status).send(data);
}
});

Expand All @@ -210,7 +214,8 @@ app.get('/api/alice-bob/check-order', async (_req, res) => {

res.status(200).send({ orderInfo });
} catch (error) {
res.status(error.response.status).send({ error: error.response.data });
const { status, data } = getExternalApiErrorPayload(error);
res.status(status).send({ error: data });
}
});

Expand All @@ -228,7 +233,8 @@ app.post('/api/alice-bob/estimate-amount', async (_req, res) => {

res.status(200).send({ outputAmount });
} catch (error) {
res.status(error.response.status).send({ error: error.response.data });
const { status, data } = getExternalApiErrorPayload(error);
res.status(status).send({ error: data });
}
});

Expand Down
10 changes: 9 additions & 1 deletion src/utils/DataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,14 @@ export default class DataProvider<T, A extends any[]> {
});
}

async refetchInSubscription(...args: A) {
const subscriptions = await this.subscriptions.getData();
const subscription = subscriptions.find(({ args: subscribedArgs }) => argsAreEqual(args, subscribedArgs));
if (subscription) {
await subscription.dataProvider.refetch();
}
}

async get(...args: A): Promise<SingleQueryDataProviderState<T>> {
const subscriptions = await this.subscriptions.getData();
const subscription = subscriptions.find(({ args: subscribedArgs }) => argsAreEqual(args, subscribedArgs));
Expand All @@ -49,7 +57,7 @@ export default class DataProvider<T, A extends any[]> {

return { data };
} catch (error) {
return { error };
return { error: error as Error };
}
}
}
31 changes: 24 additions & 7 deletions src/utils/SingleQueryDataProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,24 @@ import logger from './logger';
import MutexProtectedData from './MutexProtectedData';
import PromisifiedSemaphore from './PromisifiedSemaphore';

export type SingleQueryDataProviderState<T> = {
interface SingleQueryDataProviderStateCommon<T> {
data?: T;
error?: Error;
};
}

interface SingleQueryDataProviderStateReady<T> extends SingleQueryDataProviderStateCommon<T> {
data: T;
error?: undefined;
}

interface SingleQueryDataProviderStateError<T> extends SingleQueryDataProviderStateCommon<T> {
data?: undefined;
error: Error;
}

export type SingleQueryDataProviderState<T> =
| SingleQueryDataProviderStateReady<T>
| SingleQueryDataProviderStateError<T>;

const defaultShouldGiveUp = (_e: Error, c: number) => c > 9;

Expand All @@ -28,7 +42,7 @@ export default class SingleQueryDataProvider<T> {
) {
this.fetchMutex = new PromisifiedSemaphore();
this.readyMutex = new PromisifiedSemaphore();
this.state = new MutexProtectedData({});
this.state = new MutexProtectedData({ error: new Error('This error should not be displayed') });
this.init();
}

Expand All @@ -44,10 +58,11 @@ export default class SingleQueryDataProvider<T> {
const result = await this.fetchFn();
await this.state.setData({ data: result });
} catch (e) {
const error = e as Error;
const timeSlot = 1000;
logger.error(`Error in SingleQueryDataProvider: ${e.message}\n${e.stack}`);
if (this.shouldGiveUp(e, c)) {
await this.state.setData({ error: e });
logger.error(`Error in SingleQueryDataProvider: ${error.message}\n${error.stack}`);
if (this.shouldGiveUp(error, c)) {
await this.state.setData({ error });
} else {
await new Promise<void>(resolve => {
this.refetchRetryTimeout = setTimeout(async () => {
Expand All @@ -61,7 +76,9 @@ export default class SingleQueryDataProvider<T> {

async init() {
await this.readyMutex.exec(() => this.refetch());
this.refetchInterval = setInterval(() => this.refetch(), this.refreshParams);
if (Number.isFinite(this.refreshParams)) {
this.refetchInterval = setInterval(() => this.refetch(), this.refreshParams);
}
}

async getState() {
Expand Down
57 changes: 57 additions & 0 deletions src/utils/block-finder.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { BlockResponse, BlockFullHeader } from '@taquito/rpc';

import { sleep } from './helpers';
import logger from './logger';
import { tezosToolkit } from './tezos';

export interface BlockInterface extends Pick<BlockResponse, 'protocol' | 'chain_id' | 'hash'> {
header: Pick<BlockFullHeader, 'level' | 'timestamp'>;
}

export const EMPTY_BLOCK: BlockInterface = {
protocol: '',
chain_id: '',
hash: '',
header: {
level: 0,
timestamp: ''
}
};

export const blockFinder = async (
prevBlock: BlockInterface,
onNewBlock: (block: BlockInterface) => Promise<unknown>
): Promise<unknown> => {
const block = await tezosToolkit.rpc
.getBlock()
.then(
(blockResponse): BlockInterface => ({
protocol: blockResponse.protocol,
chain_id: blockResponse.chain_id,
hash: blockResponse.hash,
header: {
level: blockResponse.header.level,
timestamp: blockResponse.header.timestamp
}
})
)
.catch(e => {
logger.error(e);

return prevBlock;
});

const isNewBlock = block.header.level > prevBlock.header.level;
const realBlock = isNewBlock ? block : prevBlock;

if (isNewBlock) {
await onNewBlock(realBlock).catch(e => {
logger.error('blockFinder error');
logger.error(e);
});
} else {
await sleep(200);
}

return blockFinder(realBlock, onNewBlock);
};
2 changes: 1 addition & 1 deletion src/utils/coingecko.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { range } from './helpers';
import makeBuildQueryFn from './makeBuildQueryFn';
import { makeBuildQueryFn } from './makeBuildQueryFn';
import SingleQueryDataProvider from './SingleQueryDataProvider';

type CoinsListParams = {
Expand Down
35 changes: 35 additions & 0 deletions src/utils/get-recent-destinations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { isDefined } from './helpers';
import logger from './logger';
import { makeBlockQuery } from './tzkt';

const PAST_BLOCKS_DEPTH = 4;

export const getRecentDestinations = (currentBlockLevel: number) =>
Promise.all(
new Array(PAST_BLOCKS_DEPTH).fill(0).map(async (_, index) => {
const pastBlockLevel = currentBlockLevel - index;

const { transactions } = await makeBlockQuery({ level: pastBlockLevel, operations: true });

if (isDefined(transactions)) {
return transactions
.map(transaction => {
if (transaction?.type === 'transaction') {
return transaction.target?.address ?? undefined;
}

return undefined;
})
.filter(isDefined);
}

return [];
})
).then(
destinationsArray => destinationsArray.flat(),
(error): string[] => {
logger.error('getRecentDestinations error:', error);

return [];
}
);
41 changes: 35 additions & 6 deletions src/utils/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,45 @@
import { AxiosError } from 'axios';
import { BigNumber } from 'bignumber.js';

export function range(start: number, end: number, step = 1) {
return Array(Math.ceil((end - start) / step))
export const range = (start: number, end: number, step = 1) =>
Array(Math.ceil((end - start) / step))
.fill(0)
.map((_x, index) => start + step * index);
}

export function rangeBn(start: number, end: number, step = 1): Array<BigNumber> {
return Array(Math.ceil((end - start) / step))
export const rangeBn = (start: number, end: number, step = 1) =>
Array(Math.ceil((end - start) / step))
.fill(0)
.map((_x, index) => new BigNumber(start + step * index));
}

export const pick = <T extends object, U extends keyof T>(obj: T, keys: U[]) => {
const newObj: Partial<T> = {};
keys.forEach(key => {
if (key in obj) {
newObj[key] = obj[key];
}
});

return newObj as Pick<T, U>;
};

export const isAbsoluteURL = (url: string) => {
// A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL).
// RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed
// by any combination of letters, digits, plus, period, or hyphen.
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url);
};

// eslint-disable-next-line @typescript-eslint/no-empty-function
export const emptyFn = () => {};

export const isDefined = <T>(value: T | undefined | null): value is T => value !== undefined && value !== null;

export const sleep = (ms: number) => new Promise(resolve => setTimeout(() => resolve('wake'), ms));

export const getExternalApiErrorPayload = (error: unknown) => {
const response = error instanceof AxiosError ? error.response : undefined;
const status = response?.status ?? 500;
const data = response?.data ?? { error: error instanceof Error ? error.message : error };

return { status, data };
};
Loading

0 comments on commit e75f0ae

Please sign in to comment.