Skip to content

Commit

Permalink
Merge pull request #401 from FrancoAguzzi/feat/add-propeller-heads
Browse files Browse the repository at this point in the history
feat: add propeller heads
  • Loading branch information
kvhnuke authored Mar 19, 2024
2 parents f85951e + 87c8766 commit 25ba092
Show file tree
Hide file tree
Showing 8 changed files with 439 additions and 3 deletions.
1 change: 1 addition & 0 deletions packages/swap/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
PROPELLER_HEADS_API_KEY=
20 changes: 19 additions & 1 deletion packages/swap/src/configs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,14 @@ import { NetworkNames } from "@enkryptcom/types";
import { numberToHex } from "web3-utils";
import { ProviderName, SupportedNetworkName, WalletIdentifier } from "./types";

const FEE_CONFIGS = {
type ProviderFeeConfig = Record<
WalletIdentifier,
{ referrer: string; fee: number }
>;

type ProvidersFeeConfigs = Partial<Record<ProviderName, ProviderFeeConfig>>;

const FEE_CONFIGS: ProvidersFeeConfigs = {
[ProviderName.oneInch]: {
[WalletIdentifier.enkrypt]: {
referrer: "0x551d9d8eb02e1c713009da8f7c194870d651054a",
Expand Down Expand Up @@ -43,6 +50,17 @@ const FEE_CONFIGS = {
fee: 0.025,
},
},
// TODO: update referrer addresses
[ProviderName.propellerHeads]: {
[WalletIdentifier.enkrypt]: {
referrer: "0xabe295bac4b5bce0edcf42d180a3a952ef718b9e",
fee: 0.00875,
},
[WalletIdentifier.mew]: {
referrer: "0x48ae878bf9f752ee65679c017e32e4cafac51696",
fee: 0.025,
},
},
};

const TOKEN_LISTS: {
Expand Down
21 changes: 19 additions & 2 deletions packages/swap/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import Paraswap from "./providers/paraswap";
import Changelly from "./providers/changelly";
import ZeroX from "./providers/zerox";
import Rango from "./providers/rango";
import PropellerHeads from "./providers/propeller-heads";
import NetworkDetails, {
isSupportedNetwork,
getSupportedNetworks,
Expand Down Expand Up @@ -57,9 +58,18 @@ class Swap extends EventEmitter {
| typeof Paraswap
| typeof ZeroX
| typeof Rango
| typeof Rango
| typeof PropellerHeads
)[];

private providers: (OneInch | Changelly | Paraswap | ZeroX | Rango)[];
private providers: (
| OneInch
| Changelly
| Paraswap
| ZeroX
| Rango
| PropellerHeads
)[];

private tokenList: FromTokenType;

Expand All @@ -81,7 +91,14 @@ class Swap extends EventEmitter {
};
this.api = options.api;
this.walletId = options.walletIdentifier;
this.providerClasses = [OneInch, Paraswap, Changelly, ZeroX, Rango];
this.providerClasses = [
OneInch,
Paraswap,
Changelly,
ZeroX,
Rango,
PropellerHeads,
];
this.topTokenInfo = {
contractsToId: {},
topTokens: {},
Expand Down
286 changes: 286 additions & 0 deletions packages/swap/src/providers/propeller-heads/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,286 @@
import dotenv from "dotenv";
import Web3Eth from "web3-eth";
import { numberToHex, stringToHex, toBN } from "web3-utils";
import {
EVMTransaction,
getQuoteOptions,
MinMaxResponse,
ProviderClass,
ProviderFromTokenResponse,
ProviderName,
ProviderQuoteResponse,
ProviderSwapResponse,
ProviderToTokenResponse,
QuoteMetaOptions,
StatusOptions,
StatusOptionsResponse,
SupportedNetworkName,
SwapQuote,
SwapTransaction,
TokenType,
TransactionStatus,
TransactionType,
} from "../../types";
import {
DEFAULT_SLIPPAGE,
FEE_CONFIGS,
GAS_LIMITS,
NATIVE_TOKEN_ADDRESS,
} from "../../configs";
import {
PropellerHeadsResponseType,
PropellerHeadsSwapResponse,
} from "./types";
import {
getAllowanceTransactions,
TOKEN_AMOUNT_INFINITY_AND_BEYOND,
} from "../../utils/approvals";
import { isEVMAddress } from "../../utils/common";

dotenv.config();

const { PROPELLER_HEADS_API_KEY } = process.env;

if (!PROPELLER_HEADS_API_KEY) {
throw new Error("PROPELLER_HEADS_API_KEY is not set");
}

const supportedNetworks: {
[key in SupportedNetworkName]?: { approvalAddress: string; chainId: string };
} = {
[SupportedNetworkName.Ethereum]: {
approvalAddress: "0x14f2b6ca0324cd2B013aD02a7D85541d215e2906",
chainId: "1",
},
[SupportedNetworkName.Zksync]: {
approvalAddress: "0xe832e655E4C3c36b2be5256915ECF8536a642f59",
chainId: "324",
},
[SupportedNetworkName.Starknet]: {
approvalAddress:
"0x060b1a6a696cbd77df0b6be6a2a951cf0fc7b951304a9371eac2f5d05a77357f",
chainId: "0x534e5f4d41494e",
},
};

const NetworkNamesToSupportedProppellerHeadsBlockchains: Partial<
Record<SupportedNetworkName, string>
> = {
[SupportedNetworkName.Ethereum]: "ethereum",
[SupportedNetworkName.Zksync]: "zksync",
[SupportedNetworkName.Starknet]: "starknet",
};

const BASE_URL = "https://api.propellerheads.xyz/partner/v2";

class PropellerHeads extends ProviderClass {
tokenList: TokenType[];

network: SupportedNetworkName;

web3eth: Web3Eth;

name: ProviderName;

fromTokens: ProviderFromTokenResponse;

toTokens: ProviderToTokenResponse;

constructor(web3eth: Web3Eth, network: SupportedNetworkName) {
super(web3eth, network);
this.name = ProviderName.propellerHeads;
this.network = network;
this.web3eth = web3eth;
this.tokenList = [];
this.fromTokens = {};
this.toTokens = {};
}

init(tokenList: TokenType[]): Promise<void> {
if (!PropellerHeads.isSupported(this.network)) return;
tokenList.forEach((t) => {
this.fromTokens[t.address] = t;
if (!this.toTokens[this.network]) this.toTokens[this.network] = {};
this.toTokens[this.network][t.address] = {
...t,
networkInfo: {
name: this.network,
isAddress: (address: string) =>
Promise.resolve(isEVMAddress(address)),
},
};
});
}

static isSupported(network: SupportedNetworkName) {
return Object.keys(supportedNetworks).includes(
network as unknown as string
);
}

getFromTokens() {
return this.fromTokens;
}

getToTokens() {
return this.toTokens;
}

getMinMaxAmount(): Promise<MinMaxResponse> {
return Promise.resolve({
minimumFrom: toBN("1"),
maximumFrom: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND),
minimumTo: toBN("1"),
maximumTo: toBN(TOKEN_AMOUNT_INFINITY_AND_BEYOND),
});
}

private getPropellerHeadsSwap(
options: getQuoteOptions,
meta: QuoteMetaOptions
): Promise<PropellerHeadsSwapResponse | null> {
if (
!PropellerHeads.isSupported(
options.toToken.networkInfo.name as SupportedNetworkName
) ||
this.network !== options.toToken.networkInfo.name
)
return Promise.resolve(null);

if (options.fromAddress.toLowerCase() !== options.toAddress.toLowerCase())
return Promise.resolve(null);

const body = {
orders: [
{
sell_token: options.fromToken.address,
buy_token: options.toToken.address,
sell_amount: options.amount.toString(),
origin_address: options.fromAddress,
},
],
};

const params = new URLSearchParams({
blockchain:
NetworkNamesToSupportedProppellerHeadsBlockchains[this.network],
});

return fetch(`${BASE_URL}/solver/quote?${params.toString()}`, {
method: "POST",
body: JSON.stringify(body),
headers: {
"x-api-key": PROPELLER_HEADS_API_KEY,
accept: "application/json",
"Content-Type": "application/json",
},
})
.then((res) => res.json())
.then(async (response: PropellerHeadsResponseType) => {
const transactions: SwapTransaction[] = [];
const transactionType: TransactionType =
this.network === SupportedNetworkName.Starknet
? TransactionType.generic
: TransactionType.evm;

if (options.fromToken.address !== NATIVE_TOKEN_ADDRESS) {
const approvalTxs = await getAllowanceTransactions({
infinityApproval: meta.infiniteApproval,
spender: supportedNetworks[this.network].approvalAddress,
web3eth: this.web3eth,
amount: options.amount,
fromAddress: options.fromAddress,
fromToken: options.fromToken,
});
transactions.push(...approvalTxs);
}
transactions.push({
from: options.fromAddress,
gasLimit: GAS_LIMITS.swap,
to: options.fromAddress,
value: numberToHex(options.amount),
data: stringToHex(JSON.stringify(response)),
type: transactionType,
});

return {
transactions,
toTokenAmount: toBN(response.quotes[0].buy_amount),
fromTokenAmount: toBN(response.quotes[0].sell_amount),
};
})
.catch((e) => {
console.error(e);
return null;
});
}

getQuote(
options: getQuoteOptions,
meta: QuoteMetaOptions
): Promise<ProviderQuoteResponse | null> {
return this.getPropellerHeadsSwap(options, meta).then(async (res) => {
if (!res) return null;
const response: ProviderQuoteResponse = {
fromTokenAmount: res.fromTokenAmount,
additionalNativeFees: toBN(0),
toTokenAmount: res.toTokenAmount,
provider: this.name,
quote: {
meta,
options,
provider: this.name,
},
totalGaslimit: res.transactions.reduce(
(total: number, curVal: EVMTransaction) =>
total + toBN(curVal.gasLimit).toNumber(),
0
),
minMax: await this.getMinMaxAmount(),
};
return response;
});
}

getSwap(quote: SwapQuote): Promise<ProviderSwapResponse | null> {
return this.getPropellerHeadsSwap(quote.options, quote.meta).then((res) => {
if (!res) return null;
const feeConfig =
FEE_CONFIGS[this.name][quote.meta.walletIdentifier].fee || 0;
const response: ProviderSwapResponse = {
fromTokenAmount: res.fromTokenAmount,
additionalNativeFees: toBN(0),
provider: this.name,
toTokenAmount: res.toTokenAmount,
transactions: res.transactions,
slippage: quote.meta.slippage || DEFAULT_SLIPPAGE,
fee: feeConfig * 100,
getStatusObject: async (
options: StatusOptions
): Promise<StatusOptionsResponse> => ({
options,
provider: this.name,
}),
};
return response;
});
}

getStatus(options: StatusOptions): Promise<TransactionStatus> {
const promises = options.transactionHashes.map((hash) =>
this.web3eth.getTransactionReceipt(hash)
);
return Promise.all(promises).then((receipts) => {
// eslint-disable-next-line no-restricted-syntax
for (const receipt of receipts) {
if (!receipt || (receipt && !receipt.blockNumber)) {
return TransactionStatus.pending;
}
if (receipt && !receipt.status) return TransactionStatus.failed;
}
return TransactionStatus.success;
});
}
}

export default PropellerHeads;
Loading

1 comment on commit 25ba092

@github-actions
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.