feat 051125/recover_wallet
This commit is contained in:
parent
f23a20df10
commit
e48669d972
2
.gitignore
vendored
2
.gitignore
vendored
@ -13,7 +13,7 @@ dist
|
||||
dist-ssr
|
||||
coverage
|
||||
*.local
|
||||
.vite/*/**
|
||||
.vite/
|
||||
wallets/*
|
||||
|
||||
/cypress/videos/
|
||||
|
||||
@ -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();
|
||||
}
|
||||
});
|
||||
@ -1 +0,0 @@
|
||||
"use strict";
|
||||
@ -1,54 +1,77 @@
|
||||
import { ipcMain } from 'electron'
|
||||
import { Wallet } from 'ethers'
|
||||
import { ipcMain, dialog, app } from 'electron'
|
||||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { encrypt, fromEncryptedJson } from './utils/keystore'
|
||||
|
||||
// Create keystore into default wallets directory
|
||||
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
||||
try {
|
||||
const wallet = Wallet.fromPhrase(seed)
|
||||
const keystore = await wallet.encrypt(password)
|
||||
const keystore = await encrypt(seed, password)
|
||||
|
||||
const savePath = path.join(process.cwd(), 'wallets')
|
||||
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)
|
||||
|
||||
return { address: wallet.address, filePath, error: null }
|
||||
return { filePath }
|
||||
} catch (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) => {
|
||||
try {
|
||||
const json = fs.readFileSync(filePath, 'utf-8')
|
||||
const wallet = await Wallet.fromEncryptedJson(json, password)
|
||||
const phrase = await fromEncryptedJson(json, password)
|
||||
|
||||
let phrase: string | undefined
|
||||
if ('mnemonic' in wallet && wallet.mnemonic) {
|
||||
phrase = wallet.mnemonic.phrase
|
||||
}
|
||||
|
||||
return { address: wallet.address, phrase, error: null }
|
||||
return { phrase }
|
||||
} catch (error) {
|
||||
console.error('Error decrypting keystore ipc:', error)
|
||||
return { address: null, phrase: null, error: String(error) }
|
||||
throw error
|
||||
}
|
||||
})
|
||||
|
||||
ipcMain.handle('wallet:checkKeystore', async () => {
|
||||
try {
|
||||
const walletDir = path.join(process.cwd(), 'wallets')
|
||||
if (!fs.existsSync(walletDir))
|
||||
return { exists: false, filePath: null, error: 'Wallet directory not found' }
|
||||
if (!fs.existsSync(walletDir)) return { exists: false, filePath: null }
|
||||
|
||||
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)
|
||||
return { exists: true, filePath, error: null }
|
||||
return { exists: true, filePath}
|
||||
} catch (error) {
|
||||
console.error('Error checking keystore:', error)
|
||||
return { exists: false, filePath: null, error: String(error) }
|
||||
|
||||
@ -9,7 +9,7 @@ if (started) {
|
||||
|
||||
const createWindow = () => {
|
||||
const mainWindow = new BrowserWindow({
|
||||
width: 1000,
|
||||
width: 800,
|
||||
height: 800,
|
||||
webPreferences: {
|
||||
preload: path.join(__dirname, 'preload.js'),
|
||||
|
||||
@ -5,6 +5,8 @@ import { contextBridge, ipcRenderer } from 'electron'
|
||||
contextBridge.exposeInMainWorld('walletApi', {
|
||||
createKeystore: (seed: string, password: string) =>
|
||||
ipcRenderer.invoke('wallet:createKeystore', seed, password),
|
||||
saveKeystoreAs: (seed: string, password: string) =>
|
||||
ipcRenderer.invoke('wallet:saveKeystoreAs', seed, password),
|
||||
decryptKeystore: (filePath: string, password: string) =>
|
||||
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
||||
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
|
||||
|
||||
1
electron/utils/index.ts
Normal file
1
electron/utils/index.ts
Normal file
@ -0,0 +1 @@
|
||||
export * from './keystore'
|
||||
54
electron/utils/keystore.ts
Normal file
54
electron/utils/keystore.ts
Normal 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
116
package-lock.json
generated
@ -14,9 +14,8 @@
|
||||
"@neptune/wasm": "file:./packages/neptune-wasm",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"crypto": "^1.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
@ -53,12 +52,6 @@
|
||||
"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": {
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@ant-design/colors/-/colors-6.0.0.tgz",
|
||||
@ -2533,30 +2526,6 @@
|
||||
"resolved": "packages/neptune-wasm",
|
||||
"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": {
|
||||
"version": "2.1.5",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz",
|
||||
@ -5338,6 +5301,13 @@
|
||||
"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": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
|
||||
@ -6734,49 +6704,6 @@
|
||||
"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": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz",
|
||||
@ -11351,12 +11278,6 @@
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
|
||||
@ -12181,27 +12102,6 @@
|
||||
"dev": true,
|
||||
"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": {
|
||||
"version": "0.1.0",
|
||||
"resolved": "https://registry.npmjs.org/wsl-utils/-/wsl-utils-0.1.0.tgz",
|
||||
|
||||
@ -24,9 +24,8 @@
|
||||
"@neptune/wasm": "file:./packages/neptune-wasm",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.6.8",
|
||||
"dayjs": "^1.11.10",
|
||||
"crypto": "^1.0.1",
|
||||
"electron-squirrel-startup": "^1.0.1",
|
||||
"ethers": "^6.15.0",
|
||||
"pinia": "^2.1.7",
|
||||
"vue": "^3.4.21",
|
||||
"vue-router": "^4.3.0",
|
||||
|
||||
@ -1,3 +1,4 @@
|
||||
import { STATUS_CODE_SUCCESS } from '@/utils/constants/code'
|
||||
import axios from 'axios'
|
||||
|
||||
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) => {
|
||||
instance.defaults.headers.common['lang'] = locale
|
||||
}
|
||||
|
||||
@ -1,18 +1,18 @@
|
||||
import { callJsonRpc } from '@/api/request'
|
||||
|
||||
export const getUtxosFromViewKey = (
|
||||
export const getUtxosFromViewKey = async (
|
||||
viewKey: string,
|
||||
startBlock: number = 0,
|
||||
endBlock: number | null = null,
|
||||
maxSearchDepth: number = 1000
|
||||
) => {
|
||||
): Promise<any> => {
|
||||
const params = {
|
||||
viewKey,
|
||||
startBlock,
|
||||
endBlock,
|
||||
maxSearchDepth,
|
||||
}
|
||||
return callJsonRpc('wallet_getUtxosFromViewKey', params)
|
||||
return await callJsonRpc('wallet_getUtxosFromViewKey', params)
|
||||
}
|
||||
|
||||
export const getBalance = async (): Promise<any> => {
|
||||
|
||||
@ -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 {
|
||||
font-family: 'Noto Sans JP';
|
||||
font-family: var(--font-primary);
|
||||
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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,9 +74,8 @@
|
||||
// ==================== TYPOGRAPHY ====================
|
||||
|
||||
// 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-noto: 'Noto Sans JP';
|
||||
|
||||
// Font Sizes
|
||||
--font-xs: 0.75rem; // 12px
|
||||
@ -87,7 +86,7 @@
|
||||
--font-xl: 1.1rem; // 17.6px
|
||||
--font-2xl: 1.2rem; // 19.2px
|
||||
--font-3xl: 1.5rem; // 24px
|
||||
--font-4xl: 3rem; // 48px
|
||||
--font-4xl: 2rem; // 32px
|
||||
|
||||
// Font Weights
|
||||
--font-normal: 400;
|
||||
@ -106,26 +105,6 @@
|
||||
--tracking-wide: 0.5px;
|
||||
--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 ====================
|
||||
|
||||
// Card
|
||||
@ -139,13 +118,6 @@
|
||||
--btn-padding-y: 0.75rem;
|
||||
--btn-padding-x: 1rem;
|
||||
--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-height: 3px;
|
||||
|
||||
@ -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>
|
||||
14
src/components/common/CardBase.vue
Normal file
14
src/components/common/CardBase.vue
Normal 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>
|
||||
20
src/components/common/CardBaseScrollable.vue
Normal file
20
src/components/common/CardBaseScrollable.vue
Normal 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>
|
||||
@ -1,12 +1,32 @@
|
||||
<script lang="ts" setup></script>
|
||||
|
||||
<template>
|
||||
<a-layout>
|
||||
<a-layout class="ant-layout-body">
|
||||
<!-- <AppHeaderVue /> -->
|
||||
<a-layout-content>
|
||||
<div class="app-layout">
|
||||
<div class="app-layout-body">
|
||||
<div class="app-content">
|
||||
<slot />
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ButtonCommon, FormCommon } from '@/components'
|
||||
import { ButtonCommon, CardBase, FormCommon } from '@/components'
|
||||
|
||||
interface Props {
|
||||
title?: string
|
||||
@ -52,13 +52,14 @@ const handleSubmit = () => {
|
||||
}
|
||||
|
||||
const handleBack = () => {
|
||||
emit('back')
|
||||
password.value = ''
|
||||
passwordError.value = ''
|
||||
emit('back')
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<CardBase>
|
||||
<div class="auth-card-content">
|
||||
<div class="form-group">
|
||||
<FormCommon
|
||||
@ -99,6 +100,7 @@ const handleBack = () => {
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
</CardBase>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
|
||||
@ -3,6 +3,8 @@ import ButtonCommon from './common/ButtonCommon.vue'
|
||||
import FormCommon from './common/FormCommon.vue'
|
||||
import PasswordForm from './common/PasswordForm.vue'
|
||||
import SpinnerCommon from './common/SpinnerCommon.vue'
|
||||
import CardBase from './common/CardBase.vue'
|
||||
import CardBaseScrollable from './common/CardBaseScrollable.vue'
|
||||
import { IconCommon } from './icon'
|
||||
|
||||
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, IconCommon }
|
||||
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, CardBase, CardBaseScrollable, IconCommon }
|
||||
|
||||
@ -26,20 +26,14 @@ export function useNeptuneWallet() {
|
||||
|
||||
initPromise = (async () => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
await initWasm()
|
||||
|
||||
wasmInitialized = true
|
||||
} catch (err) {
|
||||
wasmInitialized = false
|
||||
const errorMsg = 'Failed to initialize Neptune WASM'
|
||||
store.setError(errorMsg)
|
||||
console.error('WASM init error:', err)
|
||||
throw new Error(errorMsg)
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
})()
|
||||
|
||||
@ -48,9 +42,6 @@ export function useNeptuneWallet() {
|
||||
|
||||
const generateWallet = async (): Promise<GenerateSeedResult> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
await ensureWasmInitialized()
|
||||
|
||||
const resultJson = generate_seed()
|
||||
@ -65,30 +56,27 @@ export function useNeptuneWallet() {
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to generate wallet'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error generating wallet:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||
try {
|
||||
await ensureWasmInitialized()
|
||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||
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 {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const isValid = await validateSeedPhrase(seedPhrase)
|
||||
if (!isValid) {
|
||||
throw new Error('Invalid seed phrase')
|
||||
}
|
||||
const isValid = validate_seed_phrase(JSON.stringify(seedPhrase))
|
||||
if (!isValid) throw new Error('Invalid seed phrase')
|
||||
|
||||
const result = await getViewKeyFromSeed(seedPhrase)
|
||||
|
||||
@ -99,51 +87,84 @@ export function useNeptuneWallet() {
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to import wallet'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error recovering wallet from seed:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
await ensureWasmInitialized()
|
||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||
return validate_seed_phrase(seedPhraseJson)
|
||||
return address_from_seed(seedPhraseJson, store.getNetwork)
|
||||
} catch (err) {
|
||||
console.error('Validation error:', err)
|
||||
return false
|
||||
console.error('Error getting address from seed:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
const decryptKeystore = async (password: string): Promise<void> => {
|
||||
try {
|
||||
const keystorePath = store.getKeystorePath
|
||||
if (!keystorePath) await checkKeystore()
|
||||
|
||||
const result = await (window as any).walletApi.decryptKeystore(
|
||||
store.getKeystorePath || '',
|
||||
store.getKeystorePath,
|
||||
password
|
||||
)
|
||||
if (result.error) {
|
||||
console.error('Error decrypting keystore composable:', result.error)
|
||||
return
|
||||
}
|
||||
|
||||
const seedPhrase = result.phrase.trim().split(/\s+/)
|
||||
const viewKeyResult = await getViewKeyFromSeed(seedPhrase)
|
||||
|
||||
store.setPassword(password)
|
||||
store.setSeedPhrase(seedPhrase)
|
||||
store.setAddress(viewKeyResult.address)
|
||||
store.setViewKey(viewKeyResult.view_key)
|
||||
store.setReceiverId(viewKeyResult.receiver_identifier)
|
||||
} 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.')
|
||||
}
|
||||
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const response = await API.getUtxosFromViewKey(
|
||||
store.getViewKey,
|
||||
startBlock,
|
||||
@ -169,68 +187,49 @@ export function useNeptuneWallet() {
|
||||
maxSearchDepth
|
||||
)
|
||||
|
||||
const result = response.data?.result || response.data
|
||||
const result = response?.result || response
|
||||
store.setUtxos(result.utxos || result || [])
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to get UTXOs'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error getting UTXOs:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getBalance = async (): Promise<any> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const response = await API.getBalance()
|
||||
const result = response.data?.result || response.data
|
||||
const result = response?.result || response
|
||||
store.setBalance(result.balance || result)
|
||||
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to get balance'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error getting balance:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getBlockHeight = async (): Promise<any> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const response = await API.getBlockHeight()
|
||||
const result = response.data?.result || response.data
|
||||
return result.height || result
|
||||
const result = response?.result || response
|
||||
return result?.height || result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to get block height'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error getting block height:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const getNetworkInfo = async (): Promise<any> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
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
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to get network info'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error getting network info:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
@ -240,22 +239,17 @@ export function useNeptuneWallet() {
|
||||
fee: string
|
||||
): Promise<any> => {
|
||||
try {
|
||||
store.setLoading(true)
|
||||
store.setError(null)
|
||||
|
||||
const response = await API.sendTransaction(toAddress, amount, fee)
|
||||
const result = response.data?.result || response.data
|
||||
const result = response?.result || response
|
||||
return result
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to send transaction'
|
||||
store.setError(errorMsg)
|
||||
console.error('Error sending transaction:', err)
|
||||
throw err
|
||||
} finally {
|
||||
store.setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const setNetwork = async (network: 'mainnet' | 'testnet') => {
|
||||
try {
|
||||
store.setNetwork(network)
|
||||
|
||||
if (store.getSeedPhrase) {
|
||||
@ -263,6 +257,10 @@ export function useNeptuneWallet() {
|
||||
store.setAddress(viewKeyResult.address)
|
||||
store.setViewKey(viewKeyResult.view_key)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Error setting network:', err)
|
||||
throw err
|
||||
}
|
||||
}
|
||||
|
||||
// ===== UTILITY METHODS =====
|
||||
@ -273,10 +271,9 @@ export function useNeptuneWallet() {
|
||||
return {
|
||||
initWasm: ensureWasmInitialized,
|
||||
generateWallet,
|
||||
importWallet,
|
||||
recoverWalletFromSeed,
|
||||
getViewKeyFromSeed,
|
||||
getAddressFromSeed,
|
||||
validateSeedPhrase,
|
||||
|
||||
getUtxos,
|
||||
getBalance,
|
||||
@ -284,6 +281,9 @@ export function useNeptuneWallet() {
|
||||
getNetworkInfo,
|
||||
sendTransaction,
|
||||
decryptKeystore,
|
||||
createKeystore,
|
||||
saveKeystoreAs,
|
||||
checkKeystore,
|
||||
|
||||
clearWallet,
|
||||
setNetwork,
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import * as Page from '@/views'
|
||||
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 neptuneStore = useNeptuneStore()
|
||||
|
||||
@ -12,16 +14,12 @@ const ifAuthenticated = (to: any, from: any, next: any) => {
|
||||
next('/auth')
|
||||
}
|
||||
|
||||
|
||||
const ifNotAuthenticated = async (to: any, from: any, next: any) => {
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const neptuneWallet = useNeptuneWallet()
|
||||
const authStore = useAuthStore()
|
||||
|
||||
const keystoreFile = await (window as any).walletApi.checkKeystore()
|
||||
if (keystoreFile.exists) {
|
||||
neptuneStore.setKeystorePath(keystoreFile.filePath)
|
||||
authStore.setState('login')
|
||||
}
|
||||
const exists = await neptuneWallet.checkKeystore()
|
||||
if (exists) authStore.goToLogin()
|
||||
next()
|
||||
}
|
||||
|
||||
|
||||
@ -5,11 +5,14 @@ export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confir
|
||||
|
||||
// Auth store to manage the flow
|
||||
const currentState = ref<AuthState>('onboarding')
|
||||
const previousState = ref<AuthState | null>(null)
|
||||
|
||||
export const useAuthStore = () => {
|
||||
const getCurrentState = () => currentState.value
|
||||
const getPreviousState = () => previousState.value
|
||||
|
||||
const setState = (state: AuthState) => {
|
||||
previousState.value = currentState.value
|
||||
currentState.value = state
|
||||
}
|
||||
|
||||
@ -25,17 +28,18 @@ export const useAuthStore = () => {
|
||||
setState('recovery')
|
||||
}
|
||||
|
||||
const resetFlow = () => {
|
||||
setState('onboarding')
|
||||
const goBack = () => {
|
||||
previousState.value ? setState(previousState.value) : setState('onboarding')
|
||||
}
|
||||
|
||||
return {
|
||||
currentState: currentState.value,
|
||||
getCurrentState,
|
||||
getPreviousState,
|
||||
setState,
|
||||
goToCreate,
|
||||
goToLogin,
|
||||
goToRecover,
|
||||
resetFlow,
|
||||
goBack,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,3 +1,2 @@
|
||||
export * from './seedStore'
|
||||
export * from './authStore'
|
||||
export * from './neptuneStore'
|
||||
|
||||
@ -19,9 +19,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
|
||||
const keystorePath = ref<null | string>(null)
|
||||
|
||||
const isLoading = ref(false)
|
||||
const error = ref<string | null>(null)
|
||||
|
||||
// ===== SETTERS =====
|
||||
|
||||
const setSeedPhrase = (seedPhrase: string[] | null) => {
|
||||
@ -56,14 +53,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
wallet.value.utxos = utxos
|
||||
}
|
||||
|
||||
const setLoading = (loading: boolean) => {
|
||||
isLoading.value = loading
|
||||
}
|
||||
|
||||
const setError = (err: string | null) => {
|
||||
error.value = err
|
||||
}
|
||||
|
||||
const setWallet = (walletData: Partial<WalletState>) => {
|
||||
wallet.value = { ...wallet.value, ...walletData }
|
||||
}
|
||||
@ -83,7 +72,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
balance: null,
|
||||
utxos: [],
|
||||
}
|
||||
error.value = null
|
||||
}
|
||||
|
||||
// ===== GETTERS =====
|
||||
@ -98,8 +86,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
const getBalance = computed(() => wallet.value.balance)
|
||||
const getUtxos = computed(() => wallet.value.utxos)
|
||||
const hasWallet = computed(() => wallet.value.address !== null)
|
||||
const getLoading = computed(() => isLoading.value)
|
||||
const getError = computed(() => error.value)
|
||||
const getKeystorePath = computed(() => keystorePath.value)
|
||||
return {
|
||||
getWallet,
|
||||
@ -113,8 +99,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
getBalance,
|
||||
getUtxos,
|
||||
hasWallet,
|
||||
getLoading,
|
||||
getError,
|
||||
getKeystorePath,
|
||||
setSeedPhrase,
|
||||
setPassword,
|
||||
@ -124,8 +108,6 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
||||
setNetwork,
|
||||
setBalance,
|
||||
setUtxos,
|
||||
setLoading,
|
||||
setError,
|
||||
setWallet,
|
||||
setKeystorePath,
|
||||
clearWallet,
|
||||
|
||||
@ -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,
|
||||
}
|
||||
}
|
||||
@ -1,12 +1,2 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const PAGE_FIRST = 1
|
||||
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')
|
||||
})
|
||||
|
||||
@ -1,9 +1,3 @@
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
export const formatNumberToLocaleString = (num: number): string => {
|
||||
return num.toLocaleString('en-US')
|
||||
}
|
||||
|
||||
export const formatDate = (day: any, format = 'YYYY-MM-DD') => {
|
||||
return dayjs(new Date(day)).format(format)
|
||||
}
|
||||
|
||||
@ -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)
|
||||
@ -1,5 +1,4 @@
|
||||
export * from './constants/code'
|
||||
export * from './constants/constants'
|
||||
export * from './helpers/format'
|
||||
export * from './helpers/localStorage'
|
||||
export * from './helpers/seedPhrase'
|
||||
|
||||
@ -34,7 +34,6 @@ const handleGoToRecover = () => {
|
||||
.auth-container {
|
||||
min-height: 100vh;
|
||||
background: var(--bg-light);
|
||||
font-family: var(--font-primary);
|
||||
}
|
||||
|
||||
.complete-state {
|
||||
|
||||
@ -4,6 +4,7 @@ import { PasswordForm } from '@/components'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
@ -15,20 +16,19 @@ const error = ref(false)
|
||||
const handlePasswordSubmit = async (password: string) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
error.value = false
|
||||
await neptuneWallet.decryptKeystore(password)
|
||||
router.push({name: 'home'})
|
||||
router.push({ name: 'home' })
|
||||
} catch (err) {
|
||||
error.value = true
|
||||
console.error('Password Error')
|
||||
}
|
||||
finally {
|
||||
} finally {
|
||||
isLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const handleNewWallet = () => {
|
||||
authStore.goToCreate()
|
||||
router.push({name: 'auth'})
|
||||
router.push({ name: 'auth' })
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@ -1,20 +1,23 @@
|
||||
<script setup lang="ts">
|
||||
import { RecoverWalletComponent } from '.'
|
||||
import { useAuthStore } from '@/stores'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const authStore = useAuthStore()
|
||||
const router = useRouter()
|
||||
|
||||
const handleImported = (payload: { type: 'seed' | 'keystore'; value: string | string[] }) => {
|
||||
if (payload.type === 'keystore') {
|
||||
localStorage.setItem('temp_keystore', JSON.stringify(payload.value))
|
||||
return router.push({ name: 'password' })
|
||||
}
|
||||
const handleCancel = () => {
|
||||
authStore.goBack()
|
||||
}
|
||||
|
||||
const handleAccessWallet = () => {
|
||||
router.push({ name: 'home' })
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="recover-seed-tab">
|
||||
<RecoverWalletComponent @import-success="handleImported" />
|
||||
<RecoverWalletComponent @cancel="handleCancel" @access-wallet="handleAccessWallet" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@ -4,6 +4,20 @@ import { ButtonCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
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<{
|
||||
next: []
|
||||
back: []
|
||||
@ -50,10 +64,6 @@ const handleCopySeed = async () => {
|
||||
|
||||
<div class="recovery-content">
|
||||
<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>
|
||||
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.
|
||||
@ -91,24 +101,23 @@ const handleCopySeed = async () => {
|
||||
<p>⚠️ No seed phrase found. Please go back and generate a wallet first.</p>
|
||||
</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">
|
||||
<ButtonCommon type="default" size="large" @click="handleBack">
|
||||
BACK
|
||||
<ButtonCommon
|
||||
v-if="props.backButton"
|
||||
type="default"
|
||||
size="large"
|
||||
@click="handleBack"
|
||||
>
|
||||
{{ backButtonText }}
|
||||
</ButtonCommon>
|
||||
<ButtonCommon
|
||||
v-if="props.nextButton"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleNext"
|
||||
:disabled="!seedWords || seedWords.length === 0"
|
||||
>
|
||||
NEXT
|
||||
{{ nextButtonText }}
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
@ -121,16 +130,15 @@ const handleCopySeed = async () => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
background: var(--bg-light);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.recovery-card {
|
||||
@include card-base;
|
||||
max-width: 500px;
|
||||
padding: var(--spacing-xl);
|
||||
width: 100%;
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.recovery-header {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, defineEmits, onMounted, computed } from 'vue'
|
||||
import { ButtonCommon } from '@/components'
|
||||
import { ButtonCommon, CardBase } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
@ -82,7 +82,6 @@ const handleAnswerSelect = (answer: string) => {
|
||||
}
|
||||
|
||||
const handleNext = () => {
|
||||
emit('next')
|
||||
if (isCorrect.value) {
|
||||
correctCount.value++
|
||||
askedPositions.value.add(quizData.value!.position)
|
||||
@ -208,16 +207,13 @@ onMounted(() => {
|
||||
>
|
||||
NEXT QUESTION
|
||||
</ButtonCommon>
|
||||
<!-- <ButtonCommon
|
||||
<ButtonCommon
|
||||
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleNext"
|
||||
>
|
||||
CONTINUE
|
||||
</ButtonCommon> -->
|
||||
<ButtonCommon type="primary" size="large" @click="handleNext">
|
||||
CONTINUE
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
</div>
|
||||
@ -230,16 +226,15 @@ onMounted(() => {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: var(--spacing-xl);
|
||||
background: var(--bg-light);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.confirm-card {
|
||||
@include card-base;
|
||||
max-width: 500px;
|
||||
width: 100%;
|
||||
padding: var(--spacing-xl);
|
||||
border: 2px solid var(--primary-color);
|
||||
border-radius: var(--radius-md);
|
||||
}
|
||||
|
||||
.confirm-header {
|
||||
|
||||
@ -163,6 +163,7 @@ const handleIHaveWallet = () => {
|
||||
required
|
||||
:error="confirmPasswordError"
|
||||
@input="confirmPasswordError = ''"
|
||||
@keyup.enter="handleNext"
|
||||
/>
|
||||
<div
|
||||
v-if="confirmPassword"
|
||||
|
||||
@ -4,6 +4,7 @@ import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
|
||||
import { CreatePasswordStep, WalletCreatedStep } from '.'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { CardBaseScrollable } from '@/components'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const emit = defineEmits<{
|
||||
@ -36,9 +37,20 @@ const handleNextToWalletCreated = () => {
|
||||
}
|
||||
|
||||
const handleNextFromPassword = async () => {
|
||||
const result = await generateWallet()
|
||||
if (result) step.value = 2
|
||||
else message.error('Failed to generate wallet')
|
||||
try {
|
||||
await generateWallet()
|
||||
step.value = 2
|
||||
} catch (err) {
|
||||
message.error('Failed to generate wallet')
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackToCreatePassword = () => {
|
||||
step.value = 1
|
||||
}
|
||||
|
||||
const handleBackToSeedPhrase = () => {
|
||||
step.value = 2
|
||||
}
|
||||
|
||||
const handleAccessWallet = () => {
|
||||
@ -52,6 +64,7 @@ function resetAll() {
|
||||
</script>
|
||||
<template>
|
||||
<div class="auth-container">
|
||||
<CardBaseScrollable>
|
||||
<div class="auth-card">
|
||||
<!-- Step 1: Create Password -->
|
||||
<CreatePasswordStep
|
||||
@ -61,10 +74,18 @@ function resetAll() {
|
||||
/>
|
||||
|
||||
<!-- 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 -->
|
||||
<ConfirmSeedComponent v-else-if="step === 3" @next="handleNextToWalletCreated" />
|
||||
<ConfirmSeedComponent
|
||||
v-else-if="step === 3"
|
||||
@back="handleBackToSeedPhrase"
|
||||
@next="handleNextToWalletCreated"
|
||||
/>
|
||||
|
||||
<!-- Step 4: Success -->
|
||||
<WalletCreatedStep
|
||||
@ -72,12 +93,8 @@ function resetAll() {
|
||||
@access-wallet="handleAccessWallet"
|
||||
@create-another="resetAll"
|
||||
/>
|
||||
|
||||
<!-- Fallback slot -->
|
||||
<template v-else>
|
||||
<slot></slot>
|
||||
</template>
|
||||
</div>
|
||||
</CardBaseScrollable>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -91,7 +108,6 @@ function resetAll() {
|
||||
}
|
||||
|
||||
.auth-card {
|
||||
@include card-base;
|
||||
max-width: 720px;
|
||||
width: 100%;
|
||||
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
<script setup lang="ts">
|
||||
import { ButtonCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const { createKeystore } = useNeptuneWallet()
|
||||
|
||||
const emit = defineEmits<{
|
||||
accessWallet: []
|
||||
@ -10,10 +13,20 @@ const emit = defineEmits<{
|
||||
}>()
|
||||
|
||||
const handleAccessWallet = async () => {
|
||||
try {
|
||||
const seedPhrase = neptuneStore.getSeedPhraseString
|
||||
const password = neptuneStore.getPassword!
|
||||
const encrypted = (window as any).walletApi.createKeystore(seedPhrase, password)
|
||||
|
||||
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 = () => {
|
||||
|
||||
@ -1,34 +1,37 @@
|
||||
<script setup lang="ts">
|
||||
import { validateSeedPhrase18 } from '@/utils'
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { ref } from 'vue'
|
||||
|
||||
interface Props {
|
||||
valid?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
valid: true,
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
(e: 'update:valid', valid: boolean): 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 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) => {
|
||||
if (idx < 18) {
|
||||
document.getElementById('input-' + idx)?.focus()
|
||||
}
|
||||
}
|
||||
|
||||
const handleGridInput = (index: number, value: string) => {
|
||||
emit('update:valid', true)
|
||||
seedWords.value[index] = value
|
||||
emit('update:words', seedWords.value.filter((w) => w.trim()))
|
||||
}
|
||||
|
||||
const handlePaste = (event: ClipboardEvent) => {
|
||||
event.preventDefault()
|
||||
emit('update:valid', true)
|
||||
const pastedData = event.clipboardData?.getData('text') || ''
|
||||
const words = pastedData
|
||||
.trim()
|
||||
@ -37,13 +40,13 @@ const handlePaste = (event: ClipboardEvent) => {
|
||||
|
||||
if (words.length === 0) return
|
||||
|
||||
seedWords.value = words
|
||||
seedError.value = ''
|
||||
const filledWords = Array.from({ length: 18 }, (_, i) => words[i] || '')
|
||||
seedWords.value = filledWords
|
||||
emit('update:words', words.filter((w) => w.trim()))
|
||||
}
|
||||
|
||||
const handleSubmit = () => {
|
||||
const words = seedWords.value.filter((w) => w.trim())
|
||||
if (validateSeedPhrase18(words)) seedError.value = ''
|
||||
emit('submit', words)
|
||||
}
|
||||
|
||||
@ -73,15 +76,13 @@ defineExpose({
|
||||
:placeholder="i + 1 + '.'"
|
||||
maxlength="24"
|
||||
@keydown.enter="inputBoxFocus(i + 1)"
|
||||
:class="{ error: seedError && !word.trim() }"
|
||||
@focus="seedError = ''"
|
||||
@input="handleGridInput(i, ($event.target as HTMLInputElement).value)"
|
||||
@paste="handlePaste($event)"
|
||||
/>
|
||||
</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>
|
||||
</template>
|
||||
|
||||
|
||||
@ -1,88 +1,109 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Modal } from 'ant-design-vue'
|
||||
import { ButtonCommon, PasswordForm } from '@/components'
|
||||
import { RecoverSeedComponent } from '..'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { message } from 'ant-design-vue'
|
||||
|
||||
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 isSeedPhraseValid = ref(false)
|
||||
const showPasswordModal = ref(false)
|
||||
const showPasswordForm = ref(false)
|
||||
const seedPhrase = ref<string[]>([])
|
||||
const isLoading = ref(false)
|
||||
const passwordError = ref(false)
|
||||
const isSeedPhraseValid = ref(true)
|
||||
|
||||
const handleSeedPhraseSubmit = (words: string[]) => {
|
||||
const handleSeedWordsUpdate = (words: string[]) => {
|
||||
seedPhrase.value = words
|
||||
showPasswordModal.value = true
|
||||
}
|
||||
|
||||
const handleSeedPhraseSubmit = async (words: string[]) => {
|
||||
seedPhrase.value = words
|
||||
showPasswordForm.value = true
|
||||
}
|
||||
|
||||
const handleContinue = () => {
|
||||
recoverSeedComponentRef.value?.handleSubmit?.()
|
||||
}
|
||||
|
||||
const handlePasswordSubmit = (password: string) => {
|
||||
showPasswordModal.value = false
|
||||
emit('import-success', {
|
||||
type: 'seed',
|
||||
value: seedPhrase.value,
|
||||
password,
|
||||
})
|
||||
const handlePasswordSubmit = async (password: string) => {
|
||||
try {
|
||||
isLoading.value = true
|
||||
const result = await recoverWalletFromSeed(seedPhrase.value)
|
||||
if (result.address) {
|
||||
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 = () => {
|
||||
showPasswordModal.value = false
|
||||
passwordError.value = false
|
||||
showPasswordForm.value = false
|
||||
}
|
||||
|
||||
const handleModalCancel = () => {
|
||||
showPasswordModal.value = false
|
||||
passwordError.value = false
|
||||
const handleCancel = () => {
|
||||
emit('cancel')
|
||||
showPasswordForm.value = false
|
||||
seedPhrase.value = []
|
||||
isSeedPhraseValid.value = true
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<div class="import-wallet dark-card">
|
||||
<h2 class="title">Recover Wallet</h2>
|
||||
|
||||
<!-- Seed Phrase Step -->
|
||||
<div v-if="!showPasswordForm">
|
||||
<div class="desc">Enter your recovery seed phrase</div>
|
||||
<RecoverSeedComponent
|
||||
ref="recoverSeedComponentRef"
|
||||
@update:valid="isSeedPhraseValid = $event"
|
||||
:valid="isSeedPhraseValid"
|
||||
@submit="handleSeedPhraseSubmit"
|
||||
@update:words="handleSeedWordsUpdate"
|
||||
@update:valid="isSeedPhraseValid = $event"
|
||||
/>
|
||||
<ButtonCommon
|
||||
class="mt-lg"
|
||||
class="mt-20"
|
||||
type="primary"
|
||||
block
|
||||
size="large"
|
||||
:disabled="!isSeedPhraseValid"
|
||||
:disabled="isLoading"
|
||||
@click="handleContinue"
|
||||
>Continue</ButtonCommon
|
||||
>
|
||||
Continue
|
||||
</ButtonCommon>
|
||||
<ButtonCommon class="mt-10" type="default" block size="large" @click="handleCancel">
|
||||
Cancel
|
||||
</ButtonCommon>
|
||||
</div>
|
||||
|
||||
<Modal
|
||||
v-model:open="showPasswordModal"
|
||||
title="Enter Password"
|
||||
:footer="null"
|
||||
:width="480"
|
||||
:mask-closable="false"
|
||||
:keyboard="false"
|
||||
@cancel="handleModalCancel"
|
||||
>
|
||||
<!-- Password Step -->
|
||||
<div v-else>
|
||||
<div class="desc">Enter password to encrypt seed phrase</div>
|
||||
<PasswordForm
|
||||
button-text="Continue"
|
||||
button-text="Submit"
|
||||
placeholder="Enter password to encrypt seed phrase"
|
||||
label="Password"
|
||||
:loading="isLoading"
|
||||
:error="passwordError"
|
||||
:error-message="'Invalid password'"
|
||||
@submit="handlePasswordSubmit"
|
||||
@back="handlePasswordBack"
|
||||
/>
|
||||
</Modal>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<style lang="scss" scoped>
|
||||
.import-wallet {
|
||||
@ -107,9 +128,6 @@ const handleModalCancel = () => {
|
||||
font-size: 1rem;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.mt-lg {
|
||||
margin-top: 32px;
|
||||
}
|
||||
}
|
||||
@include screen(mobile) {
|
||||
.import-wallet {
|
||||
|
||||
@ -1,23 +1,21 @@
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import { Tabs, Row, Col } from 'ant-design-vue'
|
||||
import WalletInfo from '@/components/WalletInfo.vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import { Tabs } from 'ant-design-vue'
|
||||
import { WalletTab, NetworkTab, UTXOTab } from './components'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
|
||||
const neptuneWallet = useNeptuneWallet()
|
||||
|
||||
const activeTab = ref('UTXOs')
|
||||
const network = ref('neptune-mainnet')
|
||||
|
||||
onMounted(async () => {
|
||||
await neptuneWallet.getNetworkInfo()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="home-container">
|
||||
<Row :gutter="[24, 24]">
|
||||
<!-- Left Column --->
|
||||
<Col :xs="24" :lg="10">
|
||||
<WalletInfo />
|
||||
</Col>
|
||||
|
||||
<!-- Right Column - Tabs Content -->
|
||||
<Col :xs="24" :lg="12">
|
||||
<Tabs v-model:activeKey="activeTab" size="large" class="main-tabs">
|
||||
<!-- DEBUG TAB -->
|
||||
<Tabs.TabPane key="UTXOs" tab="UTXOs">
|
||||
@ -34,30 +32,33 @@ const network = ref('neptune-mainnet')
|
||||
<NetworkTab />
|
||||
</Tabs.TabPane>
|
||||
</Tabs>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.home-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, var(--bg-gradient-start) 0%, var(--bg-gradient-end) 100%);
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: var(--spacing-lg);
|
||||
font-family: var(--font-primary);
|
||||
|
||||
@include screen(tablet) {
|
||||
padding: var(--spacing-2xl);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
|
||||
@include screen(desktop) {
|
||||
padding: var(--spacing-2xl);
|
||||
padding: var(--spacing-xl);
|
||||
}
|
||||
}
|
||||
|
||||
:deep(.main-tabs) {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
.ant-tabs-nav {
|
||||
margin-bottom: var(--spacing-lg);
|
||||
margin-bottom: var(--spacing-md);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.ant-tabs-tab {
|
||||
@ -82,7 +83,15 @@ const network = ref('neptune-mainnet')
|
||||
}
|
||||
|
||||
.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>
|
||||
|
||||
@ -1,46 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { formatNumberToLocaleString } from '@/utils'
|
||||
import { SpinnerCommon } from '@/components'
|
||||
import { CardBase, SpinnerCommon } from '@/components'
|
||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { message } from 'ant-design-vue'
|
||||
import { get_network_info } from '@neptune/wasm'
|
||||
|
||||
const neptuneStore = useNeptuneStore()
|
||||
const { getBlockHeight, getNetworkInfo } = useNeptuneWallet()
|
||||
const { getBlockHeight } = useNeptuneWallet()
|
||||
|
||||
const blockHeight = ref(0)
|
||||
const networkInfo = ref<any>(null)
|
||||
const loading = ref(true)
|
||||
const error = ref('')
|
||||
const lastUpdate = ref<Date | null>(null)
|
||||
|
||||
// let pollingInterval: number | null = null
|
||||
let pollingInterval: number | null = null
|
||||
|
||||
const network = computed(() => neptuneStore.getNetwork)
|
||||
|
||||
const loadNetworkData = async () => {
|
||||
try {
|
||||
loading.value = true
|
||||
error.value = ''
|
||||
const [heightResult, infoResult] = await Promise.all([getBlockHeight(), getNetworkInfo()])
|
||||
|
||||
if (heightResult !== undefined) {
|
||||
blockHeight.value =
|
||||
typeof heightResult === 'number'
|
||||
? heightResult
|
||||
: heightResult.height || heightResult || 0
|
||||
}
|
||||
|
||||
if (infoResult) {
|
||||
networkInfo.value = infoResult
|
||||
}
|
||||
const result = await getBlockHeight()
|
||||
if (result.height || result) blockHeight.value = Number(result.height || result)
|
||||
|
||||
lastUpdate.value = new Date()
|
||||
|
||||
if (loading.value) {
|
||||
loading.value = false
|
||||
}
|
||||
} catch (err) {
|
||||
const errorMsg = err instanceof Error ? err.message : 'Failed to load network data'
|
||||
error.value = errorMsg
|
||||
@ -55,33 +41,31 @@ const retryConnection = async () => {
|
||||
await loadNetworkData()
|
||||
}
|
||||
|
||||
// const startPolling = () => {
|
||||
// pollingInterval = window.setInterval(() => {
|
||||
// if (!loading.value) {
|
||||
// loadNetworkData()
|
||||
// }
|
||||
// }, 10000)
|
||||
// }
|
||||
const startPolling = () => {
|
||||
pollingInterval = window.setInterval(async () => {
|
||||
if (!loading.value) await loadNetworkData()
|
||||
}, 10000)
|
||||
}
|
||||
|
||||
// const stopPolling = () => {
|
||||
// if (pollingInterval) {
|
||||
// clearInterval(pollingInterval)
|
||||
// pollingInterval = null
|
||||
// }
|
||||
// }
|
||||
const stopPolling = () => {
|
||||
if (pollingInterval) {
|
||||
clearInterval(pollingInterval)
|
||||
pollingInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
await loadNetworkData()
|
||||
// startPolling()
|
||||
startPolling()
|
||||
})
|
||||
|
||||
// onUnmounted(() => {
|
||||
// stopPolling()
|
||||
// })
|
||||
onUnmounted(() => {
|
||||
stopPolling()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content-card">
|
||||
<CardBase class="content-card">
|
||||
<div class="network-status-container">
|
||||
<h2 class="section-title">NETWORK STATUS</h2>
|
||||
|
||||
@ -105,39 +89,20 @@ onMounted(async () => {
|
||||
</div>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
<span class="status-label">Last Updated</span>
|
||||
<span class="status-value">{{ lastUpdate.toLocaleTimeString() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</CardBase>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-card {
|
||||
@include card-base;
|
||||
}
|
||||
|
||||
.network-status-container {
|
||||
.section-title {
|
||||
font-size: var(--font-xl);
|
||||
@ -181,7 +146,6 @@ onMounted(async () => {
|
||||
color: var(--text-primary);
|
||||
font-size: var(--font-lg);
|
||||
text-align: right;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@
|
||||
import { ref } from 'vue'
|
||||
import { EditOutlined } from '@ant-design/icons-vue'
|
||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||
import { CardBaseScrollable } from '@/components'
|
||||
|
||||
const { getUtxos } = useNeptuneWallet()
|
||||
|
||||
@ -10,7 +11,7 @@ const inUseUtxosAmount = ref(0)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="content-card debug-card">
|
||||
<CardBaseScrollable class="content-card debug-card">
|
||||
<div class="debug-header">
|
||||
<h3 class="debug-title">
|
||||
IN USE UTXOS
|
||||
@ -18,19 +19,15 @@ const inUseUtxosAmount = ref(0)
|
||||
</h3>
|
||||
<div class="debug-info">
|
||||
<p><strong>COUNT</strong> {{ inUseUtxosCount }}</p>
|
||||
<p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} KAS</p>
|
||||
<p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} NPT</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="list-pagination"></div>
|
||||
</div>
|
||||
</CardBaseScrollable>
|
||||
</template>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.content-card {
|
||||
@include card-base;
|
||||
}
|
||||
|
||||
.debug-card {
|
||||
.debug-header {
|
||||
text-align: center;
|
||||
|
||||
@ -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>
|
||||
@ -1,3 +1,3 @@
|
||||
export { default as WalletTab } from './WalletTab.vue'
|
||||
export { default as NetworkTab } from './NetworkTab.vue'
|
||||
export { default as UTXOTab } from './UTXOTab.vue'
|
||||
export { WalletInfo, WalletBalanceAndAddress, WalletTab } from './wallet-tab'
|
||||
|
||||
239
src/views/Home/components/wallet-tab/WalletBalanceAndAddress.vue
Normal file
239
src/views/Home/components/wallet-tab/WalletBalanceAndAddress.vue
Normal 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>
|
||||
|
||||
229
src/views/Home/components/wallet-tab/WalletInfo.vue
Normal file
229
src/views/Home/components/wallet-tab/WalletInfo.vue
Normal 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>
|
||||
15
src/views/Home/components/wallet-tab/WalletTab.vue
Normal file
15
src/views/Home/components/wallet-tab/WalletTab.vue
Normal 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>
|
||||
3
src/views/Home/components/wallet-tab/index.ts
Normal file
3
src/views/Home/components/wallet-tab/index.ts
Normal 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'
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"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__/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user