stylus-toolkit 0.2.10 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/README.md +230 -80
  2. package/dist/cli.js +9 -3
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/config.d.ts.map +1 -1
  5. package/dist/commands/config.js +3 -2
  6. package/dist/commands/config.js.map +1 -1
  7. package/dist/commands/dashboard.d.ts +6 -0
  8. package/dist/commands/dashboard.d.ts.map +1 -0
  9. package/dist/commands/dashboard.js +48 -0
  10. package/dist/commands/dashboard.js.map +1 -0
  11. package/dist/commands/deploy.d.ts.map +1 -1
  12. package/dist/commands/deploy.js +17 -80
  13. package/dist/commands/deploy.js.map +1 -1
  14. package/dist/commands/dev.d.ts.map +1 -1
  15. package/dist/commands/dev.js +157 -48
  16. package/dist/commands/dev.js.map +1 -1
  17. package/dist/commands/init.js +1 -1
  18. package/dist/commands/profile.d.ts.map +1 -1
  19. package/dist/commands/profile.js +74 -44
  20. package/dist/commands/profile.js.map +1 -1
  21. package/dist/config/constants.d.ts +56 -0
  22. package/dist/config/constants.d.ts.map +1 -0
  23. package/dist/config/constants.js +26 -0
  24. package/dist/config/constants.js.map +1 -0
  25. package/dist/profiler/comparator.d.ts.map +1 -1
  26. package/dist/profiler/comparator.js +3 -7
  27. package/dist/profiler/comparator.js.map +1 -1
  28. package/dist/profiler/gas-profiler.d.ts.map +1 -1
  29. package/dist/profiler/gas-profiler.js +30 -51
  30. package/dist/profiler/gas-profiler.js.map +1 -1
  31. package/dist/storage/results-store.d.ts +3 -0
  32. package/dist/storage/results-store.d.ts.map +1 -1
  33. package/dist/storage/results-store.js +25 -0
  34. package/dist/storage/results-store.js.map +1 -1
  35. package/dist/utils/gas-estimator.d.ts +22 -0
  36. package/dist/utils/gas-estimator.d.ts.map +1 -0
  37. package/dist/utils/gas-estimator.js +46 -0
  38. package/dist/utils/gas-estimator.js.map +1 -0
  39. package/package.json +5 -1
  40. package/src/dashboard/dashboard.js +276 -0
  41. package/src/dashboard/index.html +147 -0
