feat: 071125/send_transaction_form_and_confirm
This commit is contained in:
parent
e48669d972
commit
b9940b66a9
48
components.d.ts
vendored
Normal file
48
components.d.ts
vendored
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable */
|
||||||
|
// @ts-nocheck
|
||||||
|
// biome-ignore lint: disable
|
||||||
|
// oxlint-disable
|
||||||
|
// ------
|
||||||
|
// Generated by unplugin-vue-components
|
||||||
|
// Read more: https://github.com/vuejs/core/pull/3399
|
||||||
|
import { GlobalComponents } from 'vue'
|
||||||
|
|
||||||
|
export {}
|
||||||
|
|
||||||
|
/* prettier-ignore */
|
||||||
|
declare module 'vue' {
|
||||||
|
export interface GlobalComponents {
|
||||||
|
AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||||
|
ButtonCommon: typeof import('./src/components/common/ButtonCommon.vue')['default']
|
||||||
|
CardBase: typeof import('./src/components/common/CardBase.vue')['default']
|
||||||
|
CardBaseScrollable: typeof import('./src/components/common/CardBaseScrollable.vue')['default']
|
||||||
|
FormCommon: typeof import('./src/components/common/FormCommon.vue')['default']
|
||||||
|
IconCommon: typeof import('./src/components/icon/IconCommon.vue')['default']
|
||||||
|
LayoutVue: typeof import('./src/components/common/LayoutVue.vue')['default']
|
||||||
|
ModalCommon: typeof import('./src/components/common/ModalCommon.vue')['default']
|
||||||
|
PasswordForm: typeof import('./src/components/common/PasswordForm.vue')['default']
|
||||||
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
SpinnerCommon: typeof import('./src/components/common/SpinnerCommon.vue')['default']
|
||||||
|
TabPaneCommon: typeof import('./src/components/common/TabPaneCommon.vue')['default']
|
||||||
|
TabsCommon: typeof import('./src/components/common/TabsCommon.vue')['default']
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// For TSX support
|
||||||
|
declare global {
|
||||||
|
const AConfigProvider: typeof import('ant-design-vue/es')['ConfigProvider']
|
||||||
|
const ButtonCommon: typeof import('./src/components/common/ButtonCommon.vue')['default']
|
||||||
|
const CardBase: typeof import('./src/components/common/CardBase.vue')['default']
|
||||||
|
const CardBaseScrollable: typeof import('./src/components/common/CardBaseScrollable.vue')['default']
|
||||||
|
const FormCommon: typeof import('./src/components/common/FormCommon.vue')['default']
|
||||||
|
const IconCommon: typeof import('./src/components/icon/IconCommon.vue')['default']
|
||||||
|
const LayoutVue: typeof import('./src/components/common/LayoutVue.vue')['default']
|
||||||
|
const ModalCommon: typeof import('./src/components/common/ModalCommon.vue')['default']
|
||||||
|
const PasswordForm: typeof import('./src/components/common/PasswordForm.vue')['default']
|
||||||
|
const RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
|
const RouterView: typeof import('vue-router')['RouterView']
|
||||||
|
const SpinnerCommon: typeof import('./src/components/common/SpinnerCommon.vue')['default']
|
||||||
|
const TabPaneCommon: typeof import('./src/components/common/TabPaneCommon.vue')['default']
|
||||||
|
const TabsCommon: typeof import('./src/components/common/TabsCommon.vue')['default']
|
||||||
|
}
|
||||||
@ -3,6 +3,8 @@ import fs from 'fs'
|
|||||||
import path from 'path'
|
import path from 'path'
|
||||||
import { encrypt, fromEncryptedJson } from './utils/keystore'
|
import { encrypt, fromEncryptedJson } from './utils/keystore'
|
||||||
|
|
||||||
|
const neptuneNative = require('@neptune/native')
|
||||||
|
|
||||||
// Create keystore into default wallets directory
|
// Create keystore into default wallets directory
|
||||||
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
ipcMain.handle('wallet:createKeystore', async (_event, seed, password) => {
|
||||||
try {
|
try {
|
||||||
@ -67,13 +69,50 @@ ipcMain.handle('wallet:checkKeystore', async () => {
|
|||||||
const walletDir = path.join(process.cwd(), 'wallets')
|
const walletDir = path.join(process.cwd(), 'wallets')
|
||||||
if (!fs.existsSync(walletDir)) return { exists: false, filePath: null }
|
if (!fs.existsSync(walletDir)) return { exists: false, filePath: null }
|
||||||
|
|
||||||
const file = fs.readdirSync(walletDir).find((f) => f.endsWith('.json'))
|
const newestFile = fs
|
||||||
if (!file) return { exists: false, filePath: null }
|
.readdirSync(walletDir)
|
||||||
|
.filter((f) => f.endsWith('.json'))
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
fs.statSync(path.join(walletDir, b)).mtime.getTime() -
|
||||||
|
fs.statSync(path.join(walletDir, a)).mtime.getTime()
|
||||||
|
)[0]
|
||||||
|
|
||||||
const filePath = path.join(walletDir, file)
|
if (!newestFile) return { exists: false, filePath: null }
|
||||||
return { exists: true, filePath}
|
|
||||||
|
return { exists: true, filePath: path.join(walletDir, newestFile) }
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error checking keystore:', error)
|
console.error('Error checking keystore ipc:', error)
|
||||||
return { exists: false, filePath: null, error: String(error) }
|
return { exists: false, filePath: null, error: String(error) }
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('wallet:generateKeysFromSeed', async (_event, seedPhrase: string[]) => {
|
||||||
|
try {
|
||||||
|
const wallet = new neptuneNative.WalletManager()
|
||||||
|
return wallet.generateKeysFromSeed(seedPhrase)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error generating keys from seed ipc:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
ipcMain.handle('wallet:buildTransactionWithPrimitiveProof', async (_event, args) => {
|
||||||
|
const { spendingKeyHex, inputAdditionRecords, outputAddresses, outputAmounts, fee } = args
|
||||||
|
|
||||||
|
try {
|
||||||
|
const builder = new neptuneNative.SimpleTransactionBuilder()
|
||||||
|
const result = await builder.buildTransactionWithPrimitiveProof(
|
||||||
|
import.meta.env.VITE_APP_API,
|
||||||
|
spendingKeyHex,
|
||||||
|
inputAdditionRecords,
|
||||||
|
outputAddresses,
|
||||||
|
outputAmounts,
|
||||||
|
fee
|
||||||
|
)
|
||||||
|
return JSON.parse(result)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error building transaction with primitive proof ipc:', error)
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|||||||
@ -11,6 +11,9 @@ const createWindow = () => {
|
|||||||
const mainWindow = new BrowserWindow({
|
const mainWindow = new BrowserWindow({
|
||||||
width: 800,
|
width: 800,
|
||||||
height: 800,
|
height: 800,
|
||||||
|
minWidth: 500,
|
||||||
|
minHeight: 650,
|
||||||
|
icon: path.resolve(process.cwd(), 'public/favicon.png'),
|
||||||
webPreferences: {
|
webPreferences: {
|
||||||
preload: path.join(__dirname, 'preload.js'),
|
preload: path.join(__dirname, 'preload.js'),
|
||||||
contextIsolation: true,
|
contextIsolation: true,
|
||||||
|
|||||||
@ -10,4 +10,8 @@ contextBridge.exposeInMainWorld('walletApi', {
|
|||||||
decryptKeystore: (filePath: string, password: string) =>
|
decryptKeystore: (filePath: string, password: string) =>
|
||||||
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
ipcRenderer.invoke('wallet:decryptKeystore', filePath, password),
|
||||||
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
|
checkKeystore: () => ipcRenderer.invoke('wallet:checkKeystore'),
|
||||||
|
generateKeysFromSeed: (seedPhrase: string[]) =>
|
||||||
|
ipcRenderer.invoke('wallet:generateKeysFromSeed', seedPhrase),
|
||||||
|
buildTransactionWithPrimitiveProof: (args: any) =>
|
||||||
|
ipcRenderer.invoke('wallet:buildTransactionWithPrimitiveProof', args),
|
||||||
})
|
})
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
<html lang="en">
|
<html lang="en">
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<link rel="icon" type="image/svg+xml" href="/favicon.ico" />
|
<link rel="icon" type="image/svg+xml" href="/favicon.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Neptune Web Wallet</title>
|
<title>Neptune Web Wallet</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
1984
package-lock.json
generated
1984
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -21,6 +21,7 @@
|
|||||||
"format": "prettier --write src/"
|
"format": "prettier --write src/"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@neptune/native": "file:./packages/neptune-native",
|
||||||
"@neptune/wasm": "file:./packages/neptune-wasm",
|
"@neptune/wasm": "file:./packages/neptune-wasm",
|
||||||
"ant-design-vue": "^4.2.6",
|
"ant-design-vue": "^4.2.6",
|
||||||
"axios": "^1.6.8",
|
"axios": "^1.6.8",
|
||||||
@ -57,6 +58,8 @@
|
|||||||
"prettier": "^3.2.5",
|
"prettier": "^3.2.5",
|
||||||
"sass": "^1.75.0",
|
"sass": "^1.75.0",
|
||||||
"typescript": "~5.4.0",
|
"typescript": "~5.4.0",
|
||||||
|
"unplugin-auto-import": "^20.2.0",
|
||||||
|
"unplugin-vue-components": "^30.0.0",
|
||||||
"vite": "^5.2.8",
|
"vite": "^5.2.8",
|
||||||
"vite-plugin-vue-devtools": "^7.0.25",
|
"vite-plugin-vue-devtools": "^7.0.25",
|
||||||
"vue-tsc": "^2.0.11"
|
"vue-tsc": "^2.0.11"
|
||||||
|
|||||||
BIN
packages/neptune-native-0.1.0.tgz
Normal file
BIN
packages/neptune-native-0.1.0.tgz
Normal file
Binary file not shown.
82
packages/neptune-native/index.d.ts
vendored
Normal file
82
packages/neptune-native/index.d.ts
vendored
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
/* tslint:disable */
|
||||||
|
/* eslint-disable */
|
||||||
|
|
||||||
|
/* auto-generated by NAPI-RS */
|
||||||
|
|
||||||
|
/** Module initialization */
|
||||||
|
export declare function initNativeModule(): string
|
||||||
|
/** Quick VM test (can call immediately) */
|
||||||
|
export declare function quickVmTest(): string
|
||||||
|
export declare function getVersion(): string
|
||||||
|
/** Wallet manager for key generation and transaction signing */
|
||||||
|
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, endBlock: 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()
|
||||||
|
buildTransactionWithPrimitiveProof(rpcUrl: string, spendingKeyHex: string, inputAdditionRecords: Array<string>, outputAddresses: Array<string>, outputAmounts: Array<string>, fee: string): Promise<string>
|
||||||
|
}
|
||||||
319
packages/neptune-native/index.js
Normal file
319
packages/neptune-native/index.js
Normal 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
|
||||||
BIN
packages/neptune-native/neptune-native.win32-x64-msvc.node
Normal file
BIN
packages/neptune-native/neptune-native.win32-x64-msvc.node
Normal file
Binary file not shown.
45
packages/neptune-native/package.json
Normal file
45
packages/neptune-native/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
Before Width: | Height: | Size: 4.2 KiB |
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@ -3,7 +3,7 @@ import { LayoutVue } from '@/components'
|
|||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
token: {
|
token: {
|
||||||
colorPrimary: '#007FCF',
|
colorPrimary: '#42A5F5',
|
||||||
borderRadius: 4,
|
borderRadius: 4,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,34 +12,35 @@ export const getUtxosFromViewKey = async (
|
|||||||
endBlock,
|
endBlock,
|
||||||
maxSearchDepth,
|
maxSearchDepth,
|
||||||
}
|
}
|
||||||
return await callJsonRpc('wallet_getUtxosFromViewKey', params)
|
return await callJsonRpc('wallet_getUtxoInfo', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBalance = async (): Promise<any> => {
|
export const getBalance = async (
|
||||||
return await callJsonRpc('wallet_balance', [])
|
viewKey: string,
|
||||||
|
startBlock: number = 0,
|
||||||
|
endBlock: number | null = null,
|
||||||
|
maxSearchDepth: number = 1000
|
||||||
|
): Promise<any> => {
|
||||||
|
const params = {
|
||||||
|
viewKey,
|
||||||
|
startBlock,
|
||||||
|
endBlock,
|
||||||
|
maxSearchDepth,
|
||||||
|
}
|
||||||
|
return await callJsonRpc('wallet_getBalanceFromViewKey', params)
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getBlockHeight = async (): Promise<any> => {
|
export const getBlockHeight = async (): Promise<any> => {
|
||||||
return await callJsonRpc('chain_height', [])
|
return await callJsonRpc('chain_height')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getNetworkInfo = async (): Promise<any> => {
|
export const getNetworkInfo = async (): Promise<any> => {
|
||||||
return await callJsonRpc('node_network', [])
|
return await callJsonRpc('node_network')
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getWalletAddress = async (): Promise<any> => {
|
export const broadcastSignedTransaction = async (transactionHex: any): Promise<any> => {
|
||||||
return await callJsonRpc('wallet_address', [])
|
const params = {
|
||||||
}
|
transactionHex,
|
||||||
|
}
|
||||||
export const sendTransaction = async (
|
return await callJsonRpc('mempool_submitTransaction', params)
|
||||||
toAddress: string,
|
|
||||||
amount: string,
|
|
||||||
fee: string
|
|
||||||
): Promise<any> => {
|
|
||||||
const params = [toAddress, amount, fee]
|
|
||||||
return await callJsonRpc('wallet_send', params)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const broadcastSignedTransaction = async (signedTxData: any): Promise<any> => {
|
|
||||||
return await callJsonRpc('wallet_broadcastSignedTransaction', signedTxData)
|
|
||||||
}
|
}
|
||||||
|
|||||||
BIN
src/assets/imgs/logo.png
Normal file
BIN
src/assets/imgs/logo.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 MiB |
@ -144,14 +144,12 @@ $fw: 100;
|
|||||||
padding: var(--card-padding);
|
padding: var(--card-padding);
|
||||||
box-shadow: var(--card-shadow);
|
box-shadow: var(--card-shadow);
|
||||||
transition: var(--transition-all);
|
transition: var(--transition-all);
|
||||||
animation: fadeIn 0.6s ease-out;
|
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
padding: var(--card-padding-mobile);
|
padding: var(--card-padding-mobile);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
transform: translateY(-4px);
|
|
||||||
box-shadow: var(--card-shadow-hover);
|
box-shadow: var(--card-shadow-hover);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,38 +2,34 @@
|
|||||||
// ==================== COLORS ====================
|
// ==================== COLORS ====================
|
||||||
|
|
||||||
// Primary Colors
|
// Primary Colors
|
||||||
--primary-color: #007fcf;
|
--primary-color: #42A5F5;
|
||||||
--primary-hover: #0066a6;
|
--primary-hover: #1E88E5;
|
||||||
--primary-light: #e8f4fc;
|
--primary-light: #E3F2FD;
|
||||||
--primary-bg: #f5fbff;
|
--primary-bg: #F5FBFF;
|
||||||
|
|
||||||
// Secondary Colors
|
|
||||||
--secondary-color: #ff9500;
|
|
||||||
--secondary-hover: #e68600;
|
|
||||||
|
|
||||||
// Text Colors
|
// Text Colors
|
||||||
--text-primary: #2c3e50;
|
--text-primary: #232323;
|
||||||
--text-secondary: #5a6c7d;
|
--text-secondary: #5A5A5A;
|
||||||
--text-muted: #8b95a5;
|
--text-muted: #8B8B8B;
|
||||||
--text-light: #ffffff;
|
--text-light: #FFFFFF;
|
||||||
|
|
||||||
// Background Colors
|
// Background Colors
|
||||||
--bg-gradient-start: #f0f8ff;
|
--bg-gradient-start: #f0f8ff;
|
||||||
--bg-gradient-end: #e6f2ff;
|
--bg-gradient-end: #e3f2fd;
|
||||||
--bg-white: #ffffff;
|
--bg-white: #ffffff;
|
||||||
--bg-light: #f8fcff;
|
--bg-light: #f8fcff;
|
||||||
--bg-hover: #e8f4fc;
|
--bg-hover: #e3f2fd;
|
||||||
|
|
||||||
// Border Colors
|
// Border Colors
|
||||||
--border-light: #e6f2ff;
|
--border-light: #e3f2fd;
|
||||||
--border-color: #ebf5ff;
|
--border-color: #e8f4fc;
|
||||||
--border-primary: #007fcf;
|
--border-primary: #42A5F5;
|
||||||
|
|
||||||
// Status Colors
|
// Status Colors
|
||||||
--success-color: #10b981;
|
--success-color: #10b981;
|
||||||
--warning-color: #f59e0b;
|
--warning-color: #f59e0b;
|
||||||
--error-color: #ef4444;
|
--error-color: #ef4444;
|
||||||
--info-color: #007fcf;
|
--info-color: #42A5F5;
|
||||||
|
|
||||||
// ==================== SPACING ====================
|
// ==================== SPACING ====================
|
||||||
|
|
||||||
@ -48,7 +44,7 @@
|
|||||||
|
|
||||||
// ==================== BORDER RADIUS ====================
|
// ==================== BORDER RADIUS ====================
|
||||||
|
|
||||||
--radius-sm: 8px;
|
--radius-sm: 4px;
|
||||||
--radius-md: 10px;
|
--radius-md: 10px;
|
||||||
--radius-lg: 12px;
|
--radius-lg: 12px;
|
||||||
--radius-xl: 16px;
|
--radius-xl: 16px;
|
||||||
@ -61,7 +57,7 @@
|
|||||||
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.08);
|
--shadow-md: 0 4px 20px rgba(0, 0, 0, 0.08);
|
||||||
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
|
--shadow-lg: 0 8px 30px rgba(0, 0, 0, 0.12);
|
||||||
--shadow-xl: 0 12px 40px rgba(0, 0, 0, 0.15);
|
--shadow-xl: 0 12px 40px rgba(0, 0, 0, 0.15);
|
||||||
--shadow-primary: 0 4px 12px rgba(0, 127, 207, 0.25);
|
--shadow-primary: 0 4px 12px rgba(66, 165, 245, 0.25);
|
||||||
--shadow-secondary: 0 4px 12px rgba(255, 149, 0, 0.25);
|
--shadow-secondary: 0 4px 12px rgba(255, 149, 0, 0.25);
|
||||||
|
|
||||||
// ==================== TRANSITIONS ====================
|
// ==================== TRANSITIONS ====================
|
||||||
@ -117,7 +113,7 @@
|
|||||||
// Button
|
// Button
|
||||||
--btn-padding-y: 0.75rem;
|
--btn-padding-y: 0.75rem;
|
||||||
--btn-padding-x: 1rem;
|
--btn-padding-x: 1rem;
|
||||||
--btn-radius: var(--radius-md);
|
--btn-radius: var(--radius-sm);
|
||||||
|
|
||||||
// Tabs
|
// Tabs
|
||||||
--tabs-height: 3px;
|
--tabs-height: 3px;
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Button } from 'ant-design-vue'
|
|
||||||
import type { ButtonProps } from '@/interface'
|
import type { ButtonProps } from '@/interface'
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { SpinnerCommon } from '@/components'
|
||||||
|
|
||||||
const props = withDefaults(defineProps<ButtonProps>(), {
|
const props = withDefaults(defineProps<ButtonProps>(), {
|
||||||
type: 'default',
|
type: 'default',
|
||||||
@ -18,50 +19,154 @@ const handleClick = () => {
|
|||||||
emit('click')
|
emit('click')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buttonClasses = computed(() => {
|
||||||
|
return [
|
||||||
|
'btn-common',
|
||||||
|
`btn-${props.type}`,
|
||||||
|
`btn-${props.size}`,
|
||||||
|
{
|
||||||
|
'btn-block': props.block,
|
||||||
|
'btn-disabled': props.disabled || props.loading,
|
||||||
|
'btn-loading': props.loading,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<Button
|
<button
|
||||||
:type="props.type"
|
:type="props.htmlType"
|
||||||
:size="props.size"
|
:class="buttonClasses"
|
||||||
:block="props.block"
|
:disabled="props.disabled || props.loading"
|
||||||
:disabled="props.disabled"
|
|
||||||
:loading="props.loading"
|
|
||||||
:html-type="props.htmlType"
|
|
||||||
@click="handleClick"
|
@click="handleClick"
|
||||||
class="btn-common"
|
|
||||||
>
|
>
|
||||||
|
<span v-if="props.loading" class="btn-spinner">
|
||||||
|
<SpinnerCommon size="small" />
|
||||||
|
</span>
|
||||||
<slot />
|
<slot />
|
||||||
</Button>
|
</button>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.btn-common {
|
.btn-common {
|
||||||
:deep(.ant-btn) {
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
border-radius: var(--btn-radius);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
transition: var(--transition-all);
|
||||||
|
cursor: pointer;
|
||||||
|
border: 2px solid transparent;
|
||||||
|
outline: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
// Sizes
|
||||||
|
&.btn-small {
|
||||||
|
padding: 6px 12px;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-medium {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-large {
|
||||||
|
padding: var(--btn-padding-y) var(--btn-padding-x);
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Types
|
||||||
|
&.btn-primary {
|
||||||
background: var(--primary-color);
|
background: var(--primary-color);
|
||||||
border-color: var(--primary-color);
|
border-color: var(--primary-color);
|
||||||
font-weight: var(--font-semibold);
|
color: var(--text-light);
|
||||||
height: auto;
|
|
||||||
padding: var(--btn-padding-y) var(--btn-padding-x);
|
&:hover:not(.btn-disabled) {
|
||||||
transition: var(--transition-all);
|
|
||||||
border-radius: var(--btn-radius);
|
|
||||||
letter-spacing: var(--tracking-wide);
|
|
||||||
transition: all 2s ease-in-out;
|
|
||||||
&:hover {
|
|
||||||
background: var(--primary-hover);
|
background: var(--primary-hover);
|
||||||
border-color: var(--primary-hover);
|
border-color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
|
||||||
&:active,
|
&:active:not(.btn-disabled),
|
||||||
&:focus {
|
&:focus:not(.btn-disabled) {
|
||||||
background: var(--primary-hover);
|
background: var(--primary-hover);
|
||||||
border-color: var(--primary-hover);
|
border-color: var(--primary-hover);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&:disabled {
|
&.btn-default {
|
||||||
opacity: 0.6;
|
background: var(--bg-white);
|
||||||
cursor: not-allowed;
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover:not(.btn-disabled) {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&:active:not(.btn-disabled),
|
||||||
|
&:focus:not(.btn-disabled) {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-dashed {
|
||||||
|
background: transparent;
|
||||||
|
border-style: dashed;
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover:not(.btn-disabled) {
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-link {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--primary-color);
|
||||||
|
|
||||||
|
&:hover:not(.btn-disabled) {
|
||||||
|
color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-text {
|
||||||
|
background: transparent;
|
||||||
|
border-color: transparent;
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover:not(.btn-disabled) {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// States
|
||||||
|
&.btn-block {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn-loading {
|
||||||
|
cursor: wait;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-spinner {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
302
src/components/common/ModalCommon.vue
Normal file
302
src/components/common/ModalCommon.vue
Normal file
@ -0,0 +1,302 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
open: boolean
|
||||||
|
title?: string
|
||||||
|
width?: string | number
|
||||||
|
maskClosable?: boolean
|
||||||
|
keyboard?: boolean
|
||||||
|
footer?: boolean
|
||||||
|
disabled?: boolean
|
||||||
|
loading?: boolean
|
||||||
|
getContainer?: () => HTMLElement
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
title: '',
|
||||||
|
width: '520px',
|
||||||
|
maskClosable: true,
|
||||||
|
keyboard: true,
|
||||||
|
footer: true,
|
||||||
|
disabled: false,
|
||||||
|
loading: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:open': [value: boolean]
|
||||||
|
cancel: []
|
||||||
|
ok: []
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
if (!props.maskClosable) return
|
||||||
|
emit('update:open', false)
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('update:open', false)
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleOk = () => {
|
||||||
|
emit('ok')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleMaskClick = () => {
|
||||||
|
if (props.maskClosable) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleContentClick = (e: Event) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleKeydown = (e: KeyboardEvent) => {
|
||||||
|
if (props.keyboard && e.key === 'Escape' && props.open) {
|
||||||
|
handleClose()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalWidth = computed(() => {
|
||||||
|
if (typeof props.width === 'number') {
|
||||||
|
return `${props.width}px`
|
||||||
|
}
|
||||||
|
return props.width
|
||||||
|
})
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.open,
|
||||||
|
(isOpen) => {
|
||||||
|
if (isOpen) {
|
||||||
|
document.body.style.overflow = 'hidden'
|
||||||
|
} else {
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('keydown', handleKeydown)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('keydown', handleKeydown)
|
||||||
|
document.body.style.overflow = ''
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport :to="getContainer ? getContainer() : 'body'">
|
||||||
|
<Transition name="modal-fade">
|
||||||
|
<div v-if="open" class="modal-mask" @click="handleMaskClick">
|
||||||
|
<Transition name="modal-slide">
|
||||||
|
<div
|
||||||
|
v-if="open"
|
||||||
|
class="modal-wrapper"
|
||||||
|
:style="{ width: modalWidth }"
|
||||||
|
@click="handleContentClick"
|
||||||
|
>
|
||||||
|
<div class="modal-header" v-if="title">
|
||||||
|
<h3 class="modal-title">{{ title }}</h3>
|
||||||
|
<button
|
||||||
|
class="modal-close-btn"
|
||||||
|
@click="handleCancel"
|
||||||
|
aria-label="Close"
|
||||||
|
>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="20"
|
||||||
|
height="20"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
>
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
<div v-if="footer" class="modal-footer">
|
||||||
|
<slot name="footer">
|
||||||
|
<button class="modal-btn modal-btn-default" @click="handleCancel">
|
||||||
|
Cancel
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="modal-btn modal-btn-primary"
|
||||||
|
@click="handleOk"
|
||||||
|
:disabled="disabled"
|
||||||
|
>
|
||||||
|
<SpinnerCommon v-if="loading" size="small" />
|
||||||
|
<span v-else>OK</span>
|
||||||
|
</button>
|
||||||
|
</slot>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.modal-mask {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.45);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 1000;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: 0 3px 20px rgba(0, 0, 0, 0.2);
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 90vh;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: var(--spacing-lg) var(--spacing-xl);
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.modal-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-close-btn {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 32px;
|
||||||
|
height: 32px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
cursor: pointer;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
transition: var(--transition-all);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-body {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
overflow-y: auto;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-footer {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
padding: var(--spacing-lg) var(--spacing-xl);
|
||||||
|
border-top: 1px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.modal-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-all);
|
||||||
|
border: 1px solid transparent;
|
||||||
|
|
||||||
|
&.modal-btn-default {
|
||||||
|
background: var(--bg-white);
|
||||||
|
border-color: var(--border-color);
|
||||||
|
color: var(--text-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.modal-btn-primary {
|
||||||
|
background: var(--primary-color);
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
color: var(--text-light);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
border-color: var(--primary-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transitions
|
||||||
|
.modal-fade-enter-active,
|
||||||
|
.modal-fade-leave-active {
|
||||||
|
transition: opacity 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-fade-enter-from,
|
||||||
|
.modal-fade-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-enter-active,
|
||||||
|
.modal-slide-leave-active {
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-enter-from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-50px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-slide-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(50px) scale(0.95);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.modal-mask {
|
||||||
|
padding: var(--spacing-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-wrapper {
|
||||||
|
width: 90% !important;
|
||||||
|
max-width: 90% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal-header,
|
||||||
|
.modal-body,
|
||||||
|
.modal-footer {
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -13,6 +13,7 @@ interface Props {
|
|||||||
loading?: boolean
|
loading?: boolean
|
||||||
error?: boolean
|
error?: boolean
|
||||||
errorMessage?: string
|
errorMessage?: string
|
||||||
|
validateFormat?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
const props = withDefaults(defineProps<Props>(), {
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
@ -26,6 +27,7 @@ const props = withDefaults(defineProps<Props>(), {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: false,
|
error: false,
|
||||||
errorMessage: 'Invalid password',
|
errorMessage: 'Invalid password',
|
||||||
|
validateFormat: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
const emit = defineEmits<{
|
const emit = defineEmits<{
|
||||||
@ -36,14 +38,42 @@ const emit = defineEmits<{
|
|||||||
const password = ref('')
|
const password = ref('')
|
||||||
const passwordError = ref('')
|
const passwordError = ref('')
|
||||||
|
|
||||||
|
const passwordStrength = computed(() => {
|
||||||
|
if (!password.value || !props.validateFormat) return { level: 0, text: '', color: '' }
|
||||||
|
|
||||||
|
let strength = 0
|
||||||
|
const checks = {
|
||||||
|
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
|
||||||
|
if (strength <= 2) return { level: 1, text: 'Weak', color: 'var(--error-color)' }
|
||||||
|
if (strength <= 3) return { level: 2, text: 'Medium', color: 'var(--warning-color)' }
|
||||||
|
if (strength <= 4) return { level: 3, text: 'Good', color: 'var(--info-color)' }
|
||||||
|
return { level: 4, text: 'Strong', color: 'var(--success-color)' }
|
||||||
|
})
|
||||||
|
|
||||||
const canProceed = computed(() => {
|
const canProceed = computed(() => {
|
||||||
return password.value.length > 0 && !passwordError.value
|
if (!password.value || passwordError.value) return false
|
||||||
|
|
||||||
|
if (props.validateFormat) {
|
||||||
|
return password.value.length >= 8 && passwordStrength.value.level >= 2
|
||||||
|
}
|
||||||
|
|
||||||
|
return password.value.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleSubmit = () => {
|
const handleSubmit = () => {
|
||||||
if (!canProceed.value) {
|
if (!canProceed.value) {
|
||||||
if (!password.value) {
|
if (!password.value) {
|
||||||
passwordError.value = 'Please enter your password'
|
passwordError.value = 'Please enter your password'
|
||||||
|
} else if (props.validateFormat && password.value.length < 8) {
|
||||||
|
passwordError.value = 'Password must be at least 8 characters'
|
||||||
|
} else if (props.validateFormat && passwordStrength.value.level < 2) {
|
||||||
|
passwordError.value = 'Password is too weak'
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -73,6 +103,28 @@ const handleBack = () => {
|
|||||||
@input="passwordError = ''"
|
@input="passwordError = ''"
|
||||||
@keyup.enter="handleSubmit"
|
@keyup.enter="handleSubmit"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Password Strength Indicator -->
|
||||||
|
<div v-if="props.validateFormat && password" class="password-strength">
|
||||||
|
<div class="strength-bar">
|
||||||
|
<div
|
||||||
|
class="strength-fill"
|
||||||
|
:style="{
|
||||||
|
width: `${(passwordStrength.level / 4) * 100}%`,
|
||||||
|
backgroundColor: passwordStrength.color,
|
||||||
|
}"
|
||||||
|
></div>
|
||||||
|
</div>
|
||||||
|
<span class="strength-text" :style="{ color: passwordStrength.color }">
|
||||||
|
{{ passwordStrength.text }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Helper Text -->
|
||||||
|
<p v-if="props.validateFormat" class="helper-text">
|
||||||
|
Password must be at least 8 characters with uppercase, lowercase, and numbers.
|
||||||
|
</p>
|
||||||
|
|
||||||
<span v-if="error" class="error-message">{{ errorMessage }}</span>
|
<span v-if="error" class="error-message">{{ errorMessage }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -132,4 +184,38 @@ const handleBack = () => {
|
|||||||
font-size: var(--font-sm);
|
font-size: var(--font-sm);
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.password-strength {
|
||||||
|
margin-top: var(--spacing-sm);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
|
||||||
|
.strength-bar {
|
||||||
|
flex: 1;
|
||||||
|
height: 4px;
|
||||||
|
background: var(--border-light);
|
||||||
|
border-radius: var(--radius-full);
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.strength-fill {
|
||||||
|
height: 100%;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.strength-text {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
min-width: 50px;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.helper-text {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--text-muted);
|
||||||
|
margin: var(--spacing-xs) 0 0;
|
||||||
|
line-height: var(--leading-normal);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
29
src/components/common/TabPaneCommon.vue
Normal file
29
src/components/common/TabPaneCommon.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { inject, computed, type ComputedRef } from 'vue'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
tabKey: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
const activeKey = inject<ComputedRef<string>>('activeTabKey')
|
||||||
|
|
||||||
|
const isActive = computed(() => {
|
||||||
|
return activeKey?.value === props.tabKey
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-show="isActive" class="tab-pane">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tab-pane {
|
||||||
|
height: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
138
src/components/common/TabsCommon.vue
Normal file
138
src/components/common/TabsCommon.vue
Normal file
@ -0,0 +1,138 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { provide, computed } from 'vue'
|
||||||
|
|
||||||
|
interface TabItem {
|
||||||
|
key: string
|
||||||
|
label: string
|
||||||
|
disabled?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
modelValue: string
|
||||||
|
items: TabItem[]
|
||||||
|
size?: 'small' | 'medium' | 'large'
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
size: 'medium',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
'update:modelValue': [value: string]
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const activeKey = computed({
|
||||||
|
get: () => props.modelValue,
|
||||||
|
set: (value: string) => emit('update:modelValue', value),
|
||||||
|
})
|
||||||
|
|
||||||
|
provide('activeTabKey', activeKey)
|
||||||
|
|
||||||
|
const handleTabClick = (key: string, disabled?: boolean) => {
|
||||||
|
if (disabled) return
|
||||||
|
activeKey.value = key
|
||||||
|
}
|
||||||
|
|
||||||
|
const tabClasses = computed(() => {
|
||||||
|
return ['tabs-common', `tabs-${props.size}`]
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="tabClasses">
|
||||||
|
<div class="tabs-nav">
|
||||||
|
<div
|
||||||
|
v-for="item in items"
|
||||||
|
:key="item.key"
|
||||||
|
class="tab-item"
|
||||||
|
:class="{
|
||||||
|
'tab-active': activeKey === item.key,
|
||||||
|
'tab-disabled': item.disabled,
|
||||||
|
}"
|
||||||
|
@click="handleTabClick(item.key, item.disabled)"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</div>
|
||||||
|
<div class="tabs-ink-bar" :style="{ left: `${items.findIndex(item => item.key === activeKey) * (100 / items.length)}%`, width: `${100 / items.length}%` }" />
|
||||||
|
</div>
|
||||||
|
<div class="tabs-content">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.tabs-common {
|
||||||
|
height: 100%;
|
||||||
|
|
||||||
|
.tabs-nav {
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.tab-item {
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-all);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
letter-spacing: var(--tracking-wide);
|
||||||
|
user-select: none;
|
||||||
|
|
||||||
|
&:hover:not(.tab-disabled) {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tab-active {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tab-disabled {
|
||||||
|
opacity: 0.5;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-ink-bar {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -2px;
|
||||||
|
height: var(--tabs-height);
|
||||||
|
background: var(--primary-color);
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sizes
|
||||||
|
&.tabs-small .tab-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tabs-medium .tab-item {
|
||||||
|
padding: 10px 16px;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.tabs-large .tab-item {
|
||||||
|
padding: 12px 20px;
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.tabs-content {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screen(mobile) {
|
||||||
|
&.tabs-large .tab-item {
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
@ -5,6 +5,21 @@ import PasswordForm from './common/PasswordForm.vue'
|
|||||||
import SpinnerCommon from './common/SpinnerCommon.vue'
|
import SpinnerCommon from './common/SpinnerCommon.vue'
|
||||||
import CardBase from './common/CardBase.vue'
|
import CardBase from './common/CardBase.vue'
|
||||||
import CardBaseScrollable from './common/CardBaseScrollable.vue'
|
import CardBaseScrollable from './common/CardBaseScrollable.vue'
|
||||||
|
import TabsCommon from './common/TabsCommon.vue'
|
||||||
|
import TabPaneCommon from './common/TabPaneCommon.vue'
|
||||||
|
import ModalCommon from './common/ModalCommon.vue'
|
||||||
import { IconCommon } from './icon'
|
import { IconCommon } from './icon'
|
||||||
|
|
||||||
export { LayoutVue, ButtonCommon, FormCommon, PasswordForm, SpinnerCommon, CardBase, CardBaseScrollable, IconCommon }
|
export {
|
||||||
|
LayoutVue,
|
||||||
|
ButtonCommon,
|
||||||
|
FormCommon,
|
||||||
|
PasswordForm,
|
||||||
|
SpinnerCommon,
|
||||||
|
CardBase,
|
||||||
|
CardBaseScrollable,
|
||||||
|
TabsCommon,
|
||||||
|
TabPaneCommon,
|
||||||
|
ModalCommon,
|
||||||
|
IconCommon,
|
||||||
|
}
|
||||||
|
|||||||
@ -1,12 +1,7 @@
|
|||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import * as API from '@/api/neptuneApi'
|
import * as API from '@/api/neptuneApi'
|
||||||
import type { GenerateSeedResult, ViewKeyResult } from '@/interface'
|
import type { GenerateSeedResult, PayloadBuildTransaction, ViewKeyResult, WalletState } from '@/interface'
|
||||||
import initWasm, {
|
import initWasm, { generate_seed, address_from_seed, validate_seed_phrase } from '@neptune/wasm'
|
||||||
generate_seed,
|
|
||||||
get_viewkey,
|
|
||||||
address_from_seed,
|
|
||||||
validate_seed_phrase,
|
|
||||||
} from '@neptune/wasm'
|
|
||||||
|
|
||||||
let wasmInitialized = false
|
let wasmInitialized = false
|
||||||
let initPromise: Promise<void> | null = null
|
let initPromise: Promise<void> | null = null
|
||||||
@ -51,8 +46,11 @@ export function useNeptuneWallet() {
|
|||||||
store.setReceiverId(result.receiver_identifier)
|
store.setReceiverId(result.receiver_identifier)
|
||||||
|
|
||||||
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase)
|
const viewKeyResult = await getViewKeyFromSeed(result.seed_phrase)
|
||||||
store.setViewKey(viewKeyResult.view_key)
|
store.setViewKey(viewKeyResult.view_key_hex)
|
||||||
store.setAddress(viewKeyResult.address)
|
store.setSpendingKey(viewKeyResult.spending_key_hex)
|
||||||
|
|
||||||
|
const addressResult = await getAddressFromSeed(result.seed_phrase)
|
||||||
|
store.setAddress(addressResult)
|
||||||
|
|
||||||
return result
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -62,18 +60,11 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
const getViewKeyFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
||||||
try {
|
const result = await (window as any).walletApi.generateKeysFromSeed([...seedPhrase])
|
||||||
await ensureWasmInitialized()
|
return JSON.parse(result)
|
||||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
|
||||||
const resultJson = get_viewkey(seedPhraseJson, store.getNetwork)
|
|
||||||
return JSON.parse(resultJson)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error getting view key from seed:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const recoverWalletFromSeed = async (seedPhrase: string[]): Promise<ViewKeyResult> => {
|
const recoverWalletFromSeed = async (seedPhrase: string[]): Promise<WalletState> => {
|
||||||
try {
|
try {
|
||||||
const isValid = validate_seed_phrase(JSON.stringify(seedPhrase))
|
const isValid = validate_seed_phrase(JSON.stringify(seedPhrase))
|
||||||
if (!isValid) throw new Error('Invalid seed phrase')
|
if (!isValid) throw new Error('Invalid seed phrase')
|
||||||
@ -82,10 +73,20 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
store.setSeedPhrase(seedPhrase)
|
store.setSeedPhrase(seedPhrase)
|
||||||
store.setReceiverId(result.receiver_identifier)
|
store.setReceiverId(result.receiver_identifier)
|
||||||
store.setViewKey(result.view_key)
|
store.setViewKey(result.view_key_hex)
|
||||||
store.setAddress(result.address)
|
store.setSpendingKey(result.spending_key_hex)
|
||||||
|
|
||||||
return result
|
const addressResult = await getAddressFromSeed(seedPhrase)
|
||||||
|
store.setAddress(addressResult)
|
||||||
|
|
||||||
|
return {
|
||||||
|
seedPhrase: seedPhrase,
|
||||||
|
network: store.getNetwork,
|
||||||
|
receiverId: result.receiver_identifier,
|
||||||
|
viewKey: result.view_key_hex,
|
||||||
|
spendingKey: result.spending_key_hex,
|
||||||
|
address: addressResult,
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error recovering wallet from seed:', err)
|
console.error('Error recovering wallet from seed:', err)
|
||||||
throw err
|
throw err
|
||||||
@ -93,14 +94,9 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
|
const getAddressFromSeed = async (seedPhrase: string[]): Promise<string> => {
|
||||||
try {
|
await ensureWasmInitialized()
|
||||||
await ensureWasmInitialized()
|
const seedPhraseJson = JSON.stringify(seedPhrase)
|
||||||
const seedPhraseJson = JSON.stringify(seedPhrase)
|
return address_from_seed(seedPhraseJson, store.getNetwork)
|
||||||
return address_from_seed(seedPhraseJson, store.getNetwork)
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Error getting address from seed:', err)
|
|
||||||
throw err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const decryptKeystore = async (password: string): Promise<void> => {
|
const decryptKeystore = async (password: string): Promise<void> => {
|
||||||
@ -118,9 +114,12 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
store.setPassword(password)
|
store.setPassword(password)
|
||||||
store.setSeedPhrase(seedPhrase)
|
store.setSeedPhrase(seedPhrase)
|
||||||
store.setAddress(viewKeyResult.address)
|
store.setViewKey(viewKeyResult.view_key_hex)
|
||||||
store.setViewKey(viewKeyResult.view_key)
|
|
||||||
store.setReceiverId(viewKeyResult.receiver_identifier)
|
store.setReceiverId(viewKeyResult.receiver_identifier)
|
||||||
|
store.setSpendingKey(viewKeyResult.spending_key_hex)
|
||||||
|
|
||||||
|
const addressResult = await getAddressFromSeed(seedPhrase)
|
||||||
|
store.setAddress(addressResult)
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
if (
|
if (
|
||||||
err instanceof Error &&
|
err instanceof Error &&
|
||||||
@ -137,6 +136,7 @@ export function useNeptuneWallet() {
|
|||||||
const createKeystore = async (seed: string, password: string): Promise<string> => {
|
const createKeystore = async (seed: string, password: string): Promise<string> => {
|
||||||
try {
|
try {
|
||||||
const result = await (window as any).walletApi.createKeystore(seed, password)
|
const result = await (window as any).walletApi.createKeystore(seed, password)
|
||||||
|
store.setKeystorePath(result.filePath)
|
||||||
return result.filePath
|
return result.filePath
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error creating keystore:', err)
|
console.error('Error creating keystore:', err)
|
||||||
@ -170,22 +170,13 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
// ===== API METHODS =====
|
// ===== API METHODS =====
|
||||||
|
|
||||||
const getUtxos = async (
|
const getUtxos = async (): Promise<any> => {
|
||||||
startBlock: number = 0,
|
|
||||||
endBlock: number | null = null,
|
|
||||||
maxSearchDepth: number = 1000
|
|
||||||
): Promise<any> => {
|
|
||||||
try {
|
try {
|
||||||
if (!store.getViewKey) {
|
if (!store.getViewKey) {
|
||||||
throw new Error('No view key available. Please import or generate a wallet first.')
|
throw new Error('No view key available. Please import or generate a wallet first.')
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await API.getUtxosFromViewKey(
|
const response = await API.getUtxosFromViewKey(store.getViewKey || '')
|
||||||
store.getViewKey,
|
|
||||||
startBlock,
|
|
||||||
endBlock,
|
|
||||||
maxSearchDepth
|
|
||||||
)
|
|
||||||
|
|
||||||
const result = response?.result || response
|
const result = response?.result || response
|
||||||
store.setUtxos(result.utxos || result || [])
|
store.setUtxos(result.utxos || result || [])
|
||||||
@ -198,11 +189,14 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
const getBalance = async (): Promise<any> => {
|
const getBalance = async (): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
const response = await API.getBalance()
|
const response = await API.getBalance(store.getViewKey || '')
|
||||||
const result = response?.result || response
|
const result = response?.result || response
|
||||||
store.setBalance(result.balance || result)
|
store.setBalance(result?.balance || result)
|
||||||
|
store.setPendingBalance(result?.pendingBalance || result)
|
||||||
return result
|
return {
|
||||||
|
balance: result?.balance || result,
|
||||||
|
pendingBalance: result?.pendingBalance || result,
|
||||||
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error getting balance:', err)
|
console.error('Error getting balance:', err)
|
||||||
throw err
|
throw err
|
||||||
@ -233,13 +227,20 @@ export function useNeptuneWallet() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const sendTransaction = async (
|
const buildTransactionWithPrimitiveProof = async (args: PayloadBuildTransaction): Promise<any> => {
|
||||||
toAddress: string,
|
const payload = {
|
||||||
amount: string,
|
spendingKeyHex: store.getSpendingKey,
|
||||||
fee: string
|
inputAdditionRecords: args.inputAdditionRecords,
|
||||||
): Promise<any> => {
|
outputAddresses: args.outputAddresses,
|
||||||
|
outputAmounts: args.outputAmounts,
|
||||||
|
fee: args.fee,
|
||||||
|
}
|
||||||
|
return await (window as any).walletApi.buildTransactionWithPrimitiveProof(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastSignedTransaction = async (transactionHex: string): Promise<any> => {
|
||||||
try {
|
try {
|
||||||
const response = await API.sendTransaction(toAddress, amount, fee)
|
const response = await API.broadcastSignedTransaction(transactionHex)
|
||||||
const result = response?.result || response
|
const result = response?.result || response
|
||||||
return result
|
return result
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -254,8 +255,11 @@ export function useNeptuneWallet() {
|
|||||||
|
|
||||||
if (store.getSeedPhrase) {
|
if (store.getSeedPhrase) {
|
||||||
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
|
const viewKeyResult = await getViewKeyFromSeed(store.getSeedPhrase)
|
||||||
store.setAddress(viewKeyResult.address)
|
store.setViewKey(viewKeyResult.view_key_hex)
|
||||||
store.setViewKey(viewKeyResult.view_key)
|
store.setSpendingKey(viewKeyResult.spending_key_hex)
|
||||||
|
|
||||||
|
const addressResult = await getAddressFromSeed(store.getSeedPhrase)
|
||||||
|
store.setAddress(addressResult)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.error('Error setting network:', err)
|
console.error('Error setting network:', err)
|
||||||
@ -279,7 +283,8 @@ export function useNeptuneWallet() {
|
|||||||
getBalance,
|
getBalance,
|
||||||
getBlockHeight,
|
getBlockHeight,
|
||||||
getNetworkInfo,
|
getNetworkInfo,
|
||||||
sendTransaction,
|
buildTransactionWithPrimitiveProof,
|
||||||
|
broadcastSignedTransaction,
|
||||||
decryptKeystore,
|
decryptKeystore,
|
||||||
createKeystore,
|
createKeystore,
|
||||||
saveKeystoreAs,
|
saveKeystoreAs,
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
import type { ButtonType, ButtonSize } from 'ant-design-vue/es/button'
|
|
||||||
|
|
||||||
// Button Component Props
|
// Button Component Props
|
||||||
|
export type ButtonType = 'default' | 'primary' | 'dashed' | 'link' | 'text'
|
||||||
|
export type ButtonSize = 'small' | 'medium' | 'large'
|
||||||
|
|
||||||
export interface ButtonProps {
|
export interface ButtonProps {
|
||||||
type?: ButtonType
|
type?: ButtonType
|
||||||
size?: ButtonSize
|
size?: ButtonSize
|
||||||
|
|||||||
@ -1,12 +1,14 @@
|
|||||||
export interface WalletState {
|
export interface WalletState {
|
||||||
seedPhrase: string[] | null
|
seedPhrase: string[] | null
|
||||||
password: string | null
|
password?: string | null
|
||||||
receiverId: string | null
|
receiverId: string | null
|
||||||
viewKey: string | null
|
viewKey: string | null
|
||||||
|
spendingKey?: string | null
|
||||||
address: string | null
|
address: string | null
|
||||||
network: 'mainnet' | 'testnet'
|
network: 'mainnet' | 'testnet'
|
||||||
balance: string | null
|
balance?: string | null
|
||||||
utxos: any[]
|
pendingBalance?: string | null
|
||||||
|
utxos?: Utxo[]
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GenerateSeedResult {
|
export interface GenerateSeedResult {
|
||||||
@ -16,6 +18,26 @@ export interface GenerateSeedResult {
|
|||||||
|
|
||||||
export interface ViewKeyResult {
|
export interface ViewKeyResult {
|
||||||
receiver_identifier: string
|
receiver_identifier: string
|
||||||
view_key: string
|
spending_key_hex: string
|
||||||
address: string
|
view_key_hex: string
|
||||||
|
success?: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadBuildTransaction {
|
||||||
|
spendingKeyHex?: string
|
||||||
|
inputAdditionRecords?: string[]
|
||||||
|
outputAddresses?: string[]
|
||||||
|
outputAmounts?: string[]
|
||||||
|
fee?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PayloadBroadcastSignedTransaction {
|
||||||
|
transactionHex: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Utxo {
|
||||||
|
additionRecord: string
|
||||||
|
amount: string
|
||||||
|
blockHeight: number
|
||||||
|
utxoHash: string
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import { createApp } from 'vue'
|
|||||||
import { createPinia } from 'pinia'
|
import { createPinia } from 'pinia'
|
||||||
import i18n from '@/lang'
|
import i18n from '@/lang'
|
||||||
import App from './App.vue'
|
import App from './App.vue'
|
||||||
import Antd from 'ant-design-vue'
|
|
||||||
import router from './router'
|
import router from './router'
|
||||||
import 'ant-design-vue/dist/reset.css'
|
import 'ant-design-vue/dist/reset.css'
|
||||||
import './assets/scss/main.scss'
|
import './assets/scss/main.scss'
|
||||||
@ -12,12 +11,11 @@ const app = createApp(App)
|
|||||||
app.use(i18n)
|
app.use(i18n)
|
||||||
app.use(createPinia())
|
app.use(createPinia())
|
||||||
app.use(router)
|
app.use(router)
|
||||||
app.use(Antd)
|
|
||||||
|
|
||||||
// Hide 'DOMNodeInserted'
|
// Hide 'DOMNodeInserted'
|
||||||
const originalAddEventListener = Element.prototype.addEventListener
|
const originalAddEventListener = Element.prototype.addEventListener
|
||||||
Element.prototype.addEventListener = function (type: any, listener: any, options: any) {
|
Element.prototype.addEventListener = function (type: any, listener: any, options: any) {
|
||||||
if (type === 'DOMNodeInserted') return // Ignore this event type
|
if (type === 'DOMNodeInserted') return
|
||||||
return originalAddEventListener.call(this, type, listener, options)
|
return originalAddEventListener.call(this, type, listener, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import { ref, computed } from 'vue'
|
|||||||
import type { WalletState } from '@/interface'
|
import type { WalletState } from '@/interface'
|
||||||
|
|
||||||
export const useNeptuneStore = defineStore('neptune', () => {
|
export const useNeptuneStore = defineStore('neptune', () => {
|
||||||
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'testnet') as 'mainnet' | 'testnet'
|
const defaultNetwork = (import.meta.env.VITE_NODE_NETWORK || 'mainnet') as 'mainnet' | 'testnet'
|
||||||
|
|
||||||
// ===== STATE =====
|
// ===== STATE =====
|
||||||
const wallet = ref<WalletState>({
|
const wallet = ref<WalletState>({
|
||||||
@ -11,9 +11,11 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
password: null,
|
password: null,
|
||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
|
spendingKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
network: defaultNetwork,
|
network: defaultNetwork,
|
||||||
balance: null,
|
balance: null,
|
||||||
|
pendingBalance: null,
|
||||||
utxos: [],
|
utxos: [],
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -37,6 +39,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
wallet.value.viewKey = viewKey
|
wallet.value.viewKey = viewKey
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setSpendingKey = (spendingKey: string | null) => {
|
||||||
|
wallet.value.spendingKey = spendingKey
|
||||||
|
}
|
||||||
|
|
||||||
const setAddress = (address: string | null) => {
|
const setAddress = (address: string | null) => {
|
||||||
wallet.value.address = address
|
wallet.value.address = address
|
||||||
}
|
}
|
||||||
@ -49,6 +55,10 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
wallet.value.balance = balance
|
wallet.value.balance = balance
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const setPendingBalance = (pendingBalance: string | null) => {
|
||||||
|
wallet.value.pendingBalance = pendingBalance
|
||||||
|
}
|
||||||
|
|
||||||
const setUtxos = (utxos: any[]) => {
|
const setUtxos = (utxos: any[]) => {
|
||||||
wallet.value.utxos = utxos
|
wallet.value.utxos = utxos
|
||||||
}
|
}
|
||||||
@ -67,9 +77,11 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
password: null,
|
password: null,
|
||||||
receiverId: null,
|
receiverId: null,
|
||||||
viewKey: null,
|
viewKey: null,
|
||||||
|
spendingKey: null,
|
||||||
address: null,
|
address: null,
|
||||||
network: defaultNetwork,
|
network: defaultNetwork,
|
||||||
balance: null,
|
balance: null,
|
||||||
|
pendingBalance: null,
|
||||||
utxos: [],
|
utxos: [],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -81,9 +93,11 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
const getPassword = computed(() => wallet.value.password)
|
const getPassword = computed(() => wallet.value.password)
|
||||||
const getReceiverId = computed(() => wallet.value.receiverId)
|
const getReceiverId = computed(() => wallet.value.receiverId)
|
||||||
const getViewKey = computed(() => wallet.value.viewKey)
|
const getViewKey = computed(() => wallet.value.viewKey)
|
||||||
|
const getSpendingKey = computed(() => wallet.value.spendingKey)
|
||||||
const getAddress = computed(() => wallet.value.address)
|
const getAddress = computed(() => wallet.value.address)
|
||||||
const getNetwork = computed(() => wallet.value.network)
|
const getNetwork = computed(() => wallet.value.network)
|
||||||
const getBalance = computed(() => wallet.value.balance)
|
const getBalance = computed(() => wallet.value.balance)
|
||||||
|
const getPendingBalance = computed(() => wallet.value.pendingBalance)
|
||||||
const getUtxos = computed(() => wallet.value.utxos)
|
const getUtxos = computed(() => wallet.value.utxos)
|
||||||
const hasWallet = computed(() => wallet.value.address !== null)
|
const hasWallet = computed(() => wallet.value.address !== null)
|
||||||
const getKeystorePath = computed(() => keystorePath.value)
|
const getKeystorePath = computed(() => keystorePath.value)
|
||||||
@ -94,9 +108,11 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
getPassword,
|
getPassword,
|
||||||
getReceiverId,
|
getReceiverId,
|
||||||
getViewKey,
|
getViewKey,
|
||||||
|
getSpendingKey,
|
||||||
getAddress,
|
getAddress,
|
||||||
getNetwork,
|
getNetwork,
|
||||||
getBalance,
|
getBalance,
|
||||||
|
getPendingBalance,
|
||||||
getUtxos,
|
getUtxos,
|
||||||
hasWallet,
|
hasWallet,
|
||||||
getKeystorePath,
|
getKeystorePath,
|
||||||
@ -104,9 +120,11 @@ export const useNeptuneStore = defineStore('neptune', () => {
|
|||||||
setPassword,
|
setPassword,
|
||||||
setReceiverId,
|
setReceiverId,
|
||||||
setViewKey,
|
setViewKey,
|
||||||
|
setSpendingKey,
|
||||||
setAddress,
|
setAddress,
|
||||||
setNetwork,
|
setNetwork,
|
||||||
setBalance,
|
setBalance,
|
||||||
|
setPendingBalance,
|
||||||
setUtxos,
|
setUtxos,
|
||||||
setWallet,
|
setWallet,
|
||||||
setKeystorePath,
|
setKeystorePath,
|
||||||
|
|||||||
@ -1,2 +1,3 @@
|
|||||||
export const PAGE_FIRST = 1
|
export const PAGE_FIRST = 1
|
||||||
export const PER_PAGE = 40
|
export const PER_PAGE = 20
|
||||||
|
export const POLLING_INTERVAL = 1000 * 60 // 1 minute
|
||||||
|
|||||||
@ -33,7 +33,6 @@ const handleGoToRecover = () => {
|
|||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.auth-container {
|
.auth-container {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
background: var(--bg-light);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.complete-state {
|
.complete-state {
|
||||||
|
|||||||
@ -4,7 +4,6 @@ import { PasswordForm } from '@/components'
|
|||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
import { useAuthStore } from '@/stores/authStore'
|
import { useAuthStore } from '@/stores/authStore'
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
|
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
@ -38,74 +37,7 @@ const handleNewWallet = () => {
|
|||||||
<div class="auth-card-header">
|
<div class="auth-card-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<div class="logo-circle">
|
<div class="logo-circle">
|
||||||
<svg
|
<img src="@/assets/imgs/logo.png" alt="Neptune Logo" class="neptune-logo" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
class="neptune-logo"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="neptuneGradientLogin"
|
|
||||||
x1="0%"
|
|
||||||
y1="0%"
|
|
||||||
x2="100%"
|
|
||||||
y2="100%"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 1"
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
style="stop-color: #0066a6; stop-opacity: 1"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient
|
|
||||||
id="ringGradientLogin"
|
|
||||||
x1="0%"
|
|
||||||
y1="0%"
|
|
||||||
x2="100%"
|
|
||||||
y2="0%"
|
|
||||||
>
|
|
||||||
<stop
|
|
||||||
offset="0%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 0.3"
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="50%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 0.6"
|
|
||||||
/>
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 0.3"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradientLogin)" />
|
|
||||||
|
|
||||||
<ellipse
|
|
||||||
cx="50"
|
|
||||||
cy="45"
|
|
||||||
rx="22"
|
|
||||||
ry="6"
|
|
||||||
fill="rgba(255, 255, 255, 0.1)"
|
|
||||||
/>
|
|
||||||
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
|
||||||
|
|
||||||
<ellipse
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
rx="42"
|
|
||||||
ry="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="url(#ringGradientLogin)"
|
|
||||||
stroke-width="4"
|
|
||||||
opacity="0.8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="logo-text">
|
<div class="logo-text">
|
||||||
<span class="coin-name">Neptune</span>
|
<span class="coin-name">Neptune</span>
|
||||||
@ -172,6 +104,8 @@ const handleNewWallet = () => {
|
|||||||
.neptune-logo {
|
.neptune-logo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { defineEmits, computed } from 'vue'
|
import { computed } from 'vue'
|
||||||
import { ButtonCommon } from '@/components'
|
import { ButtonCommon } from '@/components'
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineEmits, onMounted, computed } from 'vue'
|
import { ref, onMounted, computed } from 'vue'
|
||||||
import { ButtonCommon, CardBase } from '@/components'
|
import { ButtonCommon } from '@/components'
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
|
||||||
|
|||||||
@ -71,50 +71,7 @@ const handleIHaveWallet = () => {
|
|||||||
<div class="auth-card-header">
|
<div class="auth-card-header">
|
||||||
<div class="logo-container">
|
<div class="logo-container">
|
||||||
<div class="logo-circle">
|
<div class="logo-circle">
|
||||||
<svg
|
<img src="@/assets/imgs/logo.png" alt="Neptune Logo" class="neptune-logo" />
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 100 100"
|
|
||||||
class="neptune-logo"
|
|
||||||
>
|
|
||||||
<defs>
|
|
||||||
<linearGradient
|
|
||||||
id="neptuneGradient"
|
|
||||||
x1="0%"
|
|
||||||
y1="0%"
|
|
||||||
x2="100%"
|
|
||||||
y2="100%"
|
|
||||||
>
|
|
||||||
<stop offset="0%" style="stop-color: #007fcf; stop-opacity: 1" />
|
|
||||||
<stop offset="100%" style="stop-color: #0066a6; stop-opacity: 1" />
|
|
||||||
</linearGradient>
|
|
||||||
<linearGradient id="ringGradient" x1="0%" y1="0%" x2="100%" y2="0%">
|
|
||||||
<stop offset="0%" style="stop-color: #007fcf; stop-opacity: 0.3" />
|
|
||||||
<stop offset="50%" style="stop-color: #007fcf; stop-opacity: 0.6" />
|
|
||||||
<stop
|
|
||||||
offset="100%"
|
|
||||||
style="stop-color: #007fcf; stop-opacity: 0.3"
|
|
||||||
/>
|
|
||||||
</linearGradient>
|
|
||||||
</defs>
|
|
||||||
|
|
||||||
<circle cx="50" cy="50" r="28" fill="url(#neptuneGradient)" />
|
|
||||||
|
|
||||||
<ellipse cx="50" cy="45" rx="22" ry="6" fill="rgba(255, 255, 255, 0.1)" />
|
|
||||||
<ellipse cx="50" cy="55" rx="20" ry="5" fill="rgba(0, 0, 0, 0.1)" />
|
|
||||||
|
|
||||||
<ellipse
|
|
||||||
cx="50"
|
|
||||||
cy="50"
|
|
||||||
rx="42"
|
|
||||||
ry="12"
|
|
||||||
fill="none"
|
|
||||||
stroke="url(#ringGradient)"
|
|
||||||
stroke-width="4"
|
|
||||||
opacity="0.8"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<circle cx="42" cy="42" r="6" fill="rgba(255, 255, 255, 0.4)" />
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="logo-text">
|
<div class="logo-text">
|
||||||
<span class="coin-name">Neptune</span>
|
<span class="coin-name">Neptune</span>
|
||||||
@ -237,6 +194,8 @@ const handleIHaveWallet = () => {
|
|||||||
.neptune-logo {
|
.neptune-logo {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
object-fit: contain;
|
||||||
|
border-radius: 50%;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, defineEmits, onMounted } from 'vue'
|
import { ref, onMounted } from 'vue'
|
||||||
import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
|
import { SeedPhraseDisplayComponent, ConfirmSeedComponent } from '..'
|
||||||
import { CreatePasswordStep, WalletCreatedStep } from '.'
|
import { CreatePasswordStep, WalletCreatedStep } from '.'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
|
|||||||
@ -42,30 +42,7 @@ const handleCreateAnother = () => {
|
|||||||
keystore file should only be used in an offline setting.
|
keystore file should only be used in an offline setting.
|
||||||
</p>
|
</p>
|
||||||
<div class="center-svg" style="margin: 14px auto 12px auto">
|
<div class="center-svg" style="margin: 14px auto 12px auto">
|
||||||
<svg width="180" height="95" viewBox="0 0 175 92" fill="none">
|
<img src="@/assets/imgs/logo.png" alt="Neptune Logo" style="max-width: 180px; height: auto;" />
|
||||||
<rect x="111" y="37" width="64" height="33" rx="7" fill="#23B1EC" />
|
|
||||||
<rect
|
|
||||||
x="30.5"
|
|
||||||
y="37.5"
|
|
||||||
width="80"
|
|
||||||
height="46"
|
|
||||||
rx="7.5"
|
|
||||||
fill="#D6F9FE"
|
|
||||||
stroke="#AEEBF8"
|
|
||||||
stroke-width="5"
|
|
||||||
/>
|
|
||||||
<rect x="56" y="67" width="32" height="10" rx="3" fill="#B0F3A6" />
|
|
||||||
<rect x="46" y="49" width="52" height="12" rx="3" fill="#a2d2f5" />
|
|
||||||
<circle cx="155" cy="52" r="8" fill="#fff" />
|
|
||||||
<rect x="121" y="43" width="27" height="7" rx="1.5" fill="#5AE9D2" />
|
|
||||||
<rect x="128" y="59" width="17" height="4" rx="1.5" fill="#FCEBBA" />
|
|
||||||
<circle cx="40" cy="27" r="7" fill="#A2D2F5" />
|
|
||||||
<g>
|
|
||||||
<circle cx="128" cy="21" r="3" fill="#FF8585" />
|
|
||||||
<circle cx="57.5" cy="20.5" r="1.5" fill="#67DEFF" />
|
|
||||||
<rect x="95" y="18" width="7" height="5" rx="2" fill="#A2D2F5" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="btn-row">
|
<div class="btn-row">
|
||||||
<ButtonCommon
|
<ButtonCommon
|
||||||
|
|||||||
@ -37,6 +37,7 @@ const handlePasswordSubmit = async (password: string) => {
|
|||||||
const result = await recoverWalletFromSeed(seedPhrase.value)
|
const result = await recoverWalletFromSeed(seedPhrase.value)
|
||||||
if (result.address) {
|
if (result.address) {
|
||||||
await createKeystore(seedPhrase.value.join(' '), password)
|
await createKeystore(seedPhrase.value.join(' '), password)
|
||||||
|
message.success('Loading wallet...')
|
||||||
emit('accessWallet')
|
emit('accessWallet')
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
@ -99,6 +100,7 @@ const handleCancel = () => {
|
|||||||
placeholder="Enter password to encrypt seed phrase"
|
placeholder="Enter password to encrypt seed phrase"
|
||||||
label="Password"
|
label="Password"
|
||||||
:loading="isLoading"
|
:loading="isLoading"
|
||||||
|
:validate-format="true"
|
||||||
@submit="handlePasswordSubmit"
|
@submit="handlePasswordSubmit"
|
||||||
@back="handlePasswordBack"
|
@back="handlePasswordBack"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -1,14 +1,20 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted, ref } from 'vue'
|
import { onMounted, ref } from 'vue'
|
||||||
import { Tabs } from 'ant-design-vue'
|
import { TabsCommon, TabPaneCommon } from '@/components'
|
||||||
import { WalletTab, NetworkTab, UTXOTab } from './components'
|
import { WalletTab, NetworkTab, UTXOTab } from './components'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
|
|
||||||
const neptuneWallet = useNeptuneWallet()
|
const neptuneWallet = useNeptuneWallet()
|
||||||
|
|
||||||
const activeTab = ref('UTXOs')
|
const activeTab = ref('WALLET')
|
||||||
const network = ref('neptune-mainnet')
|
const network = ref('neptune-mainnet')
|
||||||
|
|
||||||
|
const tabItems = [
|
||||||
|
{ key: 'WALLET', label: 'WALLET' },
|
||||||
|
{ key: 'UTXOs', label: 'UTXOs' },
|
||||||
|
{ key: 'NETWORK', label: 'NETWORK' },
|
||||||
|
]
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await neptuneWallet.getNetworkInfo()
|
await neptuneWallet.getNetworkInfo()
|
||||||
})
|
})
|
||||||
@ -16,22 +22,22 @@ onMounted(async () => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="home-container">
|
<div class="home-container">
|
||||||
<Tabs v-model:activeKey="activeTab" size="large" class="main-tabs">
|
<TabsCommon v-model="activeTab" :items="tabItems" size="large" class="main-tabs">
|
||||||
<!-- DEBUG TAB -->
|
<!-- WALLET TAB -->
|
||||||
<Tabs.TabPane key="UTXOs" tab="UTXOs">
|
<TabPaneCommon tab-key="WALLET">
|
||||||
<UTXOTab />
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<!-- WALLET TAB -->
|
|
||||||
<Tabs.TabPane key="WALLET" tab="WALLET">
|
|
||||||
<WalletTab :network="network" />
|
<WalletTab :network="network" />
|
||||||
</Tabs.TabPane>
|
</TabPaneCommon>
|
||||||
|
|
||||||
|
<!-- UTXO TAB -->
|
||||||
|
<TabPaneCommon tab-key="UTXOs">
|
||||||
|
<UTXOTab />
|
||||||
|
</TabPaneCommon>
|
||||||
|
|
||||||
<!-- NETWORK TAB -->
|
<!-- NETWORK TAB -->
|
||||||
<Tabs.TabPane key="NETWORK" tab="NETWORK">
|
<TabPaneCommon tab-key="NETWORK">
|
||||||
<NetworkTab />
|
<NetworkTab />
|
||||||
</Tabs.TabPane>
|
</TabPaneCommon>
|
||||||
</Tabs>
|
</TabsCommon>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -51,47 +57,7 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:deep(.main-tabs) {
|
.main-tabs {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.ant-tabs-nav {
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
flex-shrink: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-tab {
|
|
||||||
font-size: 14px;
|
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
letter-spacing: var(--tracking-wide);
|
|
||||||
padding: 10px 16px;
|
|
||||||
|
|
||||||
@include screen(mobile) {
|
|
||||||
font-size: 12px;
|
|
||||||
padding: 8px 12px;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-ink-bar {
|
|
||||||
background: var(--primary-color);
|
|
||||||
height: var(--tabs-height);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-tab-active .ant-tabs-tab-btn {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
|
|
||||||
.ant-tabs-content {
|
|
||||||
flex: 1;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
|
|
||||||
.ant-tabs-tabpane {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -5,6 +5,7 @@ import { CardBase, SpinnerCommon } from '@/components'
|
|||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
|
import { POLLING_INTERVAL } from '@/utils'
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
const { getBlockHeight } = useNeptuneWallet()
|
const { getBlockHeight } = useNeptuneWallet()
|
||||||
@ -44,7 +45,7 @@ const retryConnection = async () => {
|
|||||||
const startPolling = () => {
|
const startPolling = () => {
|
||||||
pollingInterval = window.setInterval(async () => {
|
pollingInterval = window.setInterval(async () => {
|
||||||
if (!loading.value) await loadNetworkData()
|
if (!loading.value) await loadNetworkData()
|
||||||
}, 10000)
|
}, POLLING_INTERVAL)
|
||||||
}
|
}
|
||||||
|
|
||||||
const stopPolling = () => {
|
const stopPolling = () => {
|
||||||
@ -89,7 +90,7 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="status-item">
|
<div class="status-item">
|
||||||
<span class="status-label">DAA Score</span>
|
<span class="status-label">Block Height</span>
|
||||||
<span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span>
|
<span class="status-value">{{ formatNumberToLocaleString(blockHeight) }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@ -1,29 +1,81 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref, computed, onMounted } from 'vue'
|
||||||
import { EditOutlined } from '@ant-design/icons-vue'
|
import { Table, message } from 'ant-design-vue'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
import { CardBaseScrollable } from '@/components'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
|
import { CardBaseScrollable, SpinnerCommon } from '@/components'
|
||||||
|
import { PER_PAGE } from '@/utils'
|
||||||
|
import { columns } from '../utils'
|
||||||
|
|
||||||
const { getUtxos } = useNeptuneWallet()
|
const { getUtxos } = useNeptuneWallet()
|
||||||
|
const neptuneStore = useNeptuneStore()
|
||||||
|
|
||||||
const inUseUtxosCount = ref(0)
|
const loading = ref(false)
|
||||||
const inUseUtxosAmount = ref(0)
|
|
||||||
|
const utxosList = computed(() => [...(neptuneStore.getUtxos || []), ...Array.from({ length: 18 }, (_, i) => ({
|
||||||
|
additionRecord: `additionRecord${i}`,
|
||||||
|
amount: `${i}.00000000`,
|
||||||
|
blockHeight: `blockHeight${i}`,
|
||||||
|
utxoHash: `utxoHash${i}`,
|
||||||
|
}))])
|
||||||
|
|
||||||
|
const inUseUtxosCount = computed(() => (utxosList.value?.length ? utxosList.value.length : 0))
|
||||||
|
const inUseUtxosAmount = computed(() => {
|
||||||
|
if (!utxosList.value?.length) return '0.00000000'
|
||||||
|
|
||||||
|
const total = utxosList.value.reduce((total: number, utxo: any) => {
|
||||||
|
const amount = parseFloat(utxo.amount || utxo.value || 0)
|
||||||
|
return total + amount
|
||||||
|
}, 0)
|
||||||
|
|
||||||
|
return total.toFixed(8)
|
||||||
|
})
|
||||||
|
|
||||||
|
const loadUtxos = async () => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const result = await getUtxos()
|
||||||
|
if (result.error) {
|
||||||
|
console.error(result.error.message)
|
||||||
|
message.error('Failed to load UTXOs')
|
||||||
|
loading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loading.value = false
|
||||||
|
} catch (err) {
|
||||||
|
message.error('Failed to load UTXOs')
|
||||||
|
console.error('Error loading UTXOs:', err)
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
loadUtxos()
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBaseScrollable class="content-card debug-card">
|
<CardBaseScrollable class="content-card debug-card">
|
||||||
<div class="debug-header">
|
<div class="debug-header">
|
||||||
<h3 class="debug-title">
|
<h3 class="debug-title">IN USE UTXOS</h3>
|
||||||
IN USE UTXOS
|
|
||||||
<EditOutlined style="margin-left: 8px; font-size: 16px" />
|
|
||||||
</h3>
|
|
||||||
<div class="debug-info">
|
<div class="debug-info">
|
||||||
<p><strong>COUNT</strong> {{ inUseUtxosCount }}</p>
|
<p><span>COUNT</span> {{ inUseUtxosCount }}</p>
|
||||||
<p><strong>AMOUNT</strong> {{ inUseUtxosAmount }} NPT</p>
|
<p><span>AMOUNT</span> {{ inUseUtxosAmount }} <strong>NPT</strong></p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-pagination"></div>
|
<div v-if="loading" class="loading-container">
|
||||||
|
<SpinnerCommon />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="list-pagination">
|
||||||
|
<Table
|
||||||
|
:columns="columns"
|
||||||
|
:data-source="utxosList"
|
||||||
|
:scroll="{ x: 'max-content', y: '200px' }"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</CardBaseScrollable>
|
</CardBaseScrollable>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@ -32,14 +84,14 @@ const inUseUtxosAmount = ref(0)
|
|||||||
.debug-header {
|
.debug-header {
|
||||||
text-align: center;
|
text-align: center;
|
||||||
margin-bottom: var(--spacing-2xl);
|
margin-bottom: var(--spacing-2xl);
|
||||||
padding-bottom: var(--spacing-xl);
|
padding-bottom: var(--spacing-sm);
|
||||||
border-bottom: 2px solid var(--border-color);
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
|
||||||
.debug-title {
|
.debug-title {
|
||||||
font-size: var(--font-2xl);
|
font-size: var(--font-2xl);
|
||||||
font-weight: var(--font-bold);
|
font-weight: var(--font-bold);
|
||||||
color: var(--text-primary);
|
color: var(--text-primary);
|
||||||
margin-bottom: var(--spacing-lg);
|
margin-bottom: var(--spacing-xl);
|
||||||
letter-spacing: var(--tracking-wide);
|
letter-spacing: var(--tracking-wide);
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -48,20 +100,54 @@ const inUseUtxosAmount = ref(0)
|
|||||||
|
|
||||||
.debug-info {
|
.debug-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
justify-content: flex-start;
|
||||||
gap: var(--spacing-sm);
|
gap: var(--spacing-4xl);
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
font-size: var(--font-lg);
|
font-size: var(--font-lg);
|
||||||
color: var(--text-secondary);
|
color: var(--text-secondary);
|
||||||
|
|
||||||
strong {
|
span {
|
||||||
font-weight: var(--font-semibold);
|
color: var(--text-primary);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
margin-right: var(--spacing-sm);
|
margin-right: var(--spacing-sm);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.loading-container {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 300px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.list-pagination {
|
||||||
|
:deep(.ant-table) {
|
||||||
|
background: transparent;
|
||||||
|
|
||||||
|
.ant-table-thead > tr > th {
|
||||||
|
background: var(--bg-light);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr > td {
|
||||||
|
border-bottom: 1px solid var(--border-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ant-table-tbody > tr:hover > td {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.ant-pagination) {
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,3 +1,3 @@
|
|||||||
export { default as NetworkTab } from './NetworkTab.vue'
|
export { default as NetworkTab } from './NetworkTab.vue'
|
||||||
export { default as UTXOTab } from './UTXOTab.vue'
|
export { default as UTXOTab } from './UTXOTab.vue'
|
||||||
export { WalletInfo, WalletBalanceAndAddress, WalletTab } from './wallet-tab'
|
export { WalletInfo, WalletBalance, WalletAddress, WalletTab } from './wallet-tab'
|
||||||
|
|||||||
@ -0,0 +1,466 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { ButtonCommon, ModalCommon } from '@/components'
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
isLoading?: boolean
|
||||||
|
availableBalance?: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
isLoading: false,
|
||||||
|
availableBalance: '0.00000000',
|
||||||
|
})
|
||||||
|
|
||||||
|
const emit = defineEmits(['cancel', 'send'])
|
||||||
|
|
||||||
|
const outputAddresses = ref('')
|
||||||
|
const outputAmounts = ref('')
|
||||||
|
const fee = ref('')
|
||||||
|
const showConfirmModal = ref(false)
|
||||||
|
const isAddressExpanded = ref(false)
|
||||||
|
|
||||||
|
// Validation
|
||||||
|
const isAddressValid = computed(() => outputAddresses.value.trim().length > 0)
|
||||||
|
const isAmountValid = computed(() => {
|
||||||
|
if (!outputAmounts.value) return false
|
||||||
|
const num = parseFloat(outputAmounts.value)
|
||||||
|
if (isNaN(num) || num <= 0) return false
|
||||||
|
|
||||||
|
// Check if amount exceeds available balance
|
||||||
|
const balance = parseFloat(props.availableBalance)
|
||||||
|
if (!isNaN(balance) && num > balance) return false
|
||||||
|
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
const isFeeValid = computed(() => {
|
||||||
|
if (!fee.value) return false
|
||||||
|
const num = parseFloat(fee.value)
|
||||||
|
return !isNaN(num) && num >= 0
|
||||||
|
})
|
||||||
|
|
||||||
|
const amountErrorMessage = computed(() => {
|
||||||
|
if (!outputAmounts.value) return ''
|
||||||
|
const num = parseFloat(outputAmounts.value)
|
||||||
|
if (isNaN(num) || num <= 0) return 'Invalid amount'
|
||||||
|
|
||||||
|
const balance = parseFloat(props.availableBalance)
|
||||||
|
if (!isNaN(balance) && num > balance) {
|
||||||
|
return `Insufficient balance. Available: ${props.availableBalance} NPT`
|
||||||
|
}
|
||||||
|
|
||||||
|
return ''
|
||||||
|
})
|
||||||
|
|
||||||
|
const isFormValid = computed(
|
||||||
|
() => isAddressValid.value && isAmountValid.value && isFeeValid.value && !props.isLoading
|
||||||
|
)
|
||||||
|
|
||||||
|
// Format decimal for amount and fee
|
||||||
|
const formatDecimal = (value: string) => {
|
||||||
|
if (!value) return ''
|
||||||
|
|
||||||
|
// Remove any non-numeric characters except dot
|
||||||
|
let cleaned = value.replace(/[^\d.]/g, '')
|
||||||
|
|
||||||
|
// Ensure only one dot
|
||||||
|
const parts = cleaned.split('.')
|
||||||
|
if (parts.length > 2) {
|
||||||
|
cleaned = parts[0] + '.' + parts.slice(1).join('')
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a valid number and doesn't have a dot, add decimal point and zeros
|
||||||
|
if (cleaned && !cleaned.includes('.') && /^\d+$/.test(cleaned)) {
|
||||||
|
cleaned = cleaned + '.0'
|
||||||
|
}
|
||||||
|
|
||||||
|
// Limit to 8 decimal places
|
||||||
|
if (cleaned.includes('.')) {
|
||||||
|
const [integer, decimal] = cleaned.split('.')
|
||||||
|
cleaned = integer + '.' + decimal.slice(0, 8)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cleaned
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAmountBlur = () => {
|
||||||
|
if (outputAmounts.value) {
|
||||||
|
outputAmounts.value = formatDecimal(outputAmounts.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleFeeBlur = () => {
|
||||||
|
if (fee.value) {
|
||||||
|
fee.value = formatDecimal(fee.value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancel = () => {
|
||||||
|
emit('cancel')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSend = () => {
|
||||||
|
if (!isFormValid.value) return
|
||||||
|
showConfirmModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleConfirm = () => {
|
||||||
|
showConfirmModal.value = false
|
||||||
|
isAddressExpanded.value = false
|
||||||
|
emit('send', {
|
||||||
|
outputAddresses: outputAddresses.value.trim(),
|
||||||
|
outputAmounts: outputAmounts.value,
|
||||||
|
fee: fee.value,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelConfirm = () => {
|
||||||
|
showConfirmModal.value = false
|
||||||
|
isAddressExpanded.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const toggleAddressExpand = () => {
|
||||||
|
isAddressExpanded.value = !isAddressExpanded.value
|
||||||
|
}
|
||||||
|
|
||||||
|
const displayAddress = computed(() => {
|
||||||
|
if (!outputAddresses.value) return ''
|
||||||
|
if (isAddressExpanded.value) return outputAddresses.value
|
||||||
|
|
||||||
|
const address = outputAddresses.value
|
||||||
|
|
||||||
|
if (address.length <= 40) return address
|
||||||
|
return address.slice(0, 20) + '...' + address.slice(-20)
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="send-transaction-container">
|
||||||
|
<div class="send-transaction-form">
|
||||||
|
<!-- Recipient Address -->
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Recipient Address <span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<textarea
|
||||||
|
v-model="outputAddresses"
|
||||||
|
class="form-textarea"
|
||||||
|
:class="{ invalid: outputAddresses && !isAddressValid }"
|
||||||
|
placeholder="Type the recipient address"
|
||||||
|
rows="3"
|
||||||
|
:disabled="isLoading"
|
||||||
|
/>
|
||||||
|
<span v-if="outputAddresses && !isAddressValid" class="error-message">
|
||||||
|
Address is required
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Amount and Priority Fee Row -->
|
||||||
|
<div class="amount-row">
|
||||||
|
<div class="amount-field">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Amount <span class="required">*</span>
|
||||||
|
<span class="balance-info">Available: {{ availableBalance }} NPT</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="outputAmounts"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
:class="{ invalid: outputAmounts && !isAmountValid }"
|
||||||
|
placeholder="0.0"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@blur="handleAmountBlur"
|
||||||
|
/>
|
||||||
|
<span v-if="outputAmounts && amountErrorMessage" class="error-message">
|
||||||
|
{{ amountErrorMessage }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="priority-fee-field">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label">
|
||||||
|
Priority Fee <span class="required">*</span>
|
||||||
|
</label>
|
||||||
|
<input
|
||||||
|
v-model="fee"
|
||||||
|
type="text"
|
||||||
|
class="form-input"
|
||||||
|
:class="{ invalid: fee && !isFeeValid }"
|
||||||
|
placeholder="0.0"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@blur="handleFeeBlur"
|
||||||
|
/>
|
||||||
|
<span v-if="fee && !isFeeValid" class="error-message"> Invalid fee </span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="action-buttons">
|
||||||
|
<ButtonCommon
|
||||||
|
type="default"
|
||||||
|
size="large"
|
||||||
|
:disabled="isLoading"
|
||||||
|
@click="handleCancel"
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
:disabled="!isFormValid"
|
||||||
|
@click="handleSend"
|
||||||
|
>
|
||||||
|
SEND
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Confirm Modal -->
|
||||||
|
<ModalCommon
|
||||||
|
v-model:open="showConfirmModal"
|
||||||
|
title="Confirm Transaction"
|
||||||
|
:footer="false"
|
||||||
|
width="500px"
|
||||||
|
:mask-closable="false"
|
||||||
|
:keyboard="false"
|
||||||
|
@cancel="handleCancelConfirm"
|
||||||
|
>
|
||||||
|
<div class="confirm-modal-content">
|
||||||
|
<div class="confirm-section">
|
||||||
|
<h4 class="confirm-label">Recipient Address:</h4>
|
||||||
|
<div class="confirm-value address-value" :class="{ expanded: isAddressExpanded }">
|
||||||
|
<span class="address-text">{{ displayAddress }}</span>
|
||||||
|
<button
|
||||||
|
v-if="outputAddresses.length > 40"
|
||||||
|
class="toggle-btn"
|
||||||
|
@click="toggleAddressExpand"
|
||||||
|
>
|
||||||
|
{{ isAddressExpanded ? 'Show Less' : 'Show More' }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="confirm-row">
|
||||||
|
<div class="confirm-section">
|
||||||
|
<h4 class="confirm-label">Amount:</h4>
|
||||||
|
<div class="confirm-value">{{ outputAmounts }} NPT</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="confirm-section">
|
||||||
|
<h4 class="confirm-label">Priority Fee:</h4>
|
||||||
|
<div class="confirm-value">{{ fee }} NPT</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="confirm-actions">
|
||||||
|
<ButtonCommon type="default" size="large" @click="handleCancelConfirm">
|
||||||
|
Cancel
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon type="primary" size="large" @click="handleConfirm">
|
||||||
|
Confirm & Send
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</ModalCommon>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.send-transaction-container {
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-transaction-form {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-primary);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
.required {
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-info {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-regular);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-input,
|
||||||
|
.form-textarea {
|
||||||
|
padding: var(--spacing-sm) var(--spacing-md);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-base);
|
||||||
|
color: var(--text-primary);
|
||||||
|
background: var(--bg-primary);
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
border-color: var(--primary-color);
|
||||||
|
box-shadow: 0 0 0 2px rgba(66, 165, 245, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:disabled {
|
||||||
|
background: var(--bg-disabled);
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.invalid {
|
||||||
|
border-color: var(--error-color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-textarea {
|
||||||
|
resize: vertical;
|
||||||
|
min-height: 80px;
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
line-height: 1.5;
|
||||||
|
&::placeholder {
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error-message {
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
color: var(--error-color);
|
||||||
|
margin-top: -4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.amount-field,
|
||||||
|
.priority-fee-field {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-lg);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screen(mobile) {
|
||||||
|
.amount-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-modal-content {
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-label {
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-value {
|
||||||
|
font-size: var(--font-base);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
padding: var(--spacing-md);
|
||||||
|
background: var(--bg-secondary);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-value {
|
||||||
|
font-family: 'Courier New', monospace;
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
line-height: 1.6;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
word-break: break-all;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toggle-btn {
|
||||||
|
align-self: flex-end;
|
||||||
|
padding: var(--spacing-xs) var(--spacing-sm);
|
||||||
|
background: var(--primary-color);
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: var(--font-xs);
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: all 0.2s ease;
|
||||||
|
font-family: var(--font-primary);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary-hover);
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:active {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-row {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 1fr 1fr;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
.confirm-actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: var(--spacing-md);
|
||||||
|
margin-top: var(--spacing-md);
|
||||||
|
}
|
||||||
|
|
||||||
|
@include screen(mobile) {
|
||||||
|
.confirm-row {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
133
src/views/Home/components/wallet-tab/WalletAddress.vue
Normal file
133
src/views/Home/components/wallet-tab/WalletAddress.vue
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import { message } from 'ant-design-vue'
|
||||||
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
|
|
||||||
|
const neptuneStore = useNeptuneStore()
|
||||||
|
|
||||||
|
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
||||||
|
|
||||||
|
const copyAddress = async () => {
|
||||||
|
if (!receiveAddress.value) {
|
||||||
|
message.error('No address available')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await navigator.clipboard.writeText(receiveAddress.value)
|
||||||
|
message.success('Address copied to clipboard!')
|
||||||
|
} catch (err) {
|
||||||
|
message.error('Failed to copy address')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div v-if="!receiveAddress" class="empty-state">
|
||||||
|
<p>No address available</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-else class="receive-section">
|
||||||
|
<div class="address-label">Receive Address:</div>
|
||||||
|
<div class="address-value" @click="copyAddress">
|
||||||
|
<span class="address-text">
|
||||||
|
{{ receiveAddress }}
|
||||||
|
</span>
|
||||||
|
<svg
|
||||||
|
class="copy-icon"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
fill="none"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
>
|
||||||
|
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
||||||
|
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.empty-state {
|
||||||
|
text-align: center;
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.receive-section {
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.address-label {
|
||||||
|
font-size: var(--font-base);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
margin-bottom: var(--spacing-md);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-value {
|
||||||
|
background: var(--bg-light);
|
||||||
|
padding: var(--spacing-lg);
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
word-break: break-all;
|
||||||
|
font-family: var(--font-mono);
|
||||||
|
font-size: var(--font-sm);
|
||||||
|
color: var(--primary-color);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: var(--transition-all);
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
border: 2px solid transparent;
|
||||||
|
line-height: 1.5;
|
||||||
|
position: relative;
|
||||||
|
height: 120px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.address-text {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
overflow-x: hidden;
|
||||||
|
max-height: 100%;
|
||||||
|
padding-right: var(--spacing-xs);
|
||||||
|
|
||||||
|
&::-webkit-scrollbar {
|
||||||
|
width: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-track {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::-webkit-scrollbar-thumb {
|
||||||
|
background: var(--border-color);
|
||||||
|
border-radius: 3px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--text-muted);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--bg-hover);
|
||||||
|
border-color: var(--border-primary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
color: var(--primary-color);
|
||||||
|
margin-top: 2px;
|
||||||
|
align-self: flex-start;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
69
src/views/Home/components/wallet-tab/WalletBalance.vue
Normal file
69
src/views/Home/components/wallet-tab/WalletBalance.vue
Normal file
@ -0,0 +1,69 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
interface Props {
|
||||||
|
isLoadingData: boolean
|
||||||
|
availableBalance: string
|
||||||
|
pendingBalance: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="balance-section">
|
||||||
|
<div class="balance-label">Available</div>
|
||||||
|
<div class="balance-amount">
|
||||||
|
<span v-if="props.isLoadingData"><SpinnerCommon size="medium" /></span>
|
||||||
|
<span v-else>{{ props.availableBalance }} NPT</span>
|
||||||
|
</div>
|
||||||
|
<div class="pending-section">
|
||||||
|
<span class="pending-label">Pending</span>
|
||||||
|
<span class="pending-amount">
|
||||||
|
{{ props.isLoadingData ? '...' : props.pendingBalance }}
|
||||||
|
NPT
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
.balance-section {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-xl);
|
||||||
|
padding-bottom: var(--spacing-lg);
|
||||||
|
border-bottom: 2px solid var(--border-color);
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
.balance-label {
|
||||||
|
color: var(--text-muted);
|
||||||
|
font-size: var(--font-base);
|
||||||
|
margin-bottom: var(--spacing-sm);
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: var(--tracking-wider);
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-amount {
|
||||||
|
font-size: var(--font-4xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin-bottom: var(--spacing-lg);
|
||||||
|
letter-spacing: var(--tracking-tight);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-section {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-sm);
|
||||||
|
color: var(--text-secondary);
|
||||||
|
font-size: var(--font-md);
|
||||||
|
|
||||||
|
.pending-label {
|
||||||
|
font-weight: var(--font-medium);
|
||||||
|
}
|
||||||
|
|
||||||
|
.pending-amount {
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,239 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { ref, computed } from 'vue'
|
|
||||||
import { message } from 'ant-design-vue'
|
|
||||||
import { formatNumberToLocaleString } from '@/utils'
|
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
|
||||||
import { SpinnerCommon } from '@/components'
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
isLoadingData: boolean
|
|
||||||
availableBalance: number
|
|
||||||
pendingBalance: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const props = defineProps<Props>()
|
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
|
||||||
|
|
||||||
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
|
||||||
const isAddressExpanded = ref(false)
|
|
||||||
|
|
||||||
const toggleAddressExpanded = () => {
|
|
||||||
isAddressExpanded.value = !isAddressExpanded.value
|
|
||||||
}
|
|
||||||
|
|
||||||
const copyAddress = async () => {
|
|
||||||
if (!receiveAddress.value) {
|
|
||||||
message.error('No address available')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await navigator.clipboard.writeText(receiveAddress.value)
|
|
||||||
message.success('Address copied to clipboard!')
|
|
||||||
} catch (err) {
|
|
||||||
message.error('Failed to copy address')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div v-if="props.isLoadingData && !receiveAddress" class="loading-state">
|
|
||||||
<SpinnerCommon size="medium" />
|
|
||||||
<p>Loading wallet data...</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else-if="!receiveAddress" class="empty-state">
|
|
||||||
<p>No wallet found. Please create or import a wallet.</p>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div v-else>
|
|
||||||
<!-- Balance Section -->
|
|
||||||
<div class="balance-section">
|
|
||||||
<div class="balance-label">Available</div>
|
|
||||||
<div class="balance-amount">
|
|
||||||
<span v-if="props.isLoadingData">Loading...</span>
|
|
||||||
<span v-else>{{ formatNumberToLocaleString(props.availableBalance) }} NPT</span>
|
|
||||||
</div>
|
|
||||||
<div class="pending-section">
|
|
||||||
<span class="pending-label">Pending</span>
|
|
||||||
<span class="pending-amount">
|
|
||||||
{{ props.isLoadingData ? '...' : formatNumberToLocaleString(props.pendingBalance) }}
|
|
||||||
NPT
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Receive Address Section -->
|
|
||||||
<div class="receive-section">
|
|
||||||
<div class="address-label">Receive Address:</div>
|
|
||||||
<div
|
|
||||||
class="address-value"
|
|
||||||
:class="{ expanded: isAddressExpanded, collapsed: !isAddressExpanded }"
|
|
||||||
@click="copyAddress"
|
|
||||||
>
|
|
||||||
<span class="address-text">
|
|
||||||
{{ receiveAddress || 'No address available' }}
|
|
||||||
</span>
|
|
||||||
<svg
|
|
||||||
class="copy-icon"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
fill="none"
|
|
||||||
stroke="currentColor"
|
|
||||||
stroke-width="2"
|
|
||||||
>
|
|
||||||
<rect x="9" y="9" width="13" height="13" rx="2" ry="2"></rect>
|
|
||||||
<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
<button
|
|
||||||
v-if="receiveAddress && receiveAddress.length > 80"
|
|
||||||
class="toggle-address-btn"
|
|
||||||
@click.stop="toggleAddressExpanded"
|
|
||||||
>
|
|
||||||
{{ isAddressExpanded ? 'Show less' : 'Show more' }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.loading-state,
|
|
||||||
.empty-state {
|
|
||||||
text-align: center;
|
|
||||||
padding: var(--spacing-3xl);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
|
|
||||||
p {
|
|
||||||
margin: var(--spacing-lg) 0 0;
|
|
||||||
font-size: var(--font-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-section {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: var(--spacing-xl);
|
|
||||||
padding-bottom: var(--spacing-lg);
|
|
||||||
border-bottom: 2px solid var(--border-color);
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.balance-label {
|
|
||||||
color: var(--text-muted);
|
|
||||||
font-size: var(--font-base);
|
|
||||||
margin-bottom: var(--spacing-sm);
|
|
||||||
text-transform: uppercase;
|
|
||||||
letter-spacing: var(--tracking-wider);
|
|
||||||
}
|
|
||||||
|
|
||||||
.balance-amount {
|
|
||||||
font-size: var(--font-4xl);
|
|
||||||
font-weight: var(--font-bold);
|
|
||||||
color: var(--text-primary);
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
letter-spacing: var(--tracking-tight);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending-section {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
font-size: var(--font-md);
|
|
||||||
|
|
||||||
.pending-label {
|
|
||||||
font-weight: var(--font-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.pending-amount {
|
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.receive-section {
|
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
flex-shrink: 0;
|
|
||||||
|
|
||||||
.address-label {
|
|
||||||
font-size: var(--font-base);
|
|
||||||
color: var(--text-secondary);
|
|
||||||
margin-bottom: var(--spacing-md);
|
|
||||||
font-weight: var(--font-semibold);
|
|
||||||
}
|
|
||||||
|
|
||||||
.address-value {
|
|
||||||
background: var(--bg-light);
|
|
||||||
padding: var(--spacing-lg);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
word-break: break-all;
|
|
||||||
font-family: var(--font-mono);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
color: var(--primary-color);
|
|
||||||
cursor: pointer;
|
|
||||||
transition: var(--transition-all);
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
justify-content: space-between;
|
|
||||||
gap: var(--spacing-sm);
|
|
||||||
border: 2px solid transparent;
|
|
||||||
line-height: 1.5;
|
|
||||||
position: relative;
|
|
||||||
|
|
||||||
.address-text {
|
|
||||||
flex: 1;
|
|
||||||
display: -webkit-box;
|
|
||||||
-webkit-box-orient: vertical;
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.collapsed .address-text {
|
|
||||||
-webkit-line-clamp: 2;
|
|
||||||
line-clamp: 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
&.expanded .address-text {
|
|
||||||
display: block;
|
|
||||||
overflow: visible;
|
|
||||||
text-overflow: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
background: var(--bg-hover);
|
|
||||||
border-color: var(--border-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.copy-icon {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
flex-shrink: 0;
|
|
||||||
color: var(--primary-color);
|
|
||||||
margin-top: 2px;
|
|
||||||
align-self: flex-start;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.toggle-address-btn {
|
|
||||||
margin-top: var(--spacing-md);
|
|
||||||
padding: var(--spacing-xs) var(--spacing-md);
|
|
||||||
background: none;
|
|
||||||
border: none;
|
|
||||||
color: var(--primary-color);
|
|
||||||
font-size: var(--font-sm);
|
|
||||||
font-weight: var(--font-medium);
|
|
||||||
cursor: pointer;
|
|
||||||
text-decoration: underline;
|
|
||||||
transition: color 0.2s ease;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--primary-hover);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:active {
|
|
||||||
opacity: 0.8;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
|
|
||||||
@ -1,26 +1,32 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { Modal } from 'ant-design-vue'
|
|
||||||
import { useNeptuneStore } from '@/stores/neptuneStore'
|
import { useNeptuneStore } from '@/stores/neptuneStore'
|
||||||
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
import { useNeptuneWallet } from '@/composables/useNeptuneWallet'
|
||||||
import { message } from 'ant-design-vue'
|
import { message } from 'ant-design-vue'
|
||||||
import { ButtonCommon, CardBaseScrollable } from '@/components'
|
import { ButtonCommon, CardBaseScrollable, ModalCommon, SpinnerCommon, PasswordForm } from '@/components'
|
||||||
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
|
import SeedPhraseDisplayComponent from '@/views/Auth/components/SeedPhraseDisplayComponent.vue'
|
||||||
import WalletBalanceAndAddress from './WalletBalanceAndAddress.vue'
|
import SendTransactionComponent from './SendTransactionComponent.vue'
|
||||||
|
import { WalletAddress, WalletBalance } from '.'
|
||||||
|
import type { Utxo } from '@/interface'
|
||||||
|
|
||||||
const neptuneStore = useNeptuneStore()
|
const neptuneStore = useNeptuneStore()
|
||||||
const { getBalance, saveKeystoreAs } = useNeptuneWallet()
|
const {
|
||||||
|
getBalance,
|
||||||
|
saveKeystoreAs,
|
||||||
|
buildTransactionWithPrimitiveProof,
|
||||||
|
broadcastSignedTransaction,
|
||||||
|
decryptKeystore,
|
||||||
|
} = useNeptuneWallet()
|
||||||
|
|
||||||
const availableBalance = ref(0)
|
const availableBalance = ref<string>('0.00000000')
|
||||||
const pendingBalance = ref(0)
|
const pendingBalance = ref<string>('0.00000000')
|
||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const error = ref<string | null>(null)
|
const error = ref<string | null>(null)
|
||||||
const showSeedModal = ref(false)
|
const showSeedModal = ref(false)
|
||||||
|
const isSendingMode = ref(false)
|
||||||
const getModalContainer = (): HTMLElement => {
|
const isVerifyingPassword = ref(false)
|
||||||
const homeContainer = document.querySelector('.home-container') as HTMLElement
|
const passwordError = ref(false)
|
||||||
return homeContainer || document.body
|
const isVerifying = ref(false)
|
||||||
}
|
|
||||||
|
|
||||||
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
const receiveAddress = computed(() => neptuneStore.getWallet?.address || '')
|
||||||
|
|
||||||
@ -44,7 +50,47 @@ const handleResize = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleClickSendButton = () => {
|
const handleClickSendButton = () => {
|
||||||
// TODO: Implement send transaction functionality
|
isSendingMode.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelSend = () => {
|
||||||
|
isSendingMode.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleSendTransaction = async (data: {
|
||||||
|
outputAddresses: string
|
||||||
|
outputAmounts: string
|
||||||
|
fee: string
|
||||||
|
}) => {
|
||||||
|
try {
|
||||||
|
loading.value = true
|
||||||
|
const payload = {
|
||||||
|
spendingKeyHex: neptuneStore.getSpendingKey || '',
|
||||||
|
inputAdditionRecords: neptuneStore.getUtxos?.map((utxo: Utxo) => utxo.additionRecord),
|
||||||
|
outputAddresses: [data.outputAddresses],
|
||||||
|
outputAmounts: [data.outputAmounts],
|
||||||
|
fee: data.fee,
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await buildTransactionWithPrimitiveProof(payload)
|
||||||
|
if (!result.success) {
|
||||||
|
message.error('Failed to build transaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const broadcastResult = await broadcastSignedTransaction(result.transaction)
|
||||||
|
if (!broadcastResult.success) {
|
||||||
|
message.error('Failed to broadcast transaction')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
message.success('Transaction sent successfully!')
|
||||||
|
handleCancelSend()
|
||||||
|
await loadWalletData()
|
||||||
|
} catch (error) {
|
||||||
|
message.error('Failed to send transaction')
|
||||||
|
} finally {
|
||||||
|
loading.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleBackupFile = async () => {
|
const handleBackupFile = async () => {
|
||||||
@ -66,17 +112,34 @@ const handleBackupFile = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const handleBackupSeed = () => {
|
const handleBackupSeed = () => {
|
||||||
showSeedModal.value = true
|
isVerifyingPassword.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleCancelVerify = () => {
|
||||||
|
isVerifyingPassword.value = false
|
||||||
|
passwordError.value = false
|
||||||
|
}
|
||||||
|
|
||||||
|
const handlePasswordVerify = async (password: string) => {
|
||||||
|
try {
|
||||||
|
isVerifying.value = true
|
||||||
|
passwordError.value = false
|
||||||
|
|
||||||
|
await decryptKeystore(password)
|
||||||
|
|
||||||
|
isVerifyingPassword.value = false
|
||||||
|
showSeedModal.value = true
|
||||||
|
} catch (err) {
|
||||||
|
passwordError.value = true
|
||||||
|
} finally {
|
||||||
|
isVerifying.value = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleCloseModal = () => {
|
const handleCloseModal = () => {
|
||||||
showSeedModal.value = false
|
showSeedModal.value = false
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleModalNext = () => {
|
|
||||||
handleCloseModal()
|
|
||||||
}
|
|
||||||
|
|
||||||
const loadWalletData = async () => {
|
const loadWalletData = async () => {
|
||||||
const receiveAddress = neptuneStore.getWallet?.address || ''
|
const receiveAddress = neptuneStore.getWallet?.address || ''
|
||||||
if (!receiveAddress) return
|
if (!receiveAddress) return
|
||||||
@ -86,8 +149,8 @@ const loadWalletData = async () => {
|
|||||||
try {
|
try {
|
||||||
const result = await getBalance()
|
const result = await getBalance()
|
||||||
|
|
||||||
availableBalance.value = +result.confirmedAvailable || 0
|
availableBalance.value = result?.balance || result || '0.00000000'
|
||||||
pendingBalance.value = +result.unconfirmedAvailable || 0
|
pendingBalance.value = result?.pendingBalance || result || '0.00000000'
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('Failed to load wallet data')
|
message.error('Failed to load wallet data')
|
||||||
} finally {
|
} finally {
|
||||||
@ -107,58 +170,98 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<template>
|
<template>
|
||||||
<CardBaseScrollable class="wallet-info-container">
|
<CardBaseScrollable class="wallet-info-container">
|
||||||
<div class="wallet-content">
|
<!-- Normal Wallet View -->
|
||||||
<WalletBalanceAndAddress
|
<div v-if="!isSendingMode && !isVerifyingPassword" class="wallet-content">
|
||||||
:is-loading-data="loading"
|
<div class="balance-wrapper">
|
||||||
:available-balance="availableBalance"
|
<WalletBalance
|
||||||
:pending-balance="pendingBalance"
|
:is-loading-data="loading"
|
||||||
/>
|
:available-balance="availableBalance"
|
||||||
|
:pending-balance="pendingBalance"
|
||||||
<!-- Action Buttons -->
|
/>
|
||||||
<div v-if="receiveAddress" class="action-buttons">
|
|
||||||
<ButtonCommon
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
@click="handleClickSendButton"
|
|
||||||
class="btn-send"
|
|
||||||
>
|
|
||||||
SEND
|
|
||||||
</ButtonCommon>
|
|
||||||
<ButtonCommon type="primary" size="large" block @click="handleBackupFile">
|
|
||||||
Backup File
|
|
||||||
</ButtonCommon>
|
|
||||||
<ButtonCommon type="primary" size="large" block @click="handleBackupSeed">
|
|
||||||
Backup Seed
|
|
||||||
</ButtonCommon>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Wallet Status -->
|
<div v-if="receiveAddress" class="address-actions-row">
|
||||||
<div v-if="receiveAddress" class="wallet-status">
|
<div class="address-wrapper">
|
||||||
<span
|
<WalletAddress />
|
||||||
>Wallet Status: <strong>{{ walletStatus }}</strong></span
|
</div>
|
||||||
>
|
<div class="action-buttons">
|
||||||
|
<ButtonCommon
|
||||||
|
type="primary"
|
||||||
|
size="large"
|
||||||
|
block
|
||||||
|
@click="handleClickSendButton"
|
||||||
|
class="btn-send"
|
||||||
|
>
|
||||||
|
SEND
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon type="primary" size="large" block @click="handleBackupFile">
|
||||||
|
Backup File
|
||||||
|
</ButtonCommon>
|
||||||
|
<ButtonCommon type="primary" size="large" block @click="handleBackupSeed">
|
||||||
|
Backup Seed
|
||||||
|
</ButtonCommon>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Password Verification View -->
|
||||||
|
<div v-else-if="isVerifyingPassword" class="password-verify-wrapper">
|
||||||
|
<div class="password-verify-content">
|
||||||
|
<div class="verify-header">
|
||||||
|
<h2 class="verify-title">Verify Password</h2>
|
||||||
|
<p class="verify-subtitle">Enter your password to view seed phrase</p>
|
||||||
|
</div>
|
||||||
|
<PasswordForm
|
||||||
|
button-text="Verify"
|
||||||
|
placeholder="Enter your password"
|
||||||
|
back-button-text="Cancel"
|
||||||
|
label="Password"
|
||||||
|
:loading="isVerifying"
|
||||||
|
:error="passwordError"
|
||||||
|
error-message="Invalid password"
|
||||||
|
@submit="handlePasswordVerify"
|
||||||
|
@back="handleCancelVerify"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Send Transaction View -->
|
||||||
|
<div v-else-if="isSendingMode" class="send-transaction-wrapper">
|
||||||
|
<SendTransactionComponent
|
||||||
|
:is-loading="loading"
|
||||||
|
:available-balance="availableBalance"
|
||||||
|
@cancel="handleCancelSend"
|
||||||
|
@send="handleSendTransaction"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<!-- Loading Overlay -->
|
||||||
|
<div v-if="loading" class="sending-overlay">
|
||||||
|
<div class="sending-content">
|
||||||
|
<SpinnerCommon size="large" />
|
||||||
|
<p class="sending-text">Sending...</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="receiveAddress && !isSendingMode && !isVerifyingPassword" class="wallet-status">
|
||||||
|
<span
|
||||||
|
>Wallet Status: <strong>{{ walletStatus }}</strong></span
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</CardBaseScrollable>
|
</CardBaseScrollable>
|
||||||
<Modal
|
<ModalCommon
|
||||||
v-model:open="showSeedModal"
|
v-model:open="showSeedModal"
|
||||||
title="Backup Seed Phrase"
|
title="Backup Seed Phrase"
|
||||||
:footer="null"
|
:footer="false"
|
||||||
:width="modalWidth"
|
:width="modalWidth"
|
||||||
:mask-closable="false"
|
:mask-closable="false"
|
||||||
:keyboard="false"
|
:keyboard="false"
|
||||||
:get-container="getModalContainer"
|
|
||||||
@cancel="handleCloseModal"
|
@cancel="handleCloseModal"
|
||||||
>
|
>
|
||||||
<div class="seed-modal-content">
|
<div class="seed-modal-content">
|
||||||
<SeedPhraseDisplayComponent
|
<SeedPhraseDisplayComponent :back-button="false" :next-button="false" />
|
||||||
:back-button="false"
|
|
||||||
:next-button-text="'DONE'"
|
|
||||||
@next="handleModalNext"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</ModalCommon>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
@ -167,15 +270,36 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.wallet-content {
|
.wallet-content {
|
||||||
background: inherit;
|
display: flex;
|
||||||
height: 100%;
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.balance-wrapper {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.address-actions-row {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-xl);
|
||||||
|
align-items: center;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
|
||||||
|
.address-wrapper {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.action-buttons {
|
.action-buttons {
|
||||||
@include center_flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
gap: var(--spacing-md);
|
gap: var(--spacing-md);
|
||||||
margin-bottom: var(--spacing-lg);
|
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
min-width: 200px;
|
||||||
|
|
||||||
:deep(.btn-send) {
|
:deep(.btn-send) {
|
||||||
letter-spacing: var(--tracking-wide);
|
letter-spacing: var(--tracking-wide);
|
||||||
@ -214,16 +338,74 @@ onUnmounted(() => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Modal responsive width
|
.password-verify-wrapper {
|
||||||
:deep(.ant-modal) {
|
display: flex;
|
||||||
@media (max-width: 767px) {
|
align-items: center;
|
||||||
width: 90% !important;
|
justify-content: center;
|
||||||
max-width: 90% !important;
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
padding: var(--spacing-xl);
|
||||||
|
}
|
||||||
|
|
||||||
|
.password-verify-content {
|
||||||
|
width: 100%;
|
||||||
|
max-width: 480px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.verify-header {
|
||||||
|
text-align: center;
|
||||||
|
margin-bottom: var(--spacing-2xl);
|
||||||
|
|
||||||
|
.verify-title {
|
||||||
|
font-size: var(--font-2xl);
|
||||||
|
font-weight: var(--font-bold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
margin: 0 0 var(--spacing-xs) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (min-width: 768px) {
|
.verify-subtitle {
|
||||||
width: 60% !important;
|
font-size: var(--font-sm);
|
||||||
max-width: 60% !important;
|
color: var(--text-secondary);
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.send-transaction-wrapper {
|
||||||
|
position: relative;
|
||||||
|
flex: 1;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sending-overlay {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
background: rgba(0, 0, 0, 0.6);
|
||||||
|
backdrop-filter: blur(2px);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
z-index: 10;
|
||||||
|
border-radius: var(--radius-md);
|
||||||
|
|
||||||
|
.sending-content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
gap: var(--spacing-lg);
|
||||||
|
padding: var(--spacing-2xl);
|
||||||
|
background: rgba(255, 255, 255, 0.95);
|
||||||
|
border-radius: var(--radius-lg);
|
||||||
|
box-shadow: var(--shadow-lg);
|
||||||
|
|
||||||
|
.sending-text {
|
||||||
|
margin: 0;
|
||||||
|
font-size: var(--font-lg);
|
||||||
|
font-weight: var(--font-semibold);
|
||||||
|
color: var(--text-primary);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
export { default as WalletInfo } from './WalletInfo.vue'
|
export { default as WalletInfo } from './WalletInfo.vue'
|
||||||
export { default as WalletBalanceAndAddress } from './WalletBalanceAndAddress.vue'
|
export { default as WalletBalance } from './WalletBalance.vue'
|
||||||
|
export { default as WalletAddress } from './WalletAddress.vue'
|
||||||
export { default as WalletTab } from './WalletTab.vue'
|
export { default as WalletTab } from './WalletTab.vue'
|
||||||
|
|||||||
31
src/views/Home/utils/columns.ts
Normal file
31
src/views/Home/utils/columns.ts
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
export const columns = [
|
||||||
|
{
|
||||||
|
title: 'UTXO Hash',
|
||||||
|
dataIndex: 'utxoHash',
|
||||||
|
key: 'utxoHash',
|
||||||
|
width: '60%',
|
||||||
|
ellipsis: true,
|
||||||
|
customRender: ({ text }: any) => {
|
||||||
|
if (!text) return 'N/A'
|
||||||
|
const str = typeof text === 'string' ? text : JSON.stringify(text)
|
||||||
|
return str.length > 20 ? `${str.slice(0, 5)}...${str.slice(-5)}` : str
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Amount (NPT)',
|
||||||
|
dataIndex: 'amount',
|
||||||
|
key: 'amount',
|
||||||
|
width: '20%',
|
||||||
|
customRender: ({ record }: any) => {
|
||||||
|
const amount = record.amount || record.value || 0
|
||||||
|
return parseFloat(amount).toFixed(8)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: 'Block Height',
|
||||||
|
dataIndex: 'blockHeight',
|
||||||
|
key: 'blockHeight',
|
||||||
|
width: '20%',
|
||||||
|
customRender: ({ text }: any) => text || 'N/A',
|
||||||
|
},
|
||||||
|
]
|
||||||
1
src/views/Home/utils/index.ts
Normal file
1
src/views/Home/utils/index.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export * from './columns'
|
||||||
@ -3,19 +3,29 @@ import { defineConfig } from 'vite'
|
|||||||
import vue from '@vitejs/plugin-vue'
|
import vue from '@vitejs/plugin-vue'
|
||||||
import vueJsx from '@vitejs/plugin-vue-jsx'
|
import vueJsx from '@vitejs/plugin-vue-jsx'
|
||||||
import VueDevTools from 'vite-plugin-vue-devtools'
|
import VueDevTools from 'vite-plugin-vue-devtools'
|
||||||
|
import Components from 'unplugin-vue-components/vite'
|
||||||
|
import { AntDesignVueResolver } from 'unplugin-vue-components/resolvers'
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
server: {
|
server: {
|
||||||
port: 3008,
|
port: 3008,
|
||||||
},
|
},
|
||||||
base: './',
|
base: './',
|
||||||
plugins: [vue(), vueJsx(), VueDevTools()],
|
plugins: [vue(), vueJsx(), VueDevTools(), Components({
|
||||||
|
resolvers: [AntDesignVueResolver({importStyle: false})],
|
||||||
|
})],
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
exclude: ['@neptune/wasm'],
|
exclude: ['@neptune/wasm', '@neptune/native'],
|
||||||
},
|
},
|
||||||
|
build: {
|
||||||
|
rollupOptions: {
|
||||||
|
external: ['@neptune/wasm', '@neptune/native'],
|
||||||
|
},
|
||||||
|
},
|
||||||
css: {
|
css: {
|
||||||
preprocessorOptions: {
|
preprocessorOptions: {
|
||||||
scss: {
|
scss: {
|
||||||
|
silenceDeprecations: ['import'],
|
||||||
additionalData: `
|
additionalData: `
|
||||||
@import "@/assets/scss/__variables.scss";
|
@import "@/assets/scss/__variables.scss";
|
||||||
@import "@/assets/scss/__mixin.scss";
|
@import "@/assets/scss/__mixin.scss";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user