Skip to content

Commit

Permalink
feat: introduce module manager (#1508)
Browse files Browse the repository at this point in the history
Co-authored-by: ruijialin <ruijia.lin@avalabs.org>
  • Loading branch information
meeh0w and ruijialin-avalabs authored Jul 25, 2024
1 parent 75c7c1c commit a8c9985
Show file tree
Hide file tree
Showing 18 changed files with 822 additions and 2 deletions.
7 changes: 6 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
"sentry": "node sentryscript.js"
},
"dependencies": {
"@avalabs/vm-module-types": "0.0.15",
"@avalabs/avalanchejs": "4.0.5",
"@avalabs/bridge-sdk": "2.8.0-alpha.188",
"@avalabs/bridge-unified": "2.1.0",
Expand Down Expand Up @@ -242,7 +243,11 @@
"web3>web3-bzz": false,
"web3>web3-shh": false,
"yarn": false,
"@avalabs/bridge-sdk>@avalabs/wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false
"@avalabs/bridge-sdk>@avalabs/wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
"@avalabs/vm-module-types": false,
"@avalabs/vm-module-types>@avalabs/wallets-sdk>@avalabs/hw-app-avalanche>@ledgerhq/hw-app-eth>@ledgerhq/domain-service>eip55>keccak": false,
"@avalabs/vm-module-types>@avalabs/wallets-sdk>@ledgerhq/hw-app-btc>bitcoinjs-lib>bip32>tiny-secp256k1": false,
"@avalabs/vm-module-types>@avalabs/wallets-sdk>hdkey>secp256k1": false
}
}
}
88 changes: 88 additions & 0 deletions src/background/vmModules/ModuleManager.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { NetworkVMType } from '@avalabs/chains-sdk';

import ModuleManager from './ModuleManager';
import { VMModuleError } from './models';

describe('ModuleManager', () => {
describe('when not initialized', () => {
it('should throw not initialized error', async () => {
try {
await ModuleManager.loadModule('eip155:123', 'eth_randomMethod');
} catch (e: any) {
expect(e.data.reason).toBe(VMModuleError.ModulesNotInitialized);
}
});
});

describe('when initialized', () => {
beforeEach(async () => {
await ModuleManager.init();
});

it('should load the correct modules', async () => {
const params = [
{
chainId: 'eip155:1',
method: 'eth_randomMethod',
name: NetworkVMType.EVM,
},
{
chainId: 'bip122:000000000019d6689c085ae165831e93',
method: 'bitcoin_randomMethod',
name: NetworkVMType.BITCOIN,
},
{
chainId: 'avax:2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM',
method: 'avalanche_randomMethod',
name: NetworkVMType.AVM,
},
{
chainId: 'avax:11111111111111111111111111111111LpoYY',
method: 'avalanche_randomMethod',
name: NetworkVMType.PVM,
},
{
chainId: 'eip2256:1',
method: 'eth_randomMethod',
name: NetworkVMType.CoreEth,
},
];

await Promise.all(
params.map(async (param) => {
const module = await ModuleManager.loadModule(
param.chainId,
param.method
);
expect(module?.getManifest()?.name.toLowerCase()).toContain(
param.name.toLowerCase()
);
})
);
});

it('should have thrown with incorrect chainId', async () => {
try {
await ModuleManager.loadModule('eip155:123', 'eth_randomMethod');
} catch (e: any) {
expect(e.data.reason).toBe(VMModuleError.UnsupportedChain);
}
});

it('should have thrown with incorrect method', async () => {
try {
await ModuleManager.loadModule('eip155:1', 'evth_randomMethod');
} catch (e: any) {
expect(e.data.reason).toBe(VMModuleError.UnsupportedMethod);
}
});

it('should have thrown with incorrect namespace', async () => {
try {
await ModuleManager.loadModule('avalanche:1', 'eth_method');
} catch (e: any) {
expect(e.data.reason).toBe(VMModuleError.UnsupportedNamespace);
}
});
});
});
123 changes: 123 additions & 0 deletions src/background/vmModules/ModuleManager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
import { Module } from '@avalabs/vm-module-types';
import { ethErrors } from 'eth-rpc-errors';

import { assertPresent } from '@src/utils/assertions';

import { NetworkWithCaipId } from '../services/network/models';

import { AVMModule } from './mocks/avm';
import { EVMModule } from './mocks/evm';
import { PVMModule } from './mocks/pvm';
import { BitcoinModule } from './mocks/bitcoin';
import { CoreEthModule } from './mocks/coreEth';
import { VMModuleError } from './models';

// https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md
// Syntax for namespace is defined in CAIP-2
const NAMESPACE_REGEX = new RegExp('^[-a-z0-9]{3,8}$');

class ModuleManager {
#_modules: Module[] | undefined;

get #modules(): Module[] {
assertPresent(this.#_modules, VMModuleError.ModulesNotInitialized);

return this.#_modules;
}

set #modules(modules: Module[]) {
this.#_modules = modules;
}

async init(): Promise<void> {
if (this.#_modules !== undefined) return;

this.#modules = [
new EVMModule(),
new BitcoinModule(),
new AVMModule(),
new CoreEthModule(),
new PVMModule(),
];
}

