-
Notifications
You must be signed in to change notification settings - Fork 25
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #44 from mmackz/hyphen-bridge
feat(hyphen): add support for hyphen bridge
- Loading branch information
Showing
18 changed files
with
885 additions
and
1,355 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
--- | ||
"@rabbitholegg/questdk-plugin-hyphen": minor | ||
"@rabbitholegg/questdk-plugin-registry": minor | ||
--- | ||
|
||
add bridge support for hyphen |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
# Hyphen Plugin | ||
|
||
This plugin is designed to filter valid bridge transaction on the biconomy hyphen bridge | ||
|
||
## Overview | ||
|
||
The Hyphen Bridge is relatively simple compared to other bridges. The same function is used regardless of wether you are bridging to or from L1. There is 3 functions we are watching, ```depositNative``` for native tokens like ETH (or MATIC for Polygon), ```depositErc20``` for supported ERC-20 tokens, and ```depositErc20AndSwap``` which will swap out some of the bridged tokens for some of the native gas token on the receiving chain. | ||
|
||
There is also a ```depositNativeAndSwap``` function, but I cannot find any instances of it being used in the wild, so I have chosen to exclude it from the filter. | ||
|
||
## Example Transactions | ||
|
||
### depositNative | ||
- [From L1 to L2](https://etherscan.io/tx/0x8658d84686792ff03e4749dcd08cd750ec00632965d423214381595f32673dea) | ||
- [From L2 to L1](https://optimistic.etherscan.io/tx/0x39349b8bc309e3e861565b2a08efa6fb5bb1726713ba17ff166396c15147e625) | ||
|
||
|
||
### depositErc20 | ||
- https://etherscan.io/tx/0xbb7a23d915fd2b7e2df1e5116a785210c48671b0db5b790659db7f922d2c18ca | ||
|
||
### depositErc20AndSwap | ||
- https://polygonscan.com/tx/0x826839c49ecb2e25e263ad2299ac444d8e0bc92d92f8934d326a4ecd7ea8bc39 | ||
|
||
|
||
## Bridge Contract Addresses | ||
- Ethereum - https://etherscan.io/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b | ||
- Polygon - https://polygonscan.com/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b | ||
- Avalanche - https://snowtrace.io/address/0x2a5c2568b10a0e826bfa892cf21ba7218310180b | ||
- Optimism - https://optimistic.etherscan.io/address/0x856cb5c3cBBe9e2E21293A644aA1f9363CEE11E8 | ||
- Arbitrum - https://arbiscan.io/address/0x856cb5c3cBBe9e2E21293A644aA1f9363CEE11E8 | ||
- Binance - https://bscscan.com/address/0x94D3E62151B12A12A4976F60EdC18459538FaF08 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
{ | ||
"name": "@rabbitholegg/questdk-plugin-hyphen", | ||
"private": true, | ||
"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": "1.0.1-alpha.10", | ||
"viem": "^1.2.15" | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,114 @@ | ||
import { GreaterThanOrEqual, apply } from '@rabbitholegg/questdk/filter' | ||
import { bridge } from './Hyphen' | ||
import { describe, expect, test } from 'vitest' | ||
import { ABI } from './abi' | ||
import { CHAIN_TO_CONTRACT } from './chain-to-contract' | ||
import { NATIVE_TOKEN_ADDRESS, CHAIN_TO_TOKENS } from './chain-to-tokens' | ||
import { | ||
ARBITRUM_CHAIN_ID, | ||
BINANCE_CHAIN_ID, | ||
ETH_CHAIN_ID, | ||
OPTIMISM_CHAIN_ID, | ||
POLYGON_CHAIN_ID, | ||
} from './chain-ids' | ||
import { | ||
NATIVE_TRANSFER, | ||
ERC20_TRANSFER, | ||
ERC20_BRIDGE_SWAP, | ||
} from './test-transactions' | ||
|
||
describe('Given the Hyphen plugin', () => { | ||
describe('When handling the bridge', () => { | ||
const TEST_ADDRESS = '0x081F992BB28E1D32E138FFAB57AF8F8B932573B5' | ||
|
||
test('should return a valid bridge action filter using native token', async () => { | ||
// bridge ETH from Mainnet to Polygon | ||
const sourceChainId = ETH_CHAIN_ID | ||
const destinationChainId = POLYGON_CHAIN_ID | ||
|
||
const filter = await bridge({ | ||
sourceChainId, | ||
destinationChainId, | ||
tokenAddress: NATIVE_TOKEN_ADDRESS, | ||
recipient: TEST_ADDRESS, | ||
amount: GreaterThanOrEqual(100000n), | ||
}) | ||
expect(filter).to.deep.equal({ | ||
chainId: sourceChainId, | ||
to: CHAIN_TO_CONTRACT[sourceChainId], | ||
value: { | ||
$gte: '100000', | ||
}, | ||
input: { | ||
$abi: ABI, | ||
toChainId: destinationChainId, | ||
receiver: TEST_ADDRESS, | ||
}, | ||
}) | ||
}) | ||
|
||
test('should return a valid bridge action filter using erc-20 token', async () => { | ||
// bridge erc-20 from Optimism to Polygon | ||
const tokenAddress = CHAIN_TO_TOKENS[OPTIMISM_CHAIN_ID][0] | ||
const sourceChainId = OPTIMISM_CHAIN_ID | ||
const destinationChainId = POLYGON_CHAIN_ID | ||
|
||
const filter = await bridge({ | ||
sourceChainId, | ||
destinationChainId, | ||
tokenAddress, | ||
recipient: TEST_ADDRESS, | ||
amount: GreaterThanOrEqual(100000n), | ||
}) | ||
expect(filter).to.deep.equal({ | ||
chainId: sourceChainId, | ||
to: CHAIN_TO_CONTRACT[sourceChainId], | ||
input: { | ||
$abi: ABI, | ||
toChainId: destinationChainId, | ||
receiver: TEST_ADDRESS, | ||
tokenAddress, | ||
amount: { | ||
$gte: '100000', | ||
}, | ||
}, | ||
}) | ||
}) | ||
|
||
test('should pass filter when bridging native token', async () => { | ||
// Bridge ETH from Optimism to Arbitrum | ||
const filter = await bridge({ | ||
sourceChainId: OPTIMISM_CHAIN_ID, | ||
destinationChainId: ARBITRUM_CHAIN_ID, | ||
tokenAddress: NATIVE_TOKEN_ADDRESS, | ||
amount: GreaterThanOrEqual(100000n), | ||
}) | ||
|
||
expect(apply(NATIVE_TRANSFER, filter)).to.be.true | ||
}) | ||
|
||
test('should pass filter when bridging erc20 token', async () => { | ||
// Bridge USDT from Mainnet to Binance Chain | ||
const filter = await bridge({ | ||
sourceChainId: ETH_CHAIN_ID, | ||
destinationChainId: BINANCE_CHAIN_ID, | ||
tokenAddress: CHAIN_TO_TOKENS[ETH_CHAIN_ID][0], // USDT | ||
amount: GreaterThanOrEqual(100000n), | ||
}) | ||
|
||
expect(apply(ERC20_TRANSFER, filter)).to.be.true | ||
}) | ||
|
||
test('should pass filter when using bridge and swap function', async () => { | ||
// Bridge USDC from Mainnet to Optimism | ||
const filter = await bridge({ | ||
sourceChainId: ETH_CHAIN_ID, | ||
destinationChainId: OPTIMISM_CHAIN_ID, | ||
tokenAddress: CHAIN_TO_TOKENS[ETH_CHAIN_ID][1], // USDC | ||
amount: GreaterThanOrEqual(100000n), | ||
}) | ||
|
||
expect(apply(ERC20_BRIDGE_SWAP, filter)).to.be.true | ||
}) | ||
}) | ||
}) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,65 @@ | ||
import { | ||
type BridgeActionParams, | ||
type TransactionFilter, | ||
compressJson, | ||
} from '@rabbitholegg/questdk' | ||
import { type Address } from 'viem' | ||
import { ABI } from './abi.js' | ||
import { NATIVE_TOKEN_ADDRESS, CHAIN_TO_TOKENS } from './chain-to-tokens' | ||
import { CHAIN_TO_CONTRACT } from './chain-to-contract' | ||
import { type ChainIds, CHAIN_ID_ARRAY } from './chain-ids' | ||
|
||
export const bridge = async ( | ||
bridge: BridgeActionParams, | ||
): Promise<TransactionFilter> => { | ||
const { | ||
sourceChainId, | ||
destinationChainId, | ||
contractAddress, | ||
tokenAddress, | ||
amount, | ||
recipient, | ||
} = bridge | ||
|
||
const bridgeContract = | ||
contractAddress ?? CHAIN_TO_CONTRACT[sourceChainId as ChainIds] | ||
|
||
// if transfer is using the native gas token | ||
if (tokenAddress === NATIVE_TOKEN_ADDRESS) { | ||
return compressJson({ | ||
chainId: sourceChainId, // The chainId of the source chain | ||
to: bridgeContract, // The contract address of the bridge | ||
value: amount, | ||
input: { | ||
$abi: ABI, | ||
toChainId: destinationChainId, | ||
receiver: recipient, | ||
}, | ||
}) | ||
} | ||
|
||
// if transfer is for ERC-20 tokens | ||
return compressJson({ | ||
chainId: sourceChainId, | ||
to: bridgeContract, | ||
input: { | ||
$abi: ABI, | ||
toChainId: destinationChainId, | ||
tokenAddress, | ||
receiver: recipient, | ||
amount, | ||
}, | ||
}) | ||
} | ||
|
||
export const getSupportedTokenAddresses = async ( | ||
_chainId: number, | ||
): Promise<Address[]> => { | ||
// Given a specific chain we would expect this function to return a list of supported token addresses | ||
return CHAIN_TO_TOKENS[_chainId as ChainIds] ?? [] | ||
} | ||
|
||
export const getSupportedChainIds = async (): Promise<number[]> => { | ||
// This should return all of the ChainIds that are supported by the Project we're integrating | ||
return CHAIN_ID_ARRAY | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,57 @@ | ||
import { type Abi } from 'viem' | ||
|
||
export const ABI: Abi = [ | ||
{ | ||
inputs: [ | ||
{ internalType: 'address', name: 'receiver', type: 'address' }, | ||
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' }, | ||
{ internalType: 'string', name: 'tag', type: 'string' }, | ||
], | ||
name: 'depositNative', | ||
outputs: [], | ||
stateMutability: 'payable', | ||
type: 'function', | ||
}, | ||
{ | ||
inputs: [ | ||
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' }, | ||
{ internalType: 'address', name: 'tokenAddress', type: 'address' }, | ||
{ internalType: 'address', name: 'receiver', type: 'address' }, | ||
{ internalType: 'uint256', name: 'amount', type: 'uint256' }, | ||
{ internalType: 'string', name: 'tag', type: 'string' }, | ||
], | ||
name: 'depositErc20', | ||
outputs: [], | ||
stateMutability: 'nonpayable', | ||
type: 'function', | ||
}, | ||
{ | ||
inputs: [ | ||
{ internalType: 'address', name: 'tokenAddress', type: 'address' }, | ||
{ internalType: 'address', name: 'receiver', type: 'address' }, | ||
{ internalType: 'uint256', name: 'toChainId', type: 'uint256' }, | ||
{ internalType: 'uint256', name: 'amount', type: 'uint256' }, | ||
{ internalType: 'string', name: 'tag', type: 'string' }, | ||
{ | ||
components: [ | ||
{ internalType: 'address', name: 'tokenAddress', type: 'address' }, | ||
{ internalType: 'uint256', name: 'percentage', type: 'uint256' }, | ||
{ internalType: 'uint256', name: 'amount', type: 'uint256' }, | ||
{ | ||
internalType: 'enum SwapOperation', | ||
name: 'operation', | ||
type: 'uint8', | ||
}, | ||
{ internalType: 'bytes', name: 'path', type: 'bytes' }, | ||
], | ||
internalType: 'struct SwapRequest[]', | ||
name: 'swapRequest', | ||
type: 'tuple[]', | ||
}, | ||
], | ||
name: 'depositAndSwapErc20', | ||
outputs: [], | ||
stateMutability: 'nonpayable', | ||
type: 'function', | ||
}, | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
export const ETH_CHAIN_ID = 1 | ||
export const OPTIMISM_CHAIN_ID = 10 | ||
export const BINANCE_CHAIN_ID = 56 | ||
export const POLYGON_CHAIN_ID = 137 | ||
export const ARBITRUM_CHAIN_ID = 42161 | ||
export const AVALANCHE_CHAIN_ID = 43114 | ||
|
||
export const CHAIN_ID_ARRAY = [ | ||
ETH_CHAIN_ID, | ||
OPTIMISM_CHAIN_ID, | ||
BINANCE_CHAIN_ID, | ||
POLYGON_CHAIN_ID, | ||
ARBITRUM_CHAIN_ID, | ||
AVALANCHE_CHAIN_ID, | ||
] | ||
|
||
export type ChainIds = | ||
| typeof ETH_CHAIN_ID | ||
| typeof OPTIMISM_CHAIN_ID | ||
| typeof BINANCE_CHAIN_ID | ||
| typeof POLYGON_CHAIN_ID | ||
| typeof ARBITRUM_CHAIN_ID | ||
| typeof AVALANCHE_CHAIN_ID |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
import type { Address } from 'viem' | ||
|
||
import { | ||
MAIN_SPOKE, // mainnet, polygon, avalanche | ||
LAYER2_SPOKE, // arbitrum, optimism | ||
BINANCE_SPOKE, | ||
} from './contract-addresses.js' | ||
|
||
import { | ||
type ChainIds, | ||
ETH_CHAIN_ID, | ||
OPTIMISM_CHAIN_ID, | ||
BINANCE_CHAIN_ID, | ||
POLYGON_CHAIN_ID, | ||
ARBITRUM_CHAIN_ID, | ||
AVALANCHE_CHAIN_ID, | ||
} from './chain-ids.js' | ||
|
||
export const CHAIN_TO_CONTRACT: { [chainId in ChainIds]: Address } = { | ||
[ETH_CHAIN_ID]: MAIN_SPOKE, | ||
[OPTIMISM_CHAIN_ID]: LAYER2_SPOKE, | ||
[BINANCE_CHAIN_ID]: BINANCE_SPOKE, | ||
[POLYGON_CHAIN_ID]: MAIN_SPOKE, | ||
[ARBITRUM_CHAIN_ID]: LAYER2_SPOKE, | ||
[AVALANCHE_CHAIN_ID]: MAIN_SPOKE, | ||
} as const |
Oops, something went wrong.