Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WL-701] tezos wallet sdk #379

Merged
merged 23 commits into from
Dec 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
4c17f46
feat: add ping pong messages between dapps and walless on tezos
tanlethanh Nov 30, 2023
04ccbd6
feat: handle pairing request on tezos
tanlethanh Dec 6, 2023
9fe126b
feat: send ack response to connect request
tanlethanh Dec 6, 2023
0ec3a74
Merge remote-tracking branch 'origin/dev' into tanle/tezos-wallet-sdk
tanlethanh Dec 6, 2023
bd9604b
refactor: make respond method reusable
tanlethanh Dec 6, 2023
ada4cfc
refactor: connection popup for multi-chain
tanlethanh Dec 6, 2023
37df901
chore: expose tezos meta with generic request type
tanlethanh Dec 6, 2023
8c1829c
refactor: make connect request work multi-chain
tanlethanh Dec 6, 2023
7a90278
feat: complete handle tezos permission request
tanlethanh Dec 6, 2023
fe656b6
chore: clean
tanlethanh Dec 6, 2023
eb09c40
chore: fix eslint
tanlethanh Dec 7, 2023
2369b2d
chore: clean permission handle
tanlethanh Dec 7, 2023
9ef0b2d
chore: clean handle structure
tanlethanh Dec 7, 2023
634badb
test: add p2p encryption test
tanlethanh Dec 8, 2023
5c980d2
feat: handle sign payload request on tezos
tanlethanh Dec 8, 2023
8ee9ae5
feat: add simple tezos wallet sdk doc
tanlethanh Dec 8, 2023
07cb93b
feat: impl persist transport keypair and recipient public key
tanlethanh Dec 8, 2023
22c2ff8
feat: use persist keys
tanlethanh Dec 8, 2023
7ff6966
chore: add timeout to kernel request
tanlethanh Dec 8, 2023
02104d4
fix: store keys with origin for different dapps
tanlethanh Dec 8, 2023
d20aada
fix: complete sign payload on tezos by using built-in signer
tanlethanh Dec 8, 2023
3254b88
chore: clean
tanlethanh Dec 8, 2023
7fc48e7
Merge remote-tracking branch 'origin/dev' into tanle/tezos-wallet-sdk
tanlethanh Dec 8, 2023
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
2 changes: 2 additions & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
"postinstall": "yarn batch"
},
"dependencies": {
"@airgap/beacon-sdk": "^4.0.12",
"@airgap/beacon-utils": "^4.0.12",
"@metaplex-foundation/js": "0.18.3",
"@mysten/sui.js": "0.34.0",
"@solana/spl-token": "0.3.7",
Expand Down
4 changes: 3 additions & 1 deletion apps/web/scripts/content/index.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { logger } from '@walless/core';

import './tezos';

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

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

setTimeout(() => {
injectScript('injection.js');
Expand Down
2 changes: 1 addition & 1 deletion apps/web/scripts/content/messaging.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createMessenger } from '@walless/messaging';

const messenger = createMessenger();
export const messenger = createMessenger();

export const initializeMessaging = async () => {
window.postMessage({ from: 'walless-content-script-loaded' });
Expand Down
239 changes: 239 additions & 0 deletions apps/web/scripts/content/tezos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,239 @@
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 { ConnectOptions, UnknownObject } from '@walless/core';
import { Networks } from '@walless/core';
import {
createCryptoBoxClient,
createCryptoBoxServer,
} from '@walless/crypto/utils/p2p';
import { RequestType } from '@walless/messaging';

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

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

const ONE_MINUTE_TO_MS = 60000;

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

window.addEventListener('message', async (e) => {
const isNotTezosRequest = e.data?.target !== ExtensionMessageTarget.EXTENSION;
const wrongTarget = e.data?.targetId && e.data?.targetId !== WALLESS_TEZOS.id;
if (isNotTezosRequest || wrongTarget) {
return;
}

if (e.data?.payload === 'ping') {
return handlePingPong();
} else {
origin = e.origin;
let payload = e.data?.payload;
const encryptedPayload = e.data?.encryptedPayload;
if (payload) {
if (typeof payload === 'string') payload = deserialize(payload);
if (payload.type === TEZOS_PAIRING_REQUEST) {
return handlePairingRequest(payload);
}
} else if (encryptedPayload) {
handleEncryptedRequest(encryptedPayload);
}
}
});

const handlePingPong = () => {
window.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,
);

window.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);

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

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 resPayload = {
id: payload.id,
type: BeaconMessageType.PermissionResponse,
network: { type: NetworkType.MAINNET }, // TODO: handle custom networks
publicKey: res.publicKeys[0]?.meta?.publicKey,
scopes: [PermissionScope.OPERATION_REQUEST, PermissionScope.SIGN],
};

respondWithSharedKeyEncrypt(resPayload);
};

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 resPayload: SignPayloadResponse = {
id: payload.id,
signature: res.signature,
signingType: payload.signingType,
type: BeaconMessageType.SignPayloadResponse,
senderId: payload.senderId,
version: '2',
};

respondWithSharedKeyEncrypt(resPayload);
};

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

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

