Skip to content

Commit

Permalink
Merge branch 'main' into fix-e2e-tests
Browse files Browse the repository at this point in the history
  • Loading branch information
jagodarybacka authored Nov 13, 2023
2 parents 938b65d + 2db9485 commit 68eb3ec
Show file tree
Hide file tree
Showing 15 changed files with 319 additions and 67 deletions.
1 change: 1 addition & 0 deletions .github/ISSUE_TEMPLATE/BUG.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ body:
label: Version
description: What version of the extension are you running?
options:
- v0.51.0
- v0.50.0
- v0.49.0
- v0.48.0
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test-list/release-test-list.md
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ This release checklist should be performed before release is published.

- [ ] check `hide balance under $2` option
- [ ] check bug reports - export logs
- [ ] check connected dapp - confirm you are able to disconnect from a dapp
- [ ] check connected dapp - confirm you are able to disconnect from a dapp and connect again on different network

### ☀️ Abilities

Expand Down
18 changes: 18 additions & 0 deletions background/services/internal-ethereum-provider/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,24 @@ export class InternalEthereumProviderDatabase extends Dexie {
return this.currentNetwork.put({ origin, network })
}

/**
* Clear origin's current network state if it is the same as the passed
* chainId.
*/
async unsetCurrentNetworkForOrigin(
origin: string,
chainID: string,
): Promise<void> {
const originMatches =
(await this.currentNetwork
.where({ origin, "network.chainID": chainID })
.count()) > 0

if (originMatches) {
await this.currentNetwork.delete(origin)
}
}

async getCurrentNetworkForOrigin(
origin: string,
): Promise<EVMNetwork | undefined> {
Expand Down
14 changes: 14 additions & 0 deletions background/services/internal-ethereum-provider/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,6 +539,20 @@ export default class InternalEthereumProviderService extends BaseService<Events>
await this.db.setCurrentChainIdForOrigin(origin, supportedNetwork)
}

/**
* Clears the current network for the given origin if and only if it matches
* the passed chain id.
*
* If the origin has a different chain id set as the current network, the
* current network will be left in place.
*/
async unsetCurrentNetworkForOrigin(
origin: string,
chainId: string,
): Promise<void> {
await this.db.unsetCurrentNetworkForOrigin(origin, chainId)
}

