feat 051125/recover_wallet

This commit is contained in:
NguyenAnhQuan 2025-11-05 04:14:37 +07:00
parent f23a20df10
commit e48669d972
50 changed files with 1160 additions and 1622 deletions

2
.gitignore vendored
View File

@ -13,7 +13,7 @@ dist
dist-ssr dist-ssr
coverage coverage
*.local *.local
.vite/*/** .vite/
wallets/* wallets/*
/cypress/videos/ /cypress/videos/

View File

@ -1,502 +0,0 @@
"use strict";
const require$$3$1 = require("electron");
const path$1 = require("node:path");
const require$$0$1 = require("path");
const require$$1$1 = require("child_process");
const require$$0 = require("tty");
const require$$1 = require("util");
const require$$3 = require("fs");
const require$$4 = require("net");
function getDefaultExportFromCjs(x) {
return x && x.__esModule && Object.prototype.hasOwnProperty.call(x, "default") ? x["default"] : x;
}
var src = { exports: {} };
var browser = { exports: {} };
var debug$1 = { exports: {} };
var ms;
var hasRequiredMs;
function requireMs() {
if (hasRequiredMs) return ms;
hasRequiredMs = 1;
var s = 1e3;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var y = d * 365.25;
ms = function(val, options) {
options = options || {};
var type = typeof val;
if (type === "string" && val.length > 0) {
return parse(val);
} else if (type === "number" && isNaN(val) === false) {
return options.long ? fmtLong(val) : fmtShort(val);
}
throw new Error(
"val is not a non-empty string or a valid number. val=" + JSON.stringify(val)
);
};
function parse(str) {
str = String(str);
if (str.length > 100) {
return;
}
var match = /^((?:\d+)?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|years?|yrs?|y)?$/i.exec(
str
);
if (!match) {
return;
}
var n = parseFloat(match[1]);
var type = (match[2] || "ms").toLowerCase();
switch (type) {
case "years":
case "year":
case "yrs":
case "yr":
case "y":
return n * y;
case "days":
case "day":
case "d":
return n * d;
case "hours":
case "hour":
case "hrs":
case "hr":
case "h":
return n * h;
case "minutes":
case "minute":
case "mins":
case "min":
case "m":
return n * m;
case "seconds":
case "second":
case "secs":
case "sec":
case "s":
return n * s;
case "milliseconds":
case "millisecond":
case "msecs":
case "msec":
case "ms":
return n;
default:
return void 0;
}
}
function fmtShort(ms2) {
if (ms2 >= d) {
return Math.round(ms2 / d) + "d";
}
if (ms2 >= h) {
return Math.round(ms2 / h) + "h";
}
if (ms2 >= m) {
return Math.round(ms2 / m) + "m";
}
if (ms2 >= s) {
return Math.round(ms2 / s) + "s";
}
return ms2 + "ms";
}
function fmtLong(ms2) {
return plural(ms2, d, "day") || plural(ms2, h, "hour") || plural(ms2, m, "minute") || plural(ms2, s, "second") || ms2 + " ms";
}
function plural(ms2, n, name) {
if (ms2 < n) {
return;
}
if (ms2 < n * 1.5) {
return Math.floor(ms2 / n) + " " + name;
}
return Math.ceil(ms2 / n) + " " + name + "s";
}
return ms;
}
var hasRequiredDebug;
function requireDebug() {
if (hasRequiredDebug) return debug$1.exports;
hasRequiredDebug = 1;
(function(module2, exports2) {
exports2 = module2.exports = createDebug.debug = createDebug["default"] = createDebug;
exports2.coerce = coerce;
exports2.disable = disable;
exports2.enable = enable;
exports2.enabled = enabled;
exports2.humanize = requireMs();
exports2.names = [];
exports2.skips = [];
exports2.formatters = {};
var prevTime;
function selectColor(namespace) {
var hash = 0, i;
for (i in namespace) {
hash = (hash << 5) - hash + namespace.charCodeAt(i);
hash |= 0;
}
return exports2.colors[Math.abs(hash) % exports2.colors.length];
}
function createDebug(namespace) {
function debug2() {
if (!debug2.enabled) return;
var self = debug2;
var curr = +/* @__PURE__ */ new Date();
var ms2 = curr - (prevTime || curr);
self.diff = ms2;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
var args = new Array(arguments.length);
for (var i = 0; i < args.length; i++) {
args[i] = arguments[i];
}
args[0] = exports2.coerce(args[0]);
if ("string" !== typeof args[0]) {
args.unshift("%O");
}
var index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, function(match, format) {
if (match === "%%") return match;
index++;
var formatter = exports2.formatters[format];
if ("function" === typeof formatter) {
var val = args[index];
match = formatter.call(self, val);
args.splice(index, 1);
index--;
}
return match;
});
exports2.formatArgs.call(self, args);
var logFn = debug2.log || exports2.log || console.log.bind(console);
logFn.apply(self, args);
}
debug2.namespace = namespace;
debug2.enabled = exports2.enabled(namespace);
debug2.useColors = exports2.useColors();
debug2.color = selectColor(namespace);
if ("function" === typeof exports2.init) {
exports2.init(debug2);
}
return debug2;
}
function enable(namespaces) {
exports2.save(namespaces);
exports2.names = [];
exports2.skips = [];
var split = (typeof namespaces === "string" ? namespaces : "").split(/[\s,]+/);
var len = split.length;
for (var i = 0; i < len; i++) {
if (!split[i]) continue;
namespaces = split[i].replace(/\*/g, ".*?");
if (namespaces[0] === "-") {
exports2.skips.push(new RegExp("^" + namespaces.substr(1) + "$"));
} else {
exports2.names.push(new RegExp("^" + namespaces + "$"));
}
}
}
function disable() {
exports2.enable("");
}
function enabled(name) {
var i, len;
for (i = 0, len = exports2.skips.length; i < len; i++) {
if (exports2.skips[i].test(name)) {
return false;
}
}
for (i = 0, len = exports2.names.length; i < len; i++) {
if (exports2.names[i].test(name)) {
return true;
}
}
return false;
}
function coerce(val) {
if (val instanceof Error) return val.stack || val.message;
return val;
}
})(debug$1, debug$1.exports);
return debug$1.exports;
}
var hasRequiredBrowser;
function requireBrowser() {
if (hasRequiredBrowser) return browser.exports;
hasRequiredBrowser = 1;
(function(module2, exports2) {
exports2 = module2.exports = requireDebug();
exports2.log = log;
exports2.formatArgs = formatArgs;
exports2.save = save;
exports2.load = load;
exports2.useColors = useColors;
exports2.storage = "undefined" != typeof chrome && "undefined" != typeof chrome.storage ? chrome.storage.local : localstorage();
exports2.colors = [
"lightseagreen",
"forestgreen",
"goldenrod",
"dodgerblue",
"darkorchid",
"crimson"
];
function useColors() {
if (typeof window !== "undefined" && window.process && window.process.type === "renderer") {
return true;
}
return typeof document !== "undefined" && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // is firebug? http://stackoverflow.com/a/398120/376773
typeof window !== "undefined" && window.console && (window.console.firebug || window.console.exception && window.console.table) || // is firefox >= v31?
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31 || // double check webkit in userAgent just in case we are in a worker
typeof navigator !== "undefined" && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
}
exports2.formatters.j = function(v) {
try {
return JSON.stringify(v);
} catch (err) {
return "[UnexpectedJSONParseError]: " + err.message;
}
};
function formatArgs(args) {
var useColors2 = this.useColors;
args[0] = (useColors2 ? "%c" : "") + this.namespace + (useColors2 ? " %c" : " ") + args[0] + (useColors2 ? "%c " : " ") + "+" + exports2.humanize(this.diff);
if (!useColors2) return;
var c = "color: " + this.color;
args.splice(1, 0, c, "color: inherit");
var index = 0;
var lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, function(match) {
if ("%%" === match) return;
index++;
if ("%c" === match) {
lastC = index;
}
});
args.splice(lastC, 0, c);
}
function log() {
return "object" === typeof console && console.log && Function.prototype.apply.call(console.log, console, arguments);
}
function save(namespaces) {
try {
if (null == namespaces) {
exports2.storage.removeItem("debug");
} else {
exports2.storage.debug = namespaces;
}
} catch (e) {
}
}
function load() {
var r;
try {
r = exports2.storage.debug;
} catch (e) {
}
if (!r && typeof process !== "undefined" && "env" in process) {
r = process.env.DEBUG;
}
return r;
}
exports2.enable(load());
function localstorage() {
try {
return window.localStorage;
} catch (e) {
}
}
})(browser, browser.exports);
return browser.exports;
}
var node = { exports: {} };
var hasRequiredNode;
function requireNode() {
if (hasRequiredNode) return node.exports;
hasRequiredNode = 1;
(function(module2, exports2) {
var tty = require$$0;
var util = require$$1;
exports2 = module2.exports = requireDebug();
exports2.init = init;
exports2.log = log;
exports2.formatArgs = formatArgs;
exports2.save = save;
exports2.load = load;
exports2.useColors = useColors;
exports2.colors = [6, 2, 3, 4, 5, 1];
exports2.inspectOpts = Object.keys(process.env).filter(function(key) {
return /^debug_/i.test(key);
}).reduce(function(obj, key) {
var prop = key.substring(6).toLowerCase().replace(/_([a-z])/g, function(_, k) {
return k.toUpperCase();
});
var val = process.env[key];
if (/^(yes|on|true|enabled)$/i.test(val)) val = true;
else if (/^(no|off|false|disabled)$/i.test(val)) val = false;
else if (val === "null") val = null;
else val = Number(val);
obj[prop] = val;
return obj;
}, {});
var fd = parseInt(process.env.DEBUG_FD, 10) || 2;
if (1 !== fd && 2 !== fd) {
util.deprecate(function() {
}, "except for stderr(2) and stdout(1), any other usage of DEBUG_FD is deprecated. Override debug.log if you want to use a different log function (https://git.io/debug_fd)")();
}
var stream = 1 === fd ? process.stdout : 2 === fd ? process.stderr : createWritableStdioStream(fd);
function useColors() {
return "colors" in exports2.inspectOpts ? Boolean(exports2.inspectOpts.colors) : tty.isatty(fd);
}
exports2.formatters.o = function(v) {
this.inspectOpts.colors = this.useColors;
return util.inspect(v, this.inspectOpts).split("\n").map(function(str) {
return str.trim();
}).join(" ");
};
exports2.formatters.O = function(v) {
this.inspectOpts.colors = this.useColors;
return util.inspect(v, this.inspectOpts);
};
function formatArgs(args) {
var name = this.namespace;
var useColors2 = this.useColors;
if (useColors2) {
var c = this.color;
var prefix = " \x1B[3" + c + ";1m" + name + " \x1B[0m";
args[0] = prefix + args[0].split("\n").join("\n" + prefix);
args.push("\x1B[3" + c + "m+" + exports2.humanize(this.diff) + "\x1B[0m");
} else {
args[0] = (/* @__PURE__ */ new Date()).toUTCString() + " " + name + " " + args[0];
}
}
function log() {
return stream.write(util.format.apply(util, arguments) + "\n");
}
function save(namespaces) {
if (null == namespaces) {
delete process.env.DEBUG;
} else {
process.env.DEBUG = namespaces;
}
}
function load() {
return process.env.DEBUG;
}
function createWritableStdioStream(fd2) {
var stream2;
var tty_wrap = process.binding("tty_wrap");
switch (tty_wrap.guessHandleType(fd2)) {
case "TTY":
stream2 = new tty.WriteStream(fd2);
stream2._type = "tty";
if (stream2._handle && stream2._handle.unref) {
stream2._handle.unref();
}
break;
case "FILE":
var fs = require$$3;
stream2 = new fs.SyncWriteStream(fd2, { autoClose: false });
stream2._type = "fs";
break;
case "PIPE":
case "TCP":
var net = require$$4;
stream2 = new net.Socket({
fd: fd2,
readable: false,
writable: true
});
stream2.readable = false;
stream2.read = null;
stream2._type = "pipe";
if (stream2._handle && stream2._handle.unref) {
stream2._handle.unref();
}
break;
default:
throw new Error("Implement me. Unknown stream file type!");
}
stream2.fd = fd2;
stream2._isStdio = true;
return stream2;
}
function init(debug2) {
debug2.inspectOpts = {};
var keys = Object.keys(exports2.inspectOpts);
for (var i = 0; i < keys.length; i++) {
debug2.inspectOpts[keys[i]] = exports2.inspectOpts[keys[i]];
}
}
exports2.enable(load());
})(node, node.exports);
return node.exports;
}
if (typeof process !== "undefined" && process.type === "renderer") {
src.exports = requireBrowser();
} else {
src.exports = requireNode();
}
var srcExports = src.exports;
var path = require$$0$1;
var spawn = require$$1$1.spawn;
var debug = srcExports("electron-squirrel-startup");
var app = require$$3$1.app;
var run = function(args, done) {
var updateExe = path.resolve(path.dirname(process.execPath), "..", "Update.exe");
debug("Spawning `%s` with args `%s`", updateExe, args);
spawn(updateExe, args, {
detached: true
}).on("close", done);
};
var check = function() {
if (process.platform === "win32") {
var cmd = process.argv[1];
debug("processing squirrel command `%s`", cmd);
var target = path.basename(process.execPath);
if (cmd === "--squirrel-install" || cmd === "--squirrel-updated") {
run(["--createShortcut=" + target], app.quit);
return true;
}
if (cmd === "--squirrel-uninstall") {
run(["--removeShortcut=" + target], app.quit);
return true;
}
if (cmd === "--squirrel-obsolete") {
app.quit();
return true;
}
}
return false;
};
var electronSquirrelStartup = check();
const started = /* @__PURE__ */ getDefaultExportFromCjs(electronSquirrelStartup);
if (started) {
require$$3$1.app.quit();
}
const createWindow = () => {
const mainWindow = new require$$3$1.BrowserWindow({
width: 800,
height: 600,
webPreferences: {
preload: path$1.join(__dirname, "preload.js")
}
});
{
mainWindow.loadURL("http://localhost:3008");
}
mainWindow.webContents.openDevTools();
};
require$$3$1.app.on("ready", createWindow);
require$$3$1.app.on("window-all-closed", () => {
if (process.platform !== "darwin") {
require$$3$1.app.quit();
}
});
require$$3$1.app.on("activate", () => {
if (require$$3$1.BrowserWindow.getAllWindows().length === 0) {
createWindow();
}
});

