Skip to content

Commit

Permalink
feat: add wallet functionality (#144)
Browse files Browse the repository at this point in the history
  • Loading branch information
tomicvladan committed Jun 16, 2023
1 parent 4568cbc commit bfb7ad4
Show file tree
Hide file tree
Showing 23 changed files with 263 additions and 30 deletions.
3 changes: 2 additions & 1 deletion assets/locales/en-US.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,5 +78,6 @@
"DISCLAIMER": "Disclaimer: Account integrity persistence and security are not assured. Expect that funds used for account might be lost, as well as any data the account uses.",
"DAPP_SIGN_MESSAGE_MESSAGE": "Dapp with ID {dappId} wants to sign the following message: {message}. Do you want to sign the message?",
"EXTENSION_SIGN_MESSAGE_MESSAGE": "Extension with ID {dappId} wants to sign the following message: {message}. Do you want to sign the message?",
"ERROR_USER_NOT_LOGGED_IN": "User is not logged in"
"ERROR_USER_NOT_LOGGED_IN": "User is not logged in",
"DIALOG_DAPP_TRANSACTION": "dApp with ID {dappId} wants to send a transaction.\nTo address: {to}\nAmount: {amount}\nDo you allow this transaction?"
}
7 changes: 7 additions & 0 deletions library/src/blossom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { FdpStorage } from './model/fdp-storage.model'
import createFdpStorageProxy from './proxy/fdp-storage.proxy.factory'
import { Signer } from './signer'
import { getDappId } from './utils/dapp.util'
import { Wallet } from './wallet'

/**
* Interface of the Blossom browser extension
Expand All @@ -26,6 +27,11 @@ export class Blossom {
*/
public readonly signer: Signer

/**
* Wallet object. This object contains methods for interaction with blockchain.
*/
public readonly wallet: Wallet

/**
* dApp ENS name. If dApp is loaded from an invalid URL, the value will be null.
*/
Expand All @@ -39,6 +45,7 @@ export class Blossom {
this.messages = createBlossomMessages(extensionId)
this.fdpStorage = createFdpStorageProxy(this.messages)
this.signer = new Signer(this.messages)
this.wallet = new Wallet(this.messages)
}

/**
Expand Down
2 changes: 2 additions & 0 deletions library/src/constants/api-actions.enum.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
export enum ApiActions {
FDP_STORAGE = 'fdp-storage',
SIGNER_SIGN_MESSAGE = 'signer.signMessage',
SEND_TRANSACTION = 'send-transaction',
GET_USER_BALANCE = 'get-user-balance',
ECHO = 'echo',
}
2 changes: 1 addition & 1 deletion library/src/signer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,6 @@ export class Signer {
constructor(private messages: BlossomMessages) {}

public signMessage(podName: string, message: string): Promise<string> {
return this.messages.sendMessage(`${ApiActions.SIGNER_SIGN_MESSAGE}`, { podName, message })
return this.messages.sendMessage(ApiActions.SIGNER_SIGN_MESSAGE, { podName, message })
}
}
23 changes: 23 additions & 0 deletions library/src/wallet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ApiActions } from './constants/api-actions.enum'
import { BlossomMessages } from './messages/blossom-messages'

export class Wallet {
constructor(private messages: BlossomMessages) {}

/**
* Returns account balance of current user in wei
* @returns current balance in wei
*/
public getUserBalance(): Promise<string> {
return this.messages.sendMessage(ApiActions.GET_USER_BALANCE)
}

