From 248de30b39002853f60f059012e6fb795b36e596 Mon Sep 17 00:00:00 2001 From: aleacevedo Date: Wed, 13 Apr 2022 17:15:33 -0300 Subject: [PATCH 1/5] feat: implement some handlers and utisl --- package.json | 1 + source/Background/utils.js | 7 ++++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 216a1900..fb477a3f 100644 --- a/package.json +++ b/package.json @@ -34,6 +34,7 @@ "dependencies": { "@babel/plugin-proposal-optional-chaining": "^7.16.0", "@babel/runtime": "^7.12.5", + "@dfinity/principal": "0.9.3", "@fleekhq/browser-rpc": "^2.0.2", "@material-ui/core": "^4.11.3", "@material-ui/icons": "^4.11.2", diff --git a/source/Background/utils.js b/source/Background/utils.js index a9f68b30..602d3a6b 100644 --- a/source/Background/utils.js +++ b/source/Background/utils.js @@ -106,9 +106,7 @@ export const fetchCanistersInfo = async (whitelist) => { // TokenIdentifier is SYMBOL or CanisterID // Return ICP by default export const getToken = (tokenIdentifier, assets) => { - if (!tokenIdentifier) { - return assets.filter((asset) => asset.canisterId === ICP_CANISTER_ID)[0]; - } + if (!tokenIdentifier) return assets.filter((asset) => asset.canisterId === ICP_CANISTER_ID)[0]; if (validateCanisterId(tokenIdentifier)) { return assets.filter((asset) => asset.canisterId === tokenIdentifier)[0]; @@ -116,3 +114,6 @@ export const getToken = (tokenIdentifier, assets) => { return assets.filter((asset) => asset.symbol === tokenIdentifier)[0]; }; +export const bufferToBase64 = (buf) => Buffer.from(buf).toString('base64'); + +export const base64ToBuffer = (base64) => Buffer.from(base64, 'base64'); From e9b962dc28435e8a5b485f9a27b8af135328c969 Mon Sep 17 00:00:00 2001 From: aleacevedo Date: Fri, 29 Apr 2022 11:14:15 -0300 Subject: [PATCH 2/5] feat: implement some utils --- source/Background/utils.js | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/source/Background/utils.js b/source/Background/utils.js index 602d3a6b..3d8b3cca 100644 --- a/source/Background/utils.js +++ b/source/Background/utils.js @@ -6,8 +6,11 @@ import { ASSET_CANISTER_IDS, ICP_CANISTER_ID } from '@shared/constants/canisters import { CYCLES_PER_TC } from '@shared/constants/currencies'; import { XTC_FEE } from '@shared/constants/addresses'; import { setProtectedIds } from '@modules/storageManager'; +import { Principal } from '@dfinity/principal'; +import { blobFromBuffer, blobToUint8Array } from '@dfinity/candid'; import ERRORS from './errors'; +import { recursiveParseBigint } from './Keyring'; const validateAmount = (amount) => !Number.isNaN(amount) && Number.isInteger(amount) && amount >= 0; const validateFloatStrAmount = (amount) => !Number.isNaN(parseFloat(amount)) @@ -117,3 +120,43 @@ export const getToken = (tokenIdentifier, assets) => { export const bufferToBase64 = (buf) => Buffer.from(buf).toString('base64'); export const base64ToBuffer = (base64) => Buffer.from(base64, 'base64'); + +export const handleCallRequest = async ({ + keyring, request, callId, portId, callback, redirected, +}) => { + const arg = blobFromBuffer(base64ToBuffer(request.arguments)); + try { + const signed = await keyring + .getAgent().call( + Principal.fromText(request.canisterId), + { + methodName: request.methodName, + arg, + }, + ); + callback(null, { + ...signed, + requestId: bufferToBase64(blobToUint8Array(signed.requestId)), + }, [ + { callId, portId }, + ]); + if (redirected) callback(null, true); + } catch (e) { + callback(ERRORS.SERVER_ERROR(e), null, [{ portId, callId }]); + if (redirected) callback(null, false); + } +}; + +export const generateRequestInfo = (args) => { + const decodedArguments = Object.values(recursiveParseBigint( + PlugController.IDLDecode(blobFromBuffer(base64ToBuffer(args.arg))), + )); + return { + canisterId: args.canisterId, + methodName: args.methodName, + sender: args.sender, + arguments: args.arg, + decodedArguments, + type: 'call', + }; +}; From d6be5eb1a7534d74ebf34b3efcaaa7c7d06c93d8 Mon Sep 17 00:00:00 2001 From: aleacevedo Date: Fri, 29 Apr 2022 11:15:05 -0300 Subject: [PATCH 3/5] feat: add some handlers --- source/Modules/Controller/transaction.js | 254 ++++++++++++++---- .../components/Sign/hooks/useRequest.jsx | 4 +- 2 files changed, 210 insertions(+), 48 deletions(-) diff --git a/source/Modules/Controller/transaction.js b/source/Modules/Controller/transaction.js index a660a754..c2ef5631 100644 --- a/source/Modules/Controller/transaction.js +++ b/source/Modules/Controller/transaction.js @@ -6,21 +6,20 @@ import { validateTransactions, validateBurnArgs, getToken, + base64ToBuffer, + bufferToBase64, + handleCallRequest, + generateRequestInfo, } from '@background/utils'; import { CONNECTION_STATUS } from '@shared/constants/connectionStatus'; import { ICP_CANISTER_ID } from '@shared/constants/canisters'; import { E8S_PER_ICP, CYCLES_PER_TC } from '@shared/constants/currencies'; import { XTC_FEE } from '@shared/constants/addresses'; -import { - getKeyringHandler, - HANDLER_TYPES, -} from '@background/Keyring'; +import { getKeyringHandler, HANDLER_TYPES } from '@background/Keyring'; +import { blobFromBuffer, blobToUint8Array } from '@dfinity/candid'; import SIZES from '../../Pages/Notification/components/Transfer/constants'; -import { - getApps, - getProtectedIds, -} from '../storageManager'; +import { getApps, getProtectedIds } from '../storageManager'; import { ControllerModuleBase } from './controllerBase'; export class TransactionModule extends ControllerModuleBase { @@ -45,6 +44,10 @@ export class TransactionModule extends ControllerModuleBase { TransactionModule.#handleBatchTransactions(), this.#requestSign(), this.#handleSign(), + this.#requestCall(), + this.#handleCall(), + this.#requestReadState(), + this.#requestQuery(), ]; } @@ -108,11 +111,17 @@ export class TransactionModule extends ControllerModuleBase { if (transfer?.status === 'declined') { callback(ERRORS.TRANSACTION_REJECTED, null, [{ portId, callId }]); } else { - const getBalance = getKeyringHandler(HANDLER_TYPES.GET_BALANCE, this.keyring); - const sendToken = getKeyringHandler(HANDLER_TYPES.SEND_TOKEN, this.keyring); + const getBalance = getKeyringHandler( + HANDLER_TYPES.GET_BALANCE, + this.keyring, + ); + const sendToken = getKeyringHandler( + HANDLER_TYPES.SEND_TOKEN, + this.keyring, + ); const assets = await getBalance(); - const parsedAmount = (transfer.amount / E8S_PER_ICP); + const parsedAmount = transfer.amount / E8S_PER_ICP; if (assets?.[this.DEFAULT_CURRENCY_MAP.ICP]?.amount > parsedAmount) { const response = await sendToken({ ...transfer, @@ -157,7 +166,10 @@ export class TransactionModule extends ControllerModuleBase { return; } - const getBalance = getKeyringHandler(HANDLER_TYPES.GET_BALANCE, this.keyring); + const getBalance = getKeyringHandler( + HANDLER_TYPES.GET_BALANCE, + this.keyring, + ); const assets = await getBalance(); const token = getToken(args.token, assets); @@ -172,7 +184,11 @@ export class TransactionModule extends ControllerModuleBase { callId, portId, metadataJson: JSON.stringify(metadata), - argsJson: JSON.stringify({ ...args, token, timeout: app?.timeout }), + argsJson: JSON.stringify({ + ...args, + token, + timeout: app?.timeout, + }), type: 'transfer', }, }); @@ -209,7 +225,10 @@ export class TransactionModule extends ControllerModuleBase { callback(null, true); callback(ERRORS.TRANSACTION_REJECTED, null, [{ portId, callId }]); } else { - const sendToken = getKeyringHandler(HANDLER_TYPES.SEND_TOKEN, this.keyring); + const sendToken = getKeyringHandler( + HANDLER_TYPES.SEND_TOKEN, + this.keyring, + ); if (transfer.token.amount > amount) { const response = await sendToken({ @@ -288,8 +307,14 @@ export class TransactionModule extends ControllerModuleBase { if (transfer?.status === 'declined') { callback(ERRORS.TRANSACTION_REJECTED, null, [{ portId, callId }]); } else { - const burnXTC = getKeyringHandler(HANDLER_TYPES.BURN_XTC, this.keyring); - const getBalance = getKeyringHandler(HANDLER_TYPES.GET_BALANCE, this.keyring); + const burnXTC = getKeyringHandler( + HANDLER_TYPES.BURN_XTC, + this.keyring, + ); + const getBalance = getKeyringHandler( + HANDLER_TYPES.GET_BALANCE, + this.keyring, + ); const assets = await getBalance(); const xtcAmount = assets?.[this.DEFAULT_CURRENCY_MAP.XTC]?.amount * CYCLES_PER_TC; @@ -399,48 +424,148 @@ export class TransactionModule extends ControllerModuleBase { try { const isDangerousUpdateCall = !preApprove && requestType === 'call'; if (isDangerousUpdateCall) { - getApps(this.keyring?.currentWalletId.toString(), async (apps = {}) => { + getApps( + this.keyring?.currentWalletId.toString(), + async (apps = {}) => { + const app = apps?.[metadata.url] || {}; + if (app.status !== CONNECTION_STATUS.accepted) { + callback(ERRORS.CONNECTION_ERROR, null); + return; + } + if (canisterId && !(canisterId in app.whitelist)) { + callback( + ERRORS.CANISTER_NOT_WHITLESTED_ERROR(canisterId), + null, + ); + return; + } + getProtectedIds(async (protectedIds) => { + const canisterInfo = app.whitelist[canisterId]; + const shouldShowModal = protectedIds.includes( + canisterInfo.id, + ); + + if (shouldShowModal) { + const height = this.keyring?.isUnlocked + ? SIZES.appConnectHeight + : SIZES.loginHeight; + + this.displayPopUp({ + callId, + portId, + type: 'sign', + metadataJson: JSON.stringify(metadata), + argsJson: JSON.stringify({ + requestInfo, + payload, + canisterInfo, + timeout: app?.timeout, + }), + screenArgs: { + fixedHeight: height, + }, + }); + } else { + this.#signData(payload, callback); + } + }); + }, + ); + } else { + this.#signData(payload, callback); + } + } catch (e) { + callback(ERRORS.SERVER_ERROR(e), null); + } + }, + }; + } + + #handleSign() { + return { + methodName: 'handleSign', + handler: async (opts, status, request, callId, portId) => { + const { callback } = opts; + + if (status === CONNECTION_STATUS.accepted) { + try { + const parsedPayload = new Uint8Array(Object.values(request.payload)); + + const signed = await this.keyring?.sign(parsedPayload.buffer); + callback(null, new Uint8Array(signed), [{ callId, portId }]); + callback(null, true); + } catch (e) { + callback(ERRORS.SERVER_ERROR(e), null, [{ portId, callId }]); + callback(null, false); + } + } else { + callback(ERRORS.SIGN_REJECTED, null, [{ portId, callId }]); + callback(null, true); // Return true to close the modal + } + }, + }; + } + + #requestCall() { + return { + methodName: 'requestCall', + handler: async (opts, metadata, args, preAprove) => { + const { message, sender, callback } = opts; + const { id: callId } = message.data.data; + const { id: portId } = sender; + const { currentWalletId } = this.keyring; + const { canisterId, arg, methodName } = args; + const senderPID = (await this.keyring.getState()).wallets[ + currentWalletId + ] + .principal; + try { + getApps( + this.keyring.currentWalletId.toString(), + async (apps = {}) => { const app = apps?.[metadata.url] || {}; if (app.status !== CONNECTION_STATUS.accepted) { callback(ERRORS.CONNECTION_ERROR, null); return; } if (canisterId && !(canisterId in app.whitelist)) { - callback(ERRORS.CANISTER_NOT_WHITLESTED_ERROR(canisterId), null); + callback( + ERRORS.CANISTER_NOT_WHITLESTED_ERROR(canisterId), + null, + ); return; } getProtectedIds(async (protectedIds) => { const canisterInfo = app.whitelist[canisterId]; - const shouldShowModal = protectedIds.includes(canisterInfo.id); + const shouldShowModal = !preAprove && protectedIds.includes(canisterInfo.id); + const requestInfo = generateRequestInfo({ ...args, sender: senderPID }); if (shouldShowModal) { - const height = this.keyring?.isUnlocked - ? SIZES.appConnectHeight - : SIZES.loginHeight; - this.displayPopUp({ callId, portId, type: 'sign', - metadataJson: JSON.stringify(metadata), argsJson: JSON.stringify({ - requestInfo, - payload, canisterInfo, + requestInfo, timeout: app?.timeout, }), - screenArgs: { - fixedHeight: height, - }, + metadataJson: JSON.stringify(metadata), }); } else { - this.#signData(payload, callback); + handleCallRequest({ + keyring: this.keyring, + request: { + arguments: arg, methodName, canisterId, + }, + portId, + callId, + callback, + }); } }); - }); - } else { - this.#signData(payload, callback); - } + }, + ); } catch (e) { callback(ERRORS.SERVER_ERROR(e), null); } @@ -448,23 +573,16 @@ export class TransactionModule extends ControllerModuleBase { }; } - #handleSign() { + #handleCall() { return { - methodName: 'handleSign', - handler: async (opts, status, payload, callId, portId) => { + methodName: 'handleCall', + handler: async (opts, status, request, callId, portId) => { const { callback } = opts; if (status === CONNECTION_STATUS.accepted) { - try { - const parsedPayload = new Uint8Array(Object.values(payload)); - - const signed = await this.keyring?.sign(parsedPayload.buffer); - callback(null, new Uint8Array(signed), [{ callId, portId }]); - callback(null, true); - } catch (e) { - callback(ERRORS.SERVER_ERROR(e), null, [{ portId, callId }]); - callback(null, false); - } + await handleCallRequest({ + keyring: this.keyring, request, portId, callId, callback, redirected: true, + }); } else { callback(ERRORS.SIGN_REJECTED, null, [{ portId, callId }]); callback(null, true); // Return true to close the modal @@ -473,6 +591,48 @@ export class TransactionModule extends ControllerModuleBase { }; } + #requestReadState() { + return { + methodName: 'requestReadState', + handler: async (opts, { canisterId, paths }) => { + const { callback } = opts; + + try { + const response = await this.keyring.getAgent().readState(canisterId, { + paths: [paths.map((path) => blobFromBuffer(base64ToBuffer(path)))], + }); + callback(null, { + certificate: bufferToBase64(blobToUint8Array(response.certificate)), + }); + } catch (e) { + callback(ERRORS.SERVER_ERROR(e), null); + } + }, + }; + } + + #requestQuery() { + return { + methodName: 'requestQuery', + handler: async (opts, { canisterId, methodName, arg }) => { + const { callback } = opts; + + try { + const response = await this.keyring.getAgent() + .query(canisterId, { methodName, arg: blobFromBuffer(base64ToBuffer(arg)) }); + + if (response.reply) { + response.reply.arg = bufferToBase64(blobToUint8Array(response.reply.arg)); + } + + callback(null, response); + } catch (e) { + callback(ERRORS.SERVER_ERROR(e), null); + } + }, + }; + } + // Exposer exposeMethods() { this.#getHandlerObjects().forEach((handlerObject) => { diff --git a/source/Pages/Notification/components/Sign/hooks/useRequest.jsx b/source/Pages/Notification/components/Sign/hooks/useRequest.jsx index a9d4084d..91fa8a53 100644 --- a/source/Pages/Notification/components/Sign/hooks/useRequest.jsx +++ b/source/Pages/Notification/components/Sign/hooks/useRequest.jsx @@ -16,6 +16,7 @@ const portRPC = new PortRPC({ portRPC.start(); const formatRequest = ({ requestInfo, canisterInfo, payload }) => ({ + type: requestInfo.type || 'sign', canisterId: requestInfo.canisterId, methodName: requestInfo.methodName, sender: requestInfo.sender, @@ -46,8 +47,9 @@ const useRequests = (incomingRequest, callId, portId) => { const handleResponse = async (status) => { request.status = status; + const handler = request.type === 'sign' ? 'handleSign' : 'handleCall'; - const success = await portRPC.call('handleSign', [status, request.payload, callId, portId]); + const success = await portRPC.call(handler, [status, request, callId, portId]); if (success) { window.close(); } From 2f6bdb7a21b87616996679fe7f1031b7e50bd26c Mon Sep 17 00:00:00 2001 From: aleacevedo Date: Wed, 4 May 2022 10:22:49 -0300 Subject: [PATCH 4/5] feat: add batchTransaction storage --- source/Modules/storageManager.js | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/source/Modules/storageManager.js b/source/Modules/storageManager.js index 38a09ad0..030cfc87 100644 --- a/source/Modules/storageManager.js +++ b/source/Modules/storageManager.js @@ -37,13 +37,13 @@ export const getApp = (currentWalletId, appUrl, cb) => { )); }; -export const setApps = (currentWalletId, apps, cb = () => {}) => { +export const setApps = (currentWalletId, apps, cb = () => { }) => { const defaultValue = false; secureSetWrapper({ [currentWalletId]: { apps } }, defaultValue, cb); }; -export const removeApp = (currentWalletId, appUrl, cb = () => {}) => { +export const removeApp = (currentWalletId, appUrl, cb = () => { }) => { const defaultValue = false; getApps(currentWalletId, (apps) => { @@ -56,7 +56,7 @@ export const removeApp = (currentWalletId, appUrl, cb = () => {}) => { }); }; -export const setRouter = (route, cb = () => {}) => { +export const setRouter = (route, cb = () => { }) => { const defaultValue = false; secureSetWrapper({ router: route }, defaultValue, cb); @@ -70,13 +70,13 @@ export const getContacts = (cb) => { }); }; -export const setContacts = (contacts, cb = () => {}) => { +export const setContacts = (contacts, cb = () => { }) => { const defaultValue = false; secureSetWrapper({ contacts }, defaultValue, cb); }; -export const setHiddenAccounts = (hiddenAccounts, cb = () => {}) => { +export const setHiddenAccounts = (hiddenAccounts, cb = () => { }) => { const defaultValue = false; secureSetWrapper({ hiddenAccounts }, defaultValue, cb); @@ -98,7 +98,7 @@ export const getAppsKey = (cb) => { }); }; -export const clearStorage = (cb = () => {}) => { +export const clearStorage = (cb = () => { }) => { try { storage.clear(cb(true)); } catch (e) { @@ -106,7 +106,7 @@ export const clearStorage = (cb = () => {}) => { } }; -export const setProtectedIds = (protectedIds = [], cb = () => {}) => { +export const setProtectedIds = (protectedIds = [], cb = () => { }) => { secureSetWrapper({ protectedIds }, [], cb); }; @@ -129,3 +129,15 @@ export const getUseICNS = (walletNumber, cb) => { cb(state?.icns?.[parseInt(walletNumber, 10)] ?? defaultValue); }); }; + +export const setBatchTransactions = (batchTransactions = {}, cb = () => ({})) => { + secureSetWrapper({ batchTransactions }, {}, cb); +}; + +export const getBatchTransactions = (cb) => { + const defaultValue = {}; + + secureGetWrapper('batchTransactions', defaultValue, (state) => { + cb(state?.batchTransactions || defaultValue); + }); +}; From 647bb9de853d427d347db97c8b968798da39a447 Mon Sep 17 00:00:00 2001 From: aleacevedo Date: Wed, 4 May 2022 10:25:19 -0300 Subject: [PATCH 5/5] feat: validate batch transaction into fix: rocky comments fix: change filter to find chore: update controller and provider chore: bump provider version --- package.json | 4 +- source/Background/errors.js | 3 + source/Background/utils.js | 61 +++++- source/Modules/Controller/transaction.js | 174 +++++------------- .../hooks/useRPCTransactions.jsx | 1 + yarn.lock | 44 ++--- 6 files changed, 123 insertions(+), 164 deletions(-) diff --git a/package.json b/package.json index fb477a3f..71d55362 100644 --- a/package.json +++ b/package.json @@ -39,8 +39,8 @@ "@material-ui/core": "^4.11.3", "@material-ui/icons": "^4.11.2", "@psychedelic/dab-js": "1.3.2", - "@psychedelic/plug-controller": "../plug-controller", - "@psychedelic/plug-inpage-provider": "1.9.4", + "@psychedelic/plug-controller": "0.16.11", + "@psychedelic/plug-inpage-provider": "2.0.1", "@reduxjs/toolkit": "^1.6.0", "advanced-css-reset": "^1.2.2", "axios": "^0.21.1", diff --git a/source/Background/errors.js b/source/Background/errors.js index 657e881a..64197c93 100644 --- a/source/Background/errors.js +++ b/source/Background/errors.js @@ -23,5 +23,8 @@ export default { ICNS_ERROR: { code: 400, message: 'There was an error trying to fetch your ICNS information.' }, CLIENT_ERROR: (message) => ({ code: 400, message }), SERVER_ERROR: (message) => ({ code: 500, message }), + NOT_VALID_BATCH_TRANSACTION: { + code: 401, message: 'The transaction that was just attempted failed because it was not a valid batch transaction. Please contact the project’s developers.', + }, ...SILENT_ERRORS, }; diff --git a/source/Background/utils.js b/source/Background/utils.js index 3d8b3cca..9866e66a 100644 --- a/source/Background/utils.js +++ b/source/Background/utils.js @@ -63,7 +63,7 @@ export const validateBurnArgs = ({ to, amount }) => { export const validateTransactions = (transactions) => Array.isArray(transactions) && transactions?.every( - (tx) => tx.idl && tx.canisterId && tx.methodName && tx.args, + (tx) => tx.sender && tx.canisterId && tx.methodName, ); export const initializeProtectedIds = async () => { @@ -109,23 +109,63 @@ export const fetchCanistersInfo = async (whitelist) => { // TokenIdentifier is SYMBOL or CanisterID // Return ICP by default export const getToken = (tokenIdentifier, assets) => { - if (!tokenIdentifier) return assets.filter((asset) => asset.canisterId === ICP_CANISTER_ID)[0]; + if (!tokenIdentifier) return assets.find((asset) => asset.canisterId === ICP_CANISTER_ID); if (validateCanisterId(tokenIdentifier)) { - return assets.filter((asset) => asset.canisterId === tokenIdentifier)[0]; + return assets.find((asset) => asset.canisterId === tokenIdentifier); } - return assets.filter((asset) => asset.symbol === tokenIdentifier)[0]; + return assets.find((asset) => asset.symbol === tokenIdentifier); }; export const bufferToBase64 = (buf) => Buffer.from(buf).toString('base64'); export const base64ToBuffer = (base64) => Buffer.from(base64, 'base64'); +export const isDeepEqualObject = (obj1, obj2) => JSON.stringify(obj1) === JSON.stringify(obj2); + +export const validateBatchTx = (savedTxInfo, canisterId, methodName, arg) => { + if (!savedTxInfo + || savedTxInfo.canisterId !== canisterId + || savedTxInfo.methodName !== methodName) { + // if you dont have savedTxInfo + // or the methodName or cannotisterId is different from the savedTxInfo + // the batch tx is not valid + return false; + } + + if (savedTxInfo.args) { + // if there is args saved in the savedTxInfo + // coming args must be the same as the saved args + // args and savedTxInfo.args gonna be base64 encoded + return savedTxInfo.args === arg; + } + + return true; +}; + export const handleCallRequest = async ({ keyring, request, callId, portId, callback, redirected, }) => { const arg = blobFromBuffer(base64ToBuffer(request.arguments)); try { + if (request.batchTxId && request.batchTxId.lenght !== 0 && !request.savedBatchTrx) { + callback(ERRORS.NOT_VALID_BATCH_TRANSACTION, null, [{ portId, callId }]); + if (redirected) callback(null, false); + return false; + } + if (request.savedBatchTrx) { + const validate = validateBatchTx( + request.savedBatchTrx, + request.canisterId, + request.methodName, + request.arguments, + ); + if (!validate) { + callback(ERRORS.NOT_VALID_BATCH_TRANSACTION, null, [{ portId, callId }]); + if (redirected) callback(null, false); + return false; + } + } const signed = await keyring .getAgent().call( Principal.fromText(request.canisterId), @@ -141,16 +181,18 @@ export const handleCallRequest = async ({ { callId, portId }, ]); if (redirected) callback(null, true); + return true; } catch (e) { callback(ERRORS.SERVER_ERROR(e), null, [{ portId, callId }]); if (redirected) callback(null, false); + return false; } }; export const generateRequestInfo = (args) => { - const decodedArguments = Object.values(recursiveParseBigint( + const decodedArguments = recursiveParseBigint( PlugController.IDLDecode(blobFromBuffer(base64ToBuffer(args.arg))), - )); + ); return { canisterId: args.canisterId, methodName: args.methodName, @@ -160,3 +202,10 @@ export const generateRequestInfo = (args) => { type: 'call', }; }; + +export const lebEncodeArgs = (args) => { + if (!Array.isArray(args) || args.length() === 0) { + return undefined; + } + return PlugController.IDLEncode(args); +}; diff --git a/source/Modules/Controller/transaction.js b/source/Modules/Controller/transaction.js index c2ef5631..cd401c0b 100644 --- a/source/Modules/Controller/transaction.js +++ b/source/Modules/Controller/transaction.js @@ -19,7 +19,9 @@ import { getKeyringHandler, HANDLER_TYPES } from '@background/Keyring'; import { blobFromBuffer, blobToUint8Array } from '@dfinity/candid'; import SIZES from '../../Pages/Notification/components/Transfer/constants'; -import { getApps, getProtectedIds } from '../storageManager'; +import { + getBatchTransactions, getProtectedIds, setBatchTransactions, getApp, +} from '../storageManager'; import { ControllerModuleBase } from './controllerBase'; export class TransactionModule extends ControllerModuleBase { @@ -42,8 +44,6 @@ export class TransactionModule extends ControllerModuleBase { this.#handleRequestBurnXTC(), this.#batchTransactions(), TransactionModule.#handleBatchTransactions(), - this.#requestSign(), - this.#handleSign(), this.#requestCall(), this.#handleCall(), this.#requestReadState(), @@ -67,9 +67,7 @@ export class TransactionModule extends ControllerModuleBase { const { id: callId } = message.data.data; const { id: portId } = sender; - getApps(this.keyring?.currentWalletId.toString(), (apps = {}) => { - const app = apps?.[metadata?.url] || {}; - + getApp(this.keyring?.currentWalletId.toString(), metadata.url, app => { if (app?.status === CONNECTION_STATUS.accepted) { const argsError = validateTransferArgs(args); if (argsError) { @@ -156,9 +154,7 @@ export class TransactionModule extends ControllerModuleBase { const { id: callId } = message.data.data; const { id: portId } = sender; - getApps(this.keyring?.currentWalletId.toString(), async (apps = {}) => { - const app = apps?.[metadata?.url] || {}; - + getApp(this.keyring?.currentWalletId.toString(), metadata.url, async (app = {}) => { if (app?.status === CONNECTION_STATUS.accepted) { const argsError = validateTransferArgs(args); if (argsError) { @@ -264,8 +260,7 @@ export class TransactionModule extends ControllerModuleBase { const { id: callId } = message.data.data; const { id: portId } = sender; - getApps(this.keyring?.currentWalletId.toString(), async (apps = {}) => { - const app = apps?.[metadata.url] || {}; + getApp(this.keyring?.currentWalletId.toString(), metadata.url, async (app = {}) => { if (app?.status === CONNECTION_STATUS.accepted) { const argsError = validateBurnArgs(args); if (argsError) { @@ -354,11 +349,9 @@ export class TransactionModule extends ControllerModuleBase { const { id: callId } = message.data.data; const { id: portId } = sender; - getApps(this.keyring?.currentWalletId.toString(), async (apps = {}) => { - const app = apps?.[metadata?.url] || {}; - + getApp(this.keyring?.currentWalletId.toString(), metadata.url, async (app = {}) => { if (app?.status === CONNECTION_STATUS.accepted) { - const transactionsError = validateTransactions(transactions); + const transactionsError = !validateTransactions(transactions); if (transactionsError) { callback(transactionsError, null); @@ -400,107 +393,25 @@ export class TransactionModule extends ControllerModuleBase { static #handleBatchTransactions() { return { methodName: 'handleBatchTransactions', - handler: async (opts, accepted, callId, portId) => { + handler: async (opts, accepted, transactions, callId, portId) => { const { callback } = opts; if (accepted) { - callback(null, accepted, [{ callId, portId }]); - callback(null, true); // close the modal - } else { - callback(ERRORS.TRANSACTION_REJECTED, false, [{ callId, portId }]); - } - }, - }; - } - - #requestSign() { - return { - methodName: 'requestSign', - handler: async (opts, payload, metadata, requestInfo) => { - const { message, sender, callback } = opts; - const { id: callId } = message.data.data; - const { id: portId } = sender; - const { canisterId, requestType, preApprove } = requestInfo; - - try { - const isDangerousUpdateCall = !preApprove && requestType === 'call'; - if (isDangerousUpdateCall) { - getApps( - this.keyring?.currentWalletId.toString(), - async (apps = {}) => { - const app = apps?.[metadata.url] || {}; - if (app.status !== CONNECTION_STATUS.accepted) { - callback(ERRORS.CONNECTION_ERROR, null); - return; - } - if (canisterId && !(canisterId in app.whitelist)) { - callback( - ERRORS.CANISTER_NOT_WHITLESTED_ERROR(canisterId), - null, - ); - return; - } - getProtectedIds(async (protectedIds) => { - const canisterInfo = app.whitelist[canisterId]; - const shouldShowModal = protectedIds.includes( - canisterInfo.id, - ); - - if (shouldShowModal) { - const height = this.keyring?.isUnlocked - ? SIZES.appConnectHeight - : SIZES.loginHeight; - - this.displayPopUp({ - callId, - portId, - type: 'sign', - metadataJson: JSON.stringify(metadata), - argsJson: JSON.stringify({ - requestInfo, - payload, - canisterInfo, - timeout: app?.timeout, - }), - screenArgs: { - fixedHeight: height, - }, - }); - } else { - this.#signData(payload, callback); - } - }); - }, - ); - } else { - this.#signData(payload, callback); - } - } catch (e) { - callback(ERRORS.SERVER_ERROR(e), null); - } - }, - }; - } - - #handleSign() { - return { - methodName: 'handleSign', - handler: async (opts, status, request, callId, portId) => { - const { callback } = opts; - - if (status === CONNECTION_STATUS.accepted) { - try { - const parsedPayload = new Uint8Array(Object.values(request.payload)); - - const signed = await this.keyring?.sign(parsedPayload.buffer); - callback(null, new Uint8Array(signed), [{ callId, portId }]); - callback(null, true); - } catch (e) { - callback(ERRORS.SERVER_ERROR(e), null, [{ portId, callId }]); - callback(null, false); - } + getBatchTransactions(async (err, batchTransactions) => { + const newBatchTransactionId = crypto.randomUUID(); + const updatedBatchTransactions = { + ...batchTransactions, + [newBatchTransactionId]: transactions + .map((tx) => ({ + canisterId: tx.canisterId, methodName: tx.methodName, args: tx.arguments, + })), + }; + setBatchTransactions(updatedBatchTransactions); + + callback(null, { status: accepted, txId: newBatchTransactionId }, [{ callId, portId }]); + callback(null, true); // close the modal + }); } else { - callback(ERRORS.SIGN_REJECTED, null, [{ portId, callId }]); - callback(null, true); // Return true to close the modal + callback(ERRORS.TRANSACTION_REJECTED, { status: false }, [{ callId, portId }]); } }, }; @@ -509,7 +420,7 @@ export class TransactionModule extends ControllerModuleBase { #requestCall() { return { methodName: 'requestCall', - handler: async (opts, metadata, args, preAprove) => { + handler: async (opts, metadata, args, batchTxId) => { const { message, sender, callback } = opts; const { id: callId } = message.data.data; const { id: portId } = sender; @@ -520,10 +431,10 @@ export class TransactionModule extends ControllerModuleBase { ] .principal; try { - getApps( + getApp( this.keyring.currentWalletId.toString(), - async (apps = {}) => { - const app = apps?.[metadata.url] || {}; + metadata.url, + async (app = {}) => { if (app.status !== CONNECTION_STATUS.accepted) { callback(ERRORS.CONNECTION_ERROR, null); return; @@ -537,7 +448,8 @@ export class TransactionModule extends ControllerModuleBase { } getProtectedIds(async (protectedIds) => { const canisterInfo = app.whitelist[canisterId]; - const shouldShowModal = !preAprove && protectedIds.includes(canisterInfo.id); + const shouldShowModal = (!batchTxId || batchTxId.lenght === 0) + && protectedIds.includes(canisterInfo.id); const requestInfo = generateRequestInfo({ ...args, sender: senderPID }); if (shouldShowModal) { @@ -553,14 +465,24 @@ export class TransactionModule extends ControllerModuleBase { metadataJson: JSON.stringify(metadata), }); } else { - handleCallRequest({ - keyring: this.keyring, - request: { - arguments: arg, methodName, canisterId, - }, - portId, - callId, - callback, + getBatchTransactions((batchTransactions) => { + const savedBatchTrx = batchTxId + ? batchTransactions[batchTxId].shift() + : undefined; + + setBatchTransactions({ + ...batchTransactions, + }, () => { + handleCallRequest({ + keyring: this.keyring, + request: { + arguments: arg, methodName, canisterId, savedBatchTrx, batchTxId, + }, + portId, + callId, + callback, + }); + }); }); } }); diff --git a/source/Pages/Notification/components/Sign/components/BatchTransactions/hooks/useRPCTransactions.jsx b/source/Pages/Notification/components/Sign/components/BatchTransactions/hooks/useRPCTransactions.jsx index 90aa1a3b..36983318 100644 --- a/source/Pages/Notification/components/Sign/components/BatchTransactions/hooks/useRPCTransactions.jsx +++ b/source/Pages/Notification/components/Sign/components/BatchTransactions/hooks/useRPCTransactions.jsx @@ -34,6 +34,7 @@ const useTransactions = (transactions, callId, portId) => { setLoading(true); await portRPC.call('handleBatchTransactions', [ accepted, + transactions, callId, portId, ]); diff --git a/yarn.lock b/yarn.lock index 4ead1d3f..70421b88 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1684,40 +1684,24 @@ cross-fetch "^3.1.4" crypto-js "^4.1.1" -"@psychedelic/plug-controller@../plug-controller": - version "0.16.10" +"@psychedelic/dab-js@1.3.2": + version "1.3.2" + resolved "https://npm.pkg.github.com/download/@psychedelic/dab-js/1.3.2/c444fead13a843c6bed7cd0b9756a5c8a6d2da45abb3b666dd1425adff820776#6a26b7f4628f9b3c4b300e25b26585b214918212" + integrity sha512-RYrw/WgZNmkt/aD1RfkwFefuQwQOVoz8SEC/eyzyJDTk0L9MlmsS+4ernJM7On/8LfZaA9EKF281Fac4lsATtQ== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3" "@dfinity/identity" "0.9.3" "@dfinity/principal" "0.9.3" - "@psychedelic/cap-js" "0.0.7" - "@psychedelic/dab-js" "1.3.2" - "@types/secp256k1" "^4.0.3" - axios "^0.21.1" - babel-jest "^25.5.1" - bigint-conversion "^2.2.1" - bip39 "^3.0.4" + axios "^0.24.0" buffer-crc32 "^0.2.13" - create-hmac "^1.1.7" cross-fetch "^3.1.4" - crypto-js "^4.0.0" - ed25519-hd-key "^1.2.0" - extensionizer "^1.0.1" - hdkey "^2.0.1" - js-sha256 "^0.9.0" - json-bigint "^1.0.0" - random-color "^1.0.1" - reflect-metadata "^0.1.13" - secp256k1 "^4.0.2" - text-encoding "^0.7.0" - text-encoding-shim "^1.0.5" - tweetnacl "^1.0.3" + crypto-js "^4.1.1" -"@psychedelic/plug-controller@0.16.10": - version "0.16.10" - resolved "https://npm.pkg.github.com/download/@psychedelic/plug-controller/0.16.10/002aaccb42bb69a278b11ce6c4498ab4e8fb19315e7e094809f26c25be9235e9#606fdb66f933ed7474888d2e1b84b8c2af650ab9" - integrity sha512-gWH6QVer3BbPgItpL8tLVW9JHdDPR1/cDIqTEqEKlcz1VD6j2dUZHWvHiANfsPn/LN3rLZMbeKjkzto0OUrbcw== +"@psychedelic/plug-controller@0.16.11": + version "0.16.11" + resolved "https://npm.pkg.github.com/download/@psychedelic/plug-controller/0.16.11/35cc5ae87cfb1594c3c3e466465cb4b57df2539aba3e37ecf3bcd751bb3629b3#79eae4d5cb44ca5c5acf49630dbd8183110b32bf" + integrity sha512-IhGyS6LObNQpbIKy36oacHZkvrlx2uKCfiIgCpx6lhPtU3q9TX4rL5OWv+XPCxhSm8bQxTjNdE8GBZoMs/2MLA== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3" @@ -1746,10 +1730,10 @@ text-encoding-shim "^1.0.5" tweetnacl "^1.0.3" -"@psychedelic/plug-inpage-provider@1.9.4": - version "1.9.4" - resolved "https://npm.pkg.github.com/download/@psychedelic/plug-inpage-provider/1.9.4/da1bf8157a4b4ab3762241a9edd506690e19ea6bcd9c7bf88921c19107fc4b47#029533363e325fcf566f7dd0799a584ab57e9b84" - integrity sha512-Q8/TsIpVOfyZPs85QizCEWIBbSHCoTI42cLNsWrXFXe4RI2z1zhghJNVx0/zJCqLy+ZUm+YHaes7q2hFEWVwCw== +"@psychedelic/plug-inpage-provider@2.0.1": + version "2.0.1" + resolved "https://npm.pkg.github.com/download/@psychedelic/plug-inpage-provider/2.0.1/d6efde7527ee0663771ca57b5c4550c70b354a21bc3501e7a996de79841d271b#fa69f079ad6b1bd79b193953516cd0748509b0e9" + integrity sha512-XkO69/6afCi7diyfL1jxCACre6MZviRoDUAS3rEc4IB7/IchdfrV4+OO0t1KK0xyFVBKa6D/B2SgMZEyy7dt9A== dependencies: "@dfinity/agent" "0.9.3" "@dfinity/candid" "0.9.3"