Skip to content

Commit a88d22f

Browse files
authored
Merge pull request #114 from rabbitholegg/mmackz-add_balancer
feat(balancer): add support for balancer swap
2 parents e7f7288 + 4ebacf4 commit a88d22f

17 files changed

+2336
-434
lines changed

.changeset/curly-jeans-dress.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@rabbitholegg/questdk-plugin-balancer": minor
3+
"@rabbitholegg/questdk-plugin-registry": minor
4+
---
5+
6+
add swap support for balancer

packages/balancer/CHANGELOG.md

Whitespace-only changes.

packages/balancer/README.md

Whitespace-only changes.

packages/balancer/package.json

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
{
2+
"name": "@rabbitholegg/questdk-plugin-balancer",
3+
"version": "1.0.0-alpha.4",
4+
"type": "module",
5+
"exports": {
6+
"require": "./dist/cjs/index.js",
7+
"import": "./dist/esm/index.js",
8+
"types": "./dist/types/index.d.ts"
9+
},
10+
"main": "./dist/cjs/index.js",
11+
"module": "./dist/esm/index.js",
12+
"packageManager": "pnpm@8.3.1",
13+
"description": "",
14+
"scripts": {
15+
"bench": "vitest bench",
16+
"bench:ci": "CI=true vitest bench",
17+
"build": "pnpm run clean && pnpm run build:cjs && pnpm run build:esm && pnpm run build:types",
18+
"build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'",
19+
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm && echo > ./dist/esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'",
20+
"build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap",
21+
"clean": "rimraf dist",
22+
"format": "rome format . --write",
23+
"lint": "rome check .",
24+
"lint:fix": "pnpm lint --apply",
25+
"preinstall": "npx only-allow pnpm",
26+
"test": "vitest dev",
27+
"test:cov": "vitest dev --coverage",
28+
"test:ci": "CI=true vitest --coverage",
29+
"test:ui": "vitest dev --ui"
30+
},
31+
"keywords": [],
32+
"author": "",
33+
"license": "ISC",
34+
"types": "./dist/types/index.d.ts",
35+
"typings": "./dist/types/index.d.ts",
36+
"devDependencies": {
37+
"@types/node": "^20.4.5",
38+
"@vitest/coverage-v8": "^0.33.0",
39+
"rimraf": "^5.0.1",
40+
"rome": "^12.1.3",
41+
"ts-node": "^10.9.1",
42+
"tsconfig": "workspace:*",
43+
"typescript": "^5.1.6",
44+
"vitest": "^0.33.0"
45+
},
46+
"dependencies": {
47+
"@rabbitholegg/questdk": "2.0.0-alpha.27",
48+
"viem": "^1.2.15"
49+
}
50+
}
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import { apply, GreaterThanOrEqual } from '@rabbitholegg/questdk/filter'
2+
import { describe, expect, test } from 'vitest'
3+
import { zeroAddress, parseEther, parseUnits } from 'viem'
4+
import { swap, getSupportedTokenAddresses } from './Balancer'
5+
import { Chains } from './utils'
6+
import { BALANCER_ABI } from './abi'
7+
import { VAULT_CONTRACT, CHAIN_ID_ARRAY } from './constants'
8+
import { failingTestCases, passingTestCases } from './test-transactions'
9+
10+
describe('Given the balancer plugin', () => {
11+
describe('When handling the swap action', () => {
12+
describe('should return a valid action filter', () => {
13+
test('when making a swap', async () => {
14+
const filter = await swap({
15+
chainId: Chains.POLYGON_POS,
16+
contractAddress: VAULT_CONTRACT,
17+
tokenIn: zeroAddress,
18+
tokenOut: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174', // USDC.e
19+
amountIn: GreaterThanOrEqual(parseEther('3')),
20+
amountOut: GreaterThanOrEqual(parseUnits('2', 6)),
21+
recipient: '0xa99f898530df1514a566f1a6562d62809e99557d',
22+
})
23+
24+
expect(filter).to.deep.equal({
25+
chainId: Chains.POLYGON_POS,
26+
to: VAULT_CONTRACT,
27+
input: {
28+
$abiAbstract: BALANCER_ABI,
29+
$or: [
30+
{
31+
singleSwap: {
32+
assetIn: '0x0000000000000000000000000000000000000000',
33+
assetOut: '0x2791bca1f2de4661ed88a30c99a7a9449aa84174',
34+
amount: {
35+
$gte: '3000000000000000000',
36+
},
37+
},
38+
funds: {
39+
recipient: '0xa99f898530df1514a566f1a6562d62809e99557d',
40+
},
41+
},
42+
{
43+
assets: {
44+
$and: [
45+
{
46+
$first: '0x0000000000000000000000000000000000000000',
47+
},
48+
{
49+
$last: '0x2791Bca1f2de4661ED88A30C99A7a9449Aa84174',
50+
},
51+
],
52+
},
53+
limits: {
54+
$and: [
55+
{
56+
$first: {
57+
$gte: '3000000000000000000',
58+
},
59+
},
60+
{
61+
$last: {
62+
$lte: '-2000000',
63+
},
64+
},
65+
],
66+
},
67+
funds: {
68+
recipient: '0xa99f898530df1514a566f1a6562d62809e99557d',
69+
},
70+
},
71+
],
72+
},
73+
})
74+
})
75+
})
76+
77+
describe('should pass filter when all parameters are valid', () => {
78+
passingTestCases.forEach((testCase) => {
79+
const { transaction, params, description } = testCase
80+
test(description, async () => {
81+
const filter = await swap(params)
82+
expect(apply(transaction, filter)).to.be.true
83+
})
84+
})
85+
})
86+
87+
describe('should not pass filter when parameters are invalid', () => {
88+
failingTestCases.forEach((testCase) => {
89+
const { transaction, params, description } = testCase
90+
test(description, async () => {
91+
const filter = await swap(params)
92+
expect(apply(transaction, filter)).to.be.false
93+
})
94+
})
95+
})
96+
97+
describe('should return a valid list of tokens for each supported chain', () => {
98+
CHAIN_ID_ARRAY.forEach((chainId) => {
99+
test(`for chainId: ${chainId}`, async () => {
100+
const tokens = await getSupportedTokenAddresses(chainId)
101+
const addressRegex = /^0x[a-fA-F0-9]{40}$/
102+
expect(tokens).to.be.an('array')
103+
expect(tokens).to.have.length.greaterThan(0)
104+
expect(tokens).to.have.length.lessThan(100)
105+
tokens.forEach((token) => {
106+
expect(token).to.match(
107+
addressRegex,
108+
`Token address ${token} is not a valid Ethereum address`,
109+
)
110+
})
111+
})
112+
})
113+
})
114+
})
115+
})

