feat: 251125/create_wallet_flow

This commit is contained in:
NguyenAnhQuan 2025-11-27 00:14:54 +07:00
parent 4da3a8ff7d
commit df5158b318
66 changed files with 2984 additions and 910 deletions

View File

@ -15,10 +15,13 @@
"tauri:build": "tauri build"
},
"dependencies": {
"@neptune/native": "file:./packages/neptune-native",
"@neptune/wasm": "file:./packages/neptune-wasm",
"@tailwindcss/vite": "^4.1.17",
"@tanstack/vue-form": "^1.26.0",
"@tauri-apps/api": "^2.9.0",
"@vueuse/core": "^14.0.0",
"axios": "^1.7.9",
"axios": "1.13.2",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"lucide-vue-next": "^0.554.0",

79
packages/neptune-native/index.d.ts vendored Normal file
View File

@ -0,0 +1,79 @@
/* tslint:disable */
/* eslint-disable */
/* auto-generated by NAPI-RS */
export declare function initNativeModule(): string
export declare function quickVmTest(): string
export declare function getVersion(): string
export declare class WalletManager {
constructor()
/**
* Generate spending key from BIP39 seed phrase
*
* # Arguments
* * `seed_phrase` - Array of words (12, 15, 18, 21, or 24 words)
*
* # Returns
* JSON with spending_key_hex, view_key_hex, receiver_identifier
*/
generateKeysFromSeed(seedPhrase: Array<string>): string
/**
* Generate lock_script_and_witness (transaction signature)
*
* # Arguments
* * `spending_key_hex` - Hex-encoded spending key from generate_keys_from_seed
*
* # Returns
* JSON with lock_script_and_witness in hex format
*/
createLockScriptAndWitness(spendingKeyHex: string): string
/** Derive ViewKey hex (0x-prefixed bincode) from a Generation spending key hex */
spendingKeyToViewKeyHex(spendingKeyHex: string): string
/** Call wallet_getAdditionRecordsFromViewKey and return JSON as string */
getAdditionRecordsFromViewKeyCall(rpcUrl: string, viewKeyHex: string, startBlock: number, endBlock: number | undefined | null, maxSearchDepth: number): Promise<string>
/**
* Build RPC request to get UTXOs from view key
*
* # Arguments
* * `view_key_hex` - Hex-encoded view key from generate_keys_from_seed
* * `start_block` - Starting block height (0 for genesis)
* * `end_block` - Ending block height (current tip)
* * `max_search_depth` - Maximum blocks to search (default: 1000)
*
* # Returns
* JSON-RPC request ready to send
*
* # Note
* Method name format: namespace_method (e.g. wallet_getUtxosFromViewKey)
*/
buildGetUtxosRequest(viewKeyHex: string, startBlock: number, endBlock: number, maxSearchDepth?: number | undefined | null): string
/** Build RPC request to test chain height (for connectivity testing) */
buildTestRpcRequest(): string
/** Build JSON-RPC request to fetch current chain height */
buildChainHeightRequest(): string
/** Build JSON-RPC request to fetch current chain header (tip) */
buildChainHeaderRequest(): string
/** Get network information */
getNetworkInfo(): string
getChainHeightCall(rpcUrl: string): Promise<string>
/** Call node_getState to get server state information */
getStateCall(rpcUrl: string): Promise<string>
/** Call mempool_submitTransaction to broadcast a pre-built transaction */
submitTransactionCall(rpcUrl: string, transactionHex: string): Promise<string>
getUtxosFromViewKeyCall(rpcUrl: string, viewKeyHex: string, startBlock: number, maxSearchDepth?: number | undefined | null): Promise<string>
getArchivalMutatorSet(rpcUrl: string): Promise<string>
/**
* Build JSON-RPC request to find the canonical block that created a UTXO (by addition_record)
* Method: archival_getUtxoCreationBlock
*/
buildGetUtxoCreationBlockRequest(additionRecordHex: string, maxSearchDepth?: number | undefined | null): string
/** Perform JSON-RPC call to find the canonical block that created a UTXO (by addition_record) */
getUtxoCreationBlockCall(rpcUrl: string, additionRecordHex: string, maxSearchDepth?: number | undefined | null): Promise<string>
/** Call wallet_sendWithSpendingKey to build and broadcast transaction */
generateUtxoWithProofCall(rpcUrl: string, utxoHex: string, additionRecordHex: string, senderRandomnessHex: string, receiverPreimageHex: string, maxSearchDepth: string): Promise<string>
}
export declare class SimpleTransactionBuilder {
constructor()
buildTransaction(rpcUrl: string, spendingKeyHex: string, inputAdditionRecords: Array<string>, minBlockHeight: number, outputAddresses: Array<string>, outputAmounts: Array<string>, fee: string): Promise<string>
}

View File

@ -0,0 +1,319 @@
/* tslint:disable */
/* eslint-disable */
/* prettier-ignore */
/* auto-generated by NAPI-RS */
const { existsSync, readFileSync } = require('fs')
const { join } = require('path')
const { platform, arch } = process
let nativeBinding = null
let localFileExisted = false
let loadError = null
function isMusl() {
// For Node 10
if (!process.report || typeof process.report.getReport !== 'function') {
try {
const lddPath = require('child_process').execSync('which ldd').toString().trim()
return readFileSync(lddPath, 'utf8').includes('musl')
} catch (e) {
return true
}
} else {
const { glibcVersionRuntime } = process.report.getReport().header
return !glibcVersionRuntime
}
}
switch (platform) {
case 'android':
switch (arch) {
case 'arm64':
localFileExisted = existsSync(join(__dirname, 'neptune-native.android-arm64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.android-arm64.node')
} else {
nativeBinding = require('neptune-native-android-arm64')
}
} catch (e) {
loadError = e
}
break
case 'arm':
localFileExisted = existsSync(join(__dirname, 'neptune-native.android-arm-eabi.node'))
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.android-arm-eabi.node')
} else {
nativeBinding = require('neptune-native-android-arm-eabi')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Android ${arch}`)
}
break
case 'win32':
switch (arch) {
case 'x64':
localFileExisted = existsSync(
join(__dirname, 'neptune-native.win32-x64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.win32-x64-msvc.node')
} else {
nativeBinding = require('neptune-native-win32-x64-msvc')
}
} catch (e) {
loadError = e
}
break
case 'ia32':
localFileExisted = existsSync(
join(__dirname, 'neptune-native.win32-ia32-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.win32-ia32-msvc.node')
} else {
nativeBinding = require('neptune-native-win32-ia32-msvc')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'neptune-native.win32-arm64-msvc.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.win32-arm64-msvc.node')
} else {
nativeBinding = require('neptune-native-win32-arm64-msvc')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Windows: ${arch}`)
}
break
case 'darwin':
localFileExisted = existsSync(join(__dirname, 'neptune-native.darwin-universal.node'))
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.darwin-universal.node')
} else {
nativeBinding = require('neptune-native-darwin-universal')
}
break
} catch {}
switch (arch) {
case 'x64':
localFileExisted = existsSync(join(__dirname, 'neptune-native.darwin-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.darwin-x64.node')
} else {
nativeBinding = require('neptune-native-darwin-x64')
}
} catch (e) {
loadError = e
}
break
case 'arm64':
localFileExisted = existsSync(
join(__dirname, 'neptune-native.darwin-arm64.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.darwin-arm64.node')
} else {
nativeBinding = require('neptune-native-darwin-arm64')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on macOS: ${arch}`)
}
break
case 'freebsd':
if (arch !== 'x64') {
throw new Error(`Unsupported architecture on FreeBSD: ${arch}`)
}
localFileExisted = existsSync(join(__dirname, 'neptune-native.freebsd-x64.node'))
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.freebsd-x64.node')
} else {
nativeBinding = require('neptune-native-freebsd-x64')
}
} catch (e) {
loadError = e
}
break
case 'linux':
switch (arch) {
case 'x64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-x64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-x64-musl.node')
} else {
nativeBinding = require('neptune-native-linux-x64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-x64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-x64-gnu.node')
} else {
nativeBinding = require('neptune-native-linux-x64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-arm64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-arm64-musl.node')
} else {
nativeBinding = require('neptune-native-linux-arm64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-arm64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-arm64-gnu.node')
} else {
nativeBinding = require('neptune-native-linux-arm64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 'arm':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-arm-musleabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-arm-musleabihf.node')
} else {
nativeBinding = require('neptune-native-linux-arm-musleabihf')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-arm-gnueabihf.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-arm-gnueabihf.node')
} else {
nativeBinding = require('neptune-native-linux-arm-gnueabihf')
}
} catch (e) {
loadError = e
}
}
break
case 'riscv64':
if (isMusl()) {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-riscv64-musl.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-riscv64-musl.node')
} else {
nativeBinding = require('neptune-native-linux-riscv64-musl')
}
} catch (e) {
loadError = e
}
} else {
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-riscv64-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-riscv64-gnu.node')
} else {
nativeBinding = require('neptune-native-linux-riscv64-gnu')
}
} catch (e) {
loadError = e
}
}
break
case 's390x':
localFileExisted = existsSync(
join(__dirname, 'neptune-native.linux-s390x-gnu.node')
)
try {
if (localFileExisted) {
nativeBinding = require('./neptune-native.linux-s390x-gnu.node')
} else {
nativeBinding = require('neptune-native-linux-s390x-gnu')
}
} catch (e) {
loadError = e
}
break
default:
throw new Error(`Unsupported architecture on Linux: ${arch}`)
}
break
default:
throw new Error(`Unsupported OS: ${platform}, architecture: ${arch}`)
}
if (!nativeBinding) {
if (loadError) {
throw loadError
}
throw new Error(`Failed to load native binding`)
}
const { initNativeModule, quickVmTest, getVersion, WalletManager, SimpleTransactionBuilder } = nativeBinding
module.exports.initNativeModule = initNativeModule
module.exports.quickVmTest = quickVmTest
module.exports.getVersion = getVersion
module.exports.WalletManager = WalletManager
module.exports.SimpleTransactionBuilder = SimpleTransactionBuilder

View File

@ -0,0 +1,45 @@
{
"name": "@neptune/native",
"version": "0.1.0",
"description": "Native Node.js addon for Neptune transaction building",
"main": "index.js",
"files": [
"index.js",
"index.d.ts",
"*.node"
],
"scripts": {
"build": "npx napi build --platform --release",
"build:debug": "npx napi build --platform",
"prepublishOnly": "napi prepublish -t npm",
"test": "cargo test",
"universal": "napi universal"
},
"napi": {
"name": "neptune-native",
"triples": {
"defaults": true,
"additional": [
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"aarch64-apple-darwin",
"aarch64-unknown-linux-musl"
]
}
},
"devDependencies": {
"@napi-rs/cli": "^2.18.4"
},
"keywords": [
"neptune",
"blockchain",
"native",
"napi",
"vm",
"proof"
],
"license": "Apache-2.0",
"engines": {
"node": ">= 16"
}
}

0
packages/neptune-wasm/.gitignore vendored Normal file
View File

209
packages/neptune-wasm/neptune_wasm.d.ts vendored Normal file
View File

@ -0,0 +1,209 @@
/* tslint:disable */
/* eslint-disable */
/**
* Generate a new random seed phrase (18 words, 192 bits entropy)
* This is completely offline - uses browser's crypto.getRandomValues()
*/
export function generate_seed(): string;
/**
* Get view key and address from seed phrase (offline) - COMPATIBLE WITH NEPTUNE-CORE
*
* This derives the view key from a BIP39 seed phrase using neptune-crypto-core.
* The view key can be used with wallet_getUtxosFromViewKey RPC method.
*
* Input: JSON string with seed_phrase array and network ("mainnet" or "testnet")
* Output: JSON string with receiver_identifier, view_key (hex), and address (bech32m)
*
* Example:
* ```javascript
* const result = get_viewkey('["word1", "word2", ...]', "testnet");
* const { receiver_identifier, view_key, address } = JSON.parse(result);
* console.log('View key:', view_key); // Compatible with neptune-core!
* ```
*/
export function get_viewkey(seed_phrase_json: string, network: string): string;
/**
* Get receiving address from seed phrase (offline)
* Input: JSON string with seed_phrase array and network
* Output: bech32m encoded address string
*/
export function address_from_seed(seed_phrase_json: string, network: string): string;
/**
* Validate a seed phrase (offline)
*/
export function validate_seed_phrase(seed_phrase_json: string): boolean;
/**
* Decode view key from hex string (offline)
*/
export function decode_viewkey(view_key_hex: string): string;
/**
* Prepare transaction data for server-side signing
*
* This prepares transaction details but does NOT sign locally.
* Instead, it exports the spending key seed so the server can sign.
*
* Input: JSON string with BuildTxRequest
* Output: JSON string with SignedTxData (contains seed for server signing)
*
* Example:
* ```javascript
* const request = {
* seed_phrase: ["word1", "word2", ...],
* inputs: [{addition_record: "0xabc..."}],
* outputs: [{address: "nep1...", amount: "50.0"}],
* fee: "0.01",
* network: "testnet"
* };
* const txData = build_and_sign_tx(JSON.stringify(request));
* // Now broadcast via JSON-RPC: wallet_broadcastSignedTransaction(txData)
* ```
*/
export function build_and_sign_tx(request_json: string): string;
/**
* Get wallet balance via JSON-RPC (Neptune core ext format)
* Uses wallet_balance method from Wallet namespace
*/
export function get_balance(rpc_url: string): Promise<string>;
/**
* Send transaction via JSON-RPC
* Note: Neptune core ext may not have direct "send" method in public RPC
* This is a placeholder - check actual available methods
*/
export function send_tx_jsonrpc(rpc_url: string, to_address: string, amount: string, fee: string): Promise<string>;
/**
* Get block height via JSON-RPC (Neptune core ext format)
* Uses chain_height method from Chain namespace
*/
export function get_block_height(rpc_url: string): Promise<bigint>;
/**
* Get network info via JSON-RPC (Neptune core ext format)
* Uses node_network method from Node namespace
*/
export function get_network_info(rpc_url: string): Promise<string>;
/**
* Get wallet addresses from Neptune core (PRODUCTION - EXACT Neptune addresses!)
* Returns both generation_address and symmetric_address
*/
export function get_wallet_address(rpc_url: string): Promise<string>;
/**
* Get view key from Neptune core (RECOMMENDED)
* This exports the proper view key format from Neptune core's wallet
* Returns a hex-encoded view key that can be used with wallet_getUtxosFromViewKey
*/
export function get_viewkey_from_neptune(rpc_url: string): Promise<string>;
/**
* Get UTXOs from view key (scan blockchain)
* This calls Neptune core's wallet_getUtxosFromViewKey JSON-RPC method
*
* Input: view_key (hex string from neptune-core format), start_block, end_block (optional)
* Output: JSON string with list of UTXOs
*/
export function get_utxos_from_viewkey(rpc_url: string, view_key_hex: string, start_block: bigint, end_block?: bigint | null): Promise<string>;
/**
* Generate viewkey from BIP39 seed phrase
*
* # Arguments
* * `phrase` - Space-separated BIP39 seed phrase (12-24 words)
* * `key_index` - Key derivation index (default: 0)
*
* # Returns
* Hex-encoded viewkey compatible with neptune-core
*/
export function generate_viewkey_from_phrase(phrase: string, key_index: bigint): string;
/**
* Generate receiving address from BIP39 seed phrase
*
* # Arguments
* * `phrase` - Space-separated BIP39 seed phrase (12-24 words)
* * `key_index` - Key derivation index (default: 0)
* * `testnet` - Use testnet network (true) or mainnet (false)
*
* # Returns
* Bech32m-encoded receiving address
*/
export function generate_address_from_phrase(phrase: string, key_index: bigint, testnet: boolean): string;
/**
* Get receiver identifier from viewkey hex
*/
export function get_receiver_id_from_viewkey(viewkey_hex: string): string;
/**
* Debug: Get detailed key derivation info
*/
export function debug_key_derivation(phrase: string, key_index: bigint): string;
/**
* Sign transaction offline (WASM client-side signing)
* This creates lock_script_and_witness WITHOUT exposing spending key to server
*
* # Arguments
* * `phrase` - BIP39 seed phrase (18 words)
* * `utxos_json` - JSON array of UTXOs from get_utxos (with addition_record)
* * `outputs_json` - JSON array of outputs [{address, amount}, ...]
* * `fee` - Transaction fee as string
* * `key_index` - Key derivation index (usually 0)
* * `testnet` - Network selection
*
* # Returns
* JSON containing:
* - lock_script_and_witness: hex-encoded signature
* - view_key: hex-encoded view key
* - inputs: array of addition_records
* - outputs: array of {address, amount}
* - fee: fee string
*/
export function sign_transaction_offline(phrase: string, utxos_json: string, outputs_json: string, fee: string, key_index: bigint, testnet: boolean): string;
export type InitInput = RequestInfo | URL | Response | BufferSource | WebAssembly.Module;
export interface InitOutput {
readonly memory: WebAssembly.Memory;
readonly generate_seed: () => [number, number, number, number];
readonly get_viewkey: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly address_from_seed: (a: number, b: number, c: number, d: number) => [number, number, number, number];
readonly validate_seed_phrase: (a: number, b: number) => [number, number, number];
readonly decode_viewkey: (a: number, b: number) => [number, number, number, number];
readonly build_and_sign_tx: (a: number, b: number) => [number, number, number, number];
readonly get_balance: (a: number, b: number) => any;
readonly send_tx_jsonrpc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => any;
readonly get_block_height: (a: number, b: number) => any;
readonly get_network_info: (a: number, b: number) => any;
readonly get_wallet_address: (a: number, b: number) => any;
readonly get_viewkey_from_neptune: (a: number, b: number) => any;
readonly get_utxos_from_viewkey: (a: number, b: number, c: number, d: number, e: bigint, f: number, g: bigint) => any;
readonly generate_viewkey_from_phrase: (a: number, b: number, c: bigint) => [number, number, number, number];
readonly generate_address_from_phrase: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
readonly get_receiver_id_from_viewkey: (a: number, b: number) => [number, number, number, number];
readonly debug_key_derivation: (a: number, b: number, c: bigint) => [number, number, number, number];
readonly sign_transaction_offline: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: number) => [number, number, number, number];
readonly __wbindgen_exn_store: (a: number) => void;
readonly __externref_table_alloc: () => number;
readonly __wbindgen_export_2: WebAssembly.Table;
readonly __wbindgen_export_3: WebAssembly.Table;
readonly __wbindgen_malloc: (a: number, b: number) => number;
readonly __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
readonly __externref_table_dealloc: (a: number) => void;
readonly __wbindgen_free: (a: number, b: number, c: number) => void;
readonly closure49_externref_shim: (a: number, b: number, c: any) => void;
readonly closure268_externref_shim: (a: number, b: number, c: any, d: any) => void;
readonly __wbindgen_start: () => void;
}
export type SyncInitInput = BufferSource | WebAssembly.Module;
/**
* Instantiates the given `module`, which can either be bytes or
* a precompiled `WebAssembly.Module`.
*
* @param {{ module: SyncInitInput }} module - Passing `SyncInitInput` directly is deprecated.
*
* @returns {InitOutput}
*/
export function initSync(module: { module: SyncInitInput } | SyncInitInput): InitOutput;
/**
* If `module_or_path` is {RequestInfo} or {URL}, makes a request and
* for everything else, calls `WebAssembly.instantiate` directly.
*
* @param {{ module_or_path: InitInput | Promise<InitInput> }} module_or_path - Passing `InitInput` directly is deprecated.
*
* @returns {Promise<InitOutput>}
*/
export default function __wbg_init (module_or_path?: { module_or_path: InitInput | Promise<InitInput> } | InitInput | Promise<InitInput>): Promise<InitOutput>;

View File

@ -0,0 +1,984 @@
let wasm;
function addToExternrefTable0(obj) {
const idx = wasm.__externref_table_alloc();
wasm.__wbindgen_export_2.set(idx, obj);
return idx;
}
function handleError(f, args) {
try {
return f.apply(this, args);
} catch (e) {
const idx = addToExternrefTable0(e);
wasm.__wbindgen_exn_store(idx);
}
}
let cachedUint8ArrayMemory0 = null;
function getUint8ArrayMemory0() {
if (cachedUint8ArrayMemory0 === null || cachedUint8ArrayMemory0.byteLength === 0) {
cachedUint8ArrayMemory0 = new Uint8Array(wasm.memory.buffer);
}
return cachedUint8ArrayMemory0;
}
function getArrayU8FromWasm0(ptr, len) {
ptr = ptr >>> 0;
return getUint8ArrayMemory0().subarray(ptr / 1, ptr / 1 + len);
}
const cachedTextDecoder = (typeof TextDecoder !== 'undefined' ? new TextDecoder('utf-8', { ignoreBOM: true, fatal: true }) : { decode: () => { throw Error('TextDecoder not available') } } );
if (typeof TextDecoder !== 'undefined') { cachedTextDecoder.decode(); };
function getStringFromWasm0(ptr, len) {
ptr = ptr >>> 0;
return cachedTextDecoder.decode(getUint8ArrayMemory0().subarray(ptr, ptr + len));
}
function isLikeNone(x) {
return x === undefined || x === null;
}
const CLOSURE_DTORS = (typeof FinalizationRegistry === 'undefined')
? { register: () => {}, unregister: () => {} }
: new FinalizationRegistry(state => {
wasm.__wbindgen_export_3.get(state.dtor)(state.a, state.b)
});
function makeMutClosure(arg0, arg1, dtor, f) {
const state = { a: arg0, b: arg1, cnt: 1, dtor };
const real = (...args) => {
// First up with a closure we increment the internal reference
// count. This ensures that the Rust closure environment won't
// be deallocated while we're invoking it.
state.cnt++;
const a = state.a;
state.a = 0;
try {
return f(a, state.b, ...args);
} finally {
if (--state.cnt === 0) {
wasm.__wbindgen_export_3.get(state.dtor)(a, state.b);
CLOSURE_DTORS.unregister(state);
} else {
state.a = a;
}
}
};
real.original = state;
CLOSURE_DTORS.register(real, state, state);
return real;
}
function debugString(val) {
// primitive types
const type = typeof val;
if (type == 'number' || type == 'boolean' || val == null) {
return `${val}`;
}
if (type == 'string') {
return `"${val}"`;
}
if (type == 'symbol') {
const description = val.description;
if (description == null) {
return 'Symbol';
} else {
return `Symbol(${description})`;
}
}
if (type == 'function') {
const name = val.name;
if (typeof name == 'string' && name.length > 0) {
return `Function(${name})`;
} else {
return 'Function';
}
}
// objects
if (Array.isArray(val)) {
const length = val.length;
let debug = '[';
if (length > 0) {
debug += debugString(val[0]);
}
for(let i = 1; i < length; i++) {
debug += ', ' + debugString(val[i]);
}
debug += ']';
return debug;
}
// Test for built-in
const builtInMatches = /\[object ([^\]]+)\]/.exec(toString.call(val));
let className;
if (builtInMatches && builtInMatches.length > 1) {
className = builtInMatches[1];
} else {
// Failed to match the standard '[object ClassName]'
return toString.call(val);
}
if (className == 'Object') {
// we're a user defined class or Object
// JSON.stringify avoids problems with cycles, and is generally much
// easier than looping through ownProperties of `val`.
try {
return 'Object(' + JSON.stringify(val) + ')';
} catch (_) {
return 'Object';
}
}
// errors
if (val instanceof Error) {
return `${val.name}: ${val.message}\n${val.stack}`;
}
// TODO we could test for more things here, like `Set`s and `Map`s.
return className;
}
let WASM_VECTOR_LEN = 0;
const cachedTextEncoder = (typeof TextEncoder !== 'undefined' ? new TextEncoder('utf-8') : { encode: () => { throw Error('TextEncoder not available') } } );
const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
? function (arg, view) {
return cachedTextEncoder.encodeInto(arg, view);
}
: function (arg, view) {
const buf = cachedTextEncoder.encode(arg);
view.set(buf);
return {
read: arg.length,
written: buf.length
};
});
function passStringToWasm0(arg, malloc, realloc) {
if (realloc === undefined) {
const buf = cachedTextEncoder.encode(arg);
const ptr = malloc(buf.length, 1) >>> 0;
getUint8ArrayMemory0().subarray(ptr, ptr + buf.length).set(buf);
WASM_VECTOR_LEN = buf.length;
return ptr;
}
let len = arg.length;
let ptr = malloc(len, 1) >>> 0;
const mem = getUint8ArrayMemory0();
let offset = 0;
for (; offset < len; offset++) {
const code = arg.charCodeAt(offset);
if (code > 0x7F) break;
mem[ptr + offset] = code;
}
if (offset !== len) {
if (offset !== 0) {
arg = arg.slice(offset);
}
ptr = realloc(ptr, len, len = offset + arg.length * 3, 1) >>> 0;
const view = getUint8ArrayMemory0().subarray(ptr + offset, ptr + len);
const ret = encodeString(arg, view);
offset += ret.written;
ptr = realloc(ptr, len, offset, 1) >>> 0;
}
WASM_VECTOR_LEN = offset;
return ptr;
}
let cachedDataViewMemory0 = null;
function getDataViewMemory0() {
if (cachedDataViewMemory0 === null || cachedDataViewMemory0.buffer.detached === true || (cachedDataViewMemory0.buffer.detached === undefined && cachedDataViewMemory0.buffer !== wasm.memory.buffer)) {
cachedDataViewMemory0 = new DataView(wasm.memory.buffer);
}
return cachedDataViewMemory0;
}
function takeFromExternrefTable0(idx) {
const value = wasm.__wbindgen_export_2.get(idx);
wasm.__externref_table_dealloc(idx);
return value;
}
/**
* Generate a new random seed phrase (18 words, 192 bits entropy)
* This is completely offline - uses browser's crypto.getRandomValues()
* @returns {string}
*/
export function generate_seed() {
let deferred2_0;
let deferred2_1;
try {
const ret = wasm.generate_seed();
var ptr1 = ret[0];
var len1 = ret[1];
if (ret[3]) {
ptr1 = 0; len1 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred2_0 = ptr1;
deferred2_1 = len1;
return getStringFromWasm0(ptr1, len1);
} finally {
wasm.__wbindgen_free(deferred2_0, deferred2_1, 1);
}
}
/**
* Get view key and address from seed phrase (offline) - COMPATIBLE WITH NEPTUNE-CORE
*
* This derives the view key from a BIP39 seed phrase using neptune-crypto-core.
* The view key can be used with wallet_getUtxosFromViewKey RPC method.
*
* Input: JSON string with seed_phrase array and network ("mainnet" or "testnet")
* Output: JSON string with receiver_identifier, view_key (hex), and address (bech32m)
*
* Example:
* ```javascript
* const result = get_viewkey('["word1", "word2", ...]', "testnet");
* const { receiver_identifier, view_key, address } = JSON.parse(result);
* console.log('View key:', view_key); // Compatible with neptune-core!
* ```
* @param {string} seed_phrase_json
* @param {string} network
* @returns {string}
*/
export function get_viewkey(seed_phrase_json, network) {
let deferred4_0;
let deferred4_1;
try {
const ptr0 = passStringToWasm0(seed_phrase_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.get_viewkey(ptr0, len0, ptr1, len1);
var ptr3 = ret[0];
var len3 = ret[1];
if (ret[3]) {
ptr3 = 0; len3 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred4_0 = ptr3;
deferred4_1 = len3;
return getStringFromWasm0(ptr3, len3);
} finally {
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
}
}
/**
* Get receiving address from seed phrase (offline)
* Input: JSON string with seed_phrase array and network
* Output: bech32m encoded address string
* @param {string} seed_phrase_json
* @param {string} network
* @returns {string}
*/
export function address_from_seed(seed_phrase_json, network) {
let deferred4_0;
let deferred4_1;
try {
const ptr0 = passStringToWasm0(seed_phrase_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(network, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.address_from_seed(ptr0, len0, ptr1, len1);
var ptr3 = ret[0];
var len3 = ret[1];
if (ret[3]) {
ptr3 = 0; len3 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred4_0 = ptr3;
deferred4_1 = len3;
return getStringFromWasm0(ptr3, len3);
} finally {
wasm.__wbindgen_free(deferred4_0, deferred4_1, 1);
}
}
/**
* Validate a seed phrase (offline)
* @param {string} seed_phrase_json
* @returns {boolean}
*/
export function validate_seed_phrase(seed_phrase_json) {
const ptr0 = passStringToWasm0(seed_phrase_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.validate_seed_phrase(ptr0, len0);
if (ret[2]) {
throw takeFromExternrefTable0(ret[1]);
}
return ret[0] !== 0;
}
/**
* Decode view key from hex string (offline)
* @param {string} view_key_hex
* @returns {string}
*/
export function decode_viewkey(view_key_hex) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(view_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.decode_viewkey(ptr0, len0);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Prepare transaction data for server-side signing
*
* This prepares transaction details but does NOT sign locally.
* Instead, it exports the spending key seed so the server can sign.
*
* Input: JSON string with BuildTxRequest
* Output: JSON string with SignedTxData (contains seed for server signing)
*
* Example:
* ```javascript
* const request = {
* seed_phrase: ["word1", "word2", ...],
* inputs: [{addition_record: "0xabc..."}],
* outputs: [{address: "nep1...", amount: "50.0"}],
* fee: "0.01",
* network: "testnet"
* };
* const txData = build_and_sign_tx(JSON.stringify(request));
* // Now broadcast via JSON-RPC: wallet_broadcastSignedTransaction(txData)
* ```
* @param {string} request_json
* @returns {string}
*/
export function build_and_sign_tx(request_json) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(request_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.build_and_sign_tx(ptr0, len0);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Get wallet balance via JSON-RPC (Neptune core ext format)
* Uses wallet_balance method from Wallet namespace
* @param {string} rpc_url
* @returns {Promise<string>}
*/
export function get_balance(rpc_url) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_balance(ptr0, len0);
return ret;
}
/**
* Send transaction via JSON-RPC
* Note: Neptune core ext may not have direct "send" method in public RPC
* This is a placeholder - check actual available methods
* @param {string} rpc_url
* @param {string} to_address
* @param {string} amount
* @param {string} fee
* @returns {Promise<string>}
*/
export function send_tx_jsonrpc(rpc_url, to_address, amount, fee) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(to_address, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passStringToWasm0(amount, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passStringToWasm0(fee, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len3 = WASM_VECTOR_LEN;
const ret = wasm.send_tx_jsonrpc(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
return ret;
}
/**
* Get block height via JSON-RPC (Neptune core ext format)
* Uses chain_height method from Chain namespace
* @param {string} rpc_url
* @returns {Promise<bigint>}
*/
export function get_block_height(rpc_url) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_block_height(ptr0, len0);
return ret;
}
/**
* Get network info via JSON-RPC (Neptune core ext format)
* Uses node_network method from Node namespace
* @param {string} rpc_url
* @returns {Promise<string>}
*/
export function get_network_info(rpc_url) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_network_info(ptr0, len0);
return ret;
}
/**
* Get wallet addresses from Neptune core (PRODUCTION - EXACT Neptune addresses!)
* Returns both generation_address and symmetric_address
* @param {string} rpc_url
* @returns {Promise<string>}
*/
export function get_wallet_address(rpc_url) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_wallet_address(ptr0, len0);
return ret;
}
/**
* Get view key from Neptune core (RECOMMENDED)
* This exports the proper view key format from Neptune core's wallet
* Returns a hex-encoded view key that can be used with wallet_getUtxosFromViewKey
* @param {string} rpc_url
* @returns {Promise<string>}
*/
export function get_viewkey_from_neptune(rpc_url) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_viewkey_from_neptune(ptr0, len0);
return ret;
}
/**
* Get UTXOs from view key (scan blockchain)
* This calls Neptune core's wallet_getUtxosFromViewKey JSON-RPC method
*
* Input: view_key (hex string from neptune-core format), start_block, end_block (optional)
* Output: JSON string with list of UTXOs
* @param {string} rpc_url
* @param {string} view_key_hex
* @param {bigint} start_block
* @param {bigint | null} [end_block]
* @returns {Promise<string>}
*/
export function get_utxos_from_viewkey(rpc_url, view_key_hex, start_block, end_block) {
const ptr0 = passStringToWasm0(rpc_url, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(view_key_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ret = wasm.get_utxos_from_viewkey(ptr0, len0, ptr1, len1, start_block, !isLikeNone(end_block), isLikeNone(end_block) ? BigInt(0) : end_block);
return ret;
}
/**
* Generate viewkey from BIP39 seed phrase
*
* # Arguments
* * `phrase` - Space-separated BIP39 seed phrase (12-24 words)
* * `key_index` - Key derivation index (default: 0)
*
* # Returns
* Hex-encoded viewkey compatible with neptune-core
* @param {string} phrase
* @param {bigint} key_index
* @returns {string}
*/
export function generate_viewkey_from_phrase(phrase, key_index) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(phrase, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.generate_viewkey_from_phrase(ptr0, len0, key_index);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Generate receiving address from BIP39 seed phrase
*
* # Arguments
* * `phrase` - Space-separated BIP39 seed phrase (12-24 words)
* * `key_index` - Key derivation index (default: 0)
* * `testnet` - Use testnet network (true) or mainnet (false)
*
* # Returns
* Bech32m-encoded receiving address
* @param {string} phrase
* @param {bigint} key_index
* @param {boolean} testnet
* @returns {string}
*/
export function generate_address_from_phrase(phrase, key_index, testnet) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(phrase, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.generate_address_from_phrase(ptr0, len0, key_index, testnet);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Get receiver identifier from viewkey hex
* @param {string} viewkey_hex
* @returns {string}
*/
export function get_receiver_id_from_viewkey(viewkey_hex) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(viewkey_hex, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.get_receiver_id_from_viewkey(ptr0, len0);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Debug: Get detailed key derivation info
* @param {string} phrase
* @param {bigint} key_index
* @returns {string}
*/
export function debug_key_derivation(phrase, key_index) {
let deferred3_0;
let deferred3_1;
try {
const ptr0 = passStringToWasm0(phrase, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ret = wasm.debug_key_derivation(ptr0, len0, key_index);
var ptr2 = ret[0];
var len2 = ret[1];
if (ret[3]) {
ptr2 = 0; len2 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred3_0 = ptr2;
deferred3_1 = len2;
return getStringFromWasm0(ptr2, len2);
} finally {
wasm.__wbindgen_free(deferred3_0, deferred3_1, 1);
}
}
/**
* Sign transaction offline (WASM client-side signing)
* This creates lock_script_and_witness WITHOUT exposing spending key to server
*
* # Arguments
* * `phrase` - BIP39 seed phrase (18 words)
* * `utxos_json` - JSON array of UTXOs from get_utxos (with addition_record)
* * `outputs_json` - JSON array of outputs [{address, amount}, ...]
* * `fee` - Transaction fee as string
* * `key_index` - Key derivation index (usually 0)
* * `testnet` - Network selection
*
* # Returns
* JSON containing:
* - lock_script_and_witness: hex-encoded signature
* - view_key: hex-encoded view key
* - inputs: array of addition_records
* - outputs: array of {address, amount}
* - fee: fee string
* @param {string} phrase
* @param {string} utxos_json
* @param {string} outputs_json
* @param {string} fee
* @param {bigint} key_index
* @param {boolean} testnet
* @returns {string}
*/
export function sign_transaction_offline(phrase, utxos_json, outputs_json, fee, key_index, testnet) {
let deferred6_0;
let deferred6_1;
try {
const ptr0 = passStringToWasm0(phrase, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len0 = WASM_VECTOR_LEN;
const ptr1 = passStringToWasm0(utxos_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
const ptr2 = passStringToWasm0(outputs_json, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len2 = WASM_VECTOR_LEN;
const ptr3 = passStringToWasm0(fee, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len3 = WASM_VECTOR_LEN;
const ret = wasm.sign_transaction_offline(ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, key_index, testnet);
var ptr5 = ret[0];
var len5 = ret[1];
if (ret[3]) {
ptr5 = 0; len5 = 0;
throw takeFromExternrefTable0(ret[2]);
}
deferred6_0 = ptr5;
deferred6_1 = len5;
return getStringFromWasm0(ptr5, len5);
} finally {
wasm.__wbindgen_free(deferred6_0, deferred6_1, 1);
}
}
function __wbg_adapter_24(arg0, arg1, arg2) {
wasm.closure49_externref_shim(arg0, arg1, arg2);
}
function __wbg_adapter_83(arg0, arg1, arg2, arg3) {
wasm.closure268_externref_shim(arg0, arg1, arg2, arg3);
}
const __wbindgen_enum_RequestMode = ["same-origin", "no-cors", "cors", "navigate"];
async function __wbg_load(module, imports) {
if (typeof Response === 'function' && module instanceof Response) {
if (typeof WebAssembly.instantiateStreaming === 'function') {
try {
return await WebAssembly.instantiateStreaming(module, imports);
} catch (e) {
if (module.headers.get('Content-Type') != 'application/wasm') {
console.warn("`WebAssembly.instantiateStreaming` failed because your server does not serve Wasm with `application/wasm` MIME type. Falling back to `WebAssembly.instantiate` which is slower. Original error:\n", e);
} else {
throw e;
}
}
}
const bytes = await module.arrayBuffer();
return await WebAssembly.instantiate(bytes, imports);
} else {
const instance = await WebAssembly.instantiate(module, imports);
if (instance instanceof WebAssembly.Instance) {
return { instance, module };
} else {
return instance;
}
}
}
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_call_672a4d21634d4a24 = function() { return handleError(function (arg0, arg1) {
const ret = arg0.call(arg1);
return ret;
}, arguments) };
imports.wbg.__wbg_call_7cccdd69e0791ae2 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = arg0.call(arg1, arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_error_524f506f44df1645 = function(arg0) {
console.error(arg0);
};
imports.wbg.__wbg_fetch_b7bf320f681242d2 = function(arg0, arg1) {
const ret = arg0.fetch(arg1);
return ret;
};
imports.wbg.__wbg_getRandomValues_bb689d73e9ab7af6 = function(arg0, arg1) {
crypto.getRandomValues(getArrayU8FromWasm0(arg0, arg1));
};
imports.wbg.__wbg_headers_7852a8ea641c1379 = function(arg0) {
const ret = arg0.headers;
return ret;
};
imports.wbg.__wbg_instanceof_Response_f2cc20d9f7dfd644 = function(arg0) {
let result;
try {
result = arg0 instanceof Response;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_instanceof_Window_def73ea0955fc569 = function(arg0) {
let result;
try {
result = arg0 instanceof Window;
} catch (_) {
result = false;
}
const ret = result;
return ret;
};
imports.wbg.__wbg_log_7fbbb36c3875e666 = function(arg0, arg1) {
console.log(getStringFromWasm0(arg0, arg1));
};
imports.wbg.__wbg_log_c222819a41e063d3 = function(arg0) {
console.log(arg0);
};
imports.wbg.__wbg_new_23a2665fac83c611 = function(arg0, arg1) {
try {
var state0 = {a: arg0, b: arg1};
var cb0 = (arg0, arg1) => {
const a = state0.a;
state0.a = 0;
try {
return __wbg_adapter_83(a, state0.b, arg0, arg1);
} finally {
state0.a = a;
}
};
const ret = new Promise(cb0);
return ret;
} finally {
state0.a = state0.b = 0;
}
};
imports.wbg.__wbg_new_405e22f390576ce2 = function() {
const ret = new Object();
return ret;
};
imports.wbg.__wbg_newnoargs_105ed471475aaf50 = function(arg0, arg1) {
const ret = new Function(getStringFromWasm0(arg0, arg1));
return ret;
};
imports.wbg.__wbg_newwithstrandinit_06c535e0a867c635 = function() { return handleError(function (arg0, arg1, arg2) {
const ret = new Request(getStringFromWasm0(arg0, arg1), arg2);
return ret;
}, arguments) };
imports.wbg.__wbg_queueMicrotask_97d92b4fcc8a61c5 = function(arg0) {
queueMicrotask(arg0);
};
imports.wbg.__wbg_queueMicrotask_d3219def82552485 = function(arg0) {
const ret = arg0.queueMicrotask;
return ret;
};
imports.wbg.__wbg_resolve_4851785c9c5f573d = function(arg0) {
const ret = Promise.resolve(arg0);
return ret;
};
imports.wbg.__wbg_set_11cd83f45504cedf = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4) {
arg0.set(getStringFromWasm0(arg1, arg2), getStringFromWasm0(arg3, arg4));
}, arguments) };
imports.wbg.__wbg_setbody_5923b78a95eedf29 = function(arg0, arg1) {
arg0.body = arg1;
};
imports.wbg.__wbg_setmethod_3c5280fe5d890842 = function(arg0, arg1, arg2) {
arg0.method = getStringFromWasm0(arg1, arg2);
};
imports.wbg.__wbg_setmode_5dc300b865044b65 = function(arg0, arg1) {
arg0.mode = __wbindgen_enum_RequestMode[arg1];
};
imports.wbg.__wbg_static_accessor_GLOBAL_88a902d13a557d07 = function() {
const ret = typeof global === 'undefined' ? null : global;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_GLOBAL_THIS_56578be7e9f832b0 = function() {
const ret = typeof globalThis === 'undefined' ? null : globalThis;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_SELF_37c5d418e4bf5819 = function() {
const ret = typeof self === 'undefined' ? null : self;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_static_accessor_WINDOW_5de37043a91a9c40 = function() {
const ret = typeof window === 'undefined' ? null : window;
return isLikeNone(ret) ? 0 : addToExternrefTable0(ret);
};
imports.wbg.__wbg_text_7805bea50de2af49 = function() { return handleError(function (arg0) {
const ret = arg0.text();
return ret;
}, arguments) };
imports.wbg.__wbg_then_44b73946d2fb3e7d = function(arg0, arg1) {
const ret = arg0.then(arg1);
return ret;
};
imports.wbg.__wbg_then_48b406749878a531 = function(arg0, arg1, arg2) {
const ret = arg0.then(arg1, arg2);
return ret;
};
imports.wbg.__wbindgen_bigint_from_u64 = function(arg0) {
const ret = BigInt.asUintN(64, arg0);
return ret;
};
imports.wbg.__wbindgen_cb_drop = function(arg0) {
const obj = arg0.original;
if (obj.cnt-- == 1) {
obj.a = 0;
return true;
}
const ret = false;
return ret;
};
imports.wbg.__wbindgen_closure_wrapper214 = function(arg0, arg1, arg2) {
const ret = makeMutClosure(arg0, arg1, 50, __wbg_adapter_24);
return ret;
};
imports.wbg.__wbindgen_debug_string = function(arg0, arg1) {
const ret = debugString(arg1);
const ptr1 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
const len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_init_externref_table = function() {
const table = wasm.__wbindgen_export_2;
const offset = table.grow(4);
table.set(0, undefined);
table.set(offset + 0, undefined);
table.set(offset + 1, null);
table.set(offset + 2, true);
table.set(offset + 3, false);
;
};
imports.wbg.__wbindgen_is_function = function(arg0) {
const ret = typeof(arg0) === 'function';
return ret;
};
imports.wbg.__wbindgen_is_undefined = function(arg0) {
const ret = arg0 === undefined;
return ret;
};
imports.wbg.__wbindgen_string_get = function(arg0, arg1) {
const obj = arg1;
const ret = typeof(obj) === 'string' ? obj : undefined;
var ptr1 = isLikeNone(ret) ? 0 : passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len1 = WASM_VECTOR_LEN;
getDataViewMemory0().setInt32(arg0 + 4 * 1, len1, true);
getDataViewMemory0().setInt32(arg0 + 4 * 0, ptr1, true);
};
imports.wbg.__wbindgen_string_new = function(arg0, arg1) {
const ret = getStringFromWasm0(arg0, arg1);
return ret;
};
imports.wbg.__wbindgen_throw = function(arg0, arg1) {
throw new Error(getStringFromWasm0(arg0, arg1));
};
return imports;
}
function __wbg_init_memory(imports, memory) {
}
function __wbg_finalize_init(instance, module) {
wasm = instance.exports;
__wbg_init.__wbindgen_wasm_module = module;
cachedDataViewMemory0 = null;
cachedUint8ArrayMemory0 = null;
wasm.__wbindgen_start();
return wasm;
}
function initSync(module) {
if (wasm !== undefined) return wasm;
if (typeof module !== 'undefined') {
if (Object.getPrototypeOf(module) === Object.prototype) {
({module} = module)
} else {
console.warn('using deprecated parameters for `initSync()`; pass a single object instead')
}
}
const imports = __wbg_get_imports();
__wbg_init_memory(imports);
if (!(module instanceof WebAssembly.Module)) {
module = new WebAssembly.Module(module);
}
const instance = new WebAssembly.Instance(module, imports);
return __wbg_finalize_init(instance, module);
}
async function __wbg_init(module_or_path) {
if (wasm !== undefined) return wasm;
if (typeof module_or_path !== 'undefined') {
if (Object.getPrototypeOf(module_or_path) === Object.prototype) {
({module_or_path} = module_or_path)
} else {
console.warn('using deprecated parameters for the initialization function; pass a single object instead')
}
}
if (typeof module_or_path === 'undefined') {
module_or_path = new URL('neptune_wasm_bg.wasm', import.meta.url);
}
const imports = __wbg_get_imports();
if (typeof module_or_path === 'string' || (typeof Request === 'function' && module_or_path instanceof Request) || (typeof URL === 'function' && module_or_path instanceof URL)) {
module_or_path = fetch(module_or_path);
}
__wbg_init_memory(imports);
const { instance, module } = await __wbg_load(await module_or_path, imports);
return __wbg_finalize_init(instance, module);
}
export { initSync };
export default __wbg_init;

Binary file not shown.

View File

@ -0,0 +1,32 @@
/* tslint:disable */
/* eslint-disable */
export const memory: WebAssembly.Memory;
export const generate_seed: () => [number, number, number, number];
export const get_viewkey: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const address_from_seed: (a: number, b: number, c: number, d: number) => [number, number, number, number];
export const validate_seed_phrase: (a: number, b: number) => [number, number, number];
export const decode_viewkey: (a: number, b: number) => [number, number, number, number];
export const build_and_sign_tx: (a: number, b: number) => [number, number, number, number];
export const get_balance: (a: number, b: number) => any;
export const send_tx_jsonrpc: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number) => any;
export const get_block_height: (a: number, b: number) => any;
export const get_network_info: (a: number, b: number) => any;
export const get_wallet_address: (a: number, b: number) => any;
export const get_viewkey_from_neptune: (a: number, b: number) => any;
export const get_utxos_from_viewkey: (a: number, b: number, c: number, d: number, e: bigint, f: number, g: bigint) => any;
export const generate_viewkey_from_phrase: (a: number, b: number, c: bigint) => [number, number, number, number];
export const generate_address_from_phrase: (a: number, b: number, c: bigint, d: number) => [number, number, number, number];
export const get_receiver_id_from_viewkey: (a: number, b: number) => [number, number, number, number];
export const debug_key_derivation: (a: number, b: number, c: bigint) => [number, number, number, number];
export const sign_transaction_offline: (a: number, b: number, c: number, d: number, e: number, f: number, g: number, h: number, i: bigint, j: number) => [number, number, number, number];
export const __wbindgen_exn_store: (a: number) => void;
export const __externref_table_alloc: () => number;
export const __wbindgen_export_2: WebAssembly.Table;
export const __wbindgen_export_3: WebAssembly.Table;
export const __wbindgen_malloc: (a: number, b: number) => number;
export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
export const __externref_table_dealloc: (a: number) => void;
export const __wbindgen_free: (a: number, b: number, c: number) => void;
export const closure49_externref_shim: (a: number, b: number, c: any) => void;
export const closure268_externref_shim: (a: number, b: number, c: any, d: any) => void;
export const __wbindgen_start: () => void;

View File

@ -0,0 +1,16 @@
{
"name": "@neptune/wasm",
"type": "module",
"version": "0.1.0",
"license": "Apache-2.0",
"files": [
"neptune_wasm_bg.wasm",
"neptune_wasm.js",
"neptune_wasm.d.ts"
],
"main": "neptune_wasm.js",
"types": "neptune_wasm.d.ts",
"sideEffects": [
"./snippets/*"
]
}

391
pnpm-lock.yaml generated
View File

@ -4,19 +4,31 @@ settings:
autoInstallPeers: true
excludeLinksFromLockfile: false
overrides:
axios: ^1.7.9
importers:
.:
dependencies:
'@neptune/native':
specifier: file:./packages/neptune-native
version: link:packages/neptune-native
'@neptune/wasm':
specifier: file:./packages/neptune-wasm
version: link:packages/neptune-wasm
'@tailwindcss/vite':
specifier: ^4.1.17
version: 4.1.17(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))
'@tanstack/vue-form':
specifier: ^1.26.0
version: 1.26.0(vue@3.5.24(typescript@5.9.3))
version: 1.26.0(vue@3.5.25(typescript@5.9.3))
'@tauri-apps/api':
specifier: ^2.9.0
version: 2.9.0
'@vueuse/core':
specifier: ^14.0.0
version: 14.0.0(vue@3.5.24(typescript@5.9.3))
version: 14.0.0(vue@3.5.25(typescript@5.9.3))
axios:
specifier: ^1.7.9
version: 1.13.2
@ -28,13 +40,13 @@ importers:
version: 2.1.1
lucide-vue-next:
specifier: ^0.554.0
version: 0.554.0(vue@3.5.24(typescript@5.9.3))
version: 0.554.0(vue@3.5.25(typescript@5.9.3))
pinia:
specifier: ^2.3.1
version: 2.3.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
version: 2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
reka-ui:
specifier: ^2.6.0
version: 2.6.0(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
version: 2.6.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
tailwind-merge:
specifier: ^3.4.0
version: 3.4.0
@ -46,13 +58,13 @@ importers:
version: 1.0.7(tailwindcss@4.1.17)
vue:
specifier: ^3.5.24
version: 3.5.24(typescript@5.9.3)
version: 3.5.25(typescript@5.9.3)
vue-i18n:
specifier: ^10.0.8
version: 10.0.8(vue@3.5.24(typescript@5.9.3))
version: 10.0.8(vue@3.5.25(typescript@5.9.3))
vue-router:
specifier: ^4.5.0
version: 4.6.3(vue@3.5.24(typescript@5.9.3))
version: 4.6.3(vue@3.5.25(typescript@5.9.3))
vue-sonner:
specifier: ^2.0.9
version: 2.0.9
@ -71,13 +83,13 @@ importers:
version: 24.10.1
'@typescript-eslint/eslint-plugin':
specifier: ^8.0.0
version: 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
version: 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser':
specifier: ^8.0.0
version: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
version: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@vitejs/plugin-vue':
specifier: ^6.0.1
version: 6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.24(typescript@5.9.3))
version: 6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.25(typescript@5.9.3))
'@vue/eslint-config-prettier':
specifier: ^10.0.0
version: 10.2.0(eslint@9.39.1(jiti@2.6.1))(prettier@3.6.2)
@ -86,7 +98,7 @@ importers:
version: 14.6.0(eslint-plugin-vue@9.33.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@vue/tsconfig':
specifier: ^0.8.1
version: 0.8.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))
version: 0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))
eslint:
specifier: ^9.0.0
version: 9.39.1(jiti@2.6.1)
@ -101,10 +113,10 @@ importers:
version: 5.9.3
unplugin-auto-import:
specifier: ^20.2.0
version: 20.2.0(@vueuse/core@14.0.0(vue@3.5.24(typescript@5.9.3)))
version: 20.2.0(@vueuse/core@14.0.0(vue@3.5.25(typescript@5.9.3)))
unplugin-vue-components:
specifier: ^30.0.0
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.24(typescript@5.9.3))
version: 30.0.0(@babel/parser@7.28.5)(vue@3.5.25(typescript@5.9.3))
vite:
specifier: ^7.2.4
version: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)
@ -112,6 +124,14 @@ importers:
specifier: ^3.1.4
version: 3.1.5(typescript@5.9.3)
packages/neptune-native:
devDependencies:
'@napi-rs/cli':
specifier: ^2.18.4
version: 2.18.4
packages/neptune-wasm: {}
packages:
'@babel/helper-string-parser@7.27.1':
@ -387,6 +407,11 @@ packages:
'@jridgewell/trace-mapping@0.3.31':
resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==}
'@napi-rs/cli@2.18.4':
resolution: {integrity: sha512-SgJeA4df9DE2iAEpr3M2H0OKl/yjtg1BnRI5/JyowS71tUWhrfSu2LT0V3vlHET+g1hBVlrO60PmEXwUEKp8Mg==}
engines: {node: '>= 10'}
hasBin: true
'@nodelib/fs.scandir@2.1.5':
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
engines: {node: '>= 8'}
@ -648,6 +673,9 @@ packages:
peerDependencies:
vue: ^2.7.0 || ^3.0.0
'@tauri-apps/api@2.9.0':
resolution: {integrity: sha512-qD5tMjh7utwBk9/5PrTA/aGr3i5QaJ/Mlt7p8NilQ45WgbifUNPyKWsA63iQ8YfQq6R8ajMapU+/Q8nMcPRLNw==}
'@tauri-apps/cli-darwin-arm64@2.9.4':
resolution: {integrity: sha512-9rHkMVtbMhe0AliVbrGpzMahOBg3rwV46JYRELxR9SN6iu1dvPOaMaiC4cP6M/aD1424ziXnnMdYU06RAH8oIw==}
engines: {node: '>= 10'}
@ -731,63 +759,63 @@ packages:
'@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@typescript-eslint/eslint-plugin@8.47.0':
resolution: {integrity: sha512-fe0rz9WJQ5t2iaLfdbDc9T80GJy0AeO453q8C3YCilnGozvOyCG5t+EZtg7j7D88+c3FipfP/x+wzGnh1xp8ZA==}
'@typescript-eslint/eslint-plugin@8.48.0':
resolution: {integrity: sha512-XxXP5tL1txl13YFtrECECQYeZjBZad4fyd3cFV4a19LkAY/bIp9fev3US4S5fDVV2JaYFiKAZ/GRTOLer+mbyQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
'@typescript-eslint/parser': ^8.47.0
'@typescript-eslint/parser': ^8.48.0
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/parser@8.47.0':
resolution: {integrity: sha512-lJi3PfxVmo0AkEY93ecfN+r8SofEqZNGByvHAI3GBLrvt1Cw6H5k1IM02nSzu0RfUafr2EvFSw0wAsZgubNplQ==}
'@typescript-eslint/parser@8.48.0':
resolution: {integrity: sha512-jCzKdm/QK0Kg4V4IK/oMlRZlY+QOcdjv89U2NgKHZk1CYTj82/RVSx1mV/0gqCVMJ/DA+Zf/S4NBWNF8GQ+eqQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/project-service@8.47.0':
resolution: {integrity: sha512-2X4BX8hUeB5JcA1TQJ7GjcgulXQ+5UkNb0DL8gHsHUHdFoiCTJoYLTpib3LtSDPZsRET5ygN4qqIWrHyYIKERA==}
'@typescript-eslint/project-service@8.48.0':
resolution: {integrity: sha512-Ne4CTZyRh1BecBf84siv42wv5vQvVmgtk8AuiEffKTUo3DrBaGYZueJSxxBZ8fjk/N3DrgChH4TOdIOwOwiqqw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/scope-manager@8.47.0':
resolution: {integrity: sha512-a0TTJk4HXMkfpFkL9/WaGTNuv7JWfFTQFJd6zS9dVAjKsojmv9HT55xzbEpnZoY+VUb+YXLMp+ihMLz/UlZfDg==}
'@typescript-eslint/scope-manager@8.48.0':
resolution: {integrity: sha512-uGSSsbrtJrLduti0Q1Q9+BF1/iFKaxGoQwjWOIVNJv0o6omrdyR8ct37m4xIl5Zzpkp69Kkmvom7QFTtue89YQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/tsconfig-utils@8.47.0':
resolution: {integrity: sha512-ybUAvjy4ZCL11uryalkKxuT3w3sXJAuWhOoGS3T/Wu+iUu1tGJmk5ytSY8gbdACNARmcYEB0COksD2j6hfGK2g==}
'@typescript-eslint/tsconfig-utils@8.48.0':
resolution: {integrity: sha512-WNebjBdFdyu10sR1M4OXTt2OkMd5KWIL+LLfeH9KhgP+jzfDV/LI3eXzwJ1s9+Yc0Kzo2fQCdY/OpdusCMmh6w==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/type-utils@8.47.0':
resolution: {integrity: sha512-QC9RiCmZ2HmIdCEvhd1aJELBlD93ErziOXXlHEZyuBo3tBiAZieya0HLIxp+DoDWlsQqDawyKuNEhORyku+P8A==}
'@typescript-eslint/type-utils@8.48.0':
resolution: {integrity: sha512-zbeVaVqeXhhab6QNEKfK96Xyc7UQuoFWERhEnj3mLVnUWrQnv15cJNseUni7f3g557gm0e46LZ6IJ4NJVOgOpw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/types@8.47.0':
resolution: {integrity: sha512-nHAE6bMKsizhA2uuYZbEbmp5z2UpffNrPEqiKIeN7VsV6UY/roxanWfoRrf6x/k9+Obf+GQdkm0nPU+vnMXo9A==}
'@typescript-eslint/types@8.48.0':
resolution: {integrity: sha512-cQMcGQQH7kwKoVswD1xdOytxQR60MWKM1di26xSUtxehaDs/32Zpqsu5WJlXTtTTqyAVK8R7hvsUnIXRS+bjvA==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@typescript-eslint/typescript-estree@8.47.0':
resolution: {integrity: sha512-k6ti9UepJf5NpzCjH31hQNLHQWupTRPhZ+KFF8WtTuTpy7uHPfeg2NM7cP27aCGajoEplxJDFVCEm9TGPYyiVg==}
'@typescript-eslint/typescript-estree@8.48.0':
resolution: {integrity: sha512-ljHab1CSO4rGrQIAyizUS6UGHHCiAYhbfcIZ1zVJr5nMryxlXMVWS3duFPSKvSUbFPwkXMFk1k0EMIjub4sRRQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/utils@8.47.0':
resolution: {integrity: sha512-g7XrNf25iL4TJOiPqatNuaChyqt49a/onq5YsJ9+hXeugK+41LVg7AxikMfM02PC6jbNtZLCJj6AUcQXJS/jGQ==}
'@typescript-eslint/utils@8.48.0':
resolution: {integrity: sha512-yTJO1XuGxCsSfIVt1+1UrLHtue8xz16V8apzPYI06W0HbEbEWHxHXgZaAgavIkoh+GeV6hKKd5jm0sS6OYxWXQ==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
typescript: '>=4.8.4 <6.0.0'
'@typescript-eslint/visitor-keys@8.47.0':
resolution: {integrity: sha512-SIV3/6eftCy1bNzCQoPmbWsRLujS8t5iDIZ4spZOBHqrM+yfX2ogg8Tt3PDTAVKw3sSCiUgg30uOAvK2r9zGjQ==}
'@typescript-eslint/visitor-keys@8.48.0':
resolution: {integrity: sha512-T0XJMaRPOH3+LBbAfzR2jalckP1MSG/L9eUtY0DEzUyVaXJ/t6zN0nR7co5kz0Jko/nkSYCBRkz1djvjajVTTg==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
'@vitejs/plugin-vue@6.0.2':
@ -806,17 +834,17 @@ packages:
'@volar/typescript@2.4.23':
resolution: {integrity: sha512-lAB5zJghWxVPqfcStmAP1ZqQacMpe90UrP5RJ3arDyrhy4aCUQqmxPPLB2PWDKugvylmO41ljK7vZ+t6INMTag==}
'@vue/compiler-core@3.5.24':
resolution: {integrity: sha512-eDl5H57AOpNakGNAkFDH+y7kTqrQpJkZFXhWZQGyx/5Wh7B1uQYvcWkvZi11BDhscPgj8N7XV3oRwiPnx1Vrig==}
'@vue/compiler-core@3.5.25':
resolution: {integrity: sha512-vay5/oQJdsNHmliWoZfHPoVZZRmnSWhug0BYT34njkYTPqClh3DNWLkZNJBVSjsNMrg0CCrBfoKkjZQPM/QVUw==}
'@vue/compiler-dom@3.5.24':
resolution: {integrity: sha512-1QHGAvs53gXkWdd3ZMGYuvQFXHW4ksKWPG8HP8/2BscrbZ0brw183q2oNWjMrSWImYLHxHrx1ItBQr50I/q2zw==}
'@vue/compiler-dom@3.5.25':
resolution: {integrity: sha512-4We0OAcMZsKgYoGlMjzYvaoErltdFI2/25wqanuTu+S4gismOTRTBPi4IASOjxWdzIwrYSjnqONfKvuqkXzE2Q==}
'@vue/compiler-sfc@3.5.24':
resolution: {integrity: sha512-8EG5YPRgmTB+YxYBM3VXy8zHD9SWHUJLIGPhDovo3Z8VOgvP+O7UP5vl0J4BBPWYD9vxtBabzW1EuEZ+Cqs14g==}
'@vue/compiler-sfc@3.5.25':
resolution: {integrity: sha512-PUgKp2rn8fFsI++lF2sO7gwO2d9Yj57Utr5yEsDf3GNaQcowCLKL7sf+LvVFvtJDXUp/03+dC6f2+LCv5aK1ag==}
'@vue/compiler-ssr@3.5.24':
resolution: {integrity: sha512-trOvMWNBMQ/odMRHW7Ae1CdfYx+7MuiQu62Jtu36gMLXcaoqKvAyh+P73sYG9ll+6jLB6QPovqoKGGZROzkFFg==}
'@vue/compiler-ssr@3.5.25':
resolution: {integrity: sha512-ritPSKLBcParnsKYi+GNtbdbrIE1mtuFEJ4U1sWeuOMlIziK5GtOL85t5RhsNy4uWIXPgk+OUdpnXiTdzn8o3A==}
'@vue/devtools-api@6.6.4':
resolution: {integrity: sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==}
@ -846,22 +874,22 @@ packages:
typescript:
optional: true
'@vue/reactivity@3.5.24':
resolution: {integrity: sha512-BM8kBhtlkkbnyl4q+HiF5R5BL0ycDPfihowulm02q3WYp2vxgPcJuZO866qa/0u3idbMntKEtVNuAUp5bw4teg==}
'@vue/reactivity@3.5.25':
resolution: {integrity: sha512-5xfAypCQepv4Jog1U4zn8cZIcbKKFka3AgWHEFQeK65OW+Ys4XybP6z2kKgws4YB43KGpqp5D/K3go2UPPunLA==}
'@vue/runtime-core@3.5.24':
resolution: {integrity: sha512-RYP/byyKDgNIqfX/gNb2PB55dJmM97jc9wyF3jK7QUInYKypK2exmZMNwnjueWwGceEkP6NChd3D2ZVEp9undQ==}
'@vue/runtime-core@3.5.25':
resolution: {integrity: sha512-Z751v203YWwYzy460bzsYQISDfPjHTl+6Zzwo/a3CsAf+0ccEjQ8c+0CdX1WsumRTHeywvyUFtW6KvNukT/smA==}
'@vue/runtime-dom@3.5.24':
resolution: {integrity: sha512-Z8ANhr/i0XIluonHVjbUkjvn+CyrxbXRIxR7wn7+X7xlcb7dJsfITZbkVOeJZdP8VZwfrWRsWdShH6pngMxRjw==}
'@vue/runtime-dom@3.5.25':
resolution: {integrity: sha512-a4WrkYFbb19i9pjkz38zJBg8wa/rboNERq3+hRRb0dHiJh13c+6kAbgqCPfMaJ2gg4weWD3APZswASOfmKwamA==}
'@vue/server-renderer@3.5.24':
resolution: {integrity: sha512-Yh2j2Y4G/0/4z/xJ1Bad4mxaAk++C2v4kaa8oSYTMJBJ00/ndPuxCnWeot0/7/qafQFLh5pr6xeV6SdMcE/G1w==}
'@vue/server-renderer@3.5.25':
resolution: {integrity: sha512-UJaXR54vMG61i8XNIzTSf2Q7MOqZHpp8+x3XLGtE3+fL+nQd+k7O5+X3D/uWrnQXOdMw5VPih+Uremcw+u1woQ==}
peerDependencies:
vue: 3.5.24
vue: 3.5.25
'@vue/shared@3.5.24':
resolution: {integrity: sha512-9cwHL2EsJBdi8NY22pngYYWzkTDhld6fAD6jlaeloNGciNSJL6bLpbxVgXl96X00Jtc6YWQv96YA/0sxex/k1A==}
'@vue/shared@3.5.25':
resolution: {integrity: sha512-AbOPdQQnAnzs58H2FrrDxYj/TJfmeS2jdfEEhgiKINy+bnOANmVizIEgq1r+C5zsbs6l1CCQxtcj71rwNQ4jWg==}
'@vue/tsconfig@0.8.1':
resolution: {integrity: sha512-aK7feIWPXFSUhsCP9PFqPyFOcz4ENkb8hZ2pneL6m2UjCkccvaOhC/5KCKluuBufvp2KzkbdA2W2pk20vLzu3g==}
@ -909,8 +937,8 @@ packages:
ajv@6.12.6:
resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==}
alien-signals@3.1.0:
resolution: {integrity: sha512-yufC6VpSy8tK3I0lO67pjumo5JvDQVQyr38+3OHqe6CHl1t2VZekKZ7EKKZSqk0cRmE7U7tfZbpXiKNzuc+ckg==}
alien-signals@3.1.1:
resolution: {integrity: sha512-ogkIWbVrLwKtHY6oOAXaYkAxP+cTH7V5FZ5+Tm4NZFd8VDZ6uNMDrfzqctTZ42eTMCSR3ne3otpcxmqSnFfPYA==}
ansi-styles@4.3.0:
resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==}
@ -1657,8 +1685,8 @@ packages:
resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==}
engines: {node: '>=10'}
typescript-eslint@8.47.0:
resolution: {integrity: sha512-Lwe8i2XQ3WoMjua/r1PHrCTpkubPYJCAfOurtn+mtTzqB6jNd+14n9UN1bJ4s3F49x9ixAm0FLflB/JzQ57M8Q==}
typescript-eslint@8.48.0:
resolution: {integrity: sha512-fcKOvQD9GUn3Xw63EgiDqhvWJ5jsyZUaekl3KVpGsDJnN46WJTe3jWxtQP9lMZm1LJNkFLlTaWAxK2vUQR+cqw==}
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
peerDependencies:
eslint: ^8.57.0 || ^9.0.0
@ -1815,8 +1843,8 @@ packages:
peerDependencies:
typescript: '>=5.0.0'
vue@3.5.24:
resolution: {integrity: sha512-uTHDOpVQTMjcGgrqFPSb8iO2m1DUvo+WbGqoXQz8Y1CeBYQ0FXf2z1gLRaBtHjlRz7zZUBHxjVB5VTLzYkvftg==}
vue@3.5.25:
resolution: {integrity: sha512-YLVdgv2K13WJ6n+kD5owehKtEXwdwXuj2TTyJMsO7pSeKw2bfRNZGjhB7YzrpbMYj5b5QsUebHpOqR3R3ziy/g==}
peerDependencies:
typescript: '*'
peerDependenciesMeta:
@ -1996,11 +2024,11 @@ snapshots:
'@floating-ui/utils@0.2.10': {}
'@floating-ui/vue@1.1.9(vue@3.5.24(typescript@5.9.3))':
'@floating-ui/vue@1.1.9(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@floating-ui/dom': 1.7.4
'@floating-ui/utils': 0.2.10
vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3))
vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
transitivePeerDependencies:
- '@vue/composition-api'
- vue
@ -2055,6 +2083,8 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5
'@napi-rs/cli@2.18.4': {}
'@nodelib/fs.scandir@2.1.5':
dependencies:
'@nodelib/fs.stat': 2.0.5
@ -2228,24 +2258,26 @@ snapshots:
'@tanstack/virtual-core@3.13.12': {}
'@tanstack/vue-form@1.26.0(vue@3.5.24(typescript@5.9.3))':
'@tanstack/vue-form@1.26.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@tanstack/form-core': 1.26.0
'@tanstack/vue-store': 0.7.7(vue@3.5.24(typescript@5.9.3))
vue: 3.5.24(typescript@5.9.3)
'@tanstack/vue-store': 0.7.7(vue@3.5.25(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
'@tanstack/vue-store@0.7.7(vue@3.5.24(typescript@5.9.3))':
'@tanstack/vue-store@0.7.7(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@tanstack/store': 0.7.7
vue: 3.5.24(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
'@tanstack/vue-virtual@3.13.12(vue@3.5.24(typescript@5.9.3))':
'@tanstack/vue-virtual@3.13.12(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@tanstack/virtual-core': 3.13.12
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
'@tauri-apps/api@2.9.0': {}
'@tauri-apps/cli-darwin-arm64@2.9.4':
optional: true
@ -2304,14 +2336,14 @@ snapshots:
'@types/web-bluetooth@0.0.21': {}
'@typescript-eslint/eslint-plugin@8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/eslint-plugin@8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/regexpp': 4.12.2
'@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.47.0
'@typescript-eslint/type-utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.47.0
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/type-utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.0
eslint: 9.39.1(jiti@2.6.1)
graphemer: 1.4.0
ignore: 7.0.5
@ -2321,41 +2353,41 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/scope-manager': 8.47.0
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.47.0
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
eslint: 9.39.1(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/project-service@8.47.0(typescript@5.9.3)':
'@typescript-eslint/project-service@8.48.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3)
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
'@typescript-eslint/types': 8.48.0
debug: 4.4.3
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/scope-manager@8.47.0':
'@typescript-eslint/scope-manager@8.48.0':
dependencies:
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/visitor-keys': 8.47.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/visitor-keys': 8.48.0
'@typescript-eslint/tsconfig-utils@8.47.0(typescript@5.9.3)':
'@typescript-eslint/tsconfig-utils@8.48.0(typescript@5.9.3)':
dependencies:
typescript: 5.9.3
'@typescript-eslint/type-utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/type-utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
debug: 4.4.3
eslint: 9.39.1(jiti@2.6.1)
ts-api-utils: 2.1.0(typescript@5.9.3)
@ -2363,45 +2395,44 @@ snapshots:
transitivePeerDependencies:
- supports-color
'@typescript-eslint/types@8.47.0': {}
'@typescript-eslint/types@8.48.0': {}
'@typescript-eslint/typescript-estree@8.47.0(typescript@5.9.3)':
'@typescript-eslint/typescript-estree@8.48.0(typescript@5.9.3)':
dependencies:
'@typescript-eslint/project-service': 8.47.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.47.0(typescript@5.9.3)
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/visitor-keys': 8.47.0
'@typescript-eslint/project-service': 8.48.0(typescript@5.9.3)
'@typescript-eslint/tsconfig-utils': 8.48.0(typescript@5.9.3)
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/visitor-keys': 8.48.0
debug: 4.4.3
fast-glob: 3.3.3
is-glob: 4.0.3
minimatch: 9.0.5
semver: 7.7.3
tinyglobby: 0.2.15
ts-api-utils: 2.1.0(typescript@5.9.3)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/utils@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
'@typescript-eslint/utils@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@eslint-community/eslint-utils': 4.9.0(eslint@9.39.1(jiti@2.6.1))
'@typescript-eslint/scope-manager': 8.47.0
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
'@typescript-eslint/scope-manager': 8.48.0
'@typescript-eslint/types': 8.48.0
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
- supports-color
'@typescript-eslint/visitor-keys@8.47.0':
'@typescript-eslint/visitor-keys@8.48.0':
dependencies:
'@typescript-eslint/types': 8.47.0
'@typescript-eslint/types': 8.48.0
eslint-visitor-keys: 4.2.1
'@vitejs/plugin-vue@6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.24(typescript@5.9.3))':
'@vitejs/plugin-vue@6.0.2(vite@7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2))(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@rolldown/pluginutils': 1.0.0-beta.50
vite: 7.2.4(@types/node@24.10.1)(jiti@2.6.1)(lightningcss@1.30.2)
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
'@volar/language-core@2.4.23':
dependencies:
@ -2415,35 +2446,35 @@ snapshots:
path-browserify: 1.0.1
vscode-uri: 3.1.0
'@vue/compiler-core@3.5.24':
'@vue/compiler-core@3.5.25':
dependencies:
'@babel/parser': 7.28.5
'@vue/shared': 3.5.24
'@vue/shared': 3.5.25
entities: 4.5.0
estree-walker: 2.0.2
source-map-js: 1.2.1
'@vue/compiler-dom@3.5.24':
'@vue/compiler-dom@3.5.25':
dependencies:
'@vue/compiler-core': 3.5.24
'@vue/shared': 3.5.24
'@vue/compiler-core': 3.5.25
'@vue/shared': 3.5.25
'@vue/compiler-sfc@3.5.24':
'@vue/compiler-sfc@3.5.25':
dependencies:
'@babel/parser': 7.28.5
'@vue/compiler-core': 3.5.24
'@vue/compiler-dom': 3.5.24
'@vue/compiler-ssr': 3.5.24
'@vue/shared': 3.5.24
'@vue/compiler-core': 3.5.25
'@vue/compiler-dom': 3.5.25
'@vue/compiler-ssr': 3.5.25
'@vue/shared': 3.5.25
estree-walker: 2.0.2
magic-string: 0.30.21
postcss: 8.5.6
source-map-js: 1.2.1
'@vue/compiler-ssr@3.5.24':
'@vue/compiler-ssr@3.5.25':
dependencies:
'@vue/compiler-dom': 3.5.24
'@vue/shared': 3.5.24
'@vue/compiler-dom': 3.5.25
'@vue/shared': 3.5.25
'@vue/devtools-api@6.6.4': {}
@ -2458,11 +2489,11 @@ snapshots:
'@vue/eslint-config-typescript@14.6.0(eslint-plugin-vue@9.33.0(eslint@9.39.1(jiti@2.6.1)))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)':
dependencies:
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
eslint-plugin-vue: 9.33.0(eslint@9.39.1(jiti@2.6.1))
fast-glob: 3.3.3
typescript-eslint: 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
typescript-eslint: 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
vue-eslint-parser: 10.2.0(eslint@9.39.1(jiti@2.6.1))
optionalDependencies:
typescript: 5.9.3
@ -2472,59 +2503,59 @@ snapshots:
'@vue/language-core@3.1.5(typescript@5.9.3)':
dependencies:
'@volar/language-core': 2.4.23
'@vue/compiler-dom': 3.5.24
'@vue/shared': 3.5.24
alien-signals: 3.1.0
'@vue/compiler-dom': 3.5.25
'@vue/shared': 3.5.25
alien-signals: 3.1.1
muggle-string: 0.4.1
path-browserify: 1.0.1
picomatch: 4.0.3
optionalDependencies:
typescript: 5.9.3
'@vue/reactivity@3.5.24':
'@vue/reactivity@3.5.25':
dependencies:
'@vue/shared': 3.5.24
'@vue/shared': 3.5.25
'@vue/runtime-core@3.5.24':
'@vue/runtime-core@3.5.25':
dependencies:
'@vue/reactivity': 3.5.24
'@vue/shared': 3.5.24
'@vue/reactivity': 3.5.25
'@vue/shared': 3.5.25
'@vue/runtime-dom@3.5.24':
'@vue/runtime-dom@3.5.25':
dependencies:
'@vue/reactivity': 3.5.24
'@vue/runtime-core': 3.5.24
'@vue/shared': 3.5.24
'@vue/reactivity': 3.5.25
'@vue/runtime-core': 3.5.25
'@vue/shared': 3.5.25
csstype: 3.2.3
'@vue/server-renderer@3.5.24(vue@3.5.24(typescript@5.9.3))':
'@vue/server-renderer@3.5.25(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@vue/compiler-ssr': 3.5.24
'@vue/shared': 3.5.24
vue: 3.5.24(typescript@5.9.3)
'@vue/compiler-ssr': 3.5.25
'@vue/shared': 3.5.25
vue: 3.5.25(typescript@5.9.3)
'@vue/shared@3.5.24': {}
'@vue/shared@3.5.25': {}
'@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3))':
'@vue/tsconfig@0.8.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3))':
optionalDependencies:
typescript: 5.9.3
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
'@vueuse/core@12.8.2(typescript@5.9.3)':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 12.8.2
'@vueuse/shared': 12.8.2(typescript@5.9.3)
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- typescript
'@vueuse/core@14.0.0(vue@3.5.24(typescript@5.9.3))':
'@vueuse/core@14.0.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
'@types/web-bluetooth': 0.0.21
'@vueuse/metadata': 14.0.0
'@vueuse/shared': 14.0.0(vue@3.5.24(typescript@5.9.3))
vue: 3.5.24(typescript@5.9.3)
'@vueuse/shared': 14.0.0(vue@3.5.25(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
'@vueuse/metadata@12.8.2': {}
@ -2532,13 +2563,13 @@ snapshots:
'@vueuse/shared@12.8.2(typescript@5.9.3)':
dependencies:
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- typescript
'@vueuse/shared@14.0.0(vue@3.5.24(typescript@5.9.3))':
'@vueuse/shared@14.0.0(vue@3.5.25(typescript@5.9.3))':
dependencies:
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
acorn-jsx@5.3.2(acorn@8.15.0):
dependencies:
@ -2553,7 +2584,7 @@ snapshots:
json-schema-traverse: 0.4.1
uri-js: 4.4.1
alien-signals@3.1.0: {}
alien-signals@3.1.1: {}
ansi-styles@4.3.0:
dependencies:
@ -3043,9 +3074,9 @@ snapshots:
lodash@4.17.21: {}
lucide-vue-next@0.554.0(vue@3.5.24(typescript@5.9.3)):
lucide-vue-next@0.554.0(vue@3.5.25(typescript@5.9.3)):
dependencies:
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
magic-string@0.30.21:
dependencies:
@ -3130,11 +3161,11 @@ snapshots:
picomatch@4.0.3: {}
pinia@2.3.1(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)):
pinia@2.3.1(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.24(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.24(typescript@5.9.3))
vue: 3.5.25(typescript@5.9.3)
vue-demi: 0.14.10(vue@3.5.25(typescript@5.9.3))
optionalDependencies:
typescript: 5.9.3
transitivePeerDependencies:
@ -3181,19 +3212,19 @@ snapshots:
readdirp@4.1.2: {}
reka-ui@2.6.0(typescript@5.9.3)(vue@3.5.24(typescript@5.9.3)):
reka-ui@2.6.0(typescript@5.9.3)(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@floating-ui/dom': 1.7.4
'@floating-ui/vue': 1.1.9(vue@3.5.24(typescript@5.9.3))
'@floating-ui/vue': 1.1.9(vue@3.5.25(typescript@5.9.3))
'@internationalized/date': 3.10.0
'@internationalized/number': 3.6.5
'@tanstack/vue-virtual': 3.13.12(vue@3.5.24(typescript@5.9.3))
'@tanstack/vue-virtual': 3.13.12(vue@3.5.25(typescript@5.9.3))
'@vueuse/core': 12.8.2(typescript@5.9.3)
'@vueuse/shared': 12.8.2(typescript@5.9.3)
aria-hidden: 1.2.6
defu: 6.1.4
ohash: 2.0.11
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
transitivePeerDependencies:
- '@vue/composition-api'
- typescript
@ -3291,12 +3322,12 @@ snapshots:
type-fest@0.20.2: {}
typescript-eslint@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
typescript-eslint@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3):
dependencies:
'@typescript-eslint/eslint-plugin': 8.47.0(@typescript-eslint/parser@8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.47.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.47.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/eslint-plugin': 8.48.0(@typescript-eslint/parser@8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/parser': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
'@typescript-eslint/typescript-estree': 8.48.0(typescript@5.9.3)
'@typescript-eslint/utils': 8.48.0(eslint@9.39.1(jiti@2.6.1))(typescript@5.9.3)
eslint: 9.39.1(jiti@2.6.1)
typescript: 5.9.3
transitivePeerDependencies:
@ -3325,7 +3356,7 @@ snapshots:
unplugin: 2.3.11
unplugin-utils: 0.3.1
unplugin-auto-import@20.2.0(@vueuse/core@14.0.0(vue@3.5.24(typescript@5.9.3))):
unplugin-auto-import@20.2.0(@vueuse/core@14.0.0(vue@3.5.25(typescript@5.9.3))):
dependencies:
local-pkg: 1.1.2
magic-string: 0.30.21
@ -3334,14 +3365,14 @@ snapshots:
unplugin: 2.3.11
unplugin-utils: 0.3.1
optionalDependencies:
'@vueuse/core': 14.0.0(vue@3.5.24(typescript@5.9.3))
'@vueuse/core': 14.0.0(vue@3.5.25(typescript@5.9.3))
unplugin-utils@0.3.1:
dependencies:
pathe: 2.0.3
picomatch: 4.0.3
unplugin-vue-components@30.0.0(@babel/parser@7.28.5)(vue@3.5.24(typescript@5.9.3)):
unplugin-vue-components@30.0.0(@babel/parser@7.28.5)(vue@3.5.25(typescript@5.9.3)):
dependencies:
chokidar: 4.0.3
debug: 4.4.3
@ -3351,7 +3382,7 @@ snapshots:
tinyglobby: 0.2.15
unplugin: 2.3.11
unplugin-utils: 0.3.1
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
optionalDependencies:
'@babel/parser': 7.28.5
transitivePeerDependencies:
@ -3386,9 +3417,9 @@ snapshots:
vscode-uri@3.1.0: {}
vue-demi@0.14.10(vue@3.5.24(typescript@5.9.3)):
vue-demi@0.14.10(vue@3.5.25(typescript@5.9.3)):
dependencies:
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
vue-eslint-parser@10.2.0(eslint@9.39.1(jiti@2.6.1)):
dependencies:
@ -3415,17 +3446,17 @@ snapshots:
transitivePeerDependencies:
- supports-color
vue-i18n@10.0.8(vue@3.5.24(typescript@5.9.3)):
vue-i18n@10.0.8(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@intlify/core-base': 10.0.8
'@intlify/shared': 10.0.8
'@vue/devtools-api': 6.6.4
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
vue-router@4.6.3(vue@3.5.24(typescript@5.9.3)):
vue-router@4.6.3(vue@3.5.25(typescript@5.9.3)):
dependencies:
'@vue/devtools-api': 6.6.4
vue: 3.5.24(typescript@5.9.3)
vue: 3.5.25(typescript@5.9.3)
vue-sonner@2.0.9: {}
@ -3435,13 +3466,13 @@ snapshots:
'@vue/language-core': 3.1.5(typescript@5.9.3)
typescript: 5.9.3
vue@3.5.24(typescript@5.9.3):
vue@3.5.25(typescript@5.9.3):
dependencies:
'@vue/compiler-dom': 3.5.24
'@vue/compiler-sfc': 3.5.24
'@vue/runtime-dom': 3.5.24
'@vue/server-renderer': 3.5.24(vue@3.5.24(typescript@5.9.3))
'@vue/shared': 3.5.24
'@vue/compiler-dom': 3.5.25
'@vue/compiler-sfc': 3.5.25
'@vue/runtime-dom': 3.5.25
'@vue/server-renderer': 3.5.25(vue@3.5.25(typescript@5.9.3))
'@vue/shared': 3.5.25
optionalDependencies:
typescript: 5.9.3

2
pnpm-workspace.yaml Normal file
View File

@ -0,0 +1,2 @@
packages:
- 'packages/*'

View File

@ -1,6 +1,5 @@
import axios, { type AxiosInstance, type AxiosResponse, type AxiosError } from 'axios'
const STATUS_CODE_SUCCESS = 200
import { STATUS_CODE_SUCCESS } from '@/utils/constants'
export const API_URL = import.meta.env.VITE_APP_API || ''
@ -14,17 +13,6 @@ const instance: AxiosInstance = axios.create({
withCredentials: false,
})
// Request interceptor
instance.interceptors.request.use(
config => {
// Add request interceptors here (e.g., auth tokens)
return config
},
(error: AxiosError) => {
return Promise.reject(error)
}
)
// Response interceptor
instance.interceptors.response.use(
(response: AxiosResponse) => {
@ -41,10 +29,4 @@ instance.interceptors.response.use(
}
)
// Set locale for API requests
export const setLocaleApi = (locale: string) => {
instance.defaults.headers.common['lang'] = locale
}
export default instance

View File

@ -0,0 +1,18 @@
<script setup lang="ts">
import { ArrowLeft } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
const emit = defineEmits<{
click: []
}>()
const handleClick = () => {
emit('click')
}
</script>
<template>
<Button variant="ghost" size="icon-lg" @click="handleClick">
<ArrowLeft />
</Button>
</template>

View File

@ -0,0 +1 @@
export { default as ArrowLeftCommon } from './ArrowLeftCommon.vue'

View File

@ -0,0 +1,4 @@
export * from './layout'
export * from './logo'
export * from './password-form'

View File

@ -6,9 +6,9 @@ const router = useRouter()
// Navigation items for bottom tab bar
const navItems = [
{ name: 'Home', icon: Home, route: '/', label: 'Home' },
{ name: 'UTXO', icon: Home, route: '/', label: 'UTXO' },
{ name: 'Wallet', icon: Wallet, route: '/wallet', label: 'Wallet' },
{ name: 'History', icon: History, route: '/history', label: 'History' },
{ name: 'History', icon: History, route: '/transaction-history', label: 'History' },
{ name: 'Settings', icon: Settings, route: '/settings', label: 'Settings' },
]
@ -28,7 +28,7 @@ const isActiveRoute = (routePath: string) => {
alt="Neptune"
class="h-8 w-8 rounded-lg object-cover"
/>
<span class="text-lg font-semibold text-foreground">Neptune</span>
<span class="text-lg font-semibold text-foreground">Neptune Privacy</span>
</div>
<ThemeToggle />
</div>

View File

@ -0,0 +1,25 @@
<script setup lang="ts">
import { computed } from 'vue'
import { useColorMode } from '@vueuse/core'
const mode = useColorMode()
const isDark = computed(() => mode.value === 'dark')
</script>
<template>
<div class="relative">
<div
v-if="isDark"
class="absolute -inset-1 animate-pulse rounded-full bg-linear-to-r from-primary via-accent to-primary opacity-75 blur-xl"
></div>
<div
class="relative flex h-24 w-24 items-center justify-center rounded-full bg-linear-to-br from-primary to-accent shadow-2xl ring-4 ring-background"
>
<img
src="@/assets/imgs/neptune_logo.jpg"
alt="Neptune Privacy"
class="h-20 w-20 rounded-full object-cover"
/>
</div>
</div>
</template>

View File

@ -0,0 +1 @@
export { default as Logo } from './Logo.vue'

View File

@ -0,0 +1,209 @@
<script setup lang="ts">
import { ref, computed } from 'vue'
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
import { Eye, EyeOff, Lock, Loader2, ChevronLeft } from 'lucide-vue-next'
import { Card, CardContent } from '@/components/ui/card'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
interface Props {
label?: string
placeholder?: string
buttonText?: string
hideBackButton?: boolean
backButtonText?: string
loading?: boolean
error?: boolean
errorMessage?: string
validateFormat?: boolean
autocomplete?: string
}
const props = withDefaults(defineProps<Props>(), {
label: 'Password',
placeholder: 'Enter your password',
buttonText: 'Submit',
backButtonText: 'Back',
hideBackButton: true,
loading: false,
error: false,
errorMessage: 'Incorrect password. Please try again.',
validateFormat: false,
autocomplete: 'current-password',
})
const emit = defineEmits<{
submit: [password: string]
back: []
}>()
const showPassword = ref(false)
// Password strength calculation
const passwordStrength = computed(() => {
const password = form.state.values?.password
if (!password || !props.validateFormat) return { level: 0, text: '', color: '' }
let strength = 0
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
}
strength = Object.values(checks).filter(Boolean).length
if (strength <= 2) return { level: 1, text: 'Weak', color: 'hsl(var(--destructive))' }
if (strength <= 3) return { level: 2, text: 'Medium', color: 'hsl(48 96% 53%)' } // yellow
if (strength <= 4) return { level: 3, text: 'Good', color: 'hsl(221 83% 53%)' } // blue
return { level: 4, text: 'Strong', color: 'hsl(142 76% 36%)' } // green
})
// Custom password schema with strength validation
const createPasswordSchema = () => {
let schema = z.string().min(1, 'Password is required')
if (props.validateFormat) {
schema = schema
.min(8, 'Password must be at least 8 characters')
.refine((val) => {
const checks = {
uppercase: /[A-Z]/.test(val),
lowercase: /[a-z]/.test(val),
number: /[0-9]/.test(val),
}
const strength = Object.values(checks).filter(Boolean).length
return strength >= 2 // Medium level minimum
}, 'Password is too weak. Use uppercase, lowercase, and numbers.')
}
return schema
}
const form = useForm({
defaultValues: {
password: '',
},
validators: {
onChange: z.object({
password: createPasswordSchema(),
}),
},
onSubmit: async ({ value }) => {
emit('submit', value.password)
},
})
const handleBack = () => {
emit('back')
}
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
</script>
<template>
<form @submit.prevent="form.handleSubmit">
<form.Field name="password">
<template #default="{ field }">
<div class="space-y-4">
<div class="space-y-2">
<Label :for="field.name" class="text-base">{{ label }}</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
v-model="field.state.value"
:type="showPassword ? 'text' : 'password'"
:placeholder="placeholder"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': error || isInvalid(field) }"
:autocomplete="autocomplete"
@blur="field.handleBlur"
@input="(e: Event) => {
field.handleChange((e.target as HTMLInputElement).value)
}"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<!-- Error Message -->
<p v-if="error" class="text-sm text-destructive">
{{ errorMessage }}
</p>
<!-- Validation Error from Zod -->
<p v-else-if="isInvalid(field)" class="text-sm text-destructive">
{{ field.state.meta.errors?.[0]?.message }}
</p>
<!-- Password Strength Indicator -->
<div v-if="validateFormat && field.state.value" class="space-y-2 pt-1">
<div class="flex items-center gap-3">
<div class="h-1 flex-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full transition-all duration-300 ease-in-out"
:style="{
width: `${(passwordStrength.level / 4) * 100}%`,
backgroundColor: passwordStrength.color,
}"
></div>
</div>
<span
class="min-w-[60px] text-right text-xs font-semibold"
:style="{ color: passwordStrength.color }"
>
{{ passwordStrength.text }}
</span>
</div>
<p class="text-xs text-muted-foreground">
Use at least 8 characters with uppercase, lowercase, and numbers.
</p>
</div>
</div>
<!-- Buttons -->
<div class="flex flex-col gap-3">
<Button
type="submit"
size="lg"
class="h-12 w-full text-base font-semibold"
:disabled="!field.state.value || loading"
>
<Loader2 v-if="loading" :size="20" class="mr-2 animate-spin" />
{{ buttonText }}
</Button>
<Button
v-if="!hideBackButton"
type="button"
variant="outline"
size="lg"
class="h-12 w-full text-base"
@click="handleBack"
>
<ChevronLeft :size="20" class="mr-2" />
{{ backButtonText }}
</Button>
</div>
</div>
</template>
</form.Field>
</form>
</template>

View File

@ -0,0 +1 @@
export { default as PasswordForm } from './PasswordForm.vue'

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
import type { DialogRootEmits, DialogRootProps } from "reka-ui"
import { DialogRoot, useForwardPropsEmits } from "reka-ui"
const props = defineProps<DialogRootProps>()
const emits = defineEmits<DialogRootEmits>()
const forwarded = useForwardPropsEmits(props, emits)
</script>
<template>
<DialogRoot v-bind="forwarded">
<slot />
</DialogRoot>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { DialogCloseProps } from "reka-ui"
import { DialogClose } from "reka-ui"
const props = defineProps<DialogCloseProps>()
</script>
<template>
<DialogClose v-bind="props">
<slot />
</DialogClose>
</template>

View File

@ -0,0 +1,46 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
/>
<DialogContent
v-bind="forwarded"
:class="
cn(
'fixed left-1/2 top-1/2 z-50 grid w-full max-w-lg -translate-x-1/2 -translate-y-1/2 gap-4 border bg-background p-6 shadow-lg duration-200 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 data-[state=closed]:zoom-out-95 data-[state=open]:zoom-in-95 data-[state=closed]:slide-out-to-left-1/2 data-[state=closed]:slide-out-to-top-[48%] data-[state=open]:slide-in-from-left-1/2 data-[state=open]:slide-in-from-top-[48%] sm:rounded-lg',
props.class,
)"
>
<slot />
<DialogClose
class="absolute right-4 top-4 rounded-sm opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2 disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogPortal>
</template>

View File

@ -0,0 +1,22 @@
<script setup lang="ts">
import type { DialogDescriptionProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogDescription, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogDescriptionProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogDescription
v-bind="forwardedProps"
:class="cn('text-sm text-muted-foreground', props.class)"
>
<slot />
</DialogDescription>
</template>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{ class?: HTMLAttributes["class"] }>()
</script>
<template>
<div
:class="
cn(
'flex flex-col-reverse sm:flex-row sm:justify-end sm:gap-x-2',
props.class,
)
"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,16 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
}>()
</script>
<template>
<div
:class="cn('flex flex-col gap-y-1.5 text-center sm:text-left', props.class)"
>
<slot />
</div>
</template>

View File

@ -0,0 +1,55 @@
<script setup lang="ts">
import type { DialogContentEmits, DialogContentProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { X } from "lucide-vue-next"
import {
DialogClose,
DialogContent,
DialogOverlay,
DialogPortal,
useForwardPropsEmits,
} from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogContentProps & { class?: HTMLAttributes["class"] }>()
const emits = defineEmits<DialogContentEmits>()
const delegatedProps = reactiveOmit(props, "class")
const forwarded = useForwardPropsEmits(delegatedProps, emits)
</script>
<template>
<DialogPortal>
<DialogOverlay
class="fixed inset-0 z-50 grid place-items-center overflow-y-auto bg-black/80 data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0"
>
<DialogContent
:class="
cn(
'relative z-50 grid w-full max-w-lg my-8 gap-4 border border-border bg-background p-6 shadow-lg duration-200 sm:rounded-lg md:w-full',
props.class,
)
"
v-bind="forwarded"
@pointer-down-outside="(event) => {
const originalEvent = event.detail.originalEvent;
const target = originalEvent.target as HTMLElement;
if (originalEvent.offsetX > target.clientWidth || originalEvent.offsetY > target.clientHeight) {
event.preventDefault();
}
}"
>
<slot />
<DialogClose
class="absolute top-4 right-4 p-0.5 transition-colors rounded-md hover:bg-secondary"
>
<X class="w-4 h-4" />
<span class="sr-only">Close</span>
</DialogClose>
</DialogContent>
</DialogOverlay>
</DialogPortal>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import type { DialogTitleProps } from "reka-ui"
import type { HTMLAttributes } from "vue"
import { reactiveOmit } from "@vueuse/core"
import { DialogTitle, useForwardProps } from "reka-ui"
import { cn } from "@/lib/utils"
const props = defineProps<DialogTitleProps & { class?: HTMLAttributes["class"] }>()
const delegatedProps = reactiveOmit(props, "class")
const forwardedProps = useForwardProps(delegatedProps)
</script>
<template>
<DialogTitle
v-bind="forwardedProps"
:class="
cn(
'text-lg font-semibold leading-none tracking-tight',
props.class,
)
"
>
<slot />
</DialogTitle>
</template>

View File

@ -0,0 +1,12 @@
<script setup lang="ts">
import type { DialogTriggerProps } from "reka-ui"
import { DialogTrigger } from "reka-ui"
const props = defineProps<DialogTriggerProps>()
</script>
<template>
<DialogTrigger v-bind="props">
<slot />
</DialogTrigger>
</template>

View File

@ -0,0 +1,9 @@
export { default as Dialog } from "./Dialog.vue"
export { default as DialogClose } from "./DialogClose.vue"
export { default as DialogContent } from "./DialogContent.vue"
export { default as DialogDescription } from "./DialogDescription.vue"
export { default as DialogFooter } from "./DialogFooter.vue"
export { default as DialogHeader } from "./DialogHeader.vue"
export { default as DialogScrollContent } from "./DialogScrollContent.vue"
export { default as DialogTitle } from "./DialogTitle.vue"
export { default as DialogTrigger } from "./DialogTrigger.vue"

View File

@ -22,3 +22,25 @@ const modelValue = useVModel(props, "modelValue", emits, {
<template>
<input v-model="modelValue" :class="cn('flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)">
</template>
<style scoped>
/* Hide default browser password reveal button */
input::-ms-reveal,
input::-ms-clear {
display: none;
}
/* Hide password reveal button in Edge */
input[type="password"]::-ms-reveal {
display: none;
}
/* Hide password reveal button in Chrome/Safari */
input[type="password"]::-webkit-contacts-auto-fill-button,
input[type="password"]::-webkit-credentials-auto-fill-button {
visibility: hidden;
pointer-events: none;
position: absolute;
right: 0;
}
</style>

View File

@ -0,0 +1,24 @@
<script setup lang="ts">
import type { HTMLAttributes } from "vue"
import { useVModel } from "@vueuse/core"
import { cn } from "@/lib/utils"
const props = defineProps<{
class?: HTMLAttributes["class"]
defaultValue?: string | number
modelValue?: string | number
}>()
const emits = defineEmits<{
(e: "update:modelValue", payload: string | number): void
}>()
const modelValue = useVModel(props, "modelValue", emits, {
passive: true,
defaultValue: props.defaultValue,
})
</script>
<template>
<textarea v-model="modelValue" :class="cn('flex min-h-[60px] w-full rounded-md border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50', props.class)" />
</template>

View File

@ -0,0 +1 @@
export { default as Textarea } from "./Textarea.vue"

2
src/composables/index.ts Normal file
View File

@ -0,0 +1,2 @@
export { useAuthRouting } from './useAuthRouting'
export { useMobile } from './useMobile'

View File

@ -0,0 +1,23 @@
import { useAuthStore } from '@/stores'
export function useAuthRouting() {
const authStore = useAuthStore()
const goToCreate = () => {
authStore.setState('create')
}
const goToRecover = () => {
authStore.setState('recovery')
}
const goBackToWelcome = () => {
authStore.setState('welcome')
}
return {
goToCreate,
goToRecover,
goBackToWelcome,
}
}

View File

@ -6,69 +6,70 @@ import { useMediaQuery } from '@vueuse/core'
*/
export function useMobile() {
const isMobile = useMediaQuery('(max-width: 768px)')
return isMobile
}
/**
* Detect if device is iOS
*/
export function useIsIOS() {
return computed(() => {
const isIOS = computed(() => {
if (typeof window === 'undefined') return false
return /iPhone|iPad|iPod/i.test(navigator.userAgent)
})
}
/**
* Get safe area insets for notched devices
*/
export function useSafeArea() {
/**
* Get safe area insets for notched devices
*/
function useSafeArea() {
return {
top: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)') ||
'0'
)
}),
bottom: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'env(safe-area-inset-bottom)'
) || '0'
)
}),
}
}
/**
* Prevent pull-to-refresh on mobile browsers
*/
function usePreventPullToRefresh() {
if (typeof window === 'undefined') return
let touchStartY = 0
document.addEventListener(
'touchstart',
e => {
touchStartY = e.touches[0]?.clientY ?? 0
},
{ passive: false }
)
document.addEventListener(
'touchmove',
e => {
const touchY = e.touches[0]?.clientY ?? 0
const touchYDelta = touchY - touchStartY
// Prevent pull-to-refresh if scrolling up at the top
if (touchYDelta > 0 && window.scrollY === 0) {
e.preventDefault()
}
},
{ passive: false }
)
}
return {
top: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue('env(safe-area-inset-top)') ||
'0'
)
}),
bottom: computed(() => {
if (typeof window === 'undefined') return 0
return parseInt(
getComputedStyle(document.documentElement).getPropertyValue(
'env(safe-area-inset-bottom)'
) || '0'
)
}),
isMobile,
isIOS,
useSafeArea,
usePreventPullToRefresh,
}
}
/**
* Prevent pull-to-refresh on mobile browsers
*/
export function usePreventPullToRefresh() {
if (typeof window === 'undefined') return
let touchStartY = 0
document.addEventListener(
'touchstart',
e => {
touchStartY = e.touches[0]?.clientY ?? 0
},
{ passive: false }
)
document.addEventListener(
'touchmove',
e => {
const touchY = e.touches[0]?.clientY ?? 0
const touchYDelta = touchY - touchStartY
// Prevent pull-to-refresh if scrolling up at the top
if (touchYDelta > 0 && window.scrollY === 0) {
e.preventDefault()
}
},
{ passive: false }
)
}

View File

@ -8,7 +8,7 @@ const router = createRouter({
// Navigation guards
router.beforeEach((to, _from, next) => {
const hasWallet = false // useNeptuneStore().hasWallet
const hasWallet = true // useNeptuneStore().hasWallet
if (to.meta.requiresAuth) {
if (!hasWallet) {

View File

@ -14,10 +14,14 @@ export const routes: RouteRecordRaw[] = [
component: Layout,
meta: { requiresAuth: true },
children: [
// { path: '/home', name: 'home', component: Pages.HomePage },
{ path: '/wallet', name: 'wallet', component: Pages.WalletPage },
// { path: '/history', name: 'history', component: () => import('@/views/HistoryView.vue') },
// { path: '/settings', name: 'settings', component: () => import('@/views/SettingsView.vue') },
{ path: '/utxo', name: 'utxo', component: Pages.UTXOPage },
{ path: 'wallet', name: 'wallet', component: Pages.WalletPage },
{
path: '/transaction-history',
name: 'transaction-history',
component: Pages.TransactionHistoryPage,
},
{ path: '/settings', name: 'settings', component: Pages.SettingsPage },
],
},
{

View File

@ -1,51 +1,22 @@
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export type AuthState = 'onboarding' | 'login' | 'create' | 'recovery' | 'confirm' | 'complete'
export type AuthState = 'welcome' | 'create' | 'recovery'
export const useAuthStore = defineStore('auth', () => {
// State
const currentState = ref<AuthState>('onboarding')
const previousState = ref<AuthState | null>(null)
const currentState = ref<AuthState>('welcome')
// Getters
const getCurrentState = computed(() => currentState.value)
const getPreviousState = computed(() => previousState.value)
// Actions
const setState = (state: AuthState) => {
previousState.value = currentState.value
currentState.value = state
}
const goToCreate = () => {
setState('create')
}
const goToLogin = () => {
setState('login')
}
const goToRecover = () => {
setState('recovery')
}
const goBack = () => {
if (previousState.value) {
setState(previousState.value)
} else {
setState('onboarding')
}
}
return {
currentState,
getCurrentState,
getPreviousState,
setState,
goToCreate,
goToLogin,
goToRecover,
goBack,
}
})

View File

@ -2,75 +2,54 @@
import { computed } from 'vue'
import { useAuthStore } from '@/stores'
import { Toaster } from 'vue-sonner'
import { CreateWalletFlow, LoginTab, OnboardingTab, RecoverWalletFlow } from './components'
import { WelcomeTab, CreateWalletFlow, RecoverWalletFlow } from './components'
import { useAuthRouting } from '@/composables'
const authStore = useAuthStore()
const router = useRouter()
// const neptuneWallet = useNeptuneWallet()
const { goToCreate, goToRecover, goBackToWelcome } = useAuthRouting()
const currentState = computed(() => authStore.getCurrentState)
const handleGoToCreate = () => {
authStore.goToCreate()
goToCreate()
}
const handleGoToRecover = () => {
authStore.goToRecover()
}
const handlePasswordSubmit = async (password: string) => {
const loginRef = document.querySelector('login-tab') as any
try {
// TODO: Decrypt keystore with password
// await neptuneWallet.decryptKeystore(password)
// Mock success for now
router.push({ name: 'home' })
} catch (err) {
if (loginRef) {
loginRef.setError(true)
loginRef.setLoading(false)
}
console.error('Failed to unlock wallet:', err)
}
goToRecover()
}
const handleAccessWallet = () => {
router.push({ name: 'wallet' })
}
const handleCancel = () => {
authStore.goBack()
const handleGoToWelcome = () => {
goBackToWelcome()
}
</script>
<template>
<div class="min-h-screen">
<!-- Onboarding: Welcome screen -->
<OnboardingTab
v-if="currentState === 'onboarding'"
<!-- Welcome: Welcome screen with login -->
<WelcomeTab
v-if="currentState === 'welcome'"
@go-to-create="handleGoToCreate"
@go-to-recover="handleGoToRecover"
/>
<!-- Login: Unlock existing wallet -->
<LoginTab
v-else-if="currentState === 'login'"
@go-to-create="handleGoToCreate"
@submit="handlePasswordSubmit"
@go-to-welcome="handleGoToWelcome"
/>
<!-- Create: New wallet creation flow -->
<CreateWalletFlow
v-else-if="currentState === 'create'"
@navigate-to-recover="handleGoToRecover"
@go-to-recover="handleGoToRecover"
@access-wallet="handleAccessWallet"
@go-to-welcome="handleGoToWelcome"
/>
<!-- Recovery: Recover wallet from seed phrase -->
<RecoverWalletFlow
v-else-if="currentState === 'recovery'"
@cancel="handleCancel"
@cancel="handleGoToWelcome"
@access-wallet="handleAccessWallet"
/>

View File

@ -87,23 +87,16 @@ const handleNext = () => {
if (correctCount.value >= totalQuestions) {
emit('next')
} else {
setTimeout(() => {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}, 800)
// Move to next question
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) quizData.value = newQuiz
}
} else {
setTimeout(() => {
showResult.value = false
selectedAnswer.value = ''
const newQuiz = generateQuiz()
if (newQuiz) {
quizData.value = newQuiz
}
}, 1500)
}
}
@ -125,9 +118,7 @@ onMounted(() => {
<!-- Header -->
<div class="space-y-2 text-center">
<h1 class="text-2xl font-bold text-foreground">Confirm Recovery Phrase</h1>
<p class="text-sm text-muted-foreground">
Select the correct word for each position
</p>
<p class="text-sm text-muted-foreground">Select the correct word for each position</p>
</div>
<Separator />
@ -159,7 +150,7 @@ onMounted(() => {
<!-- Quiz Section -->
<div v-if="quizData" class="space-y-5">
<!-- Question Card -->
<Card class="border-2 border-primary/30 bg-gradient-to-br from-primary/5 to-accent/5">
<Card class="border-2 border-primary/30 bg-linear-to-br from-primary/5 to-accent/5">
<CardContent class="py-8">
<h2 class="text-center text-xl font-bold text-foreground">
Select word
@ -190,10 +181,11 @@ onMounted(() => {
:class="{
'border-primary bg-primary/10 shadow-lg': selectedAnswer === option && !showResult,
'border-green-500 bg-green-500/10':
showResult && option === quizData.correctWord,
showResult && isCorrect && option === quizData.correctWord,
'border-destructive bg-destructive/10':
showResult && selectedAnswer === option && option !== quizData.correctWord,
'border-border hover:border-primary/50 hover:bg-accent': !selectedAnswer || (selectedAnswer !== option && !showResult),
'border-border hover:border-primary/50 hover:bg-accent':
!selectedAnswer || (selectedAnswer !== option && !showResult),
}"
:disabled="showResult"
@click="handleAnswerSelect(option)"
@ -204,9 +196,12 @@ onMounted(() => {
class="flex h-8 w-8 items-center justify-center rounded-full text-xs font-bold transition-colors"
:class="{
'bg-primary text-primary-foreground': selectedAnswer === option && !showResult,
'bg-green-500 text-white': showResult && option === quizData.correctWord,
'bg-destructive text-destructive-foreground': showResult && selectedAnswer === option && option !== quizData.correctWord,
'bg-muted text-muted-foreground': !selectedAnswer || (selectedAnswer !== option && !showResult),
'bg-green-500 text-white':
showResult && isCorrect && option === quizData.correctWord,
'bg-destructive text-destructive-foreground':
showResult && selectedAnswer === option && option !== quizData.correctWord,
'bg-muted text-muted-foreground':
!selectedAnswer || (selectedAnswer !== option && !showResult),
}"
>
{{ String.fromCharCode(65 + index) }}
@ -217,7 +212,7 @@ onMounted(() => {
<!-- Check/X Icon -->
<CheckCircle2
v-if="showResult && option === quizData.correctWord"
v-if="showResult && isCorrect && option === quizData.correctWord"
:size="24"
class="text-green-500"
/>
@ -232,22 +227,15 @@ onMounted(() => {
<!-- Result Message -->
<div v-if="showResult" class="animate-in fade-in slide-in-from-top-4 duration-500">
<Alert
:variant="isCorrect ? 'default' : 'destructive'"
class="border-2"
>
<Alert :variant="isCorrect ? 'default' : 'destructive'" class="border-2">
<CheckCircle2 v-if="isCorrect" :size="20" class="text-green-500" />
<XCircle v-else :size="20" class="text-destructive" />
<AlertDescription class="text-base font-medium">
<span v-if="isCorrect && correctCount + 1 >= totalQuestions">
Perfect! You've verified your recovery phrase. 🎉
</span>
<span v-else-if="isCorrect">
Correct! Moving to next question...
</span>
<span v-else>
That's not correct. Please try again.
</span>
<span v-else-if="isCorrect"> Correct! Moving to next question... </span>
<span v-else> That's not correct. Please try again. </span>
</AlertDescription>
</Alert>
</div>
@ -256,7 +244,7 @@ onMounted(() => {
<!-- Action Buttons -->
<div class="flex gap-3">
<Button
v-if="!showResult || !isCorrect || (isCorrect && correctCount + 1 < totalQuestions)"
v-if="!showResult || !isCorrect"
variant="outline"
size="lg"
class="flex-1 gap-2"
@ -266,12 +254,12 @@ onMounted(() => {
Back
</Button>
<Button
v-if="showResult && isCorrect && correctCount + 1 >= totalQuestions"
v-if="showResult && isCorrect"
size="lg"
class="flex-1 gap-2 text-base font-semibold"
@click="handleNext"
>
Continue
{{ correctCount + 1 >= totalQuestions ? 'Continue' : 'Next Question' }}
<Check :size="18" />
</Button>
</div>

View File

@ -1,18 +1,24 @@
<script setup lang="ts">
import { ref, computed, h } from 'vue'
import { useForm } from '@tanstack/vue-form'
import { ref, computed } from 'vue'
import { z } from 'zod'
import { Eye, EyeOff, Lock, Check, X, ArrowLeft, Shield, KeyRound } from 'lucide-vue-next'
import { Eye, EyeOff, Lock, Check, X, Shield, KeyRound } from 'lucide-vue-next'
import { ThemeToggle } from '@/components/ui/theme-toggle'
const emit = defineEmits<{
next: [password: string]
navigateToRecover: []
goToRecover: []
goToWelcome: []
}>()
// Form state
const password = ref('')
const confirmPassword = ref('')
const showPassword = ref(false)
const showConfirmPassword = ref(false)
const passwordTouched = ref(false)
const confirmPasswordTouched = ref(false)
// Validation schema
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
@ -20,35 +26,30 @@ const passwordSchema = z
.regex(/[a-z]/, 'Must contain at least one lowercase letter')
.regex(/[0-9]/, 'Must contain at least one number')
const form = useForm({
defaultValues: {
password: '',
confirmPassword: '',
},
validators: {
onChange: z.object({
password: passwordSchema,
confirmPassword: z.string(),
}),
},
onSubmit: async ({ value }) => {
if (value.password === value.confirmPassword) {
emit('next', value.password)
// Validate password
const passwordError = computed(() => {
if (!passwordTouched.value || !password.value) return ''
try {
passwordSchema.parse(password.value)
return ''
} catch (error) {
if (error instanceof z.ZodError) {
return error.issues[0]?.message || ''
}
},
return ''
}
})
const passwordStrength = computed(() => {
const password = form.state.values.password
if (!password) return { level: 0, text: '', color: '', width: '0%' }
if (!password.value) return { level: 0, text: '', color: '', width: '0%' }
let strength = 0
const checks = {
length: password.length >= 8,
uppercase: /[A-Z]/.test(password),
lowercase: /[a-z]/.test(password),
number: /[0-9]/.test(password),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password),
length: password.value.length >= 8,
uppercase: /[A-Z]/.test(password.value),
lowercase: /[a-z]/.test(password.value),
number: /[0-9]/.test(password.value),
special: /[!@#$%^&*(),.?":{}|<>]/.test(password.value),
}
strength = Object.values(checks).filter(Boolean).length
@ -60,32 +61,42 @@ const passwordStrength = computed(() => {
})
const isPasswordMatch = computed(() => {
const { password, confirmPassword } = form.state.values
if (!confirmPassword) return true
return password === confirmPassword
if (!confirmPassword.value) return true
return password.value === confirmPassword.value
})
const canProceed = computed(() => {
const { password, confirmPassword } = form.state.values
return (
password.length >= 8 &&
confirmPassword.length >= 8 &&
password.value.length >= 8 &&
confirmPassword.value.length >= 8 &&
isPasswordMatch.value &&
passwordStrength.value.level >= 2
passwordStrength.value.level >= 2 &&
!passwordError.value
)
})
const handleIHaveWallet = () => {
emit('navigateToRecover')
const handleSubmit = () => {
passwordTouched.value = true
confirmPasswordTouched.value = true
if (canProceed.value) {
emit('next', password.value)
}
}
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
const handleIHaveWallet = () => {
emit('goToRecover')
}
const handleGoToWelcome = () => {
emit('goToWelcome')
}
</script>
<template>
<div class="relative flex min-h-screen flex-col bg-gradient-to-br from-background via-background to-primary/5 p-4">
<div
class="relative flex min-h-screen flex-col bg-linear-to-br from-background via-background to-primary/5 p-4"
>
<!-- Theme Toggle -->
<div class="absolute right-4 top-4 z-10">
<ThemeToggle />
@ -93,9 +104,7 @@ function isInvalid(field: any) {
<!-- Back Button -->
<div class="absolute left-4 top-4 z-10">
<Button variant="ghost" size="icon" @click="handleIHaveWallet">
<ArrowLeft :size="20" />
</Button>
<ArrowLeftCommon @click="handleGoToWelcome" />
</div>
<!-- Content Container -->
@ -103,130 +112,115 @@ function isInvalid(field: any) {
<div class="w-full max-w-md space-y-8">
<!-- Header -->
<div class="flex flex-col items-center space-y-4 text-center">
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-accent shadow-xl">
<Shield :size="32" class="text-primary-foreground" />
</div>
<Logo />
<div class="space-y-2">
<h1 class="text-3xl font-bold tracking-tight text-foreground">
Create Password
</h1>
<p class="text-base text-muted-foreground">
Secure your new wallet
</p>
<h1 class="text-3xl font-bold tracking-tight text-foreground">Create Password</h1>
<p class="text-base text-muted-foreground">Secure your new wallet</p>
</div>
</div>
<!-- Form -->
<Card class="border-2 border-border/50 shadow-xl">
<CardContent class="p-6">
<form id="create-password-form" @submit.prevent="form.handleSubmit">
<form id="create-password-form" @submit.prevent="handleSubmit">
<div class="space-y-6">
<!-- Password Field -->
<form.Field name="password">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': isInvalid(field) }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<!-- Password Strength -->
<div v-if="field.state.value" class="space-y-2">
<div class="flex items-center gap-2">
<div class="h-2 flex-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full transition-all duration-300"
:class="passwordStrength.color"
:style="{ width: passwordStrength.width }"
/>
</div>
<span class="text-xs font-semibold" :class="`text-${passwordStrength.color.replace('bg-', '')}`">
{{ passwordStrength.text }}
</span>
</div>
</div>
<!-- Error Message -->
<p v-if="isInvalid(field)" class="text-sm text-destructive">
{{ field.state.meta.errors[0] }}
</p>
<div class="space-y-2">
<Label for="password" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': passwordError }"
@blur="passwordTouched = true"
/>
<div
class="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</div>
</template>
</form.Field>
</div>
<!-- Password Strength -->
<div v-if="password" class="space-y-2">
<div class="flex items-center gap-2">
<div class="h-2 flex-1 overflow-hidden rounded-full bg-muted">
<div
class="h-full transition-all duration-300"
:class="passwordStrength.color"
:style="{ width: passwordStrength.width }"
/>
</div>
<span
class="text-xs font-semibold"
:class="`text-${passwordStrength.color.replace('bg-', '')}`"
>
{{ passwordStrength.text }}
</span>
</div>
</div>
<!-- Error Message -->
<p v-if="passwordError" class="text-sm text-destructive">
{{ passwordError }}
</p>
</div>
<!-- Confirm Password Field -->
<form.Field name="confirmPassword">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Confirm Password</Label>
<div class="relative">
<KeyRound
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="Re-enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': field.state.value && !isPasswordMatch }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<!-- Password Match Indicator -->
<div v-if="field.state.value" class="flex items-center gap-2">
<Check v-if="isPasswordMatch" :size="16" class="text-green-500" />
<X v-else :size="16" class="text-destructive" />
<span class="text-sm" :class="isPasswordMatch ? 'text-green-500' : 'text-destructive'">
{{ isPasswordMatch ? 'Passwords match' : 'Passwords do not match' }}
</span>
</div>
<div class="space-y-2">
<Label for="confirmPassword" class="text-base">Confirm Password</Label>
<div class="relative">
<KeyRound
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
id="confirmPassword"
v-model="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="Re-enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': confirmPassword && !isPasswordMatch }"
@blur="confirmPasswordTouched = true"
/>
<div
class="absolute right-3 top-1/2 -translate-y-1/2 cursor-pointer text-muted-foreground transition-colors hover:text-foreground"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="20" />
<EyeOff v-else :size="20" />
</div>
</template>
</form.Field>
</div>
<!-- Password Match Indicator -->
<div v-if="confirmPassword" class="flex items-center gap-2">
<Check v-if="isPasswordMatch" :size="16" class="text-green-500" />
<X v-else :size="16" class="text-destructive" />
<span
class="text-sm"
:class="isPasswordMatch ? 'text-green-500' : 'text-destructive'"
>
{{ isPasswordMatch ? 'Passwords match' : 'Passwords do not match' }}
</span>
</div>
</div>
<!-- Security Info -->
<Alert>
<Shield :size="16" />
<AlertDescription class="text-xs">
Use at least 8 characters with uppercase, lowercase, and numbers for a strong password.
Use at least 8 characters with uppercase, lowercase, and numbers for a strong
password.
</AlertDescription>
</Alert>
@ -248,7 +242,11 @@ function isInvalid(field: any) {
<div class="text-center">
<p class="text-sm text-muted-foreground">
Already have a wallet?
<Button variant="link" class="p-0 text-sm font-semibold text-primary" @click="handleIHaveWallet">
<Button
variant="link"
class="p-0 text-sm font-semibold text-primary"
@click="handleIHaveWallet"
>
Import wallet
</Button>
</p>

View File

@ -6,8 +6,9 @@ import ConfirmSeedStep from './ConfirmSeedStep.vue'
import WalletCreatedStep from './WalletCreatedStep.vue'
const emit = defineEmits<{
navigateToRecover: []
goToRecover: []
accessWallet: []
goToWelcome: []
}>()
// TODO: Import useNeptuneWallet composable
@ -18,8 +19,12 @@ const seedPhrase = ref<string[]>([])
const password = ref('')
const isLoading = ref(false)
const handleNavigateToRecover = () => {
emit('navigateToRecover')
const handleGoToRecover = () => {
emit('goToRecover')
}
const handleGoToWelcome = () => {
emit('goToWelcome')
}
const handleNextFromPassword = async (pwd: string) => {
@ -90,14 +95,6 @@ const handleAccessWallet = async () => {
isLoading.value = false
}
}
const handleCreateAnother = () => {
step.value = 1
seedPhrase.value = []
password.value = ''
// TODO: Clear wallet
// clearWallet()
}
</script>
<template>
@ -106,13 +103,14 @@ const handleCreateAnother = () => {
<CreatePasswordStep
v-if="step === 1"
@next="handleNextFromPassword"
@navigate-to-recover="handleNavigateToRecover"
@go-to-recover="handleGoToRecover"
@go-to-welcome="handleGoToWelcome"
/>
<!-- Step 2: Display Seed Phrase -->
<div
v-else-if="step === 2"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
class="flex min-h-screen items-center justify-center bg-linear-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
@ -128,7 +126,7 @@ const handleCreateAnother = () => {
<!-- Step 3: Confirm Seed Phrase -->
<div
v-else-if="step === 3"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
class="flex min-h-screen items-center justify-center bg-linear-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
@ -144,7 +142,7 @@ const handleCreateAnother = () => {
<!-- Step 4: Wallet Created Success -->
<div
v-else-if="step === 4"
class="flex min-h-screen items-center justify-center bg-gradient-to-br from-background via-background to-primary/5 p-4"
class="flex min-h-screen items-center justify-center bg-linear-to-br from-background via-background to-primary/5 p-4"
>
<Card class="w-full max-w-2xl border-2 border-border/50 shadow-xl">
<CardContent class="p-6 md:p-8">
@ -152,7 +150,6 @@ const handleCreateAnother = () => {
:seed-phrase="seedPhrase"
:password="password"
@access-wallet="handleAccessWallet"
@create-another="handleCreateAnother"
/>
</CardContent>
</Card>

View File

@ -130,30 +130,6 @@ const handleCopySeed = async () => {
</AlertDescription>
</Alert>
<!-- Info Card -->
<Card class="border-primary/20 bg-primary/5">
<CardContent class="p-4">
<ul class="space-y-2 text-xs text-muted-foreground">
<li class="flex items-start gap-2">
<span class="mt-0.5 text-primary"></span>
<span>Write these words down on paper in the exact order</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-primary"></span>
<span>Store the paper in a secure location</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-primary"></span>
<span>Never share your recovery phrase with anyone</span>
</li>
<li class="flex items-start gap-2">
<span class="mt-0.5 text-primary"></span>
<span>Neptune support will never ask for your seed phrase</span>
</li>
</ul>
</CardContent>
</Card>
<!-- Action Buttons -->
<div class="flex gap-3">
<Button variant="outline" size="lg" class="flex-1 gap-2" @click="handleBack">

View File

@ -20,10 +20,6 @@ const handleAccessWallet = () => {
emit('accessWallet')
}
const handleCreateAnother = () => {
emit('createAnother')
}
onMounted(() => {
showConfetti.value = true
})
@ -34,9 +30,9 @@ onMounted(() => {
<!-- Success Animation -->
<div class="flex justify-center">
<div class="relative">
<div class="absolute -inset-4 animate-pulse rounded-full bg-green-500/20 blur-xl"></div>
<div class="absolute -inset-4 rounded-full bg-green-500/20 blur-xl"></div>
<div
class="relative rounded-full bg-gradient-to-br from-green-400 to-emerald-600 p-6 shadow-2xl"
class="relative rounded-full bg-linear-to-br from-green-400 to-emerald-600 p-6 shadow-2xl"
>
<CheckCircle2 :size="80" class="text-white" />
</div>
@ -55,24 +51,15 @@ onMounted(() => {
<div class="space-y-6">
<!-- Logo -->
<div class="flex justify-center">
<div class="relative">
<div
class="absolute -inset-2 animate-pulse rounded-3xl bg-gradient-to-r from-primary via-accent to-primary opacity-30 blur-2xl"
></div>
<img
src="@/assets/imgs/neptune_logo.jpg"
alt="Neptune Logo"
class="relative h-32 w-32 rounded-3xl object-cover shadow-2xl ring-4 ring-background"
/>
</div>
<Logo />
</div>
<!-- Features Grid -->
<div class="grid gap-3">
<Card class="border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-transparent">
<Card class="border-2 border-green-500/20 bg-linear-to-br from-green-500/5 to-transparent">
<CardContent class="flex items-center gap-4 p-4">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-primary/10">
<Shield :size="24" class="text-primary" />
<Shield />
</div>
<div class="flex-1">
<h3 class="font-semibold text-foreground">Secure & Private</h3>
@ -82,7 +69,7 @@ onMounted(() => {
</Card>
<Card
class="border-2 border-green-500/20 bg-gradient-to-br from-green-500/5 to-transparent"
class="border-2 border-green-500/20 bg-linear-to-br from-green-500/5 to-transparent"
>
<CardContent class="flex items-center gap-4 p-4">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-green-500/10">
@ -95,14 +82,14 @@ onMounted(() => {
</CardContent>
</Card>
<Card class="border-2 border-accent/20 bg-gradient-to-br from-accent/5 to-transparent">
<Card class="border-2 border-green-500/20 bg-linear-to-br from-green-500/5 to-transparent">
<CardContent class="flex items-center gap-4 p-4">
<div class="flex h-12 w-12 items-center justify-center rounded-xl bg-accent/10">
<Sparkles :size="24" class="text-accent-foreground" />
<Sparkles :size="24" />
</div>
<div class="flex-1">
<h3 class="font-semibold text-foreground">Ready to Use</h3>
<p class="text-xs text-muted-foreground">Start exploring Web3 now</p>
<p class="text-xs text-muted-foreground">Start exploring now</p>
</div>
</CardContent>
</Card>
@ -129,15 +116,6 @@ onMounted(() => {
Open My Wallet
<ArrowRight :size="20" class="ml-auto" />
</Button>
<Button
variant="ghost"
size="sm"
class="w-full text-sm text-muted-foreground hover:text-foreground"
@click="handleCreateAnother"
>
Create another wallet instead
</Button>
</div>
</div>
</template>

View File

@ -0,0 +1,5 @@
export { default as CreateWalletFlow } from './CreateWalletFlow.vue'
export { default as CreatePasswordStep } from './CreatePasswordStep.vue'
export { default as SeedPhraseDisplayStep } from './SeedPhraseDisplayStep.vue'
export { default as ConfirmSeedStep } from './ConfirmSeedStep.vue'
export { default as WalletCreatedStep } from './WalletCreatedStep.vue'

View File

@ -1,5 +1,5 @@
export { default as OnboardingTab } from './onboarding/OnboardingTab.vue'
export { default as LoginTab } from './login/LoginTab.vue'
export { default as CreateWalletFlow } from './create/CreateWalletFlow.vue'
export { default as RecoverWalletFlow } from './recovery/RecoverWalletFlow.vue'
export * from './welcome'
export * from './create'
export * from './onboarding'
export * from './recovery'
export * from './login'

View File

@ -1,89 +1,46 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@tanstack/vue-form'
import { z } from 'zod'
import { Eye, EyeOff, Lock, ArrowLeft, Loader2 } from 'lucide-vue-next'
import { ThemeToggle } from '@/components/ui/theme-toggle'
import { PasswordForm } from '@/components/commons'
const emit = defineEmits<{
goToCreate: []
submit: [password: string]
}>()
const showPassword = ref(false)
const isLoading = ref(false)
const error = ref(false)
const passwordSchema = z.string().min(1, 'Password is required')
const form = useForm({
defaultValues: {
password: '',
},
validators: {
onChange: z.object({
password: passwordSchema,
}),
},
onSubmit: async ({ value }) => {
try {
isLoading.value = true
error.value = false
emit('submit', value.password)
} catch (err) {
error.value = true
} finally {
isLoading.value = false
}
},
})
const handleSubmit = async (password: string) => {
try {
isLoading.value = true
error.value = false
emit('submit', password)
} catch (err) {
error.value = true
} finally {
isLoading.value = false
}
}
const handleNewWallet = () => {
emit('goToCreate')
}
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
defineExpose({
setError: (val: boolean) => {
error.value = val
},
setLoading: (val: boolean) => {
isLoading.value = false
},
})
</script>
<template>
<div class="relative flex min-h-screen flex-col bg-gradient-to-br from-background via-background to-primary/5 p-4">
<div class="relative flex min-h-screen flex-col bg-linear-to-br from-background via-background to-primary/5 p-4">
<!-- Theme Toggle -->
<div class="absolute right-4 top-4 z-10">
<ThemeToggle />
</div>
<!-- Back Button -->
<div class="absolute left-4 top-4 z-10">
<Button variant="ghost" size="icon" @click="handleNewWallet">
<ArrowLeft :size="20" />
</Button>
</div>
<!-- Content Container -->
<div class="flex flex-1 flex-col items-center justify-center">
<div class="w-full max-w-md space-y-8">
<!-- Header -->
<div class="flex flex-col items-center space-y-4 text-center">
<div class="relative">
<div class="flex h-20 w-20 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-accent shadow-xl ring-4 ring-background">
<img
src="@/assets/imgs/neptune_logo.jpg"
alt="Neptune"
class="h-16 w-16 rounded-xl object-cover"
/>
</div>
</div>
<Logo />
<div class="space-y-2">
<h1 class="text-3xl font-bold tracking-tight text-foreground">
@ -98,60 +55,16 @@ defineExpose({
<!-- Form -->
<Card class="border-2 border-border/50 shadow-xl">
<CardContent class="p-6">
<form id="login-form" @submit.prevent="form.handleSubmit">
<form.Field name="password">
<template #default="{ field }">
<div class="space-y-4">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showPassword ? 'text' : 'password'"
placeholder="Enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': error || isInvalid(field) }"
autocomplete="current-password"
@blur="field.handleBlur"
@input="(e) => {
field.handleChange((e.target as HTMLInputElement).value)
error = false
}"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<p v-if="error" class="text-sm text-destructive">
Incorrect password. Please try again.
</p>
</div>
<!-- Submit Button -->
<Button
type="submit"
size="lg"
class="h-12 w-full text-base font-semibold"
:disabled="!field.state.value || isLoading"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
{{ isLoading ? 'Unlocking...' : 'Unlock Wallet' }}
</Button>
</div>
</template>
</form.Field>
</form>
<PasswordForm
label="Password"
placeholder="Enter your password"
button-text="Unlock Wallet"
:loading="isLoading"
:error="error"
error-message="Incorrect password. Please try again."
autocomplete="current-password"
@submit="handleSubmit"
/>
</CardContent>
</Card>

View File

@ -0,0 +1 @@
export { default as LoginTab } from './LoginTab.vue'

View File

@ -17,7 +17,7 @@ const handleRecover = () => {
</script>
<template>
<div class="relative flex min-h-screen flex-col bg-gradient-to-br from-background via-background to-primary/5 p-4">
<div class="relative flex min-h-screen flex-col bg-linear-to-br from-background via-background to-primary/5 p-4">
<!-- Theme Toggle -->
<div class="absolute right-4 top-4 z-10">
<ThemeToggle />
@ -28,31 +28,19 @@ const handleRecover = () => {
<div class="w-full max-w-md space-y-8">
<!-- Logo and Branding -->
<div class="flex flex-col items-center space-y-6 text-center">
<div class="relative">
<div class="absolute -inset-1 animate-pulse rounded-full bg-gradient-to-r from-primary via-accent to-primary opacity-75 blur-xl"></div>
<div class="relative flex h-24 w-24 items-center justify-center rounded-full bg-gradient-to-br from-primary to-accent shadow-2xl ring-4 ring-background">
<img
src="@/assets/imgs/neptune_logo.jpg"
alt="Neptune"
class="h-20 w-20 rounded-full object-cover"
/>
</div>
</div>
<Logo />
<div class="space-y-2">
<h1 class="text-4xl font-bold tracking-tight text-foreground">
Neptune Wallet
Neptune Privacy
</h1>
<p class="text-lg text-muted-foreground">
Your Gateway to Web3
</p>
</div>
</div>
<!-- Action Cards -->
<div class="space-y-4">
<!-- Create New Wallet Card -->
<Card class="border-2 border-primary/20 bg-gradient-to-br from-primary/5 to-accent/5 transition-all hover:border-primary/40 hover:shadow-lg">
<Card class="border-2 border-primary/20 bg-linear-to-br from-primary/5 to-accent/5 transition-all hover:border-primary/40 hover:shadow-lg">
<CardContent class="p-6">
<button
class="w-full space-y-3 text-left"
@ -96,7 +84,7 @@ const handleRecover = () => {
<div class="rounded-xl border border-border/50 bg-muted/30 p-4 backdrop-blur-sm">
<p class="text-center text-sm text-muted-foreground">
<Wallet :size="16" class="mr-2 inline" />
Neptune is a secure, decentralized wallet that puts you in control
Neptune Privacy is a secure, decentralized wallet that puts you in control
</p>
</div>
</div>

View File

@ -0,0 +1 @@
export { default as OnboardingTab } from './OnboardingTab.vue'

View File

@ -1,8 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { useForm } from '@tanstack/vue-form'
import { ref, computed } from 'vue'
import { z } from 'zod'
import { ChevronLeft, Eye, EyeOff, Lock, ArrowLeft, Loader2, KeyRound, Shield } from 'lucide-vue-next'
import { ChevronLeft, Eye, EyeOff, Lock, Loader2, KeyRound, Shield } from 'lucide-vue-next'
import { ThemeToggle } from '@/components/ui/theme-toggle'
import RecoverSeedInput from './RecoverSeedInput.vue'
@ -22,46 +21,43 @@ const showConfirmPassword = ref(false)
const isLoading = ref(false)
const isSeedPhraseValid = ref(true)
// Form state
const password = ref('')
const confirmPassword = ref('')
const passwordTouched = ref(false)
const confirmPasswordTouched = ref(false)
// Validation schema
const passwordSchema = z
.string()
.min(8, 'Password must be at least 8 characters')
const passwordForm = useForm({
defaultValues: {
password: '',
confirmPassword: '',
},
validators: {
onChange: z.object({
password: passwordSchema,
confirmPassword: z.string(),
}),
},
onSubmit: async ({ value }) => {
if (value.password !== value.confirmPassword) {
return
// Validate password
const passwordError = computed(() => {
if (!passwordTouched.value || !password.value) return ''
try {
passwordSchema.parse(password.value)
return ''
} catch (error) {
if (error instanceof z.ZodError) {
return error.issues[0]?.message || ''
}
try {
isLoading.value = true
// TODO: Recover wallet from seed
// const result = await recoverWalletFromSeed(seedPhrase.value)
// if (result.address) {
// await createKeystore(seedPhrase.value.join(' '), value.password)
// emit('accessWallet')
// }
// Mock success for now
emit('accessWallet')
} catch (err) {
if (err instanceof Error && err.message.includes('Invalid seed phrase')) {
isSeedPhraseValid.value = false
}
console.error('Failed to recover wallet:', err)
} finally {
isLoading.value = false
}
},
return ''
}
})
const isPasswordMatch = computed(() => {
if (!confirmPassword.value) return true
return password.value === confirmPassword.value
})
const canSubmit = computed(() => {
return (
password.value.length >= 8 &&
confirmPassword.value.length >= 8 &&
isPasswordMatch.value &&
!passwordError.value
)
})
const handleSeedWordsUpdate = (words: string[]) => {
@ -79,35 +75,53 @@ const handleContinue = () => {
const handlePasswordBack = () => {
showPasswordForm.value = false
passwordForm.reset()
password.value = ''
confirmPassword.value = ''
passwordTouched.value = false
confirmPasswordTouched.value = false
}
const handleCancel = () => {
emit('cancel')
showPasswordForm.value = false
seedPhrase.value = []
passwordForm.reset()
password.value = ''
confirmPassword.value = ''
passwordTouched.value = false
confirmPasswordTouched.value = false
isSeedPhraseValid.value = true
}
function isInvalid(field: any) {
return field.state.meta.isTouched && !field.state.meta.isValid
}
const handleSubmit = async () => {
passwordTouched.value = true
confirmPasswordTouched.value = true
const isPasswordMatch = () => {
const { password, confirmPassword } = passwordForm.state.values
if (!confirmPassword) return true
return password === confirmPassword
}
if (!canSubmit.value) return
const canSubmit = () => {
const { password, confirmPassword } = passwordForm.state.values
return password.length >= 8 && confirmPassword.length >= 8 && isPasswordMatch()
try {
isLoading.value = true
// TODO: Recover wallet from seed
// const result = await recoverWalletFromSeed(seedPhrase.value)
// if (result.address) {
// await createKeystore(seedPhrase.value.join(' '), password.value)
// emit('accessWallet')
// }
// Mock success for now
emit('accessWallet')
} catch (err) {
if (err instanceof Error && err.message.includes('Invalid seed phrase')) {
isSeedPhraseValid.value = false
}
console.error('Failed to recover wallet:', err)
} finally {
isLoading.value = false
}
}
</script>
<template>
<div class="relative flex min-h-screen flex-col bg-gradient-to-br from-background via-background to-primary/5 p-4">
<div class="relative flex min-h-screen flex-col bg-linear-to-br from-background via-background to-primary/5 p-4">
<!-- Theme Toggle -->
<div class="absolute right-4 top-4 z-10">
<ThemeToggle />
@ -115,9 +129,7 @@ const canSubmit = () => {
<!-- Back Button -->
<div class="absolute left-4 top-4 z-10">
<Button variant="ghost" size="icon" @click="handleCancel">
<ArrowLeft :size="20" />
</Button>
<ArrowLeftCommon @click="handleCancel" />
</div>
<!-- Content Container -->
@ -125,7 +137,7 @@ const canSubmit = () => {
<div class="w-full max-w-md space-y-8">
<!-- Header -->
<div class="flex flex-col items-center space-y-4 text-center">
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-gradient-to-br from-primary to-accent shadow-xl">
<div class="flex h-16 w-16 items-center justify-center rounded-2xl bg-linear-to-br from-primary to-accent shadow-xl">
<KeyRound :size="32" class="text-primary-foreground" />
</div>
@ -175,7 +187,7 @@ const canSubmit = () => {
</div>
<!-- Password Step -->
<form v-else id="recover-password-form" @submit.prevent="passwordForm.handleSubmit">
<form v-else id="recover-password-form" @submit.prevent="handleSubmit">
<div class="space-y-6">
<!-- Info Alert -->
<Alert class="border-2 border-primary/20 bg-primary/5">
@ -186,80 +198,66 @@ const canSubmit = () => {
</Alert>
<!-- Password Field -->
<passwordForm.Field name="password">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showPassword ? 'text' : 'password'"
placeholder="Create a strong password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': isInvalid(field) }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<p v-if="isInvalid(field)" class="text-sm text-destructive">
{{ field.state.meta.errors[0] }}
</p>
</div>
</template>
</passwordForm.Field>
<div class="space-y-2">
<Label for="password" class="text-base">Password</Label>
<div class="relative">
<Lock
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
id="password"
v-model="password"
:type="showPassword ? 'text' : 'password'"
placeholder="Create a strong password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': passwordError }"
@blur="passwordTouched = true"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showPassword = !showPassword"
>
<Eye v-if="!showPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<p v-if="passwordError" class="text-sm text-destructive">
{{ passwordError }}
</p>
</div>
<!-- Confirm Password Field -->
<passwordForm.Field name="confirmPassword">
<template #default="{ field }">
<div class="space-y-2">
<Label :for="field.name" class="text-base">Confirm Password</Label>
<div class="relative">
<KeyRound
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
:id="field.name"
:name="field.name"
:model-value="field.state.value"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="Re-enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': field.state.value && !isPasswordMatch() }"
autocomplete="new-password"
@blur="field.handleBlur"
@input="field.handleChange(($event.target as HTMLInputElement).value)"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<p v-if="field.state.value && !isPasswordMatch()" class="text-sm text-destructive">
Passwords do not match
</p>
</div>
</template>
</passwordForm.Field>
<div class="space-y-2">
<Label for="confirmPassword" class="text-base">Confirm Password</Label>
<div class="relative">
<KeyRound
:size="20"
class="absolute left-3 top-1/2 -translate-y-1/2 text-muted-foreground"
/>
<Input
id="confirmPassword"
v-model="confirmPassword"
:type="showConfirmPassword ? 'text' : 'password'"
placeholder="Re-enter your password"
class="h-12 pl-11 pr-11 text-base"
:class="{ 'border-destructive': confirmPassword && !isPasswordMatch }"
@blur="confirmPasswordTouched = true"
/>
<button
type="button"
class="absolute right-3 top-1/2 -translate-y-1/2 text-muted-foreground transition-colors hover:text-foreground"
@click="showConfirmPassword = !showConfirmPassword"
>
<Eye v-if="!showConfirmPassword" :size="20" />
<EyeOff v-else :size="20" />
</button>
</div>
<p v-if="confirmPassword && !isPasswordMatch" class="text-sm text-destructive">
Passwords do not match
</p>
</div>
<!-- Action Buttons -->
<div class="flex gap-3">
@ -278,7 +276,7 @@ const canSubmit = () => {
type="submit"
size="lg"
class="h-12 flex-1 text-base font-semibold"
:disabled="!canSubmit() || isLoading"
:disabled="!canSubmit || isLoading"
>
<Loader2 v-if="isLoading" :size="20" class="mr-2 animate-spin" />
{{ isLoading ? 'Importing...' : 'Import Wallet' }}

View File

@ -0,0 +1,2 @@
export { default as RecoverWalletFlow } from './RecoverWalletFlow.vue'
export { default as RecoverSeedInput } from './RecoverSeedInput.vue'

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import { computed } from 'vue'
import { OnboardingTab, LoginTab } from '@/views/Auth/components'
import { useNeptuneStore } from '@/stores'
const neptuneStore = useNeptuneStore()
const emit = defineEmits<{
goToCreate: []
goToRecover: []
}>()
// Internal state to switch between onboarding and login
const showLogin = computed(() => neptuneStore.getKeystoreFileName !== null)
const handleGoToCreate = () => {
emit('goToCreate')
}
const handleGoToRecover = () => {
emit('goToRecover')
}
const handleSubmit = (password: string) => {
}
</script>
<template>
<div>
<!-- Onboarding Screen -->
<OnboardingTab
v-if="!showLogin"
@go-to-create="handleGoToCreate"
@go-to-recover="handleGoToRecover"
/>
<!-- Login Screen -->
<LoginTab
v-else
@go-to-create="handleGoToCreate"
@submit="handleSubmit"
/>
</div>
</template>

View File

@ -0,0 +1 @@
export { default as WelcomeTab } from './WelcomeTab.vue'

View File

@ -1,60 +0,0 @@
<script setup lang="ts">
const { t } = useI18n()
</script>
<template>
<div class="container mx-auto px-4 py-6">
<!-- Welcome Section -->
<div class="mb-6">
<h1 class="text-2xl font-bold text-foreground">Welcome back!</h1>
<p class="mt-1 text-sm text-muted-foreground">Manage your digital assets securely</p>
</div>
<!-- Balance Card -->
<div class="mb-6 rounded-xl bg-gradient-to-br from-primary to-accent p-6 text-primary-foreground shadow-lg">
<p class="text-sm opacity-90">Total Portfolio Value</p>
<p class="mt-2 text-4xl font-bold">$0.00</p>
<div class="mt-4 flex gap-2">
<Button variant="secondary" size="sm" class="flex-1">Send</Button>
<Button variant="secondary" size="sm" class="flex-1">Receive</Button>
</div>
</div>
<!-- Quick Actions -->
<div class="mb-6">
<h2 class="mb-3 text-lg font-semibold text-foreground">Quick Actions</h2>
<div class="grid grid-cols-2 gap-3">
<button class="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent">
<div class="mb-2 text-2xl">💸</div>
<p class="font-medium text-foreground">Send</p>
<p class="text-xs text-muted-foreground">Transfer assets</p>
</button>
<button class="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent">
<div class="mb-2 text-2xl">📥</div>
<p class="font-medium text-foreground">Receive</p>
<p class="text-xs text-muted-foreground">Get payment</p>
</button>
<button class="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent">
<div class="mb-2 text-2xl">🔄</div>
<p class="font-medium text-foreground">Swap</p>
<p class="text-xs text-muted-foreground">Exchange tokens</p>
</button>
<button class="rounded-lg border border-border bg-card p-4 text-left transition-colors hover:bg-accent">
<div class="mb-2 text-2xl">📊</div>
<p class="font-medium text-foreground">Analytics</p>
<p class="text-xs text-muted-foreground">View insights</p>
</button>
</div>
</div>
<!-- Recent Activity -->
<div>
<h2 class="mb-3 text-lg font-semibold text-foreground">Recent Activity</h2>
<div class="rounded-lg border border-border bg-card p-6 text-center">
<p class="text-sm text-muted-foreground">No recent activity</p>
<p class="mt-1 text-xs text-muted-foreground">Your transactions will appear here</p>
</div>
</div>
</div>
</template>

View File

@ -2,7 +2,7 @@
const router = useRouter()
const goHome = () => {
router.push({ name: 'home' })
router.push({ name: 'wallet' })
}
</script>

View File

@ -0,0 +1,19 @@
<script setup lang="ts">
const { t } = useI18n()
</script>
<template>
<div class="container mx-auto px-4 py-6">
<h1 class="mb-2 text-2xl font-bold text-foreground">{{ t('wallet.title') }}</h1>
<p class="text-muted-foreground">Manage your assets and balances</p>
<!-- Wallet content will go here -->
<div class="mt-6 space-y-4">
<div class="rounded-lg border border-border bg-card p-6">
<p class="text-sm text-muted-foreground">Total Balance</p>
<p class="mt-2 text-3xl font-bold text-foreground">$0.00</p>
</div>
</div>
</div>
</template>

View File

@ -1,2 +1,6 @@
export const AuthPage = () => import('@/views/Auth/AuthView.vue')
export const WalletPage = () => import('@/views/Wallet/WalletView.vue')
export const UTXOPage = () => import('@/views/UTXO/UTXOView.vue')
export const TransactionHistoryPage = () =>
import('@/views/TransactionHistory/TransactionHistoryView.vue')
export const SettingsPage = () => import('@/views/Setting/SettingsView.vue')

View File

@ -62,7 +62,6 @@ export default defineConfig({
build: {
// Tauri uses Chromium on Windows and WebKit on macOS and Linux
target: process.env.TAURI_ENV_PLATFORM == 'windows' ? 'chrome105' : 'safari13',
// Don't minify for debug builds
minify: !process.env.TAURI_ENV_DEBUG ? 'esbuild' : false,
// Produce sourcemaps for debug builds
sourcemap: !!process.env.TAURI_ENV_DEBUG,