Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
# editor
.idea
.zed

# automations
.sst
Expand Down Expand Up @@ -72,4 +74,4 @@ cli/tokens/**/cache.json
cli/tokens/**/config.json
cli/tokens/**/sugar.log

playground
playground
3 changes: 1 addition & 2 deletions apps/wallet/browser/bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,8 @@

import { runtime } from '@walless/core';
import type { SettingDocument } from '@walless/store';
import { storage } from 'utils/storage';

import { injectServiceWorker, launchSignInTab } from './utils';
import { injectServiceWorker, launchSignInTab, storage } from './utils';

export const runBridge = async (): Promise<void> => {
if (runtime.isExtension) {
Expand Down
5 changes: 4 additions & 1 deletion apps/wallet/browser/bridge/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { logger } from '@walless/core';
import type { PureMessagePayload } from '@walless/messaging';
import { createEncryptionKeyVault, createMessenger } from '@walless/messaging';
import { storage } from 'utils/storage/db';
import { create } from '@walless/store';
import IDBPouch from 'pouchdb-adapter-idb';

export const storage = create('engine', IDBPouch);

export interface PayloadOptions {
sourceRequestId: string;
Expand Down
8 changes: 8 additions & 0 deletions apps/wallet/browser/bundler/polyfill.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
global.chrome.runtime = Object.assign(global.chrome.runtime || {}, {
id: 'internal-walless-app',
});

export * from 'browser/content/utils';
export const injectScript = () => {
require('browser/content/injection');
};
42 changes: 42 additions & 0 deletions apps/wallet/browser/bundler/utils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
const path = require('path');

/**
* polyfill `chrome.runtime` usage of contentScript in web runtime
*/
module.exports.contentScriptPolyfill = (configs, { modules }) => {
const { webpack } = modules;

configs.plugins.push(
new webpack.NormalModuleReplacementPlugin(/.\/utils/, (resource) => {
const contentScriptScope = 'browser/content';
const isInContentScriptScope =
resource.contextInfo.issuer.includes(contentScriptScope);

if (isInContentScriptScope && resource.request === './utils') {
resource.request = path.resolve(__dirname, './polyfill.js');
}
}),
);

return configs;
};

/**
* experimental, dApp side need to support proxy request
*/
module.exports.widgetsProxy = (configs) => {
configs = {
...configs,
proxy: {
'/widgets-proxy': {
target: 'http://127.0.0.1:3011', // tezos dApp host
pathRewrite: function () {
return '/';
},
changeOrigin: true,
},
},
};

return configs;
};
12 changes: 3 additions & 9 deletions apps/wallet/browser/content/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,4 @@
import { logger } from '@walless/core';
import { initializeContentScript } from './init';

import { initializeMessaging, injectScript } from './utils';

(async () => {
await initializeMessaging();
logger.info('Messaging module initialized..');

injectScript('injection.js');
})();
console.log('init content script');
initializeContentScript();
12 changes: 12 additions & 0 deletions apps/wallet/browser/content/init.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { logger } from '@walless/core';

import { initializeTezosWallet } from './tezos';
import { initializeMessaging, injectScript } from './utils';

export const initializeContentScript = async () => {
initializeTezosWallet();
await initializeMessaging();
logger.info('Messaging module initialized..');

injectScript('injection.js');
};
278 changes: 278 additions & 0 deletions apps/wallet/browser/content/tezos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,278 @@
import type {
OperationRequest,
PermissionRequest,
PostMessagePairingRequest,
PostMessagePairingResponse,
SignPayloadRequest,
SignPayloadResponse,
} from '@airgap/beacon-sdk';
import {
BeaconMessageType,
decryptCryptoboxPayload,
encryptCryptoboxPayload,
ExtensionMessageTarget,
NetworkType,
PermissionScope,
sealCryptobox,
} from '@airgap/beacon-sdk';
import type { UnknownObject } from '@walless/core';
import { logger, Networks, RequestType } from '@walless/core';
import {
createCryptoBoxClient,
createCryptoBoxServer,
} from '@walless/crypto/utils/p2p';
import type { ConnectOptions } from '@walless/sdk';

import {
deserialize,
getDAppPublicKey,
getOrCreateKeypair,
messenger,
serialize,
storeDAppPublicKey,
} from './utils';

const TEZOS_PAIRING_REQUEST = 'postmessage-pairing-request';
const TEZOS_PAIRING_RESPONSE = 'postmessage-pairing-response';

const ONE_MINUTE_TO_MS = 60000;

export const WALLESS_TEZOS = {
id: chrome.runtime.id,
name: 'Walless',
iconUrl: 'https://walless.io/img/walless-icon.svg',
appUrl: 'https://walless.io',
version: '3',
};

let targetWindow = window;
export const resolveTargetWindow = (w: Window) => {
if (w) {
console.log('use external window');
targetWindow = w as never;
}
targetWindow.postMessage('hello from Walless - Tezos wallet 2', '*');
};

export let init = false;
export const initializeTezosWallet = () => {
console.log('init tezos wallet');
if (init) {
logger.error('already initialized tezos wallet');
return;
}

init = true;

window.addEventListener('message', async (e) => {
if (e.data?.type && e.data.type.includes('webpack')) {
return;
}

console.log('tezos wallet - on message:', e.data, e.origin);
if (!e.data) return;
const { target, targetId } = e.data;
const isNotTezosRequest = target !== ExtensionMessageTarget.EXTENSION;
const wrongTarget = targetId && targetId !== WALLESS_TEZOS.id;
if (isNotTezosRequest || wrongTarget) return;

if (e.data.payload === 'ping') return handlePingPong();

origin = e.origin;
if (e.data.payload) {
let payload = e.data.payload;
if (typeof payload === 'string') payload = deserialize(payload);

if (payload.type === TEZOS_PAIRING_REQUEST) {
return handlePairingRequest(payload);
} else if (payload.type === BeaconMessageType.PermissionRequest) {
const response = await handlePermissionRequest(payload as never);
targetWindow.postMessage({
message: {
target: ExtensionMessageTarget.PAGE,
payload: response,
},
sender: { id: WALLESS_TEZOS.id },
});
}
} else if (e.data.encryptedPayload) {
handleEncryptedRequest(e.data.encryptedPayload);
}
});
};

const handlePingPong = () => {
console.log('handle ping pong');
targetWindow.postMessage({
target: ExtensionMessageTarget.PAGE,
payload: 'pong',
sender: WALLESS_TEZOS,
});
};

const handlePairingRequest = async (payload: PostMessagePairingRequest) => {
const keypair = await getOrCreateKeypair(origin, true);
const recipientPublicKey = await storeDAppPublicKey(
origin,
payload.publicKey,
);

const resPayload: PostMessagePairingResponse = {
type: TEZOS_PAIRING_RESPONSE,
publicKey: Buffer.from(keypair.publicKey).toString('hex'),
...WALLESS_TEZOS,
};

const recipientPublicKeyBytes = Uint8Array.from(
Buffer.from(recipientPublicKey, 'hex'),
);

const encryptedPayload = await sealCryptobox(
JSON.stringify(resPayload),
recipientPublicKeyBytes,
);

targetWindow.postMessage({
message: {
target: ExtensionMessageTarget.PAGE,
payload: encryptedPayload,
},
sender: { id: WALLESS_TEZOS.id },
});
};

const handleEncryptedRequest = async (encryptedPayload: string) => {
if (typeof encryptedPayload !== 'string') return;
const payload = await decryptPayload(encryptedPayload);
if (!payload || !payload.type) return;
sendAckMessage(payload.id);

let response;
if (payload.type === BeaconMessageType.PermissionRequest) {
response = await handlePermissionRequest(payload as never);
} else if (payload.type === BeaconMessageType.Disconnect) {
handleDisconnect();
} else if (payload.type === BeaconMessageType.SignPayloadRequest) {
response = await handleSignPayloadRequest(payload as never);
} else if (payload.type === BeaconMessageType.OperationRequest) {
response = await handleOperationRequest(payload as never);
} else {
console.log('not support this type of request');
}

if (response) {
respondWithSharedKeyEncrypt(response);
}
};

const handlePermissionRequest = async (payload: PermissionRequest) => {
// TODO: check network is valid or not, throw error if not
const res = await messenger.request<{ options: ConnectOptions }>(
'kernel',
{
from: 'walless@sdk',
type: RequestType.REQUEST_CONNECT,
options: {
network: Networks.tezos,
domain: origin,
onlyIfTrusted: true,
},
},
ONE_MINUTE_TO_MS,
);

const response = {
id: payload.id,
type: BeaconMessageType.PermissionResponse,
network: { type: NetworkType.MAINNET }, // TODO: handle custom networks
publicKey: res.publicKeys[0].publicKey,
scopes: [PermissionScope.OPERATION_REQUEST, PermissionScope.SIGN],
};

return response;
};

const handleSignPayloadRequest = async (payload: SignPayloadRequest) => {
const res = await messenger.request(
'kernel',
{
from: 'walless@sdk',
type: RequestType.SIGN_PAYLOAD_ON_TEZOS,
payload: payload.payload,
signingType: payload.signingType,
},
ONE_MINUTE_TO_MS,
);

const response: SignPayloadResponse = {
id: payload.id,
signature: res.signature,
signingType: payload.signingType,
type: BeaconMessageType.SignPayloadResponse,
senderId: payload.senderId,
version: '2',
};

return response;
};

const handleOperationRequest = async (payload: OperationRequest) => {
// TODO: need to implement
console.log(payload);
};

const sendAckMessage = async (requestId: string) => {
const response = {
type: BeaconMessageType.Acknowledge,
id: requestId,
};

return response;
};

const decryptPayload = async (encryptedPayload: string) => {
const keypair = await getOrCreateKeypair(origin);
const recipientPublicKey = await getDAppPublicKey(origin);
const sharedKey = await createCryptoBoxServer(recipientPublicKey, keypair);
try {
const payload = await decryptCryptoboxPayload(
Buffer.from(encryptedPayload, 'hex'),
sharedKey.receive,
);

const payloadJson = deserialize(payload);
return payloadJson;
} catch (e) {
// TODO: need to respond error to client side
console.log('error decrypting payload', e);
handleDisconnect();
}
};

const respondWithSharedKeyEncrypt = async (payload: UnknownObject) => {
const keypair = await getOrCreateKeypair(origin);
const recipientPublicKey = await getDAppPublicKey(origin);
const sharedKey = await createCryptoBoxClient(recipientPublicKey, keypair);
const encryptedPayload = await encryptCryptoboxPayload(
serialize(payload),
sharedKey.send,
);

targetWindow.postMessage({
message: {
target: ExtensionMessageTarget.PAGE,
encryptedPayload,
},
sender: { id: WALLESS_TEZOS.id },
});
};

const handleDisconnect = () => {
messenger.request<{ options: ConnectOptions }>('kernel', {
from: 'walless@sdk',
type: RequestType.REQUEST_DISCONNECT,
options: {
domain: origin,
},
});
};
Loading