Skip to content
Open
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
7 changes: 7 additions & 0 deletions .changeset/olive-worms-cut.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@galacticcouncil/xc-core': minor
'@galacticcouncil/xc-cfg': minor
'@galacticcouncil/xc-sdk': patch
---

basejump base eurc
1 change: 0 additions & 1 deletion .github/workflows/snapshot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions packages/xc-cfg/src/builders/ContractBuilder.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { Batch } from './contracts/Batch';
import { Erc20 } from './contracts/Erc20';
import { Basejump } from './contracts/Basejump';
import { PolkadotXcm } from './contracts/PolkadotXcm';
import { Snowbridge } from './contracts/Snowbridge';
import { Wormhole } from './contracts/Wormhole';
Expand All @@ -8,6 +9,7 @@ export function ContractBuilder() {
return {
Batch,
Erc20,
Basejump,
PolkadotXcm,
Snowbridge,
Wormhole,
Expand Down
24 changes: 24 additions & 0 deletions packages/xc-cfg/src/builders/FeeAmountBuilder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,8 +203,32 @@ function XcmPaymentApi() {
};
}

function Basejump() {
return {
quoteFee: (): FeeAmountConfigBuilder => ({
build: async ({ feeAsset, source }) => {
const ctx = source as EvmChain;
const basejumpAddress = ctx.getBasejump();
if (!basejumpAddress) {
throw new Error(`Basejump not configured for ${ctx.name}`);
}

const feeAssetId = ctx.getAssetId(feeAsset);
const fee = await ctx.evmClient.getProvider().readContract({
abi: Abi.Basejump,
address: basejumpAddress as `0x${string}`,
args: [feeAssetId as `0x${string}`],
functionName: 'quoteFee',
});
return { amount: fee } as FeeAmount;
},
}),
};
}

export function FeeAmountBuilder() {
return {
Basejump,
XcmPaymentApi,
Snowbridge,
Wormhole,
Expand Down
112 changes: 112 additions & 0 deletions packages/xc-cfg/src/builders/contracts/Basejump.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { Abi, ContractConfigBuilderParams } from '@galacticcouncil/xc-core';

import { eurc } from '../../assets';
import { base, hydration } from '../../chains';

import { Basejump } from './Basejump';

const buildCtx = (address: string) => {
return {
address,
amount: 1000000n,
asset: eurc,
sender: '0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0',
source: { chain: base },
destination: { chain: hydration },
} as ContractConfigBuilderParams;
};

describe('Basejump contract builder', () => {
describe('bridgeViaWormhole', () => {
it('should encode EVM H160 address with ETH\\0 prefix', async () => {
const h160 = '0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0';
const ctx = buildCtx(h160);
const config = await Basejump().bridgeViaWormhole().build(ctx);

const recipient = config.args[2] as string;
// ETH\0 = 0x45544800, then H160 lowercase, then 16 zero hex chars
expect(recipient.toLowerCase()).toBe(
'0x45544800' +
'71feb8b2849101a6e62e3369eaafdc6154cd0bc0' +
'0000000000000000'
);
});

it('should encode SS58 EVM account with ETH\\0 prefix', async () => {
// This SS58 address is the Hydration mapping of 0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0
const { h160 } = await import('@galacticcouncil/common');
const ss58 = h160.H160.toAccount('0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0');
const ctx = buildCtx(ss58);
const config = await Basejump().bridgeViaWormhole().build(ctx);

const recipient = config.args[2] as string;
expect(recipient.toLowerCase()).toBe(
'0x45544800' +
'71feb8b2849101a6e62e3369eaafdc6154cd0bc0' +
'0000000000000000'
);
});

it('should encode native SS58 substrate account as raw AccountId32', async () => {
// Alice's well-known AccountId32
const alice = '5GrwvaEF5zXb26Fz9rcQpDWS57CtERHpNehXCPcNoHGKutQY';
const ctx = buildCtx(alice);
const config = await Basejump().bridgeViaWormhole().build(ctx);

const recipient = config.args[2] as string;
// Alice's raw AccountId32
expect(recipient.toLowerCase()).toBe(
'0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d'
);
});

it('should produce correct contract config', async () => {
const ctx = buildCtx('0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0');
const config = await Basejump().bridgeViaWormhole().build(ctx);

expect(config.func).toBe('bridgeViaWormhole');
expect(config.module).toBe('Basejump');
expect(config.args).toHaveLength(3);
});

it('should pass asset address as first arg', async () => {
const ctx = buildCtx('0x71FeB8b2849101a6E62e3369eaAfDc6154CD0Bc0');
const config = await Basejump().bridgeViaWormhole().build(ctx);

// First arg is the asset ERC20 address on the source chain
const assetArg = (config.args[0] as string).toLowerCase();
const expectedAssetId = base.getAssetId(eurc)?.toString().toLowerCase();
expect(assetArg).toBe(expectedAssetId);
});
});

describe('ABI', () => {
it('quoteFee should accept address param (not uint256)', () => {
const abi = Abi.Basejump as readonly Record<string, unknown>[];
const quoteFee = abi.find(
(e) => e.type === 'function' && e.name === 'quoteFee'
) as Record<string, unknown> | undefined;

expect(quoteFee).toBeDefined();
const inputs = quoteFee!.inputs as { type: string; name: string }[];
expect(inputs).toHaveLength(1);
expect(inputs[0].type).toBe('address');
expect(inputs[0].name).toBe('asset');
});

it('bridgeViaWormhole should accept (address, uint256, bytes32)', () => {
const abi = Abi.Basejump as readonly Record<string, unknown>[];
const fn = abi.find(
(e) => e.type === 'function' && e.name === 'bridgeViaWormhole'
) as Record<string, unknown> | undefined;

expect(fn).toBeDefined();
const inputs = fn!.inputs as { type: string }[];
expect(inputs.map((i) => i.type)).toEqual([
'address',
'uint256',
'bytes32',
]);
});
});
});
53 changes: 53 additions & 0 deletions packages/xc-cfg/src/builders/contracts/Basejump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
Abi,
ContractConfig,
ContractConfigBuilder,
EvmChain,
} from '@galacticcouncil/xc-core';

import { parseAssetId } from '../utils';
import { h160 } from '@galacticcouncil/common';
import { AccountId } from 'polkadot-api';
import { toHex } from '@polkadot-api/utils';

const { H160, isEvmAddress } = h160;

/**
* Convert any address (H160 or SS58) to bytes32 AccountId.
*/
function toAccountId32(address: string): `0x${string}` {
const ss58 = isEvmAddress(address) ? H160.toAccount(address) : address;
return toHex(AccountId().enc(ss58)) as `0x${string}`;
}

const bridgeViaWormhole = (): ContractConfigBuilder => ({
build: async (params) => {
const { address, amount, asset, source } = params;
const ctx = source.chain as EvmChain;

const assetId = ctx.getAssetId(asset);

const basejumpAddress = ctx.getBasejump();
if (!basejumpAddress) {
throw new Error(`Basejump not configured for ${ctx.name}`);
}

return new ContractConfig({
abi: Abi.Basejump,
address: basejumpAddress,
args: [
parseAssetId(assetId),
amount,
toAccountId32(address),
],
func: 'bridgeViaWormhole',
module: 'Basejump',
});
},
});

export const Basejump = () => {
return {
bridgeViaWormhole,
};
};
3 changes: 3 additions & 0 deletions packages/xc-cfg/src/chains/evm/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ export const base = new EvmChain({
ecosystem: Ecosystem.Ethereum,
evmChain: evmChain,
explorer: 'https://basescan.org/',
basejump: {
address: '0xf5b9334e44f800382cb47fc19669401d694e529b',
},
rpcs: ['https://stylish-quick-firefly.base-mainnet.quiknode.pro/'],
wormhole: {
id: 30,
Expand Down
11 changes: 9 additions & 2 deletions packages/xc-cfg/src/configs/evm/base/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
toHydrationViaBasejumpTemplate,
toHydrationViaWormholeTemplate,
} from './templates';

const toHydrationViaWormhole: AssetRoute[] = [
toHydrationViaWormholeTemplate(eurc, eurc_mwh),
];

const toHydrationViaBasejump: AssetRoute[] = [
toHydrationViaBasejumpTemplate(eurc, eurc_mwh),
];

export const baseConfig = new ChainRoutes({
chain: base,
routes: [...toHydrationViaWormhole],
routes: [...toHydrationViaWormhole, ...toHydrationViaBasejump],
});
37 changes: 33 additions & 4 deletions packages/xc-cfg/src/configs/evm/base/templates.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,7 @@
import { Asset, AssetRoute } from '@galacticcouncil/xc-core';

import { eth } from '../../../assets';
import {
BalanceBuilder,
ContractBuilder,
} from '../../../builders';
import { BalanceBuilder, ContractBuilder, FeeAmountBuilder } from '../../../builders';
import { hydration, moonbeam } from '../../../chains';
import { Tag } from '../../../tags';

Expand Down Expand Up @@ -41,3 +38,35 @@ export function toHydrationViaWormholeTemplate(
tags: [Tag.Mrl, Tag.Wormhole],
});
}

export function toHydrationViaBasejumpTemplate(
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: FeeAmountBuilder().Basejump().quoteFee(),
asset: assetIn,
},
},
contract: ContractBuilder()
.Basejump()
.bridgeViaWormhole(),
tags: [Tag.Basejump],
});
}
1 change: 1 addition & 0 deletions packages/xc-cfg/src/tags.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export enum Tag {
Basejump = 'Basejump',
Wormhole = 'Wormhole',
Relayer = 'Relayer',
Snowbridge = 'Snowbridge',
Expand Down
12 changes: 12 additions & 0 deletions packages/xc-core/src/chain/EvmChain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import {
import { Snowbridge, SnowbridgeDef, Wormhole, WormholeDef } from '../bridge';
import { EvmClient } from '../evm';

export type BasejumpDef = {
address: string;
};

export interface EvmChainParams extends ChainParams<ChainAssetData> {
evmChain: EvmChainDef;
id: number;
rpcs?: string[];
basejump?: BasejumpDef;
snowbridge?: SnowbridgeDef;
wormhole?: WormholeDef;
}
Expand All @@ -23,13 +28,15 @@ export class EvmChain extends Chain<ChainAssetData> {
readonly evmChain: EvmChainDef;
readonly id: number;
readonly rpcs?: string[];
readonly basejump?: BasejumpDef;
readonly snowbridge?: Snowbridge;
readonly wormhole?: Wormhole;

constructor({
evmChain,
id,
rpcs,
basejump,
snowbridge,
wormhole,
...others
Expand All @@ -38,10 +45,15 @@ export class EvmChain extends Chain<ChainAssetData> {
this.evmChain = evmChain;
this.id = id;
this.rpcs = rpcs;
this.basejump = basejump;
this.snowbridge = snowbridge && new Snowbridge(snowbridge);
this.wormhole = wormhole && new Wormhole(wormhole);
}

getBasejump(): string | undefined {
return this.basejump?.address;
}

get evmClient(): EvmClient {
return new EvmClient(this.evmChain, this.rpcs);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/xc-core/src/evm/abi/Basejump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
export const BASEJUMP = [
{
inputs: [
{ internalType: 'address', name: 'asset', type: 'address' },
{ internalType: 'uint256', name: 'amount', type: 'uint256' },
{ 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: 'address', name: 'asset', type: 'address' }],
name: 'quoteFee',
outputs: [{ internalType: 'uint256', name: 'fee', type: 'uint256' }],
stateMutability: 'view',
type: 'function',
},
] as const;
Loading
Loading