Skip to content

Commit

Permalink
feat: dcent (#976)
Browse files Browse the repository at this point in the history
* feat: dcent connector

* wip: dcent plumbing

* fix: overflow for long account name

* feat: dcent evm messages signing

* fix: missing type on prev commit

* fix: remove unnecessary permission

* wip: dcent onboarding

* feat: dcent account connect wizard

* fix: icon should not spin

* feat: error UI

* fix: ethereum dapp tx signing

* feat: polkadot transactions

* chore: cleanup

* feat: dcent logo

* feat: display dcent account label

* feat: genesisHash for polkadot

* fix: account import styling

* chore: cleanup & self review

* feat: proper dcent api

* feat: map dcent/talisman tokens from chaindata

* fix: row gap

* feat: tooltip if dcent cannot sign text message

* chore: update dcent-web-connector

* feat: info message if no account found

* fix: ledger sign evm tx on legacy networks

* chore: cleanup

* fix: dcent api

* fix: api error handling

* refactor: common interface for hardware devices

* chore: cleanup

* refactor: substrate text message sign warning

* feat: bring popup back in front when bridge opens

* feat: custom error messages

* fix: speed-up payload

* fix: flickering when closing connect modal

* fix: view portfolio after connecting dot account

* fix: imports

* fix: useBalanceByParams variables & types naming

* fix: unsupported signing methods comment
  • Loading branch information
0xKheops authored Aug 29, 2023
1 parent 8400e1b commit bafc967
Show file tree
Hide file tree
Showing 66 changed files with 2,045 additions and 415 deletions.
5 changes: 5 additions & 0 deletions .changeset/dull-dogs-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"talisman-ui": patch
---

fix: hide dcent iframe
1 change: 1 addition & 0 deletions apps/extension/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@
"copy-webpack-plugin": "^10.2.4",
"css-loader": "^6.7.3",
"date-fns": "^2.30.0",
"dcent-web-connector": "^0.14.1",
"dexie": "^3.2.4",
"dexie-react-hooks": "^1.1.6",
"dotenv": "^16.0.3",
Expand Down
3 changes: 0 additions & 3 deletions apps/extension/public/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
"web_accessible_resources": ["page.js", "dashboard.js.map"],
"content_security_policy": "script-src 'self' blob: 'unsafe-eval' 'wasm-eval'; object-src 'self'",
"permissions": ["storage", "tabs", "notifications"],
"cross_origin_embedder_policy": {
"value": "require-corp"
},
"icons": {
"16": "favicon16x16.png",
"24": "favicon24x24.png",
Expand Down
7 changes: 7 additions & 0 deletions apps/extension/src/@talisman/theme/icons/dcent-logo.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
1 change: 1 addition & 0 deletions apps/extension/src/@talisman/theme/icons/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export { ReactComponent as CopyIcon } from "./copy.svg"
export { ReactComponent as CornerDownLeftIcon } from "./corner-down-left.svg"
export { ReactComponent as CornerDownRightIcon } from "./corner-down-right.svg"
export { ReactComponent as CreditCardIcon } from "./credit-card.svg"
export { ReactComponent as DcentLogoIcon } from "./dcent-logo.svg"
export { ReactComponent as DownloadAlertIcon } from "./download-alert.svg"
export { ReactComponent as DownloadIcon } from "./download.svg"
export { ReactComponent as DragIcon } from "./drag.svg"
Expand Down
1 change: 1 addition & 0 deletions apps/extension/src/@types/dcent.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
declare module "dcent-web-connector"
58 changes: 56 additions & 2 deletions apps/extension/src/core/domains/accounts/handler.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { getPrimaryAccount, sortAccounts } from "@core/domains/accounts/helpers"
import { getPrimaryAccount, isValidAnyAddress, sortAccounts } from "@core/domains/accounts/helpers"
import type {
RequestAccountCreate,
RequestAccountCreateDcent,
RequestAccountCreateExternal,
RequestAccountCreateFromJson,
RequestAccountCreateFromSeed,
Expand All @@ -21,10 +22,12 @@ import { getPairForAddressSafely } from "@core/handlers/helpers"
import { genericAsyncSubscription } from "@core/handlers/subscriptions"
import { talismanAnalytics } from "@core/libs/Analytics"
import { ExtensionHandler } from "@core/libs/Handler"
import { chaindataProvider } from "@core/rpcs/chaindata"
import type { MessageTypes, RequestTypes, ResponseType } from "@core/types"
import { Port } from "@core/types/base"
import { getPrivateKey } from "@core/util/getPrivateKey"
import { createPair } from "@polkadot/keyring"
import { createPair, encodeAddress } from "@polkadot/keyring"
import { KeyringPair$Meta } from "@polkadot/keyring/types"
import keyring from "@polkadot/ui-keyring"
import { assert } from "@polkadot/util"
import {
Expand Down Expand Up @@ -224,6 +227,55 @@ export default class AccountsHandler extends ExtensionHandler {
return pair.address
}

private async accountCreateDcent({
name,
address,
type,
path,
tokenIds,
}: RequestAccountCreateDcent) {
if (type === "ethereum") assert(isEthereumAddress(address), "Not an Ethereum address")
else assert(isValidAnyAddress(address), "Not a Substrate address")

const meta: KeyringPair$Meta = {
name,
isHardware: true,
origin: AccountTypes.DCENT,
path,
tokenIds,
}

// hopefully in the future D'CENT will be able to sign on any chain, and code below can be simply removed.
// keep this basic check for now to avoid polluting the messaging interface, as polkadot is the only token supported by D'CENT.
if (tokenIds.length === 1 && tokenIds[0] === "polkadot-substrate-native-dot") {
const chain = await chaindataProvider.getChain("polkadot")
meta.genesisHash = chain?.genesisHash
}

// ui-keyring's addHardware method only supports substrate accounts, cannot set ethereum type
// => create the pair without helper
const pair = createPair(
{
type,
toSS58: type === "ethereum" ? ethereumEncode : encodeAddress,
},
{
publicKey: decodeAnyAddress(address),
secretKey: new Uint8Array(),
},
meta,
null
)

// add to the underlying keyring, allowing not to specify a password
keyring.keyring.addPair(pair)
keyring.saveAccount(pair)

talismanAnalytics.capture("account create", { type, method: "dcent" })

return pair.address
}

private accountsCreateHardware({
accountIndex,
address,
Expand Down Expand Up @@ -461,6 +513,8 @@ export default class AccountsHandler extends ExtensionHandler {
return this.accountCreateSeed(request as RequestAccountCreateFromSeed)
case "pri(accounts.create.json)":
return this.accountCreateJson(request as RequestAccountCreateFromJson)
case "pri(accounts.create.dcent)":
return this.accountCreateDcent(request as RequestAccountCreateDcent)
case "pri(accounts.create.hardware.substrate)":
return this.accountsCreateHardware(request as RequestAccountCreateHardware)
case "pri(accounts.create.hardware.ethereum)":
Expand Down
15 changes: 14 additions & 1 deletion apps/extension/src/core/domains/accounts/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ import { canDerive } from "@polkadot/extension-base/utils"
import type { InjectedAccount } from "@polkadot/extension-inject/types"
import keyring from "@polkadot/ui-keyring"
import type { SingleAddress, SubjectInfo } from "@polkadot/ui-keyring/observable/types"
import { hexToU8a, isHex } from "@polkadot/util"
import { decodeAnyAddress, encodeAnyAddress } from "@talismn/util"
import Browser from "webextension-polyfill"

import seedPhraseStore from "./store"
Expand Down Expand Up @@ -52,7 +54,7 @@ const legacySortAccounts = (accounts: AccountJsonAny[]) => {
// as well as QR (parity signer) and HARDWARE (ledger) accounts
// should order these by created date? probably
const imported = accounts.filter(({ origin }) =>
["SEED", "JSON", "QR", "HARDWARE"].includes(origin as string)
["SEED", "JSON", "QR", "HARDWARE", "DCENT"].includes(origin as string)
)
const importedSorted = sortAccountsByWhenCreated(imported)

Expand Down Expand Up @@ -174,3 +176,14 @@ export const hasPrivateKey = (address: Address) => {
if (["QR", "WATCHED"].includes(acc.meta?.origin as string)) return false
return true
}

export const isValidAnyAddress = (address: string) => {
try {
// validates both SS58 and ethereum addresses
encodeAnyAddress(isHex(address) ? hexToU8a(address) : decodeAnyAddress(address))

return true
} catch (error) {
return false
}
}
19 changes: 19 additions & 0 deletions apps/extension/src/core/domains/accounts/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import type {
ResponseAccountExport,
} from "@polkadot/extension-base/background/types"
import { KeyringPair$Json } from "@polkadot/keyring/types"
import { KeypairType } from "@polkadot/util-crypto/types"
import { TokenId } from "@talismn/chaindata-provider"

export type {
ResponseAccountExport,
Expand Down Expand Up @@ -58,11 +60,19 @@ type AccountJsonWatchedOwnProperties = {

export type AccountJsonWatched = AccountJson & AccountJsonWatchedOwnProperties

type AccountJsonDcentOwnProperties = {
path: string
tokenIds: TokenId[]
}

export type AccountJsonDcent = AccountJson & AccountJsonDcentOwnProperties

export type AccountJsonAny = (
| AccountJsonHardwareEthereum
| AccountJsonHardwareSubstrate
| AccountJsonQr
| AccountJsonWatched
| AccountJsonDcent
| AccountJson
) & { origin?: AccountType | undefined } & {
folderId?: string
Expand All @@ -85,6 +95,7 @@ export const AccountTypes = {
JSON: "JSON",
QR: "QR",
HARDWARE: "HARDWARE",
DCENT: "DCENT",
WATCHED: "WATCHED",
} as const

Expand Down Expand Up @@ -128,6 +139,13 @@ export interface RequestAccountCreateHardwareEthereum {
address: string
path: string
}
export interface RequestAccountCreateDcent {
name: string
address: string
type: KeypairType
path: string
tokenIds: TokenId[]
}

export interface RequestAccountCreateWatched {
name: string
Expand Down Expand Up @@ -174,6 +192,7 @@ export interface AccountsMessages {
string
]
"pri(accounts.create.hardware.ethereum)": [RequestAccountCreateHardwareEthereum, string]
"pri(accounts.create.dcent)": [RequestAccountCreateDcent, string]
"pri(accounts.create.qr.substrate)": [RequestAccountCreateExternal, string]
"pri(accounts.create.watched)": [RequestAccountCreateWatched, string]
"pri(accounts.forget)": [RequestAccountForget, boolean]
Expand Down
15 changes: 11 additions & 4 deletions apps/extension/src/core/domains/balances/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ export class BalancesHandler extends ExtensionHandler {
// create subscription callback
const callback = createSubscription<"pri(balances.byparams.subscribe)">(id, port)

const { addressesByChain, addressesByEvmNetwork } =
const { addressesByChain, addressesAndEvmNetworks, addressesAndTokens } =
request as RequestBalancesByParamsSubscribe

//
Expand All @@ -56,7 +56,7 @@ export class BalancesHandler extends ExtensionHandler {
])

//
// Convert the inputs of `addressesByChain` and `addressesByEvmNetwork` into what we need
// Convert the inputs of `addressesByChain` and `addressesAndEvmNetworks` into what we need
// for each balance module: `addressesByToken`.
//

Expand All @@ -65,9 +65,9 @@ export class BalancesHandler extends ExtensionHandler {
// convert chainIds into chains
.map(([chainId, addresses]) => [chains[chainId], addresses] as const),

...addressesByEvmNetwork.evmNetworks
...addressesAndEvmNetworks.evmNetworks
// convert evmNetworkIds into evmNetworks
.map(({ id }) => [evmNetworks[id], addressesByEvmNetwork.addresses] as const),
.map(({ id }) => [evmNetworks[id], addressesAndEvmNetworks.addresses] as const),
]
// filter out requested chains/evmNetworks which don't exist
.filter(([chainOrNetwork]) => chainOrNetwork !== undefined)
Expand All @@ -86,6 +86,13 @@ export class BalancesHandler extends ExtensionHandler {
return addressesByToken
}, {} as AddressesByToken<Token>)

for (const tokenId of addressesAndTokens.tokenIds) {
if (!addressesByToken[tokenId]) addressesByToken[tokenId] = []
addressesByToken[tokenId].push(
...addressesAndTokens.addresses.filter((a) => !addressesByToken[tokenId].includes(a))
)
}

//
// Separate out the tokens in `addressesByToken` into groups based on `token.type`
// Input: { [token.id]: addresses, [token2.id]: addresses }
Expand Down
9 changes: 7 additions & 2 deletions apps/extension/src/core/domains/balances/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,18 @@ export interface RequestBalance {
address: Address
}

export type AddressesByEvmNetwork = {
export type AddressesAndEvmNetwork = {
addresses: string[]
evmNetworks: Array<Pick<EvmNetwork, "id" | "nativeToken">>
}
export type AddressesAndTokens = {
addresses: string[]
tokenIds: TokenId[]
}
export interface RequestBalancesByParamsSubscribe {
addressesByChain: AddressesByChain
addressesByEvmNetwork: AddressesByEvmNetwork
addressesAndEvmNetworks: AddressesAndEvmNetwork
addressesAndTokens: AddressesAndTokens
}

export type NomPoolStakedBalance = {
Expand Down
25 changes: 12 additions & 13 deletions apps/extension/src/core/domains/signing/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
AccountJson,
AccountJsonHardwareEthereum,
AccountJsonHardwareSubstrate,
} from "@core/domains/accounts/types"
import { AccountJsonAny } from "@core/domains/accounts/types"
import {
EthGasSettingsEip1559,
EthGasSettingsLegacy,
Expand Down Expand Up @@ -53,12 +49,12 @@ const SUBSTRATE_SIGN: SUBSTRATE_SIGN = "substrate-sign"

export interface SubstrateSigningRequest extends BaseSigningRequest<SUBSTRATE_SIGN> {
request: RequestSign
account: AccountJson | AccountJsonHardwareSubstrate
account: AccountJsonAny
}

export interface EthBaseSignRequest<T extends ETH_SIGN | ETH_SEND> extends BaseSigningRequest<T> {
ethChainId: EvmNetworkId
account: AccountJson | AccountJsonHardwareEthereum
account: AccountJsonAny
request: string | EthTransactionRequest
}

Expand All @@ -73,15 +69,18 @@ export const SIGNING_TYPES = {
ETH_SEND,
SUBSTRATE_SIGN,
}

export type EthSignMessageMethod =
| "personal_sign"
| "eth_signTypedData"
| "eth_signTypedData_v1"
| "eth_signTypedData_v3"
| "eth_signTypedData_v4"

export interface EthSignRequest extends EthBaseSignRequest<ETH_SIGN> {
request: string
ethChainId: EvmNetworkId
method:
| "personal_sign"
| "eth_signTypedData"
| "eth_signTypedData_v1"
| "eth_signTypedData_v3"
| "eth_signTypedData_v4"
method: EthSignMessageMethod
}

export interface EthSignAndSendRequest extends EthBaseSignRequest<ETH_SEND> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,9 @@ export default class AssetTransfersRpc {
const chain = await chaindataProvider.getChain({ genesisHash })
if (!chain) throw new Error(`Could not find chain for genesisHash ${genesisHash}`)

// create the unsigned extrinsic
const { registry } = await getTypeRegistry(chain.id)

// create the unsigned extrinsic
const tx = registry.createType(
"Extrinsic",
{ method: unsigned.method },
Expand Down
16 changes: 14 additions & 2 deletions apps/extension/src/ui/api/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,14 @@ export const api: MessageTypes = {
address,
path,
}),
accountCreateDcent: (name, address, type, path, tokenIds) =>
messageService.sendMessage("pri(accounts.create.dcent)", {
name,
address,
type,
path,
tokenIds,
}),
accountCreateQr: (name, address, genesisHash) =>
messageService.sendMessage("pri(accounts.create.qr.substrate)", {
name,
Expand Down Expand Up @@ -135,10 +143,14 @@ export const api: MessageTypes = {
getNomPoolStakedBalance: ({ chainId, addresses }) =>
messageService.sendMessage("pri(balances.nompools.get)", { chainId, addresses }),
balances: (cb) => messageService.subscribe("pri(balances.subscribe)", null, cb),
balancesByParams: (addressesByChain, addressesByEvmNetwork, cb) =>
balancesByParams: (addressesByChain, addressesAndEvmNetworks, addressesAndTokens, cb) =>
messageService.subscribe(
"pri(balances.byparams.subscribe)",
{ addressesByChain, addressesByEvmNetwork },
{
addressesByChain,
addressesAndEvmNetworks,
addressesAndTokens,
},
cb
),

Expand Down
Loading

0 comments on commit bafc967

Please sign in to comment.