forked from Nova-reward/Nova-Rewards
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathverify.js
More file actions
243 lines (208 loc) · 9.67 KB
/
Copy pathverify.js
File metadata and controls
243 lines (208 loc) · 9.67 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
#!/usr/bin/env node
/**
* Deployment Verification Script
*
* Audits the live Stellar network state against the expected post-deployment
* configuration. Can be run at any time after deployment:
*
* node deploy/verify.js # verify latest testnet deploy
* node deploy/verify.js --network mainnet
* node deploy/verify.js --deployment-id <id> # verify specific deployment
*
* Exit codes:
* 0 — all checks passed
* 1 — one or more checks failed
* 2 — script error (bad args, missing log, etc.)
*
* Checks performed:
* ✓ Issuer account exists on-chain
* ✓ Distribution account exists on-chain
* ✓ NOVA asset code and issuer match expected values
* ✓ Distribution account has a NOVA trustline
* ✓ Distribution account holds NOVA tokens (> 0)
* ✓ Issuer XLM balance is sufficient for future transactions
* ✓ Distribution XLM balance is sufficient for future transactions
* ✓ Deployment log outcome is 'success'
*/
'use strict';
require('dotenv').config({ path: require('path').join(__dirname, '../.env') });
const { Horizon, Asset, StrKey } = require('stellar-sdk');
const { getNetworkConfig, resolveNetwork } = require('./config/networks');
const { Logger, Status } = require('./lib/logger');
// Minimum XLM balance considered "healthy" for operational accounts
const MIN_XLM_BALANCE = 2.0;
// ─────────────────────────────────────────────────────────────────────────────
// Check runner
// ─────────────────────────────────────────────────────────────────────────────
/**
* Runs a single named check.
*
* @param {string} name
* @param {Function} fn - async function that returns { pass, detail }
* @returns {Promise<{name, pass, detail, error}>}
*/
async function runCheck(name, fn) {
try {
const { pass, detail } = await fn();
return { name, pass, detail };
} catch (err) {
return { name, pass: false, detail: null, error: err.message };
}
}
// ─────────────────────────────────────────────────────────────────────────────
// Core verification logic
// ─────────────────────────────────────────────────────────────────────────────
/**
* Runs all verification checks against the live Stellar network.
*
* @param {object} opts
* @param {string} opts.network - 'testnet' | 'mainnet'
* @param {boolean} [opts.silent] - Suppress console output
* @param {string} [opts.deploymentId] - Load specific deployment log for context
* @returns {Promise<{allPassed, passed, failed, checks, deploymentId}>}
*/
async function runVerification({ network, silent = false, deploymentId = null }) {
const cfg = getNetworkConfig(network);
const server = new Horizon.Server(cfg.horizonUrl);
const issuerPublic = process.env.ISSUER_PUBLIC;
const distributionPublic = process.env.DISTRIBUTION_PUBLIC;
if (!issuerPublic || !distributionPublic) {
throw new Error('ISSUER_PUBLIC and DISTRIBUTION_PUBLIC must be set in environment.');
}
const novaAsset = new Asset(cfg.assetCode, issuerPublic);
if (!silent) {
console.log(`\n🔍 Verifying Nova Rewards deployment on ${cfg.name}...\n`);
}
// ── Load deployment log context (optional) ─────────────────────────────────
let logRecord = null;
if (deploymentId) {
try {
logRecord = Logger.load(deploymentId).record;
} catch {
if (!silent) console.warn(` ⚠️ Could not load deployment log: ${deploymentId}`);
}
} else {
try {
logRecord = Logger.loadLatest(network).record;
} catch {
// No prior log — verification still runs against live state
}
}
// ── Define checks ──────────────────────────────────────────────────────────
const checks = await Promise.all([
runCheck('Issuer public key format is valid', async () => {
const pass = StrKey.isValidEd25519PublicKey(issuerPublic);
return { pass, detail: issuerPublic };
}),
runCheck('Distribution public key format is valid', async () => {
const pass = StrKey.isValidEd25519PublicKey(distributionPublic);
return { pass, detail: distributionPublic };
}),
runCheck('Issuer account exists on-chain', async () => {
const account = await server.loadAccount(issuerPublic);
return { pass: !!account, detail: `sequence: ${account.sequence}` };
}),
runCheck('Distribution account exists on-chain', async () => {
const account = await server.loadAccount(distributionPublic);
return { pass: !!account, detail: `sequence: ${account.sequence}` };
}),
runCheck('Issuer account has sufficient XLM', async () => {
const account = await server.loadAccount(issuerPublic);
const xlm = account.balances.find((b) => b.asset_type === 'native');
const balance = xlm ? parseFloat(xlm.balance) : 0;
const pass = balance >= MIN_XLM_BALANCE;
return { pass, detail: `${xlm?.balance ?? '0'} XLM (min: ${MIN_XLM_BALANCE})` };
}),
runCheck('Distribution account has sufficient XLM', async () => {
const account = await server.loadAccount(distributionPublic);
const xlm = account.balances.find((b) => b.asset_type === 'native');
const balance = xlm ? parseFloat(xlm.balance) : 0;
const pass = balance >= MIN_XLM_BALANCE;
return { pass, detail: `${xlm?.balance ?? '0'} XLM (min: ${MIN_XLM_BALANCE})` };
}),
runCheck('Distribution account has NOVA trustline', async () => {
const account = await server.loadAccount(distributionPublic);
const trustline = account.balances.find(
(b) =>
b.asset_type !== 'native' &&
b.asset_code === cfg.assetCode &&
b.asset_issuer === issuerPublic
);
return { pass: !!trustline, detail: trustline ? 'trustline present' : 'not found' };
}),
runCheck('Distribution account holds NOVA tokens', async () => {
const account = await server.loadAccount(distributionPublic);
const novaBalance = account.balances.find(
(b) =>
b.asset_type !== 'native' &&
b.asset_code === cfg.assetCode &&
b.asset_issuer === issuerPublic
);
const balance = novaBalance ? parseFloat(novaBalance.balance) : 0;
const pass = balance > 0;
return { pass, detail: `${novaBalance?.balance ?? '0'} NOVA` };
}),
runCheck('NOVA asset issuer matches environment', async () => {
const pass = novaAsset.issuer === issuerPublic;
return { pass, detail: `asset=${cfg.assetCode}:${issuerPublic.slice(0, 8)}...` };
}),
runCheck('Deployment log records success outcome', async () => {
if (!logRecord) {
return { pass: false, detail: 'no deployment log found' };
}
const pass = logRecord.outcome === 'success';
return {
pass,
detail: `outcome=${logRecord.outcome}, id=${logRecord.deploymentId}`,
};
}),
]);
// ── Summarize results ──────────────────────────────────────────────────────
const passed = checks.filter((c) => c.pass).length;
const failed = checks.filter((c) => !c.pass).length;
if (!silent) {
for (const c of checks) {
const icon = c.pass ? '✅' : '❌';
const detail = c.detail ? ` (${c.detail})` : '';
const errMsg = c.error ? ` ⚠️ ${c.error}` : '';
console.log(` ${icon} ${c.name}${detail}${errMsg}`);
}
console.log(`\n ${passed}/${checks.length} checks passed`);
if (failed === 0) {
console.log('\n✅ Deployment is healthy.\n');
} else {
console.log(`\n❌ ${failed} check(s) failed. Review the issues above.\n`);
if (logRecord) {
console.log(
` To rollback: node deploy/rollback.js --deployment-id ${logRecord.deploymentId}\n`
);
}
}
}
return {
allPassed: failed === 0,
passed,
failed,
checks,
deploymentId: logRecord?.deploymentId ?? null,
};
}
// ─────────────────────────────────────────────────────────────────────────────
// CLI entry point
// ─────────────────────────────────────────────────────────────────────────────
if (require.main === module) {
const network = resolveNetwork();
const deploymentId = (() => {
const idx = process.argv.indexOf('--deployment-id');
return idx !== -1 && process.argv[idx + 1] ? process.argv[idx + 1] : null;
})();
runVerification({ network, silent: false, deploymentId })
.then((result) => {
process.exit(result.allPassed ? 0 : 1);
})
.catch((err) => {
console.error(`\n❌ Verification error: ${err.message}\n`);
process.exit(2);
});
}
module.exports = { runVerification };