-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: gelato and local signers (#1346)
* refactor: deposit and swap types * feat: add base /relay handler * feat: add qstash and gelato strategy * re-introduce requestHash * fixup * fixup * fix: addresses * fixup * fix: process message * feat: local signers strategy * fixup * fixup * fixup * fix: local signer strategy * cleanup * fix: error message * fixup * review requests
- Loading branch information
Showing
21 changed files
with
805 additions
and
478 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,46 @@ | ||
import { Client } from "@upstash/qstash"; | ||
|
||
import { RelayRequest, RelayStrategy, RelayStrategyName } from "./_types"; | ||
import { resolveVercelEndpoint } from "../_utils"; | ||
|
||
const client = new Client({ | ||
token: process.env.QSTASH_TOKEN!, | ||
}); | ||
|
||
export async function pushRelayRequestToQueue({ | ||
request, | ||
strategy, | ||
}: { | ||
request: RelayRequest; | ||
strategy: RelayStrategy; | ||
}) { | ||
const strategyName = strategy.strategyName; | ||
const queue = getRelayRequestQueue(strategyName, request.chainId); | ||
await queue.upsert({ | ||
parallelism: strategy.queueParallelism, | ||
}); | ||
|
||
const baseUrl = resolveVercelEndpoint(true); | ||
const response = await queue.enqueueJSON({ | ||
retries: 3, | ||
contentBasedDeduplication: true, | ||
headers: new Headers({ | ||
"Retry-After": "1", | ||
}), | ||
url: `${baseUrl}/api/relay/jobs/process`, | ||
body: { | ||
request, | ||
strategyName, | ||
}, | ||
}); | ||
return response; | ||
} | ||
|
||
function getRelayRequestQueue( | ||
strategyName: RelayStrategyName, | ||
chainId: number | ||
) { | ||
return client.queue({ | ||
queueName: `relay-request-queue-${chainId}-${strategyName}`, | ||
}); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import axios from "axios"; | ||
import { RelayRequest, RelayStrategy } from "../_types"; | ||
import { encodeCalldataForRelayRequest } from "../_utils"; | ||
|
||
const GELATO_API_KEY = process.env.GELATO_API_KEY; | ||
|
||
export function getGelatoStrategy(): RelayStrategy { | ||
return { | ||
strategyName: "gelato", | ||
queueParallelism: 2, | ||
relay: async (request: RelayRequest) => { | ||
const encodedCalldata = encodeCalldataForRelayRequest(request); | ||
|
||
const taskId = await relayWithGelatoApi({ | ||
chainId: request.chainId, | ||
target: request.to, | ||
data: encodedCalldata, | ||
}); | ||
|
||
let txHash: string | undefined; | ||
|
||
while (true) { | ||
const taskStatus = await getGelatoTaskStatus(taskId); | ||
|
||
if ( | ||
["Cancelled", "NotFound", "ExecReverted", "Blacklisted"].includes( | ||
taskStatus.taskState | ||
) | ||
) { | ||
throw new Error( | ||
`Can not relay request via Gelato due to task state ${taskStatus.taskState}` | ||
); | ||
} | ||
|
||
if (taskStatus.transactionHash) { | ||
txHash = taskStatus.transactionHash; | ||
break; | ||
} | ||
|
||
await new Promise((resolve) => setTimeout(resolve, 1_000)); | ||
} | ||
|
||
return txHash; | ||
}, | ||
}; | ||
} | ||
|
||
const gelatoBaseUrl = "https://api.gelato.digital"; | ||
|
||
async function relayWithGelatoApi({ | ||
chainId, | ||
target, | ||
data, | ||
}: { | ||
chainId: number; | ||
target: string; | ||
data: string; | ||
}) { | ||
if (!GELATO_API_KEY) { | ||
throw new Error("Can not call Gelato API: key is not set"); | ||
} | ||
|
||
const response = await axios.post( | ||
`${gelatoBaseUrl}/relays/v2/sponsored-call`, | ||
{ | ||
chainId, | ||
target, | ||
data, | ||
sponsorApiKey: GELATO_API_KEY, | ||
} | ||
); | ||
|
||
return response.data.taskId as string; | ||
} | ||
|
||
async function getGelatoTaskStatus(taskId: string) { | ||
const response = await axios.get<{ | ||
task: { | ||
taskState: | ||
| "CheckPending" | ||
| "ExecPending" | ||
| "ExecSuccess" | ||
| "ExecReverted" | ||
| "WaitingForConfirmation" | ||
| "Blacklisted" | ||
| "Cancelled" | ||
| "NotFound"; | ||
transactionHash?: string; | ||
}; | ||
}>(`${gelatoBaseUrl}/tasks/status/${taskId}`); | ||
return response.data.task; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
import { RelayStrategy, RelayStrategyName } from "../_types"; | ||
import { getGelatoStrategy } from "./gelato"; | ||
import { getLocalSignersStrategy } from "./local-signers"; | ||
|
||
const gelatoStrategy = getGelatoStrategy(); | ||
const localSignersStrategy = getLocalSignersStrategy(); | ||
|
||
export const strategiesByName = { | ||
[gelatoStrategy.strategyName]: gelatoStrategy, | ||
[localSignersStrategy.strategyName]: localSignersStrategy, | ||
} as Record<RelayStrategyName, RelayStrategy>; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
import { Wallet, utils } from "ethers"; | ||
|
||
import { RelayRequest, RelayStrategy } from "../_types"; | ||
import { encodeCalldataForRelayRequest } from "../_utils"; | ||
import { redisCache } from "../../_cache"; | ||
import { getProvider } from "../../_utils"; | ||
|
||
const localSignerPrivateKeys = | ||
process.env.LOCAL_SIGNER_PRIVATE_KEYS!.split(","); | ||
const balanceAlertThreshold = utils.parseEther("0.000001"); // TODO: Refine value | ||
|
||
export function getLocalSignersStrategy(): RelayStrategy { | ||
return { | ||
strategyName: "local-signers", | ||
queueParallelism: 1, // TODO: Should be dynamic based on the number of local signers | ||
relay: async (request: RelayRequest) => { | ||
const encodedCalldata = encodeCalldataForRelayRequest(request); | ||
|
||
if (localSignerPrivateKeys.length === 0) { | ||
throw new Error( | ||
"Can not relay tx via local signers: No local signers found" | ||
); | ||
} | ||
|
||
for (const signerPrivateKey of localSignerPrivateKeys) { | ||
const provider = getProvider(request.chainId); | ||
const wallet = new Wallet(signerPrivateKey, provider); | ||
try { | ||
await lockSigner(wallet.address, request.chainId); | ||
|
||
const balance = await wallet.getBalance(); | ||
if (balance.lt(balanceAlertThreshold)) { | ||
// TODO: Send PD alert | ||
} | ||
|
||
const txRequest = { | ||
chainId: request.chainId, | ||
to: request.to, | ||
data: encodedCalldata, | ||
from: wallet.address, | ||
}; | ||
const tx = await wallet.sendTransaction(txRequest); | ||
const receipt = await tx.wait(); | ||
return receipt.transactionHash; | ||
} catch (error) { | ||
if (error instanceof SignerLockedError) { | ||
continue; | ||
} | ||
throw error; | ||
} finally { | ||
await unlockSigner(wallet.address, request.chainId); | ||
} | ||
} | ||
|
||
throw new Error( | ||
"Can not relay tx via local signers: All local signers are locked" | ||
); | ||
}, | ||
}; | ||
} | ||
|
||
async function lockSigner(signerAddress: string, chainId: number) { | ||
const lockKey = getLockKey(signerAddress, chainId); | ||
const lockValue = await redisCache.get(lockKey); | ||
|
||
if (lockValue) { | ||
throw new SignerLockedError(signerAddress, chainId); | ||
} | ||
|
||
await redisCache.set(lockKey, "true", 30); | ||
} | ||
|
||
async function unlockSigner(signerAddress: string, chainId: number) { | ||
const lockKey = getLockKey(signerAddress, chainId); | ||
|
||
const lockValue = await redisCache.get(lockKey); | ||
if (!lockValue) { | ||
return; | ||
} | ||
|
||
await redisCache.del(lockKey); | ||
} | ||
|
||
function getLockKey(signerAddress: string, chainId: number) { | ||
return `signer-lock:${signerAddress}:${chainId}`; | ||
} | ||
|
||
class SignerLockedError extends Error { | ||
constructor(signerAddress: string, chainId: number) { | ||
super(`Signer ${signerAddress} on chain ${chainId} is already locked`); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
import { validateMethodArgs } from "./_utils"; | ||
|
||
export type RelayStrategyName = "gelato" | "local-signers"; | ||
|
||
export type RelayRequest = { | ||
chainId: number; | ||
to: string; | ||
methodNameAndArgs: ReturnType<typeof validateMethodArgs>; | ||
signatures: { | ||
permit: string; | ||
deposit: string; | ||
}; | ||
}; | ||
|
||
export type RelayStrategy = { | ||
strategyName: RelayStrategyName; | ||
queueParallelism: number; | ||
relay: (request: RelayRequest) => Promise<string>; | ||
}; |
Oops, something went wrong.