-
Notifications
You must be signed in to change notification settings - Fork 25
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(symbiosis): add support for symbiosis bridge #85
Changes from all commits
7589741
ce28c39
605d548
49e8daa
eaf9e39
c5e1f92
34301c0
bb38d2f
797aff8
76bfb38
e2a6945
044d366
4e01bbc
bc954a3
3995a83
a1e32f5
62d4362
b162676
7a01921
de1031b
a0734bf
108b630
d30b44e
ca787a6
473f96d
bed7516
d9e3d88
625f23d
9c33334
f6c76fb
9ebda59
1233263
41348ca
3ab4f83
b054a10
d41d31b
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@rabbitholegg/questdk-plugin-symbiosis": minor | ||
"@rabbitholegg/questdk-plugin-registry": minor | ||
--- | ||
|
||
implement symbiosis bridge plugin |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
# Symbiosis Bridge Plugin | ||
|
||
This plugin is designed to filter valid bridge transactions using the Symbiosis cross-chain bridge. | ||
|
||
## Overview | ||
|
||
The Symbiosis bridge plugin prioritizes the most popular assets on each chain, which include ETH, stablecoins, and certain chain-specific tokens like OP and ARB. By leaving the tokenAddress undefined, the filter should allow any asset to pass, as long as all the other criteria is met. This allows for additional flexibility for quest creators. The plugin utilizes the symbiosis SDK to get the latest contract addresses for their metaRouter contracts. An overview of how the bridge operates can be [found here](https://docs.symbiosis.finance/main-concepts/cross-chain-swaps-with-symbiosis) | ||
|
||
## Filtering Criteria | ||
|
||
The plugin filters transactions based on certain predefined criteria to identify valid bridge transactions. The criteria can include the following aspects: | ||
|
||
- Source chain and Destination chain | ||
- Involved assets (e.g., ETH, USDC, wETH, OP, ARB) | ||
- Amount | ||
- Contract Address | ||
- Recipient | ||
|
||
## Sample Transaction | ||
- https://etherscan.io/tx/0xc97deae76c21cb57498d2873babd212fa589eae9e4d2e979f4a10a3428f376ee | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,52 @@ | ||
{ | ||
"name": "@rabbitholegg/questdk-plugin-symbiosis", | ||
"version": "1.0.0-alpha.4", | ||
"type": "module", | ||
"exports": { | ||
"require": "./dist/cjs/index.js", | ||
"import": "./dist/esm/index.js", | ||
"types": "./dist/types/index.d.ts" | ||
}, | ||
"main": "./dist/cjs/index.js", | ||
"module": "./dist/esm/index.js", | ||
"packageManager": "pnpm@8.3.1", | ||
"description": "", | ||
"scripts": { | ||
"bench": "vitest bench", | ||
"bench:ci": "CI=true vitest bench", | ||
"build": "pnpm run clean && pnpm run build:cjs && pnpm run build:esm && pnpm run build:types", | ||
"build:cjs": "tsc --project tsconfig.build.json --module commonjs --outDir ./dist/cjs --removeComments --verbatimModuleSyntax false && echo > ./dist/cjs/package.json '{\"type\":\"commonjs\"}'", | ||
"build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir ./dist/esm && echo > ./dist/esm/package.json '{\"type\":\"module\",\"sideEffects\":false}'", | ||
"build:types": "tsc --project tsconfig.build.json --module esnext --declarationDir ./dist/types --emitDeclarationOnly --declaration --declarationMap", | ||
"clean": "rimraf dist", | ||
"format": "rome format . --write", | ||
"lint": "rome check .", | ||
"lint:fix": "pnpm lint --apply", | ||
"preinstall": "npx only-allow pnpm", | ||
"test": "vitest dev", | ||
"test:cov": "vitest dev --coverage", | ||
"test:ci": "CI=true vitest --coverage", | ||
"test:ui": "vitest dev --ui" | ||
}, | ||
"keywords": [], | ||
"author": "", | ||
"license": "ISC", | ||
"types": "./dist/types/index.d.ts", | ||
"typings": "./dist/types/index.d.ts", | ||
"devDependencies": { | ||
"@types/node": "^20.4.5", | ||
"@vitest/coverage-v8": "^0.33.0", | ||
"rimraf": "^5.0.1", | ||
"rome": "^12.1.3", | ||
"ts-node": "^10.9.1", | ||
"tsconfig": "workspace:*", | ||
"typescript": "^5.1.6", | ||
"vitest": "^0.33.0" | ||
}, | ||
"dependencies": { | ||
"@rabbitholegg/questdk": "2.0.0-alpha.22", | ||
"ethers": "^5.2.0", | ||
"symbiosis-js-sdk": "^3.0.9", | ||
"viem": "^1.2.15" | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter' | ||
import { describe, expect, test } from 'vitest' | ||
import { bridge } from './Symbiosis' | ||
import { | ||
PASSING_TEST_TRANSACTIONS, | ||
FAILING_TEST_TRANSACTIONS, | ||
controlTransaction, | ||
} from './test-transactions' | ||
import { metaBurnABI, metaRouteABI } from './abi' | ||
import { ETH_CHAIN_ID, OPTIMISM_CHAIN_ID } from './constants' | ||
import { symbiosis } from './symbiosis-sdk' | ||
|
||
const TEST_USER = '0xB7e98B3F16CC915B9C7a321c1bd95fa406BDbabe' | ||
const OPTIMISM_USDCe_ADDRESS = '0x7F5c764cBc14f9669B88837ca1490cCa17c31607' | ||
|
||
describe('Given the symbiosis plugin', () => { | ||
describe('When handling the bridge', () => { | ||
test('should return a valid action filter', async () => { | ||
const filter = await bridge({ | ||
sourceChainId: OPTIMISM_CHAIN_ID, | ||
destinationChainId: ETH_CHAIN_ID, | ||
tokenAddress: OPTIMISM_USDCe_ADDRESS, | ||
amount: GreaterThanOrEqual(100000n), | ||
recipient: TEST_USER, | ||
}) | ||
|
||
expect(filter).to.deep.equal({ | ||
chainId: 10, | ||
to: symbiosis.metaRouter(10).address, | ||
input: { | ||
$abi: metaRouteABI, | ||
_metarouteTransaction: { | ||
approvedTokens: [OPTIMISM_USDCe_ADDRESS], | ||
amount: { | ||
$gte: '100000', | ||
}, | ||
otherSideCalldata: { | ||
$abiAbstract: metaBurnABI, | ||
_metaBurnTransaction: { | ||
chainID: 1, | ||
chain2address: TEST_USER, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) | ||
}) | ||
}) | ||
describe('should pass filter with valid transactions', () => { | ||
PASSING_TEST_TRANSACTIONS.forEach((testTransaction) => { | ||
test(testTransaction.description, async () => { | ||
const { | ||
transaction, | ||
destinationChainId, | ||
tokenAddress, | ||
amount, | ||
recipient, | ||
} = testTransaction | ||
|
||
const filter = await bridge({ | ||
sourceChainId: transaction.chainId, | ||
destinationChainId, | ||
tokenAddress, | ||
amount: GreaterThanOrEqual(amount), | ||
recipient, | ||
}) | ||
expect(apply(transaction, filter)).to.be.true | ||
}) | ||
}) | ||
}) | ||
describe('should not pass filter with invalid parameters', () => { | ||
test('when sourceChainId is incorrect', async () => { | ||
const { transaction, tokenAddress, recipient } = controlTransaction | ||
|
||
const filter = await bridge({ | ||
sourceChainId: 1, // 42161 | ||
destinationChainId: 5000, | ||
tokenAddress, | ||
amount: GreaterThanOrEqual(10000n), | ||
recipient, | ||
}) | ||
expect(apply(transaction, filter)).to.be.false | ||
}) | ||
test('when bridge contract address is incorrect', async () => { | ||
const { transaction, tokenAddress, recipient } = controlTransaction | ||
|
||
const filter = await bridge({ | ||
sourceChainId: 42161, | ||
destinationChainId: 5000, | ||
contractAddress: '0x1DCfbC3fA01b2a86bC3a3f43479cCe9E8D438Adc', | ||
tokenAddress, | ||
amount: GreaterThanOrEqual(10000n), | ||
recipient, | ||
}) | ||
expect(apply(transaction, filter)).to.be.false | ||
}) | ||
FAILING_TEST_TRANSACTIONS.forEach((testTransaction) => { | ||
test(testTransaction.description, async () => { | ||
const { | ||
transaction, | ||
destinationChainId, | ||
tokenAddress, | ||
amount, | ||
recipient, | ||
} = testTransaction | ||
|
||
const filter = await bridge({ | ||
sourceChainId: transaction.chainId, | ||
destinationChainId, | ||
tokenAddress, | ||
amount: GreaterThanOrEqual(amount), | ||
recipient, | ||
}) | ||
expect(apply(transaction, filter)).to.be.false | ||
}) | ||
}) | ||
}) | ||
describe('control transaction should pass filter', () => { | ||
test(controlTransaction.description, async () => { | ||
const { | ||
transaction, | ||
destinationChainId, | ||
tokenAddress, | ||
amount, | ||
recipient, | ||
} = controlTransaction | ||
|
||
const filter = await bridge({ | ||
sourceChainId: transaction.chainId, | ||
destinationChainId, | ||
tokenAddress, | ||
amount: GreaterThanOrEqual(amount), | ||
recipient, | ||
}) | ||
expect(apply(transaction, filter)).to.be.true | ||
}) | ||
}) | ||
}) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,54 @@ | ||
import { | ||
type TransactionFilter, | ||
type BridgeActionParams, | ||
compressJson, | ||
} from '@rabbitholegg/questdk' | ||
import { type Address } from 'viem' | ||
import { CHAIN_ID_ARRAY, CHAIN_TO_TOKENS } from './constants' | ||
import { metaBurnABI, metaRouteABI } from './abi' | ||
import { symbiosis } from './symbiosis-sdk' | ||
|
||
export const bridge = async ( | ||
bridge: BridgeActionParams, | ||
): Promise<TransactionFilter> => { | ||
const { | ||
sourceChainId, | ||
destinationChainId, | ||
contractAddress, | ||
tokenAddress, | ||
amount, | ||
recipient, | ||
} = bridge | ||
|
||
const bridgeContract: Address = | ||
contractAddress ?? (symbiosis.metaRouter(sourceChainId).address as Address) | ||
|
||
return compressJson({ | ||
chainId: sourceChainId, | ||
to: bridgeContract, | ||
input: { | ||
$abi: metaRouteABI, | ||
_metarouteTransaction: { | ||
approvedTokens: tokenAddress ? [tokenAddress] : undefined, // if tokenAddress is undefined, any input token will pass filter | ||
amount: amount, | ||
otherSideCalldata: { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Right here you should be able to do
And that should work - I wouldn't bother cause this looks like it's working fine and it's more precise but just to see how you'd use that. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this solution much better, and it allows us to remove the abi for the metaSynthesize function. Im going to go ahead and implement this. |
||
$abiAbstract: metaBurnABI, | ||
_metaBurnTransaction: { | ||
chainID: destinationChainId, | ||
chain2address: recipient, | ||
}, | ||
}, | ||
}, | ||
}, | ||
}) | ||
} | ||
|
||
export const getSupportedTokenAddresses = async ( | ||
_chainId: number, | ||
): Promise<Address[]> => { | ||
return CHAIN_TO_TOKENS[_chainId] ?? [] | ||
} | ||
|
||
export const getSupportedChainIds = async (): Promise<number[]> => { | ||
return CHAIN_ID_ARRAY | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I love this pattern - NICE
Gotta convert the other plugins to use this this is slick