message:'签名消息',
confirmSignature:'确认签名'
},
+ enable:{
+ title:'请求授权',
+ domain: '域名',
+ message: '请求获取你的钱包地址,是否同意?',
+ cancel:'取消',
+ confirm:'确认授权'
+ },
receive:{
address: '地址',
tips:'提示:点击地址进行拷贝。'
message:'Sign Message',
confirmSignature:'Sign'
},
+ enable:{
+ title:'Connect Request',
+ domain: 'Domain',
+ message: 'would like to connect your account',
+ cancel:'Cancel',
+ confirm:'Connect'
+ },
receive:{
address: 'Address',
tips:'Tips: Click address to copy directly.'
import { LocalStream } from 'extension-streams'
import InternalMessage from '@/messages/internal'
import * as MsgTypes from './messages/types'
+import NotificationService from './services/NotificationService'
+import StorageService from './services/StorageService'
+import Prompt from './prompts/Prompt';
+import * as PromptTypes from './prompts/PromptTypes'
import Error from './utils/errors/Error'
import accountAction from "@/models/account";
import bytom from "@/models/bytom";
+let prompt = null;
+
export default class Background {
constructor() {
this.setupInternalMessaging()
this.signMessage(sendResponse, message.payload)
break
case MsgTypes.REQUEST_CURRENT_ACCOUNT:
- this.requestCurrentAccount(sendResponse)
+ this.requestCurrentAccount(sendResponse, message.payload)
break
case MsgTypes.REQUEST_CURRENT_NETWORK:
this.requestCurrentNetwork(sendResponse)
break
- case MsgTypes.REQUEST_ACCOUNT_LIST:
- this.requestAccountList(sendResponse)
+ case MsgTypes.ENABLE:
+ Background.authenticate(sendResponse, message.payload)
break
+ case MsgTypes.SET_PROMPT:
+ Background.setPrompt(sendResponse, message.payload);
+ break;
+ case MsgTypes.GET_PROMPT:
+ Background.getPrompt(sendResponse);
+ break;
}
}
+ static setPrompt(sendResponse, notification){
+ prompt = notification;
+ sendResponse(true);
+ }
+
+ static getPrompt(sendResponse){
+ sendResponse(prompt);
+ }
+
signMessage(sendResponse, payload) {
var promptURL = chrome.extension.getURL('pages/prompt.html')
var requestBody = payload
}
});
- chrome.windows.onRemoved.addListener(function(windowId){
- if(windowId === window.id) {
- sendResponse(Error.promptClosedWithoutAction());
- return false;
- }
- });
+ // chrome.windows.onRemoved.addListener(function(windowId){
+ // if(windowId === window.id) {
+ // sendResponse(Error.promptClosedWithoutAction());
+ // return false;
+ // }
+ // });
}
)
}
)
}
- requestCurrentAccount(sendResponse){
- const currentAccount = JSON.parse(localStorage.currentAccount)
- delete(currentAccount['label'])
- delete(currentAccount['net'])
- currentAccount['accountId'] = currentAccount['guid']
- delete(currentAccount['guid'])
- delete(currentAccount['balance'])
+ requestCurrentAccount(sendResponse, payload){
+ Background.load(bytom => {
+ const domain = payload.domain;
+ if(bytom.settings.domains.find(_domain => _domain === domain)) {
+ const currentAccount = JSON.parse(localStorage.currentAccount)
+ delete(currentAccount['label'])
+ delete(currentAccount['net'])
+ currentAccount['accountId'] = currentAccount['guid']
+ delete(currentAccount['guid'])
+ delete(currentAccount['balance'])
+
+ sendResponse(currentAccount);
+ } else{
+ sendResponse(null);
+ return false;
+ }
+ })
- sendResponse(currentAccount)
}
requestCurrentNetwork(sendResponse){
sendResponse(localStorage.bytomNet)
}
- requestAccountList(sendResponse){
- accountAction.list().then(resp=>{
- const accountList = resp
- accountList.forEach(function(account) {
- delete(account['label'])
- delete(account['net'])
- account['accountId'] = account['guid']
- delete(account['guid'])
- delete(account['balance'])
- })
- sendResponse(accountList)
- })
- }
-
send(sendResponse, payload) {
const action = payload.action
if(action){
}
}
}
+
+ /***
+ * Returns the saved instance of Bytom from the storage
+ * @param sendResponse - Delegating response handler
+ * @returns {Bytom}
+ */
+ static load(sendResponse){
+ StorageService.get().then(bytom => {
+ sendResponse(bytom)
+ })
+ }
+
+ /***
+ * Updates the Scatter instance inside persistent storage
+ * @param sendResponse - Delegating response handler
+ * @param bytom - The updated cleartext Scatter instance
+ * @returns {boolean}
+ */
+ static update(sendResponse, bytom){
+ StorageService.save(bytom).then(saved => {
+ sendResponse(bytom)
+ })
+ }
+
+ static authenticate(sendResponse, payload){
+ Background.load(bytom => {
+ const domain = payload.domain;
+ const currentAccount = JSON.parse(localStorage.currentAccount)
+ delete(currentAccount['label'])
+ delete(currentAccount['net'])
+ currentAccount['accountId'] = currentAccount['guid']
+ delete(currentAccount['guid'])
+ delete(currentAccount['balance'])
+
+ if(bytom.settings.domains.find(_domain => _domain === domain)) {
+ sendResponse(currentAccount);
+ } else{
+ NotificationService.open(new Prompt(PromptTypes.REQUEST_AUTH, payload.domain, approved => {
+ if(approved === false || approved.hasOwnProperty('isError')) sendResponse(approved);
+ else {
+ bytom.settings.domains.unshift(domain);
+ if(approved === true){
+ this.update(() => sendResponse(currentAccount), bytom);
+ }else{
+ this.update(() => sendResponse(approved), bytom);
+ }
+ }
+ }));
+ }
+ })
+ }
+
+
}
new Background()
import InternalMessage from '@/messages/internal'
import * as MsgTypes from './messages/types'
import * as EventNames from '@/messages/event'
+import {strippedHost} from '@/utils/GenericTools'
+
let stream = new WeakMap()
let INJECTION_SCRIPT_FILENAME = 'js/inject.js'
stream.onSync(async () => {
const defaultAccount = await this.getDefaultAccount();
const net = await this.getDefaultNetwork();
- const accountList = await this.getAccountList();
// Pushing an instance of Bytomdapp to the web application
stream.send(
- NetworkMessage.payload(MsgTypes.PUSH_BYTOM, {defaultAccount, net, accountList}),
+ NetworkMessage.payload(MsgTypes.PUSH_BYTOM, {defaultAccount, net}),
EventNames.INJECT
)
case MsgTypes.SEND:
this.transfer(msg.type, networkMessage)
break
+ case MsgTypes.ENABLE:
+ this.enable(msg.type, networkMessage)
+ break
default:
stream.send(networkMessage.error('errtest'), EventNames.INJECT)
break
getVersion() {}
getDefaultAccount(){
- return InternalMessage.signal(MsgTypes.REQUEST_CURRENT_ACCOUNT)
+ return InternalMessage.payload(MsgTypes.REQUEST_CURRENT_ACCOUNT,{domain:strippedHost()})
.send()
}
.send()
}
- getAccountList(){
- return InternalMessage.signal(MsgTypes.REQUEST_ACCOUNT_LIST)
- .send()
- }
-
respond(message, payload) {
if (!isReady) return
.send()
.then(res => this.respond(message, res))
}
+
+ enable(type, networkMessage) {
+ networkMessage.payload ={
+ domain: strippedHost()
+ }
+
+ this.transfer(type, networkMessage)
+ }
+
}
const content = new Content()
_subscribe()
}
+ enable(){
+ return _send(MsgTypes.ENABLE)
+ .then(async default_account =>{
+ this.default_account = default_account;
+ return default_account;
+ })
+ }
+
send_transaction(params) {
return _send(MsgTypes.TRANSFER, params)
}
export const UPDATE_BYTOM = 'updateBytom'
export const AUTHENTICATE = 'authenticate'
export const TRANSFER = 'transfer'
+export const ENABLE = 'enable'
export const ADVTRANSFER = 'advTransfer'
export const SIGNMESSAGE = 'signMessage'
export const SEND = 'send'
export const REQUEST_CURRENT_ACCOUNT = 'defaultAccount';
export const REQUEST_CURRENT_NETWORK = 'currentNetwork';
export const REQUEST_ACCOUNT_LIST = 'accountList';
+
+
+//Internal Message
+export const SET_PROMPT = 'setPrompt';
+export const GET_PROMPT = 'getPrompt';
--- /dev/null
+export default class Prompt {
+
+ constructor(_type = '', _domain = '', _responder = null){
+ this.type = _type;
+ this.domain = _domain;
+ // this.network = _network;
+ // this.data = _data;
+ this.responder = _responder;
+ }
+
+ static placeholder(){ return new Prompt(); }
+ static fromJson(json){ return Object.assign(this.placeholder(), json); }
+
+ routeName(){
+ return `${this.type}`;
+ }
+
+}
--- /dev/null
+export const REQUEST_AUTH = 'enable';
+export const REQUEST_SIGNATURE = 'requestSignature';
+export const REQUEST_ARBITRARY_SIGNATURE = 'signatureArbitrary';
+export const REQUEST_ADD_NETWORK = 'requestAddNetwork';
+export const REQUEST_UNLOCK = 'requestUnlock';
+export const UPDATE_VERSION = 'updateVersion';
}
},
{
+ path: '/enable',
+ name: 'enable',
+ meta: { title: '授权' },
+ component: resolve => {
+ require(['@/views/prompts/authentication.vue'], resolve)
+ }
+ },
+ {
path: '/transfer/info',
name: 'transfer-info',
meta: { title: '交易详情' },
--- /dev/null
+import Error from '../utils/errors/Error'
+import {apis} from '../utils/BrowserApis';
+import InternalMessage from '../messages/internal'
+import * as InternalMessageTypes from '../messages/types'
+
+let openWindow = null;
+
+export default class NotificationService {
+
+ /***
+ * Opens a prompt window outside of the extension
+ * @param notification
+ */
+ static async open(notification){
+ if(openWindow){
+ // For now we're just going to close the window to get rid of the error
+ // that is caused by already open windows swallowing all further requests
+ openWindow.close();
+ openWindow = null;
+
+ // Alternatively we could focus the old window, but this would cause
+ // urgent 1-time messages to be lost, such as after dying in a game and
+ // uploading a high-score. That message will be lost.
+ // openWindow.focus();
+ // return false;
+
+ // A third option would be to add a queue, but this could cause
+ // virus-like behavior as apps overflow the queue causing the user
+ // to have to quit the browser to regain control.
+ }
+
+
+ const height = 623;
+ const width = 360;
+ let middleX = window.screen.availWidth/2 - (width/2);
+ let middleY = window.screen.availHeight/2 - (height/2);
+
+ const getPopup = async () => {
+ try {
+ const url = `${apis.runtime.getURL('pages/prompt.html')}#${notification.routeName()}`;
+
+ // Notifications get bound differently depending on browser
+ // as Firefox does not support opening windows from background.
+ if(typeof chrome !== 'undefined') {
+ window.notification = notification;
+ apis.windows.create({
+ url,
+ height,
+ width,
+ type:'popup'
+ },(_window) => {
+ apis.windows.onRemoved.addListener(function(windowId){
+ if(windowId === _window.id) {
+ notification.responder(Error.promptClosedWithoutAction());
+ return false;
+ }
+ });
+ return _window;
+ });
+ }
+ else {
+ const win = window.open(url, 'BytomPrompt', `width=${width},height=${height},resizable=0,top=${middleY},left=${middleX},titlebar=0`);
+ win.data = notification;
+ openWindow = win;
+ return win;
+ }
+ } catch (e) {
+ console.log('notification error', e);
+ return null;
+ }
+ }
+
+ await InternalMessage.payload(InternalMessageTypes.SET_PROMPT, JSON.stringify(notification)).send();
+
+ let popup = await getPopup();
+
+ if(popup){
+ popup.onbeforeunload = () => {
+ notification.responder(Error.promptClosedWithoutAction());
+
+ // https://developer.mozilla.org/en-US/docs/Web/API/WindowEventHandlers/onbeforeunload
+ // Must return undefined to bypass form protection
+ openWindow = null;
+ return undefined;
+ };
+ }
+ }
+
+ /***
+ * Always use this method for closing notification popups.
+ * Otherwise you will double send responses and one will always be null.
+ */
+ static async close(){
+ if(typeof browser !== 'undefined') {
+ const {id: windowId,} = (await apis.windows.getCurrent());
+ apis.windows.remove(windowId);
+ } else {
+ window.onbeforeunload = () => {};
+ window.close();
+ }
+ }
+
+}
--- /dev/null
+import BytomObj from '../utils/Bytom'
+import {apis} from '../utils/BrowserApis';
+
+export default class StorageService {
+
+ constructor(){}
+
+ /***
+ * Saves an instance of Bytom in the extension's local storage
+ * The keychain will always be encrypted when in storage
+ * @param bytom
+ * @returns {Promise}
+ */
+ static save(_bytom){
+ return new Promise(resolve => {
+ apis.storage.local.set({bytom:_bytom}, () => {
+ resolve(_bytom);
+ });
+ })
+ };
+
+ /***
+ * Gets an instance of Bytom from the extension's local storage
+ * Will return a new Bytom instance if not found.
+ * @returns {Promise}
+ */
+ static get() {
+ return new Promise(resolve => {
+ apis.storage.local.get('bytom', (possible) => {
+ (possible && Object.keys(possible).length && possible.hasOwnProperty('bytom'))
+ ? resolve(BytomObj.fromJson(possible.bytom))
+ : resolve(BytomObj.placeholder());
+ });
+ })
+ }
+
+ /***
+ * Removes the instance of Bytom.
+ * This will delete all user data.
+ * @returns {Promise}
+ */
+ static remove(){
+ return new Promise(resolve => {
+ apis.storage.local.remove('bytom', () => {
+ resolve();
+ });
+ })
+ }
+
+ /***
+ * Caches an ABI
+ * @param contractName
+ * @param chainId
+ * @param abi
+ * @returns {Promise}
+ */
+ static cacheABI(contractName, chainId, abi){
+ return new Promise(resolve => {
+ apis.storage.local.set({[`abi:${contractName}:${chainId}`]:abi}, () => {
+ resolve(abi);
+ });
+ });
+ }
+
+ /***
+ * Fetches an ABI from cache
+ * @param contractName
+ * @param chainId
+ * @returns {Promise}
+ */
+ static getABI(contractName, chainId){
+ return new Promise(resolve => {
+ const prop = `abi:${contractName}:${chainId}`;
+ apis.storage.local.get(prop, (possible) => {
+ if(JSON.stringify(possible) !== '{}') resolve(possible[prop]);
+ else resolve('no cache');
+ });
+ })
+ }
+
+ static getSalt(){
+ return new Promise(resolve => {
+ apis.storage.local.get('salt', (possible) => {
+ if(JSON.stringify(possible) !== '{}') resolve(possible.salt);
+ else resolve('SALT_ME');
+ });
+ })
+ }
+
+ static setSalt(salt){
+ return new Promise(resolve => {
+ apis.storage.local.set({salt}, () => {
+ resolve(salt);
+ });
+ })
+ }
+}
--- /dev/null
+
+const swallow = fn => {try { fn() } catch(e){}};
+
+class ApiGenerator {
+ constructor(){
+ [
+ 'app',
+ 'storage',
+ 'extension',
+ 'runtime',
+ 'windows'
+ ]
+ .map(api => {
+ if(typeof chrome !== 'undefined') swallow(() => {if(chrome[api]) this[api] = chrome[api]});
+ if(typeof browser !== 'undefined') swallow(() => {if(browser[api]) this[api] = browser[api]});
+ });
+
+ if(typeof browser !== 'undefined') swallow(() => {if (browser && browser.runtime) this.runtime = browser.runtime});
+ }
+}
+
+export const apis = new ApiGenerator();
\ No newline at end of file
--- /dev/null
+import Settings from './Settings';
+
+export default class BytomObj {
+
+ constructor(){
+ this.settings = Settings.placeholder();
+ }
+
+ static placeholder(){ return new BytomObj(); }
+ static fromJson(json){
+ let p = Object.assign(this.placeholder(), json);
+ if(json.hasOwnProperty('settings')) p.settings = Settings.fromJson(json.settings);
+ return p;
+ }
+
+ clone(){ return BytomObj.fromJson(JSON.parse(JSON.stringify(this))) }
+}
--- /dev/null
+
+export const strippedHost = () => {
+ let host = location.hostname;
+
+ // Replacing www. only if the domain starts with it.
+ if(host.indexOf('www.') === 0) host = host.replace('www.', '');
+
+ return host;
+};
\ No newline at end of file
--- /dev/null
+export default class Settings {
+
+ constructor(){
+ this.domains = [];
+ this.language = 'ENGLISH';
+ }
+
+ static placeholder(){ return new Settings(); }
+ static fromJson(json){
+ let p = Object.assign(this.placeholder(), json);
+ if(json.hasOwnProperty('domains')) p.domains = json.domains;
+ return p;
+ }
+}
}
static locked(){
- return new Error(ErrorTypes.LOCKED, "The user's Scatter is locked. They have been notified and should unlock before continuing.")
+ return new Error(ErrorTypes.LOCKED, "The user's Bytom is locked. They have been notified and should unlock before continuing.")
}
static promptClosedWithoutAction(){
static usedKeyProvider(){
return new Error(
ErrorTypes.MALICIOUS,
- "Do not use a `keyProvider` with a Scatter. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.",
+ "Do not use a `keyProvider` with a Bytom. Use a `signProvider` and return only signatures to this object. A malicious person could retrieve your keys otherwise.",
ErrorCodes.NO_SIGNATURE
)
}
--- /dev/null
+<style lang="" scoped>
+ .warp {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ height: 600px;
+ z-index: 2;
+ overflow: scroll;
+ }
+ .header {
+ display: flex;
+ }
+ .header p{
+ text-align: center;
+ width: 280px;
+ padding-top: 17px;
+ }
+
+ .content {
+ margin: 20px;
+ padding: 20px;
+ overflow: hidden;
+ border-radius:4px;
+ width: 280px;
+ }
+
+ .btn-inline .btn {
+ margin: 10px 15px;
+ }
+ .row{
+ word-break: break-all;
+ }
+ .col{
+ font-size: 14px;
+ width: 35%;
+ }
+ .label{
+ color: #7B7B7B;
+ }
+ .message{
+ font-size: 14px;
+ color: #7B7B7B;
+ }
+ .value{
+ color: #282828;
+ width: 60%;
+ }
+ table{
+ width: 100%;
+ }
+ .form-item{
+ padding:0;
+ margin:0;
+ margin-bottom: 10px;
+ }
+ .btn-container .btn{
+ margin-top: 20px;
+ height: 48px;
+ width: 320px;
+ }
+</style>
+
+<template>
+ <div class="warp bg-gray">
+ <section class="header bg-header">
+ <i class="iconfont icon-back" @click="denied"></i>
+ <p>{{$t('enable.title')}}</p>
+ </section>
+
+ <section class="content bg-white">
+ <table>
+ <tbody>
+ <tr class="row">
+ <td class="col label">{{$t('enable.domain')}}</td>
+ <td class="col value">{{prompt.domain}}</td>
+ </tr>
+ </tbody>
+ </table>
+ </section>
+
+ <section class="content bg-white">
+ <div class="message">{{$t('enable.message')}}</div>
+ </section>
+
+ <div class="row btn-container" style="bottom: 20px; left: 20px; position: absolute;">
+ <div class="btn" @click="denied">{{$t('enable.cancel')}}</div>
+ <div class="btn bg-green" @click="accepted">{{$t('enable.confirm')}}</div>
+ </div>
+
+ </div>
+</template>
+
+<script>
+import { BTM } from "@/utils/constants";
+import {apis} from '@/utils/BrowserApis';
+import NotificationService from '../../services/NotificationService'
+
+
+export default {
+ data() {
+ return {
+ prompt:''
+ };
+ },
+ computed: {
+ },
+ watch: {
+ },
+ methods: {
+ accepted(){
+ this.prompt.responder(true);
+ NotificationService.close();
+ },
+ denied(){
+ this.prompt.responder(false);
+ NotificationService.close();
+ }
+ }, mounted() {
+ this.prompt = window.data || apis.extension.getBackgroundPage().notification || null;
+ console.log(this.prompt)
+ }
+};
+</script>