Skip to content
This repository was archived by the owner on May 16, 2025. It is now read-only.
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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
},
"peerDependencies": {},
"dependencies": {
"@ethersproject/experimental": "^5.7.0",
"@types/qs": "^6.9.9",
"dotenv": "^16.3.1",
"qs": "^6.11.2"
Expand Down
189 changes: 189 additions & 0 deletions src/Unizen/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import qs from "qs";

import {
addEstimatesToTransactionRequest
} from "../";

import {
ISwapperParams,
validateQuoteParams,
ITransactionRequestWithEstimate,
} from "../types";

// UNIZEN specific types
import {
ICrossQuoteResult,
IQuoteParams,
IQuoteResult,
TransactionData,
SwapData,
CrossChainTransactionData
} from './types'

// curl https://api.zcx.com/trade/v1/info/chains
export const networkById: { [chainId: number]: string } = {
1: "ethereum",
10: "optimism",
56: "bsc",
137: "polygon",
250: "fantom",
8453: "base",
42161: "arbitrum",
43114: "avax",
};

const apiRoot = "http://api.zcx.com/trade/v1";
const apiKey = process.env?.UNIZEN_API_KEY;

const getExcludedDexesList = (o: ISwapperParams): Map<string,string[]> => {
let res = new Map<string, string[]>();
if (o.denyExchanges?.length){
res.set(String(o.inputChainId), o.denyExchanges?.concat(o.denyBridges ?? []));
if (o.outputChainId){
res.set(String(o.outputChainId), o.denyExchanges?.concat(o.denyBridges ?? []))
}
}
return res;
}

const getTargetAddress = async (version: string, chainId: number) : Promise<string | undefined> => {
const baseUrlSpender = `${apiRoot}/${chainId}/approval/spender?contractVersion=${version}`;
try {
const responseSpender = await fetch(baseUrlSpender, {
headers: apiKey ? { "x-api-key": apiKey } : {}
});
const responseJson = await responseSpender.json();
return responseJson.address;
} catch (e) {
console.error(`unizen getTargetAddress failed: ${e}`);
return undefined;
}
}

export const convertQuoteParams = (o: ISwapperParams): IQuoteParams => ({
fromTokenAddress: o.input,
chainId: String(o.inputChainId),
toTokenAddress: o.output,
destinationChainId: o.outputChainId ? String(o.outputChainId) : undefined,
amount: String(o.amountWei),
sender: o.payer,
receiver: o.receiver ?? undefined,
slippage: o.maxSlippage,
deadline: o.deadline,
isSplit: false,
excludedDexes: getExcludedDexesList(o),
});



export async function getTransactionRequest(o: ISwapperParams): Promise<ITransactionRequestWithEstimate | undefined> {
const isCrossSwap = !!o.outputChainId;
const quotes = isCrossSwap ? await getCrossChainQuote(o) : await getSingleChainQuote(o);
if (!quotes?.length) return;
const bestQuote = quotes[0];
const tradeType = isCrossSwap ? undefined : (bestQuote as IQuoteResult).tradeType;
const swapData = await getSwapData(o.inputChainId, bestQuote.transactionData, bestQuote.nativeValue, o.payer, o.receiver ?? o.payer, tradeType);
const spender = await getTargetAddress(swapData!.contractVersion, o.inputChainId);
console.log('spender', spender);
const tr = {
from: o.payer,
to: spender,
value: o.amountWei,
data: swapData?.data,
gasLimit: Number(BigInt(swapData!.estimateGas)) * 2
} as ITransactionRequestWithEstimate;

const toTokenAmount = isCrossSwap ? (bestQuote as ICrossQuoteResult).dstTrade.toTokenAmount : (bestQuote as IQuoteResult).toTokenAmount;
const tokenFromDecimals = isCrossSwap ? (bestQuote as ICrossQuoteResult).tradeParams.tokenInfo[0].decimals : (bestQuote as IQuoteResult).tokenFrom.decimals;
const tokenToDecimals = isCrossSwap ? (bestQuote as ICrossQuoteResult).tradeParams.tokenInfo[1].decimals : (bestQuote as IQuoteResult).tokenTo.decimals;

return addEstimatesToTransactionRequest({
tr,
inputAmountWei: BigInt(o.amountWei as string),
outputAmountWei: BigInt(toTokenAmount),
inputDecimals: tokenFromDecimals,
outputDecimals: tokenToDecimals,
approvalAddress: o.payer ?? '',
totalGasUsd: 0,
totalGasWei: swapData!.estimateGas
});
}

// cf. https://api.zcx.com/trade/docs#/Single%20chain%20methods/QuoteSingleController_getSingleQuote
// Get a quote for your desired transfer.
export async function getSingleChainQuote(o: ISwapperParams): Promise<IQuoteResult[] | undefined> {
if (!apiKey) console.warn("missing env.UNIZEN_API_KEY, using public");
if (!validateQuoteParams(o)) throw new Error("invalid input");
const params = convertQuoteParams(o);
const url = `${apiRoot}/${params.chainId}/quote/single?${qs.stringify(params, { skipNulls: true})}`;
try {
const res = await fetch(url, {
headers: apiKey ? { "x-api-key": apiKey } : {},
});
if (res.status >= 400)
throw new Error(`${res.status}: ${res.statusText} - ${await res.text?.() ?? '?'}`);
return await res.json();
} catch (e) {
console.error(`getSingleChainQuote failed: ${e}`);
}
}

// cf. https://api.zcx.com/trade/docs#/Cross%20chain%20methods/QuoteCrossController_getCrossQuote
// Get a quote for your desired transfer.
export async function getCrossChainQuote(o: ISwapperParams): Promise<ICrossQuoteResult[] | undefined> {
if (!apiKey) console.warn("missing env.UNIZEN_API_KEY, using public");
if (!validateQuoteParams(o)) throw new Error("invalid input");
const params = convertQuoteParams(o);
const url = `${apiRoot}/${params.chainId}/quote/cross?${qs.stringify(params, { skipNulls: true})}`;
try {
const res = await fetch(url, {
headers: apiKey ? { "x-api-key": apiKey } : {},
});
if (res.status >= 400)
throw new Error(`${res.status}: ${res.statusText} - ${await res.text?.() ?? '?'}`);
return await res.json();
} catch (e) {
console.error(`getCrossChainQuote failed: ${e}`);
}
}

// cf. https://api.zcx.com/trade/docs#/Single%20chain%20methods/SwapSingleController_getSingleSwap
// Generate trade data for an on-chain swap.
export async function getSwapData(chainId: number, transactionData: TransactionData | CrossChainTransactionData, nativeValue: string, account: string, receiver: string, tradeType?: number): Promise<SwapData | undefined> {
if (!apiKey) console.warn("missing env.UNIZEN_API_KEY, using public");
const swapType = tradeType ? "single": "cross";
const url = `${apiRoot}/${chainId}/swap/${swapType}`;
const body = tradeType
? {
transactionData,
nativeValue,
account,
receiver: receiver || account,
tradeType
}
: {
transactionData,
nativeValue,
account,
receiver: receiver || account
} ;
try {
const res = await fetch(url, {
method: "POST",
body: JSON.stringify(body),
headers: apiKey ?
{
"x-api-key": apiKey,
"Content-Type": "application/json"
} :
{
"Content-Type": "application/json"
},
});
if (res.status >= 400)
throw new Error(`${res.status}: ${res.statusText} - ${await res.text?.() ?? '?'}`);
return await res.json();
} catch (e) {
console.error(`getSwapData failed: ${e}`);
}
}
169 changes: 169 additions & 0 deletions src/Unizen/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
export interface IQuoteParams {
fromTokenAddress: string;
chainId: string;
toTokenAddress: string;
destinationChainId?: string;
amount: string;
sender: string;
receiver?: string;
deadline?: number;
slippage?: number;
excludedDexes?: Map<string, string[]>;
priceImpactProtectionPercentage?: number;
isSplit?: boolean;
disableEstimate?: boolean;
}

export interface IToken {
name: string;
symbol: string;
decimals: number;
contractAddress: string;
chainId: number;
buyTax: number;
sellTax: number;
}

export interface IQuoteResult {
fromTokenAmount: string;
toTokenAmount: string;
deltaAmount: string;
tokenFrom: IToken;
tokenTo: IToken;
tradeType: number;
protocol: QuoteProtocol[];
transactionData: TransactionData;
nativeValue: string;
contractVersion: string;
gasPrice: string;
estimateGas: string;
estimateGasError?: string;
}

export interface ICrossQuoteResult {
srcTrade: IQuoteResult;
dstTrade: IQuoteResult;
transactionData: CrossChainTransactionData;
nativeValue: string;
nativeFee: string;
processingTime: number;
tradeProtocol: string;
crossChainTradeQuotesType: string;
sourceChainId: number;
destinationChainId: number,
uuid:string;
apiKeyUuid: string;
contractVersion: string;
tradeParams: TradeParams;
providerInfo: ProviderInfo;
}

export type QuoteProtocol = {
name: string;
logo: string;
route: string[];
percentage: number;
}

export type TransactionData = {
info: TransactionDataInfo;
call: TransactionDataCall[];
}

export type TransactionDataInfo = {
srcToken: string;
dstToken: string;
deadline: number;
slippage: number;
tokenHasTaxes: boolean;
path: string[];
tradeType: number;
amountIn: string;
amountOutMin: string;
actualQuote: string;
uuid: string;
apiId: string;
userPSFee: number;
}

export type CrossChainTransactionData = {
srcCalls: TransactionDataCall[];
dstCalls: TransactionDataCall[];
params: CrossChainTransactionDataParams;
nativeFee: string;
tradeProtocol: string;
}

export type CrossChainTransactionDataParams = {
dstChain: number;
srcPool: number;
dstPool: number;
isFromNative: boolean;
srcToken: string;
amount: string;
nativeFee: string;
gasDstChain: number;
receiver: string;
dstToken: string;
actualQuote: string;
minQuote: string;
uuid: string;
userPSFee: number;
apiId: string;
tradeType: number;
}

export type TransactionDataCall = {
targetExchange: string;
sellToken: string;
buyToken: string;
amountDelta: string;
amount: string;
data: string;
}

export type ProviderInfo = {
name: string;
logo: string;
contractVersion: string;
website: string;
docsLink: string;
description: string;
}

export type TradeParams = {
tokenIn: string;
tokenOut: string;
sender: string;
slippage: number;
srcChainId: number;
dstChainId: number;
receiver: string;
inNative: boolean;
outNative: boolean;
deadline: number;
tokenInfo: IToken[];
amount: string;
uuid: string;
userPSFee: number;
uuidPercentage: number;
excludeeDexList: any;
chainIdToCurve: any;
srcChainTokenHasTaxes: boolean;
dstChainTokenHasTaxes: boolean;
}

export type SwapData = {
data: string;
contractVersion: string;
estimateGas: string;
estimateGasError: string;
nativeValue: string;
insufficientFunds: boolean;
insufficientGas: boolean;
insufficientAllowance: boolean;
allowance: string;
gasPrice: string;
maxFeePerGas: string;
maxPriorityFeePerGas: string;
}
4 changes: 3 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import * as KyberSwap from "./KyberSwap";
import * as Squid from "./Squid";
import * as LiFi from "./LiFi";
import * as Socket from "./Socket";
import * as Unizen from "./Unizen";

import {
Aggregator,
Expand All @@ -24,12 +25,13 @@ export const aggregatorById: { [key: string]: Aggregator } = {
[AggregatorId.KYBERSWAP]: <Aggregator>KyberSwap,
[AggregatorId.SQUID]: <Aggregator>Squid,
[AggregatorId.LIFI]: <Aggregator>LiFi,
[AggregatorId.UNIZEN]: <Aggregator>Unizen,
// Socket disabled until we implement the getStatus method properly
// [AggregatorId.SOCKET]: <Aggregator>Socket,
};

export const aggregatorsWithContractCalls = [AggregatorId.LIFI];
export const aggregatorsAvailable = [AggregatorId.LIFI, AggregatorId.SQUID];
export const aggregatorsAvailable = [AggregatorId.LIFI, AggregatorId.SQUID, AggregatorId.UNIZEN];

/**
* Extracts the `callData` from the `transactionRequest` (if any).
Expand Down
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export enum AggregatorId {
ONE_INCH = "ONE_INCH",
ZERO_X = "ZERO_X",
PARASWAP = "PARASWAP",
UNIZEN = "UNIZEN"
}

export interface Stringifiable {
Expand Down
Loading