Skip to content

Commit

Permalink
feat: support eth_subscribe
Browse files Browse the repository at this point in the history
  • Loading branch information
Jack-Works committed Jan 8, 2024
1 parent c034c49 commit 827b05a
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 33 deletions.
5 changes: 5 additions & 0 deletions packages/mask-sdk/shared/error-generated.ts
Expand Up @@ -19,6 +19,7 @@ export enum ErrorMessages {
limit_exceeded = "Limit exceeded",
json_rpc_version_not_supported = "JSON-RPC version not supported",
invalid_request = "Invalid request",
the_method_eth_subscribe_is_only_available_on_the_mainnet = "The method \"eth_subscribe\" is only available on the mainnet.",
invalid_params = "Invalid params",
wallet_request_permissions_a_permission_request_must_contain_at_least_1_permission = "A permission request must contain at least 1 permission.",
internal_error = "Internal error",
Expand All @@ -39,6 +40,7 @@ const codeMap = {
"Limit exceeded": -32005,
"JSON-RPC version not supported": -32006,
"Invalid request": -32600,
"The method \"eth_subscribe\" is only available on the mainnet.": -32601,
"Invalid params": -32602,
"A permission request must contain at least 1 permission.": -32602,
"Internal error": -32603,
Expand Down Expand Up @@ -78,6 +80,9 @@ export const err = {
the_method_method_does_not_exist_is_not_available({ method }: Record<"method", string>,options: MaskEthereumProviderRpcErrorOptions = {}) {
return new MaskEthereumProviderRpcError(-32601, `The method "${method}" does not exist / is not available.`, options)
},
the_method_eth_subscribe_is_only_available_on_the_mainnet(options: MaskEthereumProviderRpcErrorOptions = {}) {
return new MaskEthereumProviderRpcError(-32601, "The method \"eth_subscribe\" is only available on the mainnet.", options)
},
invalid_params(options: MaskEthereumProviderRpcErrorOptions = {}) {
return new MaskEthereumProviderRpcError(-32602, "Invalid params", options)
},
Expand Down
3 changes: 3 additions & 0 deletions packages/mask-sdk/shared/messages.txt
Expand Up @@ -22,3 +22,6 @@
4200 The requested method is not supported by this Ethereum provider
4900 The provider is disconnected from all chains
4901 The provider is disconnected from the specified chain

# Our special behavior
-32601 The method "eth_subscribe" is only available on the mainnet.
4 changes: 2 additions & 2 deletions packages/mask/entry-sdk/README.md
Expand Up @@ -49,8 +49,8 @@ The list is built from what [MetaMask supported](https://docs.metamask.io/wallet

## Subscribe to events (unknown specification)

- [ ] eth_subscribe
- [ ] eth_unsubscribe
- [x] eth_subscribe
- [x] eth_unsubscribe

## Filters

Expand Down
85 changes: 54 additions & 31 deletions packages/mask/entry-sdk/bridge/eth.ts
@@ -1,4 +1,4 @@
import { readonlyMethodType, EthereumMethodType, ProviderType, type Web3Provider } from '@masknet/web3-shared-evm'
import { readonlyMethodType, EthereumMethodType, ProviderType, ChainId } from '@masknet/web3-shared-evm'
import Services from '#services'
import { type EIP2255PermissionRequest, MaskEthereumProviderRpcError, err } from '@masknet/sdk'
import { Err, Ok } from 'ts-results-es'
Expand All @@ -7,6 +7,8 @@ import * as providers from /* webpackDefer: true */ '@masknet/web3-providers'
import { ParamsValidate, fromZodError, requestSchema, ReturnValidate } from './eth/validator.js'
import { ZodError, ZodTuple } from 'zod'
import { maskSDK } from '../index.js'
import { sample } from 'lodash-es'
import { AsyncCall, JSONSerialization } from 'async-call-rpc/full'

const readonlyMethods: Record<EthereumMethodType, (params: unknown[] | undefined) => Promise<unknown>> = {} as any
for (const method of readonlyMethodType) {
Expand All @@ -15,15 +17,54 @@ for (const method of readonlyMethodType) {
}
}

let readonlyClient: Web3Provider
function setReadonlyClient(): Web3Provider {
return (readonlyClient ??= providers.EVMWeb3.getWeb3Provider({
providerType: ProviderType.MaskWallet,
silent: true,
readonly: true,
}))
interface InteractiveClient {
eth_subscribe(...params: Zod.infer<(typeof ParamsValidate)['eth_subscribe']>): Promise<string>
eth_unsubscribe(...params: Zod.infer<(typeof ParamsValidate)['eth_unsubscribe']>): Promise<string>
}
const subscriptionMap = new Map<string, () => void>()

let interactiveClient: InteractiveClient | undefined
// TODO: Our infrastructure uses HTTP endpoints (packages/web3-providers/src/helpers/createWeb3ProviderFromURL.ts).
// Only WebSocket infura endpoints can subscribe to events (required for eth_subscribe).
// As a workaround, we establish the WebSocket connection at the content script.
// This is a problem for unknown web pages (CSP limitations), but acceptable for Mask SDK users.
// They need to unblock mainnet.infura.io in their CSP.
// For initial implementation simplicity and cost consideration (subscription is only free on mainnet for infura),
// we only support subscribe on the mainnet.
function getInteractiveClient(): Promise<InteractiveClient> {
if (interactiveClient) return Promise.resolve(interactiveClient)
return new Promise<InteractiveClient>((resolve, reject) => {
// The following endpoints are from packages/web3-constants/evm/rpc.json
const ws = new WebSocket(
sample([
'wss://mainnet.infura.io/ws/v3/d74bd8586b9e44449cef131d39ceeefb',
'wss://mainnet.infura.io/ws/v3/d65858b010d249419cf8687eca12b094',
'wss://mainnet.infura.io/ws/v3/a9d66980bf334e59a42ca19095f3daeb',
'wss://mainnet.infura.io/ws/v3/f39cc8734e294fba9c3938486df2b1bc',
'wss://mainnet.infura.io/ws/v3/659123dd11294baf8a294d7a11cec92c',
])!,
)
interactiveClient = AsyncCall(
{
eth_subscription(data: unknown) {
maskSDK.eth_message({ type: 'eth_subscription', data })
},
},
{
channel: {
send: (message) => ws.send(message as string),
on: (fn) => ws.addEventListener('message', (event) => fn(event.data)),
},
serializer: JSONSerialization(),
log: false,
thenable: false,
},
)
ws.addEventListener('close', () => (interactiveClient = undefined))
ws.addEventListener('open', () => resolve(interactiveClient!))
ws.addEventListener('error', () => reject(err.internal_error()))
})
}

// Reference:
// https://ethereum.github.io/execution-apis/api-documentation/
// https://docs.metamask.io/wallet/reference/eth_subscribe/
Expand Down Expand Up @@ -93,31 +134,13 @@ const methods = {
})
},
async eth_subscribe(...params: Zod.infer<(typeof ParamsValidate)['eth_subscribe']>) {
const id = String(
await setReadonlyClient().request({
method: EthereumMethodType.ETH_SUBSCRIBE,
params,
}),
)
const fn = (message: { type: string; data: unknown }): void => {
if (message.type === EthereumMethodType.ETH_SUBSCRIBE && (message.data as any).subscription === id) {
maskSDK.eth_message(message)
}
if ((await Services.Wallet.sdk_eth_chainId()) !== ChainId.Mainnet) {
return err.the_method_eth_subscribe_is_only_available_on_the_mainnet()
}
subscriptionMap.set(id, () => readonlyClient.removeListener('message', fn))
readonlyClient.on('message', fn)
window.addEventListener('beforeunload', () =>
readonlyClient!.request({ method: EthereumMethodType.ETH_UNSUBSCRIBE, params: [id] }),
)
return id
return (await getInteractiveClient()).eth_subscribe(...params)
},
async eth_unsubscribe(...params: Zod.infer<(typeof ParamsValidate)['eth_sendRawTransaction']>) {
await setReadonlyClient().request({
method: EthereumMethodType.ETH_UNSUBSCRIBE,
params,
})
subscriptionMap.get(params[0])?.()
return null
return (await getInteractiveClient()).eth_unsubscribe(...params)
},
// https://eips.ethereum.org/EIPS/eip-2255
wallet_getPermissions() {
Expand Down

0 comments on commit 827b05a

Please sign in to comment.