respondWithSharedKeyEncrypt(resPayload);
};

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,
);

return deserialize(payload);
} 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,
);

window.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,
},
});
};
69 changes: 69 additions & 0 deletions apps/web/scripts/content/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import type { UnknownObject } from '@walless/core';
import {
extractPublicKeyFromSecretKey,
generateKeyPair,
} from '@walless/crypto/utils/p2p';
import { decode, encode } from 'bs58check';

export const injectScript = (scriptUri: string) => {
try {
const container = document.head || document.documentElement;
Expand All @@ -10,3 +17,65 @@ export const injectScript = (scriptUri: string) => {
console.error('script injection failed.', error);
}
};

export const serialize = (data: UnknownObject): string => {
return encode(Buffer.from(JSON.stringify(data)));
};

export const deserialize = (encoded: string): UnknownObject => {
return JSON.parse(decode(encoded).toString());
};

export const getOrCreateKeypair = async (origin: string, create = false) => {
if (!origin) throw Error('require origin');

const key = 'transport_secret_key:' + origin;
if (create) {
const keypair = generateKeyPair();
chrome.storage.local.set({
[key]: Buffer.from(keypair.secretKey).toString('hex'),
});

return keypair;
} else {
const result = await chrome.storage.local.get([key]);
const secretKeyString = result[key] as string;

if (secretKeyString) {
const secretKey = new Uint8Array(Buffer.from(secretKeyString, 'hex'));
const keypair = {
publicKey: extractPublicKeyFromSecretKey(secretKey),
secretKey,
};

return keypair;
} else {
const keypair = generateKeyPair();
chrome.storage.local.set({
transport_secret_key: Buffer.from(keypair.secretKey).toString('hex'),
});

return keypair;
}
}
};

export const storeDAppPublicKey = async (origin: string, publicKey: string) => {
if (!origin) throw Error('require origin');
const key = 'dapp_public_key:' + origin;
await chrome.storage.local.set({ [key]: publicKey });

return publicKey;
};

export const getDAppPublicKey = async (origin: string) => {
if (!origin) throw Error('require origin');

const key = 'dapp_public_key:' + origin;
const result = await chrome.storage.local.get([key]);
const publicKey = result[key];

if (!publicKey) throw Error('Not found dapp public key');

return publicKey;
};
17 changes: 11 additions & 6 deletions apps/web/scripts/kernel/handlers/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,15 @@ export const connect: HandleMethod<{ options?: ConnectOptions }> = async ({
}) => {
if (!payload.options) throw new Error('No connection options provided');

const connectOptions = payload.options;
if (connectOptions.domain) {
const { domain, network = Networks.solana } = payload.options;
if (domain) {
const doc = {
_id: connectOptions.domain,
_id: domain,
type: 'TrustedDomain',
trusted: true,
connectCount: 1,
connect: true,
network,
};
await modules.storage.upsert<TrustedDomainDocument>(
doc._id,
Expand All @@ -34,17 +35,21 @@ export const connect: HandleMethod<{ options?: ConnectOptions }> = async ({
doc.connectCount = prevDoc.connectCount + 1;
}

if (!prevDoc.network) {
doc.network = network;
}

return doc as TrustedDomainDocument;
},
);
}

const publicKeys = await modules.storage.find(selectors.allKeys);
const solKey = (publicKeys.docs as PublicKeyDocument[]).find(
(key) => key.network == Networks.solana,
const publicKey = (publicKeys.docs as PublicKeyDocument[]).find(
(key) => key.network == network,
);

respond(payload.requestId, ResponseCode.SUCCESS, { publicKeys: [solKey] });
respond(payload.requestId, ResponseCode.SUCCESS, { publicKeys: [publicKey] });
};

export const disconnect: HandleMethod<{
Expand Down
Loading
Loading