Skip to content

Commit

Permalink
feat: required api endpoints for dodo (#740)
Browse files Browse the repository at this point in the history
* feat(vercel): helper endpoint for building deposit tx

* feat(vercel): return spokePoolAddress in suggested-fees endpoint

* feat(vercel): helper endpoint that returns enabled tokens

* fix: use computedOriginChainId

* perf: increase token list cache age

* perf:  cache build tx response

* docs: add comment on not flooring quoteTimestamp

* adjust caching

* feat: allow ens name as referrer

* fix: native build tx endpoint
  • Loading branch information
dohaki authored Jun 2, 2023
1 parent 4f3ca01 commit 8583d3a
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 21 deletions.
80 changes: 59 additions & 21 deletions api/_utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -491,34 +491,23 @@ export const getProvider = (_chainId: number): providers.Provider => {
* @returns The corresponding SpokePool for the given `_chainId`
*/
export const getSpokePool = (_chainId: number): SpokePool => {
const spokePoolAddress = getSpokePoolAddress(_chainId);
return SpokePool__factory.connect(spokePoolAddress, getProvider(_chainId));
};

export const getSpokePoolAddress = (_chainId: number): string => {
const chainId = _chainId.toString();
const provider = getProvider(_chainId);
switch (chainId.toString()) {
case "1":
return SpokePool__factory.connect(
"0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5",
provider
);
return "0x5c7BCd6E7De5423a257D81B442095A1a6ced35C5";
case "10":
return SpokePool__factory.connect(
"0x6f26Bf09B1C792e3228e5467807a900A503c0281",
provider
);
return "0x6f26Bf09B1C792e3228e5467807a900A503c0281";
case "137":
return SpokePool__factory.connect(
"0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096",
provider
);
return "0x9295ee1d8C5b022Be115A2AD3c30C72E34e7F096";
case "288":
return SpokePool__factory.connect(
"0xBbc6009fEfFc27ce705322832Cb2068F8C1e0A58",
provider
);
return "0xBbc6009fEfFc27ce705322832Cb2068F8C1e0A58";
case "42161":
return SpokePool__factory.connect(
"0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A",
provider
);
return "0xe35e9842fceaCA96570B734083f4a58e8F7C5f2A";
default:
throw new Error("Invalid chainId provided");
}
Expand Down Expand Up @@ -698,6 +687,16 @@ export function validAddress() {
);
}

export function validAddressOrENS() {
return define<string>("validAddressOrENS", (value) => {
const ensDomainRegex =
/[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)?/;
return (
utils.isAddress(value as string) || ensDomainRegex.test(value as string)
);
});
}

export function positiveIntStr() {
return define<string>("positiveIntStr", (value) => {
return Number.isInteger(Number(value)) && Number(value) > 0;
Expand Down Expand Up @@ -732,3 +731,42 @@ export function getLpCushion(
.find((value) => value !== undefined) ?? "0"
);
}

export async function tagReferrer(
dataHex: string,
referrerAddressOrENS: string
) {
let referrerAddress: string | null;

if (ethers.utils.isAddress(referrerAddressOrENS)) {
referrerAddress = referrerAddressOrENS;
} else {
const provider = infuraProvider(1);
referrerAddress = await provider.resolveName(referrerAddressOrENS);
}

if (!referrerAddress) {
throw new Error("Invalid referrer address or ENS name");
}

if (!ethers.utils.isHexString(dataHex)) {
throw new Error("Data must be a valid hex string");
}

return ethers.utils.hexConcat([
dataHex,
"0xd00dfeeddeadbeef",
referrerAddress,
]);
}

export function getFallbackTokenLogoURI(l1TokenAddress: string) {
const isACX =
sdk.constants.TOKEN_SYMBOLS_MAP.ACX.addresses[1] === l1TokenAddress;

if (isACX) {
return "https://across.to/logo-small.png";
}

return `https://github.com/trustwallet/assets/blob/master/blockchains/ethereum/assets/${l1TokenAddress}/logo.png?raw=true`;
}
128 changes: 128 additions & 0 deletions api/build-deposit-tx.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { VercelResponse } from "@vercel/node";
import { ethers } from "ethers";
import { type, assert, Infer, optional, string } from "superstruct";
import { TypedVercelRequest } from "./_types";
import {
getLogger,
InputError,
handleErrorCondition,
parsableBigNumberString,
validAddress,
positiveIntStr,
boolStr,
getSpokePool,
tagReferrer,
validAddressOrENS,
} from "./_utils";

const BuildDepositTxQueryParamsSchema = type({
amount: parsableBigNumberString(),
token: validAddress(),
destinationChainId: positiveIntStr(),
originChainId: positiveIntStr(),
recipient: validAddress(),
relayerFeePct: parsableBigNumberString(),
quoteTimestamp: positiveIntStr(),
message: optional(string()),
maxCount: optional(boolStr()),
referrer: optional(validAddressOrENS()),
isNative: optional(boolStr()),
});

type BuildDepositTxQueryParams = Infer<typeof BuildDepositTxQueryParamsSchema>;

const handler = async (
{ query }: TypedVercelRequest<BuildDepositTxQueryParams>,
response: VercelResponse
) => {
const logger = getLogger();
logger.debug({
at: "BuildDepositTx",
message: "Query data",
query,
});
try {
assert(query, BuildDepositTxQueryParamsSchema);

let {
amount: amountInput,
token,
destinationChainId: destinationChainIdInput,
originChainId: originChainIdInput,
recipient,
relayerFeePct: relayerFeePctInput,
// Note, that the value of `quoteTimestamp` query param needs to be taken directly as returned by the
// `GET /api/suggested-fees` endpoint. This is why we don't floor the timestamp value here.
quoteTimestamp,
message = "0x",
maxCount = ethers.constants.MaxUint256.toString(),
referrer,
isNative: isNativeBoolStr,
} = query;

recipient = ethers.utils.getAddress(recipient);
token = ethers.utils.getAddress(token);
const destinationChainId = parseInt(destinationChainIdInput);
const originChainId = parseInt(originChainIdInput);
const amount = ethers.BigNumber.from(amountInput);
const relayerFeePct = ethers.BigNumber.from(relayerFeePctInput);
const isNative = isNativeBoolStr === "true";

if (originChainId === destinationChainId) {
throw new InputError("Origin and destination chains cannot be the same");
}

const spokePool = getSpokePool(originChainId);

const value = isNative ? amount : ethers.constants.Zero;
const tx = await spokePool.populateTransaction.deposit(
recipient,
token,
amount,
destinationChainId,
relayerFeePct,
quoteTimestamp,
message,
maxCount,
{ value }
);

// do not tag a referrer if data is not provided as a hex string.
tx.data = referrer ? await tagReferrer(tx.data!, referrer) : tx.data;

const responseJson = {
data: tx.data,
value: tx.value?.toString(),
};

// Two different explanations for how `stale-while-revalidate` works:

// https://vercel.com/docs/concepts/edge-network/caching#stale-while-revalidate
// This tells our CDN the value is fresh for 10 seconds. If a request is repeated within the next 10 seconds,
// the previously cached value is still fresh. The header x-vercel-cache present in the response will show the
// value HIT. If the request is repeated between 1 and 20 seconds later, the cached value will be stale but
// still render. In the background, a revalidation request will be made to populate the cache with a fresh value.
// x-vercel-cache will have the value STALE until the cache is refreshed.

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
// The response is fresh for 150s. After 150s it becomes stale, but the cache is allowed to reuse it
// for any requests that are made in the following 150s, provided that they revalidate the response in the background.
// Revalidation will make the cache be fresh again, so it appears to clients that it was always fresh during
// that period — effectively hiding the latency penalty of revalidation from them.
// If no request happened during that period, the cache became stale and the next request will revalidate normally.
logger.debug({
at: "BuildDepositTx",
message: "Response data",
responseJson,
});
response.setHeader(
"Cache-Control",
"s-maxage=150, stale-while-revalidate=150"
);
response.status(200).json(responseJson);
} catch (error) {
return handleErrorCondition("build-deposit-tx", response, logger, error);
}
};

export default handler;
2 changes: 2 additions & 0 deletions api/suggested-fees.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
boolStr,
HUP_POOL_CHAIN_ID,
ENABLED_ROUTES,
getSpokePoolAddress,
} from "./_utils";

const SuggestedFeesQueryParamsSchema = type({
Expand Down Expand Up @@ -167,6 +168,7 @@ const handler = async (
timestamp: parsedTimestamp.toString(),
isAmountTooLow: relayerFeeDetails.isAmountTooLow,
quoteBlock: blockTag.toString(),
spokePoolAddress: getSpokePoolAddress(Number(computedOriginChainId)),
};

logger.debug({
Expand Down
71 changes: 71 additions & 0 deletions api/token-list.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { VercelResponse } from "@vercel/node";
import { constants } from "@across-protocol/sdk-v2";
import {
getLogger,
handleErrorCondition,
getFallbackTokenLogoURI,
ENABLED_ROUTES,
} from "./_utils";
import { TypedVercelRequest } from "./_types";

const handler = async (_: TypedVercelRequest<{}>, response: VercelResponse) => {
const logger = getLogger();

try {
const tokensPerChain = ENABLED_ROUTES.routes.reduce(
(acc, route) => {
return {
...acc,
[`${route.fromTokenSymbol}-${route.fromChain}`]: {
symbol: route.fromTokenSymbol,
chainId: route.fromChain,
address: route.fromTokenAddress,
isNative: route.isNative,
l1TokenAddress: route.l1TokenAddress,
},
};
},
{} as Record<
string,
{
symbol: string;
chainId: number;
address: string;
isNative: boolean;
l1TokenAddress: string;
}
>
);

const enrichedTokensPerChain = Object.values(tokensPerChain).map(
(token) => {
const tokenInfo =
constants.TOKEN_SYMBOLS_MAP[
token.symbol as keyof typeof constants.TOKEN_SYMBOLS_MAP
];
return {
...token,
name: tokenInfo.name,
decimals: tokenInfo.decimals,
logoURI: getFallbackTokenLogoURI(token.l1TokenAddress),
};
}
);

// Instruct Vercel to cache limit data for this token for 6 hours. Caching can be used to limit number of
// Vercel invocations and run time for this serverless function and trades off potential inaccuracy in times of
// high volume. "max-age=0" instructs browsers not to cache, while s-maxage instructs Vercel edge caching
// to cache the responses and invalidate when deployments update.
logger.debug({
at: "TokenList",
message: "Response data",
responseJson: enrichedTokensPerChain,
});
response.setHeader("Cache-Control", "s-maxage=21600");
response.status(200).json(enrichedTokensPerChain);
} catch (error: unknown) {
return handleErrorCondition("token-list", response, logger, error);
}
};

export default handler;

2 comments on commit 8583d3a

@vercel
Copy link

@vercel vercel bot commented on 8583d3a Jun 2, 2023

Choose a reason for hiding this comment

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

Successfully deployed to the following URLs:

goerli-frontend-v2 – ./

goerli-frontend-v2.vercel.app
goerli-frontend-v2-uma.vercel.app
goerli-frontend-v2-git-master-uma.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 8583d3a Jun 2, 2023

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.