/**
* Creates a transaction with current user as signer.
* @param address receiver account
* @param amount transaction amount in wei
*/
public sendTransaction(address: string, amount: string): Promise<void> {
return this.messages.sendMessage(ApiActions.SEND_TRANSACTION, { to: address, amount })
}
}
2 changes: 2 additions & 0 deletions src/constants/background-actions.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ enum BackgroundAction {
GET_CURRENT_USER = 'get-current-user',
GET_LOCAL_ACCOUNTS = 'get-local-accounts',
GET_BALANCE = 'get-balance',
GET_USER_BALANCE = 'get-user-balance',
SEND_TRANSACTION = 'send-transaction',
SETTINGS_GET_SELECTED_NETWORK = 'settings-get-selected-network',
SETTINGS_GET_NETWORK_LIST = 'settings-get-network-list',
SETTINGS_ADD_NETWORK = 'settings-add-network',
Expand Down
4 changes: 4 additions & 0 deletions src/constants/dapp-actions.enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,15 @@ import BackgroundAction from './background-actions.enum'
export const DAPP_ACTIONS: string[] = [
BackgroundAction.FDP_STORAGE,
BackgroundAction.SIGNER_SIGN_MESSAGE,
BackgroundAction.SEND_TRANSACTION,
BackgroundAction.GET_USER_BALANCE,
BackgroundAction.ECHO,
]

export const E2E_ACTIONS: string[] = [
BackgroundAction.FDP_STORAGE,
BackgroundAction.SIGNER_SIGN_MESSAGE,
BackgroundAction.SEND_TRANSACTION,
BackgroundAction.GET_USER_BALANCE,
BackgroundAction.ECHO,
]
4 changes: 4 additions & 0 deletions src/constants/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import { ErrorObject } from '../model/error.model'

export const errorMessages = {
ACCESS_DENIED: 'Blossom: Access denied',
}

export enum ErrorCode {
USER_NOT_LOGGED_IN,
}
Expand Down
55 changes: 51 additions & 4 deletions src/listeners/message-listeners/account.listener.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,52 @@
import { BigNumber } from 'ethers'
import { BigNumber, providers } from 'ethers'
import BackgroundAction from '../../constants/background-actions.enum'
import { isAddress } from '../../messaging/message.asserts'
import { isAddress, isTransaction } from '../../messaging/message.asserts'
import { Address } from '../../model/general.types'
import { createMessageHandler } from './message-handler'
import { Blockchain } from '../../services/blockchain.service'
import { Transaction } from '../../model/internal-messages.model'
import { SessionFdpStorageProvider } from '../../services/fdp-storage/session-fdp-storage.provider'
import { isInternalMessage } from '../../utils/extension'
import { Dialog } from '../../services/dialog.service'
import { getDappId } from './listener.utils'
import { errorMessages } from '../../constants/errors'

const dialogs = new Dialog()
const blockchain = new Blockchain()
const fdpStorageProvider = new SessionFdpStorageProvider()

export function getAccountBalance(address: Address): Promise<BigNumber> {
return blockchain.getAccountBalance(address)
export async function getAccountBalance(address: Address): Promise<string> {
const balance = await blockchain.getAccountBalance(address)

return balance.toString()
}

export async function getUserAccountBalance(): Promise<string> {
const fdp = await fdpStorageProvider.getService()

const balance = await blockchain.getAccountBalance(fdp.account.wallet.address)

return balance.toString()
}

export async function sendTransaction(
{ to, amount }: Transaction,
sender: chrome.runtime.MessageSender,
): Promise<providers.TransactionReceipt> {
const fdp = await fdpStorageProvider.getService()

const { wallet } = fdp.account

if (!isInternalMessage(sender)) {
const dappId = await getDappId(sender)
const confirmed = await dialogs.ask('DIALOG_DAPP_TRANSACTION', { dappId, to, amount })

if (!confirmed) {
throw new Error(errorMessages.ACCESS_DENIED)
}
}

return blockchain.sendTransaction(wallet.privateKey, to, BigNumber.from(amount))
}

const messageHandler = createMessageHandler([
Expand All @@ -17,6 +55,15 @@ const messageHandler = createMessageHandler([
assert: isAddress,
handler: getAccountBalance,
},
{
action: BackgroundAction.GET_USER_BALANCE,
handler: getUserAccountBalance,
},
{
action: BackgroundAction.SEND_TRANSACTION,
assert: isTransaction,
handler: sendTransaction,
},
])

export default messageHandler
15 changes: 3 additions & 12 deletions src/listeners/message-listeners/fdp-storage.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,28 +11,19 @@ import {
getPodNameFromParams,
isPodBasedMethod,
} from '../../services/fdp-storage/fdp-storage-access'
import { dappUrlToId } from '../../services/fdp-storage/fdp-storage.utils'
import { SessionFdpStorageProvider } from '../../services/fdp-storage/session-fdp-storage.provider'
import { SessionService } from '../../services/session.service'
import { Storage } from '../../services/storage/storage.service'
import { SwarmExtension } from '../../swarm-api/swarm-extension'
import { isPodActionAllowed } from '../../utils/permissions'
import { createMessageHandler } from './message-handler'
import { getDappId } from './listener.utils'
import { errorMessages } from '../../constants/errors'

const fdpStorageProvider = new SessionFdpStorageProvider()
const dialogs = new Dialog()
const storage = new Storage()
const sessionService = new SessionService()

async function getDappId(sender: chrome.runtime.MessageSender): Promise<DappId> {
const { extensionId } = await storage.getSwarm()
const swarmExtension = new SwarmExtension(extensionId)

const { beeApiUrl } = await swarmExtension.beeAddress()

return dappUrlToId(sender.url, beeApiUrl)
}

async function handleFullAccessRequest(dappId: DappId, dapp: Dapp, session: MemorySession): Promise<boolean> {
if (dapp && dapp.fullStorageAccess) {
return true
Expand Down Expand Up @@ -64,7 +55,7 @@ async function handlePodBasedMethod(
const confirmed = await dialogs.ask('DIALOG_CREATE_POD', { dappId, podName })

if (!confirmed) {
throw new Error('Blossom: Access denied')
throw new Error(errorMessages.ACCESS_DENIED)
}

await storage.setDappPodPermissionBySession(
Expand Down
15 changes: 15 additions & 0 deletions src/listeners/message-listeners/listener.utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { DappId } from '../../model/general.types'
import { dappUrlToId } from '../../services/fdp-storage/fdp-storage.utils'
import { Storage } from '../../services/storage/storage.service'
import { SwarmExtension } from '../../swarm-api/swarm-extension'

const storage = new Storage()

export async function getDappId(sender: chrome.runtime.MessageSender): Promise<DappId> {
const { extensionId } = await storage.getSwarm()
const swarmExtension = new SwarmExtension(extensionId)

const { beeApiUrl } = await swarmExtension.beeAddress()

return dappUrlToId(sender.url, beeApiUrl)
}
5 changes: 3 additions & 2 deletions src/listeners/message-listeners/signer.listener.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Dialog } from '../../services/dialog.service'
import { SessionFdpStorageProvider } from '../../services/fdp-storage/session-fdp-storage.provider'
import { isOtherExtension } from '../../utils/extension'
import { createMessageHandler } from './message-handler'
import { errorMessages } from '../../constants/errors'

const dialogs = new Dialog()
const dappService = new DappService()
Expand All @@ -22,7 +23,7 @@ async function signMessage(

// TODO should check permissions
if (podName !== dappId) {
throw new Error('Blossom: Access denied')
throw new Error(errorMessages.ACCESS_DENIED)
}

const podWallet = await fdp.personalStorage.getPodWallet(fdp.account.seed, podName)
Expand All @@ -37,7 +38,7 @@ async function signMessage(
)

if (!confirmed) {
throw new Error('Blossom: Access denied')
throw new Error(errorMessages.ACCESS_DENIED)
}

return new Wallet(podWallet.privateKey).signMessage(message)
Expand Down
4 changes: 2 additions & 2 deletions src/messaging/content-api.messaging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ export function getLocales(): Promise<LocaleData> {
}

export async function getAccountBalance(address: Address): Promise<BigNumber> {
const { hex } = await sendMessage<Address, { hex: string }>(BackgroundAction.GET_BALANCE, address)
const balance = await sendMessage<Address, string>(BackgroundAction.GET_BALANCE, address)

return BigNumber.from(hex)
return BigNumber.from(balance)
}

export function getSelectedNetwork(): Promise<Network> {
Expand Down
7 changes: 7 additions & 0 deletions src/messaging/message.asserts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
RegisterDataBase,
RegisterDataMnemonic,
SignerRequest,
Transaction,
UsernameCheckData,
} from '../model/internal-messages.model'
import { Dapp, PodActions, PodPermission } from '../model/storage/dapps.model'
Expand Down Expand Up @@ -145,3 +146,9 @@ export function isSerializedUint8Array(data: unknown): data is BytesMessage {

return type === 'bytes' && isString(value)
}

export function isTransaction(data: unknown): data is Transaction {
const { to, amount } = (data || {}) as Transaction

return isAddress(to) && isString(amount)
}
6 changes: 6 additions & 0 deletions src/model/internal-messages.model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,9 @@ export interface DialogQuestion {
question: string
placeholders: Record<string, string>
}

export interface Transaction {
to: Address
// in wei
amount: string
}
18 changes: 16 additions & 2 deletions src/services/blockchain.service.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { BigNumber, providers } from 'ethers'
import { Address } from '../model/general.types'
import { BigNumber, Wallet, providers } from 'ethers'
import { Address, PrivateKey } from '../model/general.types'
import { Storage } from './storage/storage.service'

export class Blockchain {
Expand All @@ -9,6 +9,20 @@ export class Blockchain {
return (await this.getProvider()).getBalance(address)
}

public async sendTransaction(
privateKey: PrivateKey,
to: Address,
value: BigNumber,
): Promise<providers.TransactionReceipt> {
const provider = await this.getProvider()

const wallet = new Wallet(privateKey).connect(provider)

const tx = await wallet.sendTransaction({ to, value })

return tx.wait()
}

private async getProvider(): Promise<providers.JsonRpcProvider> {
const { rpc } = await this.storage.getNetwork()

Expand Down
1 change: 0 additions & 1 deletion src/services/error.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { ErrorObject } from '../model/error.model'
import { removeWarningBadge, setWarningBadge } from '../utils/extension'
import { Storage } from './storage/storage.service'
import { Errors as ErrorsModel } from '../model/storage/general.model'
import { Locales } from './locales.service'

export class Errors {
private storage: Storage = new Storage()
Expand Down
12 changes: 10 additions & 2 deletions src/ui/dialog/components/dialog.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'
import React, { Fragment, useEffect, useState } from 'react'
import intl from 'react-intl-universal'
import { AppBar, Button, GlobalStyles, Typography } from '@mui/material'
import { FlexDiv } from '../../common/components/utils/utils'
Expand Down Expand Up @@ -54,7 +54,15 @@ const Dialog = () => {
</AppBar>
<FlexDiv sx={{ marginTop: '50px', padding: '20px' }}>
<Typography variant="body1" sx={{ margin: 'auto', marginTop: '20px' }}>
{intl.get(question, placeholders)}
{intl
.get(question, placeholders)
.split('\n')
.map((line, index) => (
<Fragment key={index}>
{line}
<br />
</Fragment>
))}
</Typography>
</FlexDiv>
<FlexDiv
Expand Down
4 changes: 2 additions & 2 deletions test/config/test-prep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { BEE_DEBUG_URL, BEE_URL } from './constants'

const libraryPath = path.join('library', 'build', 'index.js')
const dappsPath = path.join('test', 'dapps')
const dapps = ['fdp-storage']
const dappReferenceVariables = ['FDP_STORAGE_PAGE_REFERENCE']
const dapps = ['fdp-storage', 'wallet']
const dappReferenceVariables = ['FDP_STORAGE_PAGE_REFERENCE', 'WALLET_PAGE_REFERENCE']

function execPromise(command: string): Promise<void> {
return new Promise((resolve, reject) => {
Expand Down
Loading

0 comments on commit bfb7ad4

Please sign in to comment.