@@ -0,0 +1,22 @@
1
+ export interface GasEstimationConfig {
2
+ baseGas: number;
3
+ activationGas: number;
4
+ perByteGas: number;
5
+ }
6
+ export declare class GasEstimator {
7
+ private static readonly STYLUS_DEPLOY_CONFIG;
8
+ private static readonly STYLUS_PROFILE_CONFIG;
9
+ private static readonly EVM_CONFIG;
10
+ /**
11
+ * Estimate gas for actual deployment (deploy command).
12
+ * Includes full 14M activation cost + 2.5x safety buffer.
13
+ */
14
+ static estimateDeploymentGas(bytecodeSize: number, language: 'rust' | 'solidity', safetyMultiplier?: number): number;
15
+ /**
16
+ * Estimate gas for profiling / TCO comparison.
17
+ * Uses bytecode storage cost only — no activation overhead.
18
+ * This gives an honest apples-to-apples comparison with Solidity.
19
+ */
20
+ static estimateProfileGas(bytecodeSize: number, language: 'rust' | 'solidity'): number;
21
+ }
22
+ //# sourceMappingURL=gas-estimator.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gas-estimator.d.ts","sourceRoot":"","sources":["../../src/utils/gas-estimator.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,qBAAa,YAAY;IAEvB,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,oBAAoB,CAI1C;IAIF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,qBAAqB,CAI3C;IAEF,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,UAAU,CAIhC;IAEF;;;OAGG;IACH,MAAM,CAAC,qBAAqB,CAC1B,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,UAAU,EAC7B,gBAAgB,GAAE,MAAU,GAC3B,MAAM;IAST;;;;OAIG;IACH,MAAM,CAAC,kBAAkB,CACvB,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,MAAM,GAAG,UAAU,GAC5B,MAAM;CAQV"}
@@ -0,0 +1,46 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.GasEstimator = void 0;
4
+ class GasEstimator {
5
+ /**
6
+ * Estimate gas for actual deployment (deploy command).
7
+ * Includes full 14M activation cost + 2.5x safety buffer.
8
+ */
9
+ static estimateDeploymentGas(bytecodeSize, language, safetyMultiplier = 1) {
10
+ const config = language === 'rust' ? this.STYLUS_DEPLOY_CONFIG : this.EVM_CONFIG;
11
+ return Math.ceil(config.baseGas +
12
+ config.activationGas +
13
+ (bytecodeSize * config.perByteGas * safetyMultiplier));
14
+ }
15
+ /**
16
+ * Estimate gas for profiling / TCO comparison.
17
+ * Uses bytecode storage cost only — no activation overhead.
18
+ * This gives an honest apples-to-apples comparison with Solidity.
19
+ */
20
+ static estimateProfileGas(bytecodeSize, language) {
21
+ const config = language === 'rust' ? this.STYLUS_PROFILE_CONFIG : this.EVM_CONFIG;
22
+ return Math.ceil(config.baseGas +
23
+ config.activationGas +
24
+ (bytecodeSize * config.perByteGas));
25
+ }
26
+ }
27
+ exports.GasEstimator = GasEstimator;
28
+ // Full deployment config — used by `deploy` command (includes 14M activation)
29
+ GasEstimator.STYLUS_DEPLOY_CONFIG = {
30
+ baseGas: 21000,
31
+ activationGas: 14000000,
32
+ perByteGas: 16,
33
+ };
34
+ // Profiling config — bytecode storage cost only, no activation overhead
35
+ // Activation is a one-time fixed cost; TCO comparison uses per-byte storage
36
+ GasEstimator.STYLUS_PROFILE_CONFIG = {
37
+ baseGas: 21000,
38
+ activationGas: 300000,
39
+ perByteGas: 16,
40
+ };
41
+ GasEstimator.EVM_CONFIG = {
42
+ baseGas: 21000,
43
+ activationGas: 32000,
44
+ perByteGas: 200,
45
+ };
46
+ //# sourceMappingURL=gas-estimator.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"gas-estimator.js","sourceRoot":"","sources":["../../src/utils/gas-estimator.ts"],"names":[],"mappings":";;;AAMA,MAAa,YAAY;IAsBvB;;;OAGG;IACH,MAAM,CAAC,qBAAqB,CAC1B,YAAoB,EACpB,QAA6B,EAC7B,mBAA2B,CAAC;QAE5B,MAAM,MAAM,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,oBAAoB,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QACjF,OAAO,IAAI,CAAC,IAAI,CACd,MAAM,CAAC,OAAO;YACd,MAAM,CAAC,aAAa;YACpB,CAAC,YAAY,GAAG,MAAM,CAAC,UAAU,GAAG,gBAAgB,CAAC,CACtD,CAAC;IACJ,CAAC;IAED;;;;OAIG;IACH,MAAM,CAAC,kBAAkB,CACvB,YAAoB,EACpB,QAA6B;QAE7B,MAAM,MAAM,GAAG,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,qBAAqB,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,CAAC;QAClF,OAAO,IAAI,CAAC,IAAI,CACd,MAAM,CAAC,OAAO;YACd,MAAM,CAAC,aAAa;YACpB,CAAC,YAAY,GAAG,MAAM,CAAC,UAAU,CAAC,CACnC,CAAC;IACJ,CAAC;;AAtDH,oCAuDC;AAtDC,8EAA8E;AACtD,iCAAoB,GAAwB;IAClE,OAAO,EAAE,KAAK;IACd,aAAa,EAAE,QAAQ;IACvB,UAAU,EAAE,EAAE;CACf,CAAC;AAEF,wEAAwE;AACxE,4EAA4E;AACpD,kCAAqB,GAAwB;IACnE,OAAO,EAAE,KAAK;IACd,aAAa,EAAE,MAAM;IACrB,UAAU,EAAE,EAAE;CACf,CAAC;AAEsB,uBAAU,GAAwB;IACxD,OAAO,EAAE,KAAK;IACd,aAAa,EAAE,KAAK;IACpB,UAAU,EAAE,GAAG;CAChB,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stylus-toolkit",
3
- "version": "0.2.10",
3
+ "version": "2.0.0",
4
4
  "description": "A comprehensive CLI development environment for Arbitrum Stylus smart contracts with automated gas profiling",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -44,6 +44,7 @@
44
44
  "files": [
45
45
  "dist/",
46
46
  "src/templates/Cargo.lock",
47
+ "src/dashboard/",
47
48
  "README.md",
48
49
  "LICENSE"
49
50
  ],
@@ -56,16 +57,19 @@
56
57
  "dotenv": "^16.3.1",
57
58
  "ethers": "^6.9.0",
58
59
  "execa": "^5.1.1",
60
+ "express": "^4.18.2",
59
61
  "fast-csv": "^4.3.6",