async loadModule(caipId: string, method?: string): Promise<Module> {
const module = await this.#getModule(caipId);

if (module === undefined) {
throw ethErrors.rpc.invalidParams({
data: {
reason: VMModuleError.UnsupportedChain,
caipId,
},
});
}

if (method && !this.#isMethodPermitted(module, method)) {
throw ethErrors.rpc.invalidParams({
data: {
reason: VMModuleError.UnsupportedMethod,
method,
},
});
}

return module;
}

async loadModuleByNetwork(
network: NetworkWithCaipId,
method?: string
): Promise<Module> {
return this.loadModule(network.caipId, method);
}

async #getModule(chainId: string): Promise<Module | undefined> {
const [namespace] = chainId.split(':');

if (!namespace || !NAMESPACE_REGEX.test(namespace)) {
throw ethErrors.rpc.invalidParams({
data: {
reason: VMModuleError.UnsupportedNamespace,
namespace,
},
});
}

return (
(await this.#getModuleByChainId(chainId)) ??
(await this.#getModuleByNamespace(namespace))
);
}

async #getModuleByChainId(chainId: string): Promise<Module | undefined> {
return this.#modules.find((module) =>
module.getManifest()?.network.chainIds.includes(chainId)
);
}

async #getModuleByNamespace(namespace: string): Promise<Module | undefined> {
return this.#modules.find((module) =>
module.getManifest()?.network.namespaces.includes(namespace)
);
}

#isMethodPermitted(module: Module, method: string): boolean {
const methods = module.getManifest()?.permissions.rpc.methods;

if (methods === undefined) {
return false;
}

return methods.some((m) => {
if (m === method) {
return true;
}
if (m.endsWith('*')) {
return method.startsWith(m.slice(0, -1));
}
});
}
}

export default new ModuleManager();
42 changes: 42 additions & 0 deletions src/background/vmModules/mocks/avm.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "AVM",
"description": "",
"version": "0.0.1",
"sources": {
"module": {
"checksum": "",
"location": {
"npm": {
"filePath": "dist/bundle.js",
"packageName": "@avalabs/avm-module",
"registry": "https://registry.npmjs.org"
}
}
},
"provider": {
"checksum": "",
"location": {
"npm": {
"filePath": "dist/provider.js",
"packageName": "@avalabs/avm-module",
"registry": "https://registry.npmjs.org"
}
}
}
},
"network": {
"chainIds": [
"avax:2oYMBNV4eNHyqk2fjjV5nVQLDbtmNJzq5s3qs3Lo6ftnC6FByM",
"avax:2JVSBoinj9C2J33VntvzYtVJNZdN2NKiwwKjcumHUWEb5DbBrm"
],
"namespaces": ["avax"]
},
"cointype": "60",
"permissions": {
"rpc": {
"dapps": true,
"methods": ["avalanche_sendTransaction", "avalanche_*"]
}
},
"manifestVersion": "0.0"
}
60 changes: 60 additions & 0 deletions src/background/vmModules/mocks/avm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import {
Manifest,
Module,
parseManifest,
GetTransactionHistory,
NetworkFees,
NetworkContractToken,
RpcRequest,
TransactionHistoryResponse,
GetBalancesResponse,
Network,
RpcResponse,
} from '@avalabs/vm-module-types';
import { ethErrors } from 'eth-rpc-errors';

import manifest from './avm.manifest.json';

export class AVMModule implements Module {
getManifest(): Manifest | undefined {
const result = parseManifest(manifest);
return result.success ? result.data : undefined;
}

getBalances(): Promise<GetBalancesResponse> {
return Promise.resolve({});
}

getTransactionHistory(
_: GetTransactionHistory // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<TransactionHistoryResponse> {
return Promise.resolve({ transactions: [], nextPageToken: '' });
}

getNetworkFee(): Promise<NetworkFees> {
return Promise.resolve({
low: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n },
medium: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n },
high: { maxPriorityFeePerGas: 0n, maxFeePerGas: 0n },
baseFee: 0n,
isFixedFee: false,
});
}

getAddress(): Promise<string> {
return Promise.resolve('AVM address');
}

// eslint-disable-next-line @typescript-eslint/no-unused-vars
getTokens(_: Network): Promise<NetworkContractToken[]> {
return Promise.resolve([]);
}

async onRpcRequest(request: RpcRequest): Promise<RpcResponse> {
return {
error: ethErrors.rpc.methodNotSupported({
data: `Method ${request.method} not supported`,
}) as any, // TODO: fix it
};
}
}
39 changes: 39 additions & 0 deletions src/background/vmModules/mocks/bitcoin.manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
{
"name": "Bitcoin",
"description": "",
"version": "0.0.1",
"sources": {
"module": {
"checksum": "",
"location": {
"npm": {
"filePath": "dist/bundle.js",
"packageName": "@avalabs/bitcoin-module",
"registry": "https://registry.npmjs.org"
}
}
},
"provider": {
"checksum": "",
"location": {
"npm": {
"filePath": "dist/provider.js",
"packageName": "@avalabs/bitcoin-module",
"registry": "https://registry.npmjs.org"
}
}
}
},
"network": {
"chainIds": ["bip122:000000000019d6689c085ae165831e93"],
"namespaces": ["bip122"]
},
"cointype": "60",
"permissions": {
"rpc": {
"dapps": true,
"methods": ["bitcoin_sendTransaction", "bitcoin_*"]
}
},
"manifestVersion": "0.0"
}
Loading

0 comments on commit a8c9985

Please sign in to comment.