Skip to content

Commit

Permalink
πŸ’‰ Inject metadata into signer (#3872)
Browse files Browse the repository at this point in the history
* New package

* Update package

* Introduce chainInfo into worker

* Introduce update metadata button into the settings

* Remove update button from settings

* Check metadata before transaction

* Test fix

* Test fix

* Keep the ApiRx compatibility

* Move type definitions around

* Fix some functions `Api` types

* Fix the TransactionButton test suite

* Minor fixes

* Mock `injectweb3-connect`

* Update `injectweb3-connect`

* Test new `injectweb3-connect` build

* Fix "Proxy object could not be cloned."

* Fix the `chain-metadata` serialization

* Remove `injectweb3-connect` archive

* Update `injectweb3-connect` to `2.1.1`

* Handle webp files in node_modules

---------

Co-authored-by: Theophile Sandoz <theophile.sandoz@gmail.com>
  • Loading branch information
WRadoslaw and thesan authored May 30, 2023
1 parent f775081 commit 7c4ffc2
Show file tree
Hide file tree
Showing 18 changed files with 171 additions and 75 deletions.
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@
"graphql-tag": "^2.12.5",
"i18next": "^21.6.3",
"i18next-browser-languagedetector": "^6.1.2",
"injectweb3-connect": "^1.1.1",
"injectweb3-connect": "2.1.1",
"jsonpath": "^1.1.1",
"lodash": "^4.17.21",
"mime": "^2.4.4",
Expand Down
5 changes: 5 additions & 0 deletions packages/ui/src/api/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { getPolkadotApiChainInfo } from 'injectweb3-connect'

import { Awaited } from '@/common/types/helpers'

export type MetadataDef = Awaited<ReturnType<typeof getPolkadotApiChainInfo>>
14 changes: 14 additions & 0 deletions packages/ui/src/api/utils/getChainMetadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { ApiRx } from '@polkadot/api'
import { getPolkadotApiChainInfo } from 'injectweb3-connect'

import { ProxyApi } from '@/proxyApi'

import { MetadataDef } from '../types'

export const getChainMetadata = async (api: ProxyApi | ApiRx): Promise<MetadataDef> => {
if ('_async' in api) {
return await api._async.chainMetadata
} else {
return await getPolkadotApiChainInfo(api)
}
}
1 change: 1 addition & 0 deletions packages/ui/src/app/pages/Settings/Settings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ export const Settings = () => {
window.location.reload()
}
}