private async signData(
{
input,
Expand Down
145 changes: 101 additions & 44 deletions background/services/provider-bridge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,12 @@ export default class ProviderBridgeService extends BaseService<Events> {
event.request.params,
origin,
)
} else if (event.request.method === "eth_requestAccounts") {
} else if (
event.request.method === "eth_requestAccounts" ||
// We implement a partial wallet_requestPermissions implementation that
// only ever allows access to eth_accounts.
event.request.method === "wallet_requestPermissions"
) {
// if it's external communication AND the dApp does not have permission BUT asks for it
// then let's ask the user what he/she thinks

Expand All @@ -252,46 +257,69 @@ export default class ProviderBridgeService extends BaseService<Events> {
// these params are taken directly from the dapp website
const [title, faviconUrl] = event.request.params as string[]

const permissionRequest: PermissionRequest = {
key: `${origin}_${accountAddress}_${dAppChainID}`,
origin,
chainID: dAppChainID,
faviconUrl: faviconUrl || tab?.favIconUrl || "", // if favicon was not found on the website then try with browser's `tab`
title,
state: "request",
accountAddress,
}
const existingPermission = await this.checkPermission(origin, dAppChainID)
if (
// If there's an existing permission record and it's not an explicit
// allow, immediately return a rejection.
(existingPermission !== undefined &&
existingPermission.state !== "allow") ||
// If there's an unresolved request for the domain, likewise return a
// rejection. We only allow one in-flight permissions request for a
// given domain at a time.
this.#pendingPermissionsRequests[origin] !== undefined
) {
response.result = new EIP1193Error(
EIP1193_ERROR_CODES.userRejectedRequest,
).toJSON()
} else {
const permissionRequest: PermissionRequest = {
key: `${origin}_${accountAddress}_${dAppChainID}`,
origin,
chainID: dAppChainID,
faviconUrl: faviconUrl || tab?.favIconUrl || "", // if favicon was not found on the website then try with browser's `tab`
title,
state: "request",
accountAddress,
}

const blockUntilUserAction =
await this.requestPermission(permissionRequest)

await blockUntilUserAction

const persistedPermission = await this.checkPermission(
origin,
dAppChainID,
)

if (typeof persistedPermission !== "undefined") {
// if agrees then let's return the account data
response.result = await this.routeContentScriptRPCRequest(
persistedPermission,
"eth_accounts",
event.request.params,
const newlyPersistedPermission = await this.checkPermission(
origin,
dAppChainID,
)

// on dApp connection, persist the current network/origin state
await this.internalEthereumProviderService.switchToSupportedNetwork(
origin,
network,
)
} else {
// if user does NOT agree, then reject
if (typeof newlyPersistedPermission !== "undefined") {
// if agrees then let's return the account data

response.result = new EIP1193Error(
EIP1193_ERROR_CODES.userRejectedRequest,
).toJSON()
if (event.request.method === "wallet_requestPermissions") {
response.result = await this.routeContentScriptRPCRequest(
newlyPersistedPermission,
"wallet_getPermissions",
event.request.params,
origin,
)
} else {
response.result = await this.routeContentScriptRPCRequest(
newlyPersistedPermission,
"eth_accounts",
event.request.params,
origin,
)
}

// on dApp connection, persist the current network/origin state
await this.internalEthereumProviderService.switchToSupportedNetwork(
origin,
network,
)
} else {
// if user does NOT agree, then reject

response.result = new EIP1193Error(
EIP1193_ERROR_CODES.userRejectedRequest,
).toJSON()
}
}
} else if (event.request.method === "eth_accounts") {
const dAppChainID = Number(
Expand Down Expand Up @@ -369,11 +397,25 @@ export default class ProviderBridgeService extends BaseService<Events> {
permissionRequest: PermissionRequest,
): Promise<unknown> {
this.emitter.emit("requestPermission", permissionRequest)
await showExtensionPopup(AllowedQueryParamPage.dappPermission)

return new Promise((resolve) => {
const permissionPromise = new Promise((resolve) => {
this.#pendingPermissionsRequests[permissionRequest.origin] = resolve

showExtensionPopup(AllowedQueryParamPage.dappPermission, {}, () => {
resolve("Time to move on")
})
})

const result = await permissionPromise

if (this.#pendingPermissionsRequests[permissionRequest.origin]) {
// Just in case this is a different promise, go ahead and resolve it with
// the same result.
this.#pendingPermissionsRequests[permissionRequest.origin](result)
delete this.#pendingPermissionsRequests[permissionRequest.origin]
}

return result
}

async grantPermission(permission: PermissionRequest): Promise<void> {
Expand All @@ -382,10 +424,7 @@ export default class ProviderBridgeService extends BaseService<Events> {

await this.db.setPermission(permission)

if (this.#pendingPermissionsRequests[permission.origin]) {
this.#pendingPermissionsRequests[permission.origin](permission)
delete this.#pendingPermissionsRequests[permission.origin]
}
this.#pendingPermissionsRequests[permission.origin]?.(permission)
}

async denyOrRevokePermission(permission: PermissionRequest): Promise<void> {
Expand All @@ -403,10 +442,14 @@ export default class ProviderBridgeService extends BaseService<Events> {
permission.chainID,
)

if (this.#pendingPermissionsRequests[permission.origin]) {
this.#pendingPermissionsRequests[permission.origin]("Time to move on")
delete this.#pendingPermissionsRequests[permission.origin]
}
this.#pendingPermissionsRequests[permission.origin]?.("Time to move on")

// If the removed permission is for the origin's current network, clear
// that state so the origin isn't stuck on a disconnected network.
this.internalEthereumProviderService.unsetCurrentNetworkForOrigin(
permission.origin,
permission.chainID,
)

if (deleted > 0) {
this.notifyContentScriptsAboutAddressChange()
Expand Down Expand Up @@ -464,6 +507,20 @@ export default class ProviderBridgeService extends BaseService<Events> {
case "eth_requestAccounts":
case "eth_accounts":
return [enablingPermission.accountAddress]
case "wallet_requestPermissions":
case "wallet_getPermissions":
return [
{
parentCapability: "eth_accounts",
caveats: [
{
type: "restrictReturnedAccounts",
value: [enablingPermission.accountAddress],
},
],
date: Date.now(),
},
]
case "eth_signTypedData":
case "eth_signTypedData_v1":
case "eth_signTypedData_v3":
Expand Down
22 changes: 21 additions & 1 deletion background/services/provider-bridge/show-popup.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
import browser from "webextension-polyfill"
import { AllowedQueryParamPageType } from "@tallyho/provider-bridge-shared"

/**
* Shows an extension popup with the given URL, which should be one of the URLs
* that can be passed to the extension as a starting point. Additional options
* can be passed to the query string where relevant, and an `onClose` callback
* can be provided that will be invoked when the given popup window is closed.
*/
export default async function showExtensionPopup(
url: AllowedQueryParamPageType,
additionalOptions: { [key: string]: string } = {},
onClose?: () => void,
): Promise<browser.Windows.Window> {
const { left = 0, top, width = 1920 } = await browser.windows.getCurrent()
const popupWidth = 384
Expand All @@ -24,5 +31,18 @@ export default async function showExtensionPopup(
focused: true,
}

return browser.windows.create(params)
const window = await browser.windows.create(params)

if (onClose !== undefined) {
const listener = (windowId: number) => {
if (windowId === window.id) {
onClose()

browser.windows.onRemoved.removeListener(listener)
}
}
browser.windows.onRemoved.addListener(listener)
}

return window
}
2 changes: 1 addition & 1 deletion manifest/manifest.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "Taho",
"version": "0.50.0",
"version": "0.51.0",
"description": "The community owned and operated Web3 wallet.",
"homepage_url": "https://taho.xyz",
"author": "https://taho.xyz",
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "@tallyho/tally-extension",
"private": true,
"version": "0.50.0",
"version": "0.51.0",
"description": "Taho, the community owned and operated Web3 wallet.",
"main": "index.js",
"repository": "git@github.com:thesis/tally-extension.git",
Expand Down
30 changes: 28 additions & 2 deletions patches/@ledgerhq+hw-app-eth+6.33.4.patch
Original file line number Diff line number Diff line change
@@ -1,13 +1,39 @@
diff --git a/node_modules/@ledgerhq/hw-app-eth/lib-es/Eth.js b/node_modules/@ledgerhq/hw-app-eth/lib-es/Eth.js
index f3815c8..7581b15 100644
--- a/node_modules/@ledgerhq/hw-app-eth/lib-es/Eth.js
+++ b/node_modules/@ledgerhq/hw-app-eth/lib-es/Eth.js
@@ -206,7 +206,7 @@ export default class Eth {
}
else {
// Legacy type transaction with a big chain ID
- v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
+ v = chainId.times(2).plus(35).plus(chainIdTruncated === 421614 ? ecc_parity % 2 : ecc_parity).toString(16);
}
}
else {
diff --git a/node_modules/@ledgerhq/hw-app-eth/lib/Eth.js b/node_modules/@ledgerhq/hw-app-eth/lib/Eth.js
index 4a7cb64..bd7051b 100644
index 4a7cb64..f11819b 100644
--- a/node_modules/@ledgerhq/hw-app-eth/lib/Eth.js
+++ b/node_modules/@ledgerhq/hw-app-eth/lib/Eth.js
@@ -228,7 +228,7 @@ class Eth {
}
else {
// Legacy type transaction with a big chain ID
- v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
+ v = chainId.times(2).plus(35).plus(chainIdTruncated === 412614 ? ecc_parity % 2 : ecc_parity).toString(16);
+ v = chainId.times(2).plus(35).plus(chainIdTruncated === 421614 ? ecc_parity % 2 : ecc_parity).toString(16);
}
}
else {
diff --git a/node_modules/@ledgerhq/hw-app-eth/src/Eth.ts b/node_modules/@ledgerhq/hw-app-eth/src/Eth.ts
index a501e97..72f63eb 100644
--- a/node_modules/@ledgerhq/hw-app-eth/src/Eth.ts
+++ b/node_modules/@ledgerhq/hw-app-eth/src/Eth.ts
@@ -316,7 +316,7 @@ export default class Eth {
v = ecc_parity % 2 == 1 ? "00" : "01";
} else {
// Legacy type transaction with a big chain ID
- v = chainId.times(2).plus(35).plus(ecc_parity).toString(16);
+ v = chainId.times(2).plus(35).plus(chainIdTruncated === 421614 ? ecc_parity % 2 : ecc_parity).toString(16);
}
} else {
v = response_byte.toString(16);
2 changes: 1 addition & 1 deletion rfb/rfb-2-signers-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ export function SignerLedgerFrame({
Reject
</SharedButton>

{signingLedgerState !== "avaiable" ? (
{signingLedgerState !== "available" ? (
<SharedButton
type="primary"
iconSize="large"
Expand Down
2 changes: 1 addition & 1 deletion rfb/rfb-3-multinetwork-dapp-connections.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ We want to change the functionality of our dApp connections to
- We don't use optimistic update to avoid the complexity of rollbacks
- typical data flow:
- initialization: service emits an initialize event > listener in main dispatches action > reducer sets the content of the slice to the payload in the action
- user action: UI dispatches async thunk > async thunk calls service through main OR emits an event (whichever makes sense in given context) > service updates persistance layer > service fires an event > in main there is a listener that dispatches the redux action > reducer updates redux > UI updates
- user action: UI dispatches async thunk > async thunk calls service through main OR emits an event (whichever makes sense in given context) > service updates persistence layer > service fires an event > in main there is a listener that dispatches the redux action > reducer updates redux > UI updates

3. The extension can be thought of as a multinetwork skeleton AND network specific internal dApps.
- Multinetwork skeleton: settings, overview, assets, activity, ...
Expand Down
Loading

0 comments on commit 68eb3ec

Please sign in to comment.