60
62
  "fs-extra": "^11.2.0",
61
63
  "handlebars": "^4.7.8",
62
64
  "inquirer": "^8.2.6",
63
65
  "json2csv": "^6.0.0-alpha.2",
66
+ "open": "^8.4.2",
64
67
  "ora": "^5.4.1",
65
68
  "solc": "^0.8.33",
66
69
  "yaml": "^2.3.4"
67
70
  },
68
71
  "devDependencies": {
72
+ "@types/express": "^4.17.21",
69
73
  "@types/fs-extra": "^11.0.4",
70
74
  "@types/inquirer": "^8.2.10",
71
75
  "@types/jest": "^29.5.11",
@@ -0,0 +1,276 @@
1
+ async function loadData() {
2
+ try {
3
+ const response = await fetch('/data.json');
4
+ if (!response.ok) {
5
+ throw new Error('Failed to load data');
6
+ }
7
+ return await response.json();
8
+ } catch (error) {
9
+ throw new Error(`Error loading data: ${error.message}`);
10
+ }
11
+ }
12
+
13
+ function calculateStats(data) {
14
+ if (data.length === 0) {
15
+ return {
16
+ totalProfiles: 0,
17
+ avgSavings: 0,
18
+ maxSavings: 0,
19
+ contractsProfiled: 0
20
+ };
21
+ }
22
+
23
+ const totalProfiles = data.length;
24
+ const avgSavings = data.reduce((sum, r) => sum + r.tco.tcoPercentage, 0) / totalProfiles;
25
+ const maxSavings = Math.max(...data.map(r => r.tco.tcoPercentage));
26
+ const uniqueContracts = new Set(data.map(r => r.contractName));
27
+ const contractsProfiled = uniqueContracts.size;
28
+
29
+ return {
30
+ totalProfiles,
31
+ avgSavings: avgSavings.toFixed(2),
32
+ maxSavings: maxSavings.toFixed(2),
33
+ contractsProfiled
34
+ };
35
+ }
36
+
37
+ function renderStats(stats) {
38
+ const statsContainer = document.getElementById('stats');
39
+ statsContainer.innerHTML = `
40
+ <div class="stat-card">
41
+ <div class="stat-label">Total Profiles</div>
42
+ <div class="stat-value">${stats.totalProfiles}</div>
43
+ </div>
44
+ <div class="stat-card">
45
+ <div class="stat-label">Average TCO Savings</div>
46
+ <div class="stat-value ${stats.avgSavings < 0 ? 'negative' : ''}">${stats.avgSavings}%</div>
47
+ </div>
48
+ <div class="stat-card">
49
+ <div class="stat-label">Maximum Savings</div>
50
+ <div class="stat-value">${stats.maxSavings}%</div>
51
+ </div>
52
+ <div class="stat-card">
53
+ <div class="stat-label">Contracts Profiled</div>
54
+ <div class="stat-value">${stats.contractsProfiled}</div>
55
+ </div>
56
+ `;
57
+ }
58
+
59
+ function renderCharts(data) {
60
+ if (data.length === 0) {
61
+ return;
62
+ }
63
+
64
+ // TCO Trend Chart
65
+ const tcoCtx = document.getElementById('tcoChart').getContext('2d');
66
+ new Chart(tcoCtx, {
67
+ type: 'line',
68
+ data: {
69
+ labels: data.map(r => new Date(r.timestamp).toLocaleDateString()),
70
+ datasets: [
71
+ {
72
+ label: 'Rust (Stylus) TCO',
73
+ data: data.map(r => r.tco.rustTCO),
74
+ borderColor: 'rgb(139, 92, 246)',
75
+ backgroundColor: 'rgba(139, 92, 246, 0.1)',
76
+ tension: 0.4,
77
+ fill: true
78
+ },
79
+ {
80
+ label: 'Solidity TCO',
81
+ data: data.map(r => r.tco.solidityTCO),
82
+ borderColor: 'rgb(239, 68, 68)',
83
+ backgroundColor: 'rgba(239, 68, 68, 0.1)',
84
+ tension: 0.4,
85
+ fill: true
86
+ }
87
+ ]
88
+ },
89
+ options: {
90
+ responsive: true,
91
+ maintainAspectRatio: false,
92
+ plugins: {
93
+ title: {
94
+ display: false
95
+ },
96
+ legend: {
97
+ labels: {
98
+ color: '#e2e8f0'
99
+ }
100
+ }
101
+ },
102
+ scales: {
103
+ y: {
104
+ beginAtZero: true,
105
+ grid: {
106
+ color: '#334155'
107
+ },
108
+ ticks: {
109
+ color: '#94a3b8'
110
+ }
111
+ },
112
+ x: {
113
+ grid: {
114
+ color: '#334155'
115
+ },
116
+ ticks: {
117
+ color: '#94a3b8'
118
+ }
119
+ }
120
+ }
121
+ }
122
+ });
123
+
124
+ // Savings Percentage Chart
125
+ const savingsCtx = document.getElementById('savingsChart').getContext('2d');
126
+ new Chart(savingsCtx, {
127
+ type: 'bar',
128
+ data: {
129
+ labels: data.map((r, i) => `${r.contractName} (${new Date(r.timestamp).toLocaleDateString()})`),
130
+ datasets: [{
131
+ label: 'TCO Savings %',
132
+ data: data.map(r => r.tco.tcoPercentage),
133
+ backgroundColor: data.map(r => r.tco.tcoPercentage >= 0 ? 'rgba(16, 185, 129, 0.8)' : 'rgba(239, 68, 68, 0.8)'),
134
+ borderColor: data.map(r => r.tco.tcoPercentage >= 0 ? 'rgb(16, 185, 129)' : 'rgb(239, 68, 68)'),
135
+ borderWidth: 1
136
+ }]
137
+ },
138
+ options: {
139
+ responsive: true,
140
+ maintainAspectRatio: false,
141
+ plugins: {
142
+ title: {
143
+ display: false
144
+ },
145
+ legend: {
146
+ labels: {
147
+ color: '#e2e8f0'
148
+ }
149
+ }
150
+ },
151
+ scales: {
152
+ y: {
153
+ beginAtZero: true,
154
+ grid: {
155
+ color: '#334155'
156
+ },
157
+ ticks: {
158
+ color: '#94a3b8',
159
+ callback: function(value) {
160
+ return value + '%';
161
+ }
162
+ }
163
+ },
164
+ x: {
165
+ grid: {
166
+ color: '#334155'
167
+ },
168
+ ticks: {
169
+ color: '#94a3b8',
170
+ maxRotation: 45,
171
+ minRotation: 45
172
+ }
173
+ }
174
+ }
175
+ }
176
+ });
177
+
178
+ // Costs Breakdown Chart
179
+ const costsCtx = document.getElementById('costsChart').getContext('2d');
180
+ const latestData = data[data.length - 1];
181
+
182
+ new Chart(costsCtx, {
183
+ type: 'bar',
184
+ data: {
185
+ labels: ['Deployment', 'Execution (100 calls)'],
186
+ datasets: [
187
+ {
188
+ label: 'Rust (Stylus)',
189
+ data: [
190
+ latestData.rustProfile.deploymentGas,
191
+ latestData.tco.rustTCO - latestData.rustProfile.deploymentGas
192
+ ],
193
+ backgroundColor: 'rgba(139, 92, 246, 0.8)',
194
+ borderColor: 'rgb(139, 92, 246)',
195
+ borderWidth: 1
196
+ },
197
+ {
198
+ label: 'Solidity',
199
+ data: [
200
+ latestData.solidityProfile.deploymentGas,
201
+ latestData.tco.solidityTCO - latestData.solidityProfile.deploymentGas
202
+ ],
203
+ backgroundColor: 'rgba(239, 68, 68, 0.8)',
204
+ borderColor: 'rgb(239, 68, 68)',
205
+ borderWidth: 1
206
+ }
207
+ ]
208
+ },
209
+ options: {
210
+ responsive: true,
211
+ maintainAspectRatio: false,
212
+ plugins: {
213
+ title: {
214
+ display: false
215
+ },
216
+ legend: {
217
+ labels: {
218
+ color: '#e2e8f0'
219
+ }
220
+ }
221
+ },
222
+ scales: {
223
+ y: {
224
+ beginAtZero: true,
225
+ grid: {
226
+ color: '#334155'
227
+ },
228
+ ticks: {
229
+ color: '#94a3b8',
230
+ callback: function(value) {
231
+ return value.toLocaleString() + ' gas';
232
+ }
233
+ }
234
+ },
235
+ x: {
236
+ grid: {
237
+ color: '#334155'
238
+ },
239
+ ticks: {
240
+ color: '#94a3b8'
241
+ }
242
+ }
243
+ }
244
+ }
245
+ });
246
+ }
247
+
248
+ async function renderDashboard() {
249
+ const loadingEl = document.getElementById('loading');
250
+ const errorEl = document.getElementById('error');
251
+ const dashboardEl = document.getElementById('dashboard');
252
+
253
+ try {
254
+ const data = await loadData();
255
+
256
+ loadingEl.style.display = 'none';
257
+
258
+ if (data.length === 0) {
259
+ errorEl.style.display = 'block';
260
+ document.getElementById('error-message').textContent = 'No profiling data available yet. Run "stylus-toolkit profile" to generate data.';
261
+ return;
262
+ }
263
+
264
+ const stats = calculateStats(data);
265
+ renderStats(stats);
266
+ renderCharts(data);
267
+
268
+ dashboardEl.style.display = 'block';
269
+ } catch (error) {
270
+ loadingEl.style.display = 'none';
271
+ errorEl.style.display = 'block';
272
+ document.getElementById('error-message').textContent = error.message;
273
+ }
274
+ }
275
+
276
+ renderDashboard();
@@ -0,0 +1,147 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Stylus Toolkit - Gas Analytics Dashboard</title>
7
+ <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
17
+ background: #0f172a;
18
+ color: #e2e8f0;
19
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
25
+ }
26
+
27
+ header {
28
+ margin-bottom: 40px;
29
+ }
30
+
31
+ h1 {
32
+ font-size: 2.5rem;
33
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
34
+ -webkit-background-clip: text;
35
+ -webkit-text-fill-color: transparent;
36
+ margin-bottom: 10px;
37
+ }
38
+
39
+ .subtitle {
40
+ color: #94a3b8;
41
+ font-size: 1.1rem;
42
+ }
43
+
44
+ .stats-grid {
45
+ display: grid;
46
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
47
+ gap: 20px;
48
+ margin-bottom: 40px;
49
+ }
50
+
51
+ .stat-card {
52
+ background: #1e293b;
53
+ border-radius: 12px;
54
+ padding: 24px;
55
+ border: 1px solid #334155;
56
+ }
57
+
58
+ .stat-label {
59
+ color: #94a3b8;
60
+ font-size: 0.875rem;
61
+ margin-bottom: 8px;
62
+ text-transform: uppercase;
63
+ letter-spacing: 0.05em;
64
+ }
65
+
66
+ .stat-value {
67
+ font-size: 2rem;
68
+ font-weight: 700;
69
+ color: #10b981;
70
+ }
71
+
72
+ .stat-value.negative {
73
+ color: #ef4444;
74
+ }
75
+
76
+ .chart-container {
77
+ background: #1e293b;
78
+ border-radius: 12px;
79
+ padding: 30px;
80
+ margin-bottom: 30px;
81
+ border: 1px solid #334155;
82
+ }
83
+
84
+ .chart-title {
85
+ font-size: 1.5rem;
86
+ margin-bottom: 20px;
87
+ color: #f1f5f9;
88
+ }
89
+
90
+ canvas {
91
+ max-height: 400px;
92
+ }
93
+
94
+ .loading {
95
+ text-align: center;
96
+ padding: 60px;
97
+ color: #94a3b8;
98
+ }
99
+
100
+ .error {
101
+ background: #7f1d1d;
102
+ border: 1px solid #991b1b;
103
+ border-radius: 12px;
104
+ padding: 20px;
105
+ color: #fecaca;
106
+ }
107
+ </style>
108
+ </head>
109
+ <body>
110
+ <div class="container">
111
+ <header>
112
+ <h1>Stylus Toolkit Analytics</h1>
113
+ <p class="subtitle">Real-time gas profiling insights for your Arbitrum Stylus contracts</p>
114
+ </header>
115
+
116
+ <div id="loading" class="loading">
117
+ <p>Loading analytics data...</p>
118
+ </div>
119
+
120
+ <div id="error" class="error" style="display: none;">
121
+ <h3>Error Loading Data</h3>
122
+ <p id="error-message"></p>
123
+ </div>
124
+
125
+ <div id="dashboard" style="display: none;">
126
+ <div id="stats" class="stats-grid"></div>
127
+
128
+ <div class="chart-container">
129
+ <h2 class="chart-title">Total Cost of Ownership (TCO) Trends</h2>
130
+ <canvas id="tcoChart"></canvas>
131
+ </div>
132
+
133
+ <div class="chart-container">
134
+ <h2 class="chart-title">Gas Savings by Contract</h2>
135
+ <canvas id="savingsChart"></canvas>
136
+ </div>
137
+
138
+ <div class="chart-container">
139
+ <h2 class="chart-title">Deployment vs Execution Costs</h2>
140
+ <canvas id="costsChart"></canvas>
141
+ </div>
142
+ </div>
143
+ </div>
144
+
145
+ <script src="dashboard.js"></script>
146
+ </body>
147
+ </html>