@strkfarm/sdk 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +636 -0
- package/dist/cli.mjs +613 -0
- package/dist/index.browser.global.js +33487 -0
- package/dist/index.d.ts +360 -0
- package/dist/index.js +1003 -0
- package/dist/index.mjs +948 -0
- package/package.json +58 -0
- package/src/cli.ts +160 -0
- package/src/data/pragma.abi.json +96 -0
- package/src/data/tokens.json +72 -0
- package/src/dataTypes/address.ts +38 -0
- package/src/dataTypes/bignumber.ts +53 -0
- package/src/dataTypes/index.ts +2 -0
- package/src/global.ts +73 -0
- package/src/index.browser.ts +6 -0
- package/src/index.ts +9 -0
- package/src/interfaces/common.ts +33 -0
- package/src/interfaces/index.ts +3 -0
- package/src/interfaces/initializable.ts +21 -0
- package/src/interfaces/lending.ts +75 -0
- package/src/modules/index.ts +3 -0
- package/src/modules/pragma.ts +22 -0
- package/src/modules/pricer.ts +125 -0
- package/src/modules/zkLend.ts +162 -0
- package/src/node/index.ts +1 -0
- package/src/node/pricer-redis.ts +71 -0
- package/src/notifs/index.ts +1 -0
- package/src/notifs/telegram.ts +48 -0
- package/src/strategies/autoCompounderStrk.ts +71 -0
- package/src/strategies/index.ts +1 -0
- package/src/utils/encrypt.ts +68 -0
- package/src/utils/index.ts +12 -0
- package/src/utils/store.ts +173 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { IConfig } from "@/interfaces/common";
|
|
2
|
+
import { TokenInfo } from "./common";
|
|
3
|
+
import { ContractAddr } from "@/dataTypes/address";
|
|
4
|
+
import { loggers } from "winston";
|
|
5
|
+
import { logger } from "@/global";
|
|
6
|
+
import { log } from "console";
|
|
7
|
+
import { Web3Number } from "@/dataTypes/bignumber";
|
|
8
|
+
|
|
9
|
+
export interface ILendingMetadata {
|
|
10
|
+
name: string;
|
|
11
|
+
logo: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export enum MarginType {
|
|
15
|
+
SHARED = "shared",
|
|
16
|
+
NONE = "none",
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ILendingPosition {
|
|
20
|
+
tokenName: string;
|
|
21
|
+
tokenSymbol: string;
|
|
22
|
+
marginType: MarginType,
|
|
23
|
+
debtAmount: Web3Number;
|
|
24
|
+
debtUSD: Web3Number;
|
|
25
|
+
supplyAmount: Web3Number;
|
|
26
|
+
supplyUSD: Web3Number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface LendingToken extends TokenInfo {
|
|
30
|
+
borrowFactor: Web3Number;
|
|
31
|
+
collareralFactor: Web3Number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export abstract class ILending {
|
|
35
|
+
readonly config: IConfig;
|
|
36
|
+
readonly metadata: ILendingMetadata;
|
|
37
|
+
readonly tokens: LendingToken[] = [];
|
|
38
|
+
|
|
39
|
+
protected initialised: boolean = false;
|
|
40
|
+
constructor(config:IConfig, metadata: ILendingMetadata) {
|
|
41
|
+
this.metadata = metadata;
|
|
42
|
+
this.config = config;
|
|
43
|
+
this.init();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Async function to init the class */
|
|
47
|
+
abstract init(): Promise<void>;
|
|
48
|
+
|
|
49
|
+
/** Wait for initialisation */
|
|
50
|
+
waitForInitilisation() {
|
|
51
|
+
return new Promise<void>((resolve, reject) => {
|
|
52
|
+
const interval = setInterval(() => {
|
|
53
|
+
logger.verbose(`Waiting for ${this.metadata.name} to initialise`);
|
|
54
|
+
if (this.initialised) {
|
|
55
|
+
logger.verbose(`${this.metadata.name} initialised`);
|
|
56
|
+
clearInterval(interval);
|
|
57
|
+
resolve();
|
|
58
|
+
}
|
|
59
|
+
}, 1000);
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
*
|
|
65
|
+
* @param lending_tokens Array of tokens to consider for compute collateral value
|
|
66
|
+
* @param debt_tokens Array of tokens to consider to compute debt values
|
|
67
|
+
* @param user
|
|
68
|
+
*/
|
|
69
|
+
abstract get_health_factor_tokenwise(lending_tokens: TokenInfo[], debt_tokens: TokenInfo[], user: ContractAddr): Promise<number>;
|
|
70
|
+
abstract get_health_factor(user: ContractAddr): Promise<number>;
|
|
71
|
+
abstract getPositionsSummary(user: ContractAddr): Promise<{
|
|
72
|
+
collateralUSD: number,
|
|
73
|
+
debtUSD: number,
|
|
74
|
+
}>
|
|
75
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Contract, RpcProvider } from "starknet";
|
|
2
|
+
import PragmaAbi from '@/data/pragma.abi.json';
|
|
3
|
+
import { logger } from "@/global";
|
|
4
|
+
|
|
5
|
+
export class Pragma {
|
|
6
|
+
contractAddr = '0x023fb3afbff2c0e3399f896dcf7400acf1a161941cfb386e34a123f228c62832';
|
|
7
|
+
readonly contract: Contract;
|
|
8
|
+
|
|
9
|
+
constructor(provider: RpcProvider) {
|
|
10
|
+
this.contract = new Contract(PragmaAbi, this.contractAddr, provider);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
async getPrice(tokenAddr: string) {
|
|
14
|
+
if (!tokenAddr) {
|
|
15
|
+
throw new Error(`Pragma:getPrice - no token`)
|
|
16
|
+
}
|
|
17
|
+
const result: any = await this.contract.call('get_price', [tokenAddr]);
|
|
18
|
+
const price = Number(result.price) / 10**8;
|
|
19
|
+
logger.verbose(`Pragma:${tokenAddr}: ${price}`);
|
|
20
|
+
return price;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import { FatalError, Global, logger } from "@/global";
|
|
3
|
+
import { TokenInfo } from "@/interfaces/common";
|
|
4
|
+
import { IConfig } from "@/interfaces/common";
|
|
5
|
+
|
|
6
|
+
export interface PriceInfo {
|
|
7
|
+
price: number,
|
|
8
|
+
timestamp: Date
|
|
9
|
+
}
|
|
10
|
+
export class Pricer {
|
|
11
|
+
readonly config: IConfig;
|
|
12
|
+
readonly tokens: TokenInfo[] = [];
|
|
13
|
+
protected prices: {
|
|
14
|
+
[key: string]: PriceInfo
|
|
15
|
+
} = {}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* TOKENA and TOKENB are the two token names to get price of TokenA in terms of TokenB
|
|
19
|
+
*/
|
|
20
|
+
protected PRICE_API = `https://api.coinbase.com/v2/prices/{{PRICER_KEY}}/buy`;
|
|
21
|
+
constructor(config: IConfig, tokens: TokenInfo[]) {
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.tokens = tokens;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
isReady() {
|
|
27
|
+
const allPricesExist = Object.keys(this.prices).length === this.tokens.length;
|
|
28
|
+
if (!allPricesExist) return false;
|
|
29
|
+
|
|
30
|
+
let atleastOneStale = false;
|
|
31
|
+
for (let token of this.tokens) {
|
|
32
|
+
const priceInfo = this.prices[token.symbol];
|
|
33
|
+
const isStale = this.isStale(priceInfo.timestamp, token.symbol);
|
|
34
|
+
if (isStale) {
|
|
35
|
+
atleastOneStale = true;
|
|
36
|
+
logger.warn(`Atleast one stale: ${token.symbol}: ${JSON.stringify(this.prices[token.symbol])}`);
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return allPricesExist && !atleastOneStale;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
waitTillReady() {
|
|
44
|
+
return new Promise<void>((resolve, reject) => {
|
|
45
|
+
const interval = setInterval(() => {
|
|
46
|
+
logger.verbose(`Waiting for pricer to initialise`);
|
|
47
|
+
if (this.isReady()) {
|
|
48
|
+
logger.verbose(`Pricer initialised`);
|
|
49
|
+
clearInterval(interval);
|
|
50
|
+
resolve();
|
|
51
|
+
}
|
|
52
|
+
}, 1000);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
start() {
|
|
57
|
+
this._loadPrices();
|
|
58
|
+
setInterval(() => {
|
|
59
|
+
this._loadPrices();
|
|
60
|
+
}, 30000);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
isStale(timestamp: Date, tokenName: string) {
|
|
64
|
+
const STALE_TIME = 60000;
|
|
65
|
+
return (new Date().getTime() - timestamp.getTime()) > STALE_TIME;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
assertNotStale(timestamp: Date, tokenName: string) {
|
|
69
|
+
Global.assert(!this.isStale(timestamp, tokenName), `Price of ${tokenName} is stale`);
|
|
70
|
+
|
|
71
|
+
}
|
|
72
|
+
async getPrice(tokenName: string) {
|
|
73
|
+
Global.assert(this.prices[tokenName], `Price of ${tokenName} not found`);
|
|
74
|
+
this.assertNotStale(this.prices[tokenName].timestamp, tokenName);
|
|
75
|
+
return this.prices[tokenName];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
protected _loadPrices(onUpdate: (tokenSymbol: string) => void = () => {}) {
|
|
79
|
+
this.tokens.forEach(async (token) => {
|
|
80
|
+
const MAX_RETRIES = 10;
|
|
81
|
+
let retry = 0;
|
|
82
|
+
while (retry < MAX_RETRIES) {
|
|
83
|
+
try {
|
|
84
|
+
if (token.symbol === 'USDT') {
|
|
85
|
+
this.prices[token.symbol] = {
|
|
86
|
+
price: 1,
|
|
87
|
+
timestamp: new Date()
|
|
88
|
+
}
|
|
89
|
+
onUpdate(token.symbol);
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
if (!token.pricerKey) {
|
|
93
|
+
throw new FatalError(`Pricer key not found for ${token.name}`);
|
|
94
|
+
}
|
|
95
|
+
const url = this.PRICE_API.replace("{{PRICER_KEY}}", token.pricerKey);
|
|
96
|
+
const result = await axios.get(url);
|
|
97
|
+
const data: any = result.data;
|
|
98
|
+
const price = Number(data.data.amount);
|
|
99
|
+
this.prices[token.symbol] = {
|
|
100
|
+
price,
|
|
101
|
+
timestamp: new Date()
|
|
102
|
+
}
|
|
103
|
+
onUpdate(token.symbol);
|
|
104
|
+
logger.verbose(`Fetched price of ${token.name} as ${price}`);
|
|
105
|
+
break;
|
|
106
|
+
} catch (error: any) {
|
|
107
|
+
if (retry < MAX_RETRIES) {
|
|
108
|
+
logger.warn(`Error fetching data from ${token.name}, retry: ${retry}`);
|
|
109
|
+
logger.warn(error);
|
|
110
|
+
retry++;
|
|
111
|
+
await new Promise((resolve) => setTimeout(resolve, retry * 2000));
|
|
112
|
+
} else {
|
|
113
|
+
throw new FatalError(`Error fetching data from ${token.name}`, error);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
})
|
|
118
|
+
if (this.isReady() && this.config.heartbeatUrl) {
|
|
119
|
+
console.log(`sending beat`)
|
|
120
|
+
axios.get(this.config.heartbeatUrl).catch(err => {
|
|
121
|
+
console.error('Pricer: Heartbeat err', err);
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import axios from "axios";
|
|
2
|
+
import BigNumber from "bignumber.js";
|
|
3
|
+
import { Web3Number } from "@/dataTypes/bignumber";
|
|
4
|
+
import { FatalError, Global, logger } from "@/global";
|
|
5
|
+
import { TokenInfo } from "@/interfaces";
|
|
6
|
+
import { ILending, ILendingPosition, LendingToken, MarginType } from "@/interfaces/lending";
|
|
7
|
+
import { ContractAddr } from "@/dataTypes/address";
|
|
8
|
+
import { IConfig } from "@/interfaces";
|
|
9
|
+
import { Pricer } from "./pricer";
|
|
10
|
+
|
|
11
|
+
export class ZkLend extends ILending implements ILending {
|
|
12
|
+
readonly pricer: Pricer;
|
|
13
|
+
static readonly POOLS_URL = 'https://app.zklend.com/api/pools';
|
|
14
|
+
private POSITION_URL = 'https://app.zklend.com/api/users/{{USER_ADDR}}/all';
|
|
15
|
+
|
|
16
|
+
constructor(config: IConfig, pricer: Pricer) {
|
|
17
|
+
super(config, {
|
|
18
|
+
name: "zkLend",
|
|
19
|
+
logo: 'https://app.zklend.com/favicon.ico'
|
|
20
|
+
});
|
|
21
|
+
this.pricer = pricer;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async init() {
|
|
25
|
+
try {
|
|
26
|
+
logger.verbose(`Initialising ${this.metadata.name}`);
|
|
27
|
+
const result = await axios.get(ZkLend.POOLS_URL);
|
|
28
|
+
const data: any[] = result.data;
|
|
29
|
+
const savedTokens = await Global.getTokens()
|
|
30
|
+
data.forEach((pool) => {
|
|
31
|
+
let collareralFactor = new Web3Number(0, 0);
|
|
32
|
+
if (pool.collateral_factor) {
|
|
33
|
+
collareralFactor = Web3Number.fromWei(pool.collateral_factor.value, pool.collateral_factor.decimals);
|
|
34
|
+
}
|
|
35
|
+
const savedTokenInfo = savedTokens.find(t => t.symbol == pool.token.symbol);
|
|
36
|
+
const token: LendingToken = {
|
|
37
|
+
name: pool.token.name,
|
|
38
|
+
symbol: pool.token.symbol,
|
|
39
|
+
address: savedTokenInfo?.address || '',
|
|
40
|
+
decimals: pool.token.decimals,
|
|
41
|
+
borrowFactor: Web3Number.fromWei(pool.borrow_factor.value, pool.borrow_factor.decimals),
|
|
42
|
+
collareralFactor
|
|
43
|
+
}
|
|
44
|
+
this.tokens.push(token);
|
|
45
|
+
});
|
|
46
|
+
logger.info(`Initialised ${this.metadata.name} with ${this.tokens.length} tokens`);
|
|
47
|
+
this.initialised = true;
|
|
48
|
+
} catch (error: any) {
|
|
49
|
+
return Global.httpError(ZkLend.POOLS_URL, error);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* @description Get the health factor of the user for given lending and debt tokens
|
|
55
|
+
* @param lending_tokens
|
|
56
|
+
* @param debt_tokens
|
|
57
|
+
* @param user
|
|
58
|
+
* @returns hf (e.g. returns 1.5 for 150% health factor)
|
|
59
|
+
*/
|
|
60
|
+
async get_health_factor_tokenwise(lending_tokens: TokenInfo[], debt_tokens: TokenInfo[], user: ContractAddr): Promise<number> {
|
|
61
|
+
const positions = await this.getPositions(user);
|
|
62
|
+
logger.verbose(`${this.metadata.name}:: Positions: ${JSON.stringify(positions)}`);
|
|
63
|
+
|
|
64
|
+
// Computes Sum of debt USD / borrow factor
|
|
65
|
+
let effectiveDebt = new Web3Number(0, 6);
|
|
66
|
+
positions.filter((pos) => {
|
|
67
|
+
return debt_tokens.find((t) => t.symbol === pos.tokenSymbol);
|
|
68
|
+
}).forEach((pos) => {
|
|
69
|
+
const token = this.tokens.find((t) => t.symbol === pos.tokenSymbol);
|
|
70
|
+
if (!token) {
|
|
71
|
+
throw new FatalError(`Token ${pos.tokenName} not found in ${this.metadata.name}`);
|
|
72
|
+
}
|
|
73
|
+
effectiveDebt = effectiveDebt.plus(pos.debtUSD.dividedBy(token.borrowFactor.toFixed(6)).toString());
|
|
74
|
+
});
|
|
75
|
+
logger.verbose(`${this.metadata.name}:: Effective debt: ${effectiveDebt}`);
|
|
76
|
+
if (effectiveDebt.isZero()) {
|
|
77
|
+
return Infinity;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Computs Sum of collateral USD * collateral factor
|
|
81
|
+
let effectiveCollateral = new Web3Number(0, 6);
|
|
82
|
+
positions.filter(pos => {
|
|
83
|
+
const exp1 = lending_tokens.find((t) => t.symbol === pos.tokenSymbol);
|
|
84
|
+
const exp2 = pos.marginType === MarginType.SHARED;
|
|
85
|
+
return exp1 && exp2;
|
|
86
|
+
}).forEach((pos) => {
|
|
87
|
+
const token = this.tokens.find((t) => t.symbol === pos.tokenSymbol);
|
|
88
|
+
if (!token) {
|
|
89
|
+
throw new FatalError(`Token ${pos.tokenName} not found in ${this.metadata.name}`);
|
|
90
|
+
}
|
|
91
|
+
logger.verbose(`${this.metadata.name}:: Token: ${pos.tokenName}, Collateral factor: ${token.collareralFactor.toFixed(6)}`);
|
|
92
|
+
effectiveCollateral = effectiveCollateral.plus(pos.supplyUSD.multipliedBy(token.collareralFactor.toFixed(6)).toString());
|
|
93
|
+
});
|
|
94
|
+
logger.verbose(`${this.metadata.name}:: Effective collateral: ${effectiveCollateral}`);
|
|
95
|
+
|
|
96
|
+
// Health factor = Effective collateral / Effective debt
|
|
97
|
+
const healthFactor = effectiveCollateral.dividedBy(effectiveDebt.toFixed(6)).toNumber();
|
|
98
|
+
logger.verbose(`${this.metadata.name}:: Health factor: ${healthFactor}`);
|
|
99
|
+
return healthFactor;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* @description Get the health factor of the user
|
|
104
|
+
* - Considers all tokens for collateral and debt
|
|
105
|
+
*/
|
|
106
|
+
async get_health_factor(user: ContractAddr): Promise<number> {
|
|
107
|
+
return this.get_health_factor_tokenwise(this.tokens, this.tokens, user);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async getPositionsSummary(user: ContractAddr): Promise<{
|
|
111
|
+
collateralUSD: number,
|
|
112
|
+
debtUSD: number,
|
|
113
|
+
}> {
|
|
114
|
+
const pos = await this.getPositions(user);
|
|
115
|
+
const collateralUSD = pos.reduce((acc, p) => acc + p.supplyUSD.toNumber(), 0);
|
|
116
|
+
const debtUSD = pos.reduce((acc, p) => acc + p.debtUSD.toNumber(), 0);
|
|
117
|
+
return {
|
|
118
|
+
collateralUSD,
|
|
119
|
+
debtUSD
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* @description Get the token-wise collateral and debt positions of the user
|
|
124
|
+
* @param user Contract address of the user
|
|
125
|
+
* @returns Promise<ILendingPosition[]>
|
|
126
|
+
*/
|
|
127
|
+
async getPositions(user: ContractAddr): Promise<ILendingPosition[]> {
|
|
128
|
+
const url = this.POSITION_URL.replace('{{USER_ADDR}}', user.address);
|
|
129
|
+
/**
|
|
130
|
+
* Sample response:
|
|
131
|
+
{"pools":[{"data":{"debt_amount":"0x0","is_collateral":false,"supply_amount":"0x0","wallet_balance":"0x0"},
|
|
132
|
+
"token_symbol":"ETH"},{"data":{"debt_amount":"0x0","is_collateral":false,"supply_amount":"0x0",
|
|
133
|
+
"wallet_balance":"0x0"},"token_symbol":"USDC"}]}
|
|
134
|
+
*/
|
|
135
|
+
const result = await axios.get(url);
|
|
136
|
+
const data: any = result.data;
|
|
137
|
+
const lendingPosition: ILendingPosition[] = [];
|
|
138
|
+
logger.verbose(`${this.metadata.name}:: Positions: ${JSON.stringify(data)}`);
|
|
139
|
+
for(let i=0; i<data.pools.length; i++) {
|
|
140
|
+
const pool = data.pools[i];
|
|
141
|
+
const token = this.tokens.find((t) => {
|
|
142
|
+
return t.symbol === pool.token_symbol
|
|
143
|
+
});
|
|
144
|
+
if (!token) {
|
|
145
|
+
throw new FatalError(`Token ${pool.token_symbol} not found in ${this.metadata.name}`);
|
|
146
|
+
}
|
|
147
|
+
const debtAmount = Web3Number.fromWei(pool.data.debt_amount, token.decimals);
|
|
148
|
+
const supplyAmount = Web3Number.fromWei(pool.data.supply_amount, token.decimals);
|
|
149
|
+
const price = (await this.pricer.getPrice(token.symbol)).price;
|
|
150
|
+
lendingPosition.push({
|
|
151
|
+
tokenName: token.name,
|
|
152
|
+
tokenSymbol: token.symbol,
|
|
153
|
+
marginType: pool.data.is_collateral ? MarginType.SHARED : MarginType.NONE,
|
|
154
|
+
debtAmount,
|
|
155
|
+
debtUSD: debtAmount.multipliedBy(price.toFixed(6)),
|
|
156
|
+
supplyAmount,
|
|
157
|
+
supplyUSD: supplyAmount.multipliedBy(price.toFixed(6))
|
|
158
|
+
});
|
|
159
|
+
};
|
|
160
|
+
return lendingPosition;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './pricer-redis';
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { FatalError, Global, logger } from '@/global';
|
|
2
|
+
import { IConfig, TokenInfo } from '@/interfaces';
|
|
3
|
+
import { PriceInfo, Pricer } from '@/modules/pricer';
|
|
4
|
+
import { createClient } from 'redis';
|
|
5
|
+
import type { RedisClientType } from 'redis'
|
|
6
|
+
|
|
7
|
+
export class PricerRedis extends Pricer {
|
|
8
|
+
private redisClient: RedisClientType | null = null;
|
|
9
|
+
constructor(config: IConfig, tokens: TokenInfo[]) {
|
|
10
|
+
super(config, tokens)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** Reads prices from Pricer._loadPrices and uses a callback to set prices in redis */
|
|
14
|
+
async startWithRedis(redisUrl: string) {
|
|
15
|
+
await this.initRedis(redisUrl);
|
|
16
|
+
|
|
17
|
+
logger.info(`Starting Pricer with Redis`);
|
|
18
|
+
this._loadPrices(this._setRedisPrices.bind(this));
|
|
19
|
+
setInterval(() => {
|
|
20
|
+
this._loadPrices(this._setRedisPrices.bind(this));
|
|
21
|
+
}, 30000);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async close() {
|
|
25
|
+
if (this.redisClient) {
|
|
26
|
+
await this.redisClient.disconnect();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async initRedis(redisUrl: string) {
|
|
31
|
+
logger.info(`Initialising Redis Client`);
|
|
32
|
+
this.redisClient = <RedisClientType>(await createClient({
|
|
33
|
+
url: redisUrl
|
|
34
|
+
}));
|
|
35
|
+
this.redisClient.on('error', (err: any) => console.log('Redis Client Error', err))
|
|
36
|
+
.connect();
|
|
37
|
+
logger.info(`Redis Client Initialised`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** sets current local price in redis */
|
|
41
|
+
private _setRedisPrices(tokenSymbol: string) {
|
|
42
|
+
if (!this.redisClient) {
|
|
43
|
+
throw new FatalError(`Redis client not initialised`);
|
|
44
|
+
}
|
|
45
|
+
this.redisClient.set(`Price:${tokenSymbol}`, JSON.stringify(this.prices[tokenSymbol]))
|
|
46
|
+
.catch(err => {
|
|
47
|
+
logger.warn(`Error setting price in redis for ${tokenSymbol}`);
|
|
48
|
+
})
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Returns price from redis */
|
|
52
|
+
async getPrice(tokenSymbol: string) {
|
|
53
|
+
const STALE_TIME = 60000;
|
|
54
|
+
if (!this.redisClient) {
|
|
55
|
+
throw new FatalError(`Redis client not initialised`);
|
|
56
|
+
}
|
|
57
|
+
const data = await this.redisClient.get(`Price:${tokenSymbol}`);
|
|
58
|
+
if (!data) {
|
|
59
|
+
throw new FatalError(`Redis:Price of ${tokenSymbol} not found`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
logger.verbose(`Redis:Price of ${tokenSymbol} is ${data}`);
|
|
63
|
+
|
|
64
|
+
const priceInfo: PriceInfo = JSON.parse(data);
|
|
65
|
+
priceInfo.timestamp = new Date(priceInfo.timestamp);
|
|
66
|
+
const isStale = (new Date().getTime() - priceInfo.timestamp.getTime()) > STALE_TIME;
|
|
67
|
+
Global.assert(!isStale, `Price of ${tokenSymbol} is stale`);
|
|
68
|
+
return priceInfo;
|
|
69
|
+
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './telegram';
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { logger } from "@/global";
|
|
2
|
+
import TelegramBot from "node-telegram-bot-api";
|
|
3
|
+
|
|
4
|
+
export class TelegramNotif {
|
|
5
|
+
private subscribers: string[] = [
|
|
6
|
+
// '6820228303',
|
|
7
|
+
'1505578076',
|
|
8
|
+
// '5434736198', // maaza
|
|
9
|
+
'1356705582', // langs
|
|
10
|
+
'1388729514', // hwashere
|
|
11
|
+
'6020162572', //minato
|
|
12
|
+
'985902592'
|
|
13
|
+
];
|
|
14
|
+
readonly bot: TelegramBot;
|
|
15
|
+
|
|
16
|
+
constructor(token: string, shouldPoll: boolean) {
|
|
17
|
+
this.bot = new TelegramBot(token, { polling: shouldPoll });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// listen to start msgs, register chatId and send registered msg
|
|
21
|
+
activateChatBot() {
|
|
22
|
+
this.bot.on('message', (msg: any) => {
|
|
23
|
+
const chatId = msg.chat.id;
|
|
24
|
+
let text = msg.text.toLowerCase().trim()
|
|
25
|
+
logger.verbose(`Tg: IncomingMsg: ID: ${chatId}, msg: ${text}`)
|
|
26
|
+
if(text=='start') {
|
|
27
|
+
this.bot.sendMessage(chatId, "Registered")
|
|
28
|
+
this.subscribers.push(chatId)
|
|
29
|
+
logger.verbose(`Tg: New subscriber: ${chatId}`);
|
|
30
|
+
} else {
|
|
31
|
+
this.bot.sendMessage(chatId, "Unrecognized command. Supported commands: start");
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// send a given msg to all registered users
|
|
37
|
+
sendMessage(msg: string) {
|
|
38
|
+
logger.verbose(`Tg: Sending message: ${msg}`);
|
|
39
|
+
for (let chatId of this.subscribers) {
|
|
40
|
+
this.bot.sendMessage(chatId, msg).catch((err: any) => {
|
|
41
|
+
logger.error(`Tg: Error sending msg to ${chatId}`);
|
|
42
|
+
logger.error(`Tg: Error sending message: ${err.message}`);
|
|
43
|
+
}).then(() => {
|
|
44
|
+
logger.verbose(`Tg: Message sent to ${chatId}`);
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { ContractAddr, Web3Number } from "@/dataTypes";
|
|
2
|
+
import { IConfig } from "@/interfaces";
|
|
3
|
+
import { Contract, uint256 } from "starknet";
|
|
4
|
+
import { Pricer } from "@/modules/pricer";
|
|
5
|
+
|
|
6
|
+
export class AutoCompounderSTRK {
|
|
7
|
+
readonly config: IConfig;
|
|
8
|
+
readonly addr = ContractAddr.from('0x541681b9ad63dff1b35f79c78d8477f64857de29a27902f7298f7b620838ea');
|
|
9
|
+
readonly pricer: Pricer;
|
|
10
|
+
private initialized: boolean = false;
|
|
11
|
+
|
|
12
|
+
contract: Contract | null = null;
|
|
13
|
+
|
|
14
|
+
readonly metadata = {
|
|
15
|
+
decimals: 18,
|
|
16
|
+
underlying: {
|
|
17
|
+
// zSTRK
|
|
18
|
+
address: ContractAddr.from('0x06d8fa671ef84f791b7f601fa79fea8f6ceb70b5fa84189e3159d532162efc21'),
|
|
19
|
+
name: 'STRK',
|
|
20
|
+
symbol: 'STRK',
|
|
21
|
+
},
|
|
22
|
+
name: 'AutoCompounderSTRK',
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
constructor(config: IConfig, pricer: Pricer) {
|
|
26
|
+
this.config = config;
|
|
27
|
+
this.pricer = pricer;
|
|
28
|
+
this.init();
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async init() {
|
|
32
|
+
const cls = await this.config.provider.getClassAt(this.addr.address);
|
|
33
|
+
this.contract = new Contract(cls.abi, this.addr.address, this.config.provider);
|
|
34
|
+
this.initialized = true;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async waitForInitilisation() {
|
|
38
|
+
return new Promise<void>((resolve, reject) => {
|
|
39
|
+
const interval = setInterval(() => {
|
|
40
|
+
if (this.initialized) {
|
|
41
|
+
clearInterval(interval);
|
|
42
|
+
resolve();
|
|
43
|
+
}
|
|
44
|
+
}, 1000);
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Returns shares of user */
|
|
49
|
+
async balanceOf(user: ContractAddr) {
|
|
50
|
+
const result = await this.contract!.balanceOf(user.address);
|
|
51
|
+
return Web3Number.fromWei(result.toString(), this.metadata.decimals);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Returns underlying assets of user */
|
|
55
|
+
async balanceOfUnderlying(user: ContractAddr) {
|
|
56
|
+
const balanceShares = await this.balanceOf(user);
|
|
57
|
+
const assets = await this.contract!.convert_to_assets(uint256.bnToUint256(balanceShares.toWei()));
|
|
58
|
+
return Web3Number.fromWei(assets.toString(), this.metadata.decimals);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Returns usd value of assets */
|
|
62
|
+
async usdBalanceOfUnderlying(user: ContractAddr) {
|
|
63
|
+
const assets = await this.balanceOfUnderlying(user);
|
|
64
|
+
const price = await this.pricer.getPrice(this.metadata.underlying.name);
|
|
65
|
+
const usd = assets.multipliedBy(price.price.toFixed(6))
|
|
66
|
+
return {
|
|
67
|
+
usd,
|
|
68
|
+
assets
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './autoCompounderStrk';
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import * as crypto from 'crypto';
|
|
2
|
+
|
|
3
|
+
export class PasswordJsonCryptoUtil {
|
|
4
|
+
private readonly algorithm = 'aes-256-gcm';
|
|
5
|
+
private readonly keyLength = 32; // 256 bits
|
|
6
|
+
private readonly saltLength = 16; // 128 bits
|
|
7
|
+
private readonly ivLength = 12; // 96 bits for GCM
|
|
8
|
+
private readonly tagLength = 16; // 128 bits
|
|
9
|
+
private readonly pbkdf2Iterations = 100000; // Number of iterations for PBKDF2
|
|
10
|
+
|
|
11
|
+
private deriveKey(password: string, salt: Buffer): Buffer {
|
|
12
|
+
return crypto.pbkdf2Sync(password, salt, this.pbkdf2Iterations, this.keyLength, 'sha256');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
encrypt(data: any, password: string): string {
|
|
16
|
+
// Convert data to JSON string
|
|
17
|
+
const jsonString = JSON.stringify(data);
|
|
18
|
+
|
|
19
|
+
// Generate a random salt and IV
|
|
20
|
+
const salt = crypto.randomBytes(this.saltLength);
|
|
21
|
+
const iv = crypto.randomBytes(this.ivLength);
|
|
22
|
+
|
|
23
|
+
// Derive a key from the password and salt
|
|
24
|
+
const key = this.deriveKey(password, salt);
|
|
25
|
+
|
|
26
|
+
// Create cipher
|
|
27
|
+
const cipher = crypto.createCipheriv(this.algorithm, key, iv, { authTagLength: this.tagLength });
|
|
28
|
+
|
|
29
|
+
// Encrypt the data
|
|
30
|
+
let encrypted = cipher.update(jsonString, 'utf8', 'hex');
|
|
31
|
+
encrypted += cipher.final('hex');
|
|
32
|
+
|
|
33
|
+
// Get the authentication tag
|
|
34
|
+
const tag = cipher.getAuthTag();
|
|
35
|
+
|
|
36
|
+
// Combine all pieces: salt (16 bytes) + iv (12 bytes) + tag (16 bytes) + encrypted data
|
|
37
|
+
return Buffer.concat([salt, iv, tag, Buffer.from(encrypted, 'hex')]).toString('base64');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
decrypt(encryptedData: string, password: string): any {
|
|
41
|
+
// Convert the base64 string back to a buffer
|
|
42
|
+
const data = Buffer.from(encryptedData, 'base64');
|
|
43
|
+
|
|
44
|
+
// Extract the pieces
|
|
45
|
+
const salt = data.subarray(0, this.saltLength);
|
|
46
|
+
const iv = data.subarray(this.saltLength, this.saltLength + this.ivLength);
|
|
47
|
+
const tag = data.subarray(this.saltLength + this.ivLength, this.saltLength + this.ivLength + this.tagLength);
|
|
48
|
+
const encrypted = data.subarray(this.saltLength + this.ivLength + this.tagLength);
|
|
49
|
+
|
|
50
|
+
// Derive the key
|
|
51
|
+
const key = this.deriveKey(password, salt);
|
|
52
|
+
|
|
53
|
+
// Create decipher
|
|
54
|
+
const decipher = crypto.createDecipheriv(this.algorithm, key, iv, { authTagLength: this.tagLength });
|
|
55
|
+
decipher.setAuthTag(tag);
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
// Decrypt the data
|
|
59
|
+
let decrypted = decipher.update(encrypted.toString('hex'), 'hex', 'utf8');
|
|
60
|
+
decrypted += decipher.final('utf8');
|
|
61
|
+
|
|
62
|
+
// Parse the JSON string
|
|
63
|
+
return JSON.parse(decrypted);
|
|
64
|
+
} catch (error) {
|
|
65
|
+
throw new Error('Decryption failed. This could be due to an incorrect password or corrupted data.');
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export * from './store';
|
|
2
|
+
export * from './encrypt';
|
|
3
|
+
|
|
4
|
+
// Utility type to make all optional properties required
|
|
5
|
+
export type RequiredFields<T> = {
|
|
6
|
+
[K in keyof T]-?: T[K]
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// Utility type to get only the required fields of a type
|
|
10
|
+
export type RequiredKeys<T> = {
|
|
11
|
+
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
|
|
12
|
+
}[keyof T]
|