diff --git a/.changeset/olive-worms-cut.md b/.changeset/olive-worms-cut.md new file mode 100644 index 00000000..d30cbcbc --- /dev/null +++ b/.changeset/olive-worms-cut.md @@ -0,0 +1,7 @@ +--- +'@galacticcouncil/xc-core': minor +'@galacticcouncil/xc-cfg': minor +'@galacticcouncil/xc-sdk': patch +--- + +instabridge base eurc diff --git a/.github/workflows/snapshot.yml b/.github/workflows/snapshot.yml index c10cb0f7..2a1b52c3 100644 --- a/.github/workflows/snapshot.yml +++ b/.github/workflows/snapshot.yml @@ -19,7 +19,6 @@ jobs: uses: actions/setup-node@v4 with: node-version: '22.21.1' - cache: 'npm' - name: 🔐 Authenticate with NPM run: echo "//registry.npmjs.org/:_authToken=${{ secrets.NPM_TOKEN }}" > ~/.npmrc diff --git a/packages/xc-cfg/src/builders/ContractBuilder.ts b/packages/xc-cfg/src/builders/ContractBuilder.ts index c6324c9b..7e6b80c1 100644 --- a/packages/xc-cfg/src/builders/ContractBuilder.ts +++ b/packages/xc-cfg/src/builders/ContractBuilder.ts @@ -1,5 +1,6 @@ import { Batch } from './contracts/Batch'; import { Erc20 } from './contracts/Erc20'; +import { InstaBridge } from './contracts/InstaBridge'; import { PolkadotXcm } from './contracts/PolkadotXcm'; import { Snowbridge } from './contracts/Snowbridge'; import { Wormhole } from './contracts/Wormhole'; @@ -8,6 +9,7 @@ export function ContractBuilder() { return { Batch, Erc20, + InstaBridge, PolkadotXcm, Snowbridge, Wormhole, diff --git a/packages/xc-cfg/src/builders/contracts/InstaBridge.ts b/packages/xc-cfg/src/builders/contracts/InstaBridge.ts new file mode 100644 index 00000000..0252d494 --- /dev/null +++ b/packages/xc-cfg/src/builders/contracts/InstaBridge.ts @@ -0,0 +1,79 @@ +import { + Abi, + Asset, + ContractConfig, + ContractConfigBuilder, + EvmChain, + EvmParachain, + Wormhole as Wh, +} from '@galacticcouncil/xc-core'; + +import { parseAssetId } from '../utils'; +import { h160 } from '@galacticcouncil/common'; + +const { H160 } = h160; + +/** + * Encode Hydration asset ID to EVM precompile address. + * Follows HydraErc20Mapping encoding from Hydration runtime. + */ +function toHydrationPrecompile(assetId: number): `0x${string}` { + const bytes = new Uint8Array(20); + bytes[15] = 1; + bytes[16] = (assetId >> 24) & 0xff; + bytes[17] = (assetId >> 16) & 0xff; + bytes[18] = (assetId >> 8) & 0xff; + bytes[19] = assetId & 0xff; + return ('0x' + Buffer.from(bytes).toString('hex')) as `0x${string}`; +} + +/** + * Pad H160 address to bytes32 (left-pad with zeros). + */ +function toBytes32(h160: `0x${string}`): `0x${string}` { + const addr = h160.replace('0x', '').toLowerCase(); + return `0x${'0'.repeat(24)}${addr}` as `0x${string}`; +} + +type BridgeViaWormholeOpts = { + destChain: EvmParachain; + destAsset: Asset; +}; + +const bridgeViaWormhole = (opts: BridgeViaWormholeOpts): ContractConfigBuilder => ({ + build: async (params) => { + const { address, amount, asset, source, destination } = params; + const ctx = source.chain as EvmChain; + const rcv = destination.chain as EvmParachain; + + const rcvWh = Wh.fromChain(opts.destChain); + + const assetId = ctx.getAssetId(asset); + const destAssetId = rcv.getAssetId(opts.destAsset); + + const instaBridgeAddress = ctx.getInstaBridge(); + if (!instaBridgeAddress) { + throw new Error(`InstaBridge not configured for ${ctx.name}`); + } + + return new ContractConfig({ + abi: Abi.InstaBridge, + address: instaBridgeAddress, + args: [ + parseAssetId(assetId), + amount, + rcvWh.getWormholeId(), + toHydrationPrecompile(destAssetId as number), + toBytes32(H160.fromAccount(address) as `0x${string}`), + ], + func: 'bridgeViaWormhole', + module: 'InstaBridge', + }); + }, +}); + +export const InstaBridge = () => { + return { + bridgeViaWormhole, + }; +}; diff --git a/packages/xc-cfg/src/chains/evm/base.ts b/packages/xc-cfg/src/chains/evm/base.ts index 2ae4547a..ed81b30a 100644 --- a/packages/xc-cfg/src/chains/evm/base.ts +++ b/packages/xc-cfg/src/chains/evm/base.ts @@ -30,6 +30,9 @@ export const base = new EvmChain({ ecosystem: Ecosystem.Ethereum, evmChain: evmChain, explorer: 'https://basescan.org/', + instaBridge: { + address: '0x73bab4cec782e1530117932cef8492ebe64e112e', + }, rpcs: ['https://stylish-quick-firefly.base-mainnet.quiknode.pro/'], wormhole: { id: 30, diff --git a/packages/xc-cfg/src/configs/evm/base/index.ts b/packages/xc-cfg/src/configs/evm/base/index.ts index d077665e..39646f01 100644 --- a/packages/xc-cfg/src/configs/evm/base/index.ts +++ b/packages/xc-cfg/src/configs/evm/base/index.ts @@ -2,13 +2,20 @@ import { AssetRoute, ChainRoutes } from '@galacticcouncil/xc-core'; import { eurc, eurc_mwh } from '../../../assets'; import { base } from '../../../chains'; -import { toHydrationViaWormholeTemplate } from './templates'; +import { + toHydrationViaInstaBridgeTemplate, + toHydrationViaWormholeTemplate, +} from './templates'; const toHydrationViaWormhole: AssetRoute[] = [ toHydrationViaWormholeTemplate(eurc, eurc_mwh), ]; +const toHydrationViaInstaBridge: AssetRoute[] = [ + toHydrationViaInstaBridgeTemplate(eurc, eurc_mwh), +]; + export const baseConfig = new ChainRoutes({ chain: base, - routes: [...toHydrationViaWormhole], + routes: [...toHydrationViaWormhole, ...toHydrationViaInstaBridge], }); diff --git a/packages/xc-cfg/src/configs/evm/base/templates.ts b/packages/xc-cfg/src/configs/evm/base/templates.ts index dff24db8..4f2f81ba 100644 --- a/packages/xc-cfg/src/configs/evm/base/templates.ts +++ b/packages/xc-cfg/src/configs/evm/base/templates.ts @@ -1,10 +1,7 @@ import { Asset, AssetRoute } from '@galacticcouncil/xc-core'; import { eth } from '../../../assets'; -import { - BalanceBuilder, - ContractBuilder, -} from '../../../builders'; +import { BalanceBuilder, ContractBuilder } from '../../../builders'; import { hydration, moonbeam } from '../../../chains'; import { Tag } from '../../../tags'; @@ -41,3 +38,35 @@ export function toHydrationViaWormholeTemplate( tags: [Tag.Mrl, Tag.Wormhole], }); } + +export function toHydrationViaInstaBridgeTemplate( + assetIn: Asset, + assetOut: Asset +): AssetRoute { + return new AssetRoute({ + source: { + asset: assetIn, + balance: BalanceBuilder().evm().erc20(), + fee: { + asset: eth, + balance: BalanceBuilder().evm().native(), + }, + destinationFee: { + asset: assetIn, + balance: BalanceBuilder().evm().erc20(), + }, + }, + destination: { + chain: hydration, + asset: assetOut, + fee: { + amount: 0, + asset: assetOut, + }, + }, + contract: ContractBuilder() + .InstaBridge() + .bridgeViaWormhole({ destChain: moonbeam, destAsset: assetOut }), + tags: [Tag.InstaBridge], + }); +} diff --git a/packages/xc-cfg/src/tags.ts b/packages/xc-cfg/src/tags.ts index f93bf07e..a6b8ec79 100644 --- a/packages/xc-cfg/src/tags.ts +++ b/packages/xc-cfg/src/tags.ts @@ -1,4 +1,5 @@ export enum Tag { + InstaBridge = 'InstaBridge', Wormhole = 'Wormhole', Relayer = 'Relayer', Snowbridge = 'Snowbridge', diff --git a/packages/xc-core/src/chain/EvmChain.ts b/packages/xc-core/src/chain/EvmChain.ts index 1c150913..6c779fdb 100644 --- a/packages/xc-core/src/chain/EvmChain.ts +++ b/packages/xc-core/src/chain/EvmChain.ts @@ -11,10 +11,15 @@ import { import { Snowbridge, SnowbridgeDef, Wormhole, WormholeDef } from '../bridge'; import { EvmClient } from '../evm'; +export type InstaBridgeDef = { + address: string; +}; + export interface EvmChainParams extends ChainParams { evmChain: EvmChainDef; id: number; rpcs?: string[]; + instaBridge?: InstaBridgeDef; snowbridge?: SnowbridgeDef; wormhole?: WormholeDef; } @@ -23,6 +28,7 @@ export class EvmChain extends Chain { readonly evmChain: EvmChainDef; readonly id: number; readonly rpcs?: string[]; + readonly instaBridge?: InstaBridgeDef; readonly snowbridge?: Snowbridge; readonly wormhole?: Wormhole; @@ -30,6 +36,7 @@ export class EvmChain extends Chain { evmChain, id, rpcs, + instaBridge, snowbridge, wormhole, ...others @@ -38,10 +45,15 @@ export class EvmChain extends Chain { this.evmChain = evmChain; this.id = id; this.rpcs = rpcs; + this.instaBridge = instaBridge; this.snowbridge = snowbridge && new Snowbridge(snowbridge); this.wormhole = wormhole && new Wormhole(wormhole); } + getInstaBridge(): string | undefined { + return this.instaBridge?.address; + } + get evmClient(): EvmClient { return new EvmClient(this.evmChain, this.rpcs); } diff --git a/packages/xc-core/src/evm/abi/InstaBridge.ts b/packages/xc-core/src/evm/abi/InstaBridge.ts new file mode 100644 index 00000000..fe763a32 --- /dev/null +++ b/packages/xc-core/src/evm/abi/InstaBridge.ts @@ -0,0 +1,32 @@ +export const INSTA_BRIDGE = [ + { + inputs: [ + { internalType: 'address', name: 'asset', type: 'address' }, + { internalType: 'uint256', name: 'amount', type: 'uint256' }, + { internalType: 'uint16', name: 'destChain', type: 'uint16' }, + { internalType: 'address', name: 'destAsset', type: 'address' }, + { internalType: 'bytes32', name: 'recipient', type: 'bytes32' }, + ], + name: 'bridgeViaWormhole', + outputs: [ + { internalType: 'uint64', name: 'transferSequence', type: 'uint64' }, + { internalType: 'uint64', name: 'messageSequence', type: 'uint64' }, + ], + stateMutability: 'payable', + type: 'function', + }, + { + inputs: [{ internalType: 'bytes', name: 'vaa', type: 'bytes' }], + name: 'completeTransfer', + outputs: [], + stateMutability: 'nonpayable', + type: 'function', + }, + { + inputs: [{ internalType: 'uint256', name: 'amount', type: 'uint256' }], + name: 'quoteFee', + outputs: [{ internalType: 'uint256', name: 'fee', type: 'uint256' }], + stateMutability: 'view', + type: 'function', + }, +] as const; diff --git a/packages/xc-core/src/evm/abi/index.ts b/packages/xc-core/src/evm/abi/index.ts index ea9d97f8..0b8d2995 100644 --- a/packages/xc-core/src/evm/abi/index.ts +++ b/packages/xc-core/src/evm/abi/index.ts @@ -3,6 +3,7 @@ import type { Abi as TAbi } from 'viem'; import { BATCH } from './Batch'; import { ERC20 } from './Erc20'; import { GMP } from './Gmp'; +import { INSTA_BRIDGE } from './InstaBridge'; import { META } from './Meta'; import { POLKADOT_XCM } from './PolkadotXcm'; import { SNOWBRIDGE } from './Snowbridge'; @@ -13,6 +14,7 @@ export const Abi: Record = { Batch: BATCH, Erc20: ERC20, Gmp: GMP, + InstaBridge: INSTA_BRIDGE, Meta: META, PolkadotXcm: POLKADOT_XCM, Snowbridge: SNOWBRIDGE,