Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions script/rewards/.env.template
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,8 @@ TURNKEY_ORGANIZATION_ID="your-organization-id"
TURNKEY_API_PUBLIC_KEY="your-api-public-key"
TURNKEY_API_PRIVATE_KEY="your-api-private-key"
TURNKEY_WALLET_ADDRESS="0x..." # The EOA wallet address controlled by Turnkey

# Slack Notifications (optional)
# If not set, scripts run normally without sending notifications.
# Create a webhook at: https://api.slack.com/messaging/webhooks
SLACK_WEBHOOK_URL=
33 changes: 32 additions & 1 deletion script/rewards/ExecuteMints.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ethers, JsonRpcProvider, formatEther as ethersFormatEther } from "ether
import { TurnkeySigner } from "@turnkey/ethers";
import { TurnkeyClient } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { notifySlack } from "./slackNotify";

// Load environment variables
dotEnvConfig();
Expand Down Expand Up @@ -190,6 +191,12 @@ async function executeMintRequest(
console.error(` Status: ❌ Failed to execute mint`);
console.error(` Error: ${error.message}`);
console.error(`\n⚠️ Mint execution failed. Skipping notify step.`);
await notifySlack(
`*executeMint failed for request #${request.id}*\n` +
`Amount: ${formatEther(request.amount)} ZK\n` +
`Error: ${error.message}`,
"error"
);
return false;
}

