Skip to content

Synapse SDK Plugin (Draft) #106

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

Merged
merged 9 commits into from
Dec 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/registry/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ import { Paraswap } from '@rabbitholegg/questdk-plugin-paraswap'
import { Rabbithole } from '@rabbitholegg/questdk-plugin-rabbithole'
import { Symbiosis } from '@rabbitholegg/questdk-plugin-symbiosis'
import { OkuTrade } from '@rabbitholegg/questdk-plugin-okutrade'
import { Synapse } from '@rabbitholegg/questdk-plugin-synapse'
import { Balancer } from '@rabbitholegg/questdk-plugin-balancer'
import { TraderJoe } from '@rabbitholegg/questdk-plugin-traderjoe'
import { ENTRYPOINT } from './contract-addresses'
Expand All @@ -50,6 +51,7 @@ export const plugins: Record<string, IActionPlugin> = {
[Paraswap.pluginId]: Paraswap,
[Rabbithole.pluginId]: Rabbithole,
[Symbiosis.pluginId]: Symbiosis,
[Synapse.pluginId]: Synapse,
[OkuTrade.pluginId]: OkuTrade,
[Balancer.pluginId]: Balancer,
[TraderJoe.pluginId]: TraderJoe,
Expand Down
19 changes: 19 additions & 0 deletions packages/synapse/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# @rabbitholegg/questdk-plugin-project

## 1.0.0-alpha.6

### Minor Changes

- [#71](https://github.com/rabbitholegg/questdk-plugins/pull/71) [`53d2ee5`](https://github.com/rabbitholegg/questdk-plugins/commit/53d2ee51b9479008bd549ebacba29fcfd82c4684) Thanks [@Quazia](https://github.com/Quazia)! - Update questdk instance and add Paraswap Stake action

## 1.0.0-alpha.5

### Patch Changes

- [#79](https://github.com/rabbitholegg/questdk-plugins/pull/79) [`d6843bd`](https://github.com/rabbitholegg/questdk-plugins/commit/d6843bd48d83bc84ca652ea081fd825f37a5f6d7) Thanks [@Quazia](https://github.com/Quazia)! - Remove preinstall hook and update questdk

## 1.0.0-alpha.4

### Minor Changes

- [#29](https://github.com/rabbitholegg/questdk-plugins/pull/29) [`5fa3757`](https://github.com/rabbitholegg/questdk-plugins/commit/5fa375719ade2563143bafb4b690f7a53671fa82) Thanks [@Quazia](https://github.com/Quazia)! - Re-release all pre-existing plugins
25 changes: 25 additions & 0 deletions packages/synapse/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
When First creating a template for a project we generally want to find at most 4 transactions. Some bridges don't have a sense of a source and target chain, but for bridges that do we need more information. Some bridges might use the same ABI and address for all 4 of these transaction types!

## ETH/BASE TOKEN
When trying to bridge ETH or the base network token (Matic for Polygon for instance) sometimes the pattern is different.
We want to find a transaction from the base chain and the target chain that shows transferring ETH from and to those chains.
In general we refer to moving value from a base chain (mainnet) to a higher order chain a `deposit` and returning back to a base chain a `withdrawal`.
In the case where it's a general purpose bridge this process is simply a `transfer`. In that case you only need to find 1 transaction, not 2.

### Deposit ETH
This handles the case where we have ETH that we want to bridge from Mainnet to L2. A lot of times this is handled differently. Often times this triggers a `payable` function and requires the `bridge` function to use the `value` param.

### Withdraw ETH
This handles the case where we have ETH that we want to bridge from an L2 to Mainnet. Depending on this chain this can also be a payable, but often times will just be a separate contract address.

## ERC20
ERC20 transactions are by far the most common for bridges.
In some cases this might be the only transaction type you need to find, although most of the time ETH is handled different. If the protocol _always_ uses wrapped ETH, this may be the only transaction type.
### Deposit ERC20
Generally this is the same as moving from L2 to L1, but especially for native bridging sometimes there are precompiles for the withdraw process.
### Withdraw ERC20



You can use the following example code to pull down test transactions in the correct format easily:
https://viem.sh/docs/actions/public/getTransaction.html#example
50 changes: 50 additions & 0 deletions packages/synapse/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
{
"name": "@rabbitholegg/questdk-plugin-project",
"private": true,
"version": "1.0.0-alpha.6",
"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",
"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.8.7",
"@vitest/coverage-v8": "^0.33.0",
"rimraf": "^5.0.5",
"rome": "^12.1.3",
"ts-node": "^10.9.1",
"tsconfig": "workspace:*",
"typescript": "^5.2.2",
"vitest": "^0.33.0"
},
"dependencies": {
"@rabbitholegg/questdk": "2.0.0-alpha.25",
"viem": "^1.16.6"
}
}
222 changes: 222 additions & 0 deletions packages/synapse/src/Synapse.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter'
import { describe, expect, test } from 'vitest'
import { bridge } from './Synapse.js'
import {
DEPOSIT_ETH,
WITHDRAW_ERC20,
WITHDRAW_ETH,
DEPOSIT_ERC20,
DEPOSIT_CCTP,
WITHDRAW_CCTP

} from './test-transactions.js'
import {
ARBITRUM_CHAIN_ID,
ETH_CHAIN_ID,
BSC_CHAIN_ID
} from './chain-ids.js'
import { SYNAPSE_BRIDGE_FRAGMENTS } from './abi.js'
import { parseEther } from 'viem'
import { SynapseCCTPContract, getContractAddress, CHAIN_TO_ROUTER } from './contract-addresses'


const ARBITRUM_USDCE_ADDRESS = '0xff970a61a04b1ca14834a43f5de4533ebddb5cc8'
const ARBITRUM_USDC_ADDRESS = '0xaf88d065e77c8cc2239327c5edb3a432268e5831'
const ETHEREUM_USDC_ADDRESS = '0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48'

// A random Ethereum Address
const TEST_USER = '0xF57D86F6bFcc76AA2C7f62616B2436C60Ad397e2'


describe('When given the Synapse plugin', () => {
describe('When generating the filter', () => {

test('Should return a valid bridge action filter for L2 token tx', async () => {
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: ARBITRUM_USDCE_ADDRESS,
amount: GreaterThanOrEqual(100000n),
recipient: TEST_USER
})

expect(filter).to.deep.equal({
chainId: ARBITRUM_CHAIN_ID,
to: CHAIN_TO_ROUTER[ARBITRUM_CHAIN_ID],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
to: TEST_USER,
amount: {
$gte: '100000'
},
chainId: ETH_CHAIN_ID,
token: ARBITRUM_USDCE_ADDRESS,
},
})
})
test('Should return a valid transaction for a L1 -> L2 transaction (Non CCTP)'), async () => {
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARBITRUM_CHAIN_ID,
tokenAddress: ETHEREUM_USDC_ADDRESS,
amount: GreaterThanOrEqual(100000n),
recipient: TEST_USER
})

expect(filter).to.deep.equal({
chainId: ETH_CHAIN_ID,
to: CHAIN_TO_ROUTER[ETH_CHAIN_ID],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
to: TEST_USER,
amount: {
$gte: '100000'
},
chainId: ARBITRUM_CHAIN_ID,
token: ETHEREUM_USDC_ADDRESS,
},
})
}
test('Should return a valid transaction for a L2 -> L1 transaction (Non CCTP)'), async () => {
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: ARBITRUM_USDCE_ADDRESS,
amount: GreaterThanOrEqual(100000n),
recipient: TEST_USER
})