packages/balancer/src/Balancer.ts

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
import {
2+
type TransactionFilter,
3+
type SwapActionParams,
4+
compressJson,
5+
} from '@rabbitholegg/questdk'
6+
import { type Address } from 'viem'
7+
import { BALANCER_ABI } from './abi'
8+
import { CHAIN_ID_ARRAY, VAULT_CONTRACT, PATCH_CONTRACT } from './constants'
9+
import { buildAmountQuery, buildPathQuery } from './utils'
10+
import { CHAIN_TO_TOKENS } from './token-addresses'
11+
12+
export const swap = async (
13+
swap: SwapActionParams,
14+
): Promise<TransactionFilter> => {
15+
const {
16+
chainId,
17+
contractAddress,
18+
tokenIn,
19+
tokenOut,
20+
amountIn,
21+
amountOut,
22+
recipient,
23+
} = swap
24+
25+
return compressJson({
26+
chainId: chainId,
27+
to: contractAddress ?? {
28+
$or: [VAULT_CONTRACT.toLowerCase(), PATCH_CONTRACT.toLowerCase()],
29+
},
30+
input: {
31+
$abiAbstract: BALANCER_ABI,
32+
$or: [
33+
{
34+
// swap
35+
singleSwap: {
36+
assetIn: tokenIn,
37+
assetOut: tokenOut,
38+
amount: amountIn,
39+
},
40+
funds: {
41+
recipient: recipient,
42+
},
43+
},
44+
{
45+
// batch swap
46+
assets: buildPathQuery(tokenIn, tokenOut),
47+
limits: buildAmountQuery(amountIn, amountOut),
48+
funds: {
49+
recipient: recipient,
50+
},
51+
},
52+
],
53+
},
54+
})
55+
}
56+
57+
export const getSupportedTokenAddresses = async (
58+
_chainId: number,
59+
): Promise<Address[]> => {
60+
return CHAIN_TO_TOKENS[_chainId] ?? []
61+
}
62+
63+
export const getSupportedChainIds = async (): Promise<number[]> => {
64+
return CHAIN_ID_ARRAY as number[]
65+
}

0 commit comments

Comments
 (0)