feat 051125/recover_wallet

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

2
.gitignore vendored
View File

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

View File

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

View File

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

View File

@ -1,54 +1,77 @@
import { ipcMain } from 'electron'
import { 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) }

View File

@ -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'),

View File

@ -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
View File

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

View File

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

116
package-lock.json generated
View File

@ -14,9 +14,8 @@
"@neptune/wasm": "file:./packages/neptune-wasm",
"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",

View File

@ -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",

View File

@ -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
}

View File

@ -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> => {

View File

@ -1,8 +1,17 @@
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+JP:wght@100;200;300;400;500;600;700;800;900&display=swap');
html {
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);
}
}

View File

@ -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;

View File

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

View File

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

View File

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

View File

@ -1,12 +1,32 @@
<script lang="ts" setup></script>
<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>

View File

@ -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>

View File

@ -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 }

View File

@ -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,

View File

@ -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()
}

View File

@ -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,
}
}

View File

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

View File

@ -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,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,13 +16,12 @@ const error = ref(false)
const handlePasswordSubmit = async (password: string) => {
try {
isLoading.value = true
error.value = false
await neptuneWallet.decryptKeystore(password)
router.push({ name: 'home' })
} catch (err) {
error.value = true
console.error('Password Error')
}
finally {
} finally {
isLoading.value = false
}
}

View File

@ -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>

View File

@ -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 {

View File

@ -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 {

View File

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

View File

@ -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%;

View File

@ -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 = () => {

View File

@ -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>

View File

@ -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 {

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,6 +1,6 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"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,