expect(filter).to.deep.equal({
chainId: ARBITRUM_CHAIN_ID,
to: CHAIN_TO_ROUTER[ARBITRUM_CHAIN_ID],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
to: TEST_USER,
amount: {
$gte: '100000'
},
chainId: ETH_CHAIN_ID,
token: ARBITRUM_USDCE_ADDRESS,
},
})
}
// CCTP Transactions
test('Should return a valid transaction for a L1 -> L2 transaction (CCTP)'), async () => {
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARBITRUM_CHAIN_ID,
tokenAddress: ETHEREUM_USDC_ADDRESS,
amount: GreaterThanOrEqual(100000n),
recipient: TEST_USER,
contractAddress: getContractAddress(ETH_CHAIN_ID),
})

expect(filter).to.deep.equal({
chainId: ETH_CHAIN_ID,
// Update
to: SynapseCCTPContract[ETH_CHAIN_ID],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
//Update
sender: TEST_USER,
amount: {
$gte: '100000'
},
chainId: ARBITRUM_CHAIN_ID,
token: ETHEREUM_USDC_ADDRESS,
},
})
}
test('Should return a valid transaction for a L2 -> L1 transaction (CCTP)'), async () => {
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: ARBITRUM_USDC_ADDRESS,
amount: GreaterThanOrEqual(100000n),
recipient: TEST_USER,
contractAddress: getContractAddress(ETH_CHAIN_ID),
})

expect(filter).to.deep.equal({
chainId: ARBITRUM_CHAIN_ID,
to: SynapseCCTPContract[ARBITRUM_CHAIN_ID],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
sender: TEST_USER,
amount: {
$gte: '100000'
},
chainId: ETH_CHAIN_ID,
token: ARBITRUM_USDC_ADDRESS,
},
})
}