return (
<Container>
<PageLayout
Expand Down
15 changes: 14 additions & 1 deletion packages/ui/src/common/hooks/useSignAndSendTransaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ import { useCallback, useEffect, useState } from 'react'
import { ActorRef } from 'xstate'

import { useBalance } from '@/accounts/hooks/useBalance'
import { useMyAccounts } from '@/accounts/hooks/useMyAccounts'
import { useApi } from '@/api/hooks/useApi'
import { getChainMetadata } from '@/api/utils/getChainMetadata'
import { BN_ZERO } from '@/common/constants'
import { getFeeSpendableBalance } from '@/common/providers/transactionFees/provider'

Expand Down Expand Up @@ -44,8 +47,18 @@ export const useSignAndSendTransaction = ({
setBlockHash,
})
const queryNodeStatus = useQueryNodeTransactionStatus(isProcessing, blockHash, skipQueryNode)
const { wallet } = useMyAccounts()
const { api } = useApi()

const sign = useCallback(() => send('SIGN'), [service])
const sign = useCallback(() => {
if (wallet && api) {
return getChainMetadata(api).then(async (metadata) => {
await wallet.updateMetadata(metadata)
send('SIGN')
})
}
send('SIGN')
}, [service, wallet])

useEffect(() => {
if (skipQueryNode && isProcessing) {
Expand Down
2 changes: 2 additions & 0 deletions packages/ui/src/common/types/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@ export type Defined<T> = T extends undefined ? never : T
export type EnumTypeString<TEnum extends string> = { [key in string]: TEnum | string }

export type KeysOfUnion<T> = T extends T ? keyof T : never

export type Awaited<T> = T extends PromiseLike<infer U> ? U : T
4 changes: 2 additions & 2 deletions packages/ui/src/forum/modals/PostReplyModal/helpers.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { ForumPostMetadata } from '@joystream/metadata-protobuf'

import { Api } from '@/api'
import { createType } from '@/common/model/createType'
import { metadataToBytes } from '@/common/model/JoystreamNode'
import { ForumPost } from '@/forum/types'
import { ProxyApi } from '@/proxyApi'

export const transactionFactory = (
api: ProxyApi,
api: Api,
module: 'forum' | 'proposalsDiscussion',
text: string,
isEditable: boolean,
Expand Down
4 changes: 2 additions & 2 deletions packages/ui/src/proposals/modals/AddNewProposal/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import BN from 'bn.js'
import * as Yup from 'yup'

import { Account } from '@/accounts/types'
import { Api } from '@/api'
import { CurrencyName } from '@/app/constants/currency'
import { QuestionValueProps } from '@/common/components/EditableInputList/EditableInputList'
import {
Expand All @@ -17,7 +18,6 @@ import {
import { AccountSchema, StakingAccountSchema } from '@/memberships/model/validation'
import { Member } from '@/memberships/types'
import { ProposalType } from '@/proposals/types'
import { ProxyApi } from '@/proxyApi'
import { GroupIdName } from '@/working-groups/types'

export const defaultProposalValues = {
Expand Down Expand Up @@ -168,7 +168,7 @@ export interface AddNewProposalForm {
}
}

export const schemaFactory = (api?: ProxyApi) => {
export const schemaFactory = (api?: Api) => {
return Yup.object().shape({
groupId: Yup.string(),
proposalType: Yup.object().shape({
Expand Down
8 changes: 7 additions & 1 deletion packages/ui/src/proxyApi/client/ProxyApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { firstWhere } from '@/common/utils/rx'
import { deserializeMessage, serializePayload, WorkerProxyMessage } from '../models/payload'
import { ClientMessage, PostMessage, RawWorkerMessageEvent, WorkerConnectMessage, WorkerInitMessage } from '../types'

import { AsyncProps, _async } from './_async'
import { query } from './query'
import { tx } from './tx'

Expand All @@ -18,6 +19,7 @@ export class ProxyApi extends Events {
rpc: ApiRx['rpc']
tx: ApiRx['tx']
consts: ApiRx['consts']
_async: AsyncProps

static create(providerEndpoint: string) {
const worker = new Worker(new URL('../worker', import.meta.url), { type: 'module' })
Expand All @@ -30,7 +32,10 @@ export class ProxyApi extends Events {
share()
)
const postMessage: PostMessage<ClientMessage> = (message) =>
worker.postMessage({ ...message, payload: serializePayload(message.payload, workerProxyMessages, postMessage) })
worker.postMessage({
...message,
payload: serializePayload(message.payload, { messages: workerProxyMessages, postMessage }),
})

postMessage({ messageType: 'init', payload: providerEndpoint })

Expand All @@ -54,6 +59,7 @@ export class ProxyApi extends Events {
this.query = query('query', messages, postMessage)
this.rpc = query('rpc', messages, postMessage)
this.tx = tx(messages, postMessage)
this._async = _async(messages, postMessage)
}

messages
Expand Down
45 changes: 45 additions & 0 deletions packages/ui/src/proxyApi/client/_async.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { filter, firstValueFrom, map, Observable } from 'rxjs'

import { MetadataDef } from '@/api/types'

import { deserializeMessage } from '../models/payload'
import { PostMessage, RawWorkerMessageEvent } from '../types'

export type ClientAsyncMessage = {
messageType: 'chain-metadata'
payload: undefined
}

export type WorkerAsyncMessage = {
messageType: 'chain-metadata'
payload: MetadataDef
}

export interface AsyncProps {
chainMetadata: Promise<MetadataDef>
}

export const _async = (
messages: Observable<RawWorkerMessageEvent>,
postMessage: PostMessage<ClientAsyncMessage>
): AsyncProps => {
let chainMetadata: Promise<MetadataDef>

return {
get chainMetadata(): Promise<MetadataDef> {
if (!chainMetadata) chainMetadata = getAsync('chain-metadata')
return chainMetadata
},
}

function getAsync(messageType: ClientAsyncMessage['messageType']): Promise<WorkerAsyncMessage['payload']> {
postMessage({ messageType, payload: undefined })
return firstValueFrom(
messages.pipe(
filter(({ data }) => data.messageType === 'chain-metadata'),
deserializeMessage<WorkerAsyncMessage>(),
map(({ payload }) => payload)
)
)
}
}
31 changes: 21 additions & 10 deletions packages/ui/src/proxyApi/models/payload.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,11 +27,13 @@ export interface ClientProxyMessage {
payload: ProxyPromisePayload
}

export const serializePayload = (
payload: any,
messages?: Observable<WorkerProxyMessage>,
interface serializationOptions {
messages?: Observable<WorkerProxyMessage>
postMessage?: PostMessage<ClientProxyMessage>
): any => {
toJSON?: boolean
}

export const serializePayload = (payload: any, { messages, postMessage, toJSON }: serializationOptions = {}): any => {
const stack: AnyObject[] = []
const result = serializeValue(payload)

Expand All @@ -56,7 +58,7 @@ export const serializePayload = (
} else if (typeof value !== 'object' || value === null) {
return value
} else if (isCodec(value)) {
return serializeCodec(value)
return toJSON ? value.toJSON() : serializeCodec(value)
} else if (value instanceof BN) {
return { kind: 'BN', value: value.toArray() }
} else if (value.kind === 'SubmittableExtrinsicProxy') {
Expand All @@ -83,12 +85,16 @@ const serializeObject = (value: Record<any, any>): Record<string, any> => {
return { ...value }
}

interface deSerializationOptions {
messages?: Observable<ClientProxyMessage>
postMessage?: PostMessage<WorkerProxyMessage>
transactionsRecord?: TransactionsRecord
}

// WARNING this mutate the serialized payload
export const deserializePayload = (
payload: any,
messages?: Observable<ClientProxyMessage>,
postMessage?: PostMessage<WorkerProxyMessage>,
transactionsRecord?: TransactionsRecord
{ messages, postMessage, transactionsRecord }: deSerializationOptions = {}
): any => {
const stack: AnyObject[] = []
const result = deserializeValue(payload)
Expand Down Expand Up @@ -225,8 +231,13 @@ const serializeCodec = (codec: Codec): SerializedCodec => {
}

const properties = (Object.getOwnPropertyNames(Object.getPrototypeOf(codec)) as (keyof Codec)[])
.map((key) => [key, codec[key]])
.filter(([, prop]) => !isFunction(prop))
.map<[keyof Codec, Codec[keyof Codec]]>((key) => [key, codec[key]])
.filter(
([key, prop]) =>
!['encodedLength', 'hash', 'initialU8aLength', 'isEmpty', 'registry', 'createdAtHash'].includes(key) &&
!isFunction(prop) &&
!Object.getOwnPropertyDescriptor(codec, key)
)

return properties.length > 0
? { ...serializedCodec, properties: serializePayload(Object.fromEntries(properties)) }
Expand Down
13 changes: 10 additions & 3 deletions packages/ui/src/proxyApi/types.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { SubmittableExtrinsic } from '@polkadot/api/types'

import { ProxyApi } from '.'
import { ClientAsyncMessage, WorkerAsyncMessage } from './client/_async'
import { ClientQueryMessage, WorkerQueryMessage } from './client/query'
import { ClientTxMessage, WorkerTxMessage } from './client/tx'
import { ClientProxyMessage, WorkerProxyMessage } from './models/payload'
Expand All @@ -12,7 +13,7 @@ export interface ProxyPromisePayload<T = any> {
result?: T
}

export type PostMessage<Message extends AnyMessage = AnyMessage> = (message: Message) => void
export type PostMessage<Message extends AnyMessage = AnyMessage> = (message: Message, asJSON?: boolean) => void

export type ApiKinds = 'derive' | 'query' | 'rpc' | 'tx'

Expand All @@ -38,7 +39,13 @@ export type WorkerMessage =
| WorkerQueryMessage
| WorkerTxMessage
| WorkerProxyMessage

export type ClientMessage = ClientInitMessage | ClientQueryMessage | ClientTxMessage | ClientProxyMessage
| WorkerAsyncMessage

export type ClientMessage =
| ClientInitMessage
| ClientQueryMessage
| ClientTxMessage
| ClientProxyMessage
| ClientAsyncMessage

export type AnyMessage = WorkerMessage | ClientMessage
23 changes: 14 additions & 9 deletions packages/ui/src/proxyApi/worker/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import '@joystream/types'
import { ApiRx, WsProvider } from '@polkadot/api'
import { getPolkadotApiChainInfo } from 'injectweb3-connect'
import { BehaviorSubject, filter, first, fromEvent, share } from 'rxjs'

import { isDefined } from '@/common/utils'
Expand All @@ -13,8 +14,8 @@ import { transactionsRecord, tx } from './tx'

const apiObserver = new BehaviorSubject<ApiRx | undefined>(undefined)

const postMessage: PostMessage<WorkerMessage> = (message) =>
self.postMessage({ ...message, payload: serializePayload(message.payload) })
const postMessage: PostMessage<WorkerMessage> = (message, toJSON = false) =>
self.postMessage({ ...message, payload: serializePayload(message.payload, { toJSON }) })

const messages = fromEvent<RawClientMessageEvent>(self, 'message')

Expand All @@ -25,7 +26,7 @@ const clientProxyMessage = messages.pipe(
)

messages.subscribe(({ data }) => {
const payload = deserializePayload(data.payload, clientProxyMessage, postMessage, transactionsRecord)
const payload = deserializePayload(data.payload, { messages: clientProxyMessage, postMessage, transactionsRecord })
const message = { ...data, payload } as ClientMessage

if (message.messageType === 'init') {
Expand All @@ -35,26 +36,30 @@ messages.subscribe(({ data }) => {
.subscribe((api) => {
postMessage({ messageType: 'init', payload: { consts: api.consts } })
postMessage({ messageType: 'isConnected', payload: true })

api.on('connected', () => self.postMessage({ messageType: 'isConnected', payload: true }))
api.on('disconnected', () => self.postMessage({ messageType: 'isConnected', payload: false }))

apiObserver.next(api)
})
} else {
apiObserver.pipe(firstWhere(isDefined)).subscribe((api) => {
apiObserver.pipe(firstWhere(isDefined)).subscribe(async (api) => {
if (!api) return

switch (message.messageType) {
case 'derive':
return query('derive', api as ApiRx, message, postMessage)
return query('derive', api, message, postMessage)

case 'query':
return query('query', api as ApiRx, message, postMessage)
return query('query', api, message, postMessage)

case 'rpc':
return query('rpc', api as ApiRx, message, postMessage)
return query('rpc', api, message, postMessage)

case 'tx':
return tx(api as ApiRx, message, postMessage)
return tx(api, message, postMessage)

case 'chain-metadata':
return postMessage({ messageType: 'chain-metadata', payload: await getPolkadotApiChainInfo(api) }, true)
}
})
}
Expand Down
1 change: 1 addition & 0 deletions packages/ui/test/_mocks/transactions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,7 @@ export const stubConst = <T>(api: UseApi, constSubPath: string, value: T) => {
export const stubApi = () => {
const api: UseApi = {
api: {
_async: { chainMetadata: Promise.resolve({}) },
isConnected: true,
} as unknown as Api,
isConnected: true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,10 @@ describe('UI: TransactionButton', () => {
stubTransaction(api, txPath)

beforeAll(async () => {
stubAccounts([{ ...alice, name: 'Alice Account' }], { wallet: new BaseDotsamaWallet({ title: 'ExtraWallet' }) })
const wallet = new BaseDotsamaWallet({ title: 'ExtraWallet' })
stubAccounts([{ ...alice, name: 'Alice Account' }], {
wallet,
})
await cryptoWaitReady()
})

Expand Down
Loading

2 comments on commit 7c4ffc2

@vercel
Copy link

@vercel vercel bot commented on 7c4ffc2 May 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2-storybook – ./

pioneer-2-storybook.vercel.app
pioneer-2-storybook-git-dev-joystream.vercel.app
pioneer-2-storybook-joystream.vercel.app

@vercel
Copy link

@vercel vercel bot commented on 7c4ffc2 May 30, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

pioneer-2 – ./

pioneer-2-joystream.vercel.app
pioneer-2.vercel.app
pioneer-2-git-dev-joystream.vercel.app

Please sign in to comment.