diff --git a/.github/workflows/balancer-js.yaml b/.github/workflows/balancer-js.yaml
index 180f5ab39..525b66ff1 100644
--- a/.github/workflows/balancer-js.yaml
+++ b/.github/workflows/balancer-js.yaml
@@ -90,3 +90,6 @@ env:
INFURA: ${{ secrets.INFURA }}
ALCHEMY_URL: ${{ secrets.ALCHEMY_URL }}
ALCHEMY_URL_GOERLI: ${{ secrets.ALCHEMY_URL_GOERLI }}
+ TENDERLY_ACCESS_KEY: ${{ secrets.TENDERLY_ACCESS_KEY }}
+ TENDERLY_PROJECT: ${{ secrets.TENDERLY_PROJECT }}
+ TENDERLY_USER: ${{ secrets.TENDERLY_USER }}
diff --git a/.github/workflows/beta-release.yaml b/.github/workflows/beta-release.yaml
new file mode 100644
index 000000000..6589fa03c
--- /dev/null
+++ b/.github/workflows/beta-release.yaml
@@ -0,0 +1,51 @@
+name: Beta release
+
+on:
+ pull_request:
+ types:
+ - closed
+ branches:
+ - develop
+
+defaults:
+ run:
+ working-directory: balancer-js
+
+jobs:
+ build_and_release:
+ if: github.event.pull_request.merged == true
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: develop
+ - uses: actions/setup-node@v3
+ with:
+ node-version: 18
+ - uses: actions/cache@v2
+ id: cache
+ with:
+ path: "**/node_modules"
+ key: yarn-v1-${{ hashFiles('**/yarn.lock') }}
+ - run: yarn --immutable
+ if: steps.cache.outputs.cache-hit != 'true'
+ - env:
+ SIGNING_KEY: ${{ secrets.SIGNING_KEY }}
+ run: |
+ echo -n "$SIGNING_KEY" | base64 --decode | gpg --import
+ git config --global user.name "gmbronco"
+ git config --global user.email "gmbronco@users.noreply.github.com"
+ git config user.signingkey A33ABD316E6629F6
+ git config gpg.program /usr/bin/gpg
+ yarn version --prerelease --no-git-tag-version
+ export NEW_VERSION=$(jq -r '.version' package.json)
+ git commit -S -am "chore: version bump v$NEW_VERSION"
+ git tag "v$NEW_VERSION"
+ yarn build
+ yarn publish --non-interactive --tag beta
+ git push
+
+env:
+ CI: true
+ GITHUB_TOKEN: ${{ secrets.RELEASE_PAT }}
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
diff --git a/balancer-js/.gitignore b/balancer-js/.gitignore
index 4f9e5b2ba..322e23c8f 100644
--- a/balancer-js/.gitignore
+++ b/balancer-js/.gitignore
@@ -7,3 +7,4 @@ yarn-error.log
dist/
src/subgraph/generated/
cache/
+balancer-js.iml
diff --git a/balancer-js/IL-class.png b/balancer-js/IL-class.png
new file mode 100644
index 000000000..c458c98a4
Binary files /dev/null and b/balancer-js/IL-class.png differ
diff --git a/balancer-js/README.md b/balancer-js/README.md
index aca9ac9aa..21bfc880a 100644
--- a/balancer-js/README.md
+++ b/balancer-js/README.md
@@ -12,10 +12,14 @@ A JavaScript SDK which provides commonly used utilties for interacting with Bala
4. Create a .env file in the balancer-js folder
5. In the .env file you will need to define and initialize the following variables
+ We have defined both Alchemy and Infura, because some of the examples use Infura, others use Alchemy. However, feel free to modify accordingly and use your favourite one.
ALCHEMY_URL=[ALCHEMY HTTPS ENDPOINT]
INFURA=[Infura API KEY]
TRADER_KEY=[MetaMask PRIVATE KEY]
- We have defined both Alchemy and Infura, because some of the examples use Infura, others use Alchemy. However, feel free to modify accordingly and use your favourite one.
+ Some examples also require the following Tenderly config parameters to be defined:
+ TENDERLY_ACCESS_KEY=[TENDERLY API ACCESS KEY]
+ TENDERLY_PROJECT=[TENDERLY PROJECT NAME]
+ TENDERLY_USER=[TENDERLY USERNAME]
6. Run 'npm run node', this runs a local Hardhat Network
7. Open a new terminal
@@ -285,9 +289,11 @@ async getSpotPrice(
[Example](./examples/spotPrice.ts)
-## Join Pool
+## Joining Pools
-Exposes Join functionality allowing user to join pools.
+### Joining with pool tokens
+
+Exposes Join functionality allowing user to join pools with its pool tokens.
```js
const balancer = new BalancerSDK(sdkConfig);
@@ -295,7 +301,7 @@ const pool = await balancer.pools.find(poolId);
const { to, functionName, attributes, data } = pool.buildJoin(params);
```
-### #buildJoin
+#### #buildJoin
Builds a join transaction.
@@ -317,6 +323,51 @@ buildJoin: (
[Example](./examples/join.ts)
+### Joining nested pools
+
+Exposes Join functionality allowing user to join a pool that has pool tokens that are BPTs of other pools, e.g.:
+
+```
+ CS0
+ / \
+ CS1 CS2
+ / \ / \
+ DAI USDC USDT FRAX
+
+Can join with tokens: DAI, USDC, USDT, FRAX, CS1_BPT, CS2_BPT
+```
+
+```js
+ /**
+ * Builds generalised join transaction
+ *
+ * @param poolId Pool id
+ * @param tokens Token addresses
+ * @param amounts Token amounts in EVM scale
+ * @param userAddress User address
+ * @param wrapMainTokens Indicates whether main tokens should be wrapped before being used
+ * @param slippage Maximum slippage tolerance in bps i.e. 50 = 0.5%.
+ * @param authorisation Optional auhtorisation call to be added to the chained transaction
+ * @returns transaction data ready to be sent to the network along with min and expected BPT amounts out.
+ */
+ async generalisedJoin(
+ poolId: string,
+ tokens: string[],
+ amounts: string[],
+ userAddress: string,
+ wrapMainTokens: boolean,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ minOut: string;
+ expectedOut: string;
+ }>
+```
+
+[Example](./examples/joinGeneralised.ts)
+
## Exit Pool
Exposes Exit functionality allowing user to exit pools.
@@ -371,6 +422,48 @@ Builds an exit transaction with exact tokens out and maximum BPT in based on sli
[Example](./examples/exitExactTokensOut.ts)
+### Exiting nested pools
+
+Exposes Exit functionality allowing user to exit a pool that has pool tokens that are BPTs of other pools, e.g.:
+
+```
+ CS0
+ / \
+ CS1 CS2
+ / \ / \
+ DAI USDC USDT FRAX
+
+Can exit with CS0_BPT proportionally to: DAI, USDC, USDT and FRAX
+```
+
+```js
+/**
+ * Builds generalised exit transaction
+ *
+ * @param poolId Pool id
+ * @param amount Token amount in EVM scale
+ * @param userAddress User address
+ * @param slippage Maximum slippage tolerance in bps i.e. 50 = 0.5%.
+ * @param authorisation Optional auhtorisation call to be added to the chained transaction
+ * @returns transaction data ready to be sent to the network along with tokens, min and expected amounts out.
+ */
+ async generalisedExit(
+ poolId: string,
+ amount: string,
+ userAddress: string,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ tokensOut: string[];
+ expectedAmountsOut: string[];
+ minAmountsOut: string[];
+ }>
+```
+
+[Example](./examples/exitGeneralised.ts)
+
## RelayerService
Relayers are (user opt-in, audited) contracts that can make calls to the vault (with the transaction “sender” being any arbitrary address) and use the sender’s ERC20 vault allowance, internal balance or BPTs on their behalf.
@@ -476,6 +569,66 @@ async relayer.exitPoolAndBatchSwap(
[Example](./examples/relayerExitPoolAndBatchSwap.ts)
+### Pools Impermanent Loss
+
+> DRAFT
+>
+> impermanent loss (IL) describes the percentage by which a pool is worth less than what one would have if they had instead just held the tokens outside the pool
+
+
+#### Service
+
+![class-diagram](IL-class.png)
+
+#### Algorithm
+
+Using the variation delta formula:
+
+![img.png](img.png)
+
+where **𝚫Pi** represents the difference between the price for a single token at the date of joining the pool and the current price.
+
+```javascript
+
+// retrieves pool's tokens
+tokens = pool.tokens;
+// get weights for tokens
+weights = tokens.map((token) => token.weight);
+// retrieves current price for tokens
+exitPrices = tokens.map((token) => tokenPrices.find(token.address));
+// retrieves historical price for tokens
+entryPrices = tokens.map((token) => tokenPrices.findBy('timestamp', { address: token.address, timestamp: timestamp}));
+// retrieves list of pool's assets with prices delta and weights
+assets = tokens.map((token) => ({
+ priceDelta: this.getDelta(entryPrices[token.address], exitPrices[token.address]),
+ weight: weights[i],
+}));
+
+poolValueDelta = assets.reduce((result, asset) => result * Math.pow(Math.abs(asset.priceDelta + 1), asset.weight), 1);
+holdValueDelta = assets.reduce((result, asset) => result + (Math.abs(asset.priceDelta + 1) * asset.weight), 0);
+
+const IL = poolValueDelta/holdValueDelta - 1;
+```
+
+#### Usage
+
+```javascript
+async impermanentLoss(
+ timestamp: number, // the UNIX timestamp from which the IL is desired
+ pool: Pool // the pool on which the IL must be calculated
+): Promise
+```
+
+```javascript
+const pool = await sdk.pools.find(poolId);
+const joins = (await sdk.data.findByUser(userAddress)).filter((it) => it.type === "Join" && it.poolId === poolId);
+const join = joins[0];
+const IL = await pools.impermanentLoss(join.timestamp, pool);
+```
+
+[Example](./examples/pools/impermanentLoss.ts)
+
+
## Licensing
[GNU General Public License Version 3 (GPL v3)](../../LICENSE).
diff --git a/balancer-js/examples/data/pool-joinExit.ts b/balancer-js/examples/data/pool-joinExit.ts
new file mode 100644
index 000000000..9ecaad5ad
--- /dev/null
+++ b/balancer-js/examples/data/pool-joinExit.ts
@@ -0,0 +1,61 @@
+import { BalancerSDK, Network } from '../../src';
+import { InvestType } from '../../src/modules/subgraph/generated/balancer-subgraph-types';
+import { PoolJoinExit } from '../../src/modules/data/pool-joinExit';
+
+// Balancer subgraph : https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2
+
+// npm run examples:run -- ./examples/data/pool-joinExit.ts
+
+const sdk = new BalancerSDK({
+ network: Network.POLYGON,
+ rpcUrl: '',
+});
+const { poolJoinExits } = sdk.data;
+
+const format = (result: PoolJoinExit[]): string => {
+ return result
+ .map(
+ (it) =>
+ `${it.poolId}\t${it.type}\t${new Date(
+ it.timestamp * 1000
+ ).toLocaleString()}`
+ )
+ .join('\n');
+};
+
+(async function () {
+ const USER_ADDR = '0xdfe6e354ce787944e67cc04ad4404a43f3112a10';
+ const POOL_ID =
+ '0x36128d5436d2d70cab39c9af9cce146c38554ff0000100000000000000000008';
+ let result;
+
+ result = await poolJoinExits.findByPool(POOL_ID, 5);
+ if (result.length) {
+ const item = result[0];
+ console.log(
+ `Pool JoinExit by Pool Id:\n${item.type}\t${new Date(
+ item.timestamp * 1000
+ ).toLocaleString()}\t${item.tokens}`
+ );
+ }
+
+ result = await poolJoinExits.findByUser(USER_ADDR, 5);
+ console.log(`Pool JoinExit by User:\n${format(result)}`);
+
+ const poolId = result[0].poolId;
+
+ result = await poolJoinExits.query({
+ where: { pool: poolId, sender: USER_ADDR },
+ });
+ console.log(`Pool JoinExit Query by PoolId and User:\n${format(result)}`);
+
+ result = await poolJoinExits.findJoins(USER_ADDR, poolId);
+ console.log(
+ `Pool JoinExit Query by PoolId and User and Type Join:\n${format(result)}`
+ );
+
+ result = await poolJoinExits.findExits(USER_ADDR, poolId);
+ console.log(
+ `Pool JoinExit Query by PoolId and User and Type Exit:\n${format(result)}`
+ );
+})();
diff --git a/balancer-js/examples/data/token-prices.ts b/balancer-js/examples/data/token-prices.ts
new file mode 100644
index 000000000..51a6984ce
--- /dev/null
+++ b/balancer-js/examples/data/token-prices.ts
@@ -0,0 +1,20 @@
+/**
+ * Display APRs for pool ids hardcoded under `const ids`
+ * Run command: yarn examples:run ./examples/data/token-prices.ts
+ */
+import { BalancerSDK } from '@/.';
+
+const sdk = new BalancerSDK({ network: 1, rpcUrl: '' });
+const { data } = sdk;
+const dai = '0x6b175474e89094c44da98b954eedeac495271d0f';
+const eth = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE';
+const matic = '0x0000000000000000000000000000000000001010';
+const tetuBal = '0x7fc9e0aa043787bfad28e29632ada302c790ce33';
+
+(async () => {
+ // It will be just one request to coingecko
+ const ps = [eth, dai, tetuBal, matic, eth, dai, tetuBal, matic].map((t) => data.tokenPrices.find(t));
+ const price = await Promise.all(ps);
+
+ console.log(price);
+})();
diff --git a/balancer-js/examples/data/token-yields.ts b/balancer-js/examples/data/token-yields.ts
index 46f241bd4..bfb247db2 100644
--- a/balancer-js/examples/data/token-yields.ts
+++ b/balancer-js/examples/data/token-yields.ts
@@ -8,10 +8,20 @@ import { yieldTokens } from '../../src/modules/data/token-yields/tokens/aave';
const sdk = new BalancerSDK({ network: 1, rpcUrl: '' });
const { data } = sdk;
+const tokens = [
+ yieldTokens[1].waDAI,
+ '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', // stMatic
+ '0xae7ab96520de3a18e5e111b5eaab095312d7fe84' // stETH
+]
+
const main = async () => {
- const tokenYield = await data.tokenYields.find(yieldTokens.waDAI);
+ const yields = await Promise.all(
+ tokens.map((token) => data.tokenYields.find(token))
+ )
- console.log(yieldTokens.waDAI, tokenYield);
+ tokens.forEach((token, id) => {
+ console.log(token, yields[id])
+ })
};
main();
diff --git a/balancer-js/examples/exitGeneralised.ts b/balancer-js/examples/exitGeneralised.ts
new file mode 100644
index 000000000..f9add8b34
--- /dev/null
+++ b/balancer-js/examples/exitGeneralised.ts
@@ -0,0 +1,139 @@
+import dotenv from 'dotenv';
+import { JsonRpcProvider } from '@ethersproject/providers';
+import { parseFixed } from '@ethersproject/bignumber';
+import { BalancerSDK, Network } from '../src/index';
+import { forkSetup, getBalances } from '../src/test/lib/utils';
+import { ADDRESSES } from '../src/test/lib/constants';
+import { Relayer } from '../src/modules/relayer/relayer.module';
+import { Contracts } from '../src/modules/contracts/contracts.module';
+
+dotenv.config();
+
+const {
+ ALCHEMY_URL_GOERLI: jsonRpcUrl,
+ TENDERLY_ACCESS_KEY,
+ TENDERLY_PROJECT,
+ TENDERLY_USER,
+} = process.env;
+const network = Network.GOERLI;
+const blockNumber = 7890980;
+const rpcUrl = 'http://127.0.0.1:8000';
+const addresses = ADDRESSES[network];
+const bbausd2 = {
+ id: addresses.bbausd2?.id as string,
+ address: addresses.bbausd2?.address as string,
+ decimals: addresses.bbausd2?.decimals,
+ slot: addresses.bbausd2?.slot as number,
+};
+
+// Setup local fork with correct balances/approval to exit bb-a-usd2 pool
+async function setUp(provider: JsonRpcProvider): Promise {
+ const signer = provider.getSigner();
+ const signerAddress = await signer.getAddress();
+
+ const mainTokens = [bbausd2.address];
+ const mainInitialBalances = [parseFixed('10', bbausd2.decimals).toString()];
+ const mainSlots = [bbausd2.slot];
+
+ await forkSetup(
+ signer,
+ mainTokens,
+ mainSlots,
+ mainInitialBalances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+
+ const { contracts, contractAddresses } = new Contracts(
+ network as number,
+ provider
+ );
+
+ return await Relayer.signRelayerApproval(
+ contractAddresses.relayerV4 as string,
+ signerAddress,
+ signer,
+ contracts.vault
+ );
+}
+
+/*
+Example showing how to use the SDK generalisedExit method.
+This allows exiting a ComposableStable that has nested pools, e.g.:
+ CS0
+ / \
+ CS1 CS2
+ / \ / \
+ DAI USDC USDT FRAX
+
+Can exit with CS0_BPT proportionally to: DAI, USDC, USDT and FRAX
+*/
+async function exit() {
+ const provider = new JsonRpcProvider(rpcUrl, network);
+ // Local fork setup
+ const relayerAuth = await setUp(provider);
+
+ const signer = provider.getSigner();
+ const signerAddress = await signer.getAddress();
+ const slippage = '100'; // 100 bps = 1%
+
+ // Here we exit with bb-a-usd BPT
+ const amount = parseFixed('10', bbausd2.decimals).toString();
+
+ // Custom Tenderly configuration parameters - remove in order to use default values
+ const tenderlyConfig = {
+ accessKey: TENDERLY_ACCESS_KEY as string,
+ user: TENDERLY_USER as string,
+ project: TENDERLY_PROJECT as string,
+ blockNumber,
+ };
+
+ const balancer = new BalancerSDK({
+ network,
+ rpcUrl,
+ customSubgraphUrl:
+ 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2-beta',
+ tenderly: tenderlyConfig,
+ });
+
+ // Use SDK to create exit transaction
+ const query = await balancer.pools.generalisedExit(
+ bbausd2.id,
+ amount,
+ signerAddress,
+ slippage,
+ relayerAuth
+ );
+
+ // Checking balances to confirm success
+ const tokenBalancesBefore = (
+ await getBalances(
+ [bbausd2.address, ...query.tokensOut],
+ signer,
+ signerAddress
+ )
+ ).map((b) => b.toString());
+
+ // Submit exit tx
+ const transactionResponse = await signer.sendTransaction({
+ to: query.to,
+ data: query.callData,
+ });
+
+ await transactionResponse.wait();
+ const tokenBalancesAfter = (
+ await getBalances(
+ [bbausd2.address, ...query.tokensOut],
+ signer,
+ signerAddress
+ )
+ ).map((b) => b.toString());
+
+ console.log('Balances before exit: ', tokenBalancesBefore);
+ console.log('Balances after exit: ', tokenBalancesAfter);
+ console.log('Expected amounts out: ', [...query.expectedAmountsOut]);
+ console.log('Min amounts out: ', [...query.minAmountsOut]);
+}
+
+// yarn examples:run ./examples/exitGeneralised.ts
+exit();
diff --git a/balancer-js/examples/join.ts b/balancer-js/examples/join.ts
index 76d7afa6f..9c2c3cfc4 100644
--- a/balancer-js/examples/join.ts
+++ b/balancer-js/examples/join.ts
@@ -14,13 +14,6 @@ dotenv.config();
const { ALCHEMY_URL: jsonRpcUrl } = process.env;
-// Slots used to set the account balance for each token through hardhat_setStorageAt
-// Info fetched using npm package slot20
-const wBTC_SLOT = 0;
-const wETH_SLOT = 3;
-const slots = [wBTC_SLOT, wETH_SLOT];
-const initialBalances = ['10000000', '1000000000000000000'];
-
/*
Example showing how to use Pools module to join pools.
*/
@@ -31,12 +24,20 @@ async function join() {
const signer = provider.getSigner();
const signerAddress = await signer.getAddress();
- const poolId =
- '0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e'; // 50/50 WBTC/WETH Pool
+ // 50/50 WBTC/WETH Pool
+ const poolId = ADDRESSES[network].WBTCWETH?.id as string;
+ // Tokens that will be provided to pool by joiner
const tokensIn = [
ADDRESSES[network].WBTC?.address,
ADDRESSES[network].WETH?.address,
- ]; // Tokens that will be provided to pool by joiner
+ ] as string[];
+ // Slots used to set the account balance for each token through hardhat_setStorageAt
+ // Info fetched using npm package slot20
+ const slots = [
+ ADDRESSES[network].WBTC?.slot,
+ ADDRESSES[network].WETH?.slot,
+ ] as number[];
+
const amountsIn = ['10000000', '1000000000000000000'];
const slippage = '100'; // 100 bps = 1%
@@ -47,13 +48,7 @@ async function join() {
const balancer = new BalancerSDK(sdkConfig);
// Sets up local fork granting signer initial balances and token approvals
- await forkSetup(
- signer,
- tokensIn as string[],
- slots,
- initialBalances,
- jsonRpcUrl as string
- );
+ await forkSetup(signer, tokensIn, slots, amountsIn, jsonRpcUrl as string);
// Use SDK to find pool info
const pool: PoolWithMethods | undefined = await balancer.pools.find(poolId);
@@ -67,16 +62,13 @@ async function join() {
// Use SDK to create join
const { to, data, minBPTOut } = pool.buildJoin(
signerAddress,
- tokensIn as string[],
+ tokensIn,
amountsIn,
slippage
);
// Calculate price impact
- const priceImpact = await pool.calcPriceImpact(
- amountsIn as string[],
- minBPTOut
- );
+ const priceImpact = await pool.calcPriceImpact(amountsIn, minBPTOut, true);
// Submit join tx
const transactionResponse = await signer.sendTransaction({
diff --git a/balancer-js/examples/joinGeneralised.ts b/balancer-js/examples/joinGeneralised.ts
new file mode 100644
index 000000000..81940b161
--- /dev/null
+++ b/balancer-js/examples/joinGeneralised.ts
@@ -0,0 +1,157 @@
+import dotenv from 'dotenv';
+import { JsonRpcProvider } from '@ethersproject/providers';
+import { parseFixed } from '@ethersproject/bignumber';
+import { BalancerSDK, Network } from '../src/index';
+import { forkSetup, getBalances } from '../src/test/lib/utils';
+import { ADDRESSES } from '../src/test/lib/constants';
+import { Relayer } from '../src/modules/relayer/relayer.module';
+import { Contracts } from '../src/modules/contracts/contracts.module';
+
+dotenv.config();
+
+const {
+ ALCHEMY_URL_GOERLI: jsonRpcUrl,
+ TENDERLY_ACCESS_KEY,
+ TENDERLY_PROJECT,
+ TENDERLY_USER,
+} = process.env;
+const network = Network.GOERLI;
+const blockNumber = 7890980;
+const rpcUrl = 'http://127.0.0.1:8000';
+const addresses = ADDRESSES[network];
+
+// Setup local fork with correct balances/approval to join pool with DAI/USDC/bbaDAI/bbaUSDC
+async function setUp(provider: JsonRpcProvider): Promise {
+ const signer = provider.getSigner();
+ const signerAddress = await signer.getAddress();
+
+ const mainTokens = [addresses.DAI.address, addresses.USDC.address];
+ const mainInitialBalances = [
+ parseFixed('100', addresses.DAI.decimals).toString(),
+ parseFixed('100', addresses.USDC.decimals).toString(),
+ ];
+ const mainSlots = [
+ addresses.DAI.slot as number,
+ addresses.USDC.slot as number,
+ ];
+
+ const linearPoolTokens = [
+ addresses.bbadai?.address as string,
+ addresses.bbausdc?.address as string,
+ ];
+ const linearInitialBalances = [
+ parseFixed('100', addresses.bbadai?.decimals).toString(),
+ parseFixed('100', addresses.bbausdc?.decimals).toString(),
+ ];
+ const linearPoolSlots = [
+ addresses.bbadai?.slot as number,
+ addresses.bbausdc?.slot as number,
+ ];
+
+ await forkSetup(
+ signer,
+ [...mainTokens, ...linearPoolTokens],
+ [...mainSlots, ...linearPoolSlots],
+ [...mainInitialBalances, ...linearInitialBalances],
+ jsonRpcUrl as string,
+ blockNumber
+ );
+
+ const { contracts, contractAddresses } = new Contracts(
+ network as number,
+ provider
+ );
+
+ return await Relayer.signRelayerApproval(
+ contractAddresses.relayerV4 as string,
+ signerAddress,
+ signer,
+ contracts.vault
+ );
+}
+
+/*
+Example showing how to use the SDK generalisedJoin method.
+This allows joining of a ComposableStable that has nested pools, e.g.:
+ CS0
+ / \
+ CS1 CS2
+ / \ / \
+ DAI USDC USDT FRAX
+
+Can join with tokens: DAI, USDC, USDT, FRAX, CS1_BPT, CS2_BPT
+*/
+async function join() {
+ const provider = new JsonRpcProvider(rpcUrl, network);
+ // Local fork setup
+ const relayerAuth = await setUp(provider);
+
+ const signer = provider.getSigner();
+ const signerAddress = await signer.getAddress();
+ const wrapLeafTokens = false;
+ const slippage = '100'; // 100 bps = 1%
+ const bbausd2 = {
+ id: addresses.bbausd2?.id as string,
+ address: addresses.bbausd2?.address as string,
+ };
+ // Here we join with USDC and bbadai
+ const tokensIn = [
+ addresses.USDC.address,
+ addresses.bbadai?.address as string,
+ ];
+ const amountsIn = [
+ parseFixed('10', 6).toString(),
+ parseFixed('10', 18).toString(),
+ ];
+
+ // Custom Tenderly configuration parameters - remove in order to use default values
+ const tenderlyConfig = {
+ accessKey: TENDERLY_ACCESS_KEY as string,
+ user: TENDERLY_USER as string,
+ project: TENDERLY_PROJECT as string,
+ blockNumber,
+ };
+
+ const balancer = new BalancerSDK({
+ network,
+ rpcUrl,
+ customSubgraphUrl:
+ 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2-beta',
+ tenderly: tenderlyConfig,
+ });
+
+ // Checking balances to confirm success
+ const tokenBalancesBefore = (
+ await getBalances([bbausd2.address, ...tokensIn], signer, signerAddress)
+ ).map((b) => b.toString());
+
+ // Use SDK to create join
+ const query = await balancer.pools.generalisedJoin(
+ bbausd2.id,
+ tokensIn,
+ amountsIn,
+ signerAddress,
+ wrapLeafTokens,
+ slippage,
+ relayerAuth
+ );
+
+ // Submit join tx
+ const transactionResponse = await signer.sendTransaction({
+ to: query.to,
+ data: query.callData,
+ });
+
+ await transactionResponse.wait();
+ const tokenBalancesAfter = (
+ await getBalances([bbausd2.address, ...tokensIn], signer, signerAddress)
+ ).map((b) => b.toString());
+
+ console.log('Balances before exit: ', tokenBalancesBefore);
+ console.log('Balances after exit: ', tokenBalancesAfter);
+ console.log('Expected BPT after exit: ', [query.expectedOut]);
+ console.log('Min BPT after exit: ', [query.minOut]);
+}
+
+// yarn examples:run ./examples/joinGeneralised.ts
+join();
diff --git a/balancer-js/examples/pools/aprs.polygon.ts b/balancer-js/examples/pools/aprs.polygon.ts
index e75c8b1cb..d91943f3e 100644
--- a/balancer-js/examples/pools/aprs.polygon.ts
+++ b/balancer-js/examples/pools/aprs.polygon.ts
@@ -1,6 +1,6 @@
/**
* Display APRs for pool ids hardcoded under `const ids`
- * Run command: yarn examples:run ./examples/pools/aprs.ts
+ * Run command: yarn examples:run ./examples/pools/aprs.polygon.ts
*/
import dotenv from 'dotenv';
import { BalancerSDK } from '../../src/modules/sdk.module';
@@ -19,7 +19,7 @@ const { pools } = sdk;
const main = async () => {
const pool = await pools.find(
- '0x0297e37f1873d2dab4487aa67cd56b58e2f27875000100000000000000000002'
+ '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d'
);
if (pool) {
diff --git a/balancer-js/examples/pools/aprs.ts b/balancer-js/examples/pools/aprs.ts
index dbccfb22d..b86b62cf5 100644
--- a/balancer-js/examples/pools/aprs.ts
+++ b/balancer-js/examples/pools/aprs.ts
@@ -23,6 +23,7 @@ const main = async () => {
pool.poolType != 'LiquidityBootstrapping'
)
)
+ // .filter((p) => p.id === '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d')
.sort((a, b) => parseFloat(b.totalLiquidity) - parseFloat(a.totalLiquidity))
.slice(0, 30);
diff --git a/balancer-js/examples/pools/impermanentLoss.ts b/balancer-js/examples/pools/impermanentLoss.ts
new file mode 100644
index 000000000..fa201a946
--- /dev/null
+++ b/balancer-js/examples/pools/impermanentLoss.ts
@@ -0,0 +1,51 @@
+/**
+ * calculate impermanent loss for a pool from a given timestamp.
+ *
+ * Run command: npm run examples:run -- ./examples/pools/impermanentLoss.ts
+ *
+ */
+
+import dotenv from "dotenv";
+import {BalancerError, BalancerErrorCode, BalancerSDK} from "../../src";
+
+dotenv.config();
+
+const sdk = new BalancerSDK({
+ network: 1,
+ rpcUrl: `${process.env.ALCHEMY_URL}`,
+});
+
+const { pools } = sdk;
+
+const { poolJoinExits } = sdk.data;
+
+const main = async (): Promise => {
+ await impermanentLoss('0x0647721e414a7ab11817427c6f49d0d15d6aae53', '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014');
+ await impermanentLoss('0x0a53d9586dd052a06fca7649a02b973cc164c1b4', '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014');
+ await impermanentLoss('0x000000000088e0120f9e6652cc058aec07564f69', '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014');
+ await impermanentLoss('0x07dd13b2705050b2f5c60da9f7f0f37b7395945a', '0xc45d42f801105e861e86658648e3678ad7aa70f900010000000000000000011e');
+ await impermanentLoss('0x00bcfc8f7471b2e4d21af417dea393c1578c67c1', '0x1e19cf2d73a72ef1332c882f20534b6519be0276000200000000000000000112');
+ // await impermanentLoss('0x356226e2f6d49749fd5f0fa5656acf86b20f3485', '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d');
+}
+
+const impermanentLoss = async (userAddress: string, poolId: string): Promise => {
+ try {
+ const joins = await poolJoinExits.findJoins(userAddress, poolId);
+ if (!joins.length) {
+ console.log(`${userAddress}: No Pool found`);
+ return;
+ }
+ const timestamp = joins[0].timestamp;
+
+ const pool = await sdk.pools.find(poolId);
+ if (!pool) {
+ throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST);
+ }
+ const IL = await pools.impermanentLoss(timestamp, pool);
+ console.log(`${userAddress} ${poolId} => ${IL}%`);
+ } catch (e: any) {
+ console.error(`${userAddress} ${poolId} => Error: ${e.message}`);
+ }
+}
+
+main().then(() => console.log('done'));
\ No newline at end of file
diff --git a/balancer-js/examples/pools/liquidity.polygon.ts b/balancer-js/examples/pools/liquidity.polygon.ts
new file mode 100644
index 000000000..7194f4d50
--- /dev/null
+++ b/balancer-js/examples/pools/liquidity.polygon.ts
@@ -0,0 +1,31 @@
+/**
+ * Display APRs for pool ids hardcoded under `const ids`
+ * Run command: yarn examples:run ./examples/pools/liquidity.polygon.ts
+ */
+import dotenv from 'dotenv'
+import { BalancerSDK } from '@/.'
+
+dotenv.config()
+
+const sdk = new BalancerSDK({
+ network: 137,
+ rpcUrl: `${process.env.ALCHEMY_URL?.replace(
+ 'eth-mainnet',
+ 'polygon-mainnet.g'
+ )}`,
+})
+
+const { pools } = sdk
+
+const main = async () => {
+ const pool = await pools.find(
+ '0xb797adfb7b268faeaa90cadbfed464c76ee599cd0002000000000000000005ba'
+ )
+
+ if (pool) {
+ const liquidity = await pools.liquidity(pool)
+ console.log(pool.id, pool.poolType, pool.totalLiquidity, liquidity)
+ }
+}
+
+main()
diff --git a/balancer-js/examples/pools/liquidity.ts b/balancer-js/examples/pools/liquidity.ts
index a89dc7e01..6a3ba89cc 100644
--- a/balancer-js/examples/pools/liquidity.ts
+++ b/balancer-js/examples/pools/liquidity.ts
@@ -6,20 +6,23 @@ const sdk = new BalancerSDK({
});
const { pools } = sdk;
+const { data } = sdk;
(() => {
[
- '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d',
+ // '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d',
// '0x2f4eb100552ef93840d5adc30560e5513dfffacb000000000000000000000334',
+ '0xc065798f227b49c150bcdc6cdc43149a12c4d75700020000000000000000010b',
].forEach(async (poolId) => {
const pool = await pools.find(poolId);
if (pool) {
const liquidity = await pools.liquidity(pool);
console.table([
{
+ pool: pool.name,
totalShares: pool.totalShares,
liquidity: liquidity,
- bptPrice: parseFloat(pool.totalShares) / parseFloat(liquidity),
+ bptPrice: parseFloat(liquidity) / parseFloat(pool.totalShares),
},
]);
}
diff --git a/balancer-js/examples/swapSor.ts b/balancer-js/examples/swapSor.ts
index 9cb080898..204a34531 100644
--- a/balancer-js/examples/swapSor.ts
+++ b/balancer-js/examples/swapSor.ts
@@ -61,7 +61,7 @@ async function getAndProcessSwaps(
swapInfo,
pools,
wallet.address,
- balancer.contracts.relayerV4!.address,
+ balancer.contracts.relayer!.address,
balancer.networkConfig.addresses.tokens.wrappedNativeAsset,
slippage,
undefined
@@ -71,7 +71,7 @@ async function getAndProcessSwaps(
// console.log(wallet.address);
// console.log(await balancer.sor.provider.getBlockNumber());
// console.log(relayerCallData.data);
- const result = await balancer.contracts.relayerV4
+ const result = await balancer.contracts.relayer
?.connect(wallet)
.callStatic.multicall(relayerCallData.rawCalls);
console.log(result);
diff --git a/balancer-js/img.png b/balancer-js/img.png
new file mode 100644
index 000000000..4140402fd
Binary files /dev/null and b/balancer-js/img.png differ
diff --git a/balancer-js/package.json b/balancer-js/package.json
index 66f7f9bb8..ff8dd76b5 100644
--- a/balancer-js/package.json
+++ b/balancer-js/package.json
@@ -1,6 +1,6 @@
{
"name": "@balancer-labs/sdk",
- "version": "0.1.37",
+ "version": "0.1.39",
"description": "JavaScript SDK for interacting with the Balancer Protocol V2",
"license": "GPL-3.0-only",
"homepage": "https://github.com/balancer-labs/balancer-sdk/balancer-js#readme",
@@ -89,7 +89,7 @@
"typescript": "^4.0.2"
},
"dependencies": {
- "@balancer-labs/sor": "^4.0.1-beta.12",
+ "@balancer-labs/sor": "^4.0.1-beta.15",
"@balancer-labs/typechain": "^1.0.0",
"axios": "^0.24.0",
"graphql": "^15.6.1",
diff --git a/balancer-js/src/balancerErrors.ts b/balancer-js/src/balancerErrors.ts
index 8a2943fc3..a26e32751 100644
--- a/balancer-js/src/balancerErrors.ts
+++ b/balancer-js/src/balancerErrors.ts
@@ -9,6 +9,7 @@ export enum BalancerErrorCode {
NO_POOL_DATA = 'NO_POOL_DATA',
INPUT_OUT_OF_BOUNDS = 'INPUT_OUT_OF_BOUNDS',
INPUT_LENGTH_MISMATCH = 'INPUT_LENGTH_MISMATCH',
+ INPUT_TOKEN_INVALID = 'INPUT_TOKEN_INVALID',
INPUT_ZERO_NOT_ALLOWED = 'INPUT_ZERO_NOT_ALLOWED',
TOKEN_MISMATCH = 'TOKEN_MISMATCH',
MISSING_TOKENS = 'MISSING_TOKENS',
@@ -17,6 +18,11 @@ export enum BalancerErrorCode {
MISSING_PRICE_RATE = 'MISSING_PRICE_RATE',
MISSING_WEIGHT = 'MISSING_WEIGHT',
RELAY_SWAP_AMOUNTS = 'RELAY_SWAP_AMOUNTS',
+ NO_VALUE_PARAMETER = 'NO_VALUE_PARAMETER',
+ ILLEGAL_PARAMETER = 'ILLEGAL_PARAMETER',
+ TIMESTAMP_IN_THE_FUTURE = 'TIMESTAMP_IN_THE_FUTURE',
+ JOIN_DELTA_AMOUNTS = 'JOIN_DELTA_AMOUNTS',
+ EXIT_DELTA_AMOUNTS = 'EXIT_DELTA_AMOUNTS',
}
export class BalancerError extends Error {
@@ -47,6 +53,8 @@ export class BalancerError extends Error {
return 'input out of bounds';
case BalancerErrorCode.INPUT_LENGTH_MISMATCH:
return 'input length mismatch';
+ case BalancerErrorCode.INPUT_TOKEN_INVALID:
+ return 'input token invalid';
case BalancerErrorCode.TOKEN_MISMATCH:
return 'token mismatch';
case BalancerErrorCode.MISSING_DECIMALS:
@@ -63,6 +71,16 @@ export class BalancerError extends Error {
return 'zero input not allowed';
case BalancerErrorCode.RELAY_SWAP_AMOUNTS:
return 'Error when checking swap amounts';
+ case BalancerErrorCode.NO_VALUE_PARAMETER:
+ return 'Illegal value passed as parameter';
+ case BalancerErrorCode.TIMESTAMP_IN_THE_FUTURE:
+ return 'Timestamp cannot be in the future';
+ case BalancerErrorCode.ILLEGAL_PARAMETER:
+ return 'An illegal parameter has been passed';
+ case BalancerErrorCode.JOIN_DELTA_AMOUNTS:
+ return 'Error when checking join call deltas';
+ case BalancerErrorCode.EXIT_DELTA_AMOUNTS:
+ return 'Error when checking exit call deltas';
default:
return 'Unknown error';
}
diff --git a/balancer-js/src/lib/abi/BatchRelayerLibrary.json b/balancer-js/src/lib/abi/BatchRelayerLibrary.json
index ca1953a4c..3c3b2bc33 100644
--- a/balancer-js/src/lib/abi/BatchRelayerLibrary.json
+++ b/balancer-js/src/lib/abi/BatchRelayerLibrary.json
@@ -486,6 +486,25 @@
"stateMutability": "payable",
"type": "function"
},
+ {
+ "inputs": [
+ {
+ "internalType": "uint256",
+ "name": "ref",
+ "type": "uint256"
+ }
+ ],
+ "name": "peekChainedReferenceValue",
+ "outputs": [
+ {
+ "internalType": "uint256",
+ "name": "value",
+ "type": "uint256"
+ }
+ ],
+ "stateMutability": "view",
+ "type": "function"
+ },
{
"inputs": [
{
@@ -1008,4 +1027,4 @@
"stateMutability": "payable",
"type": "function"
}
-]
\ No newline at end of file
+]
diff --git a/balancer-js/src/lib/constants/config.ts b/balancer-js/src/lib/constants/config.ts
index 58fbaf783..3599191f9 100644
--- a/balancer-js/src/lib/constants/config.ts
+++ b/balancer-js/src/lib/constants/config.ts
@@ -11,9 +11,10 @@ export const BALANCER_NETWORK_CONFIG: Record = {
vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
multicall: '0xeefba1e63905ef1d7acba5a8513c70307c1ce441',
lidoRelayer: '0xdcdbf71A870cc60C6F9B621E28a7D3Ffd6Dd4965',
+ relayerV3: '0x886A3Ec7bcC508B8795990B60Fa21f85F9dB7948',
+ relayerV4: '0x2536dfeeCB7A0397CF98eDaDA8486254533b1aFA',
gaugeController: '0xc128468b7ce63ea702c1f104d55a2566b13d3abd',
feeDistributor: '0xD3cf852898b21fc233251427c2DC93d3d604F3BB',
- relayerV4: '0x2536dfeeCB7A0397CF98eDaDA8486254533b1aFA',
protocolFeePercentagesProvider:
'0x97207B095e4D5C9a6e4cfbfcd2C3358E03B90c4A',
veBal: '0xC128a9954e6c874eA3d62ce62B468bA073093F25',
@@ -54,6 +55,7 @@ export const BALANCER_NETWORK_CONFIG: Record = {
contracts: {
vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
multicall: '0xa1B2b503959aedD81512C37e9dce48164ec6a94d',
+ relayerV3: '0xcf6a66E32dCa0e26AcC3426b851FD8aCbF12Dac7',
relayerV4: '0x28A224d9d398a1eBB7BA69BCA515898966Bb1B6b',
},
tokens: {
@@ -77,6 +79,7 @@ export const BALANCER_NETWORK_CONFIG: Record = {
contracts: {
vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
multicall: '0x269ff446d9892c9e19082564df3f5e8741e190a1',
+ relayerV3: '0x42E49B48573c725ee32d2579060Ed06894f97002',
relayerV4: '0x5bf3B7c14b10f16939d63Bd679264A1Aa951B4D5',
},
tokens: {
@@ -158,8 +161,9 @@ export const BALANCER_NETWORK_CONFIG: Record = {
contracts: {
vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
multicall: '0x77dCa2C955b15e9dE4dbBCf1246B4B85b651e50e',
- gaugeController: '0xBB1CE49b16d55A1f2c6e88102f32144C7334B116',
+ relayerV3: '0x7b9B6f094DC2Bd1c12024b0D9CC63d6993Be1888',
relayerV4: '0x00e695aA8000df01B8DC8401B4C34Fba5D56BBb2',
+ gaugeController: '0xBB1CE49b16d55A1f2c6e88102f32144C7334B116',
veBal: '0x33A99Dcc4C85C014cf12626959111D5898bbCAbF',
veBalProxy: '0xA1F107D1cD709514AE8A914eCB757E95f9cedB31',
},
@@ -183,6 +187,7 @@ export const BALANCER_NETWORK_CONFIG: Record = {
contracts: {
vault: '0xBA12222222228d8Ba445958a75a0704d566BF2C8',
multicall: '0x2dc0e2aa608532da689e89e237df582b783e552c',
+ relayerV3: '0x195CcCBE464EF9073d1f7A1ba1C9Bf0f56dfFFff',
relayerV4: '0x1a58897Ab366082028ced3740900ecBD765Af738',
},
tokens: {
diff --git a/balancer-js/src/lib/utils/debouncer.spec.ts b/balancer-js/src/lib/utils/debouncer.spec.ts
new file mode 100644
index 000000000..4b8e3badc
--- /dev/null
+++ b/balancer-js/src/lib/utils/debouncer.spec.ts
@@ -0,0 +1,86 @@
+import { expect } from 'chai';
+import { Debouncer } from './debouncer';
+
+const randomString = (length: number) =>
+ Array.from({ length }, () =>
+ String.fromCharCode(Math.floor(Math.random() * 26) + 97)
+ ).join('');
+
+describe('Debouncer', () => {
+ it('should call the original async function after the specified wait time', async () => {
+ let called = false;
+ const asyncFunc = async () => {
+ called = true;
+ };
+ const subject = new Debouncer(asyncFunc, 50);
+ subject.fetch();
+
+ expect(called).to.eq(false);
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(called).to.eq(true);
+ });
+
+ it('should prevent the original async function from being called multiple times within the specified wait time', async () => {
+ let callCount = 0;
+ const asyncFunc = async () => {
+ callCount++;
+ };
+ const subject = new Debouncer(asyncFunc, 50);
+
+ subject.fetch();
+ subject.fetch();
+ subject.fetch();
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(callCount).to.eq(1);
+ });
+
+ it('should aggregate attributes', async () => {
+ let attrs: string[] = [];
+ const asyncFunc = async (asyncAttrs: string[]) => {
+ attrs = asyncAttrs;
+ };
+ const subject = new Debouncer(asyncFunc, 50);
+ const testStrings = Array.from({ length: 5 }, () => randomString(5));
+
+ testStrings.forEach((str) => {
+ subject.fetch(str);
+ });
+ expect(attrs).to.eql([]);
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(attrs).to.eql(testStrings);
+ });
+
+ it("shouldn't fetch the same attribute twice", async () => {
+ let attrs: string[] = [];
+ const asyncFunc = async (asyncAttrs: string[]) => {
+ attrs = asyncAttrs;
+ };
+ const subject = new Debouncer(asyncFunc, 50);
+
+ subject.fetch('just once');
+ subject.fetch('just once');
+
+ expect(attrs).to.eql([]);
+ await new Promise((resolve) => setTimeout(resolve, 50));
+ expect(attrs).to.eql(['just once']);
+ });
+
+ it('returns a new promise when one is already running', async () => {
+ let attrs: string[] = [];
+ const asyncFunc = async (asyncAttrs: string[]) =>
+ new Promise((resolve) =>
+ setTimeout(() => {
+ attrs = asyncAttrs;
+ resolve(attrs);
+ }, 50)
+ );
+ const subject = new Debouncer(asyncFunc, 30);
+
+ const p1 = subject.fetch('first');
+ expect(attrs).to.eql([]);
+ await new Promise((resolve) => setTimeout(resolve, 40));
+ const p2 = subject.fetch('second');
+ expect(await p1).to.eql(['first']);
+ expect(await p2).to.eql(['second']);
+ });
+});
diff --git a/balancer-js/src/lib/utils/debouncer.ts b/balancer-js/src/lib/utils/debouncer.ts
new file mode 100644
index 000000000..5fb49bfb9
--- /dev/null
+++ b/balancer-js/src/lib/utils/debouncer.ts
@@ -0,0 +1,81 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+
+/**
+ * Debouncer for different attributes requested over time, which need to be aggregated into a single resolving call
+ *
+ * Choosing deferred promise since we have setTimeout that returns a promise
+ * Some reference for history buffs: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns
+ */
+
+interface Promised {
+ promise: Promise;
+ resolve: (value: T) => void;
+ reject: (reason: unknown) => void;
+}
+
+const makePromise = (): Promised => {
+ let resolve: (value: T) => void = () => {};
+ let reject: (reason: unknown) => void = () => {};
+ const promise = new Promise((res, rej) => {
+ [resolve, reject] = [res, rej];
+ });
+ return { promise, reject, resolve };
+};
+
+/**
+ * Aggregates attributes and exectutes a debounced call
+ *
+ * @param fn Function to debounce
+ * @param wait Debouncing waiting time [ms]
+ */
+export class Debouncer {
+ requestSet = new Set(); // collection of requested attributes
+ promisedCalls: Promised[] = []; // When requesting a price we return a deferred promise
+ promisedCount = 0; // New request coming when setTimeout is executing will make a new promise
+ timeout?: ReturnType;
+ debounceCancel = (): void => {}; // Allow to cancel mid-flight requests
+
+ constructor(private fn: (attrs: A[]) => Promise, private wait = 200) {}
+
+ fetch(attr?: A): Promise {
+ if (attr) {
+ this.requestSet.add(attr);
+ }
+
+ if (this.promisedCalls[this.promisedCount]) {
+ return this.promisedCalls[this.promisedCount].promise;
+ }
+
+ this.promisedCalls[this.promisedCount] = makePromise();
+
+ const { promise, resolve, reject } = this.promisedCalls[this.promisedCount];
+
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+
+ this.timeout = setTimeout(() => {
+ this.promisedCount++; // after execution started any new call will get a new promise
+ const requestAttrs = [...this.requestSet];
+ this.requestSet.clear(); // clear optimistically assuming successful results
+ this.fn(requestAttrs)
+ .then((results) => {
+ resolve(results);
+ this.debounceCancel = () => {};
+ })
+ .catch((reason) => {
+ console.error(reason);
+ });
+ }, this.wait);
+
+ this.debounceCancel = () => {
+ if (this.timeout) {
+ clearTimeout(this.timeout);
+ }
+ reject('Cancelled');
+ delete this.promisedCalls[this.promisedCount];
+ };
+
+ return promise;
+ }
+}
diff --git a/balancer-js/src/lib/utils/index.ts b/balancer-js/src/lib/utils/index.ts
index 63a5e0eaa..f0c1b91fb 100644
--- a/balancer-js/src/lib/utils/index.ts
+++ b/balancer-js/src/lib/utils/index.ts
@@ -7,6 +7,7 @@ export * from './assetHelpers';
export * from './aaveHelpers';
export * from './poolHelper';
export * from './tokens';
+export * from './debouncer';
export const isSameAddress = (address1: string, address2: string): boolean =>
getAddress(address1) === getAddress(address2);
diff --git a/balancer-js/src/lib/utils/tenderlyHelper.ts b/balancer-js/src/lib/utils/tenderlyHelper.ts
new file mode 100644
index 000000000..ec850ebb8
--- /dev/null
+++ b/balancer-js/src/lib/utils/tenderlyHelper.ts
@@ -0,0 +1,200 @@
+import axios from 'axios';
+import { MaxInt256 } from '@ethersproject/constants';
+import { networkAddresses } from '@/lib/constants/config';
+import { BalancerTenderlyConfig } from '@/types';
+
+type StateOverrides = {
+ [address: string]: { value: { [key: string]: string } };
+};
+
+export default class TenderlyHelper {
+ private vaultAddress;
+ private tenderlyUrl;
+ private opts?;
+ private blockNumber: number | undefined;
+
+ constructor(
+ private chainId: number,
+ tenderlyConfig?: BalancerTenderlyConfig
+ ) {
+ const { contracts } = networkAddresses(this.chainId);
+ this.vaultAddress = contracts.vault as string;
+ if (tenderlyConfig?.user && tenderlyConfig?.project) {
+ this.tenderlyUrl = `https://api.tenderly.co/api/v1/account/${tenderlyConfig.user}/project/${tenderlyConfig.project}/`;
+ } else {
+ this.tenderlyUrl = 'https://api.balancer.fi/tenderly/';
+ }
+
+ if (tenderlyConfig?.accessKey) {
+ this.opts = {
+ headers: {
+ 'X-Access-Key': tenderlyConfig.accessKey,
+ },
+ };
+ }
+
+ this.blockNumber = tenderlyConfig?.blockNumber;
+ }
+
+ simulateMulticall = async (
+ to: string,
+ data: string,
+ userAddress: string,
+ tokens: string[]
+ ): Promise => {
+ const tokensOverrides = await this.encodeBalanceAndAllowanceOverrides(
+ userAddress,
+ tokens
+ );
+ const relayerApprovalOverride = await this.encodeRelayerApprovalOverride(
+ userAddress,
+ to
+ );
+ const encodedStateOverrides = {
+ ...tokensOverrides,
+ ...relayerApprovalOverride,
+ };
+ return this.simulateTransaction(
+ to,
+ data,
+ userAddress,
+ encodedStateOverrides
+ );
+ };
+
+ simulateTransaction = async (
+ to: string,
+ data: string,
+ userAddress: string,
+ encodedStateOverrides: StateOverrides
+ ): Promise => {
+ // Map encoded-state response into simulate request body by replacing property names
+ const state_objects = Object.fromEntries(
+ Object.keys(encodedStateOverrides).map((address) => {
+ // Object.fromEntries require format [key, value] instead of {key: value}
+ return [address, { storage: encodedStateOverrides[address].value }];
+ })
+ );
+
+ const body = {
+ // -- Standard TX fields --
+ network_id: this.chainId.toString(),
+ block_number: this.blockNumber,
+ from: userAddress,
+ to,
+ input: data,
+ // gas: 8000000,
+ // gas_price: '0',
+ // value: '0',
+ // -- Simulation config (tenderly specific) --
+ save_if_fails: true,
+ // save: true,
+ simulation_type: 'quick', // remove this while developing/debugging
+ state_objects,
+ };
+
+ const SIMULATE_URL = this.tenderlyUrl + 'simulate';
+
+ const resp = await axios.post(SIMULATE_URL, body, this.opts);
+
+ const simulatedTransactionOutput =
+ resp.data.transaction.transaction_info.call_trace.output;
+
+ return simulatedTransactionOutput;
+ };
+
+ // Encode relayer approval state override
+ encodeRelayerApprovalOverride = async (
+ userAddress: string,
+ relayerAddress: string
+ ): Promise => {
+ const stateOverrides: StateOverrides = {
+ [`${this.vaultAddress}`]: {
+ value: {
+ [`_approvedRelayers[${userAddress}][${relayerAddress}]`]:
+ true.toString(),
+ },
+ },
+ };
+
+ const encodedStateOverrides = await this.requestStateOverrides(
+ stateOverrides
+ );
+
+ return encodedStateOverrides;
+ };
+
+ // Encode token balances and allowances overrides to max value
+ encodeBalanceAndAllowanceOverrides = async (
+ userAddress: string,
+ tokens: string[]
+ ): Promise => {
+ if (tokens.length === 0) return {};
+
+ // Create balances and allowances overrides for each token address provided
+ let stateOverrides: StateOverrides = {};
+ tokens.forEach(
+ (token) =>
+ (stateOverrides = {
+ ...stateOverrides,
+ [`${token}`]: {
+ value: {
+ [`_balances[${userAddress}]`]: MaxInt256.toString(),
+ [`_allowances[${userAddress}][${this.vaultAddress}]`]:
+ MaxInt256.toString(),
+ [`balanceOf[${userAddress}]`]: MaxInt256.toString(),
+ [`allowance[${userAddress}][${this.vaultAddress}]`]:
+ MaxInt256.toString(),
+ [`balances[${userAddress}]`]: MaxInt256.toString(),
+ [`allowed[${userAddress}][${this.vaultAddress}]`]:
+ MaxInt256.toString(),
+ },
+ },
+ })
+ );
+
+ const encodedStateOverrides = await this.requestStateOverrides(
+ stateOverrides
+ );
+
+ if (
+ Object.keys(encodedStateOverrides).some((k) => {
+ return Object.keys(encodedStateOverrides[k].value).length !== 2;
+ })
+ )
+ throw new Error(
+ "Couldn't encode state overrides - states should match the ones in the contracts"
+ );
+
+ return encodedStateOverrides;
+ };
+
+ private requestStateOverrides = async (
+ stateOverrides: StateOverrides
+ ): Promise => {
+ const ENCODE_STATES_URL = this.tenderlyUrl + 'contracts/encode-states';
+ const body = {
+ networkID: this.chainId.toString(),
+ stateOverrides,
+ };
+
+ const encodedStatesResponse = await axios.post(
+ ENCODE_STATES_URL,
+ body,
+ this.opts
+ );
+ const encodedStateOverrides = encodedStatesResponse.data
+ .stateOverrides as StateOverrides;
+
+ if (
+ !encodedStateOverrides ||
+ Object.keys(encodedStateOverrides).length !==
+ Object.keys(stateOverrides).length
+ )
+ throw new Error(
+ "Couldn't encode state overrides - contracts should be verified and whitelisted on Tenderly"
+ );
+
+ return encodedStateOverrides;
+ };
+}
diff --git a/balancer-js/src/lib/utils/tokens.ts b/balancer-js/src/lib/utils/tokens.ts
index f4840c977..a260e0f16 100644
--- a/balancer-js/src/lib/utils/tokens.ts
+++ b/balancer-js/src/lib/utils/tokens.ts
@@ -1,4 +1,6 @@
-import { Token, TokenPrices } from '@/types';
+import { Token, TokenPrices, Network } from '@/types';
+import { TOKENS } from '@/lib/constants/tokens';
+import { wrappedTokensMap as aaveWrappedMap } from '@/modules/data/token-yields/tokens/aave';
export function tokensToTokenPrices(tokens: Token[]): TokenPrices {
const tokenPrices: TokenPrices = {};
@@ -10,3 +12,56 @@ export function tokensToTokenPrices(tokens: Token[]): TokenPrices {
return tokenPrices;
}
+
+export function tokenAddressForPricing(
+ address: string,
+ chainId: Network
+): string {
+ let a = address.toLowerCase();
+ a = addressMapIn(a, chainId);
+ a = unwrapToken(a, chainId);
+
+ return a;
+}
+
+/**
+ * Maps testnet tokens, eg: on Göreli to a mainnet one.
+ * Used to get the pricing information on networks not supported by a price feed.
+ *
+ * @param address Address on a testnet network
+ */
+export const addressMapIn = (address: string, chainId: Network): string => {
+ const addressMap = TOKENS(chainId).PriceChainMap;
+ return (addressMap && addressMap[address.toLowerCase()]) || address;
+};
+
+/**
+ * Finds an underlying token address for a wrapped one
+ *
+ * @param wrappedAddress
+ * @param chainId
+ * @returns underlying token address
+ */
+export const unwrapToken = (
+ wrappedAddress: string,
+ chainId: Network
+): string => {
+ const lowercase = wrappedAddress.toLocaleLowerCase();
+
+ const aaveChain = chainId as keyof typeof aaveWrappedMap;
+ if (
+ aaveWrappedMap[aaveChain] != undefined &&
+ aaveWrappedMap[aaveChain] != null
+ ) {
+ // Double if to avoid skipping just to at after compile: Object.keys()?.includes
+ if (Object.keys(aaveWrappedMap[aaveChain]).includes(lowercase)) {
+ return aaveWrappedMap[aaveChain][
+ lowercase as keyof typeof aaveWrappedMap[typeof aaveChain]
+ ].aToken;
+ } else {
+ return lowercase;
+ }
+ } else {
+ return lowercase;
+ }
+};
diff --git a/balancer-js/src/modules/contracts/contracts.module.ts b/balancer-js/src/modules/contracts/contracts.module.ts
index fca6fdeba..2240e7389 100644
--- a/balancer-js/src/modules/contracts/contracts.module.ts
+++ b/balancer-js/src/modules/contracts/contracts.module.ts
@@ -14,7 +14,7 @@ import { Multicall } from './implementations/multicall';
import { ERC20 } from './implementations/ERC20';
import { VeBal } from './implementations/veBAL';
import { VeBalProxy } from './implementations/veBAL-proxy';
-import { RelayerV4 } from './implementations/relayerV4';
+import { Relayer } from './implementations/relayer';
import { LiquidityGauge } from './implementations/liquidity-gauge';
type ContractFactory = (
@@ -26,7 +26,8 @@ export interface ContractInstances {
vault: Vault;
lidoRelayer?: LidoRelayer;
multicall: Contract;
- relayerV4: Contract | undefined;
+ relayerV3?: Contract;
+ relayerV4?: Contract;
veBal?: VeBal;
veBalProxy?: VeBalProxy;
ERC20: ContractFactory;
@@ -38,7 +39,8 @@ export class Contracts {
vault: Vault;
lidoRelayer?: LidoRelayer;
multicall: Contract;
- relayerV4: Contract | undefined;
+ relayerV3?: Contract;
+ relayerV4?: Contract;
veBal?: VeBal;
veBalProxy?: VeBalProxy;
@@ -70,8 +72,10 @@ export class Contracts {
// These contracts aren't included in Balancer Typechain but are still useful.
// TO DO - Possibly create via Typechain but seems unnecessary?
this.multicall = Multicall(this.contractAddresses.multicall, provider);
+ if (this.contractAddresses.relayerV3)
+ this.relayerV3 = Relayer(this.contractAddresses.relayerV3, provider, 3);
if (this.contractAddresses.relayerV4)
- this.relayerV4 = RelayerV4(this.contractAddresses.relayerV4, provider);
+ this.relayerV4 = Relayer(this.contractAddresses.relayerV4, provider, 4);
if (this.contractAddresses.veBal) {
this.veBal = new VeBal(this.contractAddresses, provider);
@@ -90,6 +94,7 @@ export class Contracts {
vault: this.vault,
lidoRelayer: this.lidoRelayer,
multicall: this.multicall,
+ relayerV3: this.relayerV3,
relayerV4: this.relayerV4,
veBal: this.veBal,
veBalProxy: this.veBalProxy,
@@ -101,7 +106,7 @@ export class Contracts {
/**
* Helper to create ERC20 contract.
* @param { string } address ERC20 address.
- * @param { Signer | Provider } Signer or Provider.
+ * @param { Signer | Provider } signerOrProvider Signer or Provider.
* @returns Contract.
*/
getErc20(address: string, signerOrProvider: Signer | Provider): Contract {
@@ -110,8 +115,8 @@ export class Contracts {
/**
* Helper to create LiquidityGauge contract.
- * @param { string } Gauge address.
- * @param { Signer | Provider} Signer or Provider.
+ * @param { string } address Gauge address.
+ * @param { Signer | Provider} signerOrProvider Signer or Provider.
* @returns Contract.
*/
getLiquidityGauge(
diff --git a/balancer-js/src/modules/contracts/implementations/relayer.ts b/balancer-js/src/modules/contracts/implementations/relayer.ts
new file mode 100644
index 000000000..0954b713a
--- /dev/null
+++ b/balancer-js/src/modules/contracts/implementations/relayer.ts
@@ -0,0 +1,19 @@
+import { Contract } from '@ethersproject/contracts';
+import { Provider } from '@ethersproject/providers';
+import RelayerV4ABI from '@/lib/abi/RelayerV4.json';
+import RelayerV3ABI from '@/lib/abi/BalancerRelayer.json';
+
+export const Relayer = (
+ address: string,
+ provider: Provider,
+ version: number
+): Contract => {
+ switch (version) {
+ case 3:
+ return new Contract(address, RelayerV3ABI, provider);
+ case 4:
+ return new Contract(address, RelayerV4ABI, provider);
+ default:
+ throw new Error('relayer not supported');
+ }
+};
diff --git a/balancer-js/src/modules/contracts/implementations/relayerV4.ts b/balancer-js/src/modules/contracts/implementations/relayerV4.ts
deleted file mode 100644
index d01d9c59f..000000000
--- a/balancer-js/src/modules/contracts/implementations/relayerV4.ts
+++ /dev/null
@@ -1,6 +0,0 @@
-import { Contract } from '@ethersproject/contracts';
-import { Provider } from '@ethersproject/providers';
-import abi from '../../../lib/abi/RelayerV4.json';
-
-export const RelayerV4 = (address: string, provider: Provider): Contract =>
- new Contract(address, abi, provider);
diff --git a/balancer-js/src/modules/data/index.ts b/balancer-js/src/modules/data/index.ts
index 38ed2c2c6..57cc72428 100644
--- a/balancer-js/src/modules/data/index.ts
+++ b/balancer-js/src/modules/data/index.ts
@@ -4,6 +4,7 @@ export * from './gauge-shares';
export * from './liquidity-gauges';
export * from './pool';
export * from './pool-gauges';
+export * from './pool-joinExit';
export * from './pool-shares';
export * from './token';
export * from './token-prices';
@@ -16,6 +17,7 @@ export * from './block-number';
import { BalancerNetworkConfig, BalancerDataRepositories } from '@/types';
import { PoolsSubgraphRepository } from './pool/subgraph';
import { PoolSharesRepository } from './pool-shares/repository';
+import { PoolJoinExitRepository } from './pool-joinExit/repository';
import { PoolGaugesRepository } from './pool-gauges/repository';
import { GaugeSharesRepository } from './gauge-shares/repository';
import { BlockNumberRepository } from './block-number';
@@ -23,6 +25,8 @@ import {
CoingeckoPriceRepository,
AaveRates,
TokenPriceProvider,
+ HistoricalPriceProvider,
+ CoingeckoHistoricalPriceRepository,
} from './token-prices';
import { StaticTokenProvider } from './token/static';
import { LiquidityGaugeSubgraphRPCProvider } from './liquidity-gauges/provider';
@@ -35,6 +39,7 @@ import { Provider } from '@ethersproject/providers';
// initialCoingeckoList are used to get the initial token list for coingecko
// TODO: we might want to replace that with what frontend is using
import initialCoingeckoList from '@/modules/data/token-prices/initial-list.json';
+import { SubgraphPriceRepository } from './token-prices/subgraph';
export class Data implements BalancerDataRepositories {
pools;
@@ -43,6 +48,7 @@ export class Data implements BalancerDataRepositories {
poolGauges;
gaugeShares;
tokenPrices;
+ tokenHistoricalPrices;
tokenMeta;
liquidityGauges;
feeDistributor;
@@ -50,6 +56,7 @@ export class Data implements BalancerDataRepositories {
protocolFees;
tokenYields;
blockNumbers;
+ poolJoinExits;
constructor(networkConfig: BalancerNetworkConfig, provider: Provider) {
this.pools = new PoolsSubgraphRepository({
@@ -62,6 +69,11 @@ export class Data implements BalancerDataRepositories {
networkConfig.chainId
);
+ this.poolJoinExits = new PoolJoinExitRepository(
+ networkConfig.urls.subgraph,
+ networkConfig.chainId
+ );
+
if (networkConfig.urls.gaugesSubgraph) {
this.poolGauges = new PoolGaugesRepository(
networkConfig.urls.gaugesSubgraph,
@@ -103,13 +115,29 @@ export class Data implements BalancerDataRepositories {
networkConfig.chainId
);
+ const subgraphPriceRepository = new SubgraphPriceRepository(
+ networkConfig.chainId
+ );
+
const aaveRates = new AaveRates(
networkConfig.addresses.contracts.multicall,
provider,
networkConfig.chainId
);
- this.tokenPrices = new TokenPriceProvider(coingeckoRepository, aaveRates);
+ this.tokenPrices = new TokenPriceProvider(
+ coingeckoRepository,
+ subgraphPriceRepository,
+ aaveRates
+ );
+
+ const coingeckoHistoricalRepository =
+ new CoingeckoHistoricalPriceRepository(networkConfig.chainId);
+
+ this.tokenHistoricalPrices = new HistoricalPriceProvider(
+ coingeckoHistoricalRepository,
+ aaveRates
+ );
this.tokenMeta = new StaticTokenProvider([]);
diff --git a/balancer-js/src/modules/data/liquidity-gauges/provider.ts b/balancer-js/src/modules/data/liquidity-gauges/provider.ts
index f48008c58..7bdf9cfa5 100644
--- a/balancer-js/src/modules/data/liquidity-gauges/provider.ts
+++ b/balancer-js/src/modules/data/liquidity-gauges/provider.ts
@@ -62,14 +62,18 @@ export class LiquidityGaugeSubgraphRPCProvider
const gauges: SubgraphLiquidityGauge[] = await this.subgraph.fetch();
const gaugeAddresses = gauges.map((g) => g.id);
if (this.chainId == 1) {
+ console.time('Fetching multicall.getWorkingSupplies');
this.workingSupplies = await this.multicall.getWorkingSupplies(
gaugeAddresses
);
+ console.timeEnd('Fetching multicall.getWorkingSupplies');
}
if (this.gaugeController) {
+ console.time('Fetching gaugeController.getRelativeWeights');
this.relativeWeights = await this.gaugeController.getRelativeWeights(
gaugeAddresses
);
+ console.timeEnd('Fetching gaugeController.getRelativeWeights');
}
// Kept as a potential fallback for getting rewardData from RPC
diff --git a/balancer-js/src/modules/data/pool-joinExit/index.ts b/balancer-js/src/modules/data/pool-joinExit/index.ts
new file mode 100644
index 000000000..ac172595a
--- /dev/null
+++ b/balancer-js/src/modules/data/pool-joinExit/index.ts
@@ -0,0 +1,2 @@
+export * from './repository';
+export * from './types';
diff --git a/balancer-js/src/modules/data/pool-joinExit/repository.ts b/balancer-js/src/modules/data/pool-joinExit/repository.ts
new file mode 100644
index 000000000..c5806781a
--- /dev/null
+++ b/balancer-js/src/modules/data/pool-joinExit/repository.ts
@@ -0,0 +1,59 @@
+/* eslint-disable @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any */
+import { PoolJoinExit, PoolJoinExitAttributes } from './types';
+import { BalancerSubgraphRepository } from '@/modules/subgraph/repository';
+import {
+ JoinExit_OrderBy,
+ OrderDirection,
+ SubgraphJoinExitFragment,
+} from '@/modules/subgraph/generated/balancer-subgraph-types';
+
+export class PoolJoinExitRepository extends BalancerSubgraphRepository<
+ PoolJoinExit,
+ PoolJoinExitAttributes
+> {
+ async query(args: any): Promise {
+ if (!args.orderBy) args.orderBy = JoinExit_OrderBy.Timestamp;
+ if (!args.orderDirection) args.orderDirection = OrderDirection.Asc;
+ if (!args.block && this.blockHeight)
+ args.block = { number: await this.blockHeight() };
+
+ const { joinExits } = await this.client.JoinExits(args);
+ return joinExits.map(this.mapType);
+ }
+
+ mapType(item: SubgraphJoinExitFragment): PoolJoinExit {
+ return {
+ id: item.id,
+ userAddress: item.user.id,
+ poolId: item.pool.id,
+ timestamp: item.timestamp,
+ type: item.type,
+ amounts: item.amounts,
+ tokens: item.pool.tokensList,
+ };
+ }
+
+ async findByUser(
+ sender: string,
+ first?: number,
+ skip?: number
+ ): Promise {
+ return this.findAllBy(PoolJoinExitAttributes.Sender, sender, first, skip);
+ }
+
+ async findJoins(sender: string, pool: string): Promise {
+ return this.query({ where: { sender, pool, type: 'Join' } });
+ }
+
+ async findExits(sender: string, pool: string): Promise {
+ return this.query({ where: { sender, pool, type: 'Exit' } });
+ }
+
+ async findByPool(
+ poolId: string,
+ first?: number,
+ skip?: number
+ ): Promise {
+ return this.findAllBy(PoolJoinExitAttributes.Pool, poolId, first, skip);
+ }
+}
diff --git a/balancer-js/src/modules/data/pool-joinExit/types.ts b/balancer-js/src/modules/data/pool-joinExit/types.ts
new file mode 100644
index 000000000..4089b0f76
--- /dev/null
+++ b/balancer-js/src/modules/data/pool-joinExit/types.ts
@@ -0,0 +1,14 @@
+export enum PoolJoinExitAttributes {
+ Pool = 'pool',
+ Sender = 'sender',
+}
+
+export interface PoolJoinExit {
+ id: string;
+ userAddress: string;
+ poolId: string;
+ timestamp: number;
+ type: string;
+ amounts: string[];
+ tokens: string[];
+}
diff --git a/balancer-js/src/modules/data/pool/balancer-api.ts b/balancer-js/src/modules/data/pool/balancer-api.ts
index b546a0c6c..f9bfb44ad 100644
--- a/balancer-js/src/modules/data/pool/balancer-api.ts
+++ b/balancer-js/src/modules/data/pool/balancer-api.ts
@@ -54,9 +54,12 @@ export class PoolsBalancerAPIRepository
};
this.query = {
- args: options.query?.args || defaultArgs,
- attrs: options.query?.attrs || defaultAttributes,
+ args: Object.assign({}, options.query?.args || defaultArgs),
+ attrs: Object.assign({}, options.query?.attrs || defaultAttributes),
};
+
+ // skip is not a valid argument for the Balancer API, it uses nextToken
+ delete this.query.args.skip;
}
fetchFromCache(options?: PoolsRepositoryFetchOptions): Pool[] {
diff --git a/balancer-js/src/modules/data/pool/subgraph.ts b/balancer-js/src/modules/data/pool/subgraph.ts
index 152d957c5..d54613f00 100644
--- a/balancer-js/src/modules/data/pool/subgraph.ts
+++ b/balancer-js/src/modules/data/pool/subgraph.ts
@@ -6,6 +6,8 @@ import {
Pool_OrderBy,
OrderDirection,
SubgraphPoolTokenFragment,
+ SubgraphSubPoolFragment,
+ SubgraphSubPoolTokenFragment,
} from '@/modules/subgraph/subgraph';
import {
GraphQLArgsBuilder,
@@ -13,7 +15,14 @@ import {
} from '@/lib/graphql/args-builder';
import { GraphQLArgs } from '@/lib/graphql/types';
import { PoolAttribute, PoolsRepositoryFetchOptions } from './types';
-import { GraphQLQuery, Pool, PoolType, PoolToken } from '@/types';
+import {
+ GraphQLQuery,
+ Pool,
+ PoolType,
+ PoolToken,
+ SubPool,
+ SubPoolMeta,
+} from '@/types';
import { Network } from '@/lib/constants/network';
import { PoolsQueryVariables } from '../../subgraph/subgraph';
@@ -24,6 +33,19 @@ interface PoolsSubgraphRepositoryOptions {
query?: GraphQLQuery;
}
+interface SubgraphSubPoolToken extends SubgraphSubPoolTokenFragment {
+ token?: SubgraphSubPoolMeta | null;
+}
+
+interface SubgraphSubPoolMeta {
+ latestUSDPrice?: string | null;
+ pool?: SubgraphSubPool | null;
+}
+
+interface SubgraphSubPool extends SubgraphSubPoolFragment {
+ tokens: SubgraphSubPoolToken[];
+}
+
/**
* Access pools using generated subgraph client.
*
@@ -64,8 +86,8 @@ export class PoolsSubgraphRepository
},
};
- const args = options.query?.args || defaultArgs;
- const attrs = options.query?.attrs || {};
+ const args = Object.assign({}, options.query?.args || defaultArgs);
+ const attrs = Object.assign({}, options.query?.attrs || {});
this.query = {
args,
@@ -118,19 +140,30 @@ export class PoolsSubgraphRepository
}
async findBy(param: PoolAttribute, value: string): Promise {
- if (this.pools) {
- return (await this.pools).find((p) => p[param] === value);
+ if (!this.pools) {
+ this.pools = this.fetchDefault();
}
- const { pools } = await this.client.Pools({
- where: {
- [param]: value,
- swapEnabled: true,
- totalShares_gt: '0.000000000001',
- },
- block: await this.block(),
- });
- const poolsTab: Pool[] = pools.map(this.mapType.bind(this));
- return poolsTab.length > 0 ? poolsTab[0] : undefined;
+
+ return (await this.pools).find((pool) => pool[param] == value);
+
+ // TODO: @Nma - Fetching pools outside of default query is causing a lot of requests
+ // on a frontend, because results aren't cached anywhere.
+ // For fetching pools directly from subgraph with custom queries please use the client not this repository.
+ // Code below kept for reference, to be removed later.
+ //
+ // if (this.pools) {
+ // return (await this.pools).find((p) => p[param] === value);
+ // }
+ // const { pools } = await this.client.Pools({
+ // where: {
+ // [param]: value,
+ // swapEnabled: true,
+ // totalShares_gt: '0.000000000001',
+ // },
+ // block: await this.block(),
+ // });
+ // const poolsTab: Pool[] = pools.map(this.mapType.bind(this));
+ // return poolsTab.length > 0 ? poolsTab[0] : undefined;
}
async all(): Promise {
@@ -167,13 +200,14 @@ export class PoolsSubgraphRepository
owner: subgraphPool.owner ?? undefined,
factory: subgraphPool.factory ?? undefined,
symbol: subgraphPool.symbol ?? undefined,
- tokens: (subgraphPool.tokens || []).map(this.mapToken),
+ tokens: (subgraphPool.tokens || []).map(this.mapToken.bind(this)),
tokensList: subgraphPool.tokensList,
tokenAddresses: (subgraphPool.tokens || []).map((t) => t.address),
totalLiquidity: subgraphPool.totalLiquidity,
totalShares: subgraphPool.totalShares,
totalSwapFee: subgraphPool.totalSwapFee,
totalSwapVolume: subgraphPool.totalSwapVolume,
+ priceRateProviders: subgraphPool.priceRateProviders ?? undefined,
// onchain: subgraphPool.onchain,
createTime: subgraphPool.createTime,
mainIndex: subgraphPool.mainIndex ?? undefined,
@@ -186,24 +220,59 @@ export class PoolsSubgraphRepository
// feesSnapshot: subgraphPool.???, // Approximated last 24h fees
// boost: subgraphPool.boost,
totalWeight: subgraphPool.totalWeight || '1',
+ lowerTarget: subgraphPool.lowerTarget ?? '0',
+ upperTarget: subgraphPool.upperTarget ?? '0',
};
}
private mapToken(subgraphToken: SubgraphPoolTokenFragment): PoolToken {
- let subgraphTokenPool = null;
- if (subgraphToken.token?.pool) {
- subgraphTokenPool = {
- ...subgraphToken.token.pool,
- poolType: subgraphToken.token.pool.poolType as PoolType,
- };
- }
+ const subPoolInfo = this.mapSubPools(
+ // need to typecast as the fragment is 3 layers deep while the type is infinite levels deep
+ subgraphToken.token as SubgraphSubPoolMeta
+ );
return {
...subgraphToken,
isExemptFromYieldProtocolFee:
subgraphToken.isExemptFromYieldProtocolFee || false,
- token: {
- pool: subgraphTokenPool,
- },
+ token: subPoolInfo,
+ };
+ }
+
+ private mapSubPools(metadata: SubgraphSubPoolMeta): SubPoolMeta {
+ let subPool: SubPool | null = null;
+ if (metadata.pool) {
+ subPool = {
+ id: metadata.pool.id,
+ address: metadata.pool.address,
+ totalShares: metadata.pool.totalShares,
+ poolType: metadata.pool.poolType as PoolType,
+ mainIndex: metadata.pool.mainIndex || 0,
+ };
+
+ if (metadata?.pool.tokens) {
+ subPool.tokens = metadata.pool.tokens.map(
+ this.mapSubPoolToken.bind(this)
+ );
+ }
+ }
+
+ return {
+ pool: subPool,
+ latestUSDPrice: metadata.latestUSDPrice || undefined,
+ };
+ }
+
+ private mapSubPoolToken(token: SubgraphSubPoolToken) {
+ return {
+ address: token.address,
+ decimals: token.decimals,
+ symbol: token.symbol,
+ balance: token.balance,
+ priceRate: token.priceRate,
+ weight: token.weight,
+ isExemptFromYieldProtocolFee:
+ token.isExemptFromYieldProtocolFee || undefined,
+ token: token.token ? this.mapSubPools(token.token) : undefined,
};
}
}
diff --git a/balancer-js/src/modules/data/token-prices/aave-rates.ts b/balancer-js/src/modules/data/token-prices/aave-rates.ts
index e43d28428..d62eb1d9b 100644
--- a/balancer-js/src/modules/data/token-prices/aave-rates.ts
+++ b/balancer-js/src/modules/data/token-prices/aave-rates.ts
@@ -10,7 +10,11 @@ const wrappedATokenInterface = new Interface([
'function rate() view returns (uint256)',
]);
-export class AaveRates {
+export interface IAaveRates {
+ getRate: (address: string) => Promise;
+}
+
+export class AaveRates implements IAaveRates {
multicall: Contract;
rates?: Promise<{ [wrappedATokenAddress: string]: number }>;
@@ -46,6 +50,9 @@ export class AaveRates {
if (this.network != Network.MAINNET && this.network != Network.POLYGON) {
return 1;
}
+ if (!Object.values(yieldTokens[this.network]).includes(wrappedAToken)) {
+ return 1;
+ }
if (!this.rates) {
this.rates = this.fetch(this.network);
}
diff --git a/balancer-js/src/modules/data/token-prices/coingecko-historical.ts b/balancer-js/src/modules/data/token-prices/coingecko-historical.ts
new file mode 100644
index 000000000..776b39b8c
--- /dev/null
+++ b/balancer-js/src/modules/data/token-prices/coingecko-historical.ts
@@ -0,0 +1,87 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+import {
+ Price,
+ Findable,
+ TokenPrices,
+ Network,
+ HistoricalPrices,
+} from '@/types';
+import axios from 'axios';
+import { tokenAddressForPricing } from '@/lib/utils';
+
+const HOUR = 60 * 60;
+
+/**
+ * Simple coingecko price source implementation. Configurable by network and token addresses.
+ */
+export class CoingeckoHistoricalPriceRepository implements Findable {
+ prices: TokenPrices = {};
+ nativePrice?: Promise;
+ urlBase: string;
+
+ constructor(private chainId: Network = 1) {
+ this.urlBase = `https://api.coingecko.com/api/v3/coins/${this.platform(
+ chainId
+ )}/contract/%TOKEN_ADDRESS%/market_chart/range?vs_currency=usd`;
+ }
+
+ private fetch(
+ address: string,
+ timestamp: number,
+ { signal }: { signal?: AbortSignal } = {}
+ ): Promise {
+ console.time(`fetching coingecko historical for ${address}`);
+ const url = this.urlRange(address, timestamp);
+ return axios
+ .get(url, { signal })
+ .then(({ data }) => {
+ return data;
+ })
+ .finally(() => {
+ console.timeEnd(`fetching coingecko historical for ${address}`);
+ });
+ }
+
+ /* eslint-disable @typescript-eslint/no-unused-vars */
+ async find(address: string): Promise {
+ throw `Historic price requires point-in-time timestamp, please use findBy(address, timestamp)`;
+ }
+
+ async findBy(
+ inputAddress: string,
+ timestamp: number
+ ): Promise {
+ const address = tokenAddressForPricing(inputAddress, this.chainId);
+ const response = await this.fetch(address, timestamp);
+
+ return {
+ usd: `${response.prices[0][1]}`,
+ };
+ }
+
+ private platform(chainId: number): string {
+ switch (chainId) {
+ case 1:
+ case 5:
+ case 42:
+ case 31337:
+ return 'ethereum';
+ case 137:
+ return 'polygon-pos';
+ case 42161:
+ return 'arbitrum-one';
+ }
+
+ return '2';
+ }
+
+ private urlRange(address: string, timestamp: number): string {
+ const range: { from: number; to: number } = {
+ from: timestamp - HOUR,
+ to: timestamp + HOUR,
+ };
+ return `${this.urlBase.replace('%TOKEN_ADDRESS%', address)}&from=${
+ range.from
+ }&to=${range.to}`;
+ }
+}
diff --git a/balancer-js/src/modules/data/token-prices/coingecko.ts b/balancer-js/src/modules/data/token-prices/coingecko.ts
index 25652a9f9..200398e95 100644
--- a/balancer-js/src/modules/data/token-prices/coingecko.ts
+++ b/balancer-js/src/modules/data/token-prices/coingecko.ts
@@ -1,52 +1,28 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { Price, Findable, TokenPrices, Network } from '@/types';
-import { wrappedTokensMap as aaveWrappedMap } from '../token-yields/tokens/aave';
import axios from 'axios';
import { TOKENS } from '@/lib/constants/tokens';
-import { isEthereumTestnet } from '@/lib/utils/network';
-
-// Conscious choice for a deferred promise since we have setTimeout that returns a promise
-// Some reference for history buffs: https://github.com/petkaantonov/bluebird/wiki/Promise-anti-patterns
-interface PromisedTokenPrices {
- promise: Promise;
- resolve: (value: TokenPrices) => void;
- reject: (reason: unknown) => void;
-}
-
-const makePromise = (): PromisedTokenPrices => {
- let resolve: (value: TokenPrices) => void = () => {};
- let reject: (reason: unknown) => void = () => {};
- const promise = new Promise((res, rej) => {
- [resolve, reject] = [res, rej];
- });
- return { promise, reject, resolve };
-};
+import { Debouncer, tokenAddressForPricing } from '@/lib/utils';
/**
* Simple coingecko price source implementation. Configurable by network and token addresses.
*/
export class CoingeckoPriceRepository implements Findable {
- prices: TokenPrices = {};
+ prices: { [key: string]: Promise } = {};
+ nativePrice?: Promise;
urlBase: string;
baseTokenAddresses: string[];
-
- // Properties used for deferring API calls
- // TODO: move this logic to hooks
- requestedAddresses = new Set(); // Accumulates requested addresses
- debounceWait = 200; // Debouncing waiting time [ms]
- promisedCalls: PromisedTokenPrices[] = []; // When requesting a price we return a deferred promise
- promisedCount = 0; // New request coming when setTimeout is executing will make a new promise
- timeout?: ReturnType;
- debounceCancel = (): void => {}; // Allow to cancel mid-flight requests
+ debouncer: Debouncer;
constructor(tokenAddresses: string[], private chainId: Network = 1) {
- this.baseTokenAddresses = tokenAddresses
- .map((a) => a.toLowerCase())
- .map((a) => this.addressMapIn(a))
- .map((a) => this.unwrapToken(a));
+ this.baseTokenAddresses = tokenAddresses.map(tokenAddressForPricing);
this.urlBase = `https://api.coingecko.com/api/v3/simple/token_price/${this.platform(
chainId
)}?vs_currencies=usd,eth`;
+ this.debouncer = new Debouncer(
+ this.fetch.bind(this),
+ 200
+ );
}
private fetch(
@@ -64,70 +40,57 @@ export class CoingeckoPriceRepository implements Findable {
});
}
- private debouncedFetch(): Promise {
- if (!this.promisedCalls[this.promisedCount]) {
- this.promisedCalls[this.promisedCount] = makePromise();
- }
-
- const { promise, resolve, reject } = this.promisedCalls[this.promisedCount];
-
- if (this.timeout) {
- clearTimeout(this.timeout);
+ private fetchNative({
+ signal,
+ }: { signal?: AbortSignal } = {}): Promise {
+ console.time(`fetching coingecko for native token`);
+ enum Assets {
+ ETH = 'matic-network',
+ MATIC = 'ethereum',
}
-
- this.timeout = setTimeout(() => {
- this.promisedCount++; // any new call will get a new promise
- this.fetch([...this.requestedAddresses])
- .then((results) => {
- resolve(results);
- this.debounceCancel = () => {};
- })
- .catch((reason) => {
- console.error(reason);
- });
- }, this.debounceWait);
-
- this.debounceCancel = () => {
- if (this.timeout) {
- clearTimeout(this.timeout);
- }
- reject('Cancelled');
- delete this.promisedCalls[this.promisedCount];
- };
-
- return promise;
+ const assetId = this.chainId === 137 ? 'matic-network' : 'ethereum';
+ return axios
+ .get<{ [key in Assets]: Price }>(
+ `https://api.coingecko.com/api/v3/simple/price/?vs_currencies=eth,usd&ids=${assetId}`,
+ { signal }
+ )
+ .then(({ data }) => {
+ return data[assetId];
+ })
+ .finally(() => {
+ console.timeEnd(`fetching coingecko for native token`);
+ });
}
- async find(address: string): Promise {
- const lowercaseAddress = address.toLowerCase();
- const mapInAddress = this.addressMapIn(lowercaseAddress);
- const unwrapped = this.unwrapToken(mapInAddress);
- if (!this.prices[unwrapped]) {
- try {
- let init = false;
- if (Object.keys(this.prices).length === 0) {
- // Make initial call with all the tokens we want to preload
- this.baseTokenAddresses.forEach(
- this.requestedAddresses.add.bind(this.requestedAddresses)
- );
- init = true;
+ find(inputAddress: string): Promise {
+ const address = tokenAddressForPricing(inputAddress, this.chainId);
+ if (!this.prices[address]) {
+ // Make initial call with all the tokens we want to preload
+ if (Object.keys(this.prices).length === 0) {
+ for (const baseAddress of this.baseTokenAddresses) {
+ this.prices[baseAddress] = this.debouncer
+ .fetch(baseAddress)
+ .then((prices) => prices[baseAddress]);
}
- this.requestedAddresses.add(unwrapped);
- const promised = await this.debouncedFetch();
- this.prices[unwrapped] = promised[unwrapped];
- this.requestedAddresses.delete(unwrapped);
- if (init) {
- this.baseTokenAddresses.forEach((a) => {
- this.prices[a] = promised[a];
- this.requestedAddresses.delete(a);
- });
+ }
+
+ // Handle native asset special case
+ if (
+ address === TOKENS(this.chainId).Addresses.nativeAsset.toLowerCase()
+ ) {
+ if (!this.nativePrice) {
+ this.prices[address] = this.fetchNative();
}
- } catch (error) {
- console.error(error);
+
+ return this.prices[address];
}
+
+ this.prices[address] = this.debouncer
+ .fetch(address)
+ .then((prices) => prices[address]);
}
- return this.prices[unwrapped];
+ return this.prices[address];
}
async findBy(attribute: string, value: string): Promise {
@@ -154,35 +117,7 @@ export class CoingeckoPriceRepository implements Findable {
return '2';
}
- private addressMapIn(address: string): string {
- const addressMap = TOKENS(this.chainId).PriceChainMap;
- return (addressMap && addressMap[address.toLowerCase()]) || address;
- }
-
- private unwrapToken(wrappedAddress: string) {
- const chainId = isEthereumTestnet(this.chainId)
- ? Network.MAINNET
- : this.chainId;
- return unwrapToken(wrappedAddress, chainId);
- }
-
private url(addresses: string[]): string {
return `${this.urlBase}&contract_addresses=${addresses.join(',')}`;
}
}
-
-const unwrapToken = (wrappedAddress: string, chainId: Network) => {
- const lowercase = wrappedAddress.toLocaleLowerCase();
-
- const aaveChain = chainId as keyof typeof aaveWrappedMap;
- if (
- aaveWrappedMap[aaveChain] &&
- Object.keys(aaveWrappedMap[aaveChain])?.includes(lowercase)
- ) {
- return aaveWrappedMap[aaveChain][
- lowercase as keyof typeof aaveWrappedMap[typeof aaveChain]
- ].aToken;
- } else {
- return lowercase;
- }
-};
diff --git a/balancer-js/src/modules/data/token-prices/historical-price-provider.ts b/balancer-js/src/modules/data/token-prices/historical-price-provider.ts
new file mode 100644
index 000000000..ab37ac201
--- /dev/null
+++ b/balancer-js/src/modules/data/token-prices/historical-price-provider.ts
@@ -0,0 +1,38 @@
+import type { Findable, Price } from '@/types';
+import { IAaveRates } from './aave-rates';
+
+export class HistoricalPriceProvider implements Findable {
+ constructor(
+ private coingeckoRepository: Findable,
+ private aaveRates: IAaveRates
+ ) {}
+
+ /**
+ * get the historical price at time of call
+ *
+ * @param address the token address
+ */
+ async find(address: string): Promise {
+ return this.findBy(address, Math.floor(Date.now() / 1000));
+ }
+
+ /**
+ * get the historical price at the given timestamp.
+ *
+ * @param address the token address
+ * @param timestamp the UNIX timestamp
+ * @private
+ */
+ async findBy(address: string, timestamp: number): Promise {
+ const price = await this.coingeckoRepository.findBy(address, timestamp);
+ const rate = (await this.aaveRates.getRate(address)) || 1;
+ if (price && price.usd) {
+ return {
+ ...price,
+ usd: (parseFloat(price.usd) * rate).toString(),
+ };
+ } else {
+ return price;
+ }
+ }
+}
diff --git a/balancer-js/src/modules/data/token-prices/index.ts b/balancer-js/src/modules/data/token-prices/index.ts
index 61defd9af..2ef329639 100644
--- a/balancer-js/src/modules/data/token-prices/index.ts
+++ b/balancer-js/src/modules/data/token-prices/index.ts
@@ -1,4 +1,7 @@
export * from './static';
export * from './coingecko';
+export * from './coingecko-historical';
+export * from './subgraph';
export * from './provider';
+export * from './historical-price-provider';
export * from './aave-rates';
diff --git a/balancer-js/src/modules/data/token-prices/provider.ts b/balancer-js/src/modules/data/token-prices/provider.ts
index 79138f138..6cf66feb3 100644
--- a/balancer-js/src/modules/data/token-prices/provider.ts
+++ b/balancer-js/src/modules/data/token-prices/provider.ts
@@ -1,15 +1,23 @@
import type { Findable, Price } from '@/types';
-import { AaveRates } from './aave-rates';
-import { CoingeckoPriceRepository } from './coingecko';
+import { IAaveRates } from './aave-rates';
export class TokenPriceProvider implements Findable {
constructor(
- private coingeckoRepository: CoingeckoPriceRepository,
- private aaveRates: AaveRates
+ private coingeckoRepository: Findable,
+ private subgraphRepository: Findable,
+ private aaveRates: IAaveRates
) {}
async find(address: string): Promise {
- const price = await this.coingeckoRepository.find(address);
+ let price;
+ try {
+ price = await this.coingeckoRepository.find(address);
+ if (!price?.usd) {
+ price = await this.subgraphRepository.find(address);
+ }
+ } catch (err) {
+ console.error(err);
+ }
const rate = (await this.aaveRates.getRate(address)) || 1;
if (price && price.usd) {
return {
@@ -24,8 +32,7 @@ export class TokenPriceProvider implements Findable {
async findBy(attribute: string, value: string): Promise {
if (attribute === 'address') {
return this.find(value);
- } else {
- throw `Token price search by ${attribute} not implemented`;
}
+ throw `Token price search by ${attribute} not implemented`;
}
}
diff --git a/balancer-js/src/modules/data/token-prices/subgraph.spec.ts b/balancer-js/src/modules/data/token-prices/subgraph.spec.ts
new file mode 100644
index 000000000..b80053bb3
--- /dev/null
+++ b/balancer-js/src/modules/data/token-prices/subgraph.spec.ts
@@ -0,0 +1,69 @@
+import { expect } from 'chai';
+import { SubgraphPriceRepository } from './subgraph';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config';
+
+const url = BALANCER_NETWORK_CONFIG[1].urls.subgraph;
+
+const mockedResponse = {
+ data: {
+ tokens: [
+ {
+ address: '0x028171bca77440897b824ca71d1c56cac55b68a3',
+ latestUSDPrice: 1,
+ },
+ {
+ address: '0x3ed3b47dd13ec9a98b44e6204a523e766b225811',
+ latestUSDPrice: 1,
+ },
+ {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ latestUSDPrice: 1,
+ },
+ {
+ address: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56',
+ latestUSDPrice: 1,
+ },
+ ],
+ },
+};
+
+const addresses = mockedResponse.data.tokens.map((t) => t.address);
+
+const repository = new SubgraphPriceRepository(1);
+
+describe('subgraph price repository', () => {
+ let mock: MockAdapter;
+
+ before(() => {
+ mock = new MockAdapter(axios);
+ mock.onPost(url).reply(
+ () =>
+ new Promise((resolve) => {
+ setTimeout(() => resolve([200, mockedResponse]), 10);
+ })
+ );
+ });
+
+ after(() => {
+ mock.restore();
+ });
+
+ it('finds prices', async () => {
+ const [price1, price2, price3, price4, price5, price6] = await Promise.all([
+ repository.find(addresses[0]),
+ repository.find(addresses[0].toUpperCase()),
+ repository.find(addresses[1]),
+ repository.find(addresses[2]),
+ repository.find(addresses[3]),
+ repository.find(addresses[3]),
+ ]);
+ expect(price1?.usd).to.eq(1);
+ expect(price2?.usd).to.eq(1);
+ expect(price3?.usd).to.eq(1);
+ expect(price4?.usd).to.eq(1);
+ expect(price5?.usd).to.eq(1);
+ expect(price6?.usd).to.eq(1);
+ });
+});
diff --git a/balancer-js/src/modules/data/token-prices/subgraph.ts b/balancer-js/src/modules/data/token-prices/subgraph.ts
new file mode 100644
index 000000000..af46361ba
--- /dev/null
+++ b/balancer-js/src/modules/data/token-prices/subgraph.ts
@@ -0,0 +1,88 @@
+/* eslint-disable @typescript-eslint/no-empty-function */
+import { Price, Findable, TokenPrices, Network } from '@/types';
+import axios from 'axios';
+import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config';
+import { Debouncer, tokenAddressForPricing } from '@/lib/utils';
+
+interface SubgraphPricesResponse {
+ data: {
+ tokens: [
+ {
+ address: string;
+ latestUSDPrice?: string;
+ }
+ ];
+ };
+}
+
+export class SubgraphPriceRepository implements Findable {
+ private subgraphUrl: string;
+ prices: { [key: string]: Promise } = {};
+ debouncer: Debouncer;
+
+ constructor(private chainId: Network = 1) {
+ this.subgraphUrl = BALANCER_NETWORK_CONFIG[chainId].urls.subgraph;
+ this.debouncer = new Debouncer(
+ this.fetch.bind(this),
+ 200
+ );
+ }
+
+ private async fetch(
+ addresses: string[],
+ { signal }: { signal?: AbortSignal } = {}
+ ): Promise {
+ console.time(`fetching subgraph prices for ${addresses.length} tokens`);
+ return axios
+ .post(
+ this.subgraphUrl,
+ {
+ variables: { addresses },
+ query: `query($addresses: [String!]) {
+ tokens(
+ where: {
+ id_in: $addresses
+ }
+ ) {
+ address
+ latestUSDPrice
+ }
+ }`,
+ },
+ { signal }
+ )
+ .then((response) => response.data.data)
+ .then(({ tokens }) =>
+ Object.fromEntries(
+ tokens.map((token) => [
+ token.address,
+ { usd: token.latestUSDPrice || undefined },
+ ])
+ )
+ )
+ .finally(() => {
+ console.timeEnd(
+ `fetching subgraph prices for ${addresses.length} tokens`
+ );
+ });
+ }
+
+ async find(inputAddress: string): Promise {
+ const address = tokenAddressForPricing(inputAddress, this.chainId);
+ if (!this.prices[address]) {
+ this.prices[address] = this.debouncer
+ .fetch(address)
+ .then((prices) => prices[address]);
+ }
+
+ return this.prices[address];
+ }
+
+ async findBy(attribute: string, value: string): Promise {
+ if (attribute != 'address') {
+ return undefined;
+ }
+
+ return this.find(value);
+ }
+}
diff --git a/balancer-js/src/modules/data/token-yields/tokens/lido-polygon.ts b/balancer-js/src/modules/data/token-yields/tokens/lido-polygon.ts
index 22036bc84..2b2cfa509 100644
--- a/balancer-js/src/modules/data/token-yields/tokens/lido-polygon.ts
+++ b/balancer-js/src/modules/data/token-yields/tokens/lido-polygon.ts
@@ -19,7 +19,9 @@ export const lidoPolygon: AprFetcher = async () => {
let returnApr = 0;
try {
- const response = await axios.get('https://polygon.lido.fi/api/stats');
+ const response = await axios.get(
+ 'https://lido-aprs-proxy.balancer.workers.dev/?network=137'
+ );
const { apr } = response.data as LidoAPIResponse;
returnApr = Math.round(parseFloat(apr) * 100);
diff --git a/balancer-js/src/modules/data/token-yields/tokens/lido.spec.ts b/balancer-js/src/modules/data/token-yields/tokens/lido.spec.ts
index b1e4ad208..74543ace4 100644
--- a/balancer-js/src/modules/data/token-yields/tokens/lido.spec.ts
+++ b/balancer-js/src/modules/data/token-yields/tokens/lido.spec.ts
@@ -4,7 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
const mockedResponse = {
- data: { eth: '1', steth: '1' },
+ data: { smaApr: '1' },
};
describe('lido apr', () => {
@@ -13,7 +13,7 @@ describe('lido apr', () => {
before(() => {
mock = new MockAdapter(axios);
mock
- .onGet('https://stake.lido.fi/api/apr')
+ .onGet('https://lido-aprs-proxy.balancer.workers.dev/?network=1')
.reply(() => [200, mockedResponse]);
});
diff --git a/balancer-js/src/modules/data/token-yields/tokens/lido.ts b/balancer-js/src/modules/data/token-yields/tokens/lido.ts
index 5ec9b483f..1e3b2ee49 100644
--- a/balancer-js/src/modules/data/token-yields/tokens/lido.ts
+++ b/balancer-js/src/modules/data/token-yields/tokens/lido.ts
@@ -2,14 +2,14 @@ import { AprFetcher } from '../repository';
import axios from 'axios';
export const yieldTokens = {
- stETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
+ stETH: '0xae7ab96520de3a18e5e111b5eaab095312d7fe84',
+ wstETH: '0x7f39c581f595b53c5cb19bd0b3f8da6c935e2ca0',
arbitrumStEth: '0x5979d7b546e38e414f7e9822514be443a4800529',
};
interface LidoAPIResponse {
data: {
- eth: string;
- steth: string;
+ smaApr: string;
};
}
@@ -22,16 +22,19 @@ export const lido: AprFetcher = async () => {
let apr = 0;
try {
- const response = await axios.get('https://stake.lido.fi/api/apr');
+ const response = await axios.get(
+ 'https://lido-aprs-proxy.balancer.workers.dev/?network=1'
+ );
const { data: aprs } = response.data as LidoAPIResponse;
- apr = Math.round(parseFloat(aprs.steth) * 100);
+ apr = Math.round(parseFloat(aprs.smaApr) * 100);
} catch (error) {
console.error('Failed to fetch stETH APR:', error);
}
return {
[yieldTokens.stETH]: apr,
+ [yieldTokens.wstETH]: apr,
[yieldTokens.arbitrumStEth]: apr,
};
};
diff --git a/balancer-js/src/modules/data/types.ts b/balancer-js/src/modules/data/types.ts
index 2d46fda30..939d5a9f2 100644
--- a/balancer-js/src/modules/data/types.ts
+++ b/balancer-js/src/modules/data/types.ts
@@ -1,3 +1,5 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
export { LiquidityGauge } from './liquidity-gauges/provider';
export { PoolAttribute } from './pool/types';
export { TokenAttribute } from './token/types';
@@ -6,9 +8,9 @@ export * from './pool-gauges/types';
export * from './pool-shares/types';
export * from './gauge-shares/types';
-export interface Findable {
+export interface Findable {
find: (id: string) => Promise;
- findBy: (attribute: P, value: string) => Promise;
+ findBy: (attribute: P, value: V) => Promise;
}
export interface Searchable {
diff --git a/balancer-js/src/modules/exits/exits.module.integration.spec.ts b/balancer-js/src/modules/exits/exits.module.integration.spec.ts
new file mode 100644
index 000000000..9cdb66a29
--- /dev/null
+++ b/balancer-js/src/modules/exits/exits.module.integration.spec.ts
@@ -0,0 +1,546 @@
+// yarn test:only ./src/modules/exits/exits.module.integration.spec.ts
+import dotenv from 'dotenv';
+import { expect } from 'chai';
+import hardhat from 'hardhat';
+
+import { BalancerSDK, BalancerTenderlyConfig, Network } from '@/.';
+import { BigNumber, parseFixed } from '@ethersproject/bignumber';
+import { Contracts } from '@/modules/contracts/contracts.module';
+import { forkSetup, getBalances } from '@/test/lib/utils';
+import { ADDRESSES } from '@/test/lib/constants';
+import { Relayer } from '@/modules/relayer/relayer.module';
+import { JsonRpcSigner } from '@ethersproject/providers';
+
+dotenv.config();
+
+const TEST_BOOSTED = true;
+const TEST_BOOSTED_META = true;
+const TEST_BOOSTED_META_ALT = true;
+const TEST_BOOSTED_META_BIG = true;
+const TEST_BOOSTED_WEIGHTED_SIMPLE = true;
+const TEST_BOOSTED_WEIGHTED_GENERAL = true;
+const TEST_BOOSTED_WEIGHTED_META = true;
+const TEST_BOOSTED_WEIGHTED_META_ALT = true;
+const TEST_BOOSTED_WEIGHTED_META_GENERAL = true;
+
+/*
+ * Testing on GOERLI
+ * - Run node on terminal: yarn run node:goerli
+ * - Uncomment section below:
+ */
+const network = Network.GOERLI;
+const blockNumber = 7890980;
+const customSubgraphUrl =
+ 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2-beta';
+const { ALCHEMY_URL_GOERLI: jsonRpcUrl } = process.env;
+const rpcUrl = 'http://127.0.0.1:8000';
+
+/*
+ * Testing on MAINNET
+ * - Run node on terminal: yarn run node
+ * - Uncomment section below:
+ */
+// const network = Network.MAINNET;
+// const blockNumber = 15519886;
+// const customSubgraphUrl =
+// 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2-beta';
+// const { ALCHEMY_URL: jsonRpcUrl } = process.env;
+// const rpcUrl = 'http://127.0.0.1:8545';
+
+const { TENDERLY_ACCESS_KEY, TENDERLY_USER, TENDERLY_PROJECT } = process.env;
+const { ethers } = hardhat;
+const MAX_GAS_LIMIT = 8e6;
+
+// Custom Tenderly configuration parameters - remove in order to use default values
+const tenderlyConfig: BalancerTenderlyConfig = {
+ accessKey: TENDERLY_ACCESS_KEY as string,
+ user: TENDERLY_USER as string,
+ project: TENDERLY_PROJECT as string,
+ blockNumber,
+};
+
+const sdk = new BalancerSDK({
+ network,
+ rpcUrl,
+ customSubgraphUrl,
+ tenderly: tenderlyConfig,
+});
+const { pools } = sdk;
+const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network);
+const signer = provider.getSigner();
+const { contracts, contractAddresses } = new Contracts(
+ network as number,
+ provider
+);
+const relayer = contractAddresses.relayerV4 as string;
+const addresses = ADDRESSES[network];
+
+interface Test {
+ signer: JsonRpcSigner;
+ description: string;
+ pool: {
+ id: string;
+ address: string;
+ };
+ amount: string;
+ authorisation: string | undefined;
+}
+
+const runTests = async (tests: Test[]) => {
+ for (let i = 0; i < tests.length; i++) {
+ const test = tests[i];
+ it(test.description, async () => {
+ const signerAddress = await test.signer.getAddress();
+ // const signerAddress = '0xb7d222a710169f42ddff2a9a5122bd7c724dc203';
+ const authorisation = await Relayer.signRelayerApproval(
+ relayer,
+ signerAddress,
+ test.signer,
+ contracts.vault
+ );
+ // const authorisation = undefined;
+ await testFlow(
+ test.signer,
+ signerAddress,
+ test.pool,
+ test.amount,
+ authorisation
+ );
+ }).timeout(120000);
+ }
+};
+
+const testFlow = async (
+ signer: JsonRpcSigner,
+ signerAddress: string,
+ pool: { id: string; address: string },
+ amount: string,
+ authorisation: string | undefined
+) => {
+ const gasLimit = MAX_GAS_LIMIT;
+ const slippage = '10'; // 10 bps = 0.1%
+
+ const { to, callData, tokensOut, expectedAmountsOut, minAmountsOut } =
+ await pools.generalisedExit(
+ pool.id,
+ amount,
+ signerAddress,
+ slippage,
+ authorisation
+ );
+
+ const [bptBalanceBefore, ...tokensOutBalanceBefore] = await getBalances(
+ [pool.address, ...tokensOut],
+ signer,
+ signerAddress
+ );
+
+ const response = await signer.sendTransaction({
+ to,
+ data: callData,
+ gasLimit,
+ });
+
+ const receipt = await response.wait();
+ console.log('Gas used', receipt.gasUsed.toString());
+
+ const [bptBalanceAfter, ...tokensOutBalanceAfter] = await getBalances(
+ [pool.address, ...tokensOut],
+ signer,
+ signerAddress
+ );
+ expect(receipt.status).to.eql(1);
+ minAmountsOut.forEach((minAmountOut) => {
+ expect(BigNumber.from(minAmountOut).gte('0')).to.be.true;
+ });
+ expectedAmountsOut.forEach((expectedAmountOut, i) => {
+ expect(
+ BigNumber.from(expectedAmountOut).gte(BigNumber.from(minAmountsOut[i]))
+ ).to.be.true;
+ });
+ expect(bptBalanceAfter.eq(bptBalanceBefore.sub(amount))).to.be.true;
+ tokensOutBalanceBefore.forEach((b) => expect(b.eq(0)).to.be.true);
+ tokensOutBalanceAfter.forEach((balanceAfter, i) => {
+ const minOut = BigNumber.from(minAmountsOut[i]);
+ return expect(balanceAfter.gte(minOut)).to.be.true;
+ });
+ // console.log('bpt after', query.tokensOut.toString());
+ // console.log('minOut', minAmountsOut.toString());
+ // console.log('expectedOut', expectedAmountsOut.toString());
+};
+
+// all contexts currently applies to GOERLI only
+describe('generalised exit execution', async () => {
+ /*
+ bbamaiweth: ComposableStable, baMai/baWeth
+ baMai: Linear, aMai/Mai
+ baWeth: Linear, aWeth/Weth
+ */
+ context('boosted', async () => {
+ if (!TEST_BOOSTED) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.bbamaiweth.address];
+ const slots = [addresses.bbamaiweth.slot];
+ const balances = [
+ parseFixed('0.02', addresses.bbamaiweth.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.bbamaiweth.id,
+ address: addresses.bbamaiweth.address,
+ },
+ amount: parseFixed('0.01', addresses.bbamaiweth.decimals).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedMeta1: ComposableStable, baMai/bbausd2
+ baMai: Linear, aMai/Mai
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMeta', async () => {
+ if (!TEST_BOOSTED_META) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedMeta1.address];
+ const slots = [addresses.boostedMeta1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedMeta1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedMeta1.id,
+ address: addresses.boostedMeta1.address,
+ },
+ amount: parseFixed('0.05', addresses.boostedMeta1.decimals).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedMetaAlt1: ComposableStable, Mai/bbausd2
+ Mai
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMetaAlt', async () => {
+ if (!TEST_BOOSTED_META_ALT) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedMetaAlt1.address];
+ const slots = [addresses.boostedMetaAlt1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedMetaAlt1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedMetaAlt1.id,
+ address: addresses.boostedMetaAlt1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedMetaAlt1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedMetaBig1: ComposableStable, bbamaiweth/bbausd2
+ bbamaiweth: ComposableStable, baMai/baWeth
+ baMai: Linear, aMai/Mai
+ baWeth: Linear, aWeth/Weth
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMetaBig', async () => {
+ if (!TEST_BOOSTED_META_BIG) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedMetaBig1.address];
+ const slots = [addresses.boostedMetaBig1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedMetaBig1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedMetaBig1.id,
+ address: addresses.boostedMetaBig1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedMetaBig1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedSimple1: 1 Linear + 1 normal token
+ b-a-weth: Linear, aWeth/Weth
+ BAL
+ */
+ context('boostedWeightedSimple', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_SIMPLE) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedWeightedSimple1.address];
+ const slots = [addresses.boostedWeightedSimple1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedWeightedSimple1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedWeightedSimple1.id,
+ address: addresses.boostedWeightedSimple1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedWeightedSimple1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedGeneral1: N Linear + M normal tokens
+ b-a-dai: Linear, aDai/Dai
+ b-a-mai: Linear, aMai/Mai
+ BAL
+ USDC
+ */
+ context('boostedWeightedGeneral', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_GENERAL) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedWeightedGeneral1.address];
+ const slots = [addresses.boostedWeightedGeneral1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedWeightedGeneral1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedWeightedGeneral1.id,
+ address: addresses.boostedWeightedGeneral1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedWeightedGeneral1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMeta1: 1 Linear + 1 ComposableStable
+ b-a-weth: Linear, aWeth/Weth
+ bb-a-usd2: ComposableStable, b-a-usdc/b-a-usdt/b-a-dai
+ BAL
+ */
+ context('boostedWeightedMeta', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedWeightedMeta1.address];
+ const slots = [addresses.boostedWeightedMeta1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedWeightedMeta1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedWeightedMeta1.id,
+ address: addresses.boostedWeightedMeta1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedWeightedMeta1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMetaAlt1: 1 normal token + 1 ComposableStable
+ WETH
+ b-a-usd2: ComposableStable, b-a-usdt/b-a-usdc/b-a-dai
+ */
+ context('boostedWeightedMetaAlt', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META_ALT) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedWeightedMetaAlt1.address];
+ const slots = [addresses.boostedWeightedMetaAlt1.slot];
+ const balances = [
+ parseFixed('1', addresses.boostedWeightedMetaAlt1.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedWeightedMetaAlt1.id,
+ address: addresses.boostedWeightedMetaAlt1.address,
+ },
+ amount: parseFixed(
+ '0.01',
+ addresses.boostedWeightedMetaAlt1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMetaGeneral1: N Linear + 1 ComposableStable
+ b-a-usdt: Linear, aUSDT/USDT
+ b-a-usdc: Linear, aUSDC/USDC
+ b-a-weth: Linear, aWeth/Weth
+ b-a-usd2: ComposableStable, b-a-usdt/b-a-usdc/b-a-dai
+ */
+ context('boostedWeightedMetaGeneral', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META_GENERAL) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [addresses.boostedWeightedMetaGeneral1.address];
+ const slots = [addresses.boostedWeightedMetaGeneral1.slot];
+ const balances = [
+ parseFixed(
+ '1',
+ addresses.boostedWeightedMetaGeneral1.decimals
+ ).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'exit pool',
+ pool: {
+ id: addresses.boostedWeightedMetaGeneral1.id,
+ address: addresses.boostedWeightedMetaGeneral1.address,
+ },
+ amount: parseFixed(
+ '0.05',
+ addresses.boostedWeightedMetaGeneral1.decimals
+ ).toString(),
+ authorisation: authorisation,
+ },
+ ]);
+ });
+});
diff --git a/balancer-js/src/modules/exits/exits.module.ts b/balancer-js/src/modules/exits/exits.module.ts
new file mode 100644
index 000000000..a7f068820
--- /dev/null
+++ b/balancer-js/src/modules/exits/exits.module.ts
@@ -0,0 +1,685 @@
+import { defaultAbiCoder } from '@ethersproject/abi';
+import { cloneDeep } from 'lodash';
+import { Interface } from '@ethersproject/abi';
+import { BigNumber } from '@ethersproject/bignumber';
+import { MaxInt256, WeiPerEther, Zero } from '@ethersproject/constants';
+
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { Relayer } from '@/modules/relayer/relayer.module';
+import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types';
+import { WeightedPoolEncoder } from '@/pool-weighted';
+import { StablePoolEncoder } from '@/pool-stable';
+import {
+ BalancerNetworkConfig,
+ ExitPoolRequest,
+ Pool,
+ PoolAttribute,
+ PoolType,
+} from '@/types';
+import { Findable } from '../data/types';
+import { PoolGraph, Node } from '../graph/graph';
+
+import { subSlippage } from '@/lib/utils/slippageHelper';
+import TenderlyHelper from '@/lib/utils/tenderlyHelper';
+import balancerRelayerAbi from '@/lib/abi/RelayerV4.json';
+import { networkAddresses } from '@/lib/constants/config';
+import { AssetHelpers } from '@/lib/utils';
+import { getPoolAddress } from '@/pool-utils';
+import { Join } from '../joins/joins.module';
+import { calcPriceImpact } from '../pricing/priceImpact';
+
+const balancerRelayerInterface = new Interface(balancerRelayerAbi);
+
+export class Exit {
+ private wrappedNativeAsset: string;
+ private relayer: string;
+ private tenderlyHelper: TenderlyHelper;
+
+ constructor(
+ private pools: Findable,
+ private networkConfig: BalancerNetworkConfig
+ ) {
+ const { tokens, contracts } = networkAddresses(networkConfig.chainId);
+ this.wrappedNativeAsset = tokens.wrappedNativeAsset;
+ this.relayer = contracts.relayerV4 as string;
+
+ this.tenderlyHelper = new TenderlyHelper(
+ networkConfig.chainId,
+ networkConfig.tenderly
+ );
+ }
+
+ async exitPool(
+ poolId: string,
+ amountBptIn: string,
+ userAddress: string,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ tokensOut: string[];
+ expectedAmountsOut: string[];
+ minAmountsOut: string[];
+ priceImpact: string;
+ }> {
+ /*
+ Overall exit flow description:
+ - Create calls with 0 expected min amount for each token out
+ - static call (or V4 special call) to get actual amounts for each token out
+ - Apply slippage to amountsOut
+ - Recreate calls with minAmounts === actualAmountsWithSlippage
+ - Return minAmoutsOut, UI would use this to display to user
+ - Return updatedCalls, UI would use this to execute tx
+ */
+
+ // Create nodes and order by breadth first
+ const orderedNodes = await PoolGraph.getGraphNodes(
+ false,
+ this.networkConfig.chainId,
+ poolId,
+ this.pools,
+ false
+ );
+
+ // Create exit paths for each output node and splits amount in proportionally between them
+ const outputNodes = orderedNodes.filter((n) => n.exitAction === 'output');
+
+ const exitPaths = this.getExitPaths(outputNodes, amountBptIn);
+
+ const tokensOutByExitPath = outputNodes.map((n) => n.address.toLowerCase());
+ const tokensOut = [...new Set(tokensOutByExitPath)].sort();
+
+ // Create calls with minimum expected amount out for each exit path
+ const staticCall = await this.createCalls(
+ exitPaths,
+ userAddress,
+ undefined,
+ authorisation
+ );
+
+ const { expectedAmountsOutByExitPath, minAmountsOutByExitPath } =
+ await this.amountsOutByExitPath(
+ userAddress,
+ staticCall.callData,
+ orderedNodes[0].address,
+ staticCall.outputIndexes,
+ slippage
+ );
+
+ // Create calls with minimum expected amount out for each exit path
+ const { callData, deltas } = await this.createCalls(
+ exitPaths,
+ userAddress,
+ minAmountsOutByExitPath,
+ authorisation
+ );
+
+ const { expectedAmountsOut, minAmountsOut } = this.amountsOutByTokenOut(
+ tokensOut,
+ tokensOutByExitPath,
+ expectedAmountsOutByExitPath,
+ slippage
+ );
+
+ this.assertDeltas(poolId, deltas, amountBptIn, tokensOut, minAmountsOut);
+
+ const priceImpact = await this.calculatePriceImpact(
+ poolId,
+ tokensOut,
+ expectedAmountsOut,
+ amountBptIn
+ );
+
+ return {
+ to: this.relayer,
+ callData,
+ tokensOut,
+ expectedAmountsOut,
+ minAmountsOut,
+ priceImpact,
+ };
+ }
+
+ /*
+ (From Fernando)
+ 1. Given a bpt amount in find the expect token amounts out (proportionally)
+ 2. Uses bptZeroPi = _bptForTokensZeroPriceImpact (the same is used for joins too)
+ 3. PI = bptAmountIn / bptZeroPi - 1
+ */
+ private async calculatePriceImpact(
+ poolId: string,
+ tokensOut: string[],
+ amountsOut: string[],
+ amountBptIn: string
+ ): Promise {
+ // Create nodes for each pool/token interaction and order by breadth first
+ const orderedNodesForJoin = await PoolGraph.getGraphNodes(
+ true,
+ this.networkConfig.chainId,
+ poolId,
+ this.pools,
+ false
+ );
+ const joinPaths = Join.getJoinPaths(
+ orderedNodesForJoin,
+ tokensOut,
+ amountsOut
+ );
+ const totalBptZeroPi = Join.totalBptZeroPriceImpact(joinPaths);
+ const priceImpact = calcPriceImpact(
+ BigInt(amountBptIn),
+ totalBptZeroPi.toBigInt(),
+ false
+ ).toString();
+ return priceImpact;
+ }
+
+ private assertDeltas(
+ poolId: string,
+ deltas: Record,
+ bptIn: string,
+ tokensOut: string[],
+ amountsOut: string[]
+ ): void {
+ const poolAddress = getPoolAddress(poolId);
+ const outDiff = deltas[poolAddress.toLowerCase()].sub(bptIn);
+
+ if (outDiff.abs().gt(3)) {
+ console.error(
+ `exit assertDeltas, bptIn: `,
+ poolAddress,
+ bptIn,
+ deltas[poolAddress.toLowerCase()]?.toString()
+ );
+ throw new BalancerError(BalancerErrorCode.EXIT_DELTA_AMOUNTS);
+ }
+ delete deltas[poolAddress.toLowerCase()];
+
+ tokensOut.forEach((token, i) => {
+ const diff = deltas[token.toLowerCase()].add(amountsOut[i]);
+ if (diff.abs().gt(1)) {
+ console.error(
+ `exit assertDeltas, tokenOut: `,
+ token,
+ amountsOut[i],
+ deltas[token.toLowerCase()]?.toString()
+ );
+ throw new BalancerError(BalancerErrorCode.EXIT_DELTA_AMOUNTS);
+ }
+ delete deltas[token.toLowerCase()];
+ });
+
+ for (const token in deltas) {
+ if (deltas[token].toString() !== '0') {
+ console.error(
+ `exit assertDeltas, non-input token should be 0: `,
+ token,
+ deltas[token].toString()
+ );
+ throw new BalancerError(BalancerErrorCode.EXIT_DELTA_AMOUNTS);
+ }
+ }
+ }
+
+ // Query amounts out through static call and return decoded result
+ private amountsOutByExitPath = async (
+ userAddress: string,
+ callData: string,
+ tokenIn: string,
+ outputIndexes: number[],
+ slippage: string
+ ): Promise<{
+ expectedAmountsOutByExitPath: string[];
+ minAmountsOutByExitPath: string[];
+ }> => {
+ const simulationResult = await this.tenderlyHelper.simulateMulticall(
+ this.relayer,
+ callData,
+ userAddress,
+ [tokenIn]
+ );
+
+ // Decode each exit path amount out from static call result
+ const multiCallResult = defaultAbiCoder.decode(
+ ['bytes[]'],
+ simulationResult
+ )[0] as string[];
+
+ const expectedAmountsOutByExitPath = outputIndexes.map((outputIndex) => {
+ const result = defaultAbiCoder.decode(
+ ['uint256'],
+ multiCallResult[outputIndex]
+ );
+ return result.toString();
+ });
+
+ // Apply slippage tolerance on expected amount out for each exit path
+ const minAmountsOutByExitPath = expectedAmountsOutByExitPath.map(
+ (expectedAmountOut) =>
+ subSlippage(
+ BigNumber.from(expectedAmountOut),
+ BigNumber.from(slippage)
+ ).toString()
+ );
+
+ return { expectedAmountsOutByExitPath, minAmountsOutByExitPath };
+ };
+
+ // Aggregate amounts out by exit path into amounts out by token out
+ private amountsOutByTokenOut = (
+ tokensOut: string[],
+ tokensOutByExitPath: string[],
+ expectedAmountsOutByExitPath: string[],
+ slippage: string
+ ) => {
+ // Aggregate amountsOutByExitPath into expectedAmountsOut
+ const expectedAmountsOutMap: Record = {};
+ tokensOutByExitPath.forEach(
+ (tokenOut, i) =>
+ (expectedAmountsOutMap[tokenOut] = (
+ expectedAmountsOutMap[tokenOut] ?? Zero
+ ).add(expectedAmountsOutByExitPath[i]))
+ );
+ const expectedAmountsOut = tokensOut.map((tokenOut) =>
+ expectedAmountsOutMap[tokenOut].toString()
+ );
+
+ // Apply slippage tolerance on each expected amount out
+ const minAmountsOut = expectedAmountsOut.map((expectedAmountOut) =>
+ subSlippage(
+ BigNumber.from(expectedAmountOut),
+ BigNumber.from(slippage)
+ ).toString()
+ );
+
+ return { expectedAmountsOut, minAmountsOut };
+ };
+
+ // Create one exit path for each output node
+ private getExitPaths = (outputNodes: Node[], amountIn: string): Node[][] => {
+ const exitPaths = outputNodes.map((outputNode) => {
+ const exitPath = [outputNode];
+ while (exitPath[0].parent) {
+ exitPath.unshift(cloneDeep(exitPath[0].parent));
+ }
+ /*
+ The input/root node requires a real amount (not a reference/index) as it is first node in chain.
+ This amount will be used when chaining to children.
+ */
+ exitPath[0].index = exitPath[exitPath.length - 1].proportionOfParent
+ .mul(amountIn)
+ .div(WeiPerEther)
+ .toString();
+ return exitPath;
+ });
+
+ /*
+ Amounts in for exit paths should be adjusted after caculated to fix eventual rounding issues
+ */
+ // Sum amountIn for each exit path
+ const amountsInSum = exitPaths.reduce((accumulator, currentExitPath) => {
+ const amountInForCurrentExitPath = currentExitPath[0].index;
+ return BigNumber.from(amountInForCurrentExitPath).add(accumulator);
+ }, Zero);
+ // Compare total amountIn with sum of calculated amountIn for each exit path
+ const amountsInDiff = BigNumber.from(amountIn).sub(amountsInSum);
+ // Add diff to last exit path amountIn
+ exitPaths[exitPaths.length - 1][0].index = amountsInDiff
+ .add(exitPaths[exitPaths.length - 1][0].index)
+ .toString();
+
+ return exitPaths;
+ };
+
+ private async createCalls(
+ exitPaths: Node[][],
+ userAddress: string,
+ minAmountsOut?: string[],
+ authorisation?: string
+ ): Promise<{
+ callData: string;
+ outputIndexes: number[];
+ deltas: Record;
+ }> {
+ const { calls, outputIndexes, deltas } = this.createActionCalls(
+ cloneDeep(exitPaths),
+ userAddress,
+ minAmountsOut
+ );
+
+ if (authorisation) {
+ calls.unshift(
+ Relayer.encodeSetRelayerApproval(this.relayer, true, authorisation)
+ );
+ }
+
+ const callData = balancerRelayerInterface.encodeFunctionData('multicall', [
+ calls,
+ ]);
+
+ return {
+ callData,
+ outputIndexes: authorisation
+ ? outputIndexes.map((i) => i + 1)
+ : outputIndexes,
+ deltas,
+ };
+ }
+
+ updateDeltas(
+ deltas: Record,
+ assets: string[],
+ amounts: string[]
+ ): Record {
+ assets.forEach((t, i) => {
+ const asset = t.toLowerCase();
+ if (!deltas[asset]) deltas[asset] = Zero;
+ deltas[asset] = deltas[asset].add(amounts[i]);
+ });
+ return deltas;
+ }
+
+ private createActionCalls(
+ exitPaths: Node[][],
+ userAddress: string,
+ minAmountsOut?: string[]
+ ): {
+ calls: string[];
+ outputIndexes: number[];
+ deltas: Record;
+ } {
+ const calls: string[] = [];
+ const outputIndexes: number[] = [];
+ const isPeek = !minAmountsOut;
+ const deltas: Record = {};
+
+ // Create actions for each Node and return in multicall array
+
+ exitPaths.forEach((exitPath, i) => {
+ exitPath.forEach((node) => {
+ // Calls from root node are sent by the user. Otherwise sent by the relayer
+ const isRootNode = !node.parent;
+ const sender = isRootNode ? userAddress : this.relayer;
+ // Always send to user on output calls otherwise send to relayer
+ const exitChild = node.children.find((child) =>
+ exitPath.map((n) => n.index).includes(child.index)
+ );
+ const isLastActionFromExitPath = exitChild?.exitAction === 'output';
+ const recipient = isLastActionFromExitPath ? userAddress : this.relayer;
+ // Last calls will use minAmountsOut to protect user. Middle calls can safely have 0 minimum as tx will revert if last fails.
+ const minAmountOut =
+ isLastActionFromExitPath && minAmountsOut ? minAmountsOut[i] : '0';
+
+ switch (node.exitAction) {
+ case 'batchSwap': {
+ const [call, assets, limits] = this.createBatchSwap(
+ node,
+ exitChild as Node,
+ i,
+ minAmountOut,
+ sender,
+ recipient
+ );
+ calls.push(call);
+ this.updateDeltas(deltas, assets, limits);
+ break;
+ }
+ case 'exitPool': {
+ const [call, bptIn, tokensOut, amountsOut] = this.createExitPool(
+ node,
+ exitChild as Node,
+ i,
+ minAmountOut,
+ sender,
+ recipient
+ );
+ calls.push(call);
+ this.updateDeltas(
+ deltas,
+ [node.address, ...tokensOut],
+ [bptIn, ...amountsOut]
+ );
+ break;
+ }
+ case 'output':
+ if (isPeek) {
+ calls.push(
+ Relayer.encodePeekChainedReferenceValue(
+ Relayer.toChainedReference(
+ this.getOutputRef(i, node.index),
+ false
+ )
+ )
+ );
+ outputIndexes.push(calls.length - 1);
+ }
+ break;
+ default:
+ return;
+ }
+ });
+ });
+
+ return { calls, outputIndexes, deltas };
+ }
+
+ private createBatchSwap(
+ node: Node,
+ exitChild: Node,
+ exitPathIndex: number,
+ minAmountOut: string,
+ sender: string,
+ recipient: string
+ ): [string, string[], string[]] {
+ const isRootNode = !node.parent;
+ const amountIn = isRootNode
+ ? node.index
+ : Relayer.toChainedReference(
+ this.getOutputRef(exitPathIndex, node.index)
+ ).toString();
+
+ const tokenOut = exitChild.address;
+ const assets = [tokenOut, node.address];
+
+ // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number.
+ // First asset will always be the output token so use expectedOut to set limit
+ // We don't know input amounts if they are part of a chain so set to max input
+ // TODO can we be safer?
+ const limits: string[] = [
+ BigNumber.from(minAmountOut).mul(-1).toString(),
+ MaxInt256.toString(),
+ ];
+
+ // TODO Change to single swap to save gas
+ const swaps: BatchSwapStep[] = [
+ {
+ poolId: node.id,
+ assetInIndex: 1,
+ assetOutIndex: 0,
+ amount: amountIn,
+ userData: '0x',
+ },
+ ];
+
+ const funds: FundManagement = {
+ sender,
+ recipient,
+ fromInternalBalance: false,
+ toInternalBalance: false,
+ };
+
+ const outputReferences = [
+ {
+ index: assets
+ .map((a) => a.toLowerCase())
+ .indexOf(tokenOut.toLowerCase()),
+ key: Relayer.toChainedReference(
+ this.getOutputRef(exitPathIndex, exitChild.index)
+ ),
+ },
+ ];
+
+ // console.log(
+ // `${node.type} ${node.address} prop: ${formatFixed(
+ // node.proportionOfParent,
+ // 18
+ // )}
+ // ${node.exitAction}(
+ // inputAmt: ${amountIn},
+ // inputToken: ${node.address},
+ // pool: ${node.id},
+ // outputToken: ${exitChild.address},
+ // outputRef: ${this.getOutputRef(exitPathIndex, exitChild.index)},
+ // sender: ${sender},
+ // recipient: ${recipient}
+ // )`
+ // );
+
+ const call = Relayer.encodeBatchSwap({
+ swapType: SwapType.SwapExactIn,
+ swaps,
+ assets,
+ funds,
+ limits,
+ deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now
+ value: '0',
+ outputReferences,
+ });
+
+ let userTokenOutAmount = limits[0];
+ const userBptAmount = limits[1];
+ // If the sender is the Relayer the swap is part of a chain and shouldn't be considered for user deltas
+ const bptIn = sender === this.relayer ? '0' : userBptAmount;
+ // If the receiver is the Relayer the swap is part of a chain and shouldn't be considered for user deltas
+ userTokenOutAmount = recipient === this.relayer ? '0' : userTokenOutAmount;
+ return [call, assets, [userTokenOutAmount, bptIn]];
+ }
+
+ private createExitPool(
+ node: Node,
+ exitChild: Node,
+ exitPathIndex: number,
+ minAmountOut: string,
+ sender: string,
+ recipient: string
+ ): [string, string, string[], string[]] {
+ const tokenOut = exitChild.address;
+ const isRootNode = !node.parent;
+ const amountIn = isRootNode
+ ? node.index
+ : Relayer.toChainedReference(
+ this.getOutputRef(exitPathIndex, node.index)
+ ).toString();
+
+ const tokensOut: string[] = [];
+ const amountsOut: string[] = [];
+
+ // tokensOut needs to include each asset even if it has 0 amount
+ node.children.forEach((child) => {
+ tokensOut.push(child.address);
+ amountsOut.push(child.address === tokenOut ? minAmountOut : '0');
+ });
+
+ if (node.type === PoolType.ComposableStable) {
+ // assets need to include the phantomPoolToken
+ tokensOut.push(node.address);
+ // need to add a placeholder so sorting works
+ amountsOut.push('0');
+ }
+
+ // sort inputs
+ const assetHelpers = new AssetHelpers(this.wrappedNativeAsset);
+ const [sortedTokens, sortedAmounts] = assetHelpers.sortTokens(
+ tokensOut,
+ amountsOut
+ ) as [string[], string[]];
+
+ // userData amounts should not include the BPT of the pool being joined
+ let userDataTokens = [];
+ const bptIndex = sortedTokens
+ .map((t) => t.toLowerCase())
+ .indexOf(node.address.toLowerCase());
+ if (bptIndex === -1) {
+ userDataTokens = sortedTokens;
+ } else {
+ userDataTokens = [
+ ...sortedTokens.slice(0, bptIndex),
+ ...sortedTokens.slice(bptIndex + 1),
+ ];
+ }
+
+ let userData: string;
+ if (node.type === PoolType.Weighted) {
+ userData = WeightedPoolEncoder.exitExactBPTInForOneTokenOut(
+ amountIn,
+ userDataTokens.indexOf(tokenOut)
+ );
+ } else {
+ userData = StablePoolEncoder.exitExactBPTInForOneTokenOut(
+ amountIn,
+ userDataTokens.indexOf(tokenOut)
+ );
+ }
+
+ const outputReferences = [
+ {
+ index: sortedTokens
+ .map((t) => t.toLowerCase())
+ .indexOf(tokenOut.toLowerCase()),
+ key: Relayer.toChainedReference(
+ this.getOutputRef(exitPathIndex, exitChild.index)
+ ),
+ },
+ ];
+
+ // console.log(
+ // `${node.type} ${node.address} prop: ${formatFixed(
+ // node.proportionOfParent,
+ // 18
+ // )}
+ // ${node.exitAction}(
+ // poolId: ${node.id},
+ // tokensOut: ${sortedTokens},
+ // tokenOut: ${sortedTokens[sortedTokens.indexOf(tokenOut)].toString()},
+ // amountOut: ${sortedAmounts[sortedTokens.indexOf(tokenOut)].toString()},
+ // amountIn: ${amountIn},
+ // minAmountOut: ${minAmountOut},
+ // outputRef: ${this.getOutputRef(exitPathIndex, exitChild.index)},
+ // sender: ${sender},
+ // recipient: ${recipient}
+ // )`
+ // );
+
+ const call = Relayer.constructExitCall({
+ poolId: node.id,
+ poolKind: 0,
+ sender,
+ recipient,
+ outputReferences,
+ exitPoolRequest: {} as ExitPoolRequest,
+ assets: sortedTokens,
+ minAmountsOut: sortedAmounts,
+ userData,
+ toInternalBalance: false,
+ });
+
+ const userAmountTokensOut = sortedAmounts.map((a) =>
+ Relayer.isChainedReference(a) ? '0' : Zero.sub(a).toString()
+ );
+ const userBptIn = Relayer.isChainedReference(amountIn) ? '0' : amountIn;
+
+ return [
+ call,
+ // If the sender is the Relayer the exit is part of a chain and shouldn't be considered for user deltas
+ sender === this.relayer ? Zero.toString() : userBptIn,
+ // If the receiver is the Relayer the exit is part of a chain and shouldn't be considered for user deltas
+ recipient === this.relayer ? [] : sortedTokens,
+ recipient === this.relayer ? [] : userAmountTokensOut,
+ ];
+ }
+
+ private getOutputRef = (exitPathIndex: number, nodeIndex: string): number => {
+ return exitPathIndex * 100 + parseInt(nodeIndex);
+ };
+}
diff --git a/balancer-js/src/modules/graph/graph.module.spec.ts b/balancer-js/src/modules/graph/graph.module.spec.ts
new file mode 100644
index 000000000..9adde62d4
--- /dev/null
+++ b/balancer-js/src/modules/graph/graph.module.spec.ts
@@ -0,0 +1,749 @@
+import { expect } from 'chai';
+import { parseFixed } from '@ethersproject/bignumber';
+import { factories } from '@/test/factories';
+import { PoolsStaticRepository } from '../data';
+import { SubgraphToken } from '@balancer-labs/sor';
+import { PoolGraph, Node } from './graph';
+import {
+ BoostedInfo,
+ BoostedMetaInfo,
+ BoostedMetaBigInfo,
+ BoostedParams,
+ LinearParams,
+ BoostedMetaBigParams,
+ Pool,
+ LinearInfo,
+} from '@/test/factories/pools';
+import { Pool as SdkPool } from '@/types';
+import { Network } from '@/lib/constants/network';
+
+function checkNode(
+ node: Node,
+ expectedId: string,
+ expectedAddress: string,
+ expectedType: string,
+ expectedJoinAction: string,
+ expectedExitAction: string,
+ childLength: number,
+ expectedOutputReference: string,
+ expectedProportionOfParent: string
+): void {
+ const proportionOfParentWei = parseFixed(expectedProportionOfParent, 18);
+ expect(node.id).to.eq(expectedId);
+ expect(node.address).to.eq(expectedAddress);
+ expect(node.type).to.eq(expectedType);
+ expect(node.joinAction).to.eq(expectedJoinAction);
+ expect(node.exitAction).to.eq(expectedExitAction);
+ expect(node.children.length).to.eq(childLength);
+ expect(node.index).to.eq(expectedOutputReference);
+ expect(node.proportionOfParent.toString()).to.eq(
+ proportionOfParentWei.toString()
+ );
+}
+
+/*
+Check a Linear Node which should consist of:
+LinearPool -> wrappedToken -> mainToken
+*/
+function checkLinearNode(
+ linearNode: Node,
+ poolIndex: number,
+ linearPools: Pool[],
+ wrappedTokens: SubgraphToken[],
+ mainTokens: SubgraphToken[],
+ expectedOutPutReference: number,
+ wrapMainTokens: boolean
+): void {
+ checkNode(
+ linearNode,
+ linearPools[poolIndex].id,
+ linearPools[poolIndex].address,
+ 'AaveLinear',
+ 'batchSwap',
+ 'batchSwap',
+ 1,
+ expectedOutPutReference.toString(),
+ linearPools[poolIndex].proportionOfParent
+ );
+ if (wrapMainTokens) {
+ checkNode(
+ linearNode.children[0],
+ 'N/A',
+ wrappedTokens[poolIndex].address,
+ 'WrappedToken',
+ 'wrapAaveDynamicToken',
+ 'unwrapAaveStaticToken',
+ 1,
+ (expectedOutPutReference + 1).toString(),
+ linearPools[poolIndex].proportionOfParent
+ );
+ checkNode(
+ linearNode.children[0].children[0],
+ 'N/A',
+ mainTokens[poolIndex].address,
+ 'Input',
+ 'input',
+ 'output',
+ 0,
+ (expectedOutPutReference + 2).toString(),
+ linearPools[poolIndex].proportionOfParent
+ );
+ } else {
+ checkNode(
+ linearNode.children[0],
+ 'N/A',
+ mainTokens[poolIndex].address,
+ 'Input',
+ 'input',
+ 'output',
+ 0,
+ (expectedOutPutReference + 1).toString(),
+ linearPools[poolIndex].proportionOfParent
+ );
+ }
+}
+
+/*
+Checks a boostedPool, a phantomStable with all constituents being Linear.
+*/
+function checkBoosted(
+ boostedNode: Node,
+ boostedPool: Pool,
+ boostedPoolInfo: BoostedInfo,
+ boostedIndex: number,
+ expectedProportionOfParent: string,
+ wrapMainTokens: boolean
+): void {
+ checkNode(
+ boostedNode,
+ boostedPool.id,
+ boostedPool.address,
+ 'ComposableStable',
+ 'joinPool',
+ 'exitPool',
+ 3,
+ boostedIndex.toString(),
+ expectedProportionOfParent
+ );
+ boostedNode.children.forEach((linearNode, i) => {
+ let linearInputRef;
+ if (wrapMainTokens) linearInputRef = boostedIndex + 1 + i * 3;
+ else linearInputRef = boostedIndex + 1 + i * 2;
+ checkLinearNode(
+ linearNode,
+ i,
+ boostedPoolInfo.linearPools,
+ boostedPoolInfo.wrappedTokens,
+ boostedPoolInfo.mainTokens,
+ linearInputRef,
+ wrapMainTokens
+ );
+ });
+}
+
+/*
+Checks a boostedMeta, a phantomStable with one Linear and one boosted.
+*/
+function checkBoostedMeta(
+ rootNode: Node,
+ boostedMetaInfo: BoostedMetaInfo,
+ wrapMainTokens: boolean
+): void {
+ // Check parent node
+ checkNode(
+ rootNode,
+ boostedMetaInfo.rootInfo.pool.id,
+ boostedMetaInfo.rootInfo.pool.address,
+ 'ComposableStable',
+ 'joinPool',
+ 'exitPool',
+ 2,
+ '0',
+ '1'
+ );
+ // Check child Boosted node
+ checkBoosted(
+ rootNode.children[0],
+ boostedMetaInfo.childBoostedInfo.rootPool,
+ boostedMetaInfo.childBoostedInfo,
+ 1,
+ boostedMetaInfo.childBoostedInfo.proportion,
+ wrapMainTokens
+ );
+ let expectedOutputReference = 11;
+ if (!wrapMainTokens) expectedOutputReference = 8;
+ // Check child Linear node
+ checkLinearNode(
+ rootNode.children[1],
+ 0,
+ boostedMetaInfo.childLinearInfo.linearPools,
+ boostedMetaInfo.childLinearInfo.wrappedTokens,
+ boostedMetaInfo.childLinearInfo.mainTokens,
+ expectedOutputReference,
+ wrapMainTokens
+ );
+}
+
+/*
+Checks a boostedBig, a phantomStable with two Boosted.
+*/
+function checkBoostedMetaBig(
+ rootNode: Node,
+ boostedMetaBigInfo: BoostedMetaBigInfo,
+ wrapMainTokens: boolean
+): void {
+ // Check parent node
+ checkNode(
+ rootNode,
+ boostedMetaBigInfo.rootPool.id,
+ boostedMetaBigInfo.rootPool.address,
+ 'ComposableStable',
+ 'joinPool',
+ 'exitPool',
+ 2,
+ '0',
+ '1'
+ );
+ let numberOfNodes = 1;
+ rootNode.children.forEach((childBoosted, i) => {
+ checkBoosted(
+ rootNode.children[i],
+ boostedMetaBigInfo.childPoolsInfo[i].rootPool,
+ boostedMetaBigInfo.childPoolsInfo[i],
+ numberOfNodes,
+ boostedMetaBigInfo.childPoolsInfo[i].proportion,
+ wrapMainTokens
+ );
+ if (wrapMainTokens)
+ numberOfNodes =
+ boostedMetaBigInfo.childPoolsInfo[i].linearPools.length * 3 + 2;
+ else
+ numberOfNodes =
+ boostedMetaBigInfo.childPoolsInfo[i].linearPools.length * 2 + 2;
+ });
+}
+
+describe('Graph', () => {
+ // Single weightedPool - the algo should work for single pools too?
+
+ context('linearPool', () => {
+ let linearInfo: LinearInfo;
+ let poolsGraph: PoolGraph;
+ let rootNode: Node;
+
+ before(async () => {
+ const linearPool = {
+ tokens: {
+ wrappedSymbol: 'aDAI',
+ mainSymbol: 'DAI',
+ },
+ balance: '1000000',
+ };
+
+ linearInfo = factories.linearPools
+ .transient({ pools: [linearPool] })
+ .build();
+ const poolProvider = new PoolsStaticRepository(
+ linearInfo.linearPools as unknown as SdkPool[]
+ );
+ poolsGraph = new PoolGraph(poolProvider, {
+ network: Network.GOERLI,
+ rpcUrl: '',
+ });
+ });
+ context('using wrapped tokens', () => {
+ before(async () => {
+ rootNode = await poolsGraph.buildGraphFromRootPool(
+ linearInfo.linearPools[0].id,
+ true
+ );
+ });
+ it('should build single linearPool graph', async () => {
+ checkLinearNode(
+ rootNode,
+ 0,
+ linearInfo.linearPools,
+ linearInfo.wrappedTokens,
+ linearInfo.mainTokens,
+ 0,
+ true
+ );
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(rootNode).reverse();
+ expect(orderedNodes.length).to.eq(3);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('WrappedToken');
+ expect(orderedNodes[2].type).to.eq('AaveLinear');
+ });
+ });
+ context('using non-wrapped tokens', () => {
+ before(async () => {
+ rootNode = await poolsGraph.buildGraphFromRootPool(
+ linearInfo.linearPools[0].id,
+ false
+ );
+ });
+ it('should build single linearPool graph', async () => {
+ checkLinearNode(
+ rootNode,
+ 0,
+ linearInfo.linearPools,
+ linearInfo.wrappedTokens,
+ linearInfo.mainTokens,
+ 0,
+ false
+ );
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(rootNode).reverse();
+ expect(orderedNodes.length).to.eq(2);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('AaveLinear');
+ });
+ });
+ });
+
+ context('boostedPool', () => {
+ let boostedPoolInfo: BoostedInfo;
+ let boostedPool: Pool;
+ let poolsGraph: PoolGraph;
+ let boostedNode: Node;
+
+ before(() => {
+ // The boostedPool will contain these Linear pools.
+ const linearPools = [
+ {
+ tokens: {
+ wrappedSymbol: 'aDAI',
+ mainSymbol: 'DAI',
+ },
+ balance: '1000000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDC',
+ mainSymbol: 'USDC',
+ },
+ balance: '500000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDT',
+ mainSymbol: 'USDT',
+ },
+ balance: '500000',
+ },
+ ];
+ boostedPoolInfo = factories.boostedPool
+ .transient({
+ linearPoolsParams: {
+ pools: linearPools,
+ },
+ rootId: 'phantom_boosted_1',
+ rootAddress: 'address_phantom_boosted_1',
+ })
+ .build();
+ boostedPool = boostedPoolInfo.rootPool;
+ const pools = [...boostedPoolInfo.linearPools, boostedPool];
+ // Create staticPools provider with boosted and linear pools
+ const poolProvider = new PoolsStaticRepository(
+ pools as unknown as SdkPool[]
+ );
+ poolsGraph = new PoolGraph(poolProvider, {
+ network: Network.GOERLI,
+ rpcUrl: '',
+ });
+ });
+
+ it('should throw when pool doesnt exist', async () => {
+ let errorMessage = '';
+ try {
+ await poolsGraph.buildGraphFromRootPool('thisisntapool', true);
+ } catch (error) {
+ errorMessage = (error as Error).message;
+ }
+ expect(errorMessage).to.eq('balancer pool does not exist');
+ });
+
+ context('using wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ boostedPool.id,
+ true
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoosted(
+ boostedNode,
+ boostedPoolInfo.rootPool,
+ boostedPoolInfo,
+ 0,
+ '1',
+ true
+ );
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(10);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('WrappedToken');
+ expect(orderedNodes[4].type).to.eq('WrappedToken');
+ expect(orderedNodes[5].type).to.eq('WrappedToken');
+ expect(orderedNodes[6].type).to.eq('AaveLinear');
+ expect(orderedNodes[7].type).to.eq('AaveLinear');
+ expect(orderedNodes[8].type).to.eq('AaveLinear');
+ expect(orderedNodes[9].type).to.eq('ComposableStable');
+ });
+ });
+
+ context('using non-wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ boostedPool.id,
+ false
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoosted(
+ boostedNode,
+ boostedPoolInfo.rootPool,
+ boostedPoolInfo,
+ 0,
+ '1',
+ false
+ );
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(7);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('AaveLinear');
+ expect(orderedNodes[4].type).to.eq('AaveLinear');
+ expect(orderedNodes[5].type).to.eq('AaveLinear');
+ expect(orderedNodes[6].type).to.eq('ComposableStable');
+ });
+ });
+ });
+
+ context('boostedMetaPool', () => {
+ let boostedMetaInfo: BoostedMetaInfo;
+ let rootPool: Pool;
+ let poolsGraph: PoolGraph;
+ let boostedNode: Node;
+
+ before(() => {
+ // The boostedMeta will have:
+ // - boosted with linearPools[0], linearPools[1], linearPools[2]
+ // - a single linearPool, linearPools[3]
+ // Note proportions are referenced to parent nodes
+ const childBoostedParams: BoostedParams = {
+ rootId: 'id-child',
+ rootAddress: 'address-child',
+ rootBalance: '500000',
+ linearPoolsParams: {
+ pools: [
+ {
+ tokens: {
+ wrappedSymbol: 'aDAI',
+ mainSymbol: 'DAI',
+ },
+ balance: '1000000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDC',
+ mainSymbol: 'USDC',
+ },
+ balance: '500000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDT',
+ mainSymbol: 'USDT',
+ },
+ balance: '500000',
+ },
+ ],
+ },
+ };
+ const childLinearParam: LinearParams = {
+ pools: [
+ {
+ tokens: {
+ wrappedSymbol: 'aSTABLE',
+ mainSymbol: 'STABLE',
+ },
+ balance: '500000',
+ },
+ ],
+ };
+ boostedMetaInfo = factories.boostedMetaPool
+ .transient({
+ rootId: 'id-parent',
+ rootAddress: 'address-parent',
+ rootBalance: '1000000',
+ childBoostedParams,
+ childLinearParam,
+ })
+ .build();
+
+ rootPool = boostedMetaInfo.rootInfo.pool;
+ const pools = [
+ ...boostedMetaInfo.childLinearInfo.linearPools,
+ boostedMetaInfo.childBoostedInfo.rootPool,
+ ...boostedMetaInfo.childBoostedInfo.linearPools,
+ rootPool,
+ ];
+ // // Create staticPools provider with above pools
+ const poolProvider = new PoolsStaticRepository(
+ pools as unknown as SdkPool[]
+ );
+ poolsGraph = new PoolGraph(poolProvider, {
+ network: Network.GOERLI,
+ rpcUrl: '',
+ });
+ });
+
+ context('using wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ rootPool.id,
+ true
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoostedMeta(boostedNode, boostedMetaInfo, true);
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(14);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('Input');
+ expect(orderedNodes[4].type).to.eq('WrappedToken');
+ expect(orderedNodes[5].type).to.eq('WrappedToken');
+ expect(orderedNodes[6].type).to.eq('WrappedToken');
+ expect(orderedNodes[7].type).to.eq('WrappedToken');
+ expect(orderedNodes[8].type).to.eq('AaveLinear');
+ expect(orderedNodes[9].type).to.eq('AaveLinear');
+ expect(orderedNodes[10].type).to.eq('AaveLinear');
+ expect(orderedNodes[11].type).to.eq('AaveLinear');
+ expect(orderedNodes[12].type).to.eq('ComposableStable');
+ expect(orderedNodes[13].type).to.eq('ComposableStable');
+ });
+ });
+
+ context('using non-wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ rootPool.id,
+ false
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoostedMeta(boostedNode, boostedMetaInfo, false);
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(10);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('Input');
+ expect(orderedNodes[4].type).to.eq('AaveLinear');
+ expect(orderedNodes[5].type).to.eq('AaveLinear');
+ expect(orderedNodes[6].type).to.eq('AaveLinear');
+ expect(orderedNodes[7].type).to.eq('AaveLinear');
+ expect(orderedNodes[8].type).to.eq('ComposableStable');
+ expect(orderedNodes[9].type).to.eq('ComposableStable');
+ });
+ });
+ });
+
+ context('boostedMetaBigPool', () => {
+ let boostedMetaBigInfo: BoostedMetaBigInfo;
+ let boostedPool: Pool;
+ let poolsGraph: PoolGraph;
+ let boostedNode: Node;
+
+ before(() => {
+ // The boostedMetaBig will have a phantomStable with two boosted.
+ // Note:
+ // first pool will be parent
+ // proportions are referenced to parent nodes
+ const child1LinearPools: LinearParams = {
+ pools: [
+ {
+ tokens: {
+ wrappedSymbol: 'aDAI',
+ mainSymbol: 'DAI',
+ },
+ balance: '1000000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDC',
+ mainSymbol: 'USDC',
+ },
+ balance: '500000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'aUSDT',
+ mainSymbol: 'USDT',
+ },
+ balance: '500000',
+ },
+ ],
+ };
+ const childBoosted1: BoostedParams = {
+ linearPoolsParams: child1LinearPools,
+ rootId: 'childBoosted1-id',
+ rootAddress: 'childBoosted1-address',
+ rootBalance: '1000000',
+ };
+ const child2LinearPools: LinearParams = {
+ pools: [
+ {
+ tokens: {
+ wrappedSymbol: 'cDAI',
+ mainSymbol: 'DAI',
+ },
+ balance: '4000000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'cUSDC',
+ mainSymbol: 'USDC',
+ },
+ balance: '4000000',
+ },
+ {
+ tokens: {
+ wrappedSymbol: 'cUSDT',
+ mainSymbol: 'USDT',
+ },
+ balance: '2000000',
+ },
+ ],
+ };
+ const childBoosted2: BoostedParams = {
+ linearPoolsParams: child2LinearPools,
+ rootId: 'childBoosted2-id',
+ rootAddress: 'childBoosted2-address',
+ rootBalance: '1000000',
+ };
+ const parentPool: BoostedMetaBigParams = {
+ rootId: 'parentBoosted-id',
+ rootAddress: 'parentBoosted-address',
+ rootBalance: '7777777',
+ childPools: [childBoosted1, childBoosted2],
+ };
+
+ boostedMetaBigInfo = factories.boostedMetaBigPool
+ .transient(parentPool)
+ .build();
+ boostedPool = boostedMetaBigInfo.rootPool;
+ const pools = [
+ ...boostedMetaBigInfo.childPools,
+ boostedMetaBigInfo.rootPool,
+ ];
+ // // Create staticPools provider with above pools
+ const poolProvider = new PoolsStaticRepository(
+ pools as unknown as SdkPool[]
+ );
+ poolsGraph = new PoolGraph(poolProvider, {
+ network: Network.GOERLI,
+ rpcUrl: '',
+ });
+ });
+
+ context('using wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ boostedPool.id,
+ true
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoostedMetaBig(boostedNode, boostedMetaBigInfo, true);
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(21);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('Input');
+ expect(orderedNodes[4].type).to.eq('Input');
+ expect(orderedNodes[5].type).to.eq('Input');
+ expect(orderedNodes[6].type).to.eq('WrappedToken');
+ expect(orderedNodes[7].type).to.eq('WrappedToken');
+ expect(orderedNodes[8].type).to.eq('WrappedToken');
+ expect(orderedNodes[9].type).to.eq('WrappedToken');
+ expect(orderedNodes[10].type).to.eq('WrappedToken');
+ expect(orderedNodes[11].type).to.eq('WrappedToken');
+ expect(orderedNodes[12].type).to.eq('AaveLinear');
+ expect(orderedNodes[13].type).to.eq('AaveLinear');
+ expect(orderedNodes[14].type).to.eq('AaveLinear');
+ expect(orderedNodes[15].type).to.eq('AaveLinear');
+ expect(orderedNodes[16].type).to.eq('AaveLinear');
+ expect(orderedNodes[17].type).to.eq('AaveLinear');
+ expect(orderedNodes[18].type).to.eq('ComposableStable');
+ expect(orderedNodes[19].type).to.eq('ComposableStable');
+ expect(orderedNodes[20].type).to.eq('ComposableStable');
+ });
+ });
+
+ context('using non-wrapped tokens', () => {
+ before(async () => {
+ boostedNode = await poolsGraph.buildGraphFromRootPool(
+ boostedPool.id,
+ false
+ );
+ });
+
+ it('should build boostedPool graph', async () => {
+ checkBoostedMetaBig(boostedNode, boostedMetaBigInfo, false);
+ });
+
+ it('should sort in breadth first order', async () => {
+ const orderedNodes = PoolGraph.orderByBfs(boostedNode).reverse();
+ expect(orderedNodes.length).to.eq(15);
+ expect(orderedNodes[0].type).to.eq('Input');
+ expect(orderedNodes[1].type).to.eq('Input');
+ expect(orderedNodes[2].type).to.eq('Input');
+ expect(orderedNodes[3].type).to.eq('Input');
+ expect(orderedNodes[4].type).to.eq('Input');
+ expect(orderedNodes[5].type).to.eq('Input');
+ expect(orderedNodes[6].type).to.eq('AaveLinear');
+ expect(orderedNodes[7].type).to.eq('AaveLinear');
+ expect(orderedNodes[8].type).to.eq('AaveLinear');
+ expect(orderedNodes[9].type).to.eq('AaveLinear');
+ expect(orderedNodes[10].type).to.eq('AaveLinear');
+ expect(orderedNodes[11].type).to.eq('AaveLinear');
+ expect(orderedNodes[12].type).to.eq('ComposableStable');
+ expect(orderedNodes[13].type).to.eq('ComposableStable');
+ expect(orderedNodes[14].type).to.eq('ComposableStable');
+ });
+ });
+ });
+});
diff --git a/balancer-js/src/modules/graph/graph.ts b/balancer-js/src/modules/graph/graph.ts
new file mode 100644
index 000000000..c2bc03cbc
--- /dev/null
+++ b/balancer-js/src/modules/graph/graph.ts
@@ -0,0 +1,381 @@
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { isSameAddress, parsePoolInfo } from '@/lib/utils';
+import { BalancerSdkConfig, Pool, PoolAttribute, PoolType } from '@/types';
+import { Zero, WeiPerEther } from '@ethersproject/constants';
+import { BigNumber, parseFixed } from '@ethersproject/bignumber';
+import { Findable } from '../data/types';
+import { Pools } from '../pools';
+import { getNetworkConfig } from '../sdk.helpers';
+
+type SpotPrices = { [tokenIn: string]: string };
+export interface Node {
+ address: string;
+ id: string;
+ joinAction: JoinAction;
+ exitAction: ExitAction;
+ type: string;
+ children: Node[];
+ marked: boolean;
+ index: string;
+ proportionOfParent: BigNumber;
+ parent: Node | undefined;
+ isLeaf: boolean;
+ spotPrices: SpotPrices;
+ decimals: number;
+}
+
+type JoinAction =
+ | 'input'
+ | 'batchSwap'
+ | 'wrap'
+ | 'joinPool'
+ | 'wrapAaveDynamicToken'
+ | 'wrapERC4626';
+const joinActions = new Map();
+joinActions.set(PoolType.AaveLinear, 'batchSwap');
+joinActions.set(PoolType.ERC4626Linear, 'batchSwap');
+joinActions.set(PoolType.Element, 'batchSwap');
+joinActions.set(PoolType.Investment, 'joinPool');
+joinActions.set(PoolType.LiquidityBootstrapping, 'joinPool');
+joinActions.set(PoolType.MetaStable, 'joinPool');
+joinActions.set(PoolType.Stable, 'joinPool');
+joinActions.set(PoolType.StablePhantom, 'batchSwap');
+joinActions.set(PoolType.Weighted, 'joinPool');
+joinActions.set(PoolType.ComposableStable, 'joinPool');
+
+type ExitAction =
+ | 'output'
+ | 'batchSwap'
+ | 'unwrap'
+ | 'exitPool'
+ | 'unwrapAaveStaticToken'
+ | 'unwrapERC4626';
+const exitActions = new Map();
+exitActions.set(PoolType.AaveLinear, 'batchSwap');
+exitActions.set(PoolType.ERC4626Linear, 'batchSwap');
+exitActions.set(PoolType.Element, 'batchSwap');
+exitActions.set(PoolType.Investment, 'exitPool');
+exitActions.set(PoolType.LiquidityBootstrapping, 'exitPool');
+exitActions.set(PoolType.MetaStable, 'exitPool');
+exitActions.set(PoolType.Stable, 'exitPool');
+exitActions.set(PoolType.StablePhantom, 'batchSwap');
+exitActions.set(PoolType.Weighted, 'exitPool');
+exitActions.set(PoolType.ComposableStable, 'exitPool');
+
+export class PoolGraph {
+ constructor(
+ private pools: Findable,
+ private sdkConfig: BalancerSdkConfig
+ ) {}
+
+ async buildGraphFromRootPool(
+ poolId: string,
+ wrapMainTokens: boolean
+ ): Promise {
+ const rootPool = await this.pools.find(poolId);
+ if (!rootPool) throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST);
+ const nodeIndex = 0;
+ const rootNode = await this.buildGraphFromPool(
+ rootPool.address,
+ nodeIndex,
+ undefined,
+ WeiPerEther,
+ wrapMainTokens
+ );
+ return rootNode[0];
+ }
+
+ getTokenTotal(pool: Pool): BigNumber {
+ const bptIndex = pool.tokensList.indexOf(pool.address);
+ let total = Zero;
+ const { parsedBalances } = parsePoolInfo(pool);
+ parsedBalances.forEach((balance, i) => {
+ // Ignore phantomBpt balance
+ if (bptIndex !== i) {
+ total = total.add(balance);
+ }
+ });
+ return total;
+ }
+
+ async buildGraphFromPool(
+ address: string,
+ nodeIndex: number,
+ parent: Node | undefined,
+ proportionOfParent: BigNumber,
+ wrapMainTokens: boolean
+ ): Promise<[Node, number]> {
+ const pool = await this.pools.findBy('address', address);
+
+ if (!pool) {
+ if (!parent) {
+ // If pool not found by address and is root pool (without parent), then throw error
+ throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST);
+ } else {
+ // If pool not found by address, but it has parent, assume it's a leaf token and add a leafTokenNode
+ // TODO: maybe it's a safety issue? Can we be safer?
+ const parentPool = (await this.pools.findBy(
+ 'address',
+ parent.address
+ )) as Pool;
+ const leafTokenDecimals =
+ parentPool.tokens[parentPool.tokensList.indexOf(address)].decimals ??
+ 18;
+
+ const nodeInfo = PoolGraph.createInputTokenNode(
+ nodeIndex,
+ address,
+ leafTokenDecimals,
+ parent,
+ proportionOfParent
+ );
+ return nodeInfo;
+ }
+ }
+
+ const joinAction = joinActions.get(pool.poolType);
+ const exitAction = exitActions.get(pool.poolType);
+ if (!joinAction || !exitAction)
+ throw new BalancerError(BalancerErrorCode.UNSUPPORTED_POOL_TYPE);
+
+ const tokenTotal = this.getTokenTotal(pool);
+ const network = getNetworkConfig(this.sdkConfig);
+ const controller = Pools.wrap(pool, network);
+ const spotPrices: SpotPrices = {};
+ let decimals = 18;
+ // Spot price of a path is product of the sp of each pool in path. We calculate the sp for each pool token here to use as required later.
+ pool.tokens.forEach((token) => {
+ if (isSameAddress(token.address, pool.address)) {
+ // Updated node with BPT token decimal
+ decimals = token.decimals ? token.decimals : 18;
+ return;
+ }
+ const sp = controller.calcSpotPrice(token.address, pool.address, true);
+ spotPrices[token.address] = sp;
+ });
+
+ let poolNode: Node = {
+ address: pool.address,
+ id: pool.id,
+ type: pool.poolType,
+ joinAction,
+ exitAction,
+ children: [],
+ marked: false,
+ index: nodeIndex.toString(),
+ parent,
+ proportionOfParent,
+ isLeaf: false,
+ spotPrices,
+ decimals,
+ };
+ nodeIndex++;
+ if (pool.poolType.toString().includes('Linear')) {
+ [poolNode, nodeIndex] = this.createLinearNodeChildren(
+ poolNode,
+ nodeIndex,
+ pool,
+ wrapMainTokens
+ );
+ } else {
+ const { parsedBalances } = parsePoolInfo(pool);
+ for (let i = 0; i < pool.tokens.length; i++) {
+ // ignore any phantomBpt tokens
+ if (isSameAddress(pool.tokens[i].address, pool.address)) continue;
+ let proportion: BigNumber;
+ // If the pool is a weighted pool we can use the actual tokenWeight as proportion
+ if (pool.poolType === 'Weighted') {
+ const tokenWeight = pool.tokens[i].weight as string;
+ proportion = parseFixed(tokenWeight, 18);
+ } else {
+ proportion = BigNumber.from(parsedBalances[i])
+ .mul((1e18).toString())
+ .div(tokenTotal);
+ }
+ const finalProportion = proportion
+ .mul(proportionOfParent)
+ .div((1e18).toString());
+ const childNode = await this.buildGraphFromPool(
+ pool.tokens[i].address,
+ nodeIndex,
+ poolNode,
+ finalProportion,
+ wrapMainTokens
+ );
+ nodeIndex = childNode[1];
+ if (childNode[0]) poolNode.children.push(childNode[0]);
+ }
+ }
+ return [poolNode, nodeIndex];
+ }
+
+ createLinearNodeChildren(
+ linearPoolNode: Node,
+ nodeIndex: number,
+ linearPool: Pool,
+ wrapMainTokens: boolean
+ ): [Node, number] {
+ if (wrapMainTokens) {
+ // Linear pool will be joined via wrapped token. This will be the child node.
+ const wrappedNodeInfo = this.createWrappedTokenNode(
+ linearPool,
+ nodeIndex,
+ linearPoolNode,
+ linearPoolNode.proportionOfParent
+ );
+ linearPoolNode.children.push(wrappedNodeInfo[0]);
+ return [linearPoolNode, wrappedNodeInfo[1]];
+ } else {
+ // Main token
+ if (linearPool.mainIndex === undefined)
+ throw new Error('Issue With Linear Pool');
+
+ const mainTokenDecimals =
+ linearPool.tokens[linearPool.mainIndex].decimals ?? 18;
+
+ const nodeInfo = PoolGraph.createInputTokenNode(
+ nodeIndex,
+ linearPool.tokensList[linearPool.mainIndex],
+ mainTokenDecimals,
+ linearPoolNode,
+ linearPoolNode.proportionOfParent
+ );
+ linearPoolNode.children.push(nodeInfo[0]);
+ nodeIndex = nodeInfo[1];
+ return [linearPoolNode, nodeIndex];
+ }
+ }
+
+ createWrappedTokenNode(
+ linearPool: Pool,
+ nodeIndex: number,
+ parent: Node | undefined,
+ proportionOfParent: BigNumber
+ ): [Node, number] {
+ if (
+ linearPool.wrappedIndex === undefined ||
+ linearPool.mainIndex === undefined
+ )
+ throw new Error('Issue With Linear Pool');
+
+ // Relayer can support different wrapped tokens
+ let joinAction: JoinAction = 'wrapAaveDynamicToken';
+ switch (linearPool.poolType) {
+ case PoolType.ERC4626Linear:
+ joinAction = 'wrapERC4626';
+ }
+ let exitAction: ExitAction = 'unwrapAaveStaticToken';
+ switch (linearPool.poolType) {
+ case PoolType.ERC4626Linear:
+ exitAction = 'unwrapERC4626';
+ }
+
+ const wrappedTokenNode: Node = {
+ type: 'WrappedToken',
+ address: linearPool.tokensList[linearPool.wrappedIndex],
+ id: 'N/A',
+ children: [],
+ marked: false,
+ joinAction,
+ exitAction,
+ index: nodeIndex.toString(),
+ parent,
+ proportionOfParent,
+ isLeaf: false,
+ spotPrices: {},
+ decimals: 18,
+ };
+ nodeIndex++;
+
+ const mainTokenDecimals =
+ linearPool.tokens[linearPool.mainIndex].decimals ?? 18;
+
+ const inputNode = PoolGraph.createInputTokenNode(
+ nodeIndex,
+ linearPool.tokensList[linearPool.mainIndex],
+ mainTokenDecimals,
+ wrappedTokenNode,
+ proportionOfParent
+ );
+ wrappedTokenNode.children = [inputNode[0]];
+ nodeIndex = inputNode[1];
+ return [wrappedTokenNode, nodeIndex];
+ }
+
+ static createInputTokenNode(
+ nodeIndex: number,
+ address: string,
+ decimals: number,
+ parent: Node | undefined,
+ proportionOfParent: BigNumber
+ ): [Node, number] {
+ return [
+ {
+ address,
+ id: 'N/A',
+ type: 'Input',
+ children: [],
+ marked: false,
+ joinAction: 'input',
+ exitAction: 'output',
+ index: nodeIndex.toString(), // This will be updated with real amounts in join construction.
+ parent,
+ proportionOfParent,
+ isLeaf: true,
+ spotPrices: {},
+ decimals,
+ },
+ nodeIndex + 1,
+ ];
+ }
+
+ static orderByBfs(root: Node): Node[] {
+ // Breadth first traversal of graph
+ const nodes: Node[] = [];
+ const orderedNodes: Node[] = [];
+ root.marked = true;
+ nodes.push(root);
+ while (nodes.length > 0) {
+ const currentNode = nodes.shift(); // removes first
+ if (currentNode) orderedNodes.push(currentNode);
+ currentNode?.children.forEach((c) => {
+ if (!c.marked) {
+ c.marked = true;
+ nodes.push(c);
+ }
+ });
+ }
+ return orderedNodes;
+ }
+
+ // Return a list of leaf token addresses
+ static getLeafAddresses(nodes: Node[]): string[] {
+ return nodes.filter((n) => n.isLeaf).map((n) => n.address);
+ }
+
+ // Get full graph from root pool and return ordered nodes
+ static getGraphNodes = async (
+ isJoin: boolean,
+ chainId: number,
+ poolId: string,
+ pools: Findable,
+ wrapMainTokens: boolean
+ ): Promise => {
+ const rootPool = await pools.find(poolId);
+ if (!rootPool) throw new BalancerError(BalancerErrorCode.POOL_DOESNT_EXIST);
+ const poolsGraph = new PoolGraph(pools, {
+ network: chainId,
+ rpcUrl: '',
+ });
+
+ const rootNode = await poolsGraph.buildGraphFromRootPool(
+ poolId,
+ wrapMainTokens
+ );
+
+ if (rootNode.id !== poolId) throw new Error('Error creating graph nodes');
+
+ if (isJoin) return PoolGraph.orderByBfs(rootNode).reverse();
+ else return PoolGraph.orderByBfs(rootNode);
+ };
+}
diff --git a/balancer-js/src/modules/joins/joins.module.integration.spec.ts b/balancer-js/src/modules/joins/joins.module.integration.spec.ts
new file mode 100644
index 000000000..cfca3b54c
--- /dev/null
+++ b/balancer-js/src/modules/joins/joins.module.integration.spec.ts
@@ -0,0 +1,1230 @@
+// yarn test:only ./src/modules/joins/joins.module.integration.spec.ts
+import dotenv from 'dotenv';
+import { expect } from 'chai';
+import hardhat from 'hardhat';
+
+import { BalancerSDK, BalancerTenderlyConfig, Network } from '@/.';
+import { BigNumber, parseFixed } from '@ethersproject/bignumber';
+import { Contracts } from '@/modules/contracts/contracts.module';
+import { forkSetup, getBalances } from '@/test/lib/utils';
+import { ADDRESSES } from '@/test/lib/constants';
+import { Relayer } from '@/modules/relayer/relayer.module';
+import { JsonRpcSigner } from '@ethersproject/providers';
+
+dotenv.config();
+
+const TEST_BOOSTED = true;
+const TEST_BOOSTED_META = true;
+const TEST_BOOSTED_META_ALT = true;
+const TEST_BOOSTED_META_BIG = true;
+const TEST_BOOSTED_WEIGHTED_SIMPLE = true;
+const TEST_BOOSTED_WEIGHTED_GENERAL = true;
+const TEST_BOOSTED_WEIGHTED_META = true;
+const TEST_BOOSTED_WEIGHTED_META_ALT = true;
+const TEST_BOOSTED_WEIGHTED_META_GENERAL = true;
+
+/*
+ * Testing on GOERLI
+ * - Update hardhat.config.js with chainId = 5
+ * - Update ALCHEMY_URL on .env with a goerli api key
+ * - Run node on terminal: yarn run node
+ * - Uncomment section below:
+ */
+const network = Network.GOERLI;
+const blockNumber = 8038074;
+const customSubgraphUrl =
+ 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-goerli-v2-beta';
+const { ALCHEMY_URL_GOERLI: jsonRpcUrl } = process.env;
+const rpcUrl = 'http://127.0.0.1:8000';
+
+/*
+ * Testing on MAINNET
+ * - Update hardhat.config.js with chainId = 1
+ * - Update ALCHEMY_URL on .env with a mainnet api key
+ * - Run node on terminal: yarn run node
+ * - Uncomment section below:
+ */
+// const network = Network.MAINNET;
+// const blockNumber = 15519886;
+// const customSubgraphUrl =
+// 'https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-v2-beta';
+// const { ALCHEMY_URL: jsonRpcUrl } = process.env;
+// const rpcUrl = 'http://127.0.0.1:8545';
+
+const { TENDERLY_ACCESS_KEY, TENDERLY_USER, TENDERLY_PROJECT } = process.env;
+const { ethers } = hardhat;
+const MAX_GAS_LIMIT = 8e6;
+
+// Custom Tenderly configuration parameters - remove in order to use default values
+const tenderlyConfig: BalancerTenderlyConfig = {
+ accessKey: TENDERLY_ACCESS_KEY as string,
+ user: TENDERLY_USER as string,
+ project: TENDERLY_PROJECT as string,
+ blockNumber,
+};
+
+const sdk = new BalancerSDK({
+ network,
+ rpcUrl,
+ customSubgraphUrl,
+ tenderly: tenderlyConfig,
+});
+const { pools } = sdk;
+const provider = new ethers.providers.JsonRpcProvider(rpcUrl, network);
+const signer = provider.getSigner();
+const { contracts, contractAddresses } = new Contracts(
+ network as number,
+ provider
+);
+const relayer = contractAddresses.relayerV4 as string;
+const addresses = ADDRESSES[network];
+
+interface Test {
+ signer: JsonRpcSigner;
+ description: string;
+ pool: {
+ id: string;
+ address: string;
+ };
+ tokensIn: string[];
+ amountsIn: string[];
+ authorisation: string | undefined;
+ wrapMainTokens: boolean;
+}
+
+const runTests = async (tests: Test[]) => {
+ for (let i = 0; i < tests.length; i++) {
+ const test = tests[i];
+ it(test.description, async () => {
+ const userAddress = await test.signer.getAddress();
+ const authorisation = await Relayer.signRelayerApproval(
+ relayer,
+ userAddress,
+ signer,
+ contracts.vault
+ );
+ await testFlow(
+ userAddress,
+ test.pool,
+ test.tokensIn,
+ test.amountsIn,
+ test.wrapMainTokens,
+ authorisation
+ );
+ }).timeout(120000);
+ }
+};
+
+const testFlow = async (
+ userAddress: string,
+ pool: { id: string; address: string },
+ tokensIn: string[],
+ amountsIn: string[],
+ wrapMainTokens: boolean,
+ authorisation: string | undefined
+) => {
+ const [bptBalanceBefore, ...tokensInBalanceBefore] = await getBalances(
+ [pool.address, ...tokensIn],
+ signer,
+ userAddress
+ );
+
+ const gasLimit = MAX_GAS_LIMIT;
+ const slippage = '10'; // 10 bps = 0.1%
+
+ const query = await pools.generalisedJoin(
+ pool.id,
+ tokensIn,
+ amountsIn,
+ userAddress,
+ wrapMainTokens,
+ slippage,
+ authorisation
+ );
+
+ const response = await signer.sendTransaction({
+ to: query.to,
+ data: query.callData,
+ gasLimit,
+ });
+
+ const receipt = await response.wait();
+ console.log('Gas used', receipt.gasUsed.toString());
+
+ const [bptBalanceAfter, ...tokensInBalanceAfter] = await getBalances(
+ [pool.address, ...tokensIn],
+ signer,
+ userAddress
+ );
+ expect(receipt.status).to.eql(1);
+ expect(BigNumber.from(query.minOut).gte('0')).to.be.true;
+ expect(BigNumber.from(query.expectedOut).gt(query.minOut)).to.be.true;
+ tokensInBalanceAfter.forEach((balanceAfter, i) => {
+ expect(balanceAfter.toString()).to.eq(
+ tokensInBalanceBefore[i].sub(amountsIn[i]).toString()
+ );
+ });
+ expect(bptBalanceBefore.eq(0)).to.be.true;
+ expect(bptBalanceAfter.gte(query.minOut)).to.be.true;
+ console.log(bptBalanceAfter.toString(), 'bpt after');
+ console.log(query.minOut, 'minOut');
+ console.log(query.expectedOut, 'expectedOut');
+};
+
+// following contexts currently applies to GOERLI only
+describe('generalised join execution', async () => {
+ /*
+ bbamaiweth: ComposableStable, baMai/baWeth
+ baMai: Linear, aMai/Mai
+ baWeth: Linear, aWeth/Weth
+ */
+ context('boosted', async () => {
+ if (!TEST_BOOSTED) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.MAI.address,
+ addresses.WETH.address,
+ addresses.waMAI.address,
+ addresses.waWETH.address,
+ addresses.bbamai.address,
+ addresses.bbaweth.address,
+ ];
+ const slots = [
+ addresses.MAI.slot,
+ addresses.WETH.slot,
+ addresses.waMAI.slot,
+ addresses.waWETH.slot,
+ addresses.bbamai.slot,
+ addresses.bbaweth.slot,
+ ];
+ const balances = [
+ parseFixed('100', 18).toString(),
+ parseFixed('100', 18).toString(),
+ '0',
+ '0',
+ parseFixed('100', addresses.bbamai.decimals).toString(),
+ parseFixed('100', addresses.bbaweth.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.bbamaiweth.id,
+ address: addresses.bbamaiweth.address,
+ },
+ tokensIn: [addresses.MAI.address, addresses.WETH.address],
+ amountsIn: [
+ parseFixed('100', 18).toString(),
+ parseFixed('100', 18).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with 1 linear',
+ // pool: {
+ // id: addresses.bbamaiweth.id,
+ // address: addresses.bbamaiweth.address,
+ // },
+ // tokensIn: [addresses.bbamai.address],
+ // amountsIn: [parseFixed('10', 18).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with 1 leaf and 1 linear',
+ pool: {
+ id: addresses.bbamaiweth.id,
+ address: addresses.bbamaiweth.address,
+ },
+ tokensIn: [addresses.WETH.address, addresses.bbamai.address],
+ amountsIn: [
+ parseFixed('10', 18).toString(),
+ parseFixed('10', 18).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedMeta1: ComposableStable, baMai/bbausd2
+ baMai: Linear, aMai/Mai
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMeta', async () => {
+ if (!TEST_BOOSTED_META) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ addresses.waDAI.address,
+ addresses.waUSDC.address,
+ addresses.waUSDT.address,
+ addresses.waMAI.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbamai.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.MAI.slot,
+ addresses.waDAI.slot,
+ addresses.waUSDC.slot,
+ addresses.waUSDT.slot,
+ addresses.waMAI.slot,
+ addresses.bbadai.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbamai.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ '0',
+ '0',
+ '0',
+ '0',
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbamai.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedMeta1.id,
+ address: addresses.boostedMeta1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('0', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedMeta1.id,
+ // address: addresses.boostedMeta1.address,
+ // },
+ // tokensIn: [addresses.bbamai.address],
+ // amountsIn: [parseFixed('10', addresses.bbamai.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with some leafs, linears and boosted',
+ pool: {
+ id: addresses.boostedMeta1.id,
+ address: addresses.boostedMeta1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.bbamai.address,
+ addresses.bbausdt.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('0', addresses.bbamai.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedMetaAlt1: ComposableStable, Mai/bbausd2
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMetaAlt', async () => {
+ if (!TEST_BOOSTED_META_ALT) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ addresses.waDAI.address,
+ addresses.waUSDC.address,
+ addresses.waUSDT.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbadai.address,
+ addresses.bbamai.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.MAI.slot,
+ addresses.waDAI.slot,
+ addresses.waUSDC.slot,
+ addresses.waUSDT.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbadai.slot,
+ addresses.bbamai.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ '0',
+ '0',
+ '0',
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbamai.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedMetaAlt1.id,
+ address: addresses.boostedMetaAlt1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with single leaf token',
+ // pool: {
+ // id: addresses.boostedMetaAlt1.id,
+ // address: addresses.boostedMetaAlt1.address,
+ // },
+ // tokensIn: [addresses.MAI.address],
+ // amountsIn: [parseFixed('10', addresses.MAI.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedMetaAlt1.id,
+ // address: addresses.boostedMetaAlt1.address,
+ // },
+ // tokensIn: [addresses.bbausdc.address],
+ // amountsIn: [parseFixed('3', addresses.bbausdc.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ // {
+ // signer,
+ // description: 'join with child boosted',
+ // pool: {
+ // id: addresses.boostedMetaAlt1.id,
+ // address: addresses.boostedMetaAlt1.address,
+ // },
+ // tokensIn: [addresses.bbausd2.address],
+ // amountsIn: [parseFixed('10', addresses.bbausd2.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with some leafs, linears and boosted',
+ pool: {
+ id: addresses.boostedMetaAlt1.id,
+ address: addresses.boostedMetaAlt1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDT.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('4', addresses.DAI.decimals).toString(),
+ parseFixed('0', addresses.USDT.decimals).toString(),
+ parseFixed('4', addresses.bbadai.decimals).toString(),
+ parseFixed('4', addresses.bbausdc.decimals).toString(),
+ parseFixed('4', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedMetaBig1: ComposableStable, bbamaiweth/bbausd2
+ bbamaiweth: ComposableStable, baMai/baWeth
+ baMai: Linear, aMai/Mai
+ baWeth: Linear, aWeth/Weth
+ bbausd2 (boosted): ComposableStable, baUsdt/baDai/baUsdc
+ */
+ context('boostedMetaBig', async () => {
+ if (!TEST_BOOSTED_META_BIG) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ addresses.WETH.address,
+ addresses.waDAI.address,
+ addresses.waUSDC.address,
+ addresses.waUSDT.address,
+ addresses.waMAI.address,
+ addresses.waWETH.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbamai.address,
+ addresses.bbamaiweth.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.MAI.slot,
+ addresses.WETH.slot,
+ addresses.waDAI.slot,
+ addresses.waUSDC.slot,
+ addresses.waUSDT.slot,
+ addresses.waMAI.slot,
+ addresses.waWETH.slot,
+ addresses.bbadai.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbamai.slot,
+ addresses.bbamaiweth.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ '0',
+ '0',
+ '0',
+ '0',
+ '0',
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbamai.decimals).toString(),
+ parseFixed('10', addresses.bbamaiweth.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedMetaBig1.id,
+ address: addresses.boostedMetaBig1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.MAI.address,
+ addresses.WETH.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.USDC.decimals).toString(),
+ parseFixed('1', addresses.USDT.decimals).toString(),
+ parseFixed('1', addresses.MAI.decimals).toString(),
+ parseFixed('1', addresses.WETH.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child boosted',
+ // pool: {
+ // id: addresses.boostedMetaBig1.id,
+ // address: addresses.boostedMetaBig1.address,
+ // },
+ // tokensIn: [addresses.bbamaiweth.address],
+ // amountsIn: [parseFixed('10', addresses.bbamaiweth.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with leaf and child boosted',
+ pool: {
+ id: addresses.boostedMetaBig1.id,
+ address: addresses.boostedMetaBig1.address,
+ },
+ tokensIn: [addresses.DAI.address, addresses.bbamaiweth.address],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.bbamaiweth.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ {
+ signer,
+ description: 'join with some leafs, linears and boosted',
+ pool: {
+ id: addresses.boostedMetaBig1.id,
+ address: addresses.boostedMetaBig1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ addresses.bbausdt.address,
+ addresses.bbamai.address,
+ addresses.bbamaiweth.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('0', addresses.USDC.decimals).toString(),
+ parseFixed('1', addresses.USDT.decimals).toString(),
+ parseFixed('1', addresses.WETH.decimals).toString(),
+ parseFixed('1', addresses.bbausdt.decimals).toString(),
+ parseFixed('1', addresses.bbamai.decimals).toString(),
+ parseFixed('1', addresses.bbamaiweth.decimals).toString(),
+ parseFixed('1', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedSimple1: 1 Linear + 1 normal token
+ b-a-weth: Linear, aWeth/Weth
+ BAL
+ */
+ context('boostedWeightedSimple', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_SIMPLE) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.BAL.address,
+ addresses.WETH.address,
+ addresses.waWETH.address,
+ addresses.bbaweth.address,
+ ];
+ const slots = [
+ addresses.BAL.slot,
+ addresses.WETH.slot,
+ addresses.waWETH.slot,
+ addresses.bbaweth.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.BAL.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ '0',
+ parseFixed('10', addresses.bbaweth.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedWeightedSimple1.id,
+ address: addresses.boostedWeightedSimple1.address,
+ },
+ tokensIn: [addresses.BAL.address, addresses.WETH.address],
+ amountsIn: [
+ parseFixed('10', addresses.BAL.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedWeightedSimple1.id,
+ // address: addresses.boostedWeightedSimple1.address,
+ // },
+ // tokensIn: [addresses.bbaweth.address],
+ // amountsIn: [parseFixed('10', addresses.bbaweth.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with leaf and child linear',
+ pool: {
+ id: addresses.boostedWeightedSimple1.id,
+ address: addresses.boostedWeightedSimple1.address,
+ },
+ tokensIn: [addresses.BAL.address, addresses.bbaweth.address],
+ amountsIn: [
+ parseFixed('1', addresses.BAL.decimals).toString(),
+ parseFixed('1', addresses.bbaweth.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedGeneral1: N Linear + M normal tokens
+ b-a-dai: Linear, aDai/Dai
+ b-a-mai: Linear, aMai/Mai
+ BAL
+ USDC
+ */
+ context('boostedWeightedGeneral', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_GENERAL) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.MAI.address,
+ addresses.BAL.address,
+ addresses.USDC_old.address,
+ addresses.bbadai.address,
+ addresses.bbamai.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.MAI.slot,
+ addresses.BAL.slot,
+ addresses.USDC_old.slot,
+ addresses.bbadai.slot,
+ addresses.bbamai.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ parseFixed('10', addresses.BAL.decimals).toString(),
+ parseFixed('10', addresses.USDC_old.decimals).toString(),
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbamai.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedWeightedGeneral1.id,
+ address: addresses.boostedWeightedGeneral1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.MAI.address,
+ addresses.BAL.address,
+ addresses.USDC_old.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.MAI.decimals).toString(),
+ parseFixed('1', addresses.BAL.decimals).toString(),
+ parseFixed('1', addresses.USDC_old.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedWeightedGeneral1.id,
+ // address: addresses.boostedWeightedGeneral1.address,
+ // },
+ // tokensIn: [addresses.bbadai.address],
+ // amountsIn: [parseFixed('10', addresses.bbadai.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with some leafs and linear',
+ pool: {
+ id: addresses.boostedWeightedGeneral1.id,
+ address: addresses.boostedWeightedGeneral1.address,
+ },
+ tokensIn: [
+ addresses.MAI.address,
+ addresses.BAL.address,
+ addresses.bbamai.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.MAI.decimals).toString(),
+ parseFixed('10', addresses.BAL.decimals).toString(),
+ parseFixed('10', addresses.bbamai.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMeta1: 1 Linear + 1 ComposableStable
+ b-a-weth: Linear, aWeth/Weth
+ bb-a-usd2: ComposableStable, b-a-usdc/b-a-usdt/b-a-dai
+ BAL
+ */
+ context('boostedWeightedMeta', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbaweth.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.WETH.slot,
+ addresses.bbadai.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbaweth.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbaweth.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedWeightedMeta1.id,
+ address: addresses.boostedWeightedMeta1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedWeightedMeta1.id,
+ // address: addresses.boostedWeightedMeta1.address,
+ // },
+ // tokensIn: [addresses.bbaweth.address],
+ // amountsIn: [parseFixed('10', addresses.bbaweth.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with some leafs, linears and boosted',
+ pool: {
+ id: addresses.boostedWeightedMeta1.id,
+ address: addresses.boostedWeightedMeta1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.WETH.address,
+ addresses.bbausdt.address,
+ addresses.bbaweth.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('0', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbaweth.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMetaAlt1: 1 normal token + 1 ComposableStable
+ WETH
+ b-a-usd2: ComposableStable, b-a-usdt/b-a-usdc/b-a-dai
+ */
+ context('boostedWeightedMetaAlt', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META_ALT) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.WETH.slot,
+ addresses.bbadai.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedWeightedMetaAlt1.id,
+ address: addresses.boostedWeightedMetaAlt1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.USDC.decimals).toString(),
+ parseFixed('1', addresses.USDT.decimals).toString(),
+ parseFixed('1', addresses.WETH.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedWeightedMetaAlt1.id,
+ // address: addresses.boostedWeightedMetaAlt1.address,
+ // },
+ // tokensIn: [addresses.bbausdt.address],
+ // amountsIn: [parseFixed('1', addresses.bbausdt.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with leaf and child linear',
+ pool: {
+ id: addresses.boostedWeightedMetaAlt1.id,
+ address: addresses.boostedWeightedMetaAlt1.address,
+ },
+ tokensIn: [
+ addresses.USDC.address,
+ addresses.WETH.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.USDC.decimals).toString(),
+ parseFixed('1', addresses.WETH.decimals).toString(),
+ parseFixed('1', addresses.bbadai.decimals).toString(),
+ parseFixed('1', addresses.bbausdc.decimals).toString(),
+ parseFixed('0', addresses.bbausdt.decimals).toString(),
+ parseFixed('1', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+
+ /*
+ boostedWeightedMetaGeneral1: N Linear + 1 ComposableStable
+ b-a-usdt: Linear, aUSDT/USDT
+ b-a-usdc: Linear, aUSDC/USDC
+ b-a-weth: Linear, aWeth/Weth
+ b-a-usd2: ComposableStable, b-a-usdt/b-a-usdc/b-a-dai
+ */
+ context('boostedWeightedMetaGeneral', async () => {
+ if (!TEST_BOOSTED_WEIGHTED_META_GENERAL) return true;
+ let authorisation: string | undefined;
+ beforeEach(async () => {
+ const tokens = [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ addresses.bbadai.address,
+ addresses.bbausdc.address,
+ addresses.bbausdt.address,
+ addresses.bbaweth.address,
+ addresses.bbausd2.address,
+ ];
+ const slots = [
+ addresses.DAI.slot,
+ addresses.USDC.slot,
+ addresses.USDT.slot,
+ addresses.WETH.slot,
+ addresses.bbadai.slot,
+ addresses.bbausdc.slot,
+ addresses.bbausdt.slot,
+ addresses.bbaweth.slot,
+ addresses.bbausd2.slot,
+ ];
+ const balances = [
+ parseFixed('10', addresses.DAI.decimals).toString(),
+ parseFixed('10', addresses.USDC.decimals).toString(),
+ parseFixed('10', addresses.USDT.decimals).toString(),
+ parseFixed('10', addresses.WETH.decimals).toString(),
+ parseFixed('10', addresses.bbadai.decimals).toString(),
+ parseFixed('10', addresses.bbausdc.decimals).toString(),
+ parseFixed('10', addresses.bbausdt.decimals).toString(),
+ parseFixed('10', addresses.bbaweth.decimals).toString(),
+ parseFixed('10', addresses.bbausd2.decimals).toString(),
+ ];
+ await forkSetup(
+ signer,
+ tokens,
+ slots,
+ balances,
+ jsonRpcUrl as string,
+ blockNumber
+ );
+ });
+
+ await runTests([
+ {
+ signer,
+ description: 'join with all leaf tokens',
+ pool: {
+ id: addresses.boostedWeightedMetaGeneral1.id,
+ address: addresses.boostedWeightedMetaGeneral1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.USDT.address,
+ addresses.WETH.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.USDC.decimals).toString(),
+ parseFixed('1', addresses.USDT.decimals).toString(),
+ parseFixed('1', addresses.WETH.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ // {
+ // signer,
+ // description: 'join with child linear',
+ // pool: {
+ // id: addresses.boostedWeightedMetaGeneral1.id,
+ // address: addresses.boostedWeightedMetaGeneral1.address,
+ // },
+ // tokensIn: [addresses.bbausdc.address],
+ // amountsIn: [parseFixed('10', addresses.bbausdc.decimals).toString()],
+ // authorisation: authorisation,
+ // wrapMainTokens: false,
+ // },
+ {
+ signer,
+ description: 'join with some leafs, linears and boosted',
+ pool: {
+ id: addresses.boostedWeightedMetaGeneral1.id,
+ address: addresses.boostedWeightedMetaGeneral1.address,
+ },
+ tokensIn: [
+ addresses.DAI.address,
+ addresses.USDC.address,
+ addresses.WETH.address,
+ addresses.bbadai.address,
+ addresses.bbaweth.address,
+ addresses.bbausd2.address,
+ ],
+ amountsIn: [
+ parseFixed('1', addresses.DAI.decimals).toString(),
+ parseFixed('1', addresses.USDC.decimals).toString(),
+ parseFixed('0', addresses.WETH.decimals).toString(),
+ parseFixed('1', addresses.bbadai.decimals).toString(),
+ parseFixed('1', addresses.bbaweth.decimals).toString(),
+ parseFixed('1', addresses.bbausd2.decimals).toString(),
+ ],
+ authorisation: authorisation,
+ wrapMainTokens: false,
+ },
+ ]);
+ });
+});
diff --git a/balancer-js/src/modules/joins/joins.module.ts b/balancer-js/src/modules/joins/joins.module.ts
new file mode 100644
index 000000000..da74ed427
--- /dev/null
+++ b/balancer-js/src/modules/joins/joins.module.ts
@@ -0,0 +1,947 @@
+import { defaultAbiCoder } from '@ethersproject/abi';
+import { cloneDeep } from 'lodash';
+import { Interface } from '@ethersproject/abi';
+import { BigNumber, parseFixed } from '@ethersproject/bignumber';
+import {
+ AddressZero,
+ MaxInt256,
+ WeiPerEther,
+ Zero,
+} from '@ethersproject/constants';
+
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { Relayer } from '@/modules/relayer/relayer.module';
+import { BatchSwapStep, FundManagement, SwapType } from '@/modules/swaps/types';
+import { StablePoolEncoder } from '@/pool-stable';
+import {
+ BalancerNetworkConfig,
+ JoinPoolRequest,
+ Pool,
+ PoolAttribute,
+ PoolType,
+} from '@/types';
+import { Findable } from '../data/types';
+import { PoolGraph, Node } from '../graph/graph';
+
+import { subSlippage } from '@/lib/utils/slippageHelper';
+import TenderlyHelper from '@/lib/utils/tenderlyHelper';
+import balancerRelayerAbi from '@/lib/abi/RelayerV4.json';
+import { networkAddresses } from '@/lib/constants/config';
+import { AssetHelpers, isSameAddress } from '@/lib/utils';
+import {
+ SolidityMaths,
+ _computeScalingFactor,
+ _upscale,
+} from '@/lib/utils/solidityMaths';
+import { calcPriceImpact } from '../pricing/priceImpact';
+import { WeightedPoolEncoder } from '@/pool-weighted';
+import { getPoolAddress } from '@/pool-utils';
+const balancerRelayerInterface = new Interface(balancerRelayerAbi);
+
+export class Join {
+ private relayer: string;
+ private wrappedNativeAsset;
+ private tenderlyHelper: TenderlyHelper;
+ constructor(
+ private pools: Findable,
+ private networkConfig: BalancerNetworkConfig
+ ) {
+ const { tokens, contracts } = networkAddresses(networkConfig.chainId);
+ this.relayer = contracts.relayerV4 as string;
+ this.wrappedNativeAsset = tokens.wrappedNativeAsset;
+
+ this.tenderlyHelper = new TenderlyHelper(
+ networkConfig.chainId,
+ networkConfig.tenderly
+ );
+ }
+
+ async joinPool(
+ poolId: string,
+ tokensIn: string[],
+ amountsIn: string[],
+ userAddress: string,
+ wrapMainTokens: boolean,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ expectedOut: string;
+ minOut: string;
+ priceImpact: string;
+ }> {
+ if (tokensIn.length != amountsIn.length)
+ throw new BalancerError(BalancerErrorCode.INPUT_LENGTH_MISMATCH);
+
+ // Create nodes for each pool/token interaction and order by breadth first
+ const orderedNodes = await PoolGraph.getGraphNodes(
+ true,
+ this.networkConfig.chainId,
+ poolId,
+ this.pools,
+ wrapMainTokens
+ );
+
+ const joinPaths = Join.getJoinPaths(orderedNodes, tokensIn, amountsIn);
+
+ const totalBptZeroPi = Join.totalBptZeroPriceImpact(joinPaths);
+ /*
+ - Create calls with 0 min bpt for each root join
+ - static call (or V4 special call) to get actual amounts for each root join
+ - Apply slippage to amounts
+ - Recreate calls with minAmounts === actualAmountsWithSlippage
+ - Return minAmoutOut (sum actualAmountsWithSlippage), UI would use this to display to user
+ - Return updatedCalls, UI would use this to execute tx
+ */
+ // Create calls with 0 expected for each root join
+ // Peek is enabled here so we can static call the returned amounts and use these to set limits
+ const { callData: queryData, outputIndexes } = await this.createCalls(
+ joinPaths,
+ userAddress,
+ undefined,
+ authorisation
+ );
+
+ // static call (or V4 special call) to get actual amounts for each root join
+ const { amountsOut, totalAmountOut } = await this.amountsOutByJoinPath(
+ userAddress,
+ queryData,
+ tokensIn,
+ outputIndexes
+ );
+
+ const { minAmountsOut, totalMinAmountOut } = this.minAmountsOutByJoinPath(
+ slippage,
+ amountsOut,
+ totalAmountOut
+ );
+ const priceImpact = calcPriceImpact(
+ BigInt(totalAmountOut),
+ totalBptZeroPi.toBigInt(),
+ true
+ ).toString();
+
+ // Create calls with minAmountsOut
+ const { callData, deltas } = await this.createCalls(
+ joinPaths,
+ userAddress,
+ minAmountsOut,
+ authorisation
+ );
+
+ this.assertDeltas(poolId, deltas, tokensIn, amountsIn, totalMinAmountOut);
+
+ return {
+ to: this.relayer,
+ callData,
+ expectedOut: totalAmountOut,
+ minOut: totalMinAmountOut,
+ priceImpact,
+ };
+ }
+
+ private assertDeltas(
+ poolId: string,
+ deltas: Record,
+ tokensIn: string[],
+ amountsIn: string[],
+ minBptOut: string
+ ): void {
+ const poolAddress = getPoolAddress(poolId);
+ const outDiff = deltas[poolAddress.toLowerCase()].add(minBptOut);
+
+ if (outDiff.abs().gt(3)) {
+ console.error(
+ `join assertDeltas, bptOut: `,
+ poolAddress,
+ minBptOut,
+ deltas[poolAddress.toLowerCase()]?.toString()
+ );
+ throw new BalancerError(BalancerErrorCode.JOIN_DELTA_AMOUNTS);
+ }
+ delete deltas[poolAddress.toLowerCase()];
+
+ tokensIn.forEach((token, i) => {
+ if (
+ !BigNumber.from(amountsIn[i]).eq(0) &&
+ deltas[token.toLowerCase()]?.toString() !== amountsIn[i]
+ ) {
+ console.error(
+ `join assertDeltas, tokenIn: `,
+ token,
+ amountsIn[i],
+ deltas[token.toLowerCase()]?.toString()
+ );
+ throw new BalancerError(BalancerErrorCode.JOIN_DELTA_AMOUNTS);
+ }
+ delete deltas[token.toLowerCase()];
+ });
+
+ for (const token in deltas) {
+ if (deltas[token].toString() !== '0') {
+ console.error(
+ `join assertDeltas, non-input token should be 0: `,
+ token,
+ deltas[token].toString()
+ );
+ throw new BalancerError(BalancerErrorCode.JOIN_DELTA_AMOUNTS);
+ }
+ }
+ }
+
+ // Create join paths from tokensIn all the way to the root node.
+ static getJoinPaths = (
+ orderedNodes: Node[],
+ tokensIn: string[],
+ amountsIn: string[]
+ ): Node[][] => {
+ const joinPaths: Node[][] = [];
+
+ // Filter all nodes that contain a token in the tokensIn array
+ const inputNodes = orderedNodes.filter((node) =>
+ tokensIn
+ .filter((t, i) => BigNumber.from(amountsIn[i]).gt(0)) // Remove input tokens with 0 amounts
+ .map((tokenIn) => tokenIn.toLowerCase())
+ .includes(node.address.toLowerCase())
+ );
+
+ // If inputNodes contain at least one leaf token, then add path to join proportionally with all leaf tokens contained in tokensIn
+ const containsLeafNode = inputNodes.some((node) => node.isLeaf);
+ if (containsLeafNode) {
+ joinPaths.push(orderedNodes);
+ }
+
+ // Add a join path for each non-leaf input node
+ const nonLeafInputNodes = inputNodes.filter((node) => !node.isLeaf);
+ nonLeafInputNodes.forEach((nonLeafInputNode) => {
+ // Get amount in for current node
+ const nonLeafAmountIn = amountsIn.find((amountIn, i) =>
+ isSameAddress(tokensIn[i], nonLeafInputNode.address)
+ ) as string;
+ // Split amount in between nodes with same non-leaf input token based on proportionOfParent
+ const totalProportions = nonLeafInputNodes
+ .filter((node) => isSameAddress(node.address, nonLeafInputNode.address))
+ .reduce(
+ (total, node) => total.add(node.proportionOfParent),
+ BigNumber.from(0)
+ );
+ const proportionalNonLeafAmountIn = BigNumber.from(nonLeafAmountIn)
+ .mul(nonLeafInputNode.proportionOfParent)
+ .div(totalProportions)
+ .toString();
+ // Create input node for current non-leaf input token
+ const [inputTokenNode] = PoolGraph.createInputTokenNode(
+ 0, // temp value that will be updated after creation
+ nonLeafInputNode.address,
+ nonLeafInputNode.decimals,
+ nonLeafInputNode.parent,
+ WeiPerEther
+ );
+ // Update index to be actual amount in
+ inputTokenNode.index = proportionalNonLeafAmountIn;
+ inputTokenNode.isLeaf = false;
+ // Start join path with input node
+ const nonLeafJoinPath = [inputTokenNode];
+ // Add each parent to the join path until we reach the root node
+ let parent = nonLeafInputNode.parent;
+ while (parent) {
+ nonLeafJoinPath.push(cloneDeep(parent));
+ parent = parent.parent;
+ }
+ // Add join path to list of join paths
+ joinPaths.push(nonLeafJoinPath);
+ });
+
+ // After creating all join paths, update the index of each input node to be the amount in for that node
+ // All other node indexes will be used as a reference to store the amounts out for that node
+ this.updateInputAmounts(joinPaths, tokensIn, amountsIn);
+
+ return joinPaths;
+ };
+
+ /*
+ AmountsIn should be adjusted after being split between tokensIn to fix eventual rounding issues.
+ This prevents the transaction to leave out dust amounts.
+ */
+ private static updateInputAmounts = (
+ joinPaths: Node[][],
+ tokensIn: string[],
+ amountsIn: string[]
+ ): void => {
+ // Helper function to calculate and adjust amount difference for each token in
+ const ajdustAmountInDiff = (
+ tokenInInputNodes: Node[],
+ amountIn: string
+ ): void => {
+ if (tokenInInputNodes.length > 1) {
+ // Sum of amountsIn from each input node with same tokenIn
+ const amountsInSumforTokenIn = tokenInInputNodes.reduce(
+ (sum, currentNode) => sum.add(currentNode.index),
+ BigNumber.from(0)
+ );
+ // Compare total amountIn with sum of amountIn split between each input node with same tokenIn
+ const diff = BigNumber.from(amountIn).sub(amountsInSumforTokenIn);
+ // Apply difference to first input node with same tokenIn
+ tokenInInputNodes[0].index = diff
+ .add(tokenInInputNodes[0].index)
+ .toString();
+ }
+ };
+
+ // Update amountsIn within leaf join path
+ const leafJoinPath = joinPaths.find((joinPath) => joinPath[0].isLeaf);
+ if (leafJoinPath) {
+ // Update input proportions so inputs are shared correctly between leaf nodes with same tokenIn
+ const totalProportions = this.updateTotalProportions(leafJoinPath);
+ // Update input nodes to have correct input amount
+ leafJoinPath.forEach((node) => {
+ if (node.joinAction === 'input')
+ node = this.updateNodeAmount(
+ node,
+ tokensIn,
+ amountsIn,
+ totalProportions
+ );
+ });
+ // Adjust amountIn for each tokenIn to fix eventual rounding issues
+ tokensIn.forEach((tokenIn, i) => {
+ const tokenInInputNodes = leafJoinPath.filter(
+ (inputNode) =>
+ inputNode.isLeaf && isSameAddress(inputNode.address, tokenIn)
+ );
+ ajdustAmountInDiff(tokenInInputNodes, amountsIn[i]);
+ });
+ }
+
+ // Adjust amountsIn shared between non-leaf join paths with same tokenIn
+ const nonLeafJoinPaths = joinPaths.filter(
+ (joinPath) => !joinPath[0].isLeaf
+ );
+ if (nonLeafJoinPaths.length > 1) {
+ tokensIn.forEach((tokenIn, i) => {
+ const tokenInInputNodes = nonLeafJoinPaths
+ .map((path) => path[0])
+ .filter((node) => isSameAddress(node.address, tokenIn));
+ ajdustAmountInDiff(tokenInInputNodes, amountsIn[i]);
+ });
+ }
+ };
+
+ createCalls = async (
+ joinPaths: Node[][],
+ userAddress: string,
+ minAmountsOut?: string[], // one for each joinPath
+ authorisation?: string
+ ): Promise<{
+ callData: string;
+ outputIndexes: number[];
+ deltas: Record;
+ }> => {
+ // Create calls for both leaf and non-leaf inputs
+ const { calls, outputIndexes, deltas } = this.createActionCalls(
+ joinPaths,
+ userAddress,
+ minAmountsOut
+ );
+
+ if (authorisation) {
+ calls.unshift(this.createSetRelayerApproval(authorisation));
+ }
+
+ const callData = balancerRelayerInterface.encodeFunctionData('multicall', [
+ calls,
+ ]);
+
+ return {
+ callData,
+ outputIndexes: authorisation
+ ? outputIndexes.map((i) => i + 1)
+ : outputIndexes,
+ deltas,
+ };
+ };
+
+ /*
+ 1. For each input token:
+ 1. recursively find the spot price for each pool in the path of the join
+ 2. take the product to get the spot price of the path
+ 3. multiply the input amount of that token by the path spot price to get the "zeroPriceImpact" amount of BPT for that token
+ 2. Sum each tokens zeroPriceImpact BPT amount to get total zeroPriceImpact BPT
+ */
+ static totalBptZeroPriceImpact = (joinPaths: Node[][]): BigNumber => {
+ // Add bptZeroPriceImpact for all inputs
+ let totalBptZeroPi = BigNumber.from('0');
+ joinPaths.forEach((joinPath) => {
+ const isLeafJoin = joinPath[0].isLeaf;
+ if (isLeafJoin) {
+ // Calculate bptZeroPriceImpact for leaf inputs
+ const leafNodes = joinPath.filter((node) => node.isLeaf);
+ leafNodes.forEach((leafNode) => {
+ const bptOut = this.bptOutZeroPiForInputNode(leafNode);
+ totalBptZeroPi = totalBptZeroPi.add(bptOut);
+ });
+ } else {
+ // Calculate bptZeroPriceImpact for non-leaf inputs
+ const bptOut = this.bptOutZeroPiForInputNode(joinPath[0]);
+ totalBptZeroPi = totalBptZeroPi.add(bptOut);
+ }
+ });
+ return totalBptZeroPi;
+ };
+
+ /*
+ 1. recursively find the spot price for each pool in the path of the join
+ 2. take the product to get the spot price of the path
+ 3. multiply the input amount of that token by the path spot price to get the "zeroPriceImpact" amount of BPT for that token
+ */
+ static bptOutZeroPiForInputNode = (inputNode: Node): bigint => {
+ if (inputNode.index === '0' || inputNode.joinAction !== 'input')
+ return BigInt(0);
+ let spProduct = 1;
+ let parentNode: Node | undefined = inputNode.parent;
+ let childAddress = inputNode.address;
+ // Traverse up graph until we reach root adding each node
+ while (parentNode !== undefined) {
+ if (
+ parentNode.joinAction === 'batchSwap' ||
+ parentNode.joinAction === 'joinPool'
+ ) {
+ const sp = parentNode.spotPrices[childAddress.toLowerCase()];
+ spProduct = spProduct * parseFloat(sp);
+ childAddress = parentNode.address;
+ }
+ parentNode = parentNode.parent;
+ }
+ const spPriceScaled = parseFixed(spProduct.toFixed(18), 18);
+ const scalingFactor = _computeScalingFactor(BigInt(inputNode.decimals));
+ const inputAmountScaled = _upscale(BigInt(inputNode.index), scalingFactor);
+ const bptOut = SolidityMaths.divDownFixed(
+ inputAmountScaled,
+ spPriceScaled.toBigInt()
+ );
+ return bptOut;
+ };
+
+ /*
+ Simulate transaction and decodes each output of interest.
+ */
+ amountsOutByJoinPath = async (
+ userAddress: string,
+ callData: string,
+ tokensIn: string[],
+ outputIndexes: number[]
+ ): Promise<{ amountsOut: string[]; totalAmountOut: string }> => {
+ const amountsOut: string[] = [];
+
+ const staticResult = await this.tenderlyHelper.simulateMulticall(
+ this.relayer,
+ callData,
+ userAddress,
+ tokensIn
+ );
+
+ const multicallResult = defaultAbiCoder.decode(
+ ['bytes[]'],
+ staticResult
+ )[0] as string[];
+
+ let totalAmountOut = BigNumber.from('0');
+ // Decode each root output
+ outputIndexes.forEach((outputIndex) => {
+ const value = defaultAbiCoder.decode(
+ ['uint256'],
+ multicallResult[outputIndex]
+ );
+ amountsOut.push(value.toString());
+ totalAmountOut = totalAmountOut.add(value.toString());
+ });
+
+ return {
+ amountsOut,
+ totalAmountOut: totalAmountOut.toString(),
+ };
+ };
+
+ /*
+ Apply slippage to amounts
+ */
+ minAmountsOutByJoinPath = (
+ slippage: string,
+ amounts: string[],
+ totalAmountOut: string
+ ): { minAmountsOut: string[]; totalMinAmountOut: string } => {
+ const minAmountsOut = amounts.map((amount) =>
+ subSlippage(BigNumber.from(amount), BigNumber.from(slippage)).toString()
+ );
+ const totalMinAmountOut = subSlippage(
+ BigNumber.from(totalAmountOut),
+ BigNumber.from(slippage)
+ ).toString();
+
+ return {
+ minAmountsOut,
+ totalMinAmountOut,
+ };
+ };
+
+ updateDeltas(
+ deltas: Record,
+ assets: string[],
+ amounts: string[]
+ ): Record {
+ assets.forEach((t, i) => {
+ const asset = t.toLowerCase();
+ if (!deltas[asset]) deltas[asset] = Zero;
+ deltas[asset] = deltas[asset].add(amounts[i]);
+ });
+ return deltas;
+ }
+
+ // Create actions for each Node and return in multicall array
+ // Create calls for each path, use value stored in minBptAmounts if available
+ createActionCalls = (
+ joinPaths: Node[][],
+ userAddress: string,
+ minAmountsOut?: string[]
+ ): {
+ calls: string[];
+ outputIndexes: number[];
+ deltas: Record;
+ } => {
+ const calls: string[] = [];
+ const outputIndexes: number[] = [];
+ const isPeek = !minAmountsOut;
+ const deltas: Record = {};
+
+ joinPaths.forEach((joinPath, j) => {
+ const isLeafJoin = joinPath[0].isLeaf;
+ joinPath.forEach((node, i) => {
+ let nodeChildrenWithinJoinPath;
+ if (isLeafJoin) {
+ nodeChildrenWithinJoinPath = joinPath.filter(
+ (joinNode) =>
+ node.children.map((n) => n.address).includes(joinNode.address) &&
+ node.index === joinNode.parent?.index // Ensure child nodes with same address are not included
+ );
+ } else {
+ nodeChildrenWithinJoinPath = i > 0 ? [joinPath[i - 1]] : [];
+ }
+
+ // Prevent adding action calls with input amounts equal 0
+ if (
+ nodeChildrenWithinJoinPath.length > 0 &&
+ nodeChildrenWithinJoinPath.filter((c) => c.index !== '0').length === 0
+ ) {
+ node.index = '0';
+ return;
+ }
+
+ // If child node was input the tokens come from user not relayer
+ // wrapped tokens have to come from user (Relayer has no approval for wrapped tokens)
+ const fromUser = nodeChildrenWithinJoinPath.some(
+ (child) =>
+ child.joinAction === 'input' ||
+ child.joinAction === 'wrapAaveDynamicToken'
+ );
+ const sender = fromUser ? userAddress : userAddress;
+
+ const isLastChainedCall = i === joinPath.length - 1;
+ // Always send to user on last call otherwise send to relayer
+ const recipient = isLastChainedCall ? userAddress : userAddress;
+ // Last action will use minBptOut to protect user. Middle calls can safely have 0 minimum as tx will revert if last fails.
+ const minOut =
+ isLastChainedCall && minAmountsOut ? minAmountsOut[j] : '0';
+
+ switch (node.joinAction) {
+ // TODO - Add other Relayer supported Unwraps
+ case 'wrapAaveDynamicToken':
+ // relayer has no allowance to spend its own wrapped tokens so recipient must be the user
+ calls.push(
+ this.createAaveWrap(
+ node,
+ nodeChildrenWithinJoinPath,
+ j,
+ sender,
+ userAddress
+ )
+ );
+ break;
+ case 'batchSwap': {
+ const [call, assets, limits] = this.createBatchSwap(
+ node,
+ nodeChildrenWithinJoinPath,
+ j,
+ minOut,
+ sender,
+ recipient
+ );
+ calls.push(call);
+ this.updateDeltas(deltas, assets, limits);
+ break;
+ }
+ case 'joinPool': {
+ const [call, tokensIn, amountsIn, minBptOut] = this.createJoinPool(
+ node,
+ nodeChildrenWithinJoinPath,
+ j,
+ minOut,
+ sender,
+ recipient
+ );
+ calls.push(call);
+ this.updateDeltas(
+ deltas,
+ [node.address, ...tokensIn],
+ [minBptOut, ...amountsIn]
+ );
+ break;
+ }
+ }
+ });
+ if (isPeek) {
+ const outputRef = 100 * j;
+ const peekCall = Relayer.encodePeekChainedReferenceValue(
+ Relayer.toChainedReference(outputRef, false)
+ );
+ calls.push(peekCall);
+ outputIndexes.push(calls.indexOf(peekCall));
+ }
+ });
+
+ return { calls, outputIndexes, deltas };
+ };
+
+ /**
+ * Creates a map of node address and total proportion. Used for the case where there may be multiple inputs using same token, e.g. DAI input to 2 pools.
+ * @param nodes nodes to consider.
+ */
+ static updateTotalProportions = (
+ nodes: Node[]
+ ): Record => {
+ const totalProportions: Record = {};
+ nodes.forEach((node) => {
+ if (!totalProportions[node.address])
+ totalProportions[node.address] = node.proportionOfParent;
+ else {
+ totalProportions[node.address] = totalProportions[node.address].add(
+ node.proportionOfParent
+ );
+ }
+ });
+ return totalProportions;
+ };
+
+ /**
+ * Uses relayer to approve itself to act in behalf of the user
+ *
+ * @param authorisation Encoded authorisation call.
+ * @returns relayer approval call
+ */
+ createSetRelayerApproval = (authorisation: string): string => {
+ return Relayer.encodeSetRelayerApproval(this.relayer, true, authorisation);
+ };
+
+ static updateNodeAmount = (
+ node: Node,
+ tokensIn: string[],
+ amountsIn: string[],
+ totalProportions: Record
+ ): Node => {
+ /*
+ An input node requires a real amount (not an outputRef) as it is first node in chain.
+ This amount will be used when chaining to parent.
+ Amounts are split proportionally between all inputs with same token.
+ */
+ const tokenIndex = tokensIn
+ .map((t) => t.toLowerCase())
+ .indexOf(node.address.toLowerCase());
+ if (tokenIndex === -1) {
+ node.index = '0';
+ return node;
+ }
+
+ // Calculate proportional split
+ const totalProportion = totalProportions[node.address];
+ const inputProportion = node.proportionOfParent
+ .mul((1e18).toString())
+ .div(totalProportion);
+ const inputAmount = inputProportion
+ .mul(amountsIn[tokenIndex])
+ .div((1e18).toString());
+ // Update index with actual value
+ node.index = inputAmount.toString();
+ // console.log(
+ // `${node.type} ${node.address} prop: ${node.proportionOfParent.toString()}
+ // ${node.joinAction} (
+ // Inputs: ${inputAmount.toString()}
+ // OutputRef: ${node.index}
+ // )`
+ // );
+ return node;
+ };
+
+ createAaveWrap = (
+ node: Node,
+ nodeChildrenWithinJoinPath: Node[],
+ joinPathIndex: number,
+ sender: string,
+ recipient: string
+ ): string => {
+ // Throws error based on the assumption that aaveWrap apply only to input tokens from leaf nodes
+ if (nodeChildrenWithinJoinPath.length !== 1)
+ throw new Error('aaveWrap nodes should always have a single child node');
+
+ const childNode = nodeChildrenWithinJoinPath[0];
+
+ const staticToken = node.address;
+ const amount = childNode.index;
+ const call = Relayer.encodeWrapAaveDynamicToken({
+ staticToken,
+ sender,
+ recipient,
+ amount,
+ fromUnderlying: true,
+ outputReference: this.getOutputRefValue(joinPathIndex, node).value,
+ });
+
+ // console.log(
+ // `${node.type} ${node.address} prop: ${node.proportionOfParent.toString()}
+ // ${node.joinAction} (
+ // staticToken: ${staticToken},
+ // input: ${amount},
+ // outputRef: ${node.index.toString()}
+ // )`
+ // );
+
+ return call;
+ };
+
+ createBatchSwap = (
+ node: Node,
+ nodeChildrenWithinJoinPath: Node[],
+ joinPathIndex: number,
+ expectedOut: string,
+ sender: string,
+ recipient: string
+ ): [string, string[], string[]] => {
+ // We only need batchSwaps for main/wrapped > linearBpt so shouldn't be more than token > token
+ if (nodeChildrenWithinJoinPath.length !== 1)
+ throw new Error('Unsupported batchswap');
+ const inputToken = nodeChildrenWithinJoinPath[0].address;
+ const inputValue = this.getOutputRefValue(
+ joinPathIndex,
+ nodeChildrenWithinJoinPath[0]
+ );
+ const assets = [node.address, inputToken];
+
+ // For tokens going in to the Vault, the limit shall be a positive number. For tokens going out of the Vault, the limit shall be a negative number.
+ // First asset will always be the output token (which will be linearBpt) so use expectedOut to set limit
+ // We don't know input amounts if they are part of a chain so set to max input
+ // TODO can we be safer?
+ const limits: string[] = [
+ BigNumber.from(expectedOut).mul(-1).toString(),
+ inputValue.isRef ? MaxInt256.toString() : inputValue.value,
+ ];
+
+ // TODO Change to single swap to save gas
+ const swaps: BatchSwapStep[] = [
+ {
+ poolId: node.id,
+ assetInIndex: 1,
+ assetOutIndex: 0,
+ amount: inputValue.value,
+ userData: '0x',
+ },
+ ];
+
+ const funds: FundManagement = {
+ sender,
+ recipient,
+ fromInternalBalance: sender === this.relayer,
+ toInternalBalance: recipient === this.relayer,
+ };
+
+ const outputReferences = [
+ {
+ index: assets
+ .map((a) => a.toLowerCase())
+ .indexOf(node.address.toLowerCase()),
+ key: BigNumber.from(this.getOutputRefValue(joinPathIndex, node).value),
+ },
+ ];
+
+ // console.log(
+ // `${node.type} ${node.address} prop: ${node.proportionOfParent.toString()}
+ // ${node.joinAction}(
+ // inputAmt: ${nodeChildrenWithinJoinPath[0].index},
+ // inputToken: ${nodeChildrenWithinJoinPath[0].address},
+ // pool: ${node.id},
+ // outputToken: ${node.address},
+ // outputRef: ${this.getOutputRefValue(joinPathIndex, node).value},
+ // sender: ${sender},
+ // recipient: ${recipient}
+ // )`
+ // );
+
+ const call = Relayer.encodeBatchSwap({
+ swapType: SwapType.SwapExactIn,
+ swaps,
+ assets,
+ funds,
+ limits,
+ deadline: BigNumber.from(Math.ceil(Date.now() / 1000) + 3600), // 1 hour from now
+ value: '0',
+ outputReferences,
+ });
+
+ // If the sender is the Relayer the swap is part of a chain and shouldn't be considered for user deltas
+ const userTokenIn = sender === this.relayer ? '0' : limits[1];
+ // If the receiver is the Relayer the swap is part of a chain and shouldn't be considered for user deltas
+ const userBptOut = recipient === this.relayer ? '0' : limits[0];
+
+ return [call, assets, [userBptOut, userTokenIn]];
+ };
+
+ createJoinPool = (
+ node: Node,
+ nodeChildrenWithinJoinPath: Node[],
+ joinPathIndex: number,
+ minAmountOut: string,
+ sender: string,
+ recipient: string
+ ): [string, string[], string[], string] => {
+ const inputTokens: string[] = [];
+ const inputAmts: string[] = [];
+
+ // inputTokens needs to include each asset even if it has 0 amount
+ node.children.forEach((child) => {
+ inputTokens.push(child.address);
+ // non-leaf joins should set input amounts only for children that are in their joinPath
+ const childWithinJoinPath = nodeChildrenWithinJoinPath.find((c) =>
+ isSameAddress(c.address, child.address)
+ );
+ if (childWithinJoinPath) {
+ inputAmts.push(
+ this.getOutputRefValue(joinPathIndex, childWithinJoinPath).value
+ );
+ } else {
+ inputAmts.push('0');
+ }
+ });
+
+ if (node.type === PoolType.ComposableStable) {
+ // assets need to include the phantomPoolToken
+ inputTokens.push(node.address);
+ // need to add a placeholder so sorting works
+ inputAmts.push('0');
+ }
+
+ // sort inputs
+ const assetHelpers = new AssetHelpers(this.wrappedNativeAsset);
+ const [sortedTokens, sortedAmounts] = assetHelpers.sortTokens(
+ inputTokens,
+ inputAmts
+ ) as [string[], string[]];
+
+ // userData amounts should not include the BPT of the pool being joined
+ let userDataAmounts = [];
+ const bptIndex = sortedTokens
+ .map((t) => t.toLowerCase())
+ .indexOf(node.address.toLowerCase());
+ if (bptIndex === -1) {
+ userDataAmounts = sortedAmounts;
+ } else {
+ userDataAmounts = [
+ ...sortedAmounts.slice(0, bptIndex),
+ ...sortedAmounts.slice(bptIndex + 1),
+ ];
+ }
+
+ let userData: string;
+ if (node.type === PoolType.Weighted) {
+ userData = WeightedPoolEncoder.joinExactTokensInForBPTOut(
+ userDataAmounts,
+ minAmountOut
+ );
+ } else {
+ userData = StablePoolEncoder.joinExactTokensInForBPTOut(
+ userDataAmounts,
+ minAmountOut
+ );
+ }
+
+ // TODO: add test to join weth/wsteth pool using ETH
+ const ethIndex = sortedTokens.indexOf(AddressZero);
+ const value = ethIndex === -1 ? '0' : sortedAmounts[ethIndex];
+
+ // console.log(
+ // `${node.type} ${node.address} prop: ${node.proportionOfParent.toString()}
+ // ${node.joinAction}(
+ // poolId: ${node.id},
+ // assets: ${sortedTokens.toString()},
+ // maxAmtsIn: ${sortedAmounts.toString()},
+ // amountsIn: ${userDataAmounts.toString()},
+ // minOut: ${minAmountOut},
+ // outputRef: ${this.getOutputRefValue(joinPathIndex, node).value},
+ // sender: ${sender},
+ // recipient: ${recipient}
+ // )`
+ // );
+
+ const call = Relayer.constructJoinCall({
+ poolId: node.id,
+ kind: 0,
+ sender,
+ recipient,
+ value,
+ outputReference: this.getOutputRefValue(joinPathIndex, node).value,
+ joinPoolRequest: {} as JoinPoolRequest,
+ assets: sortedTokens, // Must include BPT token
+ maxAmountsIn: sortedAmounts,
+ userData,
+ fromInternalBalance: sender === this.relayer,
+ });
+
+ const userAmountsTokenIn = sortedAmounts.map((a) =>
+ Relayer.isChainedReference(a) ? '0' : a
+ );
+ const userAmountOut = Relayer.isChainedReference(minAmountOut)
+ ? '0'
+ : minAmountOut;
+
+ return [
+ call,
+ // If the sender is the Relayer the join is part of a chain and shouldn't be considered for user deltas
+ sender === this.relayer ? [] : sortedTokens,
+ sender === this.relayer ? [] : userAmountsTokenIn,
+ // If the receiver is the Relayer the join is part of a chain and shouldn't be considered for user deltas
+ recipient === this.relayer
+ ? Zero.toString()
+ : Zero.sub(userAmountOut).toString(), // -ve because coming from Vault
+ ];
+ };
+
+ getOutputRefValue = (
+ joinPathIndex: number,
+ node: Node
+ ): { value: string; isRef: boolean } => {
+ if (node.joinAction === 'input') {
+ // Input nodes have their indexes set as the actual input amount, instead of a chained reference
+ return { value: node.index, isRef: false };
+ } else if (node.index !== '0' || !node.parent) {
+ // Root node (parent === undefined) has index zero, but should still pass chained reference as outputRef value
+ return {
+ value: Relayer.toChainedReference(
+ BigNumber.from(node.index).add(joinPathIndex * 100)
+ ).toString(),
+ isRef: true,
+ };
+ } else {
+ return {
+ value: '0',
+ isRef: true,
+ };
+ }
+ };
+}
diff --git a/balancer-js/src/modules/liquidity/liquidity.module.spec.ts b/balancer-js/src/modules/liquidity/liquidity.module.spec.ts
index cfec86788..08d372a6c 100644
--- a/balancer-js/src/modules/liquidity/liquidity.module.spec.ts
+++ b/balancer-js/src/modules/liquidity/liquidity.module.spec.ts
@@ -82,6 +82,12 @@ describe('Liquidity Module', () => {
const liquidity = await liquidityProvider.getLiquidity(pool);
expect(liquidity).to.be.eq('116.303077211035488');
});
+
+ it('Should not show a huge amount of liquidity for this AKITA pool', async () => {
+ const pool = findPool('0xc065798f227b49c150bcdc6cdc43149a12c4d757');
+ const liquidity = await liquidityProvider.getLiquidity(pool);
+ expect(liquidity).to.be.eq('7781301.384420056605162613');
+ });
});
context('Stable Pool calculations', () => {
@@ -119,4 +125,15 @@ describe('Liquidity Module', () => {
);
});
});
+
+ context('Composable Stable pool calculations', () => {
+ it('Correctly calculates liquidity of a composable stable pool with a boosted subpool', async () => {
+ const liquidity = await liquidityProvider.getLiquidity(
+ findPool('0xb54b2125b711cd183edd3dd09433439d53961652')
+ );
+ expect(Number(liquidity).toFixed(8).toString()).to.be.eq(
+ '17901.40061800'
+ );
+ });
+ });
});
diff --git a/balancer-js/src/modules/liquidity/liquidity.module.ts b/balancer-js/src/modules/liquidity/liquidity.module.ts
index 7ac4ce346..347c54e1c 100644
--- a/balancer-js/src/modules/liquidity/liquidity.module.ts
+++ b/balancer-js/src/modules/liquidity/liquidity.module.ts
@@ -65,7 +65,9 @@ export class Liquidity {
address: token.address,
decimals: token.decimals,
priceRate: token.priceRate,
- price: tokenPrice,
+ price: (tokenPrice?.usd && tokenPrice) || {
+ usd: token.token?.latestUSDPrice,
+ },
balance: token.balance,
weight: token.weight,
};
@@ -73,6 +75,12 @@ export class Liquidity {
})
);
+ // TODO: Just in case we need it soon. Otherwise remove without mercy.
+ // Any of the tokens is missing the price, use subgraph totalLiquidity
+ // if(nonPoolTokensWithUpdatedPrice.map((t) => t.price?.usd).indexOf(undefined) > -1) {
+ // return pool.totalLiquidity
+ // }
+
const tokenLiquidity = PoolTypeConcerns.from(
pool.poolType
).liquidity.calcTotal(nonPoolTokensWithUpdatedPrice);
diff --git a/balancer-js/src/modules/pools/apr/apr.integration.spec.ts b/balancer-js/src/modules/pools/apr/apr.integration.spec.ts
index 26eddb672..8e95d2783 100644
--- a/balancer-js/src/modules/pools/apr/apr.integration.spec.ts
+++ b/balancer-js/src/modules/pools/apr/apr.integration.spec.ts
@@ -20,7 +20,7 @@ const veBalId =
'0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014';
const usdStable =
- '0x7b50775383d3d6f0215a8f290f2c9e2eebbeceb20000000000000000000000fe';
+ '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d';
const btcEth =
'0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e';
diff --git a/balancer-js/src/modules/pools/apr/apr.ts b/balancer-js/src/modules/pools/apr/apr.ts
index e7f1e019b..0cab12fe8 100644
--- a/balancer-js/src/modules/pools/apr/apr.ts
+++ b/balancer-js/src/modules/pools/apr/apr.ts
@@ -9,12 +9,9 @@ import type {
TokenAttribute,
LiquidityGauge,
Network,
+ PoolToken,
} from '@/types';
-import {
- BaseFeeDistributor,
- ProtocolFeesProvider,
- RewardData,
-} from '@/modules/data';
+import { BaseFeeDistributor, RewardData } from '@/modules/data';
import { ProtocolRevenue } from './protocol-revenue';
import { Liquidity } from '@/modules/liquidity/liquidity.module';
import { identity, zipObject, pickBy } from 'lodash';
@@ -60,8 +57,7 @@ export class PoolApr {
private feeCollector: Findable,
private yesterdaysPools?: Findable,
private liquidityGauges?: Findable,
- private feeDistributor?: BaseFeeDistributor,
- private protocolFees?: ProtocolFeesProvider
+ private feeDistributor?: BaseFeeDistributor
) {}
/**
@@ -108,70 +104,89 @@ export class PoolApr {
});
// Get each token APRs
- const aprs = bptFreeTokens.map(async (token) => {
- let apr = 0;
- const tokenYield = await this.tokenYields.find(token.address);
-
- if (tokenYield) {
- if (pool.poolType === 'MetaStable') {
- apr = tokenYield * (1 - (await this.protocolSwapFeePercentage()));
- } else if (pool.poolType === 'ComposableStable') {
- if (token.isExemptFromYieldProtocolFee) {
- apr = tokenYield;
+ const aprs = await Promise.all(
+ bptFreeTokens.map(async (token) => {
+ let apr = 0;
+ const tokenYield = await this.tokenYields.find(token.address);
+
+ if (tokenYield) {
+ if (pool.poolType === 'MetaStable') {
+ apr = tokenYield * (1 - (await this.protocolSwapFeePercentage()));
+ } else if (
+ pool.poolType === 'ComposableStable' ||
+ (pool.poolType === 'Weighted' && pool.poolTypeVersion === 2)
+ ) {
+ if (token.isExemptFromYieldProtocolFee) {
+ apr = tokenYield;
+ } else {
+ apr =
+ tokenYield *
+ (1 - parseFloat(pool.protocolYieldFeeCache || '0.5'));
+ }
} else {
- const fees = await this.protocolFeesPercentage();
- apr = tokenYield * (1 - fees.yieldFee);
- }
- } else if (pool.poolType === 'Weighted' && pool.poolTypeVersion === 2) {
- if (token.isExemptFromYieldProtocolFee) {
apr = tokenYield;
- } else {
- apr = tokenYield * (1 - parseFloat(pool.protocolYieldFeeCache));
}
} else {
- apr = tokenYield;
- }
- } else {
- // Handle subpool APRs with recursive call to get the subPool APR
- const subPool = await this.pools.findBy('address', token.address);
-
- if (subPool) {
- // INFO: Liquidity mining APR can't cascade to other pools
- const subSwapFees = await this.swapFees(subPool);
- const subtokenAprs = await this.tokenAprs(subPool);
- apr = subSwapFees + subtokenAprs.total;
+ // Handle subpool APRs with recursive call to get the subPool APR
+ const subPool = await this.pools.findBy('address', token.address);
+
+ if (subPool) {
+ // INFO: Liquidity mining APR can't cascade to other pools
+ const subSwapFees = await this.swapFees(subPool);
+ const subtokenAprs = await this.tokenAprs(subPool);
+ let subApr = subtokenAprs.total;
+ if (
+ pool.poolType === 'ComposableStable' ||
+ (pool.poolType === 'Weighted' && pool.poolTypeVersion === 2)
+ ) {
+ if (!token.isExemptFromYieldProtocolFee) {
+ subApr =
+ subApr *
+ (1 - parseFloat(pool.protocolYieldFeeCache || '0.5'));
+ }
+ }
+ apr = subSwapFees + subApr;
+ }
}
- }
- return apr;
- });
+ return apr;
+ })
+ );
// Get token weights normalised by usd price
- const weights = bptFreeTokens.map(async (token): Promise => {
+ const getWeight = async (token: PoolToken): Promise => {
+ let tokenPrice: string | undefined;
if (token.weight) {
return parseFloat(token.weight);
- } else {
- let tokenPrice =
- token.price?.usd || (await this.tokenPrices.find(token.address))?.usd;
- if (!tokenPrice) {
- const poolToken = await this.pools.findBy('address', token.address);
- if (poolToken) {
- tokenPrice = (await this.bptPrice(poolToken)).toString();
- } else {
- throw `No price for ${token.address}`;
- }
+ } else if (token.token?.pool?.poolType) {
+ const poolToken = await this.pools.findBy('address', token.address);
+ if (poolToken) {
+ tokenPrice = (await this.bptPrice(poolToken)).toString();
}
+ } else {
+ tokenPrice =
+ token.price?.usd ||
+ (await this.tokenPrices.find(token.address))?.usd ||
+ token.token?.latestUSDPrice;
+ }
+ if (tokenPrice) {
// using floats assuming frontend purposes with low precision needs
const tokenValue = parseFloat(token.balance) * parseFloat(tokenPrice);
return tokenValue / parseFloat(totalLiquidity);
+ } else {
+ throw `No price for ${token.address}`;
}
- });
+ };
// Normalise tokenAPRs according to weights
const weightedAprs = await Promise.all(
- aprs.map(async (apr, idx) => {
- const [a, w] = await Promise.all([apr, weights[idx]]);
- return Math.round(a * w);
+ bptFreeTokens.map(async (token, idx) => {
+ if (aprs[idx] === 0) {
+ return 0;
+ }
+
+ const weight = await getWeight(token);
+ return Math.round(aprs[idx] * weight);
})
);
@@ -419,10 +434,14 @@ export class PoolApr {
* @returns Pool liquidity in USD
*/
private async totalLiquidity(pool: Pool): Promise {
- const liquidityService = new Liquidity(this.pools, this.tokenPrices);
- const liquidity = await liquidityService.getLiquidity(pool);
-
- return liquidity;
+ try {
+ const liquidityService = new Liquidity(this.pools, this.tokenPrices);
+ const liquidity = await liquidityService.getLiquidity(pool);
+ return liquidity;
+ } catch (err) {
+ console.error('Liquidity calculcation failed, falling back to subgraph');
+ return pool.totalLiquidity;
+ }
}
/**
@@ -444,17 +463,6 @@ export class PoolApr {
return fee ? fee : 0;
}
- private async protocolFeesPercentage() {
- if (this.protocolFees) {
- return await this.protocolFees.getFees();
- }
-
- return {
- swapFee: 0,
- yieldFee: 0,
- };
- }
-
private async rewardTokenApr(tokenAddress: string, rewardData: RewardData) {
if (rewardData.period_finish.toNumber() < Date.now() / 1000) {
return {
diff --git a/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.integration.spec.ts b/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.integration.spec.ts
new file mode 100644
index 000000000..fd512e57e
--- /dev/null
+++ b/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.integration.spec.ts
@@ -0,0 +1,95 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { ImpermanentLossService } from '@/modules/pools/impermanentLoss/impermanentLossService';
+import { BalancerSDK } from '@/modules/sdk.module';
+import { Network, Pool } from '@/types';
+import { expect } from 'chai';
+
+const TEST_DATA: { [key: string]: { poolId: string } } = {
+ ComposableStablePool: {
+ poolId:
+ '0x8159462d255c1d24915cb51ec361f700174cd99400000000000000000000075d',
+ },
+ WeightedPool: {
+ poolId:
+ '0x3d468ab2329f296e1b9d8476bb54dd77d8c2320f000200000000000000000426',
+ },
+ WeightedPoolWithMissingPrice: {
+ poolId:
+ '0x017fe2f89a34a3485b66e50b3b25c588d70a787a0002000000000000000008c7',
+ },
+ WeightedPoolWithMissingUserData: {
+ poolId:
+ '0x3d468ab2329f296e1b9d8476bb54dd77d8c2320f000200000000000000000426',
+ },
+};
+
+const rpcUrl = 'https://rpc.ankr.com/polygon';
+const network = Network.POLYGON;
+const sdk = new BalancerSDK({ network, rpcUrl });
+const service = new ImpermanentLossService(
+ sdk.data.tokenPrices,
+ sdk.data.tokenHistoricalPrices
+);
+
+const getPool = async (poolId: string): Promise => {
+ const pool = await sdk.pools.find(poolId);
+ if (!pool) {
+ throw new Error('poll not found');
+ }
+ return pool;
+};
+/*
+ * REALLY MORE A LIST OF USE CASE SCENARIOS THAN AN INTEGRATION TEST.
+ *
+ * TODO: add stubbing
+ */
+describe('ImpermanentLossService', () => {
+ context('when queried for Composable Stable Pool', () => {
+ it('should return an IL gte 0', async () => {
+ const testData = TEST_DATA.ComposableStablePool;
+ const pool = await getPool(testData.poolId);
+ const timestamp = 1666601608;
+ const loss = await service.calcImpLoss(timestamp, pool);
+ expect(loss).gte(0);
+ });
+ });
+ context('when queried for Weighted Pool', () => {
+ it('should return an IL gte 0', async () => {
+ const testData = TEST_DATA.WeightedPool;
+ const pool = await getPool(testData.poolId);
+ const timestamp = 1666601608;
+ const loss = await service.calcImpLoss(timestamp, pool);
+ expect(loss).gte(0);
+ });
+ });
+ context('when queried for pool Weighted Pool with missing price', () => {
+ it('should throw an exception', async () => {
+ const testData = TEST_DATA.WeightedPoolWithMissingPrice;
+ const pool = await getPool(testData.poolId);
+ const timestamp = 1666276501;
+ try {
+ await service.calcImpLoss(timestamp, pool);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.MISSING_PRICE_RATE)
+ );
+ }
+ });
+ });
+ context('when queried for pool Weighted Pool with missing user data', () => {
+ it('should throw an exception', async () => {
+ const testData = TEST_DATA.WeightedPoolWithMissingUserData;
+ const pool = await getPool(testData.poolId);
+ const timestamp = Date.now() + 3600000; //1 hour from now
+ try {
+ await service.calcImpLoss(timestamp, pool);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.TIMESTAMP_IN_THE_FUTURE)
+ );
+ }
+ });
+ });
+});
diff --git a/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.spec.ts b/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.spec.ts
new file mode 100644
index 000000000..d9df1db2d
--- /dev/null
+++ b/balancer-js/src/modules/pools/impermanentLoss/impermanentLoss.spec.ts
@@ -0,0 +1,455 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { ImpermanentLossService } from '@/modules/pools/impermanentLoss/impermanentLossService';
+import {
+ MockHistoricalPriceProvider,
+ MockPriceProvider,
+} from '@/test/lib/ImpermanentLossData';
+import { Pool } from '@/types';
+import { expect } from 'chai';
+import dotenv from 'dotenv';
+import { repositores, aaveRates } from '@/test/factories/data';
+
+const stubbedRepositores = repositores({});
+
+dotenv.config();
+
+const mockTokenPriceProvider = new MockPriceProvider(
+ stubbedRepositores.tokenPrices,
+ stubbedRepositores.tokenPrices,
+ aaveRates
+);
+const mockHistoricalTokenPriceProvider = new MockHistoricalPriceProvider(
+ stubbedRepositores.tokenPrices,
+ aaveRates
+);
+
+const service = new ImpermanentLossService(
+ mockTokenPriceProvider,
+ mockHistoricalTokenPriceProvider
+);
+
+describe('ImpermanentLossService', () => {
+ context('service.getWeights', () => {
+ it('should return uniform distributed weights', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ weight: null,
+ },
+ ];
+ const weights = service.getWeights(poolTokens);
+ expect(weights).length(2);
+ expect(weights[0]).eq(0.5);
+ expect(weights[1]).eq(0.5);
+ });
+ it('should return proper weights', async () => {
+ const poolTokens = [
+ {
+ weight: '0.2',
+ balance: '0.440401496163206405',
+ address: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619',
+ },
+ {
+ weight: '0.8',
+ balance: '1005938.192755235524459442',
+ address: '0xe3627374ac4baf5375e79251b0af23afc450fc0e',
+ },
+ ];
+ const weights = service.getWeights(poolTokens);
+ expect(weights).length(2);
+ expect(weights[0]).eq(0.2);
+ expect(weights[1]).eq(0.8);
+ });
+ it('should throw error if missing weight', async () => {
+ const poolTokens = [
+ {
+ weight: null,
+ balance: '0.440401496163206405',
+ address: '0x7ceb23fd6bc0add59e62ac25578270cff1b9f619',
+ },
+ {
+ weight: '0.8',
+ balance: '1005938.192755235524459442',
+ address: '0xe3627374ac4baf5375e79251b0af23afc450fc0e',
+ },
+ ];
+ try {
+ service.getWeights(poolTokens);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.MISSING_WEIGHT)
+ );
+ }
+ });
+ });
+ context('service.getDelta', () => {
+ it('should return 50% delta variation', async () => {
+ const delta = service.getDelta(10, 15);
+ expect(delta).eq(0.5);
+ });
+ it('should return no delta variation', async () => {
+ const delta = service.getDelta(10, 10);
+ expect(delta).eq(0);
+ });
+ it('should return negative delta variation', async () => {
+ const delta = service.getDelta(15, 10);
+ expect(delta).closeTo(-0.3333, 3);
+ });
+ it('should return negative delta variation', async () => {
+ const delta = service.getDelta(15, 10);
+ expect(delta).closeTo(-0.3333, 3);
+ });
+ it('should throw an error for wrong parameter', async () => {
+ try {
+ service.getDelta(0, 10);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.ILLEGAL_PARAMETER)
+ );
+ }
+ });
+ });
+ context('service.getEntryPrices', () => {
+ it('should return prices for tokens', async () => {
+ const tokens = [
+ '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ ];
+ const prices = await service.getEntryPrices(1666276501, tokens);
+ expect(prices['0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270']).eq(
+ 0.9993785272283172
+ );
+ expect(prices['0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4']).eq(
+ 1.9996776052990013
+ );
+ });
+ it('should throw error for missing prices', async () => {
+ const tokens = [
+ '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ ];
+ try {
+ await service.getEntryPrices(1666276501, tokens);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.MISSING_PRICE_RATE)
+ );
+ }
+ });
+ });
+ context('service.getExitPrices', () => {
+ it('should return exit prices for tokens', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ weight: null,
+ },
+ ];
+ const prices = await service.getExitPrices(poolTokens);
+ expect(prices['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48']).eq(1.002);
+ expect(prices['0x6b175474e89094c44da98b954eedeac495271d0f']).eq(1.002);
+ });
+ it('should throw error for missing prices', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ weight: null,
+ },
+ ];
+ try {
+ await service.getExitPrices(poolTokens);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.MISSING_PRICE_RATE)
+ );
+ }
+ });
+ });
+ context('service.getAssets', () => {
+ it('should returns a list of assets with deltas and weights', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ weight: null,
+ },
+ ];
+ const weights = [0.5, 0.5];
+ const entryPrices = {
+ '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270': 10,
+ '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4': 15,
+ };
+ const exitPrices = {
+ '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270': 15,
+ '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4': 10,
+ };
+ const assets = service.getAssets(
+ poolTokens,
+ exitPrices,
+ entryPrices,
+ weights
+ );
+ expect(assets).length(2);
+ expect(assets[0].priceDelta).eq(0.5);
+ expect(assets[1].priceDelta).closeTo(-0.3333, 3);
+ expect(assets[0].weight).eq(0.5);
+ expect(assets[1].weight).eq(0.5);
+ });
+ });
+ context('service.prepareData', () => {
+ it('should return a list of assets with proper deltas and weights', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ weight: null,
+ },
+ ];
+ const pool = {
+ tokens: poolTokens,
+ address: '0x8159462d255c1d24915cb51ec361f700174cd994',
+ } as unknown as Pool;
+ const assets = await service.prepareData(1666276501, pool);
+ expect(assets).length(2);
+ expect(assets[0].priceDelta).eq(0.00262310295874897);
+ expect(assets[1].priceDelta).eq(-0.49891922710702347);
+ expect(assets[0].weight).eq(0.5);
+ expect(assets[1].weight).eq(0.5);
+ });
+ });
+ context('service.calculateImpermanentLoss', () => {
+ context('for uniform distributed tokens', () => {
+ it('should return proper value for deltas [0.2%, -49.89%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0.00262310295874897,
+ weight: 0.5,
+ },
+ {
+ priceDelta: -0.49891922710702347,
+ weight: 0.5,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(5.72);
+ });
+ it('should return proper value for deltas [0%, -49.89%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0,
+ weight: 0.5,
+ },
+ {
+ priceDelta: -0.49891922710702347,
+ weight: 0.5,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(5.68);
+ });
+ it('should return proper value for deltas [0%, 50%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0,
+ weight: 0.5,
+ },
+ {
+ priceDelta: 0.5,
+ weight: 0.5,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(2.02);
+ });
+ it('should return IL = 0', async () => {
+ const assets = [
+ {
+ priceDelta: 0.5,
+ weight: 0.5,
+ },
+ {
+ priceDelta: 0.5,
+ weight: 0.5,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(0);
+ });
+ });
+ context('for not uniform distributed tokens', () => {
+ it('should return proper value for deltas [0.2%, -49.89%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0.00262310295874897,
+ weight: 0.8,
+ },
+ {
+ priceDelta: -0.49891922710702347,
+ weight: 0.2,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(3.27);
+ });
+ it('should return proper value for deltas [0%, -49.89%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0,
+ weight: 0.8,
+ },
+ {
+ priceDelta: -0.49891922710702347,
+ weight: 0.2,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(3.25);
+ });
+ it('should return proper value for deltas [0%, 50%]', async () => {
+ const assets = [
+ {
+ priceDelta: 0,
+ weight: 0.8,
+ },
+ {
+ priceDelta: 0.5,
+ weight: 0.2,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(1.41);
+ });
+ it('should return IL = 0', async () => {
+ const assets = [
+ {
+ priceDelta: 0.5,
+ weight: 0.8,
+ },
+ {
+ priceDelta: 0.5,
+ weight: 0.2,
+ },
+ ];
+ const poolValueDelta = service.getPoolValueDelta(assets);
+ const holdValueDelta = service.getHoldValueDelta(assets);
+ const IL = service.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ expect(IL).eq(0);
+ });
+ });
+ });
+ context('service.calcImpLoss', () => {
+ it('should throw error for timestamp in the future', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ weight: null,
+ },
+ ];
+ const pool = {
+ tokens: poolTokens,
+ address: '0x8159462d255c1d24915cb51ec361f700174cd994',
+ } as unknown as Pool;
+ try {
+ await service.calcImpLoss(Date.now() + 3600000, pool);
+ } catch (e: any) {
+ expect(e.message).eq(
+ BalancerError.getMessage(BalancerErrorCode.TIMESTAMP_IN_THE_FUTURE)
+ );
+ }
+ });
+ it('should return impermanentLoss', async () => {
+ const poolTokens = [
+ {
+ balance: '20252425.874518101545808004',
+ address: '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ weight: null,
+ },
+ {
+ balance: '19238580.71904976339020527',
+ address: '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ weight: null,
+ },
+ ];
+ const pool = {
+ tokens: poolTokens,
+ address: '0x8159462d255c1d24915cb51ec361f700174cd994',
+ } as unknown as Pool;
+ const IL = await service.calcImpLoss(1666276501, pool);
+ expect(IL).eq(5.72);
+ });
+ });
+});
diff --git a/balancer-js/src/modules/pools/impermanentLoss/impermanentLossService.ts b/balancer-js/src/modules/pools/impermanentLoss/impermanentLossService.ts
new file mode 100644
index 000000000..aa7b23e67
--- /dev/null
+++ b/balancer-js/src/modules/pools/impermanentLoss/impermanentLossService.ts
@@ -0,0 +1,233 @@
+/**
+ * Calculate the Impermanent Loss for a given pool and user.
+ *
+ * 1. Prepare the data:
+ * a. get exit price for pools' tokens
+ * b. get entry price for pools' tokens
+ * 2. calculate delta values for tokens in pools
+ * 3. calculate and return the impermanent loss as percentage rounded to 2 decimal places.
+ *
+ */
+import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
+import { Findable, Pool, PoolToken, Price } from '@/types';
+
+type Asset = {
+ priceDelta: number;
+ weight: number;
+};
+
+type TokenPrices = {
+ [key: string]: number;
+};
+
+export class ImpermanentLossService {
+ constructor(
+ private tokenPrices: Findable,
+ private tokenHistoricalPrices: Findable
+ ) {}
+
+ /**
+ * entry point to calculate impermanent loss.
+ *
+ * The function will
+ * - retrieve the tokens' historical value at the desired time in the future
+ * - calculate the relative variation between current and historical value
+ * - return the IL in percentage rounded to 2 decimal places
+ *
+ * @param timestamp UNIX timestamp from which the IL is desired
+ * @param pool the pool
+ * @returns the impermanent loss as percentage rounded to 2 decimal places
+ */
+ async calcImpLoss(timestamp: number, pool: Pool): Promise {
+ if (timestamp * 1000 >= Date.now()) {
+ console.error(
+ `[ImpermanentLossService][calcImpLoss]Error: ${BalancerError.getMessage(
+ BalancerErrorCode.TIMESTAMP_IN_THE_FUTURE
+ )}`
+ );
+ throw new BalancerError(BalancerErrorCode.TIMESTAMP_IN_THE_FUTURE);
+ }
+ const assets = await this.prepareData(timestamp, pool);
+
+ const poolValueDelta = this.getPoolValueDelta(assets);
+ const holdValueDelta = this.getHoldValueDelta(assets);
+
+ const impLoss = this.calculateImpermanentLoss(
+ poolValueDelta,
+ holdValueDelta
+ );
+ return impLoss;
+ }
+
+ calculateImpermanentLoss(
+ poolValueDelta: number,
+ holdValueDelta: number
+ ): number {
+ return (
+ Math.floor(Math.abs(poolValueDelta / holdValueDelta - 1) * 100 * 100) /
+ 100
+ );
+ }
+
+ getPoolValueDelta(assets: Asset[]): number {
+ return assets.reduce(
+ (result, asset) =>
+ result * Math.pow(Math.abs(asset.priceDelta + 1), asset.weight),
+ 1
+ );
+ }
+
+ getHoldValueDelta(assets: Asset[]): number {
+ return assets.reduce(
+ (result, asset) => result + Math.abs(asset.priceDelta + 1) * asset.weight,
+ 0
+ );
+ }
+
+ /**
+ * prepare the data for calculating the impermanent loss
+ *
+ * @param entryTimestamp UNIX timestamp from which the IL is desired
+ * @param pool the pool
+ * @returns a list of pair weight/price delta for each token in the pool
+ * @throws BalancerError if
+ * 1. a token's price is unknown
+ * 2. a token's weight is unknown
+ * 3. the user has no liquidity invested in the pool
+ */
+ async prepareData(entryTimestamp: number, pool: Pool): Promise {
+ const poolTokens = pool.tokens.filter(
+ (token) => token.address !== pool.address
+ );
+
+ const weights = this.getWeights(poolTokens);
+
+ const tokenAddresses = poolTokens.map((t) => t.address);
+
+ const entryPrices = await this.getEntryPrices(
+ entryTimestamp,
+ tokenAddresses
+ );
+ const exitPrices: TokenPrices = await this.getExitPrices(poolTokens);
+
+ return this.getAssets(poolTokens, exitPrices, entryPrices, weights);
+ }
+
+ getAssets(
+ poolTokens: PoolToken[],
+ exitPrices: TokenPrices,
+ entryPrices: TokenPrices,
+ weights: number[]
+ ): Asset[] {
+ return poolTokens.map((token, i) => ({
+ priceDelta: this.getDelta(
+ entryPrices[token.address],
+ exitPrices[token.address]
+ ),
+ weight: weights[i],
+ }));
+ }
+
+ getDelta(entryPrice: number, exitPrice: number): number {
+ if (entryPrice === 0) {
+ console.error(
+ `[ImpermanentLossService][getDelta]Error: ${BalancerError.getMessage(
+ BalancerErrorCode.ILLEGAL_PARAMETER
+ )}: entry price is 0`
+ );
+ throw new BalancerError(BalancerErrorCode.ILLEGAL_PARAMETER);
+ }
+ return (exitPrice - entryPrice) / entryPrice;
+ }
+
+ /**
+ * returns the list of token's weights.
+ *
+ * @param poolTokens the pools' tokens
+ * @returns the list of token's weights
+ * @throws BalancerError if a token's weight is missing
+ *
+ */
+ getWeights(poolTokens: PoolToken[]): number[] {
+ const noWeights = poolTokens.every((token) => !token.weight);
+ const uniformWeight = Math.round((1 / poolTokens.length) * 100) / 100;
+ const weights: number[] = noWeights
+ ? poolTokens.map(() => uniformWeight) // if no weight is returned we assume the tokens are balanced uniformly in the pool
+ : poolTokens.map((token) => Number(token.weight ?? 0));
+
+ if (weights.some((w) => w === 0)) {
+ console.error(
+ `[ImpermanentLossService][getWeights]Error: ${BalancerError.getMessage(
+ BalancerErrorCode.MISSING_WEIGHT
+ )}`
+ );
+ throw new BalancerError(BalancerErrorCode.MISSING_WEIGHT);
+ }
+ return weights;
+ }
+
+ /**
+ * get the current's tokens' prices
+ * @param tokens the pools' tokens
+ * @returns a list of tokens with prices
+ */
+ async getExitPrices(tokens: PoolToken[]): Promise {
+ const prices = await Promise.all(
+ tokens.map((token) => this.tokenPrices.find(token.address))
+ ).catch(() => []);
+
+ if (!prices.length || prices.some((price) => price?.usd === undefined)) {
+ console.error(
+ `[ImpermanentLossService][getExitPrices]Error: ${BalancerError.getMessage(
+ BalancerErrorCode.MISSING_PRICE_RATE
+ )}`
+ );
+ throw new BalancerError(BalancerErrorCode.MISSING_PRICE_RATE);
+ }
+
+ const tokensWithPrice = tokens.map((token, i) => ({
+ ...token,
+ price: prices[i],
+ }));
+
+ const tokenPrices: TokenPrices = {};
+ for (const token of tokensWithPrice) {
+ if (token.price?.usd) tokenPrices[token.address] = +token.price.usd; // price.usd is never undefined but JS complains
+ }
+ return tokenPrices;
+ }
+
+ /**
+ * get the tokens' price at a given time
+ *
+ * @param timestamp the Unix timestamp
+ * @param tokenAddresses the tokens' addresses
+ * @returns a map of tokens' price
+ */
+ async getEntryPrices(
+ timestamp: number,
+ tokenAddresses: string[]
+ ): Promise {
+ const prices: TokenPrices = {};
+ for (const address of tokenAddresses) {
+ const price = await this.tokenHistoricalPrices
+ .findBy(address, timestamp)
+ .catch((reason) => {
+ console.error(
+ `[ImpermanentLossService][getEntryPrices]Error: ${reason.message}`
+ );
+ return undefined;
+ });
+ if (!price?.usd) {
+ console.error(
+ `[ImpermanentLossService][getEntryPrices]Error: ${BalancerError.getMessage(
+ BalancerErrorCode.MISSING_PRICE_RATE
+ )}`
+ );
+ throw new BalancerError(BalancerErrorCode.MISSING_PRICE_RATE);
+ }
+ prices[address] = +price.usd;
+ }
+ return prices;
+ }
+}
diff --git a/balancer-js/src/modules/pools/index.ts b/balancer-js/src/modules/pools/index.ts
index ee65a078a..24a87cf07 100644
--- a/balancer-js/src/modules/pools/index.ts
+++ b/balancer-js/src/modules/pools/index.ts
@@ -1,3 +1,4 @@
+import { ImpermanentLossService } from '@/modules/pools/impermanentLoss/impermanentLossService';
import type {
BalancerNetworkConfig,
BalancerDataRepositories,
@@ -12,6 +13,8 @@ import { JoinPoolAttributes } from './pool-types/concerns/types';
import { PoolTypeConcerns } from './pool-type-concerns';
import { PoolApr } from './apr/apr';
import { Liquidity } from '../liquidity/liquidity.module';
+import { Join } from '../joins/joins.module';
+import { Exit } from '../exits/exits.module';
import { PoolVolume } from './volume/volume';
import { PoolFees } from './fees/fees';
@@ -21,8 +24,11 @@ import { PoolFees } from './fees/fees';
export class Pools implements Findable {
aprService;
liquidityService;
+ joinService;
+ exitService;
feesService;
volumeService;
+ impermanentLossService;
constructor(
private networkConfig: BalancerNetworkConfig,
@@ -42,8 +48,14 @@ export class Pools implements Findable {
repositories.pools,
repositories.tokenPrices
);
+ this.joinService = new Join(this.repositories.pools, networkConfig);
+ this.exitService = new Exit(this.repositories.pools, networkConfig);
this.feesService = new PoolFees(repositories.yesterdaysPools);
this.volumeService = new PoolVolume(repositories.yesterdaysPools);
+ this.impermanentLossService = new ImpermanentLossService(
+ repositories.tokenPrices,
+ repositories.tokenHistoricalPrices
+ );
}
dataSource(): Findable & Searchable {
@@ -61,6 +73,17 @@ export class Pools implements Findable {
return this.aprService.apr(pool);
}
+ /**
+ * Calculates Impermanent Loss on any pool data
+ *
+ * @param timestamp
+ * @param pool
+ * @returns
+ */
+ async impermanentLoss(timestamp: number, pool: Pool): Promise {
+ return this.impermanentLossService.calcImpLoss(timestamp, pool);
+ }
+
/**
* Calculates total liquidity of the pool
*
@@ -71,6 +94,77 @@ export class Pools implements Findable {
return this.liquidityService.getLiquidity(pool);
}
+ /**
+ * Builds generalised join transaction
+ *
+ * @param poolId Pool id
+ * @param tokens Token addresses
+ * @param amounts Token amounts in EVM scale
+ * @param userAddress User address
+ * @param wrapMainTokens Indicates whether main tokens should be wrapped before being used
+ * @param slippage Maximum slippage tolerance in bps i.e. 50 = 0.5%.
+ * @param authorisation Optional auhtorisation call to be added to the chained transaction
+ * @returns transaction data ready to be sent to the network along with min and expected BPT amounts out.
+ */
+ async generalisedJoin(
+ poolId: string,
+ tokens: string[],
+ amounts: string[],
+ userAddress: string,
+ wrapMainTokens: boolean,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ minOut: string;
+ expectedOut: string;
+ priceImpact: string;
+ }> {
+ return this.joinService.joinPool(
+ poolId,
+ tokens,
+ amounts,
+ userAddress,
+ wrapMainTokens,
+ slippage,
+ authorisation
+ );
+ }
+
+ /**
+ * Builds generalised exit transaction
+ *
+ * @param poolId Pool id
+ * @param amount Token amount in EVM scale
+ * @param userAddress User address
+ * @param slippage Maximum slippage tolerance in bps i.e. 50 = 0.5%.
+ * @param authorisation Optional auhtorisation call to be added to the chained transaction
+ * @returns transaction data ready to be sent to the network along with tokens, min and expected amounts out.
+ */
+ async generalisedExit(
+ poolId: string,
+ amount: string,
+ userAddress: string,
+ slippage: string,
+ authorisation?: string
+ ): Promise<{
+ to: string;
+ callData: string;
+ tokensOut: string[];
+ expectedAmountsOut: string[];
+ minAmountsOut: string[];
+ priceImpact: string;
+ }> {
+ return this.exitService.exitPool(
+ poolId,
+ amount,
+ userAddress,
+ slippage,
+ authorisation
+ );
+ }
+
/**
* Calculates total fees for the pool in the last 24 hours
*
@@ -115,11 +209,16 @@ export class Pools implements Findable {
wrappedNativeAsset,
});
},
- calcPriceImpact: async (amountsIn: string[], minBPTOut: string) =>
+ calcPriceImpact: async (
+ amountsIn: string[],
+ minBPTOut: string,
+ isJoin: boolean
+ ) =>
methods.priceImpactCalculator.calcPriceImpact(
pool,
amountsIn,
- minBPTOut
+ minBPTOut,
+ isJoin
),
buildExitExactBPTIn: (
exiter,
@@ -155,8 +254,13 @@ export class Pools implements Findable {
// either we refetch or it needs a type transformation from SDK internal to SOR (subgraph)
// spotPrice: async (tokenIn: string, tokenOut: string) =>
// methods.spotPriceCalculator.calcPoolSpotPrice(tokenIn, tokenOut, data),
- calcSpotPrice: (tokenIn: string, tokenOut: string) =>
- methods.spotPriceCalculator.calcPoolSpotPrice(tokenIn, tokenOut, pool),
+ calcSpotPrice: (tokenIn: string, tokenOut: string, isDefault?: boolean) =>
+ methods.spotPriceCalculator.calcPoolSpotPrice(
+ tokenIn,
+ tokenOut,
+ pool,
+ isDefault
+ ),
};
}
diff --git a/balancer-js/src/modules/pools/pool-types/composableStable.module.ts b/balancer-js/src/modules/pools/pool-types/composableStable.module.ts
index 876eae710..70a34ea21 100644
--- a/balancer-js/src/modules/pools/pool-types/composableStable.module.ts
+++ b/balancer-js/src/modules/pools/pool-types/composableStable.module.ts
@@ -1,7 +1,7 @@
-import { ComposableStablePoolJoin } from './concerns/composableStable/join.concern';
import { StablePoolLiquidity } from './concerns/stable/liquidity.concern';
-import { StablePoolSpotPrice } from './concerns/stable/spotPrice.concern';
-import { StablePoolPriceImpact } from './concerns/stable/priceImpact.concern';
+import { PhantomStablePoolSpotPrice } from './concerns/stablePhantom/spotPrice.concern';
+import { StablePhantomPriceImpact } from './concerns/stablePhantom/priceImpact.concern';
+import { ComposableStablePoolJoin } from './concerns/composableStable/join.concern';
import { ComposableStablePoolExit } from './concerns/composableStable/exit.concern';
import { PoolType } from './pool-type.interface';
import {
@@ -15,9 +15,9 @@ import {
export class ComposableStable implements PoolType {
constructor(
public exit: ExitConcern = new ComposableStablePoolExit(),
- public join: JoinConcern = new ComposableStablePoolJoin(),
public liquidity: LiquidityConcern = new StablePoolLiquidity(),
- public spotPriceCalculator: SpotPriceConcern = new StablePoolSpotPrice(),
- public priceImpactCalculator: PriceImpactConcern = new StablePoolPriceImpact()
+ public spotPriceCalculator: SpotPriceConcern = new PhantomStablePoolSpotPrice(),
+ public priceImpactCalculator: PriceImpactConcern = new StablePhantomPriceImpact(),
+ public join: JoinConcern = new ComposableStablePoolJoin()
) {}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/linear/spotPrice.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/linear/spotPrice.concern.ts
index 0848123db..8ff606a9e 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/linear/spotPrice.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/linear/spotPrice.concern.ts
@@ -3,11 +3,19 @@ import { SubgraphPoolBase, LinearPool, ZERO } from '@balancer-labs/sor';
import { Pool } from '@/types';
export class LinearPoolSpotPrice implements SpotPriceConcern {
- calcPoolSpotPrice(tokenIn: string, tokenOut: string, pool: Pool): string {
+ calcPoolSpotPrice(
+ tokenIn: string,
+ tokenOut: string,
+ pool: Pool,
+ isDefault = false
+ ): string {
const linearPool = LinearPool.fromPool(pool as SubgraphPoolBase);
- const poolPairData = linearPool.parsePoolPairData(tokenIn, tokenOut);
- return linearPool
- ._spotPriceAfterSwapExactTokenInForTokenOut(poolPairData, ZERO)
- .toString();
+ if (isDefault) return '1';
+ else {
+ const poolPairData = linearPool.parsePoolPairData(tokenIn, tokenOut);
+ return linearPool
+ ._spotPriceAfterSwapExactTokenInForTokenOut(poolPairData, ZERO)
+ .toString();
+ }
}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/join.concern.integration.spec.ts
index d4a8ce8b6..8fdf7b5a8 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/join.concern.integration.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/join.concern.integration.spec.ts
@@ -117,7 +117,8 @@ describe('join execution', async () => {
const minBPTOut = bptMinBalanceIncrease.toString();
const priceImpact = await controller.calcPriceImpact(
amountsIn,
- minBPTOut
+ minBPTOut,
+ true
);
expect(priceImpact).to.eql('100000000010000');
});
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.concern.ts
index a5a845d78..6e23b5b26 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.concern.ts
@@ -76,12 +76,17 @@ export class MetaStablePoolPriceImpact implements PriceImpactConcern {
calcPriceImpact(
pool: Pool,
tokenAmounts: string[],
- bptAmount: string
+ bptAmount: string,
+ isJoin: boolean
): string {
const bptZeroPriceImpact = this.bptZeroPriceImpact(
pool,
tokenAmounts.map((a) => BigInt(a))
);
- return calcPriceImpact(BigInt(bptAmount), bptZeroPriceImpact).toString();
+ return calcPriceImpact(
+ BigInt(bptAmount),
+ bptZeroPriceImpact,
+ isJoin
+ ).toString();
}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.spec.ts
index 3a98513e0..437192fdd 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/metaStable/priceImpact.spec.ts
@@ -47,7 +47,8 @@ describe('metastable pool price impact', () => {
const priceImpact = priceImpactCalc.calcPriceImpact(
pool,
tokenAmounts.map((amount) => amount.toString()),
- '660816325116386208862285'
+ '660816325116386208862285',
+ true
);
expect(priceImpact.toString()).to.eq('3017427187914862');
});
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.integration.spec.ts
index aeabe9c48..7a15dc52a 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.integration.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/join.concern.integration.spec.ts
@@ -121,7 +121,8 @@ describe('join execution', async () => {
const minBPTOut = bptMinBalanceIncrease.toString();
const priceImpact = await controller.calcPriceImpact(
amountsIn,
- minBPTOut
+ minBPTOut,
+ true
);
expect(priceImpact).to.eql('100000444261607');
});
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.concern.ts
index c0a4ac7c7..32c99e5a9 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.concern.ts
@@ -61,12 +61,17 @@ export class StablePoolPriceImpact implements PriceImpactConcern {
calcPriceImpact(
pool: Pool,
tokenAmounts: string[],
- bptAmount: string
+ bptAmount: string,
+ isJoin: boolean
): string {
const bptZeroPriceImpact = this.bptZeroPriceImpact(
pool,
tokenAmounts.map((a) => BigInt(a))
);
- return calcPriceImpact(BigInt(bptAmount), bptZeroPriceImpact).toString();
+ return calcPriceImpact(
+ BigInt(bptAmount),
+ bptZeroPriceImpact,
+ isJoin
+ ).toString();
}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.spec.ts
index 4cb5246aa..e88f1f298 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/stable/priceImpact.spec.ts
@@ -49,7 +49,8 @@ describe('stable pool price impact', () => {
const priceImpact = priceImpactCalc.calcPriceImpact(
pool,
tokenAmounts,
- '109598303041827170846'
+ '109598303041827170846',
+ true
// this not the actual bptAmount that would result
// but it is still useful for testing purposes
);
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.concern.ts
index b65eb66b4..78eaff3d0 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.concern.ts
@@ -83,12 +83,17 @@ export class StablePhantomPriceImpact implements PriceImpactConcern {
calcPriceImpact(
pool: Pool,
tokenAmounts: string[],
- bptAmount: string
+ bptAmount: string,
+ isJoin: boolean
): string {
const bptZeroPriceImpact = this.bptZeroPriceImpact(
pool,
tokenAmounts.map((a) => BigInt(a))
);
- return calcPriceImpact(BigInt(bptAmount), bptZeroPriceImpact).toString();
+ return calcPriceImpact(
+ BigInt(bptAmount),
+ bptZeroPriceImpact,
+ isJoin
+ ).toString();
}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.spec.ts
index 293ce673d..e7ca2c576 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/stablePhantom/priceImpact.spec.ts
@@ -48,7 +48,8 @@ describe('phantomStable pool price impact', () => {
const priceImpact = priceImpactCalc.calcPriceImpact(
pool,
tokenAmounts.map((amount) => amount.toString()),
- '6300741387055771004078'
+ '6300741387055771004078',
+ true
);
expect(priceImpact.toString()).to.eq('1584599872926409');
});
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/types.ts b/balancer-js/src/modules/pools/pool-types/concerns/types.ts
index d7f02e6b3..abcc52bab 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/types.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/types.ts
@@ -7,7 +7,12 @@ export interface LiquidityConcern {
}
export interface SpotPriceConcern {
- calcPoolSpotPrice: (tokenIn: string, tokenOut: string, pool: Pool) => string;
+ calcPoolSpotPrice: (
+ tokenIn: string,
+ tokenOut: string,
+ pool: Pool,
+ isDefault?: boolean
+ ) => string;
}
export interface PriceImpactConcern {
@@ -15,7 +20,8 @@ export interface PriceImpactConcern {
calcPriceImpact: (
pool: Pool,
tokenAmounts: string[],
- bptAmount: string
+ bptAmount: string,
+ isJoin: boolean
) => string;
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.integration.spec.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.integration.spec.ts
index e54748587..e3ad383a6 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.integration.spec.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/join.concern.integration.spec.ts
@@ -119,7 +119,8 @@ describe('join execution', async () => {
const minBPTOut = bptMinBalanceIncrease.toString();
const priceImpact = await controller.calcPriceImpact(
amountsIn,
- minBPTOut
+ minBPTOut,
+ true
);
expect(priceImpact).to.eq('102055375201527');
});
@@ -138,59 +139,6 @@ describe('join execution', async () => {
});
});
- context('join transaction - join with params', () => {
- before(async function () {
- this.timeout(20000);
-
- amountsIn = tokensIn.map((t) =>
- parseFixed(t.balance, t.decimals).div(amountsInDiv).toString()
- );
-
- [bptBalanceBefore, ...tokensBalanceBefore] = await getBalances(
- [pool.address, ...pool.tokensList],
- signer,
- signerAddress
- );
-
- const slippage = '100';
- const { functionName, attributes, value, minBPTOut } =
- controller.buildJoin(
- signerAddress,
- tokensIn.map((t) => t.address),
- amountsIn,
- slippage
- );
- const transactionResponse = await sdk.contracts.vault
- .connect(signer)
- [functionName](...Object.values(attributes), { value });
- transactionReceipt = await transactionResponse.wait();
-
- bptMinBalanceIncrease = BigNumber.from(minBPTOut);
- [bptBalanceAfter, ...tokensBalanceAfter] = await getBalances(
- [pool.address, ...pool.tokensList],
- signer,
- signerAddress
- );
- });
-
- it('should work', async () => {
- expect(transactionReceipt.status).to.eql(1);
- });
-
- it('should increase BPT balance', async () => {
- expect(bptBalanceAfter.sub(bptBalanceBefore).gte(bptMinBalanceIncrease))
- .to.be.true;
- });
-
- it('should decrease tokens balance', async () => {
- for (let i = 0; i < tokensIn.length; i++) {
- expect(
- tokensBalanceBefore[i].sub(tokensBalanceAfter[i]).toString()
- ).to.equal(amountsIn[i]);
- }
- });
- });
-
context('join transaction - join with ETH', () => {
let transactionCost: BigNumber;
before(async function () {
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/liquidity.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/liquidity.concern.ts
index 466cbc6a9..ce7e9604a 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/liquidity.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/liquidity.concern.ts
@@ -16,7 +16,14 @@ export class WeightedPoolLiquidity implements LiquidityConcern {
continue;
}
- const price = parseFixed(token.price.usd.toString(), SCALING_FACTOR);
+ let price: BigNumber;
+ if (parseFloat(token.price.usd) < 1) {
+ price = parseFixed(parseFloat(token.price.usd).toFixed(10), 10).mul(
+ 1e8
+ );
+ } else {
+ price = parseFixed(token.price.usd, SCALING_FACTOR);
+ }
const balance = parseFixed(token.balance, SCALING_FACTOR);
const weight = parseFixed(token.weight || '0', SCALING_FACTOR);
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/priceImpact.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/priceImpact.concern.ts
index d73db42ef..bc9984fe5 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/priceImpact.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/priceImpact.concern.ts
@@ -57,12 +57,17 @@ export class WeightedPoolPriceImpact implements PriceImpactConcern {
calcPriceImpact(
pool: Pool,
tokenAmounts: string[],
- bptAmount: string
+ bptAmount: string,
+ isJoin: boolean
): string {
const bptZeroPriceImpact = this.bptZeroPriceImpact(
pool,
tokenAmounts.map((a) => BigInt(a))
);
- return calcPriceImpact(BigInt(bptAmount), bptZeroPriceImpact).toString();
+ return calcPriceImpact(
+ BigInt(bptAmount),
+ bptZeroPriceImpact,
+ isJoin
+ ).toString();
}
}
diff --git a/balancer-js/src/modules/pools/pool-types/concerns/weighted/spotPrice.concern.ts b/balancer-js/src/modules/pools/pool-types/concerns/weighted/spotPrice.concern.ts
index 6c0580a13..27c037be4 100644
--- a/balancer-js/src/modules/pools/pool-types/concerns/weighted/spotPrice.concern.ts
+++ b/balancer-js/src/modules/pools/pool-types/concerns/weighted/spotPrice.concern.ts
@@ -1,13 +1,35 @@
import { SpotPriceConcern } from '../types';
-import { SubgraphPoolBase, WeightedPool, ZERO } from '@balancer-labs/sor';
+import {
+ SubgraphPoolBase,
+ WeightedPool,
+ ZERO,
+ SubgraphToken,
+} from '@balancer-labs/sor';
import { Pool } from '@/types';
export class WeightedPoolSpotPrice implements SpotPriceConcern {
calcPoolSpotPrice(tokenIn: string, tokenOut: string, pool: Pool): string {
+ const isBPTAsToken = tokenIn === pool.address || tokenOut === pool.address;
+ if (isBPTAsToken) {
+ const bptAsToken: SubgraphToken = {
+ address: pool.address,
+ balance: pool.totalShares,
+ decimals: 18,
+ priceRate: '1',
+ weight: '0',
+ };
+ pool.tokens.push(bptAsToken);
+ pool.tokensList.push(pool.address);
+ }
const weightedPool = WeightedPool.fromPool(pool as SubgraphPoolBase);
const poolPairData = weightedPool.parsePoolPairData(tokenIn, tokenOut);
- return weightedPool
+ const spotPrice = weightedPool
._spotPriceAfterSwapExactTokenInForTokenOut(poolPairData, ZERO)
.toString();
+ if (isBPTAsToken) {
+ pool.tokens.pop();
+ pool.tokensList.pop();
+ }
+ return spotPrice;
}
}
diff --git a/balancer-js/src/modules/pricing/priceImpact.spec.ts b/balancer-js/src/modules/pricing/priceImpact.spec.ts
index beceae4d1..fb3a02a72 100644
--- a/balancer-js/src/modules/pricing/priceImpact.spec.ts
+++ b/balancer-js/src/modules/pricing/priceImpact.spec.ts
@@ -6,12 +6,12 @@ dotenv.config();
describe('priceImpact', () => {
it('zero price impact', async () => {
- const priceImpact = calcPriceImpact(BigInt(1e18), BigInt(1e18));
+ const priceImpact = calcPriceImpact(BigInt(1e18), BigInt(1e18), true);
expect(priceImpact.toString()).to.eq('0');
});
it('50% price impact', async () => {
- const priceImpact = calcPriceImpact(BigInt(1e18), BigInt(2e18));
+ const priceImpact = calcPriceImpact(BigInt(1e18), BigInt(2e18), true);
expect(priceImpact.toString()).to.eq('500000000000000000');
});
diff --git a/balancer-js/src/modules/pricing/priceImpact.ts b/balancer-js/src/modules/pricing/priceImpact.ts
index 2117def03..d28c125a1 100644
--- a/balancer-js/src/modules/pricing/priceImpact.ts
+++ b/balancer-js/src/modules/pricing/priceImpact.ts
@@ -1,9 +1,25 @@
import { ONE, SolidityMaths } from '@/lib/utils/solidityMaths';
export function calcPriceImpact(
+ bptAmount: bigint,
+ bptZeroPriceImpact: bigint,
+ isJoin: boolean
+): bigint {
+ if (isJoin) return calcPriceImpactJoin(bptAmount, bptZeroPriceImpact);
+ else return calcPriceImpactExit(bptAmount, bptZeroPriceImpact);
+}
+
+function calcPriceImpactJoin(
bptAmount: bigint,
bptZeroPriceImpact: bigint
): bigint {
// 1 - (bptAmount/bptZeroPI)
return ONE - SolidityMaths.divDownFixed(bptAmount, bptZeroPriceImpact);
}
+function calcPriceImpactExit(
+ bptAmount: bigint,
+ bptZeroPriceImpact: bigint
+): bigint {
+ // (bptAmount/bptZeroPI) - 1
+ return SolidityMaths.divDownFixed(bptAmount, bptZeroPriceImpact) - ONE;
+}
diff --git a/balancer-js/src/modules/relayer/relayer.module.ts b/balancer-js/src/modules/relayer/relayer.module.ts
index 172cc7062..9cd73cb88 100644
--- a/balancer-js/src/modules/relayer/relayer.module.ts
+++ b/balancer-js/src/modules/relayer/relayer.module.ts
@@ -1,19 +1,28 @@
+import { JsonRpcSigner } from '@ethersproject/providers';
import { BigNumberish, BigNumber } from '@ethersproject/bignumber';
import { Interface } from '@ethersproject/abi';
import { MaxUint256, WeiPerEther, Zero } from '@ethersproject/constants';
+import { Vault } from '@balancer-labs/typechain';
import { Swaps } from '@/modules/swaps/swaps.module';
import { BalancerError, BalancerErrorCode } from '@/balancerErrors';
import {
EncodeBatchSwapInput,
+ EncodeWrapAaveDynamicTokenInput,
EncodeUnwrapAaveStaticTokenInput,
OutputReference,
EncodeExitPoolInput,
+ EncodeJoinPoolInput,
ExitAndBatchSwapInput,
ExitPoolData,
- EncodeJoinPoolInput,
+ JoinPoolData,
} from './types';
-import { TransactionData, ExitPoolRequest, BalancerSdkConfig } from '@/types';
+import {
+ TransactionData,
+ ExitPoolRequest,
+ JoinPoolRequest,
+ BalancerSdkConfig,
+} from '@/types';
import {
SwapType,
FundManagement,
@@ -21,14 +30,14 @@ import {
FetchPoolsInput,
} from '../swaps/types';
import { SubgraphPoolBase } from '@balancer-labs/sor';
+import { RelayerAuthorization } from '@/lib/utils';
import relayerLibraryAbi from '@/lib/abi/BatchRelayerLibrary.json';
-import aaveWrappingAbi from '@/lib/abi/AaveWrapping.json';
-
-const relayerLibrary = new Interface(relayerLibraryAbi);
export * from './types';
+const relayerLibrary = new Interface(relayerLibraryAbi);
+
export class Relayer {
private readonly swaps: Swaps;
@@ -43,6 +52,20 @@ export class Relayer {
}
}
+ /**
+ * Returns true if `amount` is not actually an amount, but rather a chained reference.
+ */
+ static isChainedReference(amount: string): boolean {
+ const amountBn = BigNumber.from(amount);
+ const mask = BigNumber.from(
+ '0xfff0000000000000000000000000000000000000000000000000000000000000'
+ );
+ const readonly =
+ '0xba10000000000000000000000000000000000000000000000000000000000000';
+ const check = amountBn.toBigInt() & mask.toBigInt();
+ return readonly === BigNumber.from(check)._hex.toString();
+ }
+
static encodeApproveVault(tokenAddress: string, maxAmount: string): string {
return relayerLibrary.encodeFunctionData('approveVault', [
tokenAddress,
@@ -126,12 +149,23 @@ export class Relayer {
]);
}
+ static encodeWrapAaveDynamicToken(
+ params: EncodeWrapAaveDynamicTokenInput
+ ): string {
+ return relayerLibrary.encodeFunctionData('wrapAaveDynamicToken', [
+ params.staticToken,
+ params.sender,
+ params.recipient,
+ params.amount,
+ params.fromUnderlying,
+ params.outputReference,
+ ]);
+ }
+
static encodeUnwrapAaveStaticToken(
params: EncodeUnwrapAaveStaticTokenInput
): string {
- const aaveWrappingLibrary = new Interface(aaveWrappingAbi);
-
- return aaveWrappingLibrary.encodeFunctionData('unwrapAaveStaticToken', [
+ return relayerLibrary.encodeFunctionData('unwrapAaveStaticToken', [
params.staticToken,
params.sender,
params.recipient,
@@ -141,6 +175,12 @@ export class Relayer {
]);
}
+ static encodePeekChainedReferenceValue(reference: BigNumberish): string {
+ return relayerLibrary.encodeFunctionData('peekChainedReferenceValue', [
+ reference,
+ ]);
+ }
+
static toChainedReference(key: BigNumberish, isTemporary = true): BigNumber {
const prefix = isTemporary
? Relayer.CHAINED_REFERENCE_TEMP_PREFIX
@@ -192,8 +232,38 @@ export class Relayer {
return exitEncoded;
}
- static constructJoinCall(params: EncodeJoinPoolInput): string {
- const joinEncoded = Relayer.encodeJoinPool(params);
+ static constructJoinCall(params: JoinPoolData): string {
+ const {
+ assets,
+ maxAmountsIn,
+ userData,
+ fromInternalBalance,
+ poolId,
+ kind,
+ sender,
+ recipient,
+ value,
+ outputReference,
+ } = params;
+
+ const joinPoolRequest: JoinPoolRequest = {
+ assets,
+ maxAmountsIn,
+ userData,
+ fromInternalBalance,
+ };
+
+ const joinPoolInput: EncodeJoinPoolInput = {
+ poolId,
+ kind,
+ sender,
+ recipient,
+ value,
+ outputReference,
+ joinPoolRequest,
+ };
+
+ const joinEncoded = Relayer.encodeJoinPool(joinPoolInput);
return joinEncoded;
}
@@ -558,7 +628,35 @@ export class Relayer {
value: '0',
outputReferences: outputReferences,
});
-
return [encodedBatchSwap, ...unwrapCalls];
}
+
+ static signRelayerApproval = async (
+ relayerAddress: string,
+ signerAddress: string,
+ signer: JsonRpcSigner,
+ vault: Vault
+ ): Promise => {
+ const approval = vault.interface.encodeFunctionData('setRelayerApproval', [
+ signerAddress,
+ relayerAddress,
+ true,
+ ]);
+
+ const signature =
+ await RelayerAuthorization.signSetRelayerApprovalAuthorization(
+ vault,
+ signer,
+ relayerAddress,
+ approval
+ );
+
+ const calldata = RelayerAuthorization.encodeCalldataAuthorization(
+ '0x',
+ MaxUint256,
+ signature
+ );
+
+ return calldata;
+ };
}
diff --git a/balancer-js/src/modules/relayer/types.ts b/balancer-js/src/modules/relayer/types.ts
index fd71d64fb..faa983c42 100644
--- a/balancer-js/src/modules/relayer/types.ts
+++ b/balancer-js/src/modules/relayer/types.ts
@@ -43,6 +43,15 @@ export interface EncodeJoinPoolInput {
outputReference: string;
}
+export interface EncodeWrapAaveDynamicTokenInput {
+ staticToken: string;
+ sender: string;
+ recipient: string;
+ amount: BigNumberish;
+ fromUnderlying: boolean;
+ outputReference: BigNumberish;
+}
+
export interface EncodeUnwrapAaveStaticTokenInput {
staticToken: string;
sender: string;
@@ -65,3 +74,4 @@ export interface ExitAndBatchSwapInput {
}
export type ExitPoolData = ExitPoolRequest & EncodeExitPoolInput;
+export type JoinPoolData = JoinPoolRequest & EncodeJoinPoolInput;
diff --git a/balancer-js/src/modules/sdk.helpers.ts b/balancer-js/src/modules/sdk.helpers.ts
index 842e22efb..81e55ee71 100644
--- a/balancer-js/src/modules/sdk.helpers.ts
+++ b/balancer-js/src/modules/sdk.helpers.ts
@@ -12,6 +12,7 @@ export function getNetworkConfig(
...networkConfig.urls,
subgraph: config.customSubgraphUrl ?? networkConfig.urls.subgraph,
},
+ tenderly: config.tenderly,
};
}
@@ -21,5 +22,6 @@ export function getNetworkConfig(
...config.network.urls,
subgraph: config.customSubgraphUrl ?? config.network.urls.subgraph,
},
+ tenderly: config.network.tenderly,
};
}
diff --git a/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql b/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql
index f9506a243..1fa6b8b92 100644
--- a/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql
+++ b/balancer-js/src/modules/subgraph/balancer-v2/Pools.graphql
@@ -114,6 +114,9 @@ fragment SubgraphPool on Pool {
holdersCount
tokensList
amp
+ priceRateProviders(first: 100) {
+ ...SubgraphPriceRateProvider
+ }
expiryTime
unitSeconds
createTime
@@ -167,12 +170,68 @@ fragment SubgraphPoolToken on PoolToken {
priceRate
isExemptFromYieldProtocolFee
token {
+ ...TokenTree
+ }
+}
+
+
+fragment SubgraphSubPoolToken on PoolToken {
+ address
+ balance
+ weight
+ priceRate
+ symbol
+ decimals
+ isExemptFromYieldProtocolFee
+}
+
+fragment TokenAttrs on Token {
+ address
+ symbol
+ decimals
+}
+
+fragment SubgraphSubPool on Pool {
+ id
+ totalShares
+ address
+ poolType
+ mainIndex
+}
+
+fragment TokenTree on Token {
+ latestUSDPrice
+ pool {
+ ...SubgraphSubPool
+ tokens {
+ ...SubgraphSubPoolToken
+ token {
+ latestUSDPrice
pool {
- poolType
+ ...SubgraphSubPool
+ tokens {
+ ...SubgraphSubPoolToken
+ token {
+ latestUSDPrice
+ pool {
+ ...SubgraphSubPool
+ }
+ }
+ }
}
+ }
+ }
+ }
+}
+
+fragment SubgraphPriceRateProvider on PriceRateProvider {
+ address,
+ token {
+ address
}
}
+
query PoolHistoricalLiquidities(
$skip: Int
$first: Int
diff --git a/balancer-js/src/modules/subgraph/examples/pool-joinExit.ts b/balancer-js/src/modules/subgraph/examples/pool-joinExit.ts
new file mode 100644
index 000000000..9b3bb12a0
--- /dev/null
+++ b/balancer-js/src/modules/subgraph/examples/pool-joinExit.ts
@@ -0,0 +1,24 @@
+import dotenv from "dotenv";
+import {createSubgraphClient} from "@/modules/subgraph/subgraph";
+
+dotenv.config();
+
+(async function() {
+
+ const POOL_ID = '0x0297e37f1873d2dab4487aa67cd56b58e2f27875000100000000000000000002'
+ const subgraph_url = "https://api.thegraph.com/subgraphs/name/balancer-labs/balancer-polygon-v2"
+ const client = createSubgraphClient(`${subgraph_url}`);
+
+ const poolQuery = await client.Pool({ id: POOL_ID});
+ console.log(`${poolQuery.pool?.tokens?.map((token) => `${token.symbol}\t${token.weight}`).join("\n")}`)
+ let result = await client.JoinExits({ where: { pool: POOL_ID } });
+
+ const userId = result.joinExits.sort((a, b) => a.timestamp - b.timestamp)[0].user.id;
+ console.log(`${userId}`)
+ result = await client.JoinExits({ where: { sender: userId, pool: POOL_ID } });
+ result.joinExits.sort((a, b) => a.timestamp - b.timestamp).forEach((item) => console.log(`${item.id}\t${new Date(item.timestamp * 1000).toLocaleString()}\t${item.type}\t${item.amounts}`))
+
+
+})();
+
+// npm run examples:run -- ./src/modules/subgraph/examples/pool-joinExit.ts
\ No newline at end of file
diff --git a/balancer-js/src/modules/subgraph/generated/balancer-subgraph-schema.graphql b/balancer-js/src/modules/subgraph/generated/balancer-subgraph-schema.graphql
index a9aec0379..eb1d9bbd4 100644
--- a/balancer-js/src/modules/subgraph/generated/balancer-subgraph-schema.graphql
+++ b/balancer-js/src/modules/subgraph/generated/balancer-subgraph-schema.graphql
@@ -748,6 +748,50 @@ type Pool {
z: BigDecimal
}
+type PoolContract {
+ id: ID!
+ pool: Pool!
+}
+
+input PoolContract_filter {
+ """Filter for the block changed event."""
+ _change_block: BlockChangedFilter
+ id: ID
+ id_gt: ID
+ id_gte: ID
+ id_in: [ID!]
+ id_lt: ID
+ id_lte: ID
+ id_not: ID
+ id_not_in: [ID!]
+ pool: String
+ pool_: Pool_filter
+ pool_contains: String
+ pool_contains_nocase: String
+ pool_ends_with: String
+ pool_ends_with_nocase: String
+ pool_gt: String
+ pool_gte: String
+ pool_in: [String!]
+ pool_lt: String
+ pool_lte: String
+ pool_not: String
+ pool_not_contains: String
+ pool_not_contains_nocase: String
+ pool_not_ends_with: String
+ pool_not_ends_with_nocase: String
+ pool_not_in: [String!]
+ pool_not_starts_with: String
+ pool_not_starts_with_nocase: String
+ pool_starts_with: String
+ pool_starts_with_nocase: String
+}
+
+enum PoolContract_orderBy {
+ id
+ pool
+}
+
type PoolHistoricalLiquidity {
block: BigInt!
id: ID!
@@ -2127,6 +2171,34 @@ type Query {
"""
subgraphError: _SubgraphErrorPolicy_! = deny
): Pool
+ poolContract(
+ """
+ The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
+ """
+ block: Block_height
+ id: ID!
+
+ """
+ Set to `allow` to receive data even if the subgraph has skipped over errors while syncing.
+ """
+ subgraphError: _SubgraphErrorPolicy_! = deny
+ ): PoolContract
+ poolContracts(
+ """
+ The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
+ """
+ block: Block_height
+ first: Int = 100
+ orderBy: PoolContract_orderBy
+ orderDirection: OrderDirection
+ skip: Int = 0
+
+ """
+ Set to `allow` to receive data even if the subgraph has skipped over errors while syncing.
+ """
+ subgraphError: _SubgraphErrorPolicy_! = deny
+ where: PoolContract_filter
+ ): [PoolContract!]!
poolHistoricalLiquidities(
"""
The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
@@ -2748,6 +2820,34 @@ type Subscription {
"""
subgraphError: _SubgraphErrorPolicy_! = deny
): Pool
+ poolContract(
+ """
+ The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
+ """
+ block: Block_height
+ id: ID!
+
+ """
+ Set to `allow` to receive data even if the subgraph has skipped over errors while syncing.
+ """
+ subgraphError: _SubgraphErrorPolicy_! = deny
+ ): PoolContract
+ poolContracts(
+ """
+ The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
+ """
+ block: Block_height
+ first: Int = 100
+ orderBy: PoolContract_orderBy
+ orderDirection: OrderDirection
+ skip: Int = 0
+
+ """
+ Set to `allow` to receive data even if the subgraph has skipped over errors while syncing.
+ """
+ subgraphError: _SubgraphErrorPolicy_! = deny
+ where: PoolContract_filter
+ ): [PoolContract!]!
poolHistoricalLiquidities(
"""
The block at which the query should be executed. Can either be a `{ hash: Bytes }` value containing a block hash, a `{ number: Int }` containing the block number, or a `{ number_gte: Int }` containing the minimum block number. In the case of `number_gte`, the query will be executed on the latest block only if the subgraph has progressed to or past the minimum block number. Defaults to the latest block when omitted.
diff --git a/balancer-js/src/modules/subgraph/generated/balancer-subgraph-types.ts b/balancer-js/src/modules/subgraph/generated/balancer-subgraph-types.ts
index c9c1750e8..d99a4dd78 100644
--- a/balancer-js/src/modules/subgraph/generated/balancer-subgraph-types.ts
+++ b/balancer-js/src/modules/subgraph/generated/balancer-subgraph-types.ts
@@ -829,6 +829,51 @@ export type PoolWeightUpdatesArgs = {
where?: InputMaybe;
};
+export type PoolContract = {
+ __typename?: 'PoolContract';
+ id: Scalars['ID'];
+ pool: Pool;
+};
+
+export type PoolContract_Filter = {
+ /** Filter for the block changed event. */
+ _change_block?: InputMaybe;
+ id?: InputMaybe;
+ id_gt?: InputMaybe;
+ id_gte?: InputMaybe;
+ id_in?: InputMaybe>;
+ id_lt?: InputMaybe;
+ id_lte?: InputMaybe;
+ id_not?: InputMaybe;
+ id_not_in?: InputMaybe>;
+ pool?: InputMaybe;
+ pool_?: InputMaybe;
+ pool_contains?: InputMaybe;
+ pool_contains_nocase?: InputMaybe;
+ pool_ends_with?: InputMaybe;
+ pool_ends_with_nocase?: InputMaybe;
+ pool_gt?: InputMaybe;
+ pool_gte?: InputMaybe;
+ pool_in?: InputMaybe>;
+ pool_lt?: InputMaybe;
+ pool_lte?: InputMaybe;
+ pool_not?: InputMaybe;
+ pool_not_contains?: InputMaybe;
+ pool_not_contains_nocase?: InputMaybe;
+ pool_not_ends_with?: InputMaybe;
+ pool_not_ends_with_nocase?: InputMaybe;
+ pool_not_in?: InputMaybe>;
+ pool_not_starts_with?: InputMaybe;
+ pool_not_starts_with_nocase?: InputMaybe;
+ pool_starts_with?: InputMaybe;
+ pool_starts_with_nocase?: InputMaybe;
+};
+
+export enum PoolContract_OrderBy {
+ Id = 'id',
+ Pool = 'pool'
+}
+
export type PoolHistoricalLiquidity = {
__typename?: 'PoolHistoricalLiquidity';
block: Scalars['BigInt'];
@@ -2030,6 +2075,8 @@ export type Query = {
managementOperation?: Maybe;
managementOperations: Array;
pool?: Maybe;
+ poolContract?: Maybe;
+ poolContracts: Array;
poolHistoricalLiquidities: Array;
poolHistoricalLiquidity?: Maybe;
poolShare?: Maybe;
@@ -2200,6 +2247,24 @@ export type QueryPoolArgs = {
};
+export type QueryPoolContractArgs = {
+ block?: InputMaybe;
+ id: Scalars['ID'];
+ subgraphError?: _SubgraphErrorPolicy_;
+};
+
+
+export type QueryPoolContractsArgs = {
+ block?: InputMaybe;
+ first?: InputMaybe;
+ orderBy?: InputMaybe;
+ orderDirection?: InputMaybe;
+ skip?: InputMaybe;
+ subgraphError?: _SubgraphErrorPolicy_;
+ where?: InputMaybe;
+};
+
+
export type QueryPoolHistoricalLiquiditiesArgs = {
block?: InputMaybe;
first?: InputMaybe;
@@ -2481,6 +2546,8 @@ export type Subscription = {
managementOperation?: Maybe;
managementOperations: Array;
pool?: Maybe;
+ poolContract?: Maybe;
+ poolContracts: Array;
poolHistoricalLiquidities: Array;
poolHistoricalLiquidity?: Maybe;
poolShare?: Maybe;
@@ -2651,6 +2718,24 @@ export type SubscriptionPoolArgs = {
};
+export type SubscriptionPoolContractArgs = {
+ block?: InputMaybe;
+ id: Scalars['ID'];
+ subgraphError?: _SubgraphErrorPolicy_;
+};
+
+
+export type SubscriptionPoolContractsArgs = {
+ block?: InputMaybe;
+ first?: InputMaybe;
+ orderBy?: InputMaybe;
+ orderDirection?: InputMaybe;
+ skip?: InputMaybe;
+ subgraphError?: _SubgraphErrorPolicy_;
+ where?: InputMaybe;
+};
+
+
export type SubscriptionPoolHistoricalLiquiditiesArgs = {
block?: InputMaybe;
first?: InputMaybe;
@@ -3953,7 +4038,7 @@ export type PoolsQueryVariables = Exact<{
}>;
-export type PoolsQuery = { __typename?: 'Query', pools: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null }> };
+export type PoolsQuery = { __typename?: 'Query', pools: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null }> };
export type AllPoolsQueryVariables = Exact<{
skip?: InputMaybe;
@@ -3965,7 +4050,7 @@ export type AllPoolsQueryVariables = Exact<{
}>;
-export type AllPoolsQuery = { __typename?: 'Query', pool0: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null }>, pool1000: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null }>, pool2000: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null }> };
+export type AllPoolsQuery = { __typename?: 'Query', pool0: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null }>, pool1000: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null }>, pool2000: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null }> };
export type PoolQueryVariables = Exact<{
id: Scalars['ID'];
@@ -3973,7 +4058,7 @@ export type PoolQueryVariables = Exact<{
}>;
-export type PoolQuery = { __typename?: 'Query', pool?: { __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null } | null };
+export type PoolQuery = { __typename?: 'Query', pool?: { __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null } | null };
export type PoolsWithoutLinearQueryVariables = Exact<{
skip?: InputMaybe;
@@ -3985,7 +4070,7 @@ export type PoolsWithoutLinearQueryVariables = Exact<{
}>;
-export type PoolsWithoutLinearQuery = { __typename?: 'Query', pools: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null }> };
+export type PoolsWithoutLinearQuery = { __typename?: 'Query', pools: Array<{ __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null }> };
export type PoolWithoutLinearQueryVariables = Exact<{
id: Scalars['ID'];
@@ -3993,13 +4078,23 @@ export type PoolWithoutLinearQueryVariables = Exact<{
}>;
-export type PoolWithoutLinearQuery = { __typename?: 'Query', pool?: { __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null } | null };
+export type PoolWithoutLinearQuery = { __typename?: 'Query', pool?: { __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null } | null };
+
+export type SubgraphPoolFragment = { __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null, priceRateProviders?: Array<{ __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } }> | null };
+
+export type SubgraphPoolWithoutLinearFragment = { __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } }> | null };
+
+export type SubgraphPoolTokenFragment = { __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null } };
-export type SubgraphPoolFragment = { __typename?: 'Pool', id: string, address: string, poolType?: string | null, poolTypeVersion?: number | null, factory?: string | null, strategyType: number, symbol?: string | null, name?: string | null, swapEnabled: boolean, swapFee: string, protocolYieldFeeCache?: string | null, owner?: string | null, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, createTime: number, principalToken?: string | null, baseToken?: string | null, wrappedIndex?: number | null, mainIndex?: number | null, lowerTarget?: string | null, upperTarget?: string | null, sqrtAlpha?: string | null, sqrtBeta?: string | null, root3Alpha?: string | null, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null };
+export type SubgraphSubPoolTokenFragment = { __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null };
-export type SubgraphPoolWithoutLinearFragment = { __typename?: 'Pool', id: string, address: string, poolType?: string | null, symbol?: string | null, name?: string | null, swapFee: string, totalWeight?: string | null, totalSwapVolume: string, totalSwapFee: string, totalLiquidity: string, totalShares: string, swapsCount: string, holdersCount: string, tokensList: Array, amp?: string | null, expiryTime?: string | null, unitSeconds?: string | null, principalToken?: string | null, baseToken?: string | null, swapEnabled: boolean, tokens?: Array<{ __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } }> | null };
+export type TokenAttrsFragment = { __typename?: 'Token', address: string, symbol?: string | null, decimals: number };
-export type SubgraphPoolTokenFragment = { __typename?: 'PoolToken', id: string, symbol: string, name: string, decimals: number, address: string, balance: string, managedBalance: string, weight?: string | null, priceRate: string, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', pool?: { __typename?: 'Pool', poolType?: string | null } | null } };
+export type SubgraphSubPoolFragment = { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null };
+
+export type TokenTreeFragment = { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null, tokens?: Array<{ __typename?: 'PoolToken', address: string, balance: string, weight?: string | null, priceRate: string, symbol: string, decimals: number, isExemptFromYieldProtocolFee?: boolean | null, token: { __typename?: 'Token', latestUSDPrice?: string | null, pool?: { __typename?: 'Pool', id: string, totalShares: string, address: string, poolType?: string | null, mainIndex?: number | null } | null } }> | null } | null } }> | null } | null };
+
+export type SubgraphPriceRateProviderFragment = { __typename?: 'PriceRateProvider', address: string, token: { __typename?: 'PoolToken', address: string } };
export type PoolHistoricalLiquiditiesQueryVariables = Exact<{
skip?: InputMaybe;
@@ -4125,6 +4220,53 @@ export const SubgraphPoolShareFragmentDoc = gql`
}
}
`;
+export const SubgraphSubPoolFragmentDoc = gql`
+ fragment SubgraphSubPool on Pool {
+ id
+ totalShares
+ address
+ poolType
+ mainIndex
+}
+ `;
+export const SubgraphSubPoolTokenFragmentDoc = gql`
+ fragment SubgraphSubPoolToken on PoolToken {
+ address
+ balance
+ weight
+ priceRate
+ symbol
+ decimals
+ isExemptFromYieldProtocolFee
+}
+ `;
+export const TokenTreeFragmentDoc = gql`
+ fragment TokenTree on Token {
+ latestUSDPrice
+ pool {
+ ...SubgraphSubPool
+ tokens {
+ ...SubgraphSubPoolToken
+ token {
+ latestUSDPrice
+ pool {
+ ...SubgraphSubPool
+ tokens {
+ ...SubgraphSubPoolToken
+ token {
+ latestUSDPrice
+ pool {
+ ...SubgraphSubPool
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
+ ${SubgraphSubPoolFragmentDoc}
+${SubgraphSubPoolTokenFragmentDoc}`;
export const SubgraphPoolTokenFragmentDoc = gql`
fragment SubgraphPoolToken on PoolToken {
id
@@ -4138,9 +4280,15 @@ export const SubgraphPoolTokenFragmentDoc = gql`
priceRate
isExemptFromYieldProtocolFee
token {
- pool {
- poolType
- }
+ ...TokenTree
+ }
+}
+ ${TokenTreeFragmentDoc}`;
+export const SubgraphPriceRateProviderFragmentDoc = gql`
+ fragment SubgraphPriceRateProvider on PriceRateProvider {
+ address
+ token {
+ address
}
}
`;
@@ -4170,6 +4318,9 @@ export const SubgraphPoolFragmentDoc = gql`
holdersCount
tokensList
amp
+ priceRateProviders(first: 100) {
+ ...SubgraphPriceRateProvider
+ }
expiryTime
unitSeconds
createTime
@@ -4183,7 +4334,8 @@ export const SubgraphPoolFragmentDoc = gql`
sqrtBeta
root3Alpha
}
- ${SubgraphPoolTokenFragmentDoc}`;
+ ${SubgraphPoolTokenFragmentDoc}
+${SubgraphPriceRateProviderFragmentDoc}`;
export const SubgraphPoolWithoutLinearFragmentDoc = gql`
fragment SubgraphPoolWithoutLinear on Pool {
id
@@ -4212,6 +4364,13 @@ export const SubgraphPoolWithoutLinearFragmentDoc = gql`
swapEnabled
}
${SubgraphPoolTokenFragmentDoc}`;
+export const TokenAttrsFragmentDoc = gql`
+ fragment TokenAttrs on Token {
+ address
+ symbol
+ decimals
+}
+ `;
export const SubgraphPoolSnapshotFragmentDoc = gql`
fragment SubgraphPoolSnapshot on PoolSnapshot {
id
diff --git a/balancer-js/src/modules/swaps/joinAndExit.ts b/balancer-js/src/modules/swaps/joinAndExit.ts
index f9a78b5f8..78b6bfb95 100644
--- a/balancer-js/src/modules/swaps/joinAndExit.ts
+++ b/balancer-js/src/modules/swaps/joinAndExit.ts
@@ -887,7 +887,7 @@ function buildJoinCall(
// console.log(attributes);
- const callData = Relayer.constructJoinCall(attributes);
+ const callData = Relayer.encodeJoinPool(attributes);
// These are used for final amount check
const amountOut = action.hasTokenOut ? bptAmountOut : '0';
const amountIn = action.hasTokenIn ? maxAmountsIn[joinTokenIndex] : '0';
diff --git a/balancer-js/src/test/factories/data.ts b/balancer-js/src/test/factories/data.ts
index ec3487970..81267b072 100644
--- a/balancer-js/src/test/factories/data.ts
+++ b/balancer-js/src/test/factories/data.ts
@@ -1,7 +1,9 @@
/* eslint-disable @typescript-eslint/explicit-module-boundary-types */
/* eslint-disable @typescript-eslint/no-unused-vars */
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
import { BALANCER_NETWORK_CONFIG } from '@/lib/constants/config';
-import { PoolSharesRepository } from '@/modules/data';
+import { PoolJoinExitRepository, PoolSharesRepository } from '@/modules/data';
import {
BalancerDataRepositories,
Findable,
@@ -13,24 +15,28 @@ import {
Token,
} from '@/types';
-export const findable = (
- map: Map
+export const findable = (
+ map: Map
): Findable & Searchable => ({
find: (id: string) => Promise.resolve(map.get(id)),
- findBy: (param: P, value: string) => Promise.resolve(map.get(value)),
+ findBy: (param: P, value: V) => Promise.resolve(map.get(value)),
all: () => Promise.resolve(Object.values(map)),
where: (filters: (arg: T) => boolean) => Promise.resolve(Object.values(map)),
});
-export const stubbed = (
+export const stubbed = (
value: unknown
-): Findable & Searchable => ({
+): Findable & Searchable => ({
find: (id: string) => Promise.resolve(value as T),
- findBy: (param: P, _: string) => Promise.resolve(value as T),
+ findBy: (param: P, _: V) => Promise.resolve(value as T),
all: () => Promise.resolve([value as T]),
where: (filters: (arg: T) => boolean) => Promise.resolve([value as T]),
});
+export const aaveRates = {
+ getRate: (address: string) => Promise.resolve(1),
+};
+
interface IdentifiableArray {
id: number;
}
@@ -47,6 +53,7 @@ export const repositores = ({
pools = stubbed(undefined),
yesterdaysPools = stubbed(undefined),
tokenPrices = stubbed({ usd: '1' }),
+ tokenHistoricalPrices = stubbed({ usd: '1' }),
tokenMeta = stubbed({ decimals: 18 }),
liquidityGauges = stubbed(undefined),
feeDistributor = {
@@ -65,14 +72,20 @@ export const repositores = ({
BALANCER_NETWORK_CONFIG[Network.MAINNET].urls.subgraph,
Network.MAINNET
),
+ poolJoinExits = new PoolJoinExitRepository(
+ BALANCER_NETWORK_CONFIG[Network.MAINNET].urls.subgraph,
+ Network.MAINNET
+ ),
}): BalancerDataRepositories => ({
pools,
yesterdaysPools,
tokenPrices,
+ tokenHistoricalPrices,
tokenMeta,
liquidityGauges,
feeDistributor,
feeCollector,
tokenYields,
poolShares,
+ poolJoinExits,
});
diff --git a/balancer-js/src/test/factories/index.ts b/balancer-js/src/test/factories/index.ts
index 8e86224dc..11f0d81aa 100644
--- a/balancer-js/src/test/factories/index.ts
+++ b/balancer-js/src/test/factories/index.ts
@@ -1,7 +1,8 @@
import * as sor from './sor';
+import * as pools from './pools';
import * as sdk from './sdk';
import * as data from './data';
-const factories = { ...sor, ...sdk, data };
+const factories = { ...sor, ...pools, ...sdk, data };
export { factories };
diff --git a/balancer-js/src/test/factories/named-tokens.ts b/balancer-js/src/test/factories/named-tokens.ts
index 34c91dfb8..ec01e47b4 100644
--- a/balancer-js/src/test/factories/named-tokens.ts
+++ b/balancer-js/src/test/factories/named-tokens.ts
@@ -16,6 +16,42 @@ export const namedTokens: Record = {
address: '0x3472a5a71965499acd81997a54bba8d852c6e53d',
decimals: 18,
},
+ DAI: {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ decimals: 18,
+ },
+ aDAI: {
+ address: '0x02d60b84491589974263d922d9cc7a3152618ef6',
+ decimals: 18,
+ },
+ bDAI: {
+ address: '0x804cdb9116a10bb78768d3252355a1b18067bf8f',
+ decimals: 18,
+ },
+ USDC: {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ decimals: 6,
+ },
+ aUSDC: {
+ address: '0xd093fa4fb80d09bb30817fdcd442d4d02ed3e5de',
+ decimals: 18,
+ },
+ bUSDC: {
+ address: '0x9210f1204b5a24742eba12f710636d76240df3d0',
+ decimals: 18,
+ },
+ USDT: {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ decimals: 6,
+ },
+ aUSDT: {
+ address: '0xf8fd466f12e236f4c96f7cce6c79eadb819abf58',
+ decimals: 18,
+ },
+ bUSDT: {
+ address: '0x2bbf681cc4eb09218bee85ea2a5d3d13fa40fc0c',
+ decimals: 18,
+ },
BAL: {
address: '0xba100000625a3754423978a60c9317c58a424e3D'.toLowerCase(),
decimals: 18,
diff --git a/balancer-js/src/test/factories/pools.ts b/balancer-js/src/test/factories/pools.ts
new file mode 100644
index 000000000..f0181c6ba
--- /dev/null
+++ b/balancer-js/src/test/factories/pools.ts
@@ -0,0 +1,397 @@
+import { Factory } from 'fishery';
+import { SubgraphPoolBase, SubgraphToken } from '@balancer-labs/sor';
+import { BigNumber, formatFixed, parseFixed } from '@ethersproject/bignumber';
+
+import { subgraphToken, subgraphPoolBase } from './sor';
+import { formatAddress, formatId } from '../lib/utils';
+import { Zero, WeiPerEther } from '@ethersproject/constants';
+
+type LinearTokens = {
+ wrappedSymbol: string;
+ mainSymbol: string;
+};
+
+export type LinearParams = {
+ pools: {
+ tokens: LinearTokens;
+ balance: string;
+ }[];
+ parentsProportion?: string;
+};
+
+export interface Pool extends SubgraphPoolBase {
+ proportionOfParent: string;
+}
+
+export type LinearInfo = {
+ linearPools: Pool[];
+ mainTokens: SubgraphToken[];
+ wrappedTokens: SubgraphToken[];
+ linearPoolTokens: SubgraphToken[];
+};
+
+export interface ComposableStableParams {
+ id: string;
+ symbol: string;
+ address: string;
+ childTokens: SubgraphToken[];
+ tokenbalance: string;
+}
+
+export type ComposableStableInfo = {
+ pool: Pool;
+ poolToken: SubgraphToken;
+};
+
+export interface BoostedParams {
+ linearPoolsParams: LinearParams;
+ rootId: string;
+ rootAddress: string;
+ rootBalance: string;
+ parentsProportion?: string;
+}
+
+export interface BoostedInfo extends LinearInfo {
+ rootPool: Pool;
+ rootPoolToken: SubgraphToken;
+}
+
+export interface BoostedMetaParams {
+ childBoostedParams: BoostedParams;
+ childLinearParam: LinearParams;
+ rootId: string;
+ rootAddress: string;
+ rootBalance: string;
+}
+
+export interface ChildBoostedInfo extends BoostedInfo {
+ proportion: string;
+}
+
+export interface BoostedMetaInfo {
+ rootInfo: ComposableStableInfo;
+ childBoostedInfo: ChildBoostedInfo;
+ childLinearInfo: LinearInfo;
+}
+
+export interface BoostedMetaBigParams {
+ rootId: string;
+ rootAddress: string;
+ rootBalance: string;
+ childPools: BoostedParams[];
+}
+
+export interface BoostedMetaBigInfo {
+ rootPool: Pool;
+ rootPoolToken: SubgraphToken;
+ childPoolsInfo: ChildBoostedInfo[];
+ childPools: Pool[];
+}
+
+/*
+Create a set of Linear pools and associated tokens:
+LinearPools consisting of wrappedToken, mainToken, composableBpt
+*/
+const linearPools = Factory.define(
+ ({ transientParams }) => {
+ const { pools, parentsProportion: proportionOfParent = '1' } =
+ transientParams;
+ if (pools === undefined) throw new Error('Need linear pool params');
+ const linearPools: Pool[] = [];
+ const mainTokens: SubgraphToken[] = [];
+ const wrappedTokens: SubgraphToken[] = [];
+ const linearPoolTokens: SubgraphToken[] = [];
+
+ const totalBalance = pools.reduce(
+ (total: BigNumber, pool) => total.add(pool.balance),
+ Zero
+ );
+ pools?.forEach((pool) => {
+ const poolAddress = formatAddress(
+ `address-${pool.tokens.mainSymbol}_${pool.tokens.wrappedSymbol}`
+ );
+ const mainToken = subgraphToken
+ .transient({
+ symbol: pool.tokens.mainSymbol,
+ balance: '1000000',
+ })
+ .build();
+ const wrappedToken = subgraphToken
+ .transient({
+ symbol: pool.tokens.wrappedSymbol,
+ balance: '9711834',
+ })
+ .build();
+ const composableBptToken = subgraphToken
+ .transient({
+ symbol: `b${pool.tokens.mainSymbol}_${pool.tokens.wrappedSymbol}`,
+ balance: '5192296829399898',
+ address: poolAddress,
+ })
+ .build();
+ const linearPool = subgraphPoolBase.build({
+ id: formatId(
+ `id-${pool.tokens.mainSymbol}_${pool.tokens.wrappedSymbol}`
+ ),
+ address: poolAddress,
+ poolType: 'AaveLinear',
+ tokens: [mainToken, wrappedToken, composableBptToken],
+ wrappedIndex: 1,
+ mainIndex: 0,
+ tokensList: [
+ mainToken.address,
+ wrappedToken.address,
+ composableBptToken.address,
+ ],
+ lowerTarget: '1',
+ upperTarget: '1',
+ });
+ // Update the pool token to have the expected balance set in input
+ composableBptToken.balance = pool.balance;
+ linearPoolTokens.push(composableBptToken);
+ mainTokens.push(mainToken);
+ wrappedTokens.push(wrappedToken);
+ const proportion = BigNumber.from(pool.balance)
+ .mul(WeiPerEther)
+ .div(totalBalance);
+ const propOfParent = proportion
+ .mul(parseFixed(proportionOfParent, 18))
+ .div(WeiPerEther);
+ linearPools.push({
+ ...linearPool,
+ proportionOfParent: formatFixed(propOfParent.toString(), 18),
+ });
+ });
+ return {
+ linearPools,
+ mainTokens,
+ wrappedTokens,
+ linearPoolTokens,
+ };
+ }
+);
+
+/*
+Create and return a composableStable pool (with composableBpt) and token.
+*/
+const composableStablePool = Factory.define<
+ ComposableStableInfo,
+ ComposableStableParams
+>(({ transientParams }) => {
+ const { id, address, symbol, childTokens, tokenbalance } = transientParams;
+ // Create composableStable BPT
+ const composableBptToken = subgraphToken
+ .transient({
+ symbol,
+ balance: '5192296829399898', // need composableBpt balance for pool
+ address,
+ })
+ .build();
+
+ // Create composableStable pool
+ const pool = subgraphPoolBase.build({
+ poolType: 'ComposableStable',
+ id,
+ address,
+ totalWeight: undefined,
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ tokens: [...childTokens!, composableBptToken],
+ amp: '1',
+ });
+
+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+ composableBptToken.balance = tokenbalance!;
+
+ return {
+ pool: { ...pool, proportionOfParent: '1' },
+ poolToken: composableBptToken,
+ };
+});
+
+/*
+Check a boostedPool, a composableStable with all constituents being Linear.
+Also creates associated LinearPools consisting of wrappedToken, mainToken, composableBpt.
+*/
+const boostedPool = Factory.define(
+ ({ transientParams }) => {
+ const {
+ linearPoolsParams,
+ rootAddress = 'address_root',
+ rootId = 'id_root',
+ rootBalance = '1000000',
+ parentsProportion = '1',
+ } = transientParams;
+ let linearPoolInfo;
+ // Create linear pools and tokens
+ if (linearPoolsParams)
+ linearPoolInfo = linearPools
+ .transient({
+ ...linearPoolsParams,
+ parentsProportion,
+ })
+ .build();
+ else linearPoolInfo = linearPools.build();
+
+ const rootPoolParams = {
+ id: formatId(rootId),
+ symbol: 'bRootPool',
+ address: formatAddress(rootAddress),
+ childTokens: linearPoolInfo.linearPoolTokens,
+ tokenbalance: rootBalance,
+ };
+ const rootInfo = composableStablePool.build(
+ {},
+ { transient: rootPoolParams }
+ );
+
+ return {
+ rootPool: rootInfo.pool,
+ rootPoolToken: rootInfo.poolToken,
+ linearPools: linearPoolInfo.linearPools,
+ mainTokens: linearPoolInfo.mainTokens,
+ wrappedTokens: linearPoolInfo.wrappedTokens,
+ linearPoolTokens: linearPoolInfo.linearPoolTokens,
+ };
+ }
+);
+
+/*
+Check a boostedMetaPool, a composableStable with one Linear and one boosted.
+Also creates associated boosted and LinearPools consisting of wrappedToken, mainToken, composableBpt.
+*/
+const boostedMetaPool = Factory.define(
+ ({ transientParams }) => {
+ const {
+ childBoostedParams,
+ childLinearParam,
+ rootAddress,
+ rootId,
+ rootBalance,
+ } = transientParams;
+
+ if (childBoostedParams === undefined || childLinearParam === undefined)
+ throw Error('Missing Pool Params.');
+
+ const rootTokenBalanceBoosted = BigNumber.from(
+ childBoostedParams.rootBalance
+ );
+ const rootTokenBalanceLiner = BigNumber.from(
+ childLinearParam.pools[0].balance
+ );
+ const totalTokenBalance = rootTokenBalanceBoosted.add(
+ rootTokenBalanceLiner
+ );
+
+ const childBoostedProportion = formatFixed(
+ rootTokenBalanceBoosted
+ .mul(BigNumber.from(WeiPerEther))
+ .div(totalTokenBalance),
+ 18
+ );
+
+ // Build child boostedPool
+ const childBoostedInfo = boostedPool
+ .transient({
+ ...childBoostedParams,
+ parentsProportion: childBoostedProportion,
+ })
+ .build();
+ const childBoostedBpt = childBoostedInfo.rootPoolToken;
+
+ const childLinearProportion = formatFixed(
+ rootTokenBalanceLiner.mul(WeiPerEther).div(totalTokenBalance),
+ 18
+ );
+
+ // Build child Linear pool
+ const childLinearInfo = linearPools
+ .transient({
+ pools: childLinearParam.pools,
+ parentsProportion: childLinearProportion,
+ })
+ .build();
+
+ const rootPoolParams = {
+ id: formatId(rootId as string),
+ symbol: 'rootPool',
+ address: formatAddress(rootAddress as string),
+ childTokens: [childBoostedBpt, ...childLinearInfo.linearPoolTokens],
+ tokenbalance: rootBalance,
+ };
+ const rootInfo = composableStablePool.build(
+ {},
+ { transient: rootPoolParams }
+ );
+
+ return {
+ rootInfo,
+ childBoostedInfo: {
+ ...childBoostedInfo,
+ proportion: childBoostedProportion,
+ },
+ childLinearInfo,
+ };
+ }
+);
+
+/*
+Check a boostedMetaBigPool, a composableStable with two boosted.
+Also creates associated boosted and LinearPools consisting of wrappedToken, mainToken, composableBpt.
+*/
+const boostedMetaBigPool = Factory.define<
+ BoostedMetaBigInfo,
+ BoostedMetaBigParams
+>(({ transientParams }) => {
+ const childPoolsInfo: ChildBoostedInfo[] = [];
+ // These will be used in parent pool
+ const childPoolTokens: SubgraphToken[] = [];
+ // These will include composableStables and linear pools
+ const childPools: Pool[] = [];
+
+ if (transientParams.childPools === undefined)
+ throw new Error(`Can't create boostedMetaBig without child pools.`);
+
+ // TO DO - need proportions
+ let totalTokenBalance = Zero;
+ for (let i = 0; i < transientParams.childPools.length; i++) {
+ const balance = transientParams.childPools[i].rootBalance;
+ totalTokenBalance = totalTokenBalance.add(balance);
+ }
+
+ // Create each child boostedPool
+ for (let i = 0; i < transientParams.childPools.length; i++) {
+ const childPool = transientParams.childPools[i];
+ const proportion = formatFixed(
+ BigNumber.from(childPool.rootBalance)
+ .mul(WeiPerEther)
+ .div(totalTokenBalance),
+ 18
+ );
+ childPool.parentsProportion = proportion;
+ const childBoosted = boostedPool.transient(childPool).build();
+ childPoolsInfo.push({ ...childBoosted, proportion });
+ childPools.push(childBoosted.rootPool, ...childBoosted.linearPools);
+ childPoolTokens.push(childBoosted.rootPoolToken);
+ }
+
+ const composableParams = {
+ id: formatId(transientParams.rootId as string),
+ symbol: 'parentComposable',
+ address: formatAddress(transientParams.rootAddress as string),
+ childTokens: childPoolTokens,
+ tokenbalance: transientParams.rootBalance,
+ };
+ const rootInfo = composableStablePool.build(
+ {},
+ { transient: composableParams }
+ );
+
+ return {
+ rootPool: rootInfo.pool,
+ rootPoolToken: rootInfo.poolToken,
+ childPoolsInfo,
+ childPools,
+ };
+});
+
+export { linearPools, boostedPool, boostedMetaPool, boostedMetaBigPool };
diff --git a/balancer-js/src/test/factories/sdk.ts b/balancer-js/src/test/factories/sdk.ts
index 7f45d86a6..528c43a58 100644
--- a/balancer-js/src/test/factories/sdk.ts
+++ b/balancer-js/src/test/factories/sdk.ts
@@ -39,6 +39,8 @@ const poolFactory = Factory.define(({ params, afterBuild }) => {
totalWeight: '1',
totalShares: '1',
totalLiquidity: '0',
+ lowerTarget: '0',
+ upperTarget: '0',
};
});
diff --git a/balancer-js/src/test/factories/sor.ts b/balancer-js/src/test/factories/sor.ts
index b8492703f..ff6949e05 100644
--- a/balancer-js/src/test/factories/sor.ts
+++ b/balancer-js/src/test/factories/sor.ts
@@ -6,6 +6,7 @@ import {
SwapV2,
} from '@balancer-labs/sor';
import { BigNumber } from '@ethersproject/bignumber';
+import { formatAddress } from '../lib/utils';
import { namedTokens } from './named-tokens';
const swapV2 = Factory.define(() => ({
@@ -33,14 +34,19 @@ const swapInfo = Factory.define(() => ({
}));
const subgraphToken = Factory.define(({ transientParams }) => {
- const { symbol } = transientParams;
- const namedToken = namedTokens[symbol];
-
+ const { symbol, balance = '1', weight = '1', address } = transientParams;
+ let namedToken = namedTokens[symbol];
+ if (!namedToken) {
+ namedToken = {};
+ namedToken.address = formatAddress(address ?? `address_${symbol}`);
+ namedToken.decimals = 18;
+ }
return {
...namedToken,
- balance: '1',
+ balance,
priceRate: '1',
- weight: '0.5',
+ weight,
+ symbol,
};
});
diff --git a/balancer-js/src/test/fixtures/liquidityPools.json b/balancer-js/src/test/fixtures/liquidityPools.json
index a12a2bcd2..7a4c795f1 100644
--- a/balancer-js/src/test/fixtures/liquidityPools.json
+++ b/balancer-js/src/test/fixtures/liquidityPools.json
@@ -720,5 +720,397 @@
"managedBalance": "0"
}
]
+ },
+ {
+ "id": "0xb54b2125b711cd183edd3dd09433439d5396165200000000000000000000075e",
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "poolType": "ComposableStable",
+ "swapFee": "0.0004",
+ "tokensList": [
+ "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "0xa3fa99a148fa48d14ed51d610c367c61876997f1"
+ ],
+ "totalLiquidity": "18155.03979860564175032143240229646",
+ "totalSwapVolume": "102671.3531868674812919326397895474",
+ "totalSwapFee": "41.06854127474699251677305591581902",
+ "totalShares": "18229.452335273313471779",
+ "owner": "0xba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1ba1b",
+ "factory": "0x136fd06fa01ecf624c7f2b3cb15742c1339dc2c4",
+ "amp": "60",
+ "createTime": 1662580832,
+ "swapEnabled": true,
+ "symbol": "Mai BSP",
+ "name": "Balancer MAI bb-am-usd Stable",
+ "protocolYieldFeeCache": "0.5",
+ "priceRateProviders": [
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "token": {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085"
+ }
+ }
+ ],
+ "tokens": [
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "6709.966214955565090006",
+ "weight": null,
+ "priceRate": "1.001100630645200917",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
+ "balance": "11562.004232310021164248",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "miMATIC",
+ "decimals": 18
+ },
+ {
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "balance": "2596148429115008.984547065289098477",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "Mai BSP",
+ "decimals": 18
+ }
+ ],
+ "isNew": false,
+ "chainId": 137,
+ "unwrappedTokens": [],
+ "feesSnapshot": "0.05569436766835841178113130600685",
+ "volumeSnapshot": "139.2359191708960294528282650171"
+ },
+ {
+ "id": "0x48e6b98ef6329f8f0a30ebb8c7c960330d64808500000000000000000000075b",
+ "totalShares": "6210115.878004697935980954",
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "poolType": "ComposableStable",
+ "mainIndex": 0,
+ "tokens": [
+ {
+ "address": "0x178e029173417b1f9c8bc16dcec6f697bc323746",
+ "balance": "1204360.955231972026803034",
+ "weight": null,
+ "priceRate": "1.00039076943095233",
+ "symbol": "bb-am-DAI",
+ "decimals": 18
+ },
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "2596148423060491.811647377029393683",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xf93579002dbe8046c43fefe86ec78b1112247bb8",
+ "balance": "1281578.268016962309728138",
+ "weight": null,
+ "priceRate": "1.000387793129824715",
+ "symbol": "bb-am-USDC",
+ "decimals": 18
+ },
+ {
+ "address": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6",
+ "balance": "3727441.528992488683407726",
+ "weight": null,
+ "priceRate": "1.000903832871667996",
+ "symbol": "bb-am-USDT",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0xb54b2125b711cd183edd3dd09433439d5396165200000000000000000000075e",
+ "totalShares": "18229.452335273313471779",
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "poolType": "ComposableStable",
+ "mainIndex": 0,
+ "tokens": [
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "6709.966214955565090006",
+ "weight": null,
+ "priceRate": "1.001100630645200917",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
+ "balance": "11562.004232310021164248",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "miMATIC",
+ "decimals": 18
+ },
+ {
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "balance": "2596148429115008.984547065289098477",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "Mai BSP",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0x178e029173417b1f9c8bc16dcec6f697bc323746000000000000000000000758",
+ "totalShares": "1204361.979169081484934798",
+ "address": "0x178e029173417b1f9c8bc16dcec6f697bc323746",
+ "poolType": "AaveLinear",
+ "mainIndex": 1,
+ "tokens": [
+ {
+ "address": "0x178e029173417b1f9c8bc16dcec6f697bc323746",
+ "balance": "5192296857330465.649361414844285297",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-DAI",
+ "decimals": 18
+ },
+ {
+ "address": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063",
+ "balance": "375304.734603121326369737",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "DAI",
+ "decimals": 18
+ },
+ {
+ "address": "0xee029120c72b0607344f35b17cdd90025e647b00",
+ "balance": "794969.292450441860119758",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "amDAI",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0x48e6b98ef6329f8f0a30ebb8c7c960330d64808500000000000000000000075b",
+ "totalShares": "6210115.878004697935980954",
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "poolType": "ComposableStable",
+ "mainIndex": 0,
+ "tokens": [
+ {
+ "address": "0x178e029173417b1f9c8bc16dcec6f697bc323746",
+ "balance": "1204360.955231972026803034",
+ "weight": null,
+ "priceRate": "1.00039076943095233",
+ "symbol": "bb-am-DAI",
+ "decimals": 18
+ },
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "2596148423060491.811647377029393683",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xf93579002dbe8046c43fefe86ec78b1112247bb8",
+ "balance": "1281578.268016962309728138",
+ "weight": null,
+ "priceRate": "1.000387793129824715",
+ "symbol": "bb-am-USDC",
+ "decimals": 18
+ },
+ {
+ "address": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6",
+ "balance": "3727441.528992488683407726",
+ "weight": null,
+ "priceRate": "1.000903832871667996",
+ "symbol": "bb-am-USDT",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0xf93579002dbe8046c43fefe86ec78b1112247bb8000000000000000000000759",
+ "totalShares": "1281580.284714950985286573",
+ "address": "0xf93579002dbe8046c43fefe86ec78b1112247bb8",
+ "poolType": "AaveLinear",
+ "mainIndex": 1,
+ "tokens": [
+ {
+ "address": "0x221836a597948dce8f3568e044ff123108acc42a",
+ "balance": "1002299.319398",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "amUSDC",
+ "decimals": 6
+ },
+ {
+ "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
+ "balance": "240640.547859",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "USDC",
+ "decimals": 6
+ },
+ {
+ "address": "0xf93579002dbe8046c43fefe86ec78b1112247bb8",
+ "balance": "5192296857253247.343815545343933522",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-USDC",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0x48e6b98ef6329f8f0a30ebb8c7c960330d64808500000000000000000000075b",
+ "totalShares": "6210115.878004697935980954",
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "poolType": "ComposableStable",
+ "mainIndex": 0,
+ "tokens": [
+ {
+ "address": "0x178e029173417b1f9c8bc16dcec6f697bc323746",
+ "balance": "1204360.955231972026803034",
+ "weight": null,
+ "priceRate": "1.00039076943095233",
+ "symbol": "bb-am-DAI",
+ "decimals": 18
+ },
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "2596148423060491.811647377029393683",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xf93579002dbe8046c43fefe86ec78b1112247bb8",
+ "balance": "1281578.268016962309728138",
+ "weight": null,
+ "priceRate": "1.000387793129824715",
+ "symbol": "bb-am-USDC",
+ "decimals": 18
+ },
+ {
+ "address": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6",
+ "balance": "3727441.528992488683407726",
+ "weight": null,
+ "priceRate": "1.000903832871667996",
+ "symbol": "bb-am-USDT",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea600000000000000000000075a",
+ "totalShares": "3727441.547287488412857699",
+ "address": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6",
+ "poolType": "AaveLinear",
+ "mainIndex": 1,
+ "tokens": [
+ {
+ "address": "0x19c60a251e525fa88cd6f3768416a8024e98fc19",
+ "balance": "3329110.6963",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "amUSDT",
+ "decimals": 6
+ },
+ {
+ "address": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
+ "balance": "147162.90748",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "USDT",
+ "decimals": 6
+ },
+ {
+ "address": "0xff4ce5aaab5a627bf82f4a571ab1ce94aa365ea6",
+ "balance": "5192296854807386.081243007916362396",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "bb-am-USDT",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0xb54b2125b711cd183edd3dd09433439d5396165200000000000000000000075e",
+ "totalShares": "18229.452335273313471779",
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "poolType": "ComposableStable",
+ "mainIndex": 0,
+ "tokens": [
+ {
+ "address": "0x48e6b98ef6329f8f0a30ebb8c7c960330d648085",
+ "balance": "6709.966214955565090006",
+ "weight": null,
+ "priceRate": "1.001100630645200917",
+ "symbol": "bb-am-usd",
+ "decimals": 18
+ },
+ {
+ "address": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
+ "balance": "11562.004232310021164248",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "miMATIC",
+ "decimals": 18
+ },
+ {
+ "address": "0xb54b2125b711cd183edd3dd09433439d53961652",
+ "balance": "2596148429115008.984547065289098477",
+ "weight": null,
+ "priceRate": "1",
+ "symbol": "Mai BSP",
+ "decimals": 18
+ }
+ ]
+ },
+ {
+ "id": "0xc065798f227b49c150bcdc6cdc43149a12c4d75700020000000000000000010b",
+ "address": "0xc065798f227b49c150bcdc6cdc43149a12c4d757",
+ "poolType": "LiquidityBootstrapping",
+ "swapFee": "0.005",
+ "tokensList": [
+ "0x3301ee63fb29f863f2333bd4466acb46cd8323e6",
+ "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2"
+ ],
+ "totalLiquidity": "5133797.634654893764109217143567092",
+ "totalSwapVolume": "22318765.89221163343284493798122458",
+ "totalSwapFee": "111593.8294610581671642246899061264",
+ "totalShares": "36815633080869.823028415262249208",
+ "owner": "0x116f4d31e8dbfe9eb8c8656bc65dff1198cde30e",
+ "factory": "0x0f3e0c4218b7b0108a3643cfe9d3ec0d4f57c54e",
+ "amp": null,
+ "createTime": 1639690020,
+ "swapEnabled": true,
+ "symbol": "AKITA_TLA",
+ "name": "AKITA Copper Launch",
+ "tokens": [
+ {
+ "address": "0x3301ee63fb29f863f2333bd4466acb46cd8323e6",
+ "balance": "2721529192541.281946501751871327",
+ "weight": "0.048481409814721326",
+ "priceRate": "1",
+ "symbol": "AKITA",
+ "decimals": 18,
+ "token": { "pool": null }
+ },
+ {
+ "address": "0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2",
+ "balance": "3763.691355377977499777",
+ "weight": "0.951533849216315698",
+ "priceRate": "1",
+ "symbol": "WETH",
+ "decimals": 18,
+ "token": { "pool": null }
+ }
+ ],
+ "protocolYieldFeeCache": null,
+ "priceRateProviders": []
}
]
diff --git a/balancer-js/src/test/fixtures/liquidityTokens.json b/balancer-js/src/test/fixtures/liquidityTokens.json
index 8c7916bd4..9b567e206 100644
--- a/balancer-js/src/test/fixtures/liquidityTokens.json
+++ b/balancer-js/src/test/fixtures/liquidityTokens.json
@@ -93,5 +93,60 @@
"eth": "1"
},
"lastUpdate": 1648702384835
+ },
+ {
+ "chainId": 137,
+ "symbol": "USDC",
+ "decimals": 6,
+ "address": "0x2791bca1f2de4661ed88a30c99a7a9449aa84174",
+ "price": {
+ "usd": "1.01",
+ "eth": "0.000505"
+ },
+ "lastUpdate": 1648702623513
+ },
+ {
+ "chainId": 137,
+ "symbol": "DAI",
+ "decimals": 18,
+ "address": "0x8f3cf7ad23cd3cadbd9735aff958023239c6a063",
+ "price": {
+ "usd": "1",
+ "eth": "0.0005"
+ },
+ "lastUpdate": 1648702385646
+ },
+ {
+ "chainId": 137,
+ "symbol": "USDT",
+ "decimals": 6,
+ "address": "0xc2132d05d31c914a87c6611c10748aeb04b58e8f",
+ "price": {
+ "usd": "0.99",
+ "eth": "0.000495"
+ },
+ "lastUpdate": 1648702503883
+ },
+ {
+ "chainId": 137,
+ "symbol": "MAI",
+ "decimals": 18,
+ "address": "0xa3fa99a148fa48d14ed51d610c367c61876997f1",
+ "price": {
+ "usd": "1",
+ "eth": "0.0005"
+ },
+ "lastUpdate": 1648702503883
+ },
+ {
+ "chainId": 1,
+ "symbol": "AKITA",
+ "decimals": 18,
+ "address": "0x3301ee63fb29f863f2333bd4466acb46cd8323e6",
+ "price": {
+ "usd": "9.3312e-08",
+ "eth": "7.2066e-11"
+ },
+ "lastUpdate": 1648702503883
}
]
\ No newline at end of file
diff --git a/balancer-js/src/test/lib/ImpermanentLossData.ts b/balancer-js/src/test/lib/ImpermanentLossData.ts
new file mode 100644
index 000000000..359eea727
--- /dev/null
+++ b/balancer-js/src/test/lib/ImpermanentLossData.ts
@@ -0,0 +1,178 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+import { HistoricalPriceProvider, TokenPriceProvider } from '@/modules/data';
+import { Pool, PoolType, Price } from '@/types';
+
+export const MOCK_POOLS: { [key: string]: Pool } = {
+ WeightedPool_1: {
+ chainId: 1,
+ name: 'WeightedPool',
+ id: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014',
+ address: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56',
+ swapFee: '0.002',
+ swapEnabled: true,
+ tokens: [
+ {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ balance: '10000000000000',
+ decimals: 18,
+ weight: '0.5',
+ priceRate: '1',
+ symbol: 'DAI',
+ },
+ {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ balance: '20000000000000',
+ decimals: 6,
+ weight: '0.5',
+ priceRate: '1',
+ symbol: 'USDC',
+ },
+ ],
+ tokensList: [
+ '0x6b175474e89094c44da98b954eedeac495271d0f',
+ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ ],
+ totalWeight: '10',
+ totalShares: '10000000000000',
+ totalLiquidity: '10000000000000',
+ poolType: PoolType.Weighted,
+ poolTypeVersion: 1,
+ protocolYieldFeeCache: '0',
+ lowerTarget: '0',
+ upperTarget: '0',
+ },
+ WeightedPool_2: {
+ chainId: 1,
+ name: 'WeightedPool',
+ id: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56000200000000000000000014',
+ address: '0x5c6ee304399dbdb9c8ef030ab642b10820db8f56',
+ swapFee: '0.002',
+ swapEnabled: true,
+ tokens: [
+ {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ balance: '10000000000000',
+ decimals: 18,
+ weight: '0.5',
+ priceRate: '1',
+ symbol: 'DAI',
+ },
+ {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ balance: '20000000000000',
+ decimals: 6,
+ weight: '0.5',
+ priceRate: '1',
+ symbol: 'USDC',
+ },
+ ],
+ tokensList: [
+ '0x6b175474e89094c44da98b954eedeac495271d0f',
+ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ ],
+ totalWeight: '10',
+ totalShares: '10000000000000',
+ totalLiquidity: '10000000000000',
+ poolType: PoolType.Weighted,
+ poolTypeVersion: 1,
+ protocolYieldFeeCache: '0',
+ lowerTarget: '0',
+ upperTarget: '0',
+ },
+ StablePool: {
+ chainId: 1,
+ name: 'StablePool',
+ id: '0xc45d42f801105e861e86658648e3678ad7aa70f900010000000000000000011e',
+ address: '0xc45d42f801105e861e86658648e3678ad7aa70f9',
+ swapFee: '0.0004',
+ swapEnabled: true,
+ amp: '10',
+ tokens: [
+ {
+ address: '0x6b175474e89094c44da98b954eedeac495271d0f',
+ balance: '10000000',
+ decimals: 18,
+ weight: '0.3',
+ priceRate: '1',
+ symbol: 'DAI',
+ },
+ {
+ address: '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ balance: '10000000',
+ decimals: 6,
+ weight: '0.3',
+ priceRate: '1',
+ symbol: 'USDC',
+ },
+ {
+ address: '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ balance: '10000000',
+ decimals: 6,
+ weight: '0.4',
+ priceRate: '1',
+ symbol: 'USDT',
+ },
+ ],
+ tokensList: [
+ '0x6b175474e89094c44da98b954eedeac495271d0f',
+ '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48',
+ '0xdac17f958d2ee523a2206206994597c13d831ec7',
+ ],
+ totalWeight: '10',
+ totalShares: '100000',
+ totalLiquidity: '10000000000000',
+ poolType: PoolType.Stable,
+ poolTypeVersion: 1,
+ protocolYieldFeeCache: '0',
+ lowerTarget: '0',
+ upperTarget: '0',
+ },
+};
+
+export const MOCK_PRICES = new Map([
+ ['0x6b175474e89094c44da98b954eedeac495271d0f', { usd: '1.002' }],
+ ['0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48', { usd: '1.002' }],
+ ['0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270', { usd: '1.002' }],
+ ['0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4', { usd: '1.002' }],
+]);
+
+export const MOCK_HISTORICAL_PRICES = new Map<
+ string,
+ { [timestamp: number]: Price }
+>([
+ [
+ '0x0d500b1d8e8ef31e21c99d1db9a6444d3adf1270',
+ { 1666276501: { usd: '0.9993785272283172' } },
+ ],
+ [
+ '0x3a58a54c066fdc0f2d55fc9c89f0415c92ebf3c4',
+ { 1666276501: { usd: '1.9996776052990013' } },
+ ],
+]);
+
+export class MockPriceProvider extends TokenPriceProvider {
+ async find(address: string): Promise {
+ return MOCK_PRICES.get(address);
+ }
+
+ async findBy(address: string): Promise {
+ return MOCK_PRICES.get(address as unknown as string);
+ }
+}
+
+export class MockHistoricalPriceProvider extends HistoricalPriceProvider {
+ async find(address: string): Promise {
+ const historicalPrice = MOCK_HISTORICAL_PRICES.has(address)
+ ? MOCK_HISTORICAL_PRICES.get(address)
+ : undefined;
+ return historicalPrice ? historicalPrice[1666276501] : undefined;
+ }
+
+ async findBy(address: string, timestamp: number): Promise {
+ const historicalPrice = MOCK_HISTORICAL_PRICES.has(address)
+ ? MOCK_HISTORICAL_PRICES.get(address)
+ : undefined;
+ return historicalPrice ? historicalPrice[timestamp] : undefined;
+ }
+}
diff --git a/balancer-js/src/test/lib/constants.ts b/balancer-js/src/test/lib/constants.ts
index 014206c77..8c4269ced 100644
--- a/balancer-js/src/test/lib/constants.ts
+++ b/balancer-js/src/test/lib/constants.ts
@@ -72,6 +72,12 @@ export const ADDRESSES = {
decimals: 18,
symbol: 'bbausd',
},
+ bbausd2: {
+ id: '0xa13a9247ea42d743238089903570127dda72fe4400000000000000000000035d',
+ address: '0xA13a9247ea42D743238089903570127DdA72fE44'.toLowerCase(),
+ decimals: 18,
+ symbol: 'bbausd2',
+ },
bbausdc: {
address: '0x9210F1204b5a24742Eba12f710636D76240dF3d0',
decimals: 18,
@@ -81,11 +87,26 @@ export const ADDRESSES = {
address: '0x02d60b84491589974263d922d9cc7a3152618ef6',
decimals: 18,
symbol: 'waDAI',
+ slot: 52,
},
waUSDC: {
address: '0xd093fa4fb80d09bb30817fdcd442d4d02ed3e5de',
decimals: 6,
symbol: 'waUSDC',
+ slot: 52,
+ },
+ waUSDT: {
+ address: '0xf8Fd466F12e236f4c96F7Cce6c79EAdB819abF58',
+ decimals: 6,
+ symbol: 'waUSDT',
+ slot: 52,
+ },
+ WBTCWETH: {
+ id: '0xa6f548df93de924d73be7d25dc02554c6bd66db500020000000000000000000e',
+ address: '0xa6f548df93de924d73be7d25dc02554c6bd66db5',
+ decimals: 18,
+ symbol: 'B-50WBTC-50WETH',
+ slot: 0,
},
auraBal: {
address: '0x616e8bfa43f920657b3497dbf40d6b1a02d4608d',
@@ -99,6 +120,13 @@ export const ADDRESSES = {
symbol: 'BAL8020BPT',
slot: 0,
},
+ wstETH_bbaUSD: {
+ id: '0x25accb7943fd73dda5e23ba6329085a3c24bfb6a000200000000000000000387',
+ address: '0x25accb7943fd73dda5e23ba6329085a3c24bfb6a',
+ decimals: 18,
+ symbol: 'wstETH_bbaUSD',
+ slot: 0,
+ },
},
[Network.KOVAN]: {
// Visit https://balancer-faucet.on.fleek.co/#/faucet for test tokens
@@ -195,6 +223,24 @@ export const ADDRESSES = {
decimals: 18,
symbol: 'bbausd',
},
+ waDAI: {
+ address: '',
+ decimals: 18,
+ symbol: 'waDAI',
+ slot: 52,
+ },
+ waUSDC: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDC',
+ slot: 52,
+ },
+ waUSDT: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDT',
+ slot: 52,
+ },
},
[Network.POLYGON]: {
MATIC: {
@@ -272,6 +318,24 @@ export const ADDRESSES = {
decimals: 18,
symbol: 'bbausd',
},
+ waDAI: {
+ address: '',
+ decimals: 18,
+ symbol: 'waDAI',
+ slot: 52,
+ },
+ waUSDC: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDC',
+ slot: 52,
+ },
+ waUSDT: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDT',
+ slot: 52,
+ },
bbamUSD: {
address: '0x48e6B98ef6329f8f0A30eBB8c7C960330d648085',
decimals: 18,
@@ -315,5 +379,223 @@ export const ADDRESSES = {
decimals: 18,
symbol: 'bbausd',
},
+ waDAI: {
+ address: '',
+ decimals: 18,
+ symbol: 'waDAI',
+ slot: 52,
+ },
+ waUSDC: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDC',
+ slot: 52,
+ },
+ waUSDT: {
+ address: '',
+ decimals: 6,
+ symbol: 'waUSDT',
+ slot: 52,
+ },
+ },
+ [Network.GOERLI]: {
+ USDC_old: {
+ address: '0xe0c9275e44ea80ef17579d33c55136b7da269aeb',
+ decimals: 6,
+ symbol: 'USDC_old',
+ slot: 0,
+ },
+ USDC: {
+ address: '0xdabd33683bafdd448968ab6d6f47c3535c64bf0c',
+ decimals: 6,
+ symbol: 'USDC',
+ slot: 0,
+ },
+ USDT: {
+ address: '0x14468fd5e1de5a5a4882fa5f4e2217c5a8ddcadb',
+ decimals: 6,
+ symbol: 'USDT',
+ slot: 0,
+ },
+ DAI: {
+ address: '0xb8096bc53c3ce4c11ebb0069da0341d75264b104',
+ decimals: 18,
+ symbol: 'DAI',
+ slot: 0,
+ },
+ BAL: {
+ address: '0xfa8449189744799ad2ace7e0ebac8bb7575eff47',
+ decimals: 18,
+ symbol: 'BAL',
+ slot: 1,
+ },
+ bbausd: {
+ address: '0x13acd41c585d7ebb4a9460f7c8f50be60dc080cd',
+ decimals: 18,
+ symbol: 'bbausd',
+ },
+ waDAI: {
+ address: '0x0b61329839d2ebea96e21f45d4b065dbf38a7af6',
+ decimals: 18,
+ symbol: 'waDAI',
+ slot: 52,
+ },
+ waUSDC: {
+ address: '0xb8b3c69687ac048f607d75d89145bc82232098ca',
+ decimals: 6,
+ symbol: 'waUSDC',
+ slot: 52,
+ },
+ waUSDT: {
+ address: '0x014c0b2b8c4ed33231f9b33aca21735c8f71bbfb',
+ decimals: 6,
+ symbol: 'waUSDT',
+ slot: 52,
+ },
+ MAI: {
+ address: '0x398106564948fEeb1fEdeA0709AE7D969D62a391',
+ decimals: 18,
+ symbol: 'MAI',
+ slot: 0,
+ },
+ waMAI: {
+ address: '0x6B53E04299124217ebb46c2830e00DFafd0d86d6',
+ decimals: 18,
+ symbol: 'waMAI',
+ slot: 0,
+ },
+ WETH: {
+ address: '0xdfcea9088c8a88a76ff74892c1457c17dfeef9c1',
+ decimals: 18,
+ symbol: 'WETH',
+ slot: 4,
+ },
+ waWETH: {
+ address: '0xAB1Ec6011332A431d3fFA597681C29E28FeFe065',
+ decimals: 18,
+ symbol: 'waWETH',
+ slot: 0,
+ },
+ WBTC: {
+ address: '0x37f03a12241E9FD3658ad6777d289c3fb8512Bc9',
+ decimals: 18,
+ symbol: 'WBTC',
+ slot: 0,
+ },
+ waWBTC: {
+ address: '0xB846B79562Bc238b1919c665AB49F6217c072D11',
+ decimals: 18,
+ symbol: 'waWBTC',
+ slot: 0,
+ },
+ bbadai: {
+ address: '0x594920068382f64e4bc06879679bd474118b97b1',
+ decimals: 18,
+ symbol: 'bbadai',
+ slot: 0,
+ },
+ bbausdc: {
+ address: '0x4d983081b9b9f3393409a4cdf5504d0aea9cd94c',
+ decimals: 18,
+ symbol: 'bbausdc',
+ slot: 0,
+ },
+ bbausdt: {
+ address: '0xd03d4d8b4669d135569215dd6c4e790307c8e14b',
+ decimals: 18,
+ symbol: 'bbausdt',
+ slot: 0,
+ },
+ bbamai: {
+ id: '0x24c52fee349194f68a998ac4e2ce170d780d010c0000000000000000000001a1',
+ address: '0x24C52feE349194f68A998aC4E2ce170D780D010c',
+ decimals: 18,
+ symbol: 'bb-a-mai',
+ slot: 0,
+ },
+ bbaweth: {
+ id: '0xd8143b8e7a6e452e5e1bc42a3cef43590a2300310000000000000000000001a2',
+ address: '0xD8143B8E7a6e452E5E1BC42A3ceF43590A230031',
+ decimals: 18,
+ symbol: 'bb-a-weth',
+ slot: 0,
+ },
+ bbamaiweth: {
+ id: '0x45631a4b3cab78e6dfdd21a7025a61fac76839190000000000000000000001a8',
+ address: '0x45631A4b3CaB78E6DFDd21a7025A61fAC7683919',
+ decimals: 18,
+ symbol: 'bb-a-mai-weth',
+ slot: 0,
+ },
+ boostedMeta1: {
+ id: '0x48e984db5f9ba1bf2ee21d0a207a96c944d807e30000000000000000000001a9',
+ address: '0x48e984DB5F9BA1Bf2Ee21d0A207a96C944D807E3',
+ decimals: 18,
+ symbol: 'boostedMeta1',
+ slot: 0,
+ },
+ boostedMetaAlt1: {
+ id: '0x98f3b300d7d0607ed69be014ec0d99567b00be6d00000000000000000000020a',
+ address: '0x98f3b300d7d0607ed69be014ec0d99567b00be6d',
+ decimals: 18,
+ symbol: 'boostedMetaAlt1',
+ slot: 0,
+ },
+ boostedMetaBig1: {
+ id: '0xae5bfce463ab4689e9353d13b537e9896f13c7420000000000000000000001aa',
+ address: '0xae5bfce463ab4689e9353d13b537e9896f13c742',
+ decimals: 18,
+ symbol: 'boostedMetaBig1',
+ slot: 0,
+ },
+ bbausd2: {
+ id: '0x3d5981bdd8d3e49eb7bbdc1d2b156a3ee019c18e0000000000000000000001a7',
+ address: '0x3d5981BDD8D3E49EB7BBDC1d2B156a3eE019c18e',
+ decimals: 18,
+ symbol: 'bbausd2',
+ slot: 0,
+ },
+ WBTCWETH: {
+ id: '0x16faf9f73748013155b7bc116a3008b57332d1e600020000000000000000005b',
+ address: '0x16faf9f73748013155b7bc116a3008b57332d1e6',
+ decimals: 18,
+ symbol: 'B-50WBTC-50WETH',
+ slot: 0,
+ },
+ boostedWeightedSimple1: {
+ id: '0xd625c90154084cf1417227bbdea4ef151f746995000200000000000000000272',
+ address: '0xd625c90154084cf1417227bbdea4ef151f746995',
+ decimals: 18,
+ symbol: 'BWS1',
+ slot: 0,
+ },
+ boostedWeightedGeneral1: {
+ id: '0x42827c5452a8f4f13f4d5a1048dbfc58c77e98f5000100000000000000000273',
+ address: '0x42827c5452a8f4f13f4d5a1048dbfc58c77e98f5',
+ decimals: 18,
+ symbol: 'BWG1',
+ slot: 0,
+ },
+ boostedWeightedMeta1: {
+ id: '0x3b01654804b0cea899d5edf8fd7000fe1c0911bd000200000000000000000274',
+ address: '0x3b01654804b0cea899d5edf8fd7000fe1c0911bd',
+ decimals: 18,
+ symbol: 'BWM1',
+ slot: 0,
+ },
+ boostedWeightedMetaAlt1: {
+ id: '0x5f037eadf5cd0bc5fc198e6020f33f21a9361fc8000200000000000000000275',
+ address: '0x5f037eadf5cd0bc5fc198e6020f33f21a9361fc8',
+ decimals: 18,
+ symbol: 'BWMA1',
+ slot: 0,
+ },
+ boostedWeightedMetaGeneral1: {
+ id: '0xe0baf79433d233c26b41cac99ddcd73830f257e5000100000000000000000276',
+ address: '0xe0baf79433d233c26b41cac99ddcd73830f257e5',
+ decimals: 18,
+ symbol: 'BWMG1',
+ slot: 0,
+ },
},
};
diff --git a/balancer-js/src/test/lib/utils.ts b/balancer-js/src/test/lib/utils.ts
index dc5199798..9ee14ffea 100644
--- a/balancer-js/src/test/lib/utils.ts
+++ b/balancer-js/src/test/lib/utils.ts
@@ -1,9 +1,12 @@
-import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers';
import { BigNumber } from '@ethersproject/bignumber';
-import { AddressZero, MaxUint256 } from '@ethersproject/constants';
-import { balancerVault } from '@/lib/constants/config';
import { hexlify, zeroPad } from '@ethersproject/bytes';
+import { AddressZero, MaxUint256 } from '@ethersproject/constants';
+import { JsonRpcProvider, JsonRpcSigner } from '@ethersproject/providers';
import { keccak256 } from '@ethersproject/solidity';
+import { formatBytes32String } from '@ethersproject/strings';
+
+import { PoolWithMethods, BalancerError, BalancerErrorCode } from '@/.';
+import { balancerVault } from '@/lib/constants/config';
import { parseEther } from '@ethersproject/units';
import { ERC20 } from '@/modules/contracts/implementations/ERC20';
import { setBalance } from '@nomicfoundation/hardhat-network-helpers';
@@ -12,7 +15,6 @@ import { Interface } from '@ethersproject/abi';
const liquidityGaugeAbi = ['function deposit(uint value) payable'];
const liquidityGauge = new Interface(liquidityGaugeAbi);
import { Pools as PoolsProvider } from '@/modules/pools';
-import { PoolWithMethods, BalancerError, BalancerErrorCode } from '@/.';
/**
* Setup local fork with approved token balance for a given account
@@ -53,7 +55,7 @@ export const forkSetup = async (
);
// Approve appropriate allowances so that vault contract can move tokens
- await approveToken(tokens[i], balances[i], signer);
+ await approveToken(tokens[i], MaxUint256.toString(), signer);
}
};
@@ -152,6 +154,16 @@ export const getBalances = async (
return Promise.all(balances);
};
+export const formatAddress = (text: string): string => {
+ if (text.match(/^(0x)?[0-9a-fA-F]{40}$/)) return text; // Return text if it's already a valid address
+ return formatBytes32String(text).slice(0, 42);
+};
+
+export const formatId = (text: string): string => {
+ if (text.match(/^(0x)?[0-9a-fA-F]{64}$/)) return text; // Return text if it's already a valid id
+ return formatBytes32String(text);
+};
+
export const move = async (
token: string,
from: string,
diff --git a/balancer-js/src/types.ts b/balancer-js/src/types.ts
index 9b34952e1..4b4eaf6ef 100644
--- a/balancer-js/src/types.ts
+++ b/balancer-js/src/types.ts
@@ -19,6 +19,7 @@ import type {
PoolGaugesRepository,
PoolSharesRepository,
ProtocolFeesProvider,
+ PoolJoinExitRepository,
} from './modules/data';
import type { GraphQLArgs } from './lib/graphql';
import type { AprBreakdown } from '@/modules/pools/apr/apr';
@@ -35,6 +36,14 @@ export interface BalancerSdkConfig {
customSubgraphUrl?: string;
//optionally overwrite parts of the standard SOR config
sor?: Partial;
+ tenderly?: BalancerTenderlyConfig;
+}
+
+export interface BalancerTenderlyConfig {
+ accessKey?: string;
+ user?: string;
+ project?: string;
+ blockNumber?: number;
}
export interface BalancerSdkSorConfig {
@@ -53,9 +62,10 @@ export interface ContractAddresses {
vault: string;
multicall: string;
lidoRelayer?: string;
+ relayerV3?: string;
+ relayerV4?: string;
gaugeController?: string;
feeDistributor?: string;
- relayerV4?: string;
veBal?: string;
veBalProxy?: string;
protocolFeePercentagesProvider?: string;
@@ -75,6 +85,7 @@ export interface BalancerNetworkConfig {
bbaUsd?: string;
};
};
+ tenderly?: BalancerTenderlyConfig;
urls: {
subgraph: string;
gaugesSubgraph?: string;
@@ -89,6 +100,7 @@ export interface BalancerDataRepositories {
pools: Findable & Searchable;
yesterdaysPools?: Findable & Searchable