Expand All @@ -214,6 +221,13 @@ async function executeMintRequest(
console.error(`\n⚠️ CRITICAL: Mint executed but notify failed!`);
console.error(` Manual action required:`);
console.error(` Call staker.notifyRewardAmount(${request.amount}) immediately!`);
await notifySlack(
`*CRITICAL: Mint executed but notify failed!*\n` +
`Request #${request.id} | Amount: ${formatEther(request.amount)} ZK\n` +
`Manual recovery required:\n` +
`\`npx ts-node --transpileOnly NotifyReward.ts -- --amount=${request.amount}\``,
"error"
);
return false;
}
}
Expand Down Expand Up @@ -315,15 +329,32 @@ async function main() {
console.log(` Failed: ${failCount}`);
console.log(`${"=".repeat(70)}\n`);

const totalAmount = pendingRequests.reduce((sum, r) => sum + r.amount, 0n);
if (failCount > 0) {
await notifySlack(
`*Execute Mints completed with failures*\n` +
`Total: ${pendingRequests.length} | Succeeded: ${successCount} | Failed: ${failCount}\n` +
`Total amount attempted: ${formatEther(totalAmount)} ZK`,
"warning"
);
process.exit(1);
} else {
await notifySlack(
`*Mints Executed Successfully*\n` +
`Processed: ${successCount} request(s)\n` +
`Total amount: ${formatEther(totalAmount)} ZK`
);
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
.catch(async (error) => {
console.error("\n💥 Script failed:");
console.error(error.message || error);
await notifySlack(
`*ExecuteMints script crashed*\nError: ${error.message || error}`,
"error"
);
process.exit(1);
});
24 changes: 23 additions & 1 deletion script/rewards/NotifyReward.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import { ethers, JsonRpcProvider, formatEther as ethersFormatEther } from "ether
import { TurnkeySigner } from "@turnkey/ethers";
import { TurnkeyClient } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { notifySlack } from "./slackNotify";

// Load environment variables
dotEnvConfig();
Expand Down Expand Up @@ -217,6 +218,14 @@ async function main() {
console.log(` New Reward End Time: ${new Date(Number(newEndTime.toString()) * 1000).toISOString()}`);
console.log(` New Scaled Rate: ${newScaledRate.toString()}`);
console.log(`${"=".repeat(70)}\n`);

await notifySlack(
`*Disaster Recovery: notifyRewardAmount succeeded*\n` +
`Amount: ${formatEther(amount)} ZK\n` +
`New reward end: ${new Date(Number(newEndTime.toString()) * 1000).toISOString()}\n` +
`New scaled rate: ${newScaledRate.toString()}\n` +
`Tx: \`${notifyTx.hash}\``
);
} catch (error: any) {
console.error(`\n❌ Failed to notify staker`);
console.error(` Error: ${error.message}`);
Expand All @@ -227,14 +236,27 @@ async function main() {
console.error(` staker.grantRole(NOTIFIER_ROLE, ${TURNKEY_WALLET_ADDRESS})`);
}

const roleTip = error.message.includes("not notifier")
? `\nTip: Grant NOTIFIER_ROLE to \`${TURNKEY_WALLET_ADDRESS}\``
: "";
await notifySlack(
`*Disaster Recovery FAILED: notifyRewardAmount*\n` +
`Amount: ${formatEther(amount)} ZK\n` +
`Error: ${error.message}${roleTip}`,
"error"
);
process.exit(1);
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
.catch(async (error) => {
console.error("\n💥 Script failed:");
console.error(error.message || error);
await notifySlack(
`*NotifyReward (disaster recovery) script crashed*\nError: ${error.message || error}`,
"error"
);
process.exit(1);
});
45 changes: 44 additions & 1 deletion script/rewards/RequestMint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { ethers, JsonRpcProvider, formatEther as ethersFormatEther } from "ether
import { TurnkeySigner } from "@turnkey/ethers";
import { TurnkeyClient } from "@turnkey/http";
import { ApiKeyStamper } from "@turnkey/api-key-stamper";
import { notifySlack } from "./slackNotify";

// Load environment variables
dotEnvConfig();
Expand Down Expand Up @@ -309,9 +310,23 @@ async function main() {
console.log(` New Rate: ${newRate.toFixed(4)}% APR`);
console.log(` New Reward End Time: ${new Date(Number(newState.rewardEndTime) * 1000).toISOString()}`);
console.log(`${"=".repeat(70)}\n`);

await notifySlack(
`*Rate Lowered*\n` +
`Previous rate: ${currentRate.toFixed(4)}% APR\n` +
`New rate: ${newRate.toFixed(4)}% APR\n` +
`New reward end: ${new Date(Number(newState.rewardEndTime) * 1000).toISOString()}\n` +
`Tx: \`${notifyTx.hash}\``
);
} catch (error: any) {
console.error(`\n❌ Failed to call notifyRewardAmount`);
console.error(` Error: ${error.message}`);
await notifySlack(
`*Failed to lower reward rate*\n` +
`Attempted: notifyRewardAmount(0) on ZKStaker\n` +
`Error: ${error.message}`,
"error"
);
process.exit(1);
}

Expand Down Expand Up @@ -390,21 +405,49 @@ async function main() {
console.log(` 1. Wait until ${executeAfter.toISOString()}`);
console.log(` 2. Run: npx ts-node --transpileOnly script/rewards/ExecuteMints.ts`);
console.log(`${"=".repeat(70)}\n`);

await notifySlack(
`*Mint Requested*\n` +
`Amount: ${formatEther(rewardsToAdd)} ZK\n` +
`Request ID: ${mintRequestId}\n` +
`Current rate: ${currentRate.toFixed(4)}% APR | Target: ${desiredRatePercentage.toFixed(4)}% APR\n` +
`Execute after: ${executeAfter.toISOString()}\n` +
`Tx: \`${mintRequestTx.hash}\``
);
} catch (e) {
console.log(`\n✅ Mint request created (unable to determine request ID)`);
console.log(` Run ExecuteMints.ts after the delay period to execute pending mints\n`);

await notifySlack(
`*Mint Requested*\n` +
`Amount: ${formatEther(rewardsToAdd)} ZK\n` +
`Current rate: ${currentRate.toFixed(4)}% APR | Target: ${desiredRatePercentage.toFixed(4)}% APR\n` +
`Tx: \`${mintRequestTx.hash}\`\n` +
`_(Request ID could not be determined)_`
);
}
} catch (error: any) {
console.error(`\n❌ Failed to request mint`);
console.error(` Error: ${error.message}`);
await notifySlack(
`*Failed to request mint*\n` +
`Attempted: ${formatEther(rewardsToAdd)} ZK via DelayMod\n` +
`Target rate: ${desiredRatePercentage.toFixed(4)}% APR\n` +
`Error: ${error.message}`,
"error"
);
process.exit(1);
}
}

main()
.then(() => process.exit(0))
.catch((error) => {
.catch(async (error) => {
console.error("\n💥 Script failed:");
console.error(error.message || error);
await notifySlack(
`*RequestMint script crashed*\nError: ${error.message || error}`,
"error"
);
process.exit(1);
});
37 changes: 37 additions & 0 deletions script/rewards/slackNotify.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
const SLACK_WEBHOOK_URL = process.env.SLACK_WEBHOOK_URL;

const LEVEL_EMOJI = {
info: ":white_check_mark:",
warning: ":warning:",
error: ":rotating_light:",
} as const;

/**
* Posts a message to Slack via incoming webhook.
* Silently no-ops if SLACK_WEBHOOK_URL is not set.
* Never throws — logs a warning to console on failure.
*/
export async function notifySlack(
message: string,
level: "info" | "warning" | "error" = "info"
): Promise<void> {
if (!SLACK_WEBHOOK_URL) return;

const prefix = LEVEL_EMOJI[level];
const payload = {
text: `${prefix} *[zkStaker]* ${message}`,
};

try {
const response = await fetch(SLACK_WEBHOOK_URL, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
});
if (!response.ok) {
console.warn(`[Slack] Warning: webhook returned ${response.status}`);
}
} catch (err: any) {
console.warn(`[Slack] Warning: failed to send notification: ${err.message}`);
}
}
26 changes: 23 additions & 3 deletions test/DeployZkStaker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { expect } from "chai";
import { ethers } from "hardhat";
import { Contract } from "ethers";
import { execSync, spawn } from "child_process";
import * as net from "net";
import * as dotenv from "dotenv";

dotenv.config();
Expand All @@ -16,6 +17,7 @@ describe("DeployZkStaker", function () {
let mintRewardNotifierAddress = "";

before(async function () {
this.timeout(120000);
// Get the local Hardhat node is running
try {
console.log("Starting local Hardhat node...");
Expand All @@ -29,8 +31,22 @@ describe("DeployZkStaker", function () {
process.exit(1);
}

// Wait for a few seconds to ensure the local node is ready
await new Promise((resolve) => setTimeout(resolve, 3000));
// Wait for the local node to be ready by polling port 8011
const maxWaitMs = 30000;
const pollIntervalMs = 500;
const start = Date.now();
while (Date.now() - start < maxWaitMs) {
const ready = await new Promise<boolean>((resolve) => {
const socket = new net.Socket();
socket.setTimeout(pollIntervalMs);
socket.once("connect", () => { socket.destroy(); resolve(true); });
socket.once("error", () => { socket.destroy(); resolve(false); });
socket.once("timeout", () => { socket.destroy(); resolve(false); });
socket.connect(8011, "0.0.0.0");
});
if (ready) break;
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
}

console.log("About to run deploy script...");
// Run the deploy script
Expand Down Expand Up @@ -72,7 +88,11 @@ describe("DeployZkStaker", function () {
after(async function () {
// Terminate the local Hardhat node process
if (localNodeProcess) {
process.kill(-localNodeProcess.pid);
try {
process.kill(-localNodeProcess.pid);
} catch (e) {
// Process may have already exited
}
}
});

Expand Down
Loading