Skip to content
Merged
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: 4 additions & 3 deletions apps/api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ app.use(express.json());

const providers: Record<string, Protocol> = {
stonfi_v2: new StonfiProvider(process.env.TON_URL || "https://toncenter.com"),
mayan: new MayanProvider(process.env.MAYAN_URL || "https://solana-rpc.publicnode.com"),
mayan: new MayanProvider(process.env.SOLANA_URL || "https://solana-rpc.publicnode.com"),
};

app.get('/:providerId/quote', async (req, res) => {
Expand All @@ -30,7 +30,7 @@ app.get('/:providerId/quote', async (req, res) => {
referral_bps: parseInt(req.query.referral_bps as string),
slippage_bps: parseInt(req.query.slippage_bps as string),
};

const quote = await provider.get_quote(request);
res.json(quote);
} catch (error) {
Expand All @@ -53,10 +53,11 @@ app.post('/:providerId/quote_data', async (req, res) => {
const quote_request = req.body as Quote;

try {
console.log("quote_request", quote_request);

const quote = await provider.get_quote_data(quote_request);
res.json(quote);
} catch (error) {
console.log("quote_request", quote_request);
if (error instanceof Error) {
res.status(500).json({ error: error.message });
} else {
Expand Down
5 changes: 3 additions & 2 deletions packages/swapper/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"@solana/web3.js": "1.98.0",
"@ston-fi/api": "0.21.0",
"@ston-fi/sdk": "2.3.0",
"@ton/ton": "15.2.1"
"@ton/ton": "15.2.1",
"ethers": "6.13.5"
},
"devDependencies": {
"@types/jest": "29.5.14",
Expand All @@ -28,4 +29,4 @@
"files": [
"dist"
]
}
}
21 changes: 21 additions & 0 deletions packages/swapper/src/bigint.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { parseUnits } from "ethers";

/**
* Converts a decimal string or number from one decimal precision to another
* @param value The decimal to convert
* @param toDecimals The target number of decimals
* @param fromDecimals The source number of decimals (default is 18)
* @returns A BigInt with the converted value
*/
export function parseDecimals(value: string | number, toDecimals: number, fromDecimals = 18): BigInt {
const parsedValue = parseUnits(value.toString(), fromDecimals);
const decimalDiff = fromDecimals - toDecimals;

if (decimalDiff > 0) {
return BigInt(parsedValue) / BigInt(10) ** BigInt(decimalDiff);
} else if (decimalDiff < 0) {
return BigInt(parsedValue) * BigInt(10) ** BigInt(Math.abs(decimalDiff));
}

return BigInt(parsedValue);
}
13 changes: 13 additions & 0 deletions packages/swapper/src/mayan/evm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { QuoteRequest, QuoteData } from "@gemwallet/types";
import { Quote as MayanQuote, getSwapFromEvmTxPayload } from "@mayanfinance/swap-sdk";

export function buildEvmQuoteData(request: QuoteRequest, routeData: MayanQuote): QuoteData {
const signerChainId = routeData.fromToken.chainId;
const swapData = getSwapFromEvmTxPayload(routeData, request.from_address, request.to_address, { evm: request.referral_address }, request.from_address, signerChainId, null, null);
const value = BigInt(swapData.value || 0);
return {
to: swapData.to?.toString() || "",
value: value.toString(),
data: swapData.data?.toString() || "0x",
};
}
21 changes: 21 additions & 0 deletions packages/swapper/src/mayan/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { parseDecimals } from "../bigint";

describe('Fetch Quote', () => {
it('Parse decimal string to BigInt string', () => {
const output_value = "0.1384511931777";
const output_min_value = "0.13641535";

const output_value_bigint = parseDecimals(output_value, 9);
const output_min_value_bigint = parseDecimals(output_min_value, 9);

expect(output_value_bigint.toString()).toEqual("138451193");
expect(output_min_value_bigint.toString()).toEqual("136415350");
});

it('Convert hex BigInt string to decimal', () => {
const hexValue = "0x2386f26fc10000";
const expectedDecimalValue = BigInt(hexValue).toString();
expect(expectedDecimalValue).toEqual("10000000000000000");
});
});

142 changes: 24 additions & 118 deletions packages/swapper/src/mayan/index.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,29 @@
import { fetchQuote, getSwapFromEvmTxPayload, ChainName, QuoteParams, QuoteOptions, Quote as MayanQuote, createSwapFromSolanaInstructions, ReferrerAddresses } from "@mayanfinance/swap-sdk";
import { fetchQuote, ChainName, QuoteParams, QuoteOptions, Quote as MayanQuote } from "@mayanfinance/swap-sdk";
import { QuoteRequest, Quote, QuoteData, Asset, Chain } from "@gemwallet/types";
import { Protocol } from "../protocol";
import { Connection, MessageV0, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";
import { buildEvmQuoteData } from "./evm";
import { buildSolanaQuoteData } from "./solana";
import { parseDecimals } from "../bigint";

export class MayanProvider implements Protocol {

private rpcEndpoint: string;
constructor(rpcEndpoint: string) {
this.rpcEndpoint = rpcEndpoint;
}

mapAssetToTokenId(asset: Asset): string {
if (asset.isNative()) {
if (asset.chain === Chain.SOLANA) {
return "So11111111111111111111111111111111111111112";
} else {
return "0x0000000000000000000000000000000000000000";
}
return "0x0000000000000000000000000000000000000000";
}
return asset.tokenId!;
}

mapChainToName(chain: Chain): ChainName {
switch (chain) {
case Chain.SMARTCHAIN:
case Chain.SmartChain:
return "bsc";
case Chain.AvalancheC:
return "avalance" as ChainName;
default:
return chain as ChainName;
}
Expand All @@ -44,137 +43,44 @@ export class MayanProvider implements Protocol {
referrer: quoteRequest.referral_address,
referrerBps: quoteRequest.referral_bps
}

// explicitly set which types of quotes we want to fetch
const options: QuoteOptions = {
"wormhole": true,
"swift": true,
"fastMctp": false,
"gasless": false,
"mctp": true,
"shuttle": false,
"fastMctp": true,
"onlyDirect": false,
}

const timestamp = Date.now();
const quotes = await fetchQuote(params, options);
const latency = Date.now() - timestamp;
console.log("Mayan quote latency: ", latency);

if (!quotes || quotes.length === 0) {
throw new Error("No quotes available");
}

const quote = quotes[0];

const output_value = parseDecimals(quote.expectedAmountOut, quote.toToken.decimals);
const output_min_value = parseDecimals(quote.minAmountOut, quote.toToken.decimals);

return {
quote: quoteRequest,
output_value: quote.expectedAmountOut.toString(),
output_min_value: quote.minAmountOut.toString(),
output_value: output_value.toString(),
output_min_value: output_min_value.toString(),
route_data: quote
};
}

async get_quote_data(quote: Quote): Promise<QuoteData> {
const fromAsset = Asset.fromString(quote.quote.from_asset.toString());

if (fromAsset.chain === Chain.SOLANA) {
return this.buildSolanaQuoteData(quote.quote, quote.route_data as MayanQuote);
if (fromAsset.chain === Chain.Solana) {
return buildSolanaQuoteData(quote.quote, quote.route_data as MayanQuote, this.rpcEndpoint);
} else {
return this.buildEvmQuoteData(quote.quote, quote.route_data as MayanQuote);
return buildEvmQuoteData(quote.quote, quote.route_data as MayanQuote);
}
}

buildEvmQuoteData(request: QuoteRequest, routeData: MayanQuote): QuoteData {
const signerChainId = routeData.fromToken.chainId;
const swapData = getSwapFromEvmTxPayload(routeData, request.from_address, request.to_address, { evm: request.referral_address }, request.from_address, signerChainId, null, null);

return {
to: swapData.to?.toString() || "",
value: swapData.value?.toString() || "0",
data: swapData.data?.toString() || "0x",
};
}

async buildSolanaQuoteData(request: QuoteRequest, routeData: MayanQuote): Promise<QuoteData> {
const connection = new Connection(this.rpcEndpoint);
const referrerAddresses = { solana: request.referral_address };
const { serializedTrx } = await this.prepareSolanaSwapTransaction(
routeData,
request.from_address,
request.to_address,
referrerAddresses,
connection
);

return {
to: "",
value: "0",
data: Buffer.from(serializedTrx).toString("base64"),
};
}

async prepareSolanaSwapTransaction(
quote: MayanQuote,
swapperWalletAddress: string,
destinationAddress: string,
referrerAddresses: ReferrerAddresses,
connection: Connection,
): Promise<{
serializedTrx: Uint8Array,
additionalInfo: {
blockhash: string,
lastValidBlockHeight: number,
isVersionedTransaction: boolean,
feePayer: string,
}
}> {

const {
instructions,
signers,
lookupTables,
} = await createSwapFromSolanaInstructions(
quote, swapperWalletAddress, destinationAddress,
referrerAddresses, connection, { separateSwapTx: false });

const swapper = new PublicKey(swapperWalletAddress);
const feePayer = quote.gasless ? new PublicKey(quote.relayer) : swapper;

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();

let serializedTrx: Uint8Array;
let isVersionedTransaction = false;

if (lookupTables.length > 0) {
isVersionedTransaction = true;
const message = MessageV0.compile({
instructions,
payerKey: feePayer,
recentBlockhash: blockhash,
addressLookupTableAccounts: lookupTables,
});
const transaction = new VersionedTransaction(message);
transaction.sign(signers);
serializedTrx = transaction.serialize();
} else {
const transaction = new Transaction();
transaction.recentBlockhash = blockhash;
transaction.feePayer = feePayer;

instructions.forEach(instruction => transaction.add(instruction));

if (signers.length > 0) {
transaction.partialSign(...signers);
}

serializedTrx = transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
}

return {
serializedTrx,
additionalInfo: {
blockhash,
lastValidBlockHeight,
isVersionedTransaction,
feePayer: feePayer.toBase58(),
}
};
}
}
91 changes: 91 additions & 0 deletions packages/swapper/src/mayan/solana.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { QuoteRequest, QuoteData } from "@gemwallet/types";
import { Quote as MayanQuote, ReferrerAddresses, createSwapFromSolanaInstructions } from "@mayanfinance/swap-sdk";
import { Connection, MessageV0, PublicKey, Transaction, VersionedTransaction } from "@solana/web3.js";

export async function buildSolanaQuoteData(request: QuoteRequest, routeData: MayanQuote, rpcEndpoint: string): Promise<QuoteData> {
const connection = new Connection(rpcEndpoint);
const referrerAddresses = { solana: request.referral_address };
const { serializedTrx } = await prepareSolanaSwapTransaction(
routeData,
request.from_address,
request.to_address,
referrerAddresses,
connection
);

return {
to: "",
value: "0",
data: Buffer.from(serializedTrx).toString("base64"),
};
}

async function prepareSolanaSwapTransaction(
quote: MayanQuote,
swapperWalletAddress: string,
destinationAddress: string,
referrerAddresses: ReferrerAddresses,
connection: Connection,
): Promise<{
serializedTrx: Uint8Array,
additionalInfo: {
blockhash: string,
lastValidBlockHeight: number,
isVersionedTransaction: boolean,
feePayer: string,
}
}> {
const {
instructions,
signers,
lookupTables,
} = await createSwapFromSolanaInstructions(
quote, swapperWalletAddress, destinationAddress,
referrerAddresses, connection, { separateSwapTx: false });

const swapper = new PublicKey(swapperWalletAddress);
const feePayer = quote.gasless ? new PublicKey(quote.relayer) : swapper;

const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash();

let serializedTrx: Uint8Array;
let isVersionedTransaction = false;

if (lookupTables.length > 0) {
isVersionedTransaction = true;
const message = MessageV0.compile({
instructions,
payerKey: feePayer,
recentBlockhash: blockhash,
addressLookupTableAccounts: lookupTables,
});
const transaction = new VersionedTransaction(message);
transaction.sign(signers);
serializedTrx = transaction.serialize();
} else {
const transaction = new Transaction();
transaction.recentBlockhash = blockhash;
transaction.feePayer = feePayer;

instructions.forEach(instruction => transaction.add(instruction));

if (signers.length > 0) {
transaction.partialSign(...signers);
}

serializedTrx = transaction.serialize({
requireAllSignatures: false,
verifySignatures: false,
});
}

return {
serializedTrx,
additionalInfo: {
blockhash,
lastValidBlockHeight,
isVersionedTransaction,
feePayer: feePayer.toBase58(),
}
};
}
2 changes: 1 addition & 1 deletion packages/types/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
"files": [
"dist"
]
}
}
Loading