Skip to content

Commit

Permalink
feat: add checks to swap transaction returned by api (#23)
Browse files Browse the repository at this point in the history
* Added checks to swap transaction returned by api

* chore: remove manual version bump

* chore: add changeset

* chore: run prettier

* chore: set bump as minor instead of major

---------

Co-authored-by: Stefano Faieta <[email protected]>
  • Loading branch information
gidonkatten and stefanofa authored Nov 21, 2023
1 parent ced081d commit 74fbf0c
Show file tree
Hide file tree
Showing 6 changed files with 114 additions and 10 deletions.
5 changes: 5 additions & 0 deletions .changeset/tough-beers-live.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@folks-router/js-sdk": minor
---

Performs checks on the swap transaction returned by the API
14 changes: 11 additions & 3 deletions packages/folks-router-js-sdk/examples/swap.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,25 @@
import { decodeUnsignedTransaction } from "algosdk";
import { FolksRouterClient, Network, SwapMode } from "../src";
import { FolksRouterClient, Network, SwapMode, SwapParams } from "../src";
import { MainnetAlgodClient, sender } from "./config";

async function main() {
const user = sender;
const algod = MainnetAlgodClient;
const client = new FolksRouterClient(Network.MAINNET);

// construct swap params
const params: SwapParams = {
fromAssetId: 0,
toAssetId: 31566704,
amount: BigInt(10e6),
swapMode: SwapMode.FIXED_INPUT,
};

// fetch quote
const quote = await client.fetchSwapQuote(0, 31566704, BigInt(10e6), SwapMode.FIXED_INPUT);
const quote = await client.fetchSwapQuote(params);

// prepare swap
const base64txns = await client.prepareSwapTransactions(user.addr, BigInt(10), quote);
const base64txns = await client.prepareSwapTransactions(params, user.addr, BigInt(10), quote);
const unsignedTxns = base64txns.map((txn) => decodeUnsignedTransaction(Buffer.from(txn, "base64")));
const signedTxns = unsignedTxns.map((txn) => txn.signTxn(user.sk));

Expand Down
84 changes: 79 additions & 5 deletions packages/folks-router-js-sdk/src/FolksRouterClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
import {
ABIType,
decodeUnsignedTransaction,
encodeAddress,
getApplicationAddress,
OnApplicationComplete,
Transaction,
TransactionType,
} from "algosdk";
import axios, { AxiosInstance } from "axios";
import { Network, SwapMode, SwapQuote, SwapTransactions } from "./types";
import { MainnetFolksRouterAppId } from "./constants/mainnetConstants";
import { TestnetFolksRouterAppId } from "./constants/testnetConstants";
import { Network, SwapMode, SwapParams, SwapQuote, SwapTransactions } from "./types";
import { routerABIContract } from "./abiContracts";
import { mulScale, ONE_4_DP } from "./utils";

const BASE_URL = "https://api.folksrouter.io";

Expand All @@ -22,14 +35,13 @@ export class FolksRouterClient {
}

public async fetchSwapQuote(
fromAssetId: number,
toAssetId: number,
amount: number | bigint,
swapMode: SwapMode,
params: SwapParams,
maxGroupSize?: number,
feeBps?: number | bigint,
referrer?: string,
): Promise<SwapQuote> {
const { fromAssetId, toAssetId, amount, swapMode } = params;

const { data } = await this.api.get("/fetch/quote", {
params: {
network: this.network,
Expand All @@ -55,10 +67,14 @@ export class FolksRouterClient {
}

public async prepareSwapTransactions(
params: SwapParams,
userAddress: string,
slippageBps: number | bigint,
swapQuote: SwapQuote,
): Promise<SwapTransactions> {
const { fromAssetId, toAssetId, amount, swapMode } = params;

// fetch transactions
const { data } = await this.api.get("/prepare/swap", {
params: {
userAddress,
Expand All @@ -68,6 +84,64 @@ export class FolksRouterClient {
});
if (!data.success) throw Error(data.errors);

// check transactions
const unsignedTxns: Transaction[] = data.result.map((txn: string) =>
decodeUnsignedTransaction(Buffer.from(txn, "base64")),
);
const folksRouterAppId = this.network == Network.MAINNET ? MainnetFolksRouterAppId : TestnetFolksRouterAppId;
const folksRouterAddr = getApplicationAddress(folksRouterAppId);
const getHexSelector = (method: string) =>
Buffer.from(routerABIContract.getMethodByName(method).getSelector()).toString("hex");
const uint8ArrayToHex = (uint8Array: Uint8Array) => Buffer.from(uint8Array).toString("hex");

const sendAssetTxn = unsignedTxns[0]!;
const swapForwardTxns = unsignedTxns.slice(1, -1)!;
const swapEndTxn = unsignedTxns[unsignedTxns.length - 1]!;

// send algo/asset
if (encodeAddress(sendAssetTxn.to.publicKey) !== folksRouterAddr) throw Error("Incorrect receiver");
if (
!(fromAssetId === 0 && sendAssetTxn.type == TransactionType.pay) &&
!(fromAssetId === sendAssetTxn.assetIndex && sendAssetTxn.type === TransactionType.axfer)
)
throw Error("Sending incorrect algo/asset");
const sendAmount = BigInt(sendAssetTxn.amount);

// swap forward txns
swapForwardTxns.forEach((txn, i) => {
if (txn.appIndex !== folksRouterAppId) throw Error("Incorrect application index");
if (txn.type !== TransactionType.appl && txn.appOnComplete !== OnApplicationComplete.NoOpOC)
throw Error("Incorrect transaction type");
const swapForwardSelector = uint8ArrayToHex(txn.appArgs!.at(0)!);
if (swapForwardSelector !== getHexSelector("swap_forward")) throw Error("Incorrect selector");
});

// receive algo/asset
if (swapEndTxn.appIndex !== folksRouterAppId) throw Error("Incorrect application index");
if (swapEndTxn.type !== TransactionType.appl && swapEndTxn.appOnComplete !== OnApplicationComplete.NoOpOC)
throw Error("Incorrect transaction type");
const swapEndSelector = uint8ArrayToHex(swapEndTxn.appArgs!.at(0)!);
const isFixedInput = swapEndSelector === getHexSelector("fi_end_swap");
const isFixedOutput = swapEndSelector === getHexSelector("fo_end_swap");
if ((isFixedInput && swapMode !== SwapMode.FIXED_INPUT) || (isFixedOutput && swapMode !== SwapMode.FIXED_OUTPUT))
throw Error("Incorrect swap mode");
if (ABIType.from("uint64").decode(swapEndTxn.appArgs!.at(1)!) !== BigInt(toAssetId))
throw Error("Receiving incorrect algo/asset");
const receiveAmount = ABIType.from("uint64").decode(swapEndTxn.appArgs!.at(2)!) as bigint;

// check amounts
const slippageAmount = mulScale(swapQuote.quoteAmount, BigInt(slippageBps), ONE_4_DP);
if (isFixedInput) {
if (amount !== sendAmount) throw Error("Sending incorrect fixed input amount");
if (swapQuote.quoteAmount - slippageAmount !== receiveAmount)
throw Error("Receiving incorrect fixed input amount");
}
if (isFixedOutput) {
if (swapQuote.quoteAmount + slippageAmount !== sendAmount) throw Error("Sending incorrect fixed output amount");
if (amount !== receiveAmount) throw Error("Receiving incorrect fixed output amount");
}

// return
return data.result;
}
}
4 changes: 4 additions & 0 deletions packages/folks-router-js-sdk/src/abiContracts/router.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@
"type": "uint64",
"name": "percent_input_asset"
},
{
"type": "uint64",
"name": "increase_budget"
},
{
"type": "(uint64,bool,uint64,uint64,uint64,address,uint64,uint64,address,address,uint64,uint64)[]",
"name": "forward_path"
Expand Down
9 changes: 8 additions & 1 deletion packages/folks-router-js-sdk/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,13 @@ enum SwapMode {
FIXED_OUTPUT = "FIXED_OUTPUT",
}

interface SwapParams {
fromAssetId: number;
toAssetId: number;
amount: number | bigint;
swapMode: SwapMode;
}

interface SwapQuote {
quoteAmount: bigint;
priceImpact: number;
Expand All @@ -22,4 +29,4 @@ interface SwapQuote {

type SwapTransactions = string[];

export { ReferrerGroupTransaction, Network, SwapMode, SwapQuote, SwapTransactions };
export { ReferrerGroupTransaction, Network, SwapMode, SwapParams, SwapQuote, SwapTransactions };
8 changes: 7 additions & 1 deletion packages/folks-router-js-sdk/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,10 @@ function transferAlgoOrAsset(
: makePaymentTxnWithSuggestedParams(from, to, amount, undefined, undefined, params);
}

export { signer, transferAlgoOrAsset };
const ONE_4_DP = BigInt(1e4);

function mulScale(n1: bigint, n2: bigint, scale: bigint): bigint {
return (n1 * n2) / scale;
}

export { signer, transferAlgoOrAsset, ONE_4_DP, mulScale };

0 comments on commit 74fbf0c

Please sign in to comment.