-
Notifications
You must be signed in to change notification settings - Fork 0
Add Telegram notifications to keeper on successful order execution #8
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -38,3 +38,113 @@ export function determineRelayAction(result, defaultSubmissionTag = "submit") { | |
| signature: result.signature, | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Decodes the action from the ABI-encoded result data. | ||
| * | ||
| * @param {string} data - Hex string starting with or without "0x" | ||
| * @returns {string} "swap", "redeem", or "unknown" | ||
| */ | ||
| export function decodeAction(data) { | ||
| if (!data || typeof data !== "string") return "unknown"; | ||
| const cleanData = data.startsWith("0x") ? data.slice(2) : data; | ||
| if (cleanData.length < 192) return "unknown"; | ||
| const actionHex = cleanData.slice(128, 192); | ||
| try { | ||
| const actionVal = Number(BigInt("0x" + actionHex)); | ||
| if (actionVal === 0) return "swap"; | ||
| if (actionVal === 1) return "redeem"; | ||
| } catch (e) { | ||
| // ignore | ||
| } | ||
| return "unknown"; | ||
| } | ||
|
|
||
| /** | ||
| * Decodes the order ID from the ABI-encoded result data. | ||
| * | ||
| * @param {string} data - Hex string starting with or without "0x" | ||
| * @returns {string|null} The order ID as a string, or null if invalid | ||
| */ | ||
| export function decodeOrderId(data) { | ||
| if (!data || typeof data !== "string") return null; | ||
| const cleanData = data.startsWith("0x") ? data.slice(2) : data; | ||
| if (cleanData.length < 64) return null; | ||
| const orderIdHex = cleanData.slice(0, 64); | ||
| try { | ||
| return BigInt("0x" + orderIdHex).toString(); | ||
| } catch (e) { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Determines whether Telegram notifications should be sent based on environment. | ||
| * | ||
| * @param {Record<string, string>} env | ||
| * @returns {boolean} | ||
| */ | ||
| export function shouldNotify(env) { | ||
| return !!(env?.TELEGRAM_BOT_TOKEN && env?.TELEGRAM_CHAT_ID); | ||
| } | ||
|
|
||
| /** | ||
| * Constructs the explorer transaction link. | ||
| * | ||
| * @param {string} txHash | ||
| * @returns {string} | ||
| */ | ||
| export function getExplorerTxLink(txHash) { | ||
| return `https://coston2.testnet.flarescan.com/tx/${txHash}`; | ||
| } | ||
|
|
||
| /** | ||
| * Builds the pure text message for Telegram. | ||
| * | ||
| * @param {string|number} orderId | ||
| * @param {string} action - "swap", "redeem", or "unknown" | ||
| * @param {string} txHash | ||
| * @returns {string} | ||
| */ | ||
| export function buildTelegramMessage(orderId, action, txHash) { | ||
| const txLink = getExplorerTxLink(txHash); | ||
| return `Order executed!\nID: ${orderId}\nAction: ${action}\nTransaction: ${txLink}`; | ||
| } | ||
|
|
||
| /** | ||
| * Sends a notification message to the configured Telegram Bot. | ||
| * | ||
| * @param {Record<string, string>} env | ||
| * @param {string|number} orderId | ||
| * @param {string} action | ||
| * @param {string} txHash | ||
| * @returns {Promise<boolean>} | ||
| */ | ||
| export async function sendTelegramNotification(env, orderId, action, txHash) { | ||
| if (!shouldNotify(env)) { | ||
| return false; | ||
| } | ||
| const message = buildTelegramMessage(orderId, action, txHash); | ||
| const url = `https://api.telegram.org/bot${env.TELEGRAM_BOT_TOKEN}/sendMessage`; | ||
| try { | ||
| const response = await fetch(url, { | ||
| method: "POST", | ||
| headers: { | ||
| "Content-Type": "application/json", | ||
| }, | ||
| body: JSON.stringify({ | ||
| chat_id: env.TELEGRAM_CHAT_ID, | ||
| text: message, | ||
| }), | ||
| }); | ||
| if (!response.ok) { | ||
| const text = await response.text(); | ||
| console.error(`Telegram Bot API error: ${response.status} ${text}`); | ||
| return false; | ||
| } | ||
| return true; | ||
| } catch (error) { | ||
| console.error(`Telegram network/fetch error: ${error.message}`); | ||
| return false; | ||
|
Comment on lines
+123
to
+148
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win 🧩 Analysis chain🌐 Web query:
💡 Result: As of August 13, 2026, the current Node.js versions include the Active LTS release v24.19.0 and the Current release v26.7.0 [1][2]. In these versions, the recommended and supported API for aborting a global fetch request after a timeout is the AbortSignal.timeout static method [3][4]. You can implement this by passing an AbortSignal created with this method to the signal option of the fetch function: await fetch('https://api.example.com/data', { signal: AbortSignal.timeout(5000), // Aborts after 5000ms }); When the timeout is triggered, the fetch request will reject with a TimeoutError [3][5][6]. This approach is preferred over manually managing an AbortController with setTimeout because it requires no manual cleanup and avoids potential memory leaks [7][8]. If you need to distinguish between timeout-induced aborts and other types of cancellation, you can check for the TimeoutError name in your catch block [7][6]. Citations:
🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- runtime declarations ---'
find . -maxdepth 3 -type f \( -name 'package.json' -o -name '.nvmrc' -o -name '.node-version' -o -name 'wrangler.toml' -o -name 'wrangler.json' -o -name 'wrangler.jsonc' \) -print
printf '%s\n' '--- relevant symbols ---'
rg -n -C 5 'sendTelegramNotification|relayResults|fetch\(|TELEGRAM_' keeper package.json .github 2>/dev/null || true
printf '%s\n' '--- file outline and target section ---'
wc -l keeper/src/lib.js
sed -n '1,220p' keeper/src/lib.jsRepository: LSUDOKO/Wraith Length of output: 14416 🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- runtime declarations ---'
find . -maxdepth 3 -type f \( -name 'package.json' -o -name '.nvmrc' -o -name '.node-version' -o -name 'wrangler.toml' -o -name 'wrangler.json' -o -name 'wrangler.jsonc' \) -print
printf '%s\n' '--- relevant symbols ---'
rg -n -C 5 'sendTelegramNotification|relayResults|fetch\(|TELEGRAM_' keeper package.json .github 2>/dev/null || true
printf '%s\n' '--- target file ---'
wc -l keeper/src/lib.js
sed -n '1,220p' keeper/src/lib.jsRepository: LSUDOKO/Wraith Length of output: 14396 🏁 Script executed: rg -n -C 8 'sendTelegramNotification|relayResults|fetch\(|TELEGRAM_' keeper package.json .github 2>/dev/null || true
find . -maxdepth 3 -type f \( -name 'package.json' -o -name '.nvmrc' -o -name '.node-version' -o -name 'wrangler.toml' -o -name 'wrangler.json' -o -name 'wrangler.jsonc' \) -printRepository: LSUDOKO/Wraith Length of output: 13480 🌐 Web query:
💡 Result: Yes, Cloudflare Workers fully support AbortSignal.timeout [1]. This API is integrated into the Workers runtime and can be used to signal cancellation for asynchronous operations, including the fetch API [1]. Key details regarding its implementation and compatibility include: Standard Support: AbortSignal.timeout is supported as part of the broader implementation of the AbortController and AbortSignal web standards within the Workers runtime [2][1]. Fetch Integration: You can pass an AbortSignal created via AbortSignal.timeout to the fetch function to automatically cancel the request if it exceeds the specified duration [1]. Behavioral Nuance: When a timeout occurs, the signal triggers an abort event with a reason, typically a DOMException [3][4]. In older versions of the runtime, this could occasionally lead to unhandled exceptions if not managed correctly, but subsequent updates ensured that AbortSignal.timeout properly reports a DISCONNECTED exception [4]. Runtime Behavior: Because Workers are event-driven, using AbortSignal.timeout(ms) keeps the worker's execution context alive for the duration of the timeout period [5]. If you are encountering issues, ensure your project is using modern runtime features, as Cloudflare has consistently moved toward greater alignment with web standards [1]. Citations:
🏁 Script executed: #!/bin/bash
set -e
printf '%s\n' '--- package metadata ---'
cat package.json
cat keeper/package.json
printf '%s\n' '--- relay loop ---'
sed -n '100,150p' keeper/src/index.js
printf '%s\n' '--- test metadata ---'
sed -n '150,215p' keeper/test/lib.test.js
printf '%s\n' '--- static timeout check ---'
python3 - <<'PY'
from pathlib import Path
source = Path("keeper/src/lib.js").read_text()
fetch_start = source.index("export async function sendTelegramNotification")
fetch_body = source[fetch_start:]
assert "fetch(url" in fetch_body
assert "signal:" not in fetch_body
print("sendTelegramNotification passes no signal to fetch: true")
print("sendTelegramNotification catches fetch rejection and returns false: true" if "return false;" in fetch_body else "unexpected")
index = Path("keeper/src/index.js").read_text()
loop = index[index.index("async function relayResults"):index.index("async function main")]
print("relayResults iterates pending entries sequentially: true" if "for (const [instructionId, orderId] of [...pending])" in loop and "await fetchResult(instructionId)" in loop else "unexpected")
print("notification is awaited in relayResults: true" if "await sendTelegramNotification" in loop else "unexpected")
PYRepository: LSUDOKO/Wraith Length of output: 5058 Bound the Telegram request. If the Telegram connection remains open, the awaited call blocks the sequential 🤖 Prompt for AI Agents |
||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
🧩 Analysis chain
🌐 Web query:
In viem version 2.21.0, does publicClient.waitForTransactionReceipt resolve with a receipt whose status is "reverted", or does it throw for reverted transactions?💡 Result:
In viem version 2.21.0, the publicClient.waitForTransactionReceipt action is designed to throw an error if the transaction reverts [1][2][3]. Specifically, it throws a TransactionReceiptRevertedError [4]. While there have been historical bug reports regarding this behavior [5], the library maintains an explicit error-throwing mechanism for reverted transactions to signify that the execution failed [1][4]. Consequently, the action does not resolve with a receipt marked as "reverted"; instead, the Promise rejects with an error containing the transaction receipt object [4].
Citations:
🏁 Script executed:
Repository: LSUDOKO/Wraith
Length of output: 4571
🏁 Script executed:
Repository: LSUDOKO/Wraith
Length of output: 37462
🏁 Script executed:
Repository: LSUDOKO/Wraith
Length of output: 327
🏁 Script executed:
Repository: LSUDOKO/Wraith
Length of output: 11837
Check the receipt status before logging success or sending notifications.
waitForTransactionReceiptcan resolve withreceipt.status === "reverted". Skip the success log and notification for reverted receipts, and add a regression test.🤖 Prompt for AI Agents