View File

@ -1 +0,0 @@
"use strict";

View File

@ -1,54 +1,77 @@
import { ipcMain } from 'electron' import { ipcMain, dialog, app } from 'electron'
import { Wallet } from 'ethers'
import fs from 'fs' import fs from 'fs'
import path from 'path' import path from 'path'
import { encrypt, fromEncryptedJson } from './utils/keystore'
// Create keystore into default wallets directory
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => { ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
try { try {
const wallet = Wallet.fromPhrase(seed) const keystore = await encrypt(seed, password)
const keystore = await wallet.encrypt(password)
const savePath = path.join(process.cwd(), 'wallets') const savePath = path.join(process.cwd(), 'wallets')
fs.mkdirSync(savePath, { recursive: true }) fs.mkdirSync(savePath, { recursive: true })
const filePath = path.join(savePath, `${wallet.address}.json`) // Use timestamp for filename
const timestamp = Date.now()
const fileName = `neptune-wallet-${timestamp}.json`
const filePath = path.join(savePath, fileName)
fs.writeFileSync(filePath, keystore) fs.writeFileSync(filePath, keystore)
return { address: wallet.address, filePath, error: null } return { filePath }
} catch (error) { } catch (error) {
console.error('Error creating keystore:', error) console.error('Error creating keystore:', error)
return { address: null, filePath: null, error: String(error) } throw error
}
})
// New handler: let user choose folder and filename to save keystore
ipcMain.handle('wallet:saveKeystoreAs', async (_event, seed: string, password: string) => {
try {
const keystore = await encrypt(seed, password)
// Use timestamp for default filename
const timestamp = Date.now()
const defaultName = `neptune-wallet-${timestamp}.json`
const { canceled, filePath } = await dialog.showSaveDialog({
title: 'Save Keystore File',
defaultPath: path.join(app.getPath('documents'), defaultName),
filters: [{ name: 'JSON', extensions: ['json'] }],
})
if (canceled || !filePath) return { filePath: null }
fs.mkdirSync(path.dirname(filePath), { recursive: true })
fs.writeFileSync(filePath, keystore)
return { filePath }
} catch (error) {
console.error('Error saving keystore (Save As):', error)
throw error
} }
}) })
ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => { ipcMain.handle('wallet:decryptKeystore', async (_event, filePath, password) => {
try { try {
const json = fs.readFileSync(filePath, 'utf-8') const json = fs.readFileSync(filePath, 'utf-8')
const wallet = await Wallet.fromEncryptedJson(json, password) const phrase = await fromEncryptedJson(json, password)
let phrase: string | undefined return { phrase }
if ('mnemonic' in wallet && wallet.mnemonic) {
phrase = wallet.mnemonic.phrase
}
return { address: wallet.address, phrase, error: null }
} catch (error) { } catch (error) {
console.error('Error decrypting keystore ipc:', error) console.error('Error decrypting keystore ipc:', error)
return { address: null, phrase: null, error: String(error) } throw error
} }
}) })
ipcMain.handle('wallet:checkKeystore', async () => { ipcMain.handle('wallet:checkKeystore', async () => {
try { try {
const walletDir = path.join(process.cwd(), 'wallets') const walletDir = path.join(process.cwd(), 'wallets')
if (!fs.existsSync(walletDir)) if (!fs.existsSync(walletDir)) return { exists: false, filePath: null }
return { exists: false, filePath: null, error: 'Wallet directory not found' }
const file = fs.readdirSync(walletDir).find((f) => f.endsWith('.json')) const file = fs.readdirSync(walletDir).find((f) => f.endsWith('.json'))
if (!file) return { exists: false, filePath: null, error: 'Keystore file not found' } if (!file) return { exists: false, filePath: null }
const filePath = path.join(walletDir, file) const filePath = path.join(walletDir, file)
return { exists: true, filePath, error: null } return { exists: true, filePath}
} catch (error) { } catch (error) {
console.error('Error checking keystore:', error) console.error('Error checking keystore:', error)
return { exists: false, filePath: null, error: String(error) } return { exists: false, filePath: null, error: String(error) }

View File

@ -9,7 +9,7 @@ if (started) {
const createWindow = () => { const createWindow = () => {
const mainWindow = new BrowserWindow({ const mainWindow = new BrowserWindow({
width: 1000, width: 800,
height: 800, height: 800,
webPreferences: { webPreferences: {
preload: path.join(__dirname, 'preload.js'), preload: path.join(__dirname, 'preload.js'),

View File

@ -5,6 +5,8 @@ import { contextBridge, ipcRenderer } from 'electron'
contextBridge.exposeInMainWorld('walletApi', { contextBridge.exposeInMainWorld('walletApi', {
createKeystore: (seed: string, password: string) => createKeystore: (seed: string, password: string) =>
ipcRenderer.invoke('wallet:createKeystore', seed, password), ipcRenderer.invoke('wallet:createKeystore', seed, password),
saveKeystoreAs: (seed: string, password: string) =>
ipcRenderer.invoke('wallet:saveKeystoreAs', seed, password),
decryptKeystore: (filePath: string, password: string) => decryptKeystore: (filePath: string, password: string) =>
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password), ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'), checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),

1
electron/utils/index.ts Normal file
View File

@ -0,0 +1 @@
export * from './keystore'

View File

@ -0,0 +1,54 @@
import crypto from 'crypto'
export async function encrypt(seed: string, password: string) {
const salt = crypto.randomBytes(16)
const iv = crypto.randomBytes(12)
// derive 32-byte key từ password
const key = await new Promise<Buffer>((resolve, reject) => {
crypto.scrypt(password, salt, 32, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => {
if (err) reject(err)
else resolve(derivedKey as Buffer)
})
})
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv)
const ciphertext = Buffer.concat([cipher.update(seed, 'utf8'), cipher.final()])
const authTag = cipher.getAuthTag()
const encryptedMnemonic = Buffer.concat([salt, iv, authTag, ciphertext]).toString('hex')
const keystore = {
type: 'neptune-wallet',
encryption: 'aes-256-gcm',
version: 1,
wallet: {
mnemonic: encryptedMnemonic,
},
}
return JSON.stringify(keystore, null, 2)
}
export async function fromEncryptedJson(json: string, password: string) {
const data = JSON.parse(json)
const encrypted = Buffer.from(data.wallet.mnemonic, 'hex')
const salt = encrypted.subarray(0, 16)
const iv = encrypted.subarray(16, 28)
const authTag = encrypted.subarray(28, 44)
const ciphertext = encrypted.subarray(44)
const key = await new Promise<Buffer>((resolve, reject) => {
crypto.scrypt(password, salt, 32, { N: 16384, r: 8, p: 1 }, (err, derivedKey) => {
if (err) reject(err)
else resolve(derivedKey as Buffer)
})
})
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv)
decipher.setAuthTag(authTag)
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()])
return decrypted.toString('utf8')
}

116
package-lock.json generated
View File

@ -14,9 +14,8 @@
"@neptune/wasm": "file:./packages/neptune-wasm", "@neptune/wasm": "file:./packages/neptune-wasm",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.6.8", "axios": "^1.6.8",
"dayjs": "^1.11.10", "crypto": "^1.0.1",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"ethers": "^6.15.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",
@ -53,12 +52,6 @@
"vue-tsc": "^2.0.11" "vue-tsc": "^2.0.11"
} }
}, },
"node_modules/@adraffy/ens-normalize": {
"version": "1.10.1",
"resolved": "https://registry.npmjs.org/@adraffy/ens-normalize/-/ens-normalize-1.10.1.tgz",
"integrity": "sha512-96Z2IP3mYmF1Xg2cDm8f1gWGf/HUVedQ3FMifV4kG/PQ4yEP51xDtRAEfhVNt5f/uzpNkZHwWQuUcu6D6K+Ekw==",
"license": "MIT"
},
"node_modules/@ant-design/colors": { "node_modules/@ant-design/colors": {
"version": "6.0.0", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz", "resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
@ -2533,30 +2526,6 @@
"resolved": "packages/neptune-wasm", "resolved": "packages/neptune-wasm",
"link": true "link": true
}, },
"node_modules/@noble/curves": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@noble/curves/-/curves-1.2.0.tgz",
"integrity": "sha512-oYclrNgRaM9SsBUBVbb8M6DTV7ZHRTKugureoYEncY5c65HOmRzvSiTE3y5CYaPYJA/GVkrhXEoF0M3Ya9PMnw==",
"license": "MIT",
"dependencies": {
"@noble/hashes": "1.3.2"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@noble/hashes": {
"version": "1.3.2",
"resolved": "https://registry.npmjs.org/@noble/hashes/-/hashes-1.3.2.tgz",
"integrity": "sha512-MVC8EAQp7MvEcm30KWENFjgR+Mkmf+D189XJTkFIlwohU5hcBbn1ZkKq7KVTi2Hme3PMGF390DaL52beVrIihQ==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://paulmillr.com/funding/"
}
},
"node_modules/@nodelib/fs.scandir": { "node_modules/@nodelib/fs.scandir": {
"version": "2.1.5", "version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",
@ -4338,12 +4307,6 @@
"acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0"
} }
}, },
"node_modules/aes-js": {
"version": "4.0.0-beta.5",
"resolved": "https://registry.npmjs.org/aes-js/-/aes-js-4.0.0-beta.5.tgz",
"integrity": "sha512-G965FqalsNyrPqgEGON7nIx1e/OVENSgiEIzyC63haUMuvNnwIgIjMs52hlTCKhkBny7A2ORNlfY9Zu+jmGk1Q==",
"license": "MIT"
},
"node_modules/agent-base": { "node_modules/agent-base": {
"version": "6.0.2", "version": "6.0.2",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
@ -5338,6 +5301,13 @@
"node": ">=12.10" "node": ">=12.10"
} }
}, },
"node_modules/crypto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz",
"integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==",
"deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.",
"license": "ISC"
},
"node_modules/cssesc": { "node_modules/cssesc": {
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
@ -6734,49 +6704,6 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/ethers": {
"version": "6.15.0",
"resolved": "https://registry.npmjs.org/ethers/-/ethers-6.15.0.tgz",
"integrity": "sha512-Kf/3ZW54L4UT0pZtsY/rf+EkBU7Qi5nnhonjUb8yTXcxH3cdcWrV2cRyk0Xk/4jK6OoHhxxZHriyhje20If2hQ==",
"funding": [
{
"type": "individual",
"url": "https://github.com/sponsors/ethers-io/"
},
{
"type": "individual",
"url": "https://www.buymeacoffee.com/ricmoo"
}
],
"license": "MIT",
"dependencies": {
"@adraffy/ens-normalize": "1.10.1",
"@noble/curves": "1.2.0",
"@noble/hashes": "1.3.2",
"@types/node": "22.7.5",
"aes-js": "4.0.0-beta.5",
"tslib": "2.7.0",
"ws": "8.17.1"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/ethers/node_modules/@types/node": {
"version": "22.7.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
"license": "MIT",
"dependencies": {
"undici-types": "~6.19.2"
}
},
"node_modules/ethers/node_modules/undici-types": {
"version": "6.19.8",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz",
"integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==",
"license": "MIT"
},
"node_modules/eventemitter3": { "node_modules/eventemitter3": {
"version": "5.0.1", "version": "5.0.1",
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
@ -11351,12 +11278,6 @@
"typescript": ">=4.2.0" "typescript": ">=4.2.0"
} }
}, },
"node_modules/tslib": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.7.0.tgz",
"integrity": "sha512-gLXCKdN1/j47AiHiOkJN69hJmcbGTHI0ImLmbYLHykhgeN0jVGola9yVjFgzCUklsZQMW55o+dW7IXv3RCXDzA==",
"license": "0BSD"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@ -12181,27 +12102,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/ws": {
"version": "8.17.1",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz",
"integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/wsl-utils": { "node_modules/wsl-utils": {
"version": "0.1.0", "version": "0.1.0",
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz", "resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",

View File

@ -24,9 +24,8 @@
"@neptune/wasm": "file:./packages/neptune-wasm", "@neptune/wasm": "file:./packages/neptune-wasm",
"ant-design-vue": "^4.2.6", "ant-design-vue": "^4.2.6",
"axios": "^1.6.8", "axios": "^1.6.8",
"dayjs": "^1.11.10", "crypto": "^1.0.1",
"electron-squirrel-startup": "^1.0.1", "electron-squirrel-startup": "^1.0.1",
"ethers": "^6.15.0",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"vue": "^3.4.21", "vue": "^3.4.21",
"vue-router": "^4.3.0", "vue-router": "^4.3.0",

View File

@ -1,3 +1,4 @@
import { STATUS_CODE_SUCCESS } from '@/utils/constants/code'
import axios from 'axios' import axios from 'axios'
axios.defaults.withCredentials = false axios.defaults.withCredentials = false
@ -10,6 +11,28 @@ const instance = axios.create({
}, },
}) })
instance.interceptors.response.use(
(response) => {
return response
},
(error) => {
return Promise.reject(error)
}
)
instance.interceptors.response.use(
function (response) {
if (response?.status !== STATUS_CODE_SUCCESS) return Promise.reject(response?.data)
return response.data
},
function (error) {
if (error?.response?.data) {
return Promise.reject(error?.response?.data)
}
return Promise.reject(error)
}
)
export const setLocaleApi = (locale: string) => { export const setLocaleApi = (locale: string) => {
instance.defaults.headers.common['lang'] = locale instance.defaults.headers.common['lang'] = locale
} }

View File

@ -1,18 +1,18 @@
import { callJsonRpc } from '@/api/request' import { callJsonRpc } from '@/api/request'
export const getUtxosFromViewKey = ( export const getUtxosFromViewKey = async (
viewKey: string, viewKey: string,
startBlock: number = 0, startBlock: number = 0,
endBlock: number | null = null, endBlock: number | null = null,
maxSearchDepth: number = 1000 maxSearchDepth: number = 1000
) => { ): Promise<any> => {
const params = { const params = {
viewKey, viewKey,
startBlock, startBlock,
endBlock, endBlock,
maxSearchDepth, maxSearchDepth,
} }
return callJsonRpc('wallet_getUtxosFromViewKey', params) return await callJsonRpc('wallet_getUtxosFromViewKey', params)
} }
export const getBalance = async (): Promise<any> => { export const getBalance = async (): Promise<any> => {

View File

@ -1,8 +1,17 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&display=swap');
html { html {
font-family: 'Noto Sans JP'; font-family: var(--font-primary);
font-size: 15px; font-size: 15px;
scroll-behavior: smooth;
}
body {
max-height: 100vh;
display: flex;
flex-direction: column;
margin: 0;
padding: 0;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
overflow: hidden;
} }
*, *,
@ -31,3 +40,40 @@ h2 {
margin-bottom: 1rem; margin-bottom: 1rem;
text-align: center; text-align: center;
} }
// ==================== CUSTOM SCROLLBAR ====================
// Webkit browsers (Chrome, Safari, Edge)
* {
&::-webkit-scrollbar {
width: 8px;
}
&::-webkit-scrollbar-track {
background: rgba(0, 0, 0, 0.05);
border-radius: var(--radius-full);
}
&::-webkit-scrollbar-thumb {
background: rgba(0, 127, 207, 0.4);
border-radius: var(--radius-full);
border: 2px solid transparent;
background-clip: padding-box;
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.1);
transition: background var(--transition-fast), box-shadow var(--transition-fast);
&:hover {
background: rgba(0, 127, 207, 0.6);
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.15);
}
&:active {
background: var(--primary-color);
box-shadow: inset 0 0 2px rgba(0, 0, 0, 0.2);
}
}
&::-webkit-scrollbar-corner {
background: var(--bg-light);
}
}

View File

@ -74,9 +74,8 @@
// ==================== TYPOGRAPHY ==================== // ==================== TYPOGRAPHY ====================
// Font Families // Font Families
--font-primary: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; --font-primary: --apple-system, BlinkMacSystemFont, 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
--font-mono: 'Courier New', monospace; --font-mono: 'Courier New', monospace;
--font-noto: 'Noto Sans JP';
// Font Sizes // Font Sizes
--font-xs: 0.75rem; // 12px --font-xs: 0.75rem; // 12px
@ -87,7 +86,7 @@
--font-xl: 1.1rem; // 17.6px --font-xl: 1.1rem; // 17.6px
--font-2xl: 1.2rem; // 19.2px --font-2xl: 1.2rem; // 19.2px
--font-3xl: 1.5rem; // 24px --font-3xl: 1.5rem; // 24px
--font-4xl: 3rem; // 48px --font-4xl: 2rem; // 32px
// Font Weights // Font Weights
--font-normal: 400; --font-normal: 400;
@ -106,26 +105,6 @@
--tracking-wide: 0.5px; --tracking-wide: 0.5px;
--tracking-wider: 1px; --tracking-wider: 1px;
// ==================== Z-INDEX ====================
--z-base: 1;
--z-dropdown: 10;
--z-sticky: 20;
--z-fixed: 30;
--z-modal-backdrop: 40;
--z-modal: 50;
--z-popover: 60;
--z-tooltip: 70;
// ==================== BREAKPOINTS ====================
--breakpoint-xs: 480px;
--breakpoint-sm: 640px;
--breakpoint-md: 768px;
--breakpoint-lg: 1024px;
--breakpoint-xl: 1280px;
--breakpoint-2xl: 1536px;
// ==================== COMPONENTS SPECIFIC ==================== // ==================== COMPONENTS SPECIFIC ====================
// Card // Card
@ -139,13 +118,6 @@
--btn-padding-y: 0.75rem; --btn-padding-y: 0.75rem;
--btn-padding-x: 1rem; --btn-padding-x: 1rem;
--btn-radius: var(--radius-md); --btn-radius: var(--radius-md);
--btn-transition: var(--transition-all);
// QR Code
--qr-size: 200px;
--qr-border: 3px solid var(--border-light);
--qr-radius: var(--radius-lg);
--qr-shadow: var(--shadow-sm);
// Tabs // Tabs
--tabs-height: 3px; --tabs-height: 3px;

View File

@ -1,333 +0,0 @@
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import { ButtonCommon, SpinnerCommon } from '@/components'
const neptuneStore = useNeptuneStore()
const { getBalance, getBlockHeight } = useNeptuneWallet()
const availableBalance = ref(0)
const pendingBalance = ref(0)
const currentDaaScore = ref(0)
const isLoadingData = ref(false)
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const isAddressExpanded = ref(false)
const walletStatus = computed(() => {
if (neptuneStore.getLoading) return 'Loading...'
if (neptuneStore.getError) return 'Error'
if (neptuneStore.getWallet?.address) return 'Online'
return 'Offline'
})
const toggleAddressExpanded = () => {
isAddressExpanded.value = !isAddressExpanded.value
}
const copyAddress = async () => {
if (!receiveAddress.value) {
message.error('No address available')
return
}
try {
await navigator.clipboard.writeText(receiveAddress.value)
message.success('Address copied to clipboard!')
} catch (err) {
message.error('Failed to copy address')
}
}
const handleSend = () => {
// TODO: Implement send transaction functionality
}
const loadWalletData = async () => {
if (!receiveAddress.value) return
isLoadingData.value = true
try {
const [balanceResult, blockHeightResult] = await Promise.all([
getBalance(),
getBlockHeight(),
])
if (balanceResult) {
if (typeof balanceResult === 'number') {
availableBalance.value = balanceResult
} else if (balanceResult.confirmed !== undefined) {
availableBalance.value = balanceResult.confirmed || 0
pendingBalance.value = balanceResult.unconfirmed || 0
} else if (balanceResult.balance !== undefined) {
availableBalance.value = parseFloat(balanceResult.balance) || 0
}
}
if (blockHeightResult) {
currentDaaScore.value =
typeof blockHeightResult === 'number'
? blockHeightResult
: blockHeightResult.height || 0
}
} catch (error) {
message.error('Failed to load wallet data')
} finally {
isLoadingData.value = false
}
}
onMounted(() => {
loadWalletData()
})
</script>
<template>
<div class="wallet-info-container">
<div v-if="isLoadingData && !receiveAddress" class="loading-state">
<SpinnerCommon size="medium" />
<p>Loading wallet data...</p>
</div>
<div v-else-if="!receiveAddress" class="empty-state">
<p>No wallet found. Please create or import a wallet.</p>
</div>
<template v-else>
<!-- Balance Section -->
<div class="balance-section">
<div class="balance-label">Available</div>
<div class="balance-amount">
<span v-if="isLoadingData">Loading...</span>
<span v-else>{{ formatNumberToLocaleString(availableBalance) }} NEPT</span>
</div>
<div class="pending-section">
<span class="pending-label">Pending</span>
<span class="pending-amount">
{{ isLoadingData ? '...' : formatNumberToLocaleString(pendingBalance) }}
NEPT
</span>
</div>
</div>
<!-- Receive Address Section -->
<div class="receive-section">
<div class="address-label">Receive Address:</div>
<div
class="address-value"
:class="{ expanded: isAddressExpanded, collapsed: !isAddressExpanded }"
@click="copyAddress"
>
{{ receiveAddress || 'No address available' }}
<svg
class="copy-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</div>
<button
v-if="receiveAddress && receiveAddress.length > 80"
class="toggle-address-btn"
@click.stop="toggleAddressExpanded"
>
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
</button>
</div>
<!-- Action Buttons -->
<div class="action-buttons">
<ButtonCommon
type="primary"
size="large"
block
@click="handleSend"
class="btn-send"
>
SEND
</ButtonCommon>
</div>
<!-- Wallet Status -->
<div class="wallet-status">
<span
>Wallet Status: <strong>{{ walletStatus }}</strong></span
>
<span
>DAA Score:
<strong>{{
isLoadingData ? '...' : formatNumberToLocaleString(currentDaaScore)
}}</strong></span
>
</div>
</template>
</div>
</template>
<style lang="scss" scoped>
.wallet-info-container {
@include card-base;
}
.loading-state,
.empty-state {
text-align: center;
padding: var(--spacing-3xl);
color: var(--text-secondary);
p {
margin: var(--spacing-lg) 0 0;
font-size: var(--font-base);
}
}
.balance-section {
text-align: center;
margin-bottom: var(--spacing-3xl);
padding-bottom: var(--spacing-2xl);
border-bottom: 2px solid var(--border-color);
.balance-label {
color: var(--text-muted);
font-size: var(--font-base);
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.balance-amount {
font-size: var(--font-4xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
letter-spacing: var(--tracking-tight);
}
.pending-section {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-md);
.pending-label {
font-weight: var(--font-medium);
}
.pending-amount {
font-weight: var(--font-semibold);
}
}
}
.receive-section {
margin-bottom: var(--spacing-2xl);
.address-label {
font-size: var(--font-base);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
font-weight: var(--font-semibold);
}
.address-value {
background: var(--bg-light);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
word-break: break-all;
font-family: var(--font-mono);
font-size: var(--font-sm);
color: var(--primary-color);
cursor: pointer;
transition: var(--transition-all);
display: flex;
align-items: flex-start;
gap: var(--spacing-sm);
border: 2px solid transparent;
line-height: 1.5;
position: relative;
&.collapsed {
display: -webkit-box;
-webkit-line-clamp: 3;
line-clamp: 3;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
&.expanded {
display: flex;
flex-direction: column;
word-break: break-all;
}
&:hover {
background: var(--bg-hover);
border-color: var(--border-primary);
}
.copy-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--primary-color);
margin-top: 2px;
align-self: flex-start;
}
}
.toggle-address-btn {
margin-top: var(--spacing-md);
padding: var(--spacing-xs) var(--spacing-md);
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
font-weight: var(--font-medium);
cursor: pointer;
text-decoration: underline;
transition: color 0.2s ease;
&:hover {
color: var(--primary-hover);
}
&:active {
opacity: 0.8;
}
}
}
.action-buttons {
display: flex;
gap: var(--spacing-lg);
margin-bottom: var(--spacing-2xl);
:deep(.btn-send) {
letter-spacing: var(--tracking-wide);
}
}
.wallet-status {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: 1.25rem;
background: var(--bg-light);
border-radius: var(--radius-md);
font-size: var(--font-base);
color: var(--text-secondary);
strong {
color: var(--text-primary);
font-weight: var(--font-semibold);
}
}
</style>

View File

@ -0,0 +1,14 @@
<script setup lang="ts">
</script>
<template>
<div class="card-base">
<slot />
</div>
</template>
<style lang="scss" scoped>
.card-base {
@include card-base;
}
</style>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
</script>
<template>
<div class="card-base scrollable">
<slot />
</div>
</template>
<style lang="scss" scoped>
.card-base {
@include card-base;
&.scrollable {
height: 100%;
max-height: calc(100vh - 100px);
overflow-y: auto;
}
}
</style>

View File

@ -1,12 +1,32 @@
<script lang="ts" setup></script> <script lang="ts" setup></script>
<template> <template>
<a-layout> <div class="app-layout">
<a-layout class="ant-layout-body"> <div class="app-layout-body">
<!-- <AppHeaderVue /> --> <div class="app-content">
<a-layout-content>
<slot /> <slot />
</a-layout-content> </div>
</a-layout> </div>
</a-layout> </div>
</template> </template>
<style lang="scss" scoped>
.app-layout {
height: 100%;
display: flex;
flex-direction: column;
}
.app-layout-body {
height: 100%;
display: flex;
flex-direction: column;
}
.app-content {
flex: 1;
display: flex;
flex-direction: column;
height: 100%;
}
</style>

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed } from 'vue' import { ref, computed } from 'vue'
import { ButtonCommon, FormCommon } from '@/components' import { ButtonCommon, CardBase, FormCommon } from '@/components'
interface Props { interface Props {
title?: string title?: string
@ -52,53 +52,55 @@ const handleSubmit = () => {
} }
const handleBack = () => { const handleBack = () => {
emit('back')
password.value = '' password.value = ''
passwordError.value = '' passwordError.value = ''
emit('back')
} }
</script> </script>
<template> <template>
<div class="auth-card-content"> <CardBase>
<div class="form-group"> <div class="auth-card-content">
<FormCommon <div class="form-group">
v-model="password" <FormCommon
type="password" v-model="password"
:label="props.label" type="password"
:placeholder="props.placeholder" :label="props.label"
show-password-toggle :placeholder="props.placeholder"
required show-password-toggle
:error="passwordError" required
@input="passwordError = ''" :error="passwordError"
@keyup.enter="handleSubmit" @input="passwordError = ''"
/> @keyup.enter="handleSubmit"
<span v-if="error" class="error-message">{{ errorMessage }}</span> />
</div> <span v-if="error" class="error-message">{{ errorMessage }}</span>
</div>
<div class="auth-button-group"> <div class="auth-button-group">
<ButtonCommon <ButtonCommon
v-if="!props.hideBackButton" v-if="!props.hideBackButton"
type="default" type="default"
size="large" size="large"
class="auth-button" class="auth-button"
block block
@click="handleBack" @click="handleBack"
> >
{{ props.backButtonText }} {{ props.backButtonText }}
</ButtonCommon> </ButtonCommon>
<ButtonCommon <ButtonCommon
type="primary" type="primary"
size="large" size="large"
class="auth-button" class="auth-button"
block block
:disabled="!canProceed" :disabled="!canProceed"
:loading="props.loading" :loading="props.loading"
@click="handleSubmit" @click="handleSubmit"
> >
{{ props.buttonText }} {{ props.buttonText }}
</ButtonCommon> </ButtonCommon>
</div>
</div> </div>
</div> </CardBase>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>

View File

@ -3,6 +3,8 @@ import ButtonCommon from './common/ButtonCommon.vue'
import FormCommon from './common/FormCommon.vue' import FormCommon from './common/FormCommon.vue'
import PasswordForm from './common/PasswordForm.vue' import PasswordForm from './common/PasswordForm.vue'
import SpinnerCommon from './common/SpinnerCommon.vue' import SpinnerCommon from './common/SpinnerCommon.vue'
import CardBase from './common/CardBase.vue'
import CardBaseScrollable from './common/CardBaseScrollable.vue'
import { IconCommon } from './icon' import { IconCommon } from './icon'
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, IconCommon } export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, CardBase, CardBaseScrollable, IconCommon }

