Skip to content

Commit

Permalink
feat: Added Liquidity helper function in @buildwithsygma/utils (#407)
Browse files Browse the repository at this point in the history
When transferring fungible assets, its important to know whether
liquidity is available on destination chain. Therefore, v3 SDK should
provide interface to be able to query liquidity on destination chain and
it should throw and error if there isn't enough liquidity.

## Implementation details
- Interface for querying liquidity, fungible transfer passed as
parameter
- Destination provider should also be provided or created on demand by
the SDK to query balances

## Testing details
- Units tests for error and success cases

## Acceptance Criteria
- A clean tested interface to query liquidity on destination chain for a
specific 'transfer' object

## Closes
#383
  • Loading branch information
saadahmsiddiqui authored Aug 7, 2024
1 parent abe928d commit ad7b041
Show file tree
Hide file tree
Showing 47 changed files with 1,613 additions and 891 deletions.
21 changes: 7 additions & 14 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: 'Lint, Build, and Test'
name: "Lint, Build, and Test"

on:
push:
Expand All @@ -16,20 +16,13 @@ jobs:
node: [18, 20]
fail-fast: true
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
cache: 'yarn' # cache node modules
cache: "yarn" # cache node modules
node-version: ${{ matrix.node }}
- run: corepack enable
- run: yarn install --immutable
- run: yarn run core:lint # lint code
- run: yarn run core:build # compile typescript into javascript
- run: yarn run core:test:unit --silent --coverage # run unit tests
- run: yarn run evm:lint # lint code
- run: yarn run evm:build # compile typescript into javascript
- run: yarn run evm:test:unit --silent --coverage # run unit tests
- run: yarn run substrate:lint
- run: yarn run substrate:build
- run: yarn run substrate:test:unit --silent --coverage
# - run: yarn run test:integrations # run integration tests tests
- run: yarn run build
- run: yarn run lint
- run: yarn run test
2 changes: 1 addition & 1 deletion examples/substrate-to-evm-fungible-transfer/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"dependencies": {
"@buildwithsygma/core": "workspace:*",
"@buildwithsygma/substrate": "workspace:*",
"@polkadot/api": "^11.2.1",
"@polkadot/api": "^12.2.1",
"@polkadot/keyring": "^12.6.2",
"@polkadot/util-crypto": "^12.6.2",
"tsx": "^4.15.4"
Expand Down
6 changes: 6 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@
"substrate:lint": "yarn workspace @buildwithsygma/substrate lint",
"substrate:lint:fix": "yarn workspace @buildwithsygma/substrate lint:fix",
"substrate:test:unit": "yarn workspace @buildwithsygma/substrate test:unit",
"utils:build": "yarn workspace @buildwithsygma/utils build:all",
"utils:cleanDist": "yarn workspace @buildwithsygma/utils clean",
"utils:test": "yarn workspace @buildwithsygma/utils test",
"utils:lint": "yarn workspace @buildwithsygma/utils lint",
"utils:lint:fix": "yarn workspace @buildwithsygma/utils lint:fix",
"utils:test:unit": "yarn workspace @buildwithsygma/utils test:unit",
"docs": "typedoc"
},
"keywords": [],
Expand Down
85 changes: 85 additions & 0 deletions packages/core/src/baseTransfer.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import type { Config } from './config/config.js';
import type { Domainlike, EvmResource, Domain, SubstrateResource } from './types.js';

export interface BaseTransferParams {
source: Domainlike;
destination: Domainlike;
resource: string | EvmResource | SubstrateResource;
sourceAddress: string;
}

export abstract class BaseTransfer {
protected destinationDomain: Domain;
protected sourceDomain: Domain;
protected transferResource: EvmResource | SubstrateResource;
protected sygmaConfiguration: Config;

protected sourceAddress: string;

public get source(): Domain {
return this.sourceDomain;
}

public get destination(): Domain {
return this.destinationDomain;
}

public get resource(): EvmResource | SubstrateResource {
return this.transferResource;
}

public get config(): Config {
return this.sygmaConfiguration;
}

private findResource(
resource: string | EvmResource | SubstrateResource,
): EvmResource | SubstrateResource | undefined {
return this.sygmaConfiguration.getResources(this.source).find(_resource => {
return typeof resource === 'string'
? resource === _resource.resourceId
: resource.resourceId === _resource.resourceId;
});
}

protected constructor(transfer: BaseTransferParams, config: Config) {
this.sygmaConfiguration = config;
this.sourceAddress = transfer.sourceAddress;
this.sourceDomain = config.getDomain(transfer.source);
this.destinationDomain = config.getDomain(transfer.destination);
const resource = this.findResource(transfer.resource);

if (resource) {
this.transferResource = resource;
} else {
throw new Error('Resource not found.');
}
}
/**
* Method that checks whether the transfer
* is valid and route has been registered on
* the bridge
* @returns {boolean}
*/
// eslint-disable-next-line @typescript-eslint/require-await
async isValidTransfer(): Promise<boolean> {
throw new Error('Method not implemented.');
}
/**
* Set resource to be transferred
* @param {EvmResource} resource
* @returns {BaseTransfer}
*/
setResource(resource: EvmResource | SubstrateResource): void {
this.transferResource = resource;
}
/**
*
* @param destination
* @returns
*/
setDesinationDomain(destination: Domainlike): void {
const domain = this.config.getDomain(destination);
this.destinationDomain = domain;
}
}
2 changes: 0 additions & 2 deletions packages/core/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,8 +93,6 @@ export class Config {
case 'number':
return domain.chainId === domainLike;
}

return false;
});

