├── src ├── boot │ ├── .gitkeep │ ├── axios.js │ └── i18n.js ├── components │ ├── .gitkeep │ ├── CellCard.vue │ ├── DashCard.vue │ ├── FeeRate.vue │ ├── TxCard.vue │ ├── DaoInput.vue │ ├── DaoItem.vue │ ├── TxList.vue │ ├── TxItem.vue │ ├── MetaCard.vue │ └── OutputsForm.vue ├── css │ ├── app.scss │ └── quasar.variables.scss ├── store │ ├── config │ │ ├── actions.js │ │ ├── mutations.js │ │ ├── state.js │ │ ├── index.js │ │ └── getters.js │ ├── chain │ │ ├── state.js │ │ ├── getters.js │ │ ├── actions.js │ │ ├── mutations.js │ │ └── index.js │ ├── cell │ │ ├── getters.js │ │ ├── state.js │ │ ├── index.js │ │ ├── actions.js │ │ └── mutations.js │ ├── dao │ │ ├── state.js │ │ ├── index.js │ │ ├── actions.js │ │ ├── getters.js │ │ └── mutations.js │ ├── account │ │ ├── index.js │ │ ├── state.js │ │ ├── getters.js │ │ ├── mutations.js │ │ └── actions.js │ └── index.js ├── statics │ ├── icons │ │ ├── favicon.ico │ │ ├── icon-128x128.png │ │ ├── icon-192x192.png │ │ ├── icon-256x256.png │ │ ├── icon-384x384.png │ │ ├── icon-512x512.png │ │ ├── favicon-16x16.png │ │ ├── favicon-32x32.png │ │ ├── favicon-96x96.png │ │ ├── ms-icon-144x144.png │ │ ├── apple-icon-120x120.png │ │ ├── apple-icon-152x152.png │ │ ├── apple-icon-167x167.png │ │ ├── apple-icon-180x180.png │ │ └── safari-pinned-tab.svg │ └── app-logo-128x128.png ├── i18n │ ├── index.js │ ├── zh-cn │ │ └── index.js │ └── en-us │ │ └── index.js ├── App.vue ├── pages │ ├── Error404.vue │ ├── Index.vue │ ├── Explore.vue │ ├── TxRecords.vue │ ├── Dao.vue │ └── Send.vue ├── router │ ├── index.js │ └── routes.js ├── index.template.html ├── services │ ├── txSize.js │ ├── api.js │ ├── utils.js │ └── chain.js ├── layouts │ └── MyLayout.vue └── assets │ ├── sad.svg │ └── quasar-logo-full.svg ├── .eslintignore ├── babel.config.js ├── .editorconfig ├── .postcssrc.js ├── README.md ├── .gitignore ├── .stylintrc ├── .eslintrc.js ├── package.json └── quasar.conf.js /src/boot/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist 2 | -------------------------------------------------------------------------------- /src/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/css/app.scss: -------------------------------------------------------------------------------- 1 | // app global css in SCSS form 2 | -------------------------------------------------------------------------------- /src/store/config/actions.js: -------------------------------------------------------------------------------- 1 | /* 2 | export function someAction (context) { 3 | } 4 | */ 5 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@quasar/babel-preset-app' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/statics/icons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/favicon.ico -------------------------------------------------------------------------------- /src/boot/axios.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import axios from 'axios' 3 | 4 | Vue.prototype.$axios = axios 5 | -------------------------------------------------------------------------------- /src/statics/app-logo-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/app-logo-128x128.png -------------------------------------------------------------------------------- /src/statics/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/icon-128x128.png -------------------------------------------------------------------------------- /src/statics/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/icon-192x192.png -------------------------------------------------------------------------------- /src/statics/icons/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/icon-256x256.png -------------------------------------------------------------------------------- /src/statics/icons/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/icon-384x384.png -------------------------------------------------------------------------------- /src/statics/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/icon-512x512.png -------------------------------------------------------------------------------- /src/statics/icons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/favicon-16x16.png -------------------------------------------------------------------------------- /src/statics/icons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/favicon-32x32.png -------------------------------------------------------------------------------- /src/statics/icons/favicon-96x96.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/favicon-96x96.png -------------------------------------------------------------------------------- /src/statics/icons/ms-icon-144x144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/ms-icon-144x144.png -------------------------------------------------------------------------------- /src/store/chain/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | feeRate: 1000, 4 | fee: '0' 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /src/statics/icons/apple-icon-120x120.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/apple-icon-120x120.png -------------------------------------------------------------------------------- /src/statics/icons/apple-icon-152x152.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/apple-icon-152x152.png -------------------------------------------------------------------------------- /src/statics/icons/apple-icon-167x167.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/apple-icon-167x167.png -------------------------------------------------------------------------------- /src/statics/icons/apple-icon-180x180.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lite/ckb.pw/master/src/statics/icons/apple-icon-180x180.png -------------------------------------------------------------------------------- /src/store/chain/getters.js: -------------------------------------------------------------------------------- 1 | export const feeRateGetter = state => state.feeRate 2 | export const feeGetter = state => state.fee 3 | -------------------------------------------------------------------------------- /src/i18n/index.js: -------------------------------------------------------------------------------- 1 | import enUS from './en-us' 2 | import zhCN from './zh-cn' 3 | 4 | export default { 5 | 'en-us': enUS, 6 | 'zh-cn': zhCN 7 | } 8 | -------------------------------------------------------------------------------- /src/store/cell/getters.js: -------------------------------------------------------------------------------- 1 | export const unSpentGetter = state => state.unSpent 2 | export const loadingUnSpentGetter = state => state.loadingUnSpent 3 | -------------------------------------------------------------------------------- /src/store/config/mutations.js: -------------------------------------------------------------------------------- 1 | export function UPDATE(state, payload) { 2 | for (let key in payload) { 3 | state[key] = payload[key] 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/store/dao/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | locked: '-', 4 | apc: '-', 5 | revenue: '-', 6 | loadingList: false, 7 | list: [] 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/store/chain/actions.js: -------------------------------------------------------------------------------- 1 | import api from '../../services/api' 2 | 3 | export async function LOAD_FEE_RATE({ commit }) { 4 | const feeRate = await api.getFeeRate() 5 | commit('UPDATE_FEE_RATE', feeRate) 6 | } 7 | -------------------------------------------------------------------------------- /src/store/chain/mutations.js: -------------------------------------------------------------------------------- 1 | export function UPDATE_FEE_RATE(state, payload) { 2 | state.feeRate = parseInt(payload) 3 | } 4 | 5 | export function UPDATE_FEE(state, payload) { 6 | state.fee = payload 7 | } 8 | -------------------------------------------------------------------------------- /src/components/CellCard.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | // https://github.com/michael-ciniawsky/postcss-load-config 2 | 3 | module.exports = { 4 | plugins: [ 5 | // to edit target browsers: use "browserslist" field in package.json 6 | require('autoprefixer') 7 | ] 8 | } 9 | -------------------------------------------------------------------------------- /src/store/config/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | provider: '', 4 | showBar: false, 5 | showHeader: true, 6 | showFooter: true, 7 | barHeight: 0, 8 | topOffset: 0, 9 | bottomOffset: 0 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/store/cell/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | total: 0, 4 | free: 0, 5 | types: [], 6 | loadingUnSpent: false, 7 | unSpent: { 8 | capacity: '', 9 | cells: [], 10 | lastId: 0 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/store/account/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /src/store/cell/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /src/store/chain/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /src/store/config/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /src/store/dao/index.js: -------------------------------------------------------------------------------- 1 | import state from './state' 2 | import * as getters from './getters' 3 | import * as mutations from './mutations' 4 | import * as actions from './actions' 5 | 6 | export default { 7 | namespaced: true, 8 | state, 9 | getters, 10 | mutations, 11 | actions 12 | } 13 | -------------------------------------------------------------------------------- /src/store/dao/actions.js: -------------------------------------------------------------------------------- 1 | import api from '../../services/api' 2 | import { getLockHash } from '../../services/chain' 3 | export async function LOAD_LIST({ commit }, { address }) { 4 | commit('LOADING_LIST') 5 | const list = await api.getDAOList(getLockHash(address)) 6 | commit('SET_LIST', list) 7 | commit('LIST_LOADED') 8 | } 9 | -------------------------------------------------------------------------------- /src/store/account/state.js: -------------------------------------------------------------------------------- 1 | export default function() { 2 | return { 3 | loadingBalance: false, 4 | loadingTXs: false, 5 | noMoreTXs: false, 6 | platform: '', 7 | address: '-', 8 | alias: '', 9 | capacity: { 10 | total: '-', 11 | occupied: '1234' 12 | }, 13 | txs: [] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/store/dao/getters.js: -------------------------------------------------------------------------------- 1 | import { toCKB } from '../../services/utils' 2 | export const lockedGetter = state => toCKB(state.locked) 3 | export const apcGetter = state => state.apc 4 | export const revenueGetter = state => toCKB(state.revenue) 5 | export const loadingListGetter = state => state.loadingList 6 | export const listGetter = state => state.list 7 | -------------------------------------------------------------------------------- /src/boot/i18n.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueI18n from 'vue-i18n' 3 | import messages from 'src/i18n' 4 | 5 | Vue.use(VueI18n) 6 | 7 | const i18n = new VueI18n({ 8 | locale: 'en-us', 9 | fallbackLocale: 'en-us', 10 | messages 11 | }) 12 | 13 | export default ({ app }) => { 14 | // Set i18n instance on app 15 | app.i18n = i18n 16 | } 17 | 18 | export { i18n } 19 | -------------------------------------------------------------------------------- /src/store/config/getters.js: -------------------------------------------------------------------------------- 1 | export const providerGetter = state => state.provider 2 | export const showBarGetter = state => state.showBar 3 | export const showHeaderGetter = state => state.showHeader 4 | export const showFooterGetter = state => state.showFooter 5 | export const barHeightGetter = state => state.barHeight 6 | export const topOffsetGetter = state => state.topOffset 7 | export const bottomOffsetGetter = state => state.bottomOffset 8 | -------------------------------------------------------------------------------- /src/pages/Error404.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/store/account/getters.js: -------------------------------------------------------------------------------- 1 | export const addressGetter = state => state.address 2 | 3 | export const balanceGetter = state => state.capacity.total 4 | 5 | export const platformGetter = state => state.platform 6 | 7 | export const minAmountGetter = () => 61 8 | 9 | export const loadingBalanceGetter = state => state.loadingBalance 10 | 11 | export const loadingTXsGetter = state => state.loadingTXs 12 | 13 | export const txsGetter = state => state.txs 14 | 15 | export const noMoreTXsGetter = state => state.noMoreTXs 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # CKB P Wallet (ckb.pw) 2 | 3 | A web based high security wallet for ckb blockchain 4 | 5 | ## Install the dependencies 6 | ```bash 7 | yarn 8 | ``` 9 | 10 | ### Start the app in development mode (hot-code reloading, error reporting, etc.) 11 | ```bash 12 | quasar dev 13 | ``` 14 | 15 | ### Lint the files 16 | ```bash 17 | yarn run lint 18 | ``` 19 | 20 | ### Build the app for production 21 | ```bash 22 | quasar build 23 | ``` 24 | 25 | ### Customize the configuration 26 | See [Configuring quasar.conf.js](https://quasar.dev/quasar-cli/quasar-conf-js). 27 | -------------------------------------------------------------------------------- /src/store/cell/actions.js: -------------------------------------------------------------------------------- 1 | import api from '../../services/api' 2 | import { getLockHash } from '../../services/chain' 3 | 4 | export async function LOAD_UNSPENT_CELLS( 5 | { commit }, 6 | { address, capacity, lastId } 7 | ) { 8 | commit('LOADING_UNSPENT') 9 | const lockHash = getLockHash(address) 10 | const cells = await api.getUnspentCells(lockHash, capacity, lastId) 11 | console.log('unspent cells', cells) 12 | commit('SET_UNSPENT', cells) 13 | commit('UNSPENT_LOADED') 14 | } 15 | 16 | export function CLEAR_UNSPENT_CELLS({ commit }, { lastId }) { 17 | commit('SET_UNSPENT', []) 18 | lastId && commit('SET_LAST_ID', lastId) 19 | } 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .thumbs.db 3 | node_modules 4 | 5 | # Quasar core related directories 6 | .quasar 7 | /dist 8 | 9 | # Cordova related directories and files 10 | /src-cordova/node_modules 11 | /src-cordova/platforms 12 | /src-cordova/plugins 13 | /src-cordova/www 14 | 15 | # Capacitor related directories and files 16 | /src-capacitor/www 17 | /src-capacitor/node_modules 18 | 19 | # BEX related directories and files 20 | /src-bex/www 21 | /src-bex/js/core 22 | 23 | # Log files 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # Editor directories and files 29 | .idea 30 | .vscode 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | -------------------------------------------------------------------------------- /src/store/cell/mutations.js: -------------------------------------------------------------------------------- 1 | import { sumAmount } from '../../services/utils' 2 | export function SET_TOTAL_CELL(state, payload) { 3 | state.total = payload 4 | } 5 | 6 | export function SET_UNSPENT(state, cells) { 7 | state.unSpent.cells = cells 8 | try { 9 | state.unSpent.capacity = cells.map(c => c.capacity).reduce(sumAmount) 10 | state.unSpent.lastId = cells[cells.length - 1].id 11 | } catch (e) { 12 | state.unSpent = { cells: [], capacity: '0', lastId: 0 } 13 | } 14 | } 15 | 16 | export function SET_LAST_ID(state, lastId) { 17 | state.unSpent.lastId = lastId 18 | } 19 | 20 | export function LOADING_UNSPENT(state) { 21 | state.loadingUnSpent = true 22 | } 23 | 24 | export function UNSPENT_LOADED(state) { 25 | state.loadingUnSpent = false 26 | } 27 | -------------------------------------------------------------------------------- /src/router/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import VueRouter from 'vue-router' 3 | 4 | import routes from './routes' 5 | 6 | Vue.use(VueRouter) 7 | 8 | /* 9 | * If not building with SSR mode, you can 10 | * directly export the Router instantiation; 11 | * 12 | * The function below can be async too; either use 13 | * async/await or return a Promise which resolves 14 | * with the Router instance. 15 | */ 16 | 17 | export default function(/* { store, ssrContext } */) { 18 | const Router = new VueRouter({ 19 | scrollBehavior: () => ({ x: 0, y: 0 }), 20 | routes, 21 | 22 | // Leave these as they are and change in quasar.conf.js instead! 23 | // quasar.conf.js -> build -> vueRouterMode 24 | // quasar.conf.js -> build -> publicPath 25 | mode: process.env.VUE_ROUTER_MODE, 26 | base: process.env.VUE_ROUTER_BASE 27 | }) 28 | 29 | return Router 30 | } 31 | -------------------------------------------------------------------------------- /src/store/index.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import Vuex from 'vuex' 3 | 4 | import account from './account' 5 | import cell from './cell' 6 | import dao from './dao' 7 | import chain from './chain' 8 | import config from './config' 9 | 10 | Vue.use(Vuex) 11 | 12 | /* 13 | * If not building with SSR mode, you can 14 | * directly export the Store instantiation; 15 | * 16 | * The function below can be async too; either use 17 | * async/await or return a Promise which resolves 18 | * with the Store instance. 19 | */ 20 | 21 | export default function(/* { ssrContext } */) { 22 | const Store = new Vuex.Store({ 23 | modules: { 24 | account, 25 | cell, 26 | dao, 27 | chain, 28 | config 29 | }, 30 | 31 | // enable strict mode (adds overhead!) 32 | // for dev mode only 33 | strict: process.env.DEV 34 | }) 35 | 36 | return Store 37 | } 38 | -------------------------------------------------------------------------------- /.stylintrc: -------------------------------------------------------------------------------- 1 | { 2 | "blocks": "never", 3 | "brackets": "never", 4 | "colons": "never", 5 | "colors": "always", 6 | "commaSpace": "always", 7 | "commentSpace": "always", 8 | "cssLiteral": "never", 9 | "depthLimit": false, 10 | "duplicates": true, 11 | "efficient": "always", 12 | "extendPref": false, 13 | "globalDupe": true, 14 | "indentPref": 2, 15 | "leadingZero": "never", 16 | "maxErrors": false, 17 | "maxWarnings": false, 18 | "mixed": false, 19 | "namingConvention": false, 20 | "namingConventionStrict": false, 21 | "none": "never", 22 | "noImportant": false, 23 | "parenSpace": "never", 24 | "placeholder": false, 25 | "prefixVarsWithDollar": "always", 26 | "quotePref": "single", 27 | "semicolons": "never", 28 | "sortOrder": false, 29 | "stackedProperties": "never", 30 | "trailingWhitespace": "never", 31 | "universal": "never", 32 | "valid": true, 33 | "zeroUnits": "never", 34 | "zIndexNormalize": false 35 | } 36 | -------------------------------------------------------------------------------- /src/router/routes.js: -------------------------------------------------------------------------------- 1 | const routes = [ 2 | { 3 | path: '/', 4 | component: () => import('layouts/MyLayout.vue'), 5 | children: [ 6 | { path: '', redirect: 'account' }, 7 | { 8 | path: 'account', 9 | name: 'account', 10 | component: () => import('pages/Index.vue') 11 | }, 12 | { 13 | path: 'send', 14 | name: 'send', 15 | component: () => import('pages/Send.vue') 16 | }, 17 | { 18 | path: 'explore', 19 | component: () => import('pages/Explore.vue'), 20 | name: 'explore' 21 | }, 22 | { 23 | path: 'dao', 24 | name: 'dao', 25 | component: () => import('pages/Dao.vue') 26 | }, 27 | { 28 | path: '/txs', 29 | component: () => import('pages/TxRecords.vue') 30 | } 31 | ] 32 | } 33 | ] 34 | 35 | // Always leave this as last one 36 | if (process.env.MODE !== 'ssr') { 37 | routes.push({ 38 | path: '*', 39 | component: () => import('pages/Error404.vue') 40 | }) 41 | } 42 | 43 | export default routes 44 | -------------------------------------------------------------------------------- /src/components/DashCard.vue: -------------------------------------------------------------------------------- 1 | 30 | 31 | 49 | -------------------------------------------------------------------------------- /src/store/account/mutations.js: -------------------------------------------------------------------------------- 1 | export function LOADING_BALANCE(state) { 2 | state.loadingBalance = true 3 | } 4 | 5 | export function BALANCE_LOADED(state) { 6 | state.loadingBalance = false 7 | } 8 | 9 | export function LOADING_TXS(state) { 10 | state.loadingTXs = true 11 | } 12 | 13 | export function TXS_LOADED(state) { 14 | state.loadingTXs = false 15 | } 16 | 17 | export function TXS_NO_MORE(state, payload) { 18 | state.noMoreTXs = payload 19 | } 20 | 21 | export function SET_PLATFORM(state, payload) { 22 | state.platform = payload 23 | } 24 | 25 | export function SET_ADDRESS(state, payload) { 26 | state.address = payload 27 | } 28 | 29 | export function SET_CAPACITY(state, payload) { 30 | state.capacity.total = payload 31 | } 32 | 33 | export function SET_TXS(state, payload) { 34 | state.txs = payload 35 | } 36 | 37 | export function APPEND_TXS(state, payload) { 38 | state.txs = distinctObjectArray([...state.txs, ...payload], 'hash') 39 | } 40 | 41 | function distinctObjectArray(arr, prop) { 42 | const map = new Map() 43 | for (let obj of arr) { 44 | if (!map.has(obj[prop])) { 45 | map.set(obj[prop], obj) 46 | } 47 | } 48 | 49 | return Array.from(map.values()) 50 | } 51 | -------------------------------------------------------------------------------- /src/css/quasar.variables.scss: -------------------------------------------------------------------------------- 1 | // Quasar SCSS (& Sass) Variables 2 | // -------------------------------------------------- 3 | // To customize the look and feel of this app, you can override 4 | // the Sass/SCSS variables found in Quasar's source Sass/SCSS files. 5 | 6 | // Check documentation for full list of Quasar variables 7 | 8 | // Your own variables (that are declared here) and Quasar's own 9 | // ones will be available out of the box in your .vue/.scss/.sass files 10 | 11 | // It's highly recommended to change the default colors 12 | // to match your app's branding. 13 | // Tip: Use the "Theme Builder" on Quasar's documentation website. 14 | 15 | $eth-gray : #3C3C3D; 16 | $eth-lgray : #ECF0F1; 17 | $eth-gold : #C99D66; 18 | 19 | $ckb-green : #3CC68A; 20 | 21 | 22 | // $primary : #027BE3; 23 | $primary : darken($ckb-green, 10%); 24 | // $secondary : #26A69A; 25 | $secondary : #027BE3; 26 | $accent : #9C27B0; 27 | 28 | $dark : #1D1D1D; 29 | 30 | $positive : #21BA45; 31 | $negative : #C10015; 32 | $info : #31CCEC; 33 | $warning : #F2C037; 34 | 35 | 36 | // $meta-card-bg: linear-gradient(to right top, #1D1D1D, $eth-gray); 37 | $meta-card-bg: linear-gradient(to right top, darken($ckb-green, 30%), $ckb-green); 38 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | 4 | parserOptions: { 5 | parser: 'babel-eslint', 6 | sourceType: 'module' 7 | }, 8 | 9 | env: { 10 | browser: true 11 | }, 12 | 13 | extends: [ 14 | // https://eslint.vuejs.org/rules/#priority-a-essential-error-prevention 15 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 16 | 'plugin:vue/essential', 17 | '@vue/prettier' 18 | ], 19 | 20 | // required to lint *.vue files 21 | plugins: [ 22 | 'vue' 23 | ], 24 | 25 | globals: { 26 | 'ga': true, // Google Analytics 27 | 'cordova': true, 28 | '__statics': true, 29 | 'process': true, 30 | 'Capacitor': true, 31 | 'chrome': true 32 | }, 33 | 34 | // add your custom rules here 35 | rules: { 36 | 'prefer-promise-reject-errors': 'off', 37 | // allow debugger during development only 38 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 39 | 'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off', 40 | 'prettier/prettier': [ 41 | 'warn', 42 | { 43 | 'singleQuote': true, 44 | 'semi': false 45 | } 46 | ] 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ckb.pw", 3 | "version": "0.0.1", 4 | "description": "A web based high security wallet for ckb blockchain", 5 | "productName": "CKB P Wallet", 6 | "cordovaId": "org.cordova.quasar.app", 7 | "capacitorId": "", 8 | "author": "zhixian ", 9 | "private": true, 10 | "scripts": { 11 | "lint": "eslint --fix --ext .js,.vue src", 12 | "test": "echo \"No test specified\" && exit 0" 13 | }, 14 | "dependencies": { 15 | "@nervosnetwork/ckb-sdk-core": "^0.26.4", 16 | "@quasar/extras": "^1.5.0", 17 | "abcwallet": "^1.0.2", 18 | "axios": "^0.18.1", 19 | "ethereumjs-util": "^6.2.0", 20 | "moment": "^2.24.0", 21 | "quasar": "^1.8.4", 22 | "vconsole": "^3.3.4", 23 | "vue-i18n": "^8.0.0", 24 | "vue-qr": "^2.2.1", 25 | "web3-utils": "^1.2.4" 26 | }, 27 | "devDependencies": { 28 | "@quasar/app": "^1.5.2", 29 | "@vue/eslint-config-prettier": "^4.0.0", 30 | "babel-eslint": "^10.0.1", 31 | "eslint": "^5.10.0", 32 | "eslint-loader": "^2.1.1", 33 | "eslint-plugin-vue": "^5.0.0" 34 | }, 35 | "engines": { 36 | "node": ">= 8.9.0", 37 | "npm": ">= 5.6.0", 38 | "yarn": ">= 1.6.0" 39 | }, 40 | "browserslist": [ 41 | "last 1 version, not dead, ie >= 11" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/store/dao/mutations.js: -------------------------------------------------------------------------------- 1 | import { JSBI } from '../../services/chain' 2 | import { toCKB } from '../../services/utils' 3 | 4 | export function SET_APC(state, payload) { 5 | state.apc = payload 6 | } 7 | 8 | export function SET_LOCKED(state, payload) { 9 | state.locked = payload 10 | } 11 | 12 | export function SET_REVENUE(state, payload) { 13 | state.revenue = payload 14 | } 15 | 16 | export function LOADING_LIST(state) { 17 | state.loadingList = true 18 | } 19 | 20 | export function SET_LIST(state, list) { 21 | state.list = list 22 | let locked = JSBI.BigInt(0) 23 | let revenue = JSBI.BigInt(0) 24 | let apc = 0 25 | for (let item of list) { 26 | const sizeBN = JSBI.BigInt(item.size) 27 | const revenueBN = JSBI.subtract(JSBI.BigInt(item.countedCapacity), sizeBN) 28 | item.revenue = revenueBN.toString() 29 | item.apc = parseFloat(item.rate * 100).toFixed(2) 30 | locked = JSBI.ADD(locked, sizeBN) 31 | revenue = JSBI.ADD(revenue, revenueBN) 32 | const amount = parseFloat(toCKB(item.size)) 33 | apc += item.apc * amount 34 | } 35 | 36 | state.locked = locked.toString() 37 | state.revenue = revenue.toString() 38 | apc && 39 | (state.apc = parseFloat(apc / parseFloat(toCKB(state.locked))).toFixed(2)) 40 | } 41 | 42 | export function LIST_LOADED(state) { 43 | state.loadingList = false 44 | } 45 | -------------------------------------------------------------------------------- /src/index.template.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | <%= htmlWebpackPlugin.options.productName %> 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | -------------------------------------------------------------------------------- /src/pages/Index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 56 | -------------------------------------------------------------------------------- /src/store/account/actions.js: -------------------------------------------------------------------------------- 1 | // import { getBalance } from '../../services/chain' 2 | import api from '../../services/api' 3 | import { getLockHash } from '../../services/chain' 4 | 5 | export async function LOAD_BALANCE({ commit, getters }) { 6 | try { 7 | const lockHash = getLockHash(getters.addressGetter) 8 | if (lockHash) { 9 | commit('LOADING_BALANCE') 10 | const capacity = await api.getBalance(lockHash) 11 | commit('SET_CAPACITY', capacity) 12 | // commit('cell/SET_TOTAL_CELL', ret.cellsCount, { root: true }) 13 | commit('BALANCE_LOADED') 14 | } 15 | } catch (e) { 16 | e.toString() 17 | } 18 | } 19 | 20 | export async function LOAD_TXS( 21 | { commit, getters }, 22 | { lastHash, size = 20, type = 'all' } 23 | ) { 24 | try { 25 | const lockHash = getLockHash(getters.addressGetter) 26 | if (lockHash) { 27 | commit('LOADING_TXS') 28 | const txs = await api.getTxList(lockHash, lastHash, size, type) 29 | if (txs.length) { 30 | if (lastHash) { 31 | commit('APPEND_TXS', txs) 32 | } else { 33 | commit('SET_TXS', txs) 34 | commit('TXS_NO_MORE', false) 35 | } 36 | } else { 37 | !lastHash && commit(commit('SET_TXS', txs)) 38 | commit('TXS_NO_MORE', true) 39 | } 40 | commit('TXS_LOADED') 41 | } else { 42 | console.log('address not ready') 43 | } 44 | } catch (e) { 45 | e.toString() 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/services/txSize.js: -------------------------------------------------------------------------------- 1 | import { 2 | serializeOutput, 3 | serializeWitnessArgs 4 | } from '@nervosnetwork/ckb-sdk-utils/lib/serialization/transaction' 5 | import { serializeFixVec } from '@nervosnetwork/ckb-sdk-utils/lib/serialization' 6 | 7 | const SERIALIZED_OFFSET_BYTESIZE = 4 8 | 9 | // const base = () => 68 + SERIALIZED_OFFSET_BYTESIZE 10 | const base = () => 68 + 69 + SERIALIZED_OFFSET_BYTESIZE 11 | 12 | const cellDep = () => 37 13 | 14 | const headerDep = () => 32 15 | 16 | const input = () => 44 17 | 18 | const output = output => { 19 | const bytes = serializeOutput(output) 20 | return byteLength(bytes) + SERIALIZED_OFFSET_BYTESIZE 21 | } 22 | 23 | const outputData = data => { 24 | const bytes = serializeFixVec(data) 25 | return byteLength(bytes) + SERIALIZED_OFFSET_BYTESIZE 26 | } 27 | 28 | const witness = witness => { 29 | if (typeof witness !== 'string') witness = serializeWitnessArgs(witness) 30 | const bytes = serializeFixVec(witness) 31 | return byteLength(bytes) + SERIALIZED_OFFSET_BYTESIZE 32 | } 33 | 34 | export default tx => { 35 | return [ 36 | base(), 37 | cellDep() * tx.cellDeps.length, 38 | headerDep() * tx.headerDeps.length, 39 | input() * tx.inputs.length, 40 | ...tx.outputs.map(o => output(o)), 41 | ...tx.outputsData.map(data => outputData(data)), 42 | ...tx.witnesses.map(wit => witness(wit)) 43 | ].reduce((result, c) => result + c, 0) 44 | } 45 | 46 | function byteLength(hex) { 47 | // eslint-disable-next-line no-undef 48 | return Buffer.byteLength(removePrefix(hex), 'hex') 49 | } 50 | 51 | function removePrefix(hex) { 52 | if (hex.startsWith('0x')) { 53 | return hex.slice(2) 54 | } 55 | return hex 56 | } 57 | -------------------------------------------------------------------------------- /src/i18n/zh-cn/index.js: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | import { MIN_FEE_RATE } from '../../services/chain' 4 | 5 | export default { 6 | title: 'CKB P Wallet', 7 | sub_title: 'a revolutionary wallet for ckb', 8 | tab_account: 'Account', 9 | tab_explore: 'Explore', 10 | 11 | btn_transfer: '转账', 12 | btn_receive: '收款', 13 | btn_deposit: '存入', 14 | btn_withdraw: '取出', 15 | btn_recycle: 'Recycle', 16 | btn_confirm: '确认', 17 | btn_cancel: '取消', 18 | btn_change: 'Change', 19 | btn_clear: '清除', 20 | btn_send: '发送', 21 | btn_copy: '复制', 22 | btn_settings: '设置', 23 | btn_view_more: '查看更多', 24 | 25 | label_address: '地址', 26 | label_amount: '金额', 27 | label_remaining: '余额', 28 | label_fee: '手续费', 29 | label_fee_rate: '费率', 30 | label_min_amount: '最小金额', 31 | label_loading: '加载中..', 32 | label_tx_all: '总览', 33 | label_tx_in: '收入', 34 | label_tx_out: '支出', 35 | label_from: 'From', 36 | label_to: 'To', 37 | label_return: '返回主页', 38 | label_send_more: '再次发送', 39 | label_apc: '年化', 40 | label_locked: '锁定', 41 | label_in_dao: 'in DAO', 42 | label_revenue: '收益', 43 | label_advanced: 'Advanced', 44 | label_cheap: '慢', 45 | label_fast: '快', 46 | label_recent_tx: '近期收支', 47 | label_dapps: 'Dapps', 48 | label_no_record: '没有记录', 49 | label_withdrawing: '收款中', 50 | 51 | hint_address: '支持 ETH 和 CKB 地址', 52 | hint_amount: '最小转账金额为', 53 | hint_available: '当前可用', 54 | hint_dao_deposit: '存入 Nervos DAO', 55 | 56 | msg_dao_min_amount: '最少存入金额 102 CKB', 57 | msg_sent_success: '交易已发送', 58 | msg_field_required: '必填', 59 | msg_invalid_address: '无效地址', 60 | msg_min_fee_rate: `最低费率为 ${MIN_FEE_RATE} Shannons / KB`, 61 | msg_only_integer: `金额必须为整数`, 62 | msg_broke: `余额不足`, 63 | msg_confirm_delete: '确认删除 ?', 64 | msg_copy_success: '已复制 !' 65 | } 66 | -------------------------------------------------------------------------------- /src/components/FeeRate.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 71 | 72 | 77 | -------------------------------------------------------------------------------- /src/components/TxCard.vue: -------------------------------------------------------------------------------- 1 | 27 | 28 | 71 | -------------------------------------------------------------------------------- /src/i18n/en-us/index.js: -------------------------------------------------------------------------------- 1 | // This is just an example, 2 | // so you can safely delete all default props below 3 | import { MIN_FEE_RATE } from '../../services/chain' 4 | 5 | export default { 6 | title: 'CKB P Wallet', 7 | sub_title: 'a revolutionary wallet for ckb', 8 | tab_account: 'Account', 9 | tab_explore: 'Explore', 10 | 11 | btn_transfer: 'Transfer', 12 | btn_receive: 'Receive', 13 | btn_deposit: 'Deposit', 14 | btn_withdraw: 'Withdraw', 15 | btn_recycle: 'Recycle', 16 | btn_confirm: 'Confirm', 17 | btn_cancel: 'Cancel', 18 | btn_change: 'Change', 19 | btn_clear: 'Clear', 20 | btn_send: 'Send', 21 | btn_copy: 'Copy', 22 | btn_settings: 'Settings', 23 | btn_view_more: 'View More', 24 | 25 | label_address: 'Address', 26 | label_amount: 'Amount', 27 | label_remaining: 'Remaining', 28 | label_fee: 'Fee', 29 | label_fee_rate: 'Fee Rate', 30 | label_min_amount: 'Min amount', 31 | label_loading: 'Loading..', 32 | label_tx_all: 'All', 33 | label_tx_in: 'In', 34 | label_tx_out: 'Out', 35 | label_from: 'From', 36 | label_to: 'To', 37 | label_return: 'Return home', 38 | label_send_more: 'Send more', 39 | label_apc: 'APC', 40 | label_locked: 'Locked', 41 | label_in_dao: 'in DAO', 42 | label_revenue: 'Revenue', 43 | label_advanced: 'Advanced', 44 | label_cheap: 'Cheap', 45 | label_fast: 'Fast', 46 | label_recent_tx: 'Recent Transactions', 47 | label_dapps: 'Dapps', 48 | label_no_record: 'No Record', 49 | label_withdrawing: 'Withdrawing', 50 | 51 | hint_address: 'ETH and CKB addresses are supported', 52 | hint_amount: 'Min transfer amount is ', 53 | hint_available: 'available', 54 | hint_dao_deposit: 'Deposit to Nervos DAO', 55 | 56 | msg_dao_min_amount: 'Min amount is 102 CKB', 57 | msg_sent_success: 'Transaction Sent', 58 | msg_field_required: 'Field is required', 59 | msg_invalid_address: 'Invalid Address', 60 | msg_min_fee_rate: `Min fee rate is ${MIN_FEE_RATE} Shannons / KB`, 61 | msg_only_integer: `Amount must be integer`, 62 | msg_broke: `Balance not enough`, 63 | msg_confirm_delete: 'Proceed Deleting ?', 64 | msg_copy_success: 'Copied to Clipboard!' 65 | } 66 | -------------------------------------------------------------------------------- /src/pages/Explore.vue: -------------------------------------------------------------------------------- 1 | 29 | 30 | 61 | 73 | -------------------------------------------------------------------------------- /src/components/DaoInput.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 80 | 85 | -------------------------------------------------------------------------------- /src/services/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | import { Notify } from 'quasar' 3 | 4 | const BASE_URL = 'https://api.ckb.pw/' 5 | // const BASE_URL = 'http://192.168.1.137:3000/' 6 | 7 | export const API = { 8 | GetUnspentCells: BASE_URL + 'cell/unSpent', 9 | LoadDaoCell: BASE_URL + 'cell/loadDaoCell', 10 | LoadK1: BASE_URL + 'cell/loadSecp256k1Cell', 11 | GetTxList: BASE_URL + 'cell/txList', 12 | GetConfig: BASE_URL + 'cell/getConfig', 13 | GetBalance: BASE_URL + 'cell/getCapacityByLockHash', 14 | GetFeeRate: BASE_URL + 'block/feeRate', 15 | GetDAOList: BASE_URL + 'dao/daoList' 16 | } 17 | 18 | export const get = async (url, params) => { 19 | url += '?' 20 | for (let p in params) { 21 | if (params[p]) { 22 | url += `${p}=${params[p]}&` 23 | } 24 | } 25 | let ret = null 26 | try { 27 | ret = await axios.get(url) 28 | } catch (e) { 29 | Notify.create({ 30 | message: '[API] - ' + e.toString(), 31 | position: 'top', 32 | timeout: 2000, 33 | color: 'negative' 34 | }) 35 | } 36 | 37 | return ret 38 | } 39 | 40 | export default { 41 | getUnspentCells: async (lockHash, capacity, lastId) => { 42 | const { data } = await get(API.GetUnspentCells, { 43 | lockHash, 44 | capacity, 45 | lastId 46 | }) 47 | return data 48 | }, 49 | getTxList: async (lockHash, lastHash, size, type) => { 50 | const { data } = await get(API.GetTxList, { 51 | lockHash, 52 | lastHash, 53 | size, 54 | type 55 | }) 56 | return data 57 | }, 58 | loadK1: async () => { 59 | const { data } = await get(API.LoadK1) 60 | return data 61 | }, 62 | loadDaoCell: async () => { 63 | const { data } = await get(API.LoadDaoCell) 64 | return data 65 | }, 66 | getConfig: async () => { 67 | const { data } = await get(API.GetConfig) 68 | return data 69 | }, 70 | getBalance: async lockHash => { 71 | const { data } = await get(API.GetBalance, { lockHash }) 72 | return data 73 | }, 74 | getFeeRate: async speed => { 75 | const { data } = await get(API.GetFeeRate) 76 | if (speed) { 77 | return data.feeRate 78 | } else { 79 | return data.feeRate 80 | } 81 | }, 82 | getDAOList: async lockHash => { 83 | const { data } = await get(API.GetDAOList, { lockHash }) 84 | return data 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/DaoItem.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 82 | 83 | -------------------------------------------------------------------------------- /src/layouts/MyLayout.vue: -------------------------------------------------------------------------------- 1 | 28 | 29 | 92 | 103 | -------------------------------------------------------------------------------- /src/components/TxList.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 94 | 106 | -------------------------------------------------------------------------------- /src/components/TxItem.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 109 | 110 | 122 | -------------------------------------------------------------------------------- /src/statics/icons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/MetaCard.vue: -------------------------------------------------------------------------------- 1 | 70 | 128 | 149 | -------------------------------------------------------------------------------- /src/pages/TxRecords.vue: -------------------------------------------------------------------------------- 1 | 75 | 76 | 147 | 162 | -------------------------------------------------------------------------------- /src/pages/Dao.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 184 | 192 | -------------------------------------------------------------------------------- /src/components/OutputsForm.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 188 | -------------------------------------------------------------------------------- /quasar.conf.js: -------------------------------------------------------------------------------- 1 | // Configuration for your app 2 | // https://quasar.dev/quasar-cli/quasar-conf-js 3 | 4 | module.exports = function(ctx) { 5 | return { 6 | // app boot file (/src/boot) 7 | // --> boot files are part of "main.js" 8 | // https://quasar.dev/quasar-cli/cli-documentation/boot-files 9 | boot: ['i18n', 'axios'], 10 | 11 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-css 12 | css: ['app.scss'], 13 | 14 | // https://github.com/quasarframework/quasar/tree/dev/extras 15 | extras: [ 16 | // 'ionicons-v4', 17 | // 'mdi-v4', 18 | // 'fontawesome-v5', 19 | // 'eva-icons', 20 | // 'themify', 21 | // 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both! 22 | 'line-awesome', 23 | 24 | 'roboto-font', // optional, you are not bound to it 25 | 'material-icons' // optional, you are not bound to it 26 | ], 27 | 28 | // https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-framework 29 | framework: { 30 | iconSet: 'material-icons', // Quasar icon set 31 | lang: 'en-us', // Quasar language pack 32 | 33 | // Possible values for "all": 34 | // * 'auto' - Auto-import needed Quasar components & directives 35 | // (slightly higher compile time; next to minimum bundle size; most convenient) 36 | // * false - Manually specify what to import 37 | // (fastest compile time; minimum bundle size; most tedious) 38 | // * true - Import everything from Quasar 39 | // (not treeshaking Quasar; biggest bundle size; convenient) 40 | all: 'auto', 41 | 42 | components: [], 43 | directives: [], 44 | 45 | // Quasar plugins 46 | plugins: ['Notify'] 47 | }, 48 | 49 | // https://quasar.dev/quasar-cli/cli-documentation/supporting-ie 50 | supportIE: false, 51 | 52 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-build 53 | build: { 54 | scopeHoisting: true, 55 | vueRouterMode: 'hash', // available values: 'hash', 'history' 56 | showProgress: true, 57 | gzip: false, 58 | analyze: false, 59 | // Options below are automatically set depending on the env, set them if you want to override 60 | // preloadChunks: false, 61 | // extractCSS: false, 62 | 63 | // https://quasar.dev/quasar-cli/cli-documentation/handling-webpack 64 | extendWebpack(cfg) { 65 | cfg.module.rules.push({ 66 | enforce: 'pre', 67 | test: /\.(js|vue)$/, 68 | loader: 'eslint-loader', 69 | exclude: /node_modules/, 70 | options: { 71 | formatter: require('eslint').CLIEngine.getFormatter('stylish') 72 | } 73 | }) 74 | } 75 | }, 76 | 77 | // Full list of options: https://quasar.dev/quasar-cli/quasar-conf-js#Property%3A-devServer 78 | devServer: { 79 | // https: true, 80 | host: '0.0.0.0', 81 | port: 8080, 82 | open: true // opens browser window automatically 83 | }, 84 | 85 | // animations: 'all', // --- includes all animations 86 | // https://quasar.dev/options/animations 87 | animations: [], 88 | 89 | // https://quasar.dev/quasar-cli/developing-ssr/configuring-ssr 90 | ssr: { 91 | pwa: false 92 | }, 93 | 94 | // https://quasar.dev/quasar-cli/developing-pwa/configuring-pwa 95 | pwa: { 96 | workboxPluginMode: 'GenerateSW', // 'GenerateSW' or 'InjectManifest' 97 | workboxOptions: {}, // only for GenerateSW 98 | manifest: { 99 | name: 'CKB P Wallet', 100 | short_name: 'ckb.pw', 101 | description: 'A web based high security wallet for ckb blockchain', 102 | display: 'standalone', 103 | orientation: 'portrait', 104 | background_color: '#ffffff', 105 | theme_color: '#027be3', 106 | icons: [ 107 | { 108 | src: 'statics/icons/icon-128x128.png', 109 | sizes: '128x128', 110 | type: 'image/png' 111 | }, 112 | { 113 | src: 'statics/icons/icon-192x192.png', 114 | sizes: '192x192', 115 | type: 'image/png' 116 | }, 117 | { 118 | src: 'statics/icons/icon-256x256.png', 119 | sizes: '256x256', 120 | type: 'image/png' 121 | }, 122 | { 123 | src: 'statics/icons/icon-384x384.png', 124 | sizes: '384x384', 125 | type: 'image/png' 126 | }, 127 | { 128 | src: 'statics/icons/icon-512x512.png', 129 | sizes: '512x512', 130 | type: 'image/png' 131 | } 132 | ] 133 | } 134 | }, 135 | 136 | // Full list of options: https://quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova 137 | cordova: { 138 | // noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing 139 | id: 'org.cordova.quasar.app' 140 | }, 141 | 142 | // Full list of options: https://quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor 143 | capacitor: { 144 | hideSplashscreen: true 145 | }, 146 | 147 | // Full list of options: https://quasar.dev/quasar-cli/developing-electron-apps/configuring-electron 148 | electron: { 149 | bundler: 'packager', // 'packager' or 'builder' 150 | 151 | packager: { 152 | // https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options 153 | // OS X / Mac App Store 154 | // appBundleId: '', 155 | // appCategoryType: '', 156 | // osxSign: '', 157 | // protocol: 'myapp://path', 158 | // Windows only 159 | // win32metadata: { ... } 160 | }, 161 | 162 | builder: { 163 | // https://www.electron.build/configuration/configuration 164 | 165 | appId: 'ckb.pw' 166 | }, 167 | 168 | // keep in sync with /src-electron/main-process/electron-main 169 | // > BrowserWindow > webPreferences > nodeIntegration 170 | // More info: https://quasar.dev/quasar-cli/developing-electron-apps/node-integration 171 | nodeIntegration: true, 172 | 173 | extendWebpack(cfg) { 174 | // do something with Electron main process Webpack cfg 175 | // chainWebpack also available besides this extendWebpack 176 | } 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/services/utils.js: -------------------------------------------------------------------------------- 1 | import web3Utils from 'web3-utils' 2 | import { parseAddress } from '@nervosnetwork/ckb-sdk-utils' 3 | import BN from 'bn.js' 4 | import numberToBN from 'number-to-bn' 5 | 6 | export const displayLongAddress = (address, maxlen = 20) => { 7 | !address && (address = 'NULL') 8 | address.length > maxlen && 9 | (address = 10 | address.slice(0, 8) + 11 | (address.length > 46 ? '......' : '...') + 12 | address.slice(-8)) 13 | return address 14 | } 15 | 16 | export const sumAmount = (a, b) => { 17 | const A = new BN(safe(a)) 18 | const B = new BN(safe(b)) 19 | return A.add(B).toString() 20 | } 21 | 22 | export const subAmount = (a, b) => { 23 | const A = new BN(safe(a)) 24 | const B = new BN(safe(b)) 25 | return A.sub(B).toString() 26 | } 27 | 28 | export const cmpAmount = (a, b) => { 29 | const A = new BN(safe(a)) 30 | const B = new BN(safe(b)) 31 | console.log('cmp:', A.toString(), B.toString()) 32 | if (A.gt(B)) return 'gt' 33 | if (A.lt(B)) return 'lt' 34 | return 'eq' 35 | } 36 | 37 | // eslint-disable-next-line prettier/prettier 38 | const unitMap = { 'ckb': '100000000' } 39 | const zero = new BN(0) 40 | const negative1 = new BN(-1) 41 | const unit = 'ckb' 42 | export const toCKB = capacity => { 43 | if (web3Utils.isHexStrict(capacity)) { 44 | capacity = web3Utils.hexToNumberString(capacity) 45 | } 46 | var cap = numberToBN(capacity) 47 | var negative = cap.lt(zero) 48 | const base = getValueOfUnit(unit) 49 | const baseLength = unitMap[unit].length - 1 || 1 50 | const options = {} 51 | 52 | if (negative) { 53 | cap = cap.mul(negative1) 54 | } 55 | 56 | var fraction = cap.mod(base).toString(10) 57 | 58 | while (fraction.length < baseLength) { 59 | fraction = `0${fraction}` 60 | } 61 | 62 | if (!options.pad) { 63 | fraction = fraction.match(/^([0-9]*[1-9]|0)(0*)/)[1] 64 | } 65 | 66 | var whole = cap.div(base).toString(10); // eslint-disable-line 67 | 68 | if (options.commify) { 69 | whole = whole.replace(/\B(?=(\d{3})+(?!\d))/g, ',') 70 | } 71 | 72 | var value = `${whole}${fraction == '0' ? '' : `.${fraction}`}`; // eslint-disable-line 73 | 74 | if (negative) { 75 | value = `-${value}` 76 | } 77 | 78 | return value 79 | } 80 | 81 | export const fromCKB = capacity => { 82 | capacity === '' && (capacity = 0) 83 | var cap = numberToString(capacity) 84 | const base = getValueOfUnit(unit) 85 | const baseLength = unitMap[unit].length - 1 || 1 86 | 87 | // Is it negative? 88 | var negative = cap.substring(0, 1) === '-' 89 | if (negative) { 90 | cap = cap.substring(1) 91 | } 92 | 93 | if (cap === '.') { 94 | throw new Error( 95 | `[ckb-unit] while converting number ${capacity} to CKB, invalid value` 96 | ) 97 | } 98 | 99 | // Split it into a whole and fractional part 100 | var comps = cap.split('.') 101 | if (comps.length > 2) { 102 | throw new Error( 103 | `[ckb-unit] while converting number ${capacity} to CKB, too many decimal points` 104 | ) 105 | } 106 | 107 | var whole = comps[0], 108 | fraction = comps[1] 109 | 110 | if (!whole) { 111 | whole = '0' 112 | } 113 | if (!fraction) { 114 | fraction = '0' 115 | } 116 | if (fraction.length > baseLength) { 117 | throw new Error( 118 | `[ckb-unit] while converting number ${capacity} to CKB, too many decimal places` 119 | ) 120 | } 121 | 122 | while (fraction.length < baseLength) { 123 | fraction += '0' 124 | } 125 | 126 | whole = new BN(whole) 127 | fraction = new BN(fraction) 128 | cap = whole.mul(base).add(fraction) 129 | 130 | if (negative) { 131 | cap = cap.mul(negative1) 132 | } 133 | 134 | return new BN(cap.toString(10), 10) 135 | } 136 | 137 | export const verifyAddress = address => { 138 | // address must be a string 139 | if (typeof address !== 'string') return null 140 | 141 | // check if is eth address 142 | const isEthAddress = /^0x[a-fA-F0-9]{40}$/.test(address) 143 | if (isEthAddress) return 'eth' 144 | 145 | console.log('not eth address') 146 | let maybe = null 147 | 148 | address.startsWith('ckb') && (maybe = 'ckb') 149 | address.startsWith('ckt') && (maybe = 'ckt') 150 | 151 | if (!maybe) return null 152 | 153 | try { 154 | const hexAddress = parseAddress(address, 'hex') 155 | hexAddress.startsWith('0x0100') && (maybe += '_short') 156 | hexAddress.startsWith('0x0200') && (maybe += '_full') 157 | hexAddress.startsWith('0x0400') && (maybe += '_full') 158 | } catch (err) { 159 | return null 160 | } 161 | 162 | // check address length for each kind 163 | if (maybe.endsWith('short')) { 164 | if (address.length !== 46) maybe = null 165 | } else if (maybe.endsWith('full')) { 166 | if (address.length !== 95) maybe = null 167 | } 168 | 169 | return maybe 170 | } 171 | 172 | function numberToString(arg) { 173 | if (typeof arg === 'string') { 174 | if (!arg.match(/^-?[0-9.]+$/)) { 175 | throw new Error( 176 | `while converting number to string, invalid number value '${arg}', should be a number matching (^-?[0-9.]+).` 177 | ) 178 | } 179 | return arg 180 | } else if (typeof arg === 'number') { 181 | return String(arg) 182 | } else if ( 183 | typeof arg === 'object' && 184 | arg.toString && 185 | (arg.toTwos || arg.dividedToIntegerBy) 186 | ) { 187 | if (arg.toPrecision) { 188 | return String(arg.toPrecision()) 189 | } else { 190 | return arg.toString(10) 191 | } 192 | } 193 | throw new Error( 194 | `while converting number to string, invalid number value '${arg}' type ${typeof arg}.` 195 | ) 196 | } 197 | 198 | function getValueOfUnit(unitInput) { 199 | const unit = unitInput ? unitInput.toLowerCase() : 'ether' 200 | var unitValue = unitMap[unit] 201 | 202 | if (typeof unitValue !== 'string') { 203 | throw new Error( 204 | `[ethjs-unit] the unit provided ${unitInput} doesn't exists, please use the one of the following units ${JSON.stringify( 205 | unitMap, 206 | null, 207 | 2 208 | )}` 209 | ) 210 | } 211 | 212 | return new BN(unitValue, 10) 213 | } 214 | 215 | function safe(n) { 216 | !n && (n = 0) 217 | web3Utils.isHexStrict(n) && (n = web3Utils.hexToNumberString(n)) 218 | return n 219 | } 220 | 221 | export const sleep = async ms => { 222 | await new Promise(r => setTimeout(r, ms)) 223 | } 224 | 225 | export const formatCKBAddress = function(address) { 226 | if (address === null || address.length <= 17) { 227 | return address 228 | } 229 | 230 | const len = address.length 231 | const formatedAddress = 232 | address.substring(0, 7) + '...' + address.substr(len - 7, 7) 233 | console.log(address, formatedAddress) 234 | return formatedAddress 235 | } 236 | 237 | export const displayDateTime = timestamp => { 238 | return new Date(timestamp).toLocaleString() 239 | } 240 | -------------------------------------------------------------------------------- /src/pages/Send.vue: -------------------------------------------------------------------------------- 1 | 91 | 92 | 232 | 238 | -------------------------------------------------------------------------------- /src/assets/sad.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/assets/quasar-logo-full.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 19 | 21 | 43 | 45 | 46 | 48 | image/svg+xml 49 | 51 | 52 | 53 | 54 | 55 | 60 | 63 | 66 | 69 | 75 | 79 | 83 | 87 | 91 | 95 | 99 | 103 | 104 | 105 | 106 | 107 | 113 | 118 | 126 | 133 | 142 | 151 | 160 | 169 | 178 | 187 | 188 | 189 | 190 | 191 | 192 | -------------------------------------------------------------------------------- /src/services/chain.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | 3 | import web3utils from 'web3-utils' 4 | import * as ethUtil from 'ethereumjs-util' 5 | import CKBCore from '@nervosnetwork/ckb-sdk-core' 6 | import * as ckbUtils from '@nervosnetwork/ckb-sdk-utils' 7 | import api from './api' 8 | import txSize from './txSize' 9 | import ABCWallet from 'abcwallet' 10 | import { formatCKBAddress, fromCKB } from './utils' 11 | 12 | export const ckb = new CKBCore('https://aggron.ckb.dev') 13 | export const JSBI = ckb.utils.JSBI 14 | export const MIN_FEE_RATE = 1000 15 | 16 | var provider = '' 17 | var keccak_code_hash 18 | var cellDeps 19 | 20 | export const init = async ctx => { 21 | await loadDeps() 22 | let config = await api.getConfig() 23 | // keccak_code_hash = config.keccak_code_hash 24 | cellDeps = config.cellDeps 25 | 26 | ctx.$store.commit('dao/SET_APC', parseFloat(config.apc * 100).toFixed(2)) 27 | 28 | keccak_code_hash = 29 | '0xac8a4bc0656aeee68d4414681f4b2611341c4f0edd4c022f2d250ef8bb58682f' 30 | cellDeps[1].outPoint.txHash = 31 | '0xff1bb8baaa5fca57b82901f993b35d1c5f697c855133fad90ffe482c3e342612' 32 | 33 | // detecting locale 34 | ctx.$i18n.locale = ctx.$q.lang.getLocale() 35 | 36 | console.log('UA: ', navigator.userAgent) 37 | 38 | await getAccount(ctx) 39 | 40 | imTokenInit(ctx) 41 | abcInit(ctx) 42 | alphaInit(ctx) 43 | console.log('IN: ', provider) 44 | } 45 | 46 | function imTokenInit(ctx) { 47 | if (!window.ethereum.isImToken) return 48 | provider = 'imToken' 49 | 50 | try { 51 | imToken.callAPI('navigator.configure', { 52 | // navigationStyle: 'transparent' 53 | navigatorColor: 'black' 54 | }) 55 | ctx.$store.commit('config/UPDATE', { 56 | provider, 57 | showBar: false, 58 | showHeader: false, 59 | barHeight: 23 60 | }) 61 | } catch (e) { 62 | console.log(e) 63 | } 64 | } 65 | 66 | function abcInit(ctx) { 67 | if (navigator.userAgent.indexOf('ABCWallet') < 0) return 68 | provider = 'ABCWallet' 69 | ABCWallet.webview.setTitlebar({ 70 | title: 'CKB P-Wallet', 71 | forecolor: '#ffffff', 72 | bgcolor: '#000000' 73 | }) 74 | 75 | ctx.$store.commit('config/UPDATE', { 76 | provider, 77 | showBar: false, 78 | showHeader: false, 79 | showFooter: false 80 | }) 81 | } 82 | 83 | function alphaInit(ctx) { 84 | if (navigator.userAgent.indexOf('AlphaWallet') < 0) return 85 | provider = 'AlphaWallet' 86 | ctx.$store.commit('config/UPDATE', { 87 | provider, 88 | showBar: false, 89 | showHeader: true, 90 | showFooter: false 91 | }) 92 | } 93 | 94 | export const getAccount = async ctx => { 95 | console.log('get account') 96 | const getAccountPromise = new Promise((resolve, reject) => { 97 | window.web3.eth.getAccounts((err, result) => { 98 | err && reject(err) 99 | resolve(result) 100 | }) 101 | }) 102 | 103 | return new Promise(async (resolve, reject) => { 104 | let account = '0x' 105 | if (typeof window.ethereum !== 'undefined') { 106 | try { 107 | ethereum.autoRefreshOnNetworkChange = false 108 | console.log('try 1') 109 | const accounts = await window.ethereum.enable() 110 | // const accounts = await getAccountPromise 111 | account = accounts[0] 112 | console.log('account', account) 113 | ctx.$store.commit('account/SET_ADDRESS', account) 114 | ctx.$store.commit('account/SET_PLATFORM', 'eth') 115 | 116 | // watch address change 117 | ethereum.on('accountsChanged', function(accounts) { 118 | ctx.$store.commit('account/SET_ADDRESS', accounts[0]) 119 | }) 120 | 121 | resolve(account) 122 | } catch (err) { 123 | reject(err) 124 | } 125 | } else if (window.web3) { 126 | console.log('try 2') 127 | const accounts = await getAccountPromise 128 | account = accounts[0] 129 | console.log('account', account) 130 | ctx.$store.commit('account/SET_ADDRESS', account) 131 | ctx.$store.commit('account/SET_PLATFORM', 'eth') 132 | resolve(account) 133 | } 134 | }) 135 | } 136 | 137 | export const loadDeps = async () => { 138 | ckb.config.secp256k1Dep = await api.loadK1() 139 | ckb.config.daoDep = await api.loadDaoCell() 140 | console.log('deps loaded', ckb.config) 141 | } 142 | 143 | export const getFullAddress = ( 144 | address, 145 | prefix = 'ckt', 146 | type = '0x04', 147 | codeHash = keccak_code_hash 148 | ) => { 149 | return ckb.utils.fullPayloadToAddress({ 150 | arg: address, 151 | prefix, 152 | type, 153 | codeHash 154 | }) 155 | } 156 | 157 | export const getLockHash = address => { 158 | if (address === '-') return null 159 | return ckb.utils.scriptToHash({ 160 | args: address, 161 | hashType: 'type', 162 | codeHash: keccak_code_hash 163 | }) 164 | } 165 | 166 | export const sendTx = async (cells, outputs, fee, address) => { 167 | const rawTx = buildTx(cells, outputs, fee, address) 168 | const tx = await signTx(cells, rawTx, address) 169 | if (!tx) return null 170 | return await ckb.rpc.sendTransaction(tx) 171 | } 172 | 173 | export const getLockScriptFromAddress = address => { 174 | console.log('address is', address) 175 | const payload = ckbUtils.parseAddress(address, 'hex').replace('0x', '') 176 | 177 | const type = payload.substring(0, 2) 178 | 179 | let codeHash, hashType, args 180 | 181 | if (type == '01') { 182 | if (payload.substring(2, 2) == '00') { 183 | codeHash = 184 | '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8' 185 | } else { 186 | //TODO multisig code here 187 | codeHash = 188 | '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8' 189 | } 190 | hashType = 'type' 191 | args = '0x' + payload.substring(4) 192 | } else if (type == '02') { 193 | hashType = 'data' 194 | codeHash = '0x' + payload.substring(2, 66) 195 | args = '0x' + payload.substring(66) 196 | } else if (type == '04') { 197 | hashType = 'type' 198 | codeHash = '0x' + payload.substring(2, 66) 199 | args = '0x' + payload.substring(66) 200 | } 201 | 202 | return { codeHash, hashType, args } 203 | } 204 | const getLockScriptFromEthAddress = address => { 205 | return { 206 | codeHash: keccak_code_hash, 207 | args: address, 208 | hashType: 'type' 209 | } 210 | } 211 | const getDaoTypeScript = () => { 212 | return { 213 | codeHash: ckb.config.daoDep.typeHash, 214 | args: '0x', 215 | hashType: ckb.config.daoDep.hashType 216 | } 217 | } 218 | 219 | /** 220 | * build raw Tx for send etch locked cell to ckb locked cell 221 | * @param {*} cells 222 | * @param {*} outputs 223 | * @param {*} fee 224 | * @param {*} address 225 | */ 226 | export const buildTx = (cells, outputs, fee, address) => { 227 | const fromAddress = 'ckt1qyqwknsshmvnj8tj6wnaua53adc0f8jtrrzqz4xcu2' 228 | 229 | console.log('outputs', outputs) 230 | if (!keccak_code_hash) return null 231 | 232 | const receivePairs = outputs.map(o => { 233 | let { address: toAddress, amount: capacity } = o 234 | if (toAddress.indexOf('ck') !== 0) { 235 | toAddress = fromAddress 236 | } 237 | return { address: toAddress, capacity: JSBI.BigInt(capacity) } 238 | }) 239 | 240 | const cellsMap = new Map() 241 | let mapKey = ckb.utils.scriptToHash({ 242 | codeHash: ckb.config.secp256k1Dep.codeHash, 243 | hashType: ckb.config.secp256k1Dep.hashType, 244 | args: '0x' + ckb.utils.parseAddress(fromAddress, 'hex').slice(6) 245 | }) 246 | cellsMap.set(mapKey, cells) 247 | 248 | //assemble transaction 249 | const txParams = { 250 | fromAddresses: [fromAddress], 251 | receivePairs, 252 | fee: '0x' + JSBI.BigInt(fee).toString(16), 253 | cells: cellsMap, 254 | deps: ckb.config.secp256k1Dep 255 | } 256 | console.log('rawTxParams', txParams) 257 | 258 | const rawTx = ckb.generateRawTransaction(txParams) 259 | console.log('rawTX: ', rawTx) 260 | 261 | rawTx.witnesses = rawTx.inputs.map(() => '0x') 262 | rawTx.witnesses[0] = { 263 | lock: '', 264 | inputType: '', 265 | outputType: '' 266 | } 267 | 268 | rawTx.outputs = replaceOutputsLock(rawTx, outputs, address) 269 | 270 | // console.log('rawTx outputs', rawTx.outputs) 271 | 272 | // set cell deps for transaction 273 | // dep1: eth lock script 274 | // dep2: secp256k1_data_bin script in genesisBlock 275 | rawTx.cellDeps = cellDeps 276 | // console.log(rawTx.cellDeps) 277 | return rawTx 278 | } 279 | 280 | /** 281 | * sign one eth lock input 282 | * @param {*} ckb 283 | * @param {*} rawTx 284 | */ 285 | export const signTx = async (unspentCells, rawTx, address) => { 286 | const { hexToBytes, serializeWitnessArgs, toHexInLittleEndian } = ckb.utils 287 | 288 | const txHash = ckb.utils.rawTransactionToHash(rawTx) 289 | 290 | const emptyWitness = { 291 | ...rawTx.witnesses[0], 292 | lock: `0x${'0'.repeat(130)}` 293 | } 294 | 295 | const serializedEmptyWitnessBytes = hexToBytes( 296 | serializeWitnessArgs(emptyWitness) 297 | ) 298 | const serialziedEmptyWitnessSize = serializedEmptyWitnessBytes.length 299 | 300 | // Calculate keccak256 hash for rawTransaction 301 | let hashBytes = hexToBytes(txHash) 302 | 303 | hashBytes = mergeTypedArraysUnsafe( 304 | hashBytes, 305 | hexToBytes( 306 | toHexInLittleEndian(`0x${serialziedEmptyWitnessSize.toString(16)}`, 8) 307 | ) 308 | ) 309 | hashBytes = mergeTypedArraysUnsafe(hashBytes, serializedEmptyWitnessBytes) 310 | 311 | rawTx.witnesses.slice(1).forEach(w => { 312 | const bytes = hexToBytes( 313 | typeof w === 'string' ? w : serializeWitnessArgs(w) 314 | ) 315 | hashBytes = mergeTypedArraysUnsafe( 316 | hashBytes, 317 | hexToBytes(toHexInLittleEndian(`0x${bytes.length.toString(16)}`, 8)) 318 | ) 319 | hashBytes = mergeTypedArraysUnsafe(hashBytes, bytes) 320 | }) 321 | let message = web3utils.sha3(hashBytes) 322 | // let message = ethUtil.keccak256(ethUtil.toBuffer(hashBytes)); 323 | // console.log('message is', message) 324 | 325 | // Ehereum Personal Sign for keccak256 hash of rawTransaction 326 | let signatureHexString = await signWitness( 327 | unspentCells, 328 | rawTx, 329 | message, 330 | address 331 | ) 332 | let tx = null 333 | console.log('signatureHexString', signatureHexString) 334 | try { 335 | let signatureObj = ethUtil.fromRpcSig(signatureHexString) 336 | signatureObj.v -= 27 337 | signatureHexString = ethUtil.bufferToHex( 338 | Buffer.concat([ 339 | ethUtil.setLengthLeft(signatureObj.r, 32), 340 | ethUtil.setLengthLeft(signatureObj.s, 32), 341 | ethUtil.toBuffer(signatureObj.v) 342 | ]) 343 | ) 344 | 345 | emptyWitness.lock = signatureHexString 346 | 347 | let signedWitnesses = [ 348 | serializeWitnessArgs(emptyWitness), 349 | ...rawTx.witnesses.slice(1) 350 | ] 351 | 352 | tx = { 353 | ...rawTx, 354 | witnesses: signedWitnesses.map(witness => 355 | typeof witness === 'string' ? witness : serializeWitnessArgs(witness) 356 | ) 357 | } 358 | } catch (e) { 359 | console.log(e.toString()) 360 | } 361 | 362 | return tx 363 | } 364 | 365 | /** 366 | * merge two UInt8Array 367 | * @param {*} a 368 | * @param {*} b 369 | */ 370 | function mergeTypedArraysUnsafe(a, b) { 371 | var c = new a.constructor(a.length + b.length) 372 | c.set(a) 373 | c.set(b, a.length) 374 | 375 | return c 376 | } 377 | 378 | export const signWitness = async (unspentCells, tx, message, from) => { 379 | const signFunc = new Promise((resolve, reject) => { 380 | if (web3.currentProvider.isMetaMask && provider !== 'ABCWallet') { 381 | const typedData = buildTypedData(unspentCells, tx, message) 382 | const params = [from, typedData] 383 | const method = 'eth_signTypedData_v4' 384 | 385 | console.log('typedData', params) 386 | 387 | web3.currentProvider.sendAsync( 388 | { 389 | method, 390 | params, 391 | from 392 | }, 393 | function(err, result) { 394 | if (err) { 395 | // reject(err) 396 | console.log(err.toString()) 397 | } 398 | // console.log(result); 399 | resolve(result.result) 400 | } 401 | ) 402 | return 403 | } 404 | 405 | if (web3.currentProvider.isImToken) { 406 | web3.eth.sign(from, message, (err, result) => { 407 | if (err) { 408 | // reject(err) 409 | console.log(err.toString()) 410 | } 411 | resolve(result) 412 | }) 413 | return 414 | } 415 | var params = [message, from] 416 | var method = 'personal_sign' 417 | // console.log('params', params) 418 | web3.currentProvider.sendAsync( 419 | { 420 | method, 421 | params, 422 | from 423 | }, 424 | function(err, result) { 425 | // console.log(result, err) 426 | if (err) { 427 | reject(err) 428 | } else if (result.error) { 429 | reject(result.error) 430 | } else { 431 | resolve(result.result) 432 | } 433 | } 434 | ) 435 | }) 436 | 437 | const witness = await signFunc 438 | 439 | return witness 440 | } 441 | 442 | const buildTypedData = (unspentCells, rawTransaction, messageHash) => { 443 | const typedData = { 444 | domain: { 445 | chainId: 1, 446 | name: 'ckb.pw', 447 | verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', 448 | version: '1' 449 | }, 450 | 451 | message: { 452 | hash: 453 | '0x545529d4464064d8394c557afb06f489e7044a63984c6113385431d93dcffa1b', 454 | fee: '0.00100000CKB', 455 | 'input-sum': '100.00000000CKB', 456 | to: [ 457 | { 458 | address: 'ckb1qyqv4yga3pgw2h92hcnur7lepdfzmvg8wj7qwstnwm', 459 | amount: '100.00000000CKB' 460 | }, 461 | { 462 | address: 463 | 'ckb1qftyhqxwuxdzp5zk4rctscnrr6stjrmfjdx54v05q8t3ad3493m6mhcekrn0vk575h44ql9ry53z3gzhtc2exudxcyg', 464 | amount: '799.99800000CKB' 465 | } 466 | ] 467 | }, 468 | primaryType: 'CKBTransaction', 469 | types: { 470 | EIP712Domain: [ 471 | { name: 'name', type: 'string' }, 472 | { name: 'version', type: 'string' }, 473 | { name: 'chainId', type: 'uint256' }, 474 | { name: 'verifyingContract', type: 'address' } 475 | ], 476 | CKBTransaction: [ 477 | { name: 'hash', type: 'bytes32' }, 478 | { name: 'fee', type: 'string' }, 479 | { name: 'input-sum', type: 'string' }, 480 | { name: 'to', type: 'Output[]' } 481 | ], 482 | Output: [ 483 | { name: 'address', type: 'string' }, 484 | { name: 'amount', type: 'string' } 485 | ] 486 | } 487 | } 488 | 489 | const message = 490 | '0x' + 491 | ethUtil.hashPersonalMessage(ethUtil.toBuffer(messageHash)).toString('hex') 492 | console.log('hash', messageHash) 493 | console.log('personal Hash', message) 494 | 495 | typedData.message.hash = message 496 | typedData.message.to = [] 497 | 498 | let input_capacities = 0 499 | let output_capacities = 0 500 | 501 | rawTransaction.inputs.forEach(input => { 502 | const { 503 | previousOutput: { txHash, index } 504 | } = input 505 | let cell = unspentCells.filter( 506 | t => t.outPoint.txHash == txHash && t.outPoint.index == index 507 | )[0] 508 | const { capacity } = cell 509 | input_capacities += Number(capacity) 510 | }) 511 | 512 | rawTransaction.outputs.forEach(output => { 513 | let { hashType, codeHash, args } = output.lock 514 | const capacity = web3utils.hexToNumber(output.capacity) 515 | output_capacities += capacity 516 | let amount = (capacity / 100000000.0).toFixed(8) + 'CKB' 517 | 518 | let address = 'unknown' 519 | if (output.lock.keccak_code_hash === '0x00000000000000000000000000000000') { 520 | address = 'unknown' 521 | } else { 522 | if ( 523 | codeHash === 524 | '0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8' 525 | ) { 526 | address = formatCKBAddress( 527 | ckbUtils.bech32Address(args, { 528 | prefix: 'ckt', 529 | type: '0x01', 530 | codeHashOrCodeHashIndex: '0x00' 531 | }) 532 | ) 533 | } else { 534 | let type = '0x02' 535 | if (hashType == 'data') { 536 | type = '0x02' 537 | } else { 538 | type = '0x04' 539 | } 540 | address = formatCKBAddress( 541 | ckbUtils.fullPayloadToAddress({ 542 | arg: args, 543 | prefix: 'ckt', 544 | type, 545 | codeHash 546 | }) 547 | ) 548 | } 549 | } 550 | typedData.message.to.push({ address, amount }) 551 | }) 552 | 553 | console.log( 554 | 'input_capacities/output_capacities', 555 | input_capacities, 556 | output_capacities 557 | ) 558 | 559 | typedData.message['input-sum'] = 560 | (input_capacities / 100000000.0).toFixed(8) + 'CKB' 561 | typedData.message.fee = 562 | ((input_capacities - output_capacities) / 100000000.0).toFixed(8) + 'CKB' 563 | 564 | // console.log('typed data', JSON.stringify(typedData)); 565 | // const result = '0x' + sigUtil.TypedDataUtils.sign(typedData).toString('hex'); 566 | return JSON.stringify(typedData) 567 | } 568 | 569 | export const getFee = function(feeRate, cells, outputs, address) { 570 | const rawTx = buildTx(cells, outputs, '0', address) 571 | if (!rawTx) return '0' 572 | 573 | const size = txSize(rawTx) 574 | const fee = JSBI.divide( 575 | JSBI.multiply(JSBI.BigInt(size), JSBI.BigInt(feeRate)), 576 | JSBI.BigInt(1000) 577 | ) 578 | 579 | return fee.toString() 580 | } 581 | 582 | const replaceOutputsLock = (tx, toAddressList, ethAddress) => { 583 | const txOutputs = tx.outputs 584 | 585 | for (let i in txOutputs) { 586 | if (i < toAddressList.length) { 587 | const { address: toAddress } = toAddressList[i] 588 | 589 | if (toAddress.indexOf('ck') === 0) { 590 | txOutputs[i].lock = getLockScriptFromAddress(toAddress) 591 | } else { 592 | txOutputs[i].lock = { 593 | hashType: 'type', 594 | codeHash: keccak_code_hash, 595 | args: toAddress 596 | } 597 | } 598 | } else { 599 | // change output 600 | txOutputs[i].lock = { 601 | hashType: 'type', 602 | codeHash: keccak_code_hash, 603 | args: ethAddress 604 | } 605 | } 606 | } 607 | 608 | console.log(txOutputs) 609 | return txOutputs 610 | } 611 | 612 | export const DAO = { 613 | /** 614 | * Deposit 615 | */ 616 | buildDepositTx: (unspentCell, fromAddress, capacity, fee) => { 617 | const tempAddress = 'ckt1qyqwknsshmvnj8tj6wnaua53adc0f8jtrrzqz4xcu2' 618 | const txParams = { 619 | fromAddress: tempAddress, 620 | toAddress: tempAddress, 621 | capacity: '0x' + JSBI.BigInt(capacity).toString(16), 622 | fee: '0x' + JSBI.BigInt(fee).toString(16), 623 | safeMode: true, 624 | cells: unspentCell, 625 | deps: ckb.config.secp256k1Dep 626 | } 627 | console.log('tx params', txParams) 628 | const depositTx = ckb.generateRawTransaction(txParams) 629 | 630 | depositTx.outputs[0].type = getDaoTypeScript() 631 | depositTx.outputsData[0] = '0x0000000000000000' 632 | 633 | depositTx.cellDeps = [ 634 | ...cellDeps, 635 | { 636 | depType: 'code', 637 | outPoint: ckb.config.daoDep.outPoint 638 | } 639 | ] 640 | 641 | depositTx.witnesses.unshift({ lock: '', inputType: '', outputType: '' }) 642 | depositTx.outputs = replaceOutputsLock( 643 | depositTx, 644 | [{ address: fromAddress }], 645 | fromAddress 646 | ) 647 | 648 | return depositTx 649 | }, 650 | 651 | getFee: (feeRate, rawTx) => { 652 | const size = txSize(rawTx) 653 | const fee = JSBI.divide( 654 | JSBI.multiply(JSBI.BigInt(size), JSBI.BigInt(feeRate)), 655 | JSBI.BigInt(1000) 656 | ) 657 | 658 | return fee.toString() 659 | }, 660 | 661 | async deposit(address, amount, cells, feeRate) { 662 | let rawTx = this.buildDepositTx(cells, address, fromCKB(amount), 0) 663 | const fee = this.getFee(feeRate, rawTx) 664 | rawTx = this.buildDepositTx(cells, address, fromCKB(amount), fee) 665 | console.log('raw tx', rawTx) 666 | const tx = await signTx(cells, rawTx, address) 667 | if (!tx) return null 668 | const txHash = await ckb.rpc.sendTransaction(tx) 669 | console.log('DAO Deposit TX: ', txHash) 670 | return txHash 671 | }, 672 | 673 | async withdraw1(daoItem, address, feeRate) { 674 | const unspentCells = await api.getUnspentCells( 675 | getLockHash(address), 676 | fromCKB(62) 677 | ) 678 | 679 | const changeCell = unspentCells[0] 680 | console.log('change cell', changeCell) 681 | const { depositBlockHeader, hash, idx, size } = daoItem 682 | const outPoint = { 683 | txHash: hash, 684 | index: '0x' + Number(idx).toString(16) 685 | } 686 | let outputCell = { 687 | capacity: '0x' + JSBI.BigInt(size).toString(16), 688 | lock: getLockScriptFromEthAddress(address), 689 | type: getDaoTypeScript() 690 | // outPoint: outPoint 691 | } 692 | 693 | let rawTx = this.buildWithdraw1Tx( 694 | changeCell, 695 | outPoint, 696 | outputCell, 697 | '0x10000', 698 | depositBlockHeader, 699 | address 700 | ) 701 | const fee = this.getFee(feeRate, rawTx) 702 | console.log('Fee', fee) 703 | rawTx = this.buildWithdraw1Tx( 704 | changeCell, 705 | outPoint, 706 | outputCell, 707 | fee, 708 | depositBlockHeader, 709 | address 710 | ) 711 | console.log('raw tx', rawTx) 712 | 713 | outputCell = { 714 | ...outputCell, 715 | outPoint: outPoint 716 | } 717 | const tx = await signTx([changeCell, outputCell], rawTx, address) 718 | if (!tx) return null 719 | const txHash = await ckb.rpc.sendTransaction(tx) 720 | console.log('DAO Withdraw 1 TX: ', txHash) 721 | return txHash 722 | }, 723 | 724 | /** 725 | * Withdraw PHASE ONE 726 | */ 727 | buildWithdraw1Tx: ( 728 | changeCell, 729 | outPoint, 730 | outputCell, 731 | fee, 732 | depositBlockHeader, 733 | fromAddress 734 | ) => { 735 | const encodedBlockNumber = ckb.utils.toHexInLittleEndian( 736 | '0x' + Number(depositBlockHeader.number).toString(16), 737 | 8 738 | ) 739 | const tempAddress = 'ckt1qyqwknsshmvnj8tj6wnaua53adc0f8jtrrzqz4xcu2' 740 | const rawTx = ckb.generateRawTransaction({ 741 | fromAddress: tempAddress, 742 | toAddress: tempAddress, 743 | capacity: '0x0', 744 | fee: '0x' + JSBI.BigInt(fee).toString(16), 745 | safeMode: true, 746 | deps: ckb.config.secp256k1Dep, 747 | capacityThreshold: '0x0', 748 | cells: [changeCell] 749 | }) 750 | 751 | rawTx.outputs = replaceOutputsLock( 752 | rawTx, 753 | [{ address: fromAddress }], 754 | fromAddress 755 | ) 756 | 757 | rawTx.outputs.splice(0, 1) 758 | rawTx.outputsData.splice(0, 1) 759 | 760 | rawTx.inputs.unshift({ previousOutput: outPoint, since: '0x0' }) 761 | rawTx.outputs.unshift(outputCell) 762 | 763 | rawTx.cellDeps = [ 764 | ...cellDeps, 765 | { 766 | depType: 'code', 767 | outPoint: ckb.config.daoDep.outPoint 768 | } 769 | ] 770 | 771 | rawTx.headerDeps.push(depositBlockHeader.hash) 772 | rawTx.outputsData.unshift(encodedBlockNumber) 773 | rawTx.witnesses.unshift({ 774 | lock: '', 775 | inputType: '', 776 | outputType: '' 777 | }) 778 | 779 | return rawTx 780 | }, 781 | 782 | /** 783 | * Withdraw PHASE TWO 784 | */ 785 | buildWithdraw2Tx: ( 786 | depositBlockHeader, 787 | withdrawBlockHeader, 788 | withdrawOutPoint, 789 | fee, 790 | toLock, 791 | outputCapacity 792 | ) => { 793 | const DAO_LOCK_PERIOD_EPOCHS = 180 794 | 795 | const depositEpoch = depositBlockHeader.epoch 796 | const withdrawEpoch = withdrawBlockHeader.epoch 797 | 798 | const withdrawFraction = JSBI.multiply( 799 | JSBI.BigInt(withdrawEpoch.index), 800 | JSBI.BigInt(depositEpoch.length) 801 | ) 802 | const depositFraction = JSBI.multiply( 803 | JSBI.BigInt(depositEpoch.index), 804 | JSBI.BigInt(withdrawEpoch.length) 805 | ) 806 | let depositedEpochs = JSBI.subtract( 807 | JSBI.BigInt(withdrawEpoch.number), 808 | JSBI.BigInt(depositEpoch.number) 809 | ) 810 | if (JSBI.greaterThan(withdrawFraction, depositFraction)) { 811 | depositedEpochs = JSBI.add(depositedEpochs, JSBI.BigInt(1)) 812 | } 813 | const lockEpochs = JSBI.multiply( 814 | JSBI.divide( 815 | JSBI.add(depositedEpochs, JSBI.BigInt(DAO_LOCK_PERIOD_EPOCHS - 1)), 816 | JSBI.BigInt(DAO_LOCK_PERIOD_EPOCHS) 817 | ), 818 | JSBI.BigInt(DAO_LOCK_PERIOD_EPOCHS) 819 | ) 820 | const minimalSince = absoluteEpochSince({ 821 | length: `0x${JSBI.BigInt(depositEpoch.length).toString(16)}`, 822 | index: `0x${JSBI.BigInt(depositEpoch.index).toString(16)}`, 823 | number: `0x${JSBI.add( 824 | JSBI.BigInt(depositEpoch.number), 825 | lockEpochs 826 | ).toString(16)}` 827 | }) 828 | 829 | const targetCapacity = JSBI.BigInt(outputCapacity) 830 | const targetFee = JSBI.BigInt(`${fee}`) 831 | if (JSBI.lessThan(targetCapacity, targetFee)) { 832 | throw new Error( 833 | `The fee(${targetFee}) is too big that withdraw(${targetCapacity}) is not enough` 834 | ) 835 | } 836 | 837 | const outputs = [ 838 | { 839 | capacity: `0x${JSBI.subtract(targetCapacity, targetFee).toString(16)}`, 840 | lock: toLock 841 | } 842 | ] 843 | 844 | const outputsData = ['0x'] 845 | 846 | const tx = { 847 | version: '0x0', 848 | cellDeps: [ 849 | ...cellDeps, 850 | { outPoint: ckb.config.daoDep.outPoint, depType: 'code' } 851 | ], 852 | headerDeps: [depositBlockHeader.hash, withdrawBlockHeader.hash], 853 | inputs: [ 854 | { 855 | previousOutput: withdrawOutPoint, 856 | since: minimalSince 857 | } 858 | ], 859 | outputs, 860 | outputsData, 861 | witnesses: [ 862 | { 863 | lock: '', 864 | inputType: '0x0000000000000000', 865 | outputType: '' 866 | } 867 | ] 868 | } 869 | 870 | return tx 871 | } 872 | } 873 | 874 | const absoluteEpochSince = ({ length, index, number }) => { 875 | const { JSBI } = ckb.utils 876 | const epochSince = JSBI.add( 877 | JSBI.add( 878 | JSBI.add( 879 | JSBI.leftShift(JSBI.BigInt(0x20), JSBI.BigInt(56)), 880 | JSBI.leftShift(JSBI.BigInt(length), JSBI.BigInt(40)) 881 | ), 882 | JSBI.leftShift(JSBI.BigInt(index), JSBI.BigInt(24)) 883 | ), 884 | JSBI.BigInt(number) 885 | ) 886 | 887 | return `0x${epochSince.toString(16)}` 888 | } 889 | --------------------------------------------------------------------------------