View File

@ -26,20 +26,14 @@ export function useNeptuneWallet() {
initPromise = (async () => { initPromise = (async () => {
try { try {
store.setLoading(true)
store.setError(null)
await initWasm() await initWasm()
wasmInitialized = true wasmInitialized = true
} catch (err) { } catch (err) {
wasmInitialized = false wasmInitialized = false
const errorMsg = 'Failed to initialize Neptune WASM' const errorMsg = 'Failed to initialize Neptune WASM'
store.setError(errorMsg)
console.error('WASM init error:', err) console.error('WASM init error:', err)
throw new Error(errorMsg) throw new Error(errorMsg)
} finally {
store.setLoading(false)
} }
})() })()
@ -48,9 +42,6 @@ export function useNeptuneWallet() {
const generateWallet = async (): Promise<GenerateSeedResult> => { const generateWallet = async (): Promise<GenerateSeedResult> => {
try { try {
store.setLoading(true)
store.setError(null)
await ensureWasmInitialized() await ensureWasmInitialized()
const resultJson = generate_seed() const resultJson = generate_seed()
@ -65,30 +56,27 @@ export function useNeptuneWallet() {
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to generate wallet' console.error('Error generating wallet:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => { const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
await ensureWasmInitialized() try {
const seedPhraseJson = JSON.stringify(seedPhrase) await ensureWasmInitialized()
const resultJson = get_viewkey(seedPhraseJson, store.getNetwork) const seedPhraseJson = JSON.stringify(seedPhrase)
return JSON.parse(resultJson) const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
return JSON.parse(resultJson)
} catch (err) {
console.error('Error getting view key from seed:', err)
throw err
}
} }
const importWallet = async (seedPhrase: string[]): Promise<ViewKeyResult> => { const recoverWalletFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
try { try {
store.setLoading(true) const isValid = validate_seed_phrase(JSON.stringify(seedPhrase))
store.setError(null) if (!isValid) throw new Error('Invalid seed phrase')
const isValid = await validateSeedPhrase(seedPhrase)
if (!isValid) {
throw new Error('Invalid seed phrase')
}
const result = await getViewKeyFromSeed(seedPhrase) const result = await getViewKeyFromSeed(seedPhrase)
@ -99,51 +87,84 @@ export function useNeptuneWallet() {
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to import wallet' console.error('Error recovering wallet from seed:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => { const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase)
return address_from_seed(seedPhraseJson, store.getNetwork)
}
const validateSeedPhrase = async (seedPhrase: string[]): Promise<boolean> => {
try { try {
await ensureWasmInitialized() await ensureWasmInitialized()
const seedPhraseJson = JSON.stringify(seedPhrase) const seedPhraseJson = JSON.stringify(seedPhrase)
return validate_seed_phrase(seedPhraseJson) return address_from_seed(seedPhraseJson, store.getNetwork)
} catch (err) { } catch (err) {
console.error('Validation error:', err) console.error('Error getting address from seed:', err)
return false throw err
} }
} }
const decryptKeystore = async (password: string): Promise<void> => { const decryptKeystore = async (password: string): Promise<void> => {
try { try {
const keystorePath = store.getKeystorePath
if (!keystorePath) await checkKeystore()
const result = await (window as any).walletApi.decryptKeystore( const result = await (window as any).walletApi.decryptKeystore(
store.getKeystorePath || '', store.getKeystorePath,
password password
) )
if (result.error) {
console.error('Error decrypting keystore composable:', result.error)
return
}
const seedPhrase = result.phrase.trim().split(/\s+/) const seedPhrase = result.phrase.trim().split(/\s+/)
const viewKeyResult = await getViewKeyFromSeed(seedPhrase) const viewKeyResult = await getViewKeyFromSeed(seedPhrase)
store.setPassword(password)
store.setSeedPhrase(seedPhrase) store.setSeedPhrase(seedPhrase)
store.setAddress(viewKeyResult.address) store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key) store.setViewKey(viewKeyResult.view_key)
store.setReceiverId(viewKeyResult.receiver_identifier) store.setReceiverId(viewKeyResult.receiver_identifier)
} catch (err) { } catch (err) {
console.error('Error decrypting keystore composable:', err) if (
err instanceof Error &&
(err.message.includes('Unsupported state') ||
err.message.includes('unable to authenticate'))
) {
console.error('Invalid password')
} else console.error('Error decrypting keystore:', err)
throw err
}
}
const createKeystore = async (seed: string, password: string): Promise<string> => {
try {
const result = await (window as any).walletApi.createKeystore(seed, password)
return result.filePath
} catch (err) {
console.error('Error creating keystore:', err)
throw err
}
}
const saveKeystoreAs = async (seed: string, password: string): Promise<string> => {
try {
const result = await (window as any).walletApi.saveKeystoreAs(seed, password)
if (!result.filePath) throw new Error('User canceled')
return result.filePath
} catch (err) {
console.error('Error saving keystore:', err)
throw err
}
}
const checkKeystore = async (): Promise<boolean> => {
try {
const keystoreFile = await (window as any).walletApi.checkKeystore()
if (!keystoreFile.exists) return false
store.setKeystorePath(keystoreFile.filePath)
return true
} catch (err) {
console.error('Error checking keystore:', err)
throw err
} }
} }
@ -159,9 +180,6 @@ export function useNeptuneWallet() {
throw new Error('No view key available. Please import or generate a wallet first.') throw new Error('No view key available. Please import or generate a wallet first.')
} }
store.setLoading(true)
store.setError(null)
const response = await API.getUtxosFromViewKey( const response = await API.getUtxosFromViewKey(
store.getViewKey, store.getViewKey,
startBlock, startBlock,
@ -169,68 +187,49 @@ export function useNeptuneWallet() {
maxSearchDepth maxSearchDepth
) )
const result = response.data?.result || response.data const result = response?.result || response
store.setUtxos(result.utxos || result || []) store.setUtxos(result.utxos || result || [])
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get UTXOs' console.error('Error getting UTXOs:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const getBalance = async (): Promise<any> => { const getBalance = async (): Promise<any> => {
try { try {
store.setLoading(true)
store.setError(null)
const response = await API.getBalance() const response = await API.getBalance()
const result = response.data?.result || response.data const result = response?.result || response
store.setBalance(result.balance || result) store.setBalance(result.balance || result)
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get balance' console.error('Error getting balance:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const getBlockHeight = async (): Promise<any> => { const getBlockHeight = async (): Promise<any> => {
try { try {
store.setLoading(true)
store.setError(null)
const response = await API.getBlockHeight() const response = await API.getBlockHeight()
const result = response.data?.result || response.data const result = response?.result || response
return result.height || result return result?.height || result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get block height' console.error('Error getting block height:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const getNetworkInfo = async (): Promise<any> => { const getNetworkInfo = async (): Promise<any> => {
try { try {
store.setLoading(true)
store.setError(null)
const response = await API.getNetworkInfo() const response = await API.getNetworkInfo()
const result = response.data?.result || response.data const result = response?.result || response
store.setNetwork((result.network + 'net') as 'mainnet' | 'testnet')
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to get network info' console.error('Error getting network info:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
@ -240,28 +239,27 @@ export function useNeptuneWallet() {
fee: string fee: string
): Promise<any> => { ): Promise<any> => {
try { try {
store.setLoading(true)
store.setError(null)
const response = await API.sendTransaction(toAddress, amount, fee) const response = await API.sendTransaction(toAddress, amount, fee)
const result = response.data?.result || response.data const result = response?.result || response
return result return result
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to send transaction' console.error('Error sending transaction:', err)
store.setError(errorMsg)
throw err throw err
} finally {
store.setLoading(false)
} }
} }
const setNetwork = async (network: 'mainnet' | 'testnet') => { const setNetwork = async (network: 'mainnet' | 'testnet') => {
store.setNetwork(network) try {
store.setNetwork(network)
if (store.getSeedPhrase) { if (store.getSeedPhrase) {
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase) const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
store.setAddress(viewKeyResult.address) store.setAddress(viewKeyResult.address)
store.setViewKey(viewKeyResult.view_key) store.setViewKey(viewKeyResult.view_key)
}
} catch (err) {
console.error('Error setting network:', err)
throw err
} }
} }
@ -273,10 +271,9 @@ export function useNeptuneWallet() {
return { return {
initWasm: ensureWasmInitialized, initWasm: ensureWasmInitialized,
generateWallet, generateWallet,
importWallet, recoverWalletFromSeed,
getViewKeyFromSeed, getViewKeyFromSeed,
getAddressFromSeed, getAddressFromSeed,
validateSeedPhrase,
getUtxos, getUtxos,
getBalance, getBalance,
@ -284,6 +281,9 @@ export function useNeptuneWallet() {
getNetworkInfo, getNetworkInfo,
sendTransaction, sendTransaction,
decryptKeystore, decryptKeystore,
createKeystore,
saveKeystoreAs,
checkKeystore,
clearWallet, clearWallet,
setNetwork, setNetwork,

View File

@ -1,6 +1,8 @@
import * as Page from '@/views' import * as Page from '@/views'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useNeptuneStore } from '@/stores/neptuneStore'
import { useAuthStore } from '@/stores/authStore' import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useAuthStore } from '@/stores'
const ifAuthenticated = (to: any, from: any, next: any) => { const ifAuthenticated = (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore() const neptuneStore = useNeptuneStore()
@ -12,16 +14,12 @@ const ifAuthenticated = (to: any, from: any, next: any) => {
next('/auth') next('/auth')
} }
const ifNotAuthenticated = async (to: any, from: any, next: any) => { const ifNotAuthenticated = async (to: any, from: any, next: any) => {
const neptuneStore = useNeptuneStore() const neptuneWallet = useNeptuneWallet()
const authStore = useAuthStore() const authStore = useAuthStore()
const keystoreFile = await (window as any).walletApi.checkKeystore() const exists = await neptuneWallet.checkKeystore()
if (keystoreFile.exists) { if (exists) authStore.goToLogin()
neptuneStore.setKeystorePath(keystoreFile.filePath)
authStore.setState('login')
}
next() next()
} }

View File

@ -5,11 +5,14 @@ export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confir
// Auth store to manage the flow // Auth store to manage the flow
const currentState = ref<AuthState>('onboarding') const currentState = ref<AuthState>('onboarding')
const previousState = ref<AuthState | null>(null)
export const useAuthStore = () => { export const useAuthStore = () => {
const getCurrentState = () => currentState.value const getCurrentState = () => currentState.value
const getPreviousState = () => previousState.value
const setState = (state: AuthState) => { const setState = (state: AuthState) => {
previousState.value = currentState.value
currentState.value = state currentState.value = state
} }
@ -25,17 +28,18 @@ export const useAuthStore = () => {
setState('recovery') setState('recovery')
} }
const resetFlow = () => { const goBack = () => {
setState('onboarding') previousState.value ? setState(previousState.value) : setState('onboarding')
} }
return { return {
currentState: currentState.value, currentState: currentState.value,
getCurrentState, getCurrentState,
getPreviousState,
setState, setState,
goToCreate, goToCreate,
goToLogin, goToLogin,
goToRecover, goToRecover,
resetFlow, goBack,
} }
} }

View File

@ -1,3 +1,2 @@
export * from './seedStore'
export * from './authStore' export * from './authStore'
export * from './neptuneStore' export * from './neptuneStore'

View File

@ -19,9 +19,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
const keystorePath = ref<null | string>(null) const keystorePath = ref<null | string>(null)
const isLoading = ref(false)
const error = ref<string | null>(null)
// ===== SETTERS ===== // ===== SETTERS =====
const setSeedPhrase = (seedPhrase: string[] | null) => { const setSeedPhrase = (seedPhrase: string[] | null) => {
@ -56,14 +53,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
wallet.value.utxos = utxos wallet.value.utxos = utxos
} }
const setLoading = (loading: boolean) => {
isLoading.value = loading
}
const setError = (err: string | null) => {
error.value = err
}
const setWallet = (walletData: Partial<WalletState>) => { const setWallet = (walletData: Partial<WalletState>) => {
wallet.value = { ...wallet.value, ...walletData } wallet.value = { ...wallet.value, ...walletData }
} }
@ -83,7 +72,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
balance: null, balance: null,
utxos: [], utxos: [],
} }
error.value = null
} }
// ===== GETTERS ===== // ===== GETTERS =====
@ -98,8 +86,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
const getBalance = computed(() => wallet.value.balance) const getBalance = computed(() => wallet.value.balance)
const getUtxos = computed(() => wallet.value.utxos) const getUtxos = computed(() => wallet.value.utxos)
const hasWallet = computed(() => wallet.value.address !== null) const hasWallet = computed(() => wallet.value.address !== null)
const getLoading = computed(() => isLoading.value)
const getError = computed(() => error.value)
const getKeystorePath = computed(() => keystorePath.value) const getKeystorePath = computed(() => keystorePath.value)
return { return {
getWallet, getWallet,
@ -113,8 +99,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
getBalance, getBalance,
getUtxos, getUtxos,
hasWallet, hasWallet,
getLoading,
getError,
getKeystorePath, getKeystorePath,
setSeedPhrase, setSeedPhrase,
setPassword, setPassword,
@ -124,8 +108,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
setNetwork, setNetwork,
setBalance, setBalance,
setUtxos, setUtxos,
setLoading,
setError,
setWallet, setWallet,
setKeystorePath, setKeystorePath,
clearWallet, clearWallet,

View File

@ -1,33 +0,0 @@
import { ref } from 'vue'
const seedWords = ref<string[]>([])
const isSeedGenerated = ref(false)
export const useSeedStore = () => {
const setSeedWords = (words: string[]) => {
seedWords.value = words
isSeedGenerated.value = true
}
const getSeedWords = () => {
return seedWords.value
}
const clearSeedWords = () => {
seedWords.value = []
isSeedGenerated.value = false
}
const hasSeedWords = () => {
return isSeedGenerated.value && seedWords.value.length > 0
}
return {
seedWords: seedWords.value,
isSeedGenerated: isSeedGenerated.value,
setSeedWords,
getSeedWords,
clearSeedWords,
hasSeedWords,
}
}

View File

@ -1,12 +1,2 @@
import dayjs from 'dayjs'
export const PAGE_FIRST = 1 export const PAGE_FIRST = 1
export const PER_PAGE = 40 export const PER_PAGE = 40
export const MAX_STRING = 255
export const CURRENT_DAY = dayjs(new Date()).format('DD')
export const CURRENT_MONTH = dayjs(new Date()).format('MM')
export const CURRENT_YEAR = dayjs(new Date()).format('YYYY')
export const MONTHS = Array.from({ length: 12 }, (item, i) => {
return dayjs(new Date(0, i)).format('MM')
})

View File

@ -1,9 +1,3 @@
import dayjs from 'dayjs'
export const formatNumberToLocaleString = (num: number): string => { export const formatNumberToLocaleString = (num: number): string => {
return num.toLocaleString('en-US') return num.toLocaleString('en-US')
} }
export const formatDate = (day: any, format = 'YYYY-MM-DD') => {
return dayjs(new Date(day)).format(format)
}

View File

@ -1,7 +0,0 @@
export const ACCESS_TOKEN = 'access_token'
export const USER = 'user'
export const getToken = () => localStorage.getItem(ACCESS_TOKEN)
export const getAdmin = () => localStorage.getItem(USER)

View File

@ -1,5 +1,4 @@
export * from './constants/code' export * from './constants/code'
export * from './constants/constants' export * from './constants/constants'
export * from './helpers/format' export * from './helpers/format'
export * from './helpers/localStorage'
export * from './helpers/seedPhrase' export * from './helpers/seedPhrase'

View File

@ -34,7 +34,6 @@ const handleGoToRecover = () => {
.auth-container { .auth-container {
min-height: 100vh; min-height: 100vh;
background: var(--bg-light); background: var(--bg-light);
font-family: var(--font-primary);
} }
.complete-state { .complete-state {

View File

@ -4,6 +4,7 @@ import { PasswordForm } from '@/components'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet' import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { useAuthStore } from '@/stores/authStore' import { useAuthStore } from '@/stores/authStore'
import { message } from 'ant-design-vue'
const authStore = useAuthStore() const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
@ -15,20 +16,19 @@ const error = ref(false)
const handlePasswordSubmit = async (password: string) => { const handlePasswordSubmit = async (password: string) => {
try { try {
isLoading.value = true isLoading.value = true
error.value = false
await neptuneWallet.decryptKeystore(password) await neptuneWallet.decryptKeystore(password)
router.push({name: 'home'}) router.push({ name: 'home' })
} catch (err) { } catch (err) {
error.value = true error.value = true
console.error('Password Error') } finally {
}
finally {
isLoading.value = false isLoading.value = false
} }
} }
const handleNewWallet = () => { const handleNewWallet = () => {
authStore.goToCreate() authStore.goToCreate()
router.push({name: 'auth'}) router.push({ name: 'auth' })
} }
</script> </script>

View File

@ -1,20 +1,23 @@
<script setup lang="ts"> <script setup lang="ts">
import { RecoverWalletComponent } from '.' import { RecoverWalletComponent } from '.'
import { useAuthStore } from '@/stores'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const authStore = useAuthStore()
const router = useRouter() const router = useRouter()
const handleImported = (payload: { type: 'seed' | 'keystore'; value: string | string[] }) => { const handleCancel = () => {
if (payload.type === 'keystore') { authStore.goBack()
localStorage.setItem('temp_keystore', JSON.stringify(payload.value)) }
return router.push({ name: 'password' })
} const handleAccessWallet = () => {
router.push({ name: 'home' })
} }
</script> </script>
<template> <template>
<div class="recover-seed-tab"> <div class="recover-seed-tab">
<RecoverWalletComponent @import-success="handleImported" /> <RecoverWalletComponent @cancel="handleCancel" @access-wallet="handleAccessWallet" />
</div> </div>
</template> </template>

View File

@ -4,6 +4,20 @@ import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
interface Props {
backButton?: boolean
nextButton?: boolean
backButtonText?: string
nextButtonText?: string
}
const props = withDefaults(defineProps<Props>(), {
backButton: true,
nextButton: true,
backButtonText: 'BACK',
nextButtonText: 'NEXT',
})
const emit = defineEmits<{ const emit = defineEmits<{
next: [] next: []
back: [] back: []
@ -50,10 +64,6 @@ const handleCopySeed = async () => {
<div class="recovery-content"> <div class="recovery-content">
<div class="instruction-text"> <div class="instruction-text">
<p>
Your wallet is accessible by a seed phrase. The seed phrase is an ordered
18-word secret phrase.
</p>
<p> <p>
Make sure no one is looking, as anyone with your seed phrase can access your Make sure no one is looking, as anyone with your seed phrase can access your
wallet and funds. Write it down and keep it safe. wallet and funds. Write it down and keep it safe.
@ -91,24 +101,23 @@ const handleCopySeed = async () => {
<p> No seed phrase found. Please go back and generate a wallet first.</p> <p> No seed phrase found. Please go back and generate a wallet first.</p>
</div> </div>
<div class="cool-fact">
<p>
Cool fact: there are more 18-word phrase combinations than atoms in the
observable universe!
</p>
</div>
<div class="recovery-actions"> <div class="recovery-actions">
<ButtonCommon type="default" size="large" @click="handleBack"> <ButtonCommon
BACK v-if="props.backButton"
type="default"
size="large"
@click="handleBack"
>
{{ backButtonText }}
</ButtonCommon> </ButtonCommon>
<ButtonCommon <ButtonCommon
v-if="props.nextButton"
type="primary" type="primary"
size="large" size="large"
@click="handleNext" @click="handleNext"
:disabled="!seedWords || seedWords.length === 0" :disabled="!seedWords || seedWords.length === 0"
> >
NEXT {{ nextButtonText }}
</ButtonCommon> </ButtonCommon>
</div> </div>
</div> </div>
@ -121,16 +130,15 @@ const handleCopySeed = async () => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light); background: var(--bg-light);
min-height: 100vh;
} }
.recovery-card { .recovery-card {
@include card-base;
max-width: 500px; max-width: 500px;
padding: var(--spacing-xl);
width: 100%; width: 100%;
border: 2px solid var(--primary-color); border: 2px solid var(--primary-color);
border-radius: var(--radius-md);
} }
.recovery-header { .recovery-header {

View File

@ -1,6 +1,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, defineEmits, onMounted, computed } from 'vue' import { ref, defineEmits, onMounted, computed } from 'vue'
import { ButtonCommon } from '@/components' import { ButtonCommon, CardBase } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useNeptuneStore } from '@/stores/neptuneStore'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
@ -82,7 +82,6 @@ const handleAnswerSelect = (answer: string) => {
} }
const handleNext = () => { const handleNext = () => {
emit('next')
if (isCorrect.value) { if (isCorrect.value) {
correctCount.value++ correctCount.value++
askedPositions.value.add(quizData.value!.position) askedPositions.value.add(quizData.value!.position)
@ -208,16 +207,13 @@ onMounted(() => {
> >
NEXT QUESTION NEXT QUESTION
</ButtonCommon> </ButtonCommon>
<!-- <ButtonCommon <ButtonCommon
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions" v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
type="primary" type="primary"
size="large" size="large"
@click="handleNext" @click="handleNext"
> >
CONTINUE CONTINUE
</ButtonCommon> -->
<ButtonCommon type="primary" size="large" @click="handleNext">
CONTINUE
</ButtonCommon> </ButtonCommon>
</div> </div>
</div> </div>
@ -230,16 +226,15 @@ onMounted(() => {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--spacing-xl);
background: var(--bg-light); background: var(--bg-light);
min-height: 100vh;
} }
.confirm-card { .confirm-card {
@include card-base;
max-width: 500px; max-width: 500px;
width: 100%; width: 100%;
padding: var(--spacing-xl);
border: 2px solid var(--primary-color); border: 2px solid var(--primary-color);
border-radius: var(--radius-md);
} }
.confirm-header { .confirm-header {

View File

@ -163,6 +163,7 @@ const handleIHaveWallet = () => {
required required
:error="confirmPasswordError" :error="confirmPasswordError"
@input="confirmPasswordError = ''" @input="confirmPasswordError = ''"
@keyup.enter="handleNext"
/> />
<div <div
v-if="confirmPassword" v-if="confirmPassword"

View File

@ -4,6 +4,7 @@ import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
import { CreatePasswordStep, WalletCreatedStep } from '.' import { CreatePasswordStep, WalletCreatedStep } from '.'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet' import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { CardBaseScrollable } from '@/components'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const emit = defineEmits<{ const emit = defineEmits<{
@ -36,9 +37,20 @@ const handleNextToWalletCreated = () => {
} }
const handleNextFromPassword = async () => { const handleNextFromPassword = async () => {
const result = await generateWallet() try {
if (result) step.value = 2 await generateWallet()
else message.error('Failed to generate wallet') step.value = 2
} catch (err) {
message.error('Failed to generate wallet')
}
}
const handleBackToCreatePassword = () => {
step.value = 1
}
const handleBackToSeedPhrase = () => {
step.value = 2
} }
const handleAccessWallet = () => { const handleAccessWallet = () => {
@ -52,32 +64,37 @@ function resetAll() {
</script> </script>
<template> <template>
<div class="auth-container"> <div class="auth-container">
<div class="auth-card"> <CardBaseScrollable>
<!-- Step 1: Create Password --> <div class="auth-card">
<CreatePasswordStep <!-- Step 1: Create Password -->
v-if="step === 1" <CreatePasswordStep
@next="handleNextFromPassword" v-if="step === 1"
@navigate-to-recover-wallet="handleNavigateToRecoverWallet" @next="handleNextFromPassword"
/> @navigate-to-recover-wallet="handleNavigateToRecoverWallet"
/>
<!-- Step 2: Recovery Seed --> <!-- Step 2: Recovery Seed -->
<SeedPhraseDisplayComponent v-else-if="step === 2" @next="handleNextToConfirmSeed" /> <SeedPhraseDisplayComponent
v-else-if="step === 2"
@back="handleBackToCreatePassword"
@next="handleNextToConfirmSeed"
/>
<!-- Step 3: Confirm Seed --> <!-- Step 3: Confirm Seed -->
<ConfirmSeedComponent v-else-if="step === 3" @next="handleNextToWalletCreated" /> <ConfirmSeedComponent
v-else-if="step === 3"
@back="handleBackToSeedPhrase"
@next="handleNextToWalletCreated"
/>
<!-- Step 4: Success --> <!-- Step 4: Success -->
<WalletCreatedStep <WalletCreatedStep
v-else-if="step === 4" v-else-if="step === 4"
@access-wallet="handleAccessWallet" @access-wallet="handleAccessWallet"
@create-another="resetAll" @create-another="resetAll"
/> />
</div>
<!-- Fallback slot --> </CardBaseScrollable>
<template v-else>
<slot></slot>
</template>
</div>
</div> </div>
</template> </template>
@ -91,7 +108,6 @@ function resetAll() {
} }
.auth-card { .auth-card {
@include card-base;
max-width: 720px; max-width: 720px;
width: 100%; width: 100%;

View File

@ -1,8 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { ButtonCommon } from '@/components' import { ButtonCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
const neptuneStore = useNeptuneStore() const neptuneStore = useNeptuneStore()
const { createKeystore } = useNeptuneWallet()
const emit = defineEmits<{ const emit = defineEmits<{
accessWallet: [] accessWallet: []
@ -10,10 +13,20 @@ const emit = defineEmits<{
}>() }>()
const handleAccessWallet = async () => { const handleAccessWallet = async () => {
const seedPhrase = neptuneStore.getSeedPhraseString try {
const password = neptuneStore.getPassword! const seedPhrase = neptuneStore.getSeedPhraseString
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password) const password = neptuneStore.getPassword!
emit('accessWallet')
if (!seedPhrase || !password) {
message.error('Missing seed or password')
return
}
await createKeystore(seedPhrase, password)
emit('accessWallet')
} catch (err) {
message.error('Failed to create keystore')
}
} }
const handleCreateAnother = () => { const handleCreateAnother = () => {

View File

@ -1,34 +1,37 @@
<script setup lang="ts"> <script setup lang="ts">
import { validateSeedPhrase18 } from '@/utils' import { ref } from 'vue'
import { computed, ref, watch } from 'vue'
interface Props {
valid?: boolean
}
const props = withDefaults(defineProps<Props>(), {
valid: true,
})
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'update:valid', valid: boolean): void
(e: 'submit', words: string[]): void (e: 'submit', words: string[]): void
(e: 'update:words', words: string[]): void
(e: 'update:valid', valid: boolean): void
}>() }>()
const seedWords = ref<string[]>(Array.from({ length: 18 }, () => '')) const seedWords = ref<string[]>(Array.from({ length: 18 }, () => ''))
const seedError = ref('')
const isValid = computed(() => {
const words = seedWords.value.filter((w) => w.trim())
return validateSeedPhrase18(words) && !seedError.value
})
watch(isValid, (newVal) => {
emit('update:valid', newVal)
})
const inputBoxFocus = (idx: number) => { const inputBoxFocus = (idx: number) => {
document.getElementById('input-' + idx)?.focus() if (idx < 18) {
document.getElementById('input-' + idx)?.focus()
}
} }
const handleGridInput = (index: number, value: string) => { const handleGridInput = (index: number, value: string) => {
emit('update:valid', true)
seedWords.value[index] = value seedWords.value[index] = value
emit('update:words', seedWords.value.filter((w) => w.trim()))
} }
const handlePaste = (event: ClipboardEvent) => { const handlePaste = (event: ClipboardEvent) => {
event.preventDefault() event.preventDefault()
emit('update:valid', true)
const pastedData = event.clipboardData?.getData('text') || '' const pastedData = event.clipboardData?.getData('text') || ''
const words = pastedData const words = pastedData
.trim() .trim()
@ -37,13 +40,13 @@ const handlePaste = (event: ClipboardEvent) => {
if (words.length === 0) return if (words.length === 0) return
seedWords.value = words const filledWords = Array.from({ length: 18 }, (_, i) => words[i] || '')
seedError.value = '' seedWords.value = filledWords
emit('update:words', words.filter((w) => w.trim()))
} }
const handleSubmit = () => { const handleSubmit = () => {
const words = seedWords.value.filter((w) => w.trim()) const words = seedWords.value.filter((w) => w.trim())
if (validateSeedPhrase18(words)) seedError.value = ''
emit('submit', words) emit('submit', words)
} }
@ -73,15 +76,13 @@ defineExpose({
:placeholder="i + 1 + '.'" :placeholder="i + 1 + '.'"
maxlength="24" maxlength="24"
@keydown.enter="inputBoxFocus(i + 1)" @keydown.enter="inputBoxFocus(i + 1)"
:class="{ error: seedError && !word.trim() }"
@focus="seedError = ''"
@input="handleGridInput(i, ($event.target as HTMLInputElement).value)" @input="handleGridInput(i, ($event.target as HTMLInputElement).value)"
@paste="handlePaste($event)" @paste="handlePaste($event)"
/> />
</div> </div>
</div> </div>
</div> </div>
<div v-if="seedError" class="error-text">{{ seedError }}</div> <div v-if="!props.valid" class="error-text">Invalid seed phrase</div>
</div> </div>
</template> </template>

View File

@ -1,88 +1,109 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { ref } from 'vue'
import { Modal } from 'ant-design-vue'
import { ButtonCommon, PasswordForm } from '@/components' import { ButtonCommon, PasswordForm } from '@/components'
import { RecoverSeedComponent } from '..' import { RecoverSeedComponent } from '..'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
const emit = defineEmits<{ const emit = defineEmits<{
(e: 'import-success', data: { type: 'seed'; value: string[]; password: string }): void (e: 'cancel'): void
(e: 'accessWallet'): void
}>() }>()
const { recoverWalletFromSeed, createKeystore } = useNeptuneWallet()
const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>() const recoverSeedComponentRef = ref<InstanceType<typeof RecoverSeedComponent>>()
const isSeedPhraseValid = ref(false) const showPasswordForm = ref(false)
const showPasswordModal = ref(false)
const seedPhrase = ref<string[]>([]) const seedPhrase = ref<string[]>([])
const isLoading = ref(false) const isLoading = ref(false)
const passwordError = ref(false) const isSeedPhraseValid = ref(true)
const handleSeedPhraseSubmit = (words: string[]) => { const handleSeedWordsUpdate = (words: string[]) => {
seedPhrase.value = words seedPhrase.value = words
showPasswordModal.value = true }
const handleSeedPhraseSubmit = async (words: string[]) => {
seedPhrase.value = words
showPasswordForm.value = true
} }
const handleContinue = () => { const handleContinue = () => {
recoverSeedComponentRef.value?.handleSubmit?.() recoverSeedComponentRef.value?.handleSubmit?.()
} }
const handlePasswordSubmit = (password: string) => { const handlePasswordSubmit = async (password: string) => {
showPasswordModal.value = false try {
emit('import-success', { isLoading.value = true
type: 'seed', const result = await recoverWalletFromSeed(seedPhrase.value)
value: seedPhrase.value, if (result.address) {
password, await createKeystore(seedPhrase.value.join(' '), password)
}) emit('accessWallet')
}
} catch (err) {
if (err instanceof Error && err.message.includes('Invalid seed phrase')) {
isSeedPhraseValid.value = false
} else {
message.error('Failed to recover wallet from seed')
}
} finally {
isLoading.value = false
showPasswordForm.value = false
}
} }
const handlePasswordBack = () => { const handlePasswordBack = () => {
showPasswordModal.value = false showPasswordForm.value = false
passwordError.value = false
} }
const handleModalCancel = () => { const handleCancel = () => {
showPasswordModal.value = false emit('cancel')
passwordError.value = false showPasswordForm.value = false
seedPhrase.value = []
isSeedPhraseValid.value = true
} }
</script> </script>
<template> <template>
<div class="import-wallet dark-card"> <div class="import-wallet dark-card">
<h2 class="title">Recover Wallet</h2> <h2 class="title">Recover Wallet</h2>
<div class="desc">Enter your recovery seed phrase</div>
<RecoverSeedComponent
ref="recoverSeedComponentRef"
@update:valid="isSeedPhraseValid = $event"
@submit="handleSeedPhraseSubmit"
/>
<ButtonCommon
class="mt-lg"
type="primary"
block
size="large"
:disabled="!isSeedPhraseValid"
@click="handleContinue"
>Continue</ButtonCommon
>
</div>
<Modal <!-- Seed Phrase Step -->
v-model:open="showPasswordModal" <div v-if="!showPasswordForm">
title="Enter Password" <div class="desc">Enter your recovery seed phrase</div>
:footer="null" <RecoverSeedComponent
:width="480" ref="recoverSeedComponentRef"
:mask-closable="false" :valid="isSeedPhraseValid"
:keyboard="false" @submit="handleSeedPhraseSubmit"
@cancel="handleModalCancel" @update:words="handleSeedWordsUpdate"
> @update:valid="isSeedPhraseValid = $event"
<PasswordForm />
button-text="Continue" <ButtonCommon
placeholder="Enter password to encrypt seed phrase" class="mt-20"
label="Password" type="primary"
:loading="isLoading" block
:error="passwordError" size="large"
:error-message="'Invalid password'" :disabled="isLoading"
@submit="handlePasswordSubmit" @click="handleContinue"
@back="handlePasswordBack" >
/> Continue
</Modal> </ButtonCommon>
<ButtonCommon class="mt-10" type="default" block size="large" @click="handleCancel">
Cancel
</ButtonCommon>
</div>
<!-- Password Step -->
<div v-else>
<div class="desc">Enter password to encrypt seed phrase</div>
<PasswordForm
button-text="Submit"
placeholder="Enter password to encrypt seed phrase"
label="Password"
:loading="isLoading"
@submit="handlePasswordSubmit"
@back="handlePasswordBack"
/>
</div>
</div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.import-wallet { .import-wallet {
@ -107,9 +128,6 @@ const handleModalCancel = () => {
font-size: 1rem; font-size: 1rem;
margin-bottom: 24px; margin-bottom: 24px;
} }
.mt-lg {
margin-top: 32px;
}
} }
@include screen(mobile) { @include screen(mobile) {
.import-wallet { .import-wallet {

View File

@ -1,63 +1,64 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { onMounted, ref } from 'vue'
import { Tabs, Row, Col } from 'ant-design-vue' import { Tabs } from 'ant-design-vue'
import WalletInfo from '@/components/WalletInfo.vue'
import { WalletTab, NetworkTab, UTXOTab } from './components' import { WalletTab, NetworkTab, UTXOTab } from './components'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
const neptuneWallet = useNeptuneWallet()
const activeTab = ref('UTXOs') const activeTab = ref('UTXOs')
const network = ref('neptune-mainnet') const network = ref('neptune-mainnet')
onMounted(async () => {
await neptuneWallet.getNetworkInfo()
})
</script> </script>
<template> <template>
<div class="home-container"> <div class="home-container">
<Row :gutter="[24, 24]"> <Tabs v-model:activeKey="activeTab" size="large" class="main-tabs">
<!-- Left Column ---> <!-- DEBUG TAB -->
<Col :xs="24" :lg="10"> <Tabs.TabPane key="UTXOs" tab="UTXOs">
<WalletInfo /> <UTXOTab />
</Col> </Tabs.TabPane>
<!-- Right Column - Tabs Content --> <!-- WALLET TAB -->
<Col :xs="24" :lg="12"> <Tabs.TabPane key="WALLET" tab="WALLET">
<Tabs v-model:activeKey="activeTab" size="large" class="main-tabs"> <WalletTab :network="network" />
<!-- DEBUG TAB --> </Tabs.TabPane>
<Tabs.TabPane key="UTXOs" tab="UTXOs">
<UTXOTab />
</Tabs.TabPane>
<!-- WALLET TAB --> <!-- NETWORK TAB -->
<Tabs.TabPane key="WALLET" tab="WALLET"> <Tabs.TabPane key="NETWORK" tab="NETWORK">
<WalletTab :network="network" /> <NetworkTab />
</Tabs.TabPane> </Tabs.TabPane>
</Tabs>
<!-- NETWORK TAB -->
<Tabs.TabPane key="NETWORK" tab="NETWORK">
<NetworkTab />
</Tabs.TabPane>
</Tabs>
</Col>
</Row>
</div> </div>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.home-container { .home-container {
min-height: 100vh; height: 100%;
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%); display: flex;
flex-direction: column;
padding: var(--spacing-lg); padding: var(--spacing-lg);
font-family: var(--font-primary);
@include screen(tablet) { @include screen(tablet) {
padding: var(--spacing-2xl); padding: var(--spacing-xl);
} }
@include screen(desktop) { @include screen(desktop) {
padding: var(--spacing-2xl); padding: var(--spacing-xl);
} }
} }
:deep(.main-tabs) { :deep(.main-tabs) {
height: 100%;
display: flex;
flex-direction: column;
.ant-tabs-nav { .ant-tabs-nav {
margin-bottom: var(--spacing-lg); margin-bottom: var(--spacing-md);
flex-shrink: 0;
} }
.ant-tabs-tab { .ant-tabs-tab {
@ -82,7 +83,15 @@ const network = ref('neptune-mainnet')
} }
.ant-tabs-content { .ant-tabs-content {
padding-top: var(--spacing-lg); flex: 1;
display: flex;
flex-direction: column;
.ant-tabs-tabpane {
height: 100%;
display: flex;
flex-direction: column;
}
} }
} }
</style> </style>

View File

@ -1,46 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted, onUnmounted } from 'vue'
import { formatNumberToLocaleString } from '@/utils' import { formatNumberToLocaleString } from '@/utils'
import { SpinnerCommon } from '@/components' import { CardBase, SpinnerCommon } from '@/components'
import { useNeptuneStore } from '@/stores/neptuneStore' import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet' import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue' import { message } from 'ant-design-vue'
import { get_network_info } from '@neptune/wasm'
const neptuneStore = useNeptuneStore() const neptuneStore = useNeptuneStore()
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet() const { getBlockHeight } = useNeptuneWallet()
const blockHeight = ref(0) const blockHeight = ref(0)
const networkInfo = ref<any>(null)
const loading = ref(true) const loading = ref(true)
const error = ref('') const error = ref('')
const lastUpdate = ref<Date | null>(null) const lastUpdate = ref<Date | null>(null)
// let pollingInterval: number | null = null let pollingInterval: number | null = null
const network = computed(() => neptuneStore.getNetwork) const network = computed(() => neptuneStore.getNetwork)
const loadNetworkData = async () => { const loadNetworkData = async () => {
try { try {
loading.value = true
error.value = '' error.value = ''
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()]) const result = await getBlockHeight()
if (result.height || result) blockHeight.value = Number(result.height || result)
if (heightResult !== undefined) {
blockHeight.value =
typeof heightResult === 'number'
? heightResult
: heightResult.height || heightResult || 0
}
if (infoResult) {
networkInfo.value = infoResult
}
lastUpdate.value = new Date() lastUpdate.value = new Date()
loading.value = false
if (loading.value) {
loading.value = false
}
} catch (err) { } catch (err) {
const errorMsg = err instanceof Error ? err.message : 'Failed to load network data' const errorMsg = err instanceof Error ? err.message : 'Failed to load network data'
error.value = errorMsg error.value = errorMsg
@ -55,33 +41,31 @@ const retryConnection = async () => {
await loadNetworkData() await loadNetworkData()
} }
// const startPolling = () => { const startPolling = () => {
// pollingInterval = window.setInterval(() => { pollingInterval = window.setInterval(async () => {
// if (!loading.value) { if (!loading.value) await loadNetworkData()
// loadNetworkData() }, 10000)
// } }
// }, 10000)
// }
// const stopPolling = () => { const stopPolling = () => {
// if (pollingInterval) { if (pollingInterval) {
// clearInterval(pollingInterval) clearInterval(pollingInterval)
// pollingInterval = null pollingInterval = null
// } }
// } }
onMounted(async () => { onMounted(async () => {
await loadNetworkData() await loadNetworkData()
// startPolling() startPolling()
}) })
// onUnmounted(() => { onUnmounted(() => {
// stopPolling() stopPolling()
// }) })
</script> </script>
<template> <template>
<div class="content-card"> <CardBase class="content-card">
<div class="network-status-container"> <div class="network-status-container">
<h2 class="section-title">NETWORK STATUS</h2> <h2 class="section-title">NETWORK STATUS</h2>
@ -105,39 +89,20 @@ onMounted(async () => {
</div> </div>
<div class="status-item"> <div class="status-item">
<span class="status-label">Block Height</span> <span class="status-label">DAA Score</span>
<span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span> <span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span>
</div> </div>
<div v-if="networkInfo?.version" class="status-item">
<span class="status-label">Version</span>
<span class="status-value">{{ networkInfo.version }}</span>
</div>
<div v-if="networkInfo?.peer_count !== undefined" class="status-item">
<span class="status-label">Peers</span>
<span class="status-value">{{ networkInfo.peer_count }}</span>
</div>
<div v-if="networkInfo?.chain_id" class="status-item">
<span class="status-label">Chain ID</span>
<span class="status-value">{{ networkInfo.chain_id }}</span>
</div>
<div v-if="lastUpdate" class="status-item"> <div v-if="lastUpdate" class="status-item">
<span class="status-label">Last Updated</span> <span class="status-label">Last Updated</span>
<span class="status-value">{{ lastUpdate.toLocaleTimeString() }}</span> <span class="status-value">{{ lastUpdate.toLocaleTimeString() }}</span>
</div> </div>
</div> </div>
</div> </div>
</div> </CardBase>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.content-card {
@include card-base;
}
.network-status-container { .network-status-container {
.section-title { .section-title {
font-size: var(--font-xl); font-size: var(--font-xl);
@ -181,7 +146,6 @@ onMounted(async () => {
color: var(--text-primary); color: var(--text-primary);
font-size: var(--font-lg); font-size: var(--font-lg);
text-align: right; text-align: right;
font-family: var(--font-mono);
} }
} }

View File

@ -2,6 +2,7 @@
import { ref } from 'vue' import { ref } from 'vue'
import { EditOutlined } from '@ant-design/icons-vue' import { EditOutlined } from '@ant-design/icons-vue'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet' import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { CardBaseScrollable } from '@/components'
const { getUtxos } = useNeptuneWallet() const { getUtxos } = useNeptuneWallet()
@ -10,7 +11,7 @@ const inUseUtxosAmount = ref(0)
</script> </script>
<template> <template>
<div class="content-card debug-card"> <CardBaseScrollable class="content-card debug-card">
<div class="debug-header"> <div class="debug-header">
<h3 class="debug-title"> <h3 class="debug-title">
IN USE UTXOS IN USE UTXOS
@ -18,19 +19,15 @@ const inUseUtxosAmount = ref(0)
</h3> </h3>
<div class="debug-info"> <div class="debug-info">
<p><strong>COUNT</strong> {{ inUseUtxosCount }}</p> <p><strong>COUNT</strong> {{ inUseUtxosCount }}</p>
<p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} KAS</p> <p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} NPT</p>
</div> </div>
</div> </div>
<div class="list-pagination"></div> <div class="list-pagination"></div>
</div> </CardBaseScrollable>
</template> </template>
<style lang="scss" scoped> <style lang="scss" scoped>
.content-card {
@include card-base;
}
.debug-card { .debug-card {
.debug-header { .debug-header {
text-align: center; text-align: center;

View File

@ -1,140 +0,0 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Divider, Modal } from 'ant-design-vue'
import ButtonCommon from '@/components/common/ButtonCommon.vue'
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
const showSeedModal = ref(false)
const handleBackupFile = () => {
// TODO: Implement backup file functionality
}
const handleBackupSeed = () => {
showSeedModal.value = true
}
const handleCloseModal = () => {
showSeedModal.value = false
}
const handleModalNext = () => {
// SeedPhraseDisplayComponent emits 'next' but in modal context, we just close
handleCloseModal()
}
const handleModalBack = () => {
// SeedPhraseDisplayComponent emits 'back' but in modal context, we just close
handleCloseModal()
}
</script>
<template>
<div class="content-card wallet-info-card">
<div class="wallet-header">
<h2 class="wallet-title">NEPTUNE WALLET</h2>
</div>
<div class="wallet-actions">
<ButtonCommon type="primary" size="large" block @click="handleBackupFile">
Backup File
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleBackupSeed">
Backup Seed
</ButtonCommon>
</div>
<Divider />
</div>
<Modal
v-model:open="showSeedModal"
title="Backup Seed Phrase"
:footer="null"
:width="600"
:mask-closable="false"
:keyboard="false"
@cancel="handleCloseModal"
>
<div class="seed-modal-content">
<SeedPhraseDisplayComponent @next="handleModalNext" @back="handleModalBack" />
</div>
</Modal>
</template>
<style lang="scss" scoped>
.content-card {
@include card-base;
}
.wallet-info-card {
.wallet-header {
text-align: center;
margin-bottom: var(--spacing-2xl);
padding-bottom: var(--spacing-xl);
border-bottom: 2px solid var(--border-color);
.wallet-title {
font-size: var(--font-3xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-sm);
letter-spacing: var(--tracking-wider);
}
.wallet-version {
font-size: var(--font-lg);
color: var(--text-secondary);
margin-bottom: var(--spacing-sm);
}
.wallet-status-text,
.wallet-network {
font-size: var(--font-md);
color: var(--text-secondary);
margin-bottom: var(--spacing-xs);
strong {
color: var(--text-primary);
}
}
}
.wallet-actions {
display: flex;
flex-direction: column;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
}
.donations-section,
.developer-section {
.section-subtitle {
font-size: var(--font-lg);
font-weight: var(--font-semibold);
color: var(--text-primary);
margin: 0;
cursor: pointer;
transition: var(--transition-normal);
&:hover {
color: var(--primary-color);
}
}
}
}
.seed-modal-content {
:deep(.recovery-container) {
padding: 0;
min-height: auto;
background: transparent;
}
:deep(.recovery-card) {
border: none;
box-shadow: none;
padding: 0;
}
}
</style>

View File

@ -1,3 +1,3 @@
export { default as WalletTab } from './WalletTab.vue'
export { default as NetworkTab } from './NetworkTab.vue' export { default as NetworkTab } from './NetworkTab.vue'
export { default as UTXOTab } from './UTXOTab.vue' export { default as UTXOTab } from './UTXOTab.vue'
export { WalletInfo, WalletBalanceAndAddress, WalletTab } from './wallet-tab'

View File

@ -0,0 +1,239 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { message } from 'ant-design-vue'
import { formatNumberToLocaleString } from '@/utils'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { SpinnerCommon } from '@/components'
interface Props {
isLoadingData: boolean
availableBalance: number
pendingBalance: number
}
const props = defineProps<Props>()
const neptuneStore = useNeptuneStore()
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const isAddressExpanded = ref(false)
const toggleAddressExpanded = () => {
isAddressExpanded.value = !isAddressExpanded.value
}
const copyAddress = async () => {
if (!receiveAddress.value) {
message.error('No address available')
return
}
try {
await navigator.clipboard.writeText(receiveAddress.value)
message.success('Address copied to clipboard!')
} catch (err) {
message.error('Failed to copy address')
}
}
</script>
<template>
<div v-if="props.isLoadingData && !receiveAddress" class="loading-state">
<SpinnerCommon size="medium" />
<p>Loading wallet data...</p>
</div>
<div v-else-if="!receiveAddress" class="empty-state">
<p>No wallet found. Please create or import a wallet.</p>
</div>
<div v-else>
<!-- Balance Section -->
<div class="balance-section">
<div class="balance-label">Available</div>
<div class="balance-amount">
<span v-if="props.isLoadingData">Loading...</span>
<span v-else>{{ formatNumberToLocaleString(props.availableBalance) }} NPT</span>
</div>
<div class="pending-section">
<span class="pending-label">Pending</span>
<span class="pending-amount">
{{ props.isLoadingData ? '...' : formatNumberToLocaleString(props.pendingBalance) }}
NPT
</span>
</div>
</div>
<!-- Receive Address Section -->
<div class="receive-section">
<div class="address-label">Receive Address:</div>
<div
class="address-value"
:class="{ expanded: isAddressExpanded, collapsed: !isAddressExpanded }"
@click="copyAddress"
>
<span class="address-text">
{{ receiveAddress || 'No address available' }}
</span>
<svg
class="copy-icon"
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
>
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
</svg>
</div>
<button
v-if="receiveAddress && receiveAddress.length > 80"
class="toggle-address-btn"
@click.stop="toggleAddressExpanded"
>
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
</button>
</div>
</div>
</template>
<style lang="scss" scoped>
.loading-state,
.empty-state {
text-align: center;
padding: var(--spacing-3xl);
color: var(--text-secondary);
p {
margin: var(--spacing-lg) 0 0;
font-size: var(--font-base);
}
}
.balance-section {
text-align: center;
margin-bottom: var(--spacing-xl);
padding-bottom: var(--spacing-lg);
border-bottom: 2px solid var(--border-color);
flex-shrink: 0;
.balance-label {
color: var(--text-muted);
font-size: var(--font-base);
margin-bottom: var(--spacing-sm);
text-transform: uppercase;
letter-spacing: var(--tracking-wider);
}
.balance-amount {
font-size: var(--font-4xl);
font-weight: var(--font-bold);
color: var(--text-primary);
margin-bottom: var(--spacing-lg);
letter-spacing: var(--tracking-tight);
}
.pending-section {
display: flex;
justify-content: center;
align-items: center;
gap: var(--spacing-sm);
color: var(--text-secondary);
font-size: var(--font-md);
.pending-label {
font-weight: var(--font-medium);
}
.pending-amount {
font-weight: var(--font-semibold);
}
}
}
.receive-section {
margin-bottom: var(--spacing-lg);
flex-shrink: 0;
.address-label {
font-size: var(--font-base);
color: var(--text-secondary);
margin-bottom: var(--spacing-md);
font-weight: var(--font-semibold);
}
.address-value {
background: var(--bg-light);
padding: var(--spacing-lg);
border-radius: var(--radius-md);
word-break: break-all;
font-family: var(--font-mono);
font-size: var(--font-sm);
color: var(--primary-color);
cursor: pointer;
transition: var(--transition-all);
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: var(--spacing-sm);
border: 2px solid transparent;
line-height: 1.5;
position: relative;
.address-text {
flex: 1;
display: -webkit-box;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
}
&.collapsed .address-text {
-webkit-line-clamp: 2;
line-clamp: 2;
}
&.expanded .address-text {
display: block;
overflow: visible;
text-overflow: initial;
}
&:hover {
background: var(--bg-hover);
border-color: var(--border-primary);
}
.copy-icon {
width: 18px;
height: 18px;
flex-shrink: 0;
color: var(--primary-color);
margin-top: 2px;
align-self: flex-start;
}
}
.toggle-address-btn {
margin-top: var(--spacing-md);
padding: var(--spacing-xs) var(--spacing-md);
background: none;
border: none;
color: var(--primary-color);
font-size: var(--font-sm);
font-weight: var(--font-medium);
cursor: pointer;
text-decoration: underline;
transition: color 0.2s ease;
&:hover {
color: var(--primary-hover);
}
&:active {
opacity: 0.8;
}
}
}
</style>

View File

@ -0,0 +1,229 @@
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { Modal } from 'ant-design-vue'
import { useNeptuneStore } from '@/stores/neptuneStore'
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
import { message } from 'ant-design-vue'
import { ButtonCommon, CardBaseScrollable } from '@/components'
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
import WalletBalanceAndAddress from './WalletBalanceAndAddress.vue'
const neptuneStore = useNeptuneStore()
const { getBalance, saveKeystoreAs } = useNeptuneWallet()
const availableBalance = ref(0)
const pendingBalance = ref(0)
const loading = ref(false)
const error = ref<string | null>(null)
const showSeedModal = ref(false)
const getModalContainer = (): HTMLElement => {
const homeContainer = document.querySelector('.home-container') as HTMLElement
return homeContainer || document.body
}
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
const walletStatus = computed(() => {
if (loading.value) return 'Loading...'
if (error.value) return 'Error'
if (neptuneStore.getWallet?.address) return 'Online'
return 'Offline'
})
const windowWidth = ref(window.innerWidth)
const modalWidth = computed(() => {
if (windowWidth.value <= 767) {
return '90%'
}
return '60%'
})
const handleResize = () => {
windowWidth.value = window.innerWidth
}
const handleClickSendButton = () => {
// TODO: Implement send transaction functionality
}
const handleBackupFile = async () => {
try {
const seed = neptuneStore.getSeedPhraseString
const password = neptuneStore.getPassword
if (!seed || !password) {
message.error('Missing seed or password')
return
}
await saveKeystoreAs(seed, password)
message.success('Keystore saved successfully')
} catch (err) {
if (err instanceof Error && err.message.includes('User canceled')) return
message.error('Failed to save keystore')
}
}
const handleBackupSeed = () => {
showSeedModal.value = true
}
const handleCloseModal = () => {
showSeedModal.value = false
}
const handleModalNext = () => {
handleCloseModal()
}
const loadWalletData = async () => {
const receiveAddress = neptuneStore.getWallet?.address || ''
if (!receiveAddress) return
loading.value = true
error.value = null
try {
const result = await getBalance()
availableBalance.value = +result.confirmedAvailable || 0
pendingBalance.value = +result.unconfirmedAvailable || 0
} catch (error) {
message.error('Failed to load wallet data')
} finally {
loading.value = false
}
}
onMounted(() => {
loadWalletData()
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
</script>
<template>
<CardBaseScrollable class="wallet-info-container">
<div class="wallet-content">
<WalletBalanceAndAddress
:is-loading-data="loading"
:available-balance="availableBalance"
:pending-balance="pendingBalance"
/>
<!-- Action Buttons -->
<div v-if="receiveAddress" class="action-buttons">
<ButtonCommon
type="primary"
size="large"
block
@click="handleClickSendButton"
class="btn-send"
>
SEND
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleBackupFile">
Backup File
</ButtonCommon>
<ButtonCommon type="primary" size="large" block @click="handleBackupSeed">
Backup Seed
</ButtonCommon>
</div>
<!-- Wallet Status -->
<div v-if="receiveAddress" class="wallet-status">
<span
>Wallet Status: <strong>{{ walletStatus }}</strong></span
>
</div>
</div>
</CardBaseScrollable>
<Modal
v-model:open="showSeedModal"
title="Backup Seed Phrase"
:footer="null"
:width="modalWidth"
:mask-closable="false"
:keyboard="false"
:get-container="getModalContainer"
@cancel="handleCloseModal"
>
<div class="seed-modal-content">
<SeedPhraseDisplayComponent
:back-button="false"
:next-button-text="'DONE'"
@next="handleModalNext"
/>
</div>
</Modal>
</template>
<style lang="scss" scoped>
.wallet-info-container {
height: 100%;
}
.wallet-content {
background: inherit;
height: 100%;
}
.action-buttons {
@include center_flex;
gap: var(--spacing-md);
margin-bottom: var(--spacing-lg);
flex-shrink: 0;
:deep(.btn-send) {
letter-spacing: var(--tracking-wide);
}
}
.wallet-status {
display: flex;
flex-direction: column;
gap: var(--spacing-sm);
padding: 1rem;
background: var(--bg-light);
border-radius: var(--radius-md);
font-size: var(--font-base);
color: var(--text-secondary);
margin-top: auto;
flex-shrink: 0;
strong {
color: var(--text-primary);
font-weight: var(--font-semibold);
}
}
.seed-modal-content {
:deep(.recovery-container) {
padding: 0;
min-height: auto;
background: transparent;
}
:deep(.recovery-card) {
border: none;
box-shadow: none;
padding: 0;
}
}
// Modal responsive width
:deep(.ant-modal) {
@media (max-width: 767px) {
width: 90% !important;
max-width: 90% !important;
}
@media (min-width: 768px) {
width: 60% !important;
max-width: 60% !important;
}
}
</style>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import { WalletInfo } from '@/views/Home/components'
</script>
<template>
<div class="wallet-tab-container">
<WalletInfo />
</div>
</template>
<style lang="scss" scoped>
.wallet-tab-container {
height: 100%;
}
</style>

View File

@ -0,0 +1,3 @@
export { default as WalletInfo } from './WalletInfo.vue'
export { default as WalletBalanceAndAddress } from './WalletBalanceAndAddress.vue'
export { default as WalletTab } from './WalletTab.vue'

View File

@ -1,6 +1,6 @@
{ {
"extends": "@vue/tsconfig/tsconfig.dom.json", "extends": "@vue/tsconfig/tsconfig.dom.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue", "electron/*"], "include": ["env.d.ts", "src/**/*", "src/**/*.vue", "electron/*", "electron/utils/keystore.ts"],
"exclude": ["src/**/__tests__/*"], "exclude": ["src/**/__tests__/*"],
"compilerOptions": { "compilerOptions": {
"composite": true, "composite": true,