if (!config) throw new Error('Domain configuration not found.');
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './baseTransfer.js';
export * from './config/config.js';
export * from './constants.js';
export * from './errors/customErrors.js';
Expand Down
74 changes: 59 additions & 15 deletions packages/core/src/multicall.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,19 +191,22 @@ type IndexerRouteWithFeeHandlerAddressAndType = IndexerRouteWithFeeHandlerAddres
feeHandlerType: FeeHandlerType;
};

type FeeHandlerAddressesMap = Map<string, Map<string, Map<string, string>>>;

export async function getFeeHandlerAddressesOfRoutes(params: {
routes: RouteIndexerType[];
chainId: number;
bridgeAddress: string;
provider: Eip1193Provider;
}): Promise<Array<IndexerRouteWithFeeHandlerAddress>> {
}): Promise<FeeHandlerAddressesMap> {
const web3Provider = new Web3Provider(params.provider);
const bridge = Bridge__factory.connect(params.bridgeAddress, web3Provider);
const feeHandlerRouterAddress = await bridge._feeHandler();
const feeHandlerRouter = FeeHandlerRouter__factory.createInterface();

const multicallAddress = getMulticallAddress(params.chainId);
const multicall = new Contract(multicallAddress, JSON.stringify(MulticallAbi), web3Provider);
const feeHandlerAddressesMap: FeeHandlerAddressesMap = new Map();

const calls = params.routes.map(route => ({
target: feeHandlerRouterAddress,
Expand All @@ -214,36 +217,77 @@ export async function getFeeHandlerAddressesOfRoutes(params: {
}));

const results = (await multicall.callStatic.aggregate(calls)) as AggregateStaticCallResponse;
return params.routes.map((route, idx) => {
return {
...route,
feeHandlerAddress: defaultAbiCoder.decode(['address'], results.returnData[idx]).toString(),
};
params.routes.map((route, idx) => {
let source = feeHandlerAddressesMap.get(route.fromDomainId);
if (!source) {
source = new Map();
feeHandlerAddressesMap.set(route.fromDomainId, source);
}

let destination = source.get(route.toDomainId);
if (!destination) {
destination = new Map();
source.set(route.toDomainId, destination);
}

destination.set(
route.resourceId,
defaultAbiCoder.decode(['address'], results.returnData[idx]).toString(),
);
});

return feeHandlerAddressesMap;
}

export async function getFeeHandlerTypeOfRoutes(params: {
routes: Array<IndexerRouteWithFeeHandlerAddress>;
routes: RouteIndexerType[];
feeHandlerAddressesMap: FeeHandlerAddressesMap;
chainId: number;
provider: Eip1193Provider;
}): Promise<Array<IndexerRouteWithFeeHandlerAddressAndType>> {
const web3Provider = new Web3Provider(params.provider);
const multicallAddress = getMulticallAddress(params.chainId);
const { routes, feeHandlerAddressesMap, chainId, provider } = params;

const web3Provider = new Web3Provider(provider);
const multicallAddress = getMulticallAddress(chainId);
const multicall = new Contract(multicallAddress, JSON.stringify(MulticallAbi), web3Provider);
const basicFeeHandlerInterface = BasicFeeHandler__factory.createInterface();

const calls = params.routes.map(route => ({
target: route.feeHandlerAddress,
callData: basicFeeHandlerInterface.encodeFunctionData('feeHandlerType'),
}));
let feeHandlerAddress: string | undefined;
const calls = routes.map(route => {
const source = feeHandlerAddressesMap.get(route.fromDomainId);
if (source) {
const destination = source.get(route.toDomainId);
if (destination) {
feeHandlerAddress = destination.get(route.resourceId);
}
}

return {
target: feeHandlerAddress,
callData: basicFeeHandlerInterface.encodeFunctionData('feeHandlerType'),
};
});

const results = (await multicall.callStatic.aggregate(calls)) as AggregateStaticCallResponse;

return params.routes.map((route, idx) => {
const source = feeHandlerAddressesMap.get(route.fromDomainId);
if (source) {
const destination = source.get(route.toDomainId);
if (destination) {
feeHandlerAddress = destination.get(route.resourceId);
}
}

const feeHandlerType = defaultAbiCoder.decode(
['string'],
results.returnData[idx],
)[0] as unknown as FeeHandlerType;

return {
...route,
feeHandlerAddress: route.feeHandlerAddress,
feeHandlerType: results.returnData[idx].toString() as FeeHandlerType,
feeHandlerAddress: feeHandlerAddress ?? '',
feeHandlerType,
};
});
}
24 changes: 14 additions & 10 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,9 @@ export async function getDomains(options: {

/**
* Returns supported routes originating from given source domain.
* @param source Either caip2 identifier, chainId or sygmaId
* @param environment
* @param options Allows selecting bridge instance (mainnet by default) and filtering routes by type.
* @param {Domainlike} source Either caip2 identifier or chainId or Domain Object from SDK
* @param {Environment} environment
* @param {{routeTypes?: RouteType[]; sourceProvider?: Eip1193Provider;}} options Allows selecting bridge instance (mainnet by default) and filtering routes by type.
*/
export async function getRoutes(
source: Domainlike,
Expand All @@ -148,15 +148,16 @@ export async function getRoutes(
>;

if (domainConfig.type === Network.EVM && options?.sourceProvider) {
const routesWithHandlerAddresses = await getFeeHandlerAddressesOfRoutes({
const feeHandlerAddressesMap = await getFeeHandlerAddressesOfRoutes({
routes: data.routes,
chainId: domainConfig.chainId,
bridgeAddress: domainConfig.bridge,
provider: options.sourceProvider,
});

routeFeeHandlerAddressesAndTypes = await getFeeHandlerTypeOfRoutes({
routes: routesWithHandlerAddresses,
feeHandlerAddressesMap,
routes: data.routes,
chainId: domainConfig.chainId,
provider: options.sourceProvider,
});
Expand All @@ -169,11 +170,12 @@ export async function getRoutes(

let routeWithTypeAndAddress;
if (routeFeeHandlerAddressesAndTypes) {
routeWithTypeAndAddress = routeFeeHandlerAddressesAndTypes.find(_route => {
_route.fromDomainId === route.fromDomainId &&
routeWithTypeAndAddress = routeFeeHandlerAddressesAndTypes.find(
_route =>
_route.fromDomainId === route.fromDomainId &&
_route.toDomainId === route.toDomainId &&
_route.resourceId === route.resourceId;
});
_route.resourceId === route.resourceId,
);
}

let feeHandler = undefined;
Expand All @@ -184,9 +186,11 @@ export async function getRoutes(
};
}

const toDomain = config.findDomainConfigBySygmaId(Number(route.toDomainId));

return {
fromDomain: config.getDomain(domainConfig.chainId),
toDomain: config.findDomainConfigBySygmaId(Number(route.toDomainId)),
toDomain: config.getDomain(toDomain.caipId),
resource: resource,
feeHandler,
};
Expand Down
2 changes: 1 addition & 1 deletion packages/core/test/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ export const mockedDevnetConfig = {
{
chainId: 111,
id: 112,
caipId: 113,
caipId: 'ethereum',
name: 'ethereum',
type: 'evm',
bridge: '0xb36C801f644908bAAe89b7C28ad57Af18638A6a9',
Expand Down
Loading

0 comments on commit ad7b041

Please sign in to comment.