describe('When applying the filter', () => {
test('should pass filter with valid L1 ETH tx', async () => {
const transaction = DEPOSIT_ETH
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: ARBITRUM_CHAIN_ID,
tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
amount: GreaterThanOrEqual(parseEther('.2')),
})
expect(apply(transaction, filter)).to.be.true
})
test('should pass filter with valid L2 ETH tx', async () => {
const transaction = WITHDRAW_ETH
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE',
amount: GreaterThanOrEqual(parseEther('.259')),

})
expect(apply(transaction, filter)).to.be.true
})
test('should pass filter with valid L1 Token tx', async () => {
const transaction = DEPOSIT_ERC20
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID,
destinationChainId: BSC_CHAIN_ID,
tokenAddress: ETHEREUM_USDC_ADDRESS,
amount: GreaterThanOrEqual('9'),
})
expect(apply(transaction, filter)).to.be.true
})
test('should pass filter with valid L2 token tx', async () => {
const transaction = WITHDRAW_ERC20
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID ,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: ARBITRUM_USDCE_ADDRESS ,
amount: GreaterThanOrEqual('4006'),
})
expect(apply(transaction, filter)).to.be.true
})
// These are going to fail ? Im not sure how the contract address thing works.
test('should pass filter with valid CCTP Deposit tx', async () => {
const transaction = DEPOSIT_CCTP
const filter = await bridge({
sourceChainId: ETH_CHAIN_ID ,
destinationChainId: ARBITRUM_CHAIN_ID,
tokenAddress: ETHEREUM_USDC_ADDRESS ,
amount: GreaterThanOrEqual('299'),
contractAddress: '0xd359bc471554504f683fbd4f6e36848612349ddf',
})
expect(apply(transaction, filter)).to.be.true
})
test('should pass filter with valid CCTP Withdraw tx', async () => {
const transaction = WITHDRAW_CCTP
const filter = await bridge({
sourceChainId: ARBITRUM_CHAIN_ID ,
destinationChainId: ETH_CHAIN_ID,
tokenAddress: ARBITRUM_USDC_ADDRESS ,
amount: GreaterThanOrEqual('94'),
contractAddress: '0xd359bc471554504f683fbd4f6e36848612349ddf',
})
expect(apply(transaction, filter)).to.be.true
})
})
})
})
60 changes: 60 additions & 0 deletions packages/synapse/src/Synapse.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@

import { type TransactionFilter, type BridgeActionParams, compressJson } from '@rabbitholegg/questdk'
import { type Address } from 'viem'
import { SYNAPSE_CCTP_ROUTER, CHAIN_TO_ROUTER } from './contract-addresses'
import { SYNAPSE_BRIDGE_FRAGMENTS } from './abi'
import { CHAIN_ID_ARRAY } from './chain-ids'
import { Token } from './Token'
import * as tokens from './tokens'

const allTokens: Token[] = Object.values(tokens)

export const bridge = async (bridge: BridgeActionParams): Promise<TransactionFilter> => {
// This is the information we'll use to compose the Transaction object
const {
sourceChainId,
destinationChainId,
contractAddress = null,
tokenAddress,
amount,
recipient,
} = bridge

// This if statement checks if the transaction is a CCTP transaction.
if (contractAddress == '0xd359bc471554504f683fbd4f6e36848612349ddf') {
return compressJson({
chainId: sourceChainId,
to: SYNAPSE_CCTP_ROUTER[sourceChainId],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS,
sender: recipient,
amount: amount,
chainId: destinationChainId,
token: tokenAddress,
},
})
}
// We always want to return a compressed JSON object which we'll transform into a TransactionFilter (for non cctp)
return compressJson({
chainId: sourceChainId, // The chainId of the source chain
to: CHAIN_TO_ROUTER[sourceChainId],
input: {
$abi: SYNAPSE_BRIDGE_FRAGMENTS, // The ABI of the bridge contract
to: recipient, // The recipient of tokens
amount: amount, // The amount of tokens to send
chainId: destinationChainId, // The chainId of the destination chian
token: tokenAddress, // The address of the token to be recieved
},
})
}

export const getSupportedTokenAddresses = async (chainId: number): Promise<Address[]> => {
const supportedTokens = allTokens.filter(token => token.addresses.hasOwnProperty(chainId));
return supportedTokens.map(token => token.addresses[chainId]) as Address[];
}


export const getSupportedChainIds = async (): Promise<number[]> => {
return CHAIN_ID_ARRAY
}

Loading