Created
December 24, 2024 10:12
-
-
Save tinypell3ts/573375c1fce3bec9bd0770abfb698835 to your computer and use it in GitHub Desktop.
This file contains hidden or 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
import { Redis } from "ioredis"; | |
import { encodeFunctionData, isAddress, parseEther, stringToHex, toHex } from "viem"; | |
import type { Workflow } from "../@types"; | |
import { rewardsFacetAbi } from "../abis/RewardsFacet"; | |
import { CHAIN_IDS, EXPLORER_URLS } from "../constants/chains"; | |
import { privyClient } from "../lib/privy"; | |
import { publicClient } from "../lib/viem"; | |
if (!process.env.REDIS_URL) { | |
throw new Error("REDIS_URL is not set"); | |
} | |
const redis = new Redis(process.env.REDIS_URL); | |
/** | |
* Triggers a workflow execution for a given recipient | |
* @param workflow - The workflow to execute | |
* @param recipient - The recipient address | |
* @returns Object containing transaction hash and explorer link | |
* @throws Error if recipient is invalid or transaction fails | |
*/ | |
export async function triggerWorkflow(workflow: Workflow, recipient: string) { | |
if (!isAddress(recipient)) { | |
throw new Error("Valid recipient address is required"); | |
} | |
const txHash = await executeTransaction(workflow, recipient); | |
await sendWebhookNotification(workflow, txHash, recipient); | |
return { | |
txHash, | |
txLink: `${EXPLORER_URLS[CHAIN_IDS.ARBITRUM_SEPOLIA]}/tx/${txHash}`, | |
}; | |
} | |
async function executeTransaction(workflow: Workflow, recipient: string, retryCount = 0): Promise<string> { | |
// Transform the human-friendly reward format into contract arguments | |
const args = [ | |
workflow.rewards[0].token, | |
recipient, | |
parseEther(workflow.rewards[0].amount), | |
stringToHex(workflow.rewards[0].reward_id, { size: 32 }), | |
stringToHex("MISSION", { size: 32 }), | |
workflow.rewards[0].metadata_uri, | |
]; | |
const data = encodeFunctionData({ | |
abi: rewardsFacetAbi, | |
functionName: workflow.rewards[0].function_name, | |
args, | |
}); | |
// Simulate the transaction first | |
try { | |
await publicClient.simulateContract({ | |
account: workflow.owner, | |
address: workflow.app_id, | |
abi: rewardsFacetAbi, | |
functionName: workflow.rewards[0].function_name, | |
args, | |
}); | |
} catch (error) { | |
throw new Error(`Transaction simulation failed: ${error.message}`); | |
} | |
const getNonce = async (address: string, retryCount = 0): Promise<number> => { | |
try { | |
const currentNonce = await publicClient.getTransactionCount({ address }); | |
const redisKey = `nonce:${address}`; | |
// Initialize nonce in Redis if it doesn't exist | |
const exists = await redis.exists(redisKey); | |
if (!exists) { | |
await redis.set(redisKey, currentNonce.toString()); | |
} | |
// Atomic increment and get | |
const nextNonce = await redis.incr(redisKey); | |
// If our nonce got too far ahead, reset it | |
if (nextNonce > currentNonce + 10) { | |
await redis.set(redisKey, currentNonce.toString()); | |
return currentNonce; | |
} | |
return nextNonce - 1; // Return the pre-incremented value | |
} catch (error) { | |
if (retryCount >= 3) { | |
throw new Error(`Failed to get nonce after ${retryCount} retries: ${error.message}`); | |
} | |
await new Promise((resolve) => setTimeout(resolve, 1000 * 2 ** retryCount)); | |
return getNonce(address, retryCount + 1); | |
} | |
}; | |
const [feeData, gasLimit, nonce] = await Promise.all([ | |
publicClient.estimateFeesPerGas(), | |
publicClient.estimateGas({ | |
account: workflow.owner, | |
to: workflow.app_id, | |
data, | |
}), | |
getNonce(workflow.owner), | |
]); | |
const { data: clientData } = await privyClient.walletApi.rpc({ | |
address: workflow.owner, | |
chainType: "ethereum", | |
method: "eth_signTransaction", | |
params: { | |
transaction: { | |
to: workflow.app_id, | |
data, | |
from: workflow.owner, | |
chainId: CHAIN_IDS.ARBITRUM_SEPOLIA, | |
maxFeePerGas: toHex(feeData.maxFeePerGas), | |
maxPriorityFeePerGas: toHex(feeData.maxPriorityFeePerGas), | |
gasLimit: toHex(gasLimit), | |
nonce, | |
}, | |
}, | |
}); | |
try { | |
const tx = await publicClient.sendRawTransaction({ | |
serializedTransaction: clientData.signedTransaction, | |
}); | |
return tx; | |
} catch (error) { | |
// Check if error is nonce-related and we haven't exceeded retry attempts | |
if ( | |
retryCount < 5 && | |
error.message && | |
(error.message.includes("nonce too low") || error.message.includes("nonce too high")) | |
) { | |
console.log(`Nonce error detected, retrying (attempt ${retryCount + 1}/5)...`); | |
// Wait with exponential backoff before retrying | |
await new Promise((resolve) => setTimeout(resolve, 1000 * Math.pow(2, retryCount))); | |
// Reset Redis nonce to force a fresh fetch | |
await redis.del(`nonce:${workflow.owner}`); | |
// Retry the entire transaction | |
return executeTransaction(workflow, recipient, retryCount + 1); | |
} | |
// If we've exhausted retries or it's a different error, throw it | |
throw error; | |
} | |
} | |
async function sendWebhookNotification(workflow: Workflow, txHash: string, recipient: string) { | |
if (!workflow.webhook) return; | |
try { | |
const payload: WebhookPayload = { | |
workflow_id: workflow.id, | |
workflow_name: workflow.name, | |
transaction_hash: txHash, | |
transaction_url: `${EXPLORER_URLS[CHAIN_IDS.ARBITRUM_SEPOLIA]}/tx/${txHash}`, | |
recipient, | |
timestamp: new Date().toISOString(), | |
}; | |
await fetch(workflow.webhook, { | |
method: "POST", | |
headers: { "Content-Type": "application/json" }, | |
body: JSON.stringify(payload), | |
}); | |
} catch (error) { | |
console.error("Webhook notification failed:", error); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment