Skip to content

Commit

Permalink
Merge pull request #23 from BeanstalkFarms/lambda
Browse files Browse the repository at this point in the history
Database stores detailed info on each deposit
  • Loading branch information
soilking authored Oct 25, 2024
2 parents c0289fa + 722582e commit ae24173
Show file tree
Hide file tree
Showing 94 changed files with 4,256 additions and 534 deletions.
493 changes: 448 additions & 45 deletions api-spec.yaml

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,10 @@
"jest": {
"testEnvironment": "node",
"setupFiles": [
"<rootDir>/test/jest.setup.js"
"<rootDir>/test/setup/jest.setup.js"
],
"setupFilesAfterEnv": [
"<rootDir>/test/setup/jest.setup-after.js"
]
},
"keywords": [],
Expand All @@ -41,7 +44,7 @@
"node-cron": "^3.0.3",
"pg": "^8.12.0",
"pg-hstore": "^2.3.4",
"sequelize": "^6.37.3",
"sequelize": "^6.37.4",
"sequelize-cli": "^6.6.2"
},
"devDependencies": {
Expand Down
14 changes: 9 additions & 5 deletions src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ const Router = require('koa-router');
const cors = require('@koa/cors');
const { activateJobs } = require('./scheduled/cron-schedule.js');
const { sequelize } = require('./repository/postgres/models/index.js');
const { formatBigintHex } = require('./utils/bigint.js');
const AsyncContext = require('./utils/context.js');
const { formatBigintDecimal } = require('./utils/bigint.js');
const AsyncContext = require('./utils/async/context.js');
const EnvUtil = require('./utils/env.js');
const ChainUtil = require('./utils/chain.js');
const AlchemyUtil = require('./datasources/alchemy.js');
const StartupSeeder = require('./repository/postgres/startup-seeders/startup-seeder.js');

async function appStartup() {
// Activate whichever cron jobs are configured, if any
Expand All @@ -26,11 +27,14 @@ async function appStartup() {
await AlchemyUtil.ready(chain);
}

const app = new Koa();

// This can be useful for local development, though migrations should be used instead
// sequelize.sync();

// Long-running async seeder process, the api will come online before this is complete.
StartupSeeder.seedDatabase();

const app = new Koa();

app.use(
cors({
origin: '*'
Expand Down Expand Up @@ -60,7 +64,7 @@ async function appStartup() {
}
try {
await next(); // pass control to the next function specified in .use()
ctx.body = JSON.stringify(ctx.body, formatBigintHex);
ctx.body = JSON.stringify(ctx.body, formatBigintDecimal);
if (!ctx.originalUrl.includes('healthcheck')) {
console.log(
`${new Date().toISOString()} [success] ${ctx.method} ${ctx.originalUrl} - ${ctx.status} - Response Body: ${ctx.body}`
Expand Down
10 changes: 8 additions & 2 deletions src/constants/raw/beanstalk-arb.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const contracts = {
USDC: ['0xaf88d065e77c8cC2239327C5EDb3A432268e5831', 18, erc20Abi],
USDT: ['0xFd086bC7CD5C481DCC9C85ebE478A1C0b69FCbb9', 18, erc20Abi],
CP2: ['0xbA1500c28C8965521f47F17Fc21A7829D6E1343e', null, wellFunctionAbi],
CP2_121: ['0xba15000450bf6d48ec50bd6327a9403e401b72b4', null, wellFunctionAbi],
CP2_121: ['0xBA15000450Bf6d48ec50BD6327A9403E401b72b4', null, wellFunctionAbi],
STABLE2: ['0xba150052e11591D0648b17A0E608511874921CBC', null, wellFunctionAbi],
STABLE2_121: ['0xba150052e11591D0648b17A0E608511874921CBC', null, wellFunctionAbi]
};
Expand Down Expand Up @@ -57,11 +57,16 @@ SG.BEANSTALK = SubgraphClients.named(SG.BEANSTALK);
SG.BEAN = SubgraphClients.named(SG.BEAN);
SG.BASIN = SubgraphClients.named(SG.BASIN);

const MISC = {
MIN_EMA_SEASON: 6075
};

Object.freeze(ADDRESSES);
Object.freeze(DECIMALS);
Object.freeze(ABIS);
Object.freeze(MILESTONE);
Object.freeze(SG);
Object.freeze(MISC);

// ** DO NOT USE ANY OF THESE EXPORTS DIRECTLY. USE `C` IN runtime-constants.js ** //
module.exports = {
Expand All @@ -70,5 +75,6 @@ module.exports = {
DECIMALS,
ABIS,
MILESTONE,
SG
SG,
MISC
};
8 changes: 7 additions & 1 deletion src/constants/raw/beanstalk-eth.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,16 @@ SG.BEANSTALK = SubgraphClients.named(SG.BEANSTALK);
SG.BEAN = SubgraphClients.named(SG.BEAN);
SG.BASIN = SubgraphClients.named(SG.BASIN);

const MISC = {
MIN_EMA_SEASON: 6075
};

Object.freeze(ADDRESSES);
Object.freeze(DECIMALS);
Object.freeze(ABIS);
Object.freeze(MILESTONE);
Object.freeze(SG);
Object.freeze(MISC);

// ** DO NOT USE ANY OF THESE EXPORTS DIRECTLY. USE `C` IN runtime-constants.js ** //
module.exports = {
Expand All @@ -62,5 +67,6 @@ module.exports = {
DECIMALS,
ABIS,
MILESTONE,
SG
SG,
MISC
};
16 changes: 9 additions & 7 deletions src/constants/runtime-constants.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
const AlchemyUtil = require('../datasources/alchemy');
const AsyncContext = require('../utils/context');
const AsyncContext = require('../utils/async/context');
const EnvUtil = require('../utils/env');
const BeanstalkEth = require('./raw/beanstalk-eth');
const BeanstalkArb = require('./raw/beanstalk-arb');
const { isNil } = require('../utils/bigint');

const C_MAPPING = {
eth: BeanstalkEth,
Expand All @@ -13,6 +14,9 @@ const C_MAPPING = {
// i.e. jest.spyOn(RuntimeConstants, 'proxyUnderlying').mockReturnValue(4);
class RuntimeConstants {
static proxyUnderlying({ chain, season }) {
if (isNil(chain) && isNil(season)) {
throw new Error(`One of chain/season must be provided.`);
}
return new Proxy({}, RuntimeConstants._makeProxyHandler(chain, season));
}

Expand All @@ -30,8 +34,8 @@ class RuntimeConstants {
}
let value = constants[property];
if (!value) {
// Secondarily search for the property among the addresses
value = constants.ADDRESSES[property];
// Secondarily search for the property among the addresses/misc
value = constants.ADDRESSES[property] ?? constants.MISC[property];
}
return value;
}
Expand Down Expand Up @@ -60,10 +64,8 @@ class RuntimeConstants {
// C(chain).DECIMALS[token]
const C = (opt) => {
if (!opt) {
let defaultChain;
try {
defaultChain = AsyncContext.get('chain');
} catch (e) {
let defaultChain = AsyncContext.getOrUndef('chain');
if (!defaultChain) {
// If there is no async context, this is from a system process/non rest. Use default configured chain
defaultChain = EnvUtil.defaultChain();
}
Expand Down
4 changes: 3 additions & 1 deletion src/datasources/contracts/contracts.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
const { Contract: AlchemyContract } = require('alchemy-sdk');
const { C } = require('../../constants/runtime-constants');
const SuperContract = require('./super-contract');
const wellFunctionAbi = require('../../datasources/abi/basin/WellFunction.json');

class Contracts {
Expand All @@ -18,7 +19,8 @@ class Contracts {
}

static makeContract(address, abi, provider) {
return new AlchemyContract(address, abi, provider);
const underlyingContract = new AlchemyContract(address, abi, provider);
return new SuperContract(underlyingContract);
}

static _getDefaultContract(address, c = C()) {
Expand Down
53 changes: 53 additions & 0 deletions src/datasources/contracts/super-contract.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
const { allToBigInt } = require('../../utils/number');
const retryable = require('../../utils/async/retryable');

// Proxy wrapper to:
// (1) built in retry support for failed requests
// (2) transform all BigNumber -> BigInt
class SuperContract {
constructor(contract) {
const proxyHandler = {
get: (target, property, receiver) => {
if (['interface', 'provider', 'filters', 'address'].includes(property)) {
return contract[property];
}

if (property === 'then') {
return undefined;
}

return async (...args) => {
const rawResult = await retryable(() => contract[property](...args));
return SuperContract._transformAll(rawResult);
};
}
};

return new Proxy(this, proxyHandler);
}

// Transforms everything in this result to be a BigInt.
// Handles arrays/tuples (with named fields), and single values
static _transformAll(rawResult) {
if (!Array.isArray(rawResult)) {
return allToBigInt(rawResult);
} else {
// The raw result is frozen
const transformed = allToBigInt(JSON.parse(JSON.stringify(rawResult)));
// Re assign the convenience names if the return value was tuple
const namedKeys = Object.keys(rawResult).filter(isNaN);
if (namedKeys.length > 0) {
const retval = {};
for (let i = 0; i < namedKeys.length; ++i) {
retval[namedKeys[i]] = transformed[i];
}
return retval;
} else {
// expected result is an array
return transformed;
}
}
}
}

module.exports = SuperContract;
4 changes: 2 additions & 2 deletions src/datasources/contracts/upgradeable/usd-oracle.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,8 @@ class UsdOracle {
// Logic for versions 2 and 3 is the same, but version 3 is the beanstalk contract.
const result =
this.contract.__version() === 1
? BigInt(await this.contract.getUsdPrice(token))
: BigInt(await this.contract.getTokenUsdPrice(token));
? await this.contract.getUsdPrice(token)
: await this.contract.getTokenUsdPrice(token);
// Version 1 returned a twa price, but with no lookback. Its already instantaneous but needs conversion
const instPrice = this.contract.__version() === 1 ? BigInt(10 ** 24) / result : result;
return instPrice;
Expand Down
63 changes: 63 additions & 0 deletions src/datasources/events/deposit-events.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
const AlchemyUtil = require('../alchemy');
const FilterLogs = require('./filter-logs');

const DEPOSIT_EVENTS = ['AddDeposit', 'RemoveDeposit', 'RemoveDeposits'];

class DepositEvents {
// Returns a summary of add/remove deposit events. Collapses RemoveDeposits out of its array form
static async getSiloDepositEvents(fromBlock, toBlock = 'latest') {
const rawEvents = await FilterLogs.getBeanstalkEvents(DEPOSIT_EVENTS, fromBlock, toBlock);
const collapsed = [];
for (const event of rawEvents) {
if (event.name === 'RemoveDeposits') {
for (let i = 0; i < event.args.stems.length; ++i) {
collapsed.push({
type: -1,
account: event.args.account.toLowerCase(),
token: event.args.token.toLowerCase(),
stem: BigInt(event.args.stems[i]),
amount: BigInt(event.args.amounts[i]),
bdv: BigInt(event.args.bdvs[i])
});
}
} else {
collapsed.push({
type: event.name === 'AddDeposit' ? 1 : -1,
account: event.args.account.toLowerCase(),
token: event.args.token.toLowerCase(),
stem: BigInt(event.args.stem),
amount: BigInt(event.args.amount),
bdv: BigInt(event.args.bdv)
});
}
}
return collapsed;
}

// Returns condensed info from StalkBalanceChanged
static async getStalkBalanceChangedEvents(fromBlock, toBlock = 'latest') {
const rawEvents = await FilterLogs.getBeanstalkEvents(['StalkBalanceChanged'], fromBlock, toBlock);
const summary = [];
for (const event of rawEvents) {
summary.push({
account: event.args.account.toLowerCase(),
deltaStalk: BigInt(event.args.delta),
blockNumber: event.rawLog.blockNumber
});
}
return summary;
}
}
module.exports = DepositEvents;

if (require.main === module) {
(async () => {
await AlchemyUtil.ready('arb');
// const logs = await DepositEvents.getSiloDepositEvents(264547404);
// console.log(logs.filter((l) => l.name === 'AddDeposit')[0]);
// console.log(logs.filter((l) => l.name === 'RemoveDeposit')[0]);
// console.log(logs.filter((l) => l.name === 'RemoveDeposits')[0].args.stems);
// console.log(await DepositEvents.getSiloDepositEvents(264547404));
console.log(await DepositEvents.getStalkBalanceChangedEvents(264547404));
})();
}
26 changes: 26 additions & 0 deletions src/datasources/events/filter-logs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
const { C } = require('../../constants/runtime-constants');
const Contracts = require('../contracts/contracts');

class FilterLogs {
// Retrieves beanstalk events matching the requested names
static async getBeanstalkEvents(eventNames, fromBlock, toBlock, c = C()) {
const iBeanstalk = Contracts.getBeanstalk(c).interface;
const topics = eventNames.map((n) => iBeanstalk.getEventTopic(n));

const filter = {
address: c.BEANSTALK,
topics: [topics],
fromBlock,
toBlock
};

const logs = await c.RPC.getLogs(filter);
const events = logs.map((log) => {
const parsed = iBeanstalk.parseLog(log);
parsed.rawLog = log;
return parsed;
});
return events;
}
}
module.exports = FilterLogs;
Loading

0 comments on commit ae24173

Please sign in to comment.