Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions packages/sdk-multichain/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,4 +19,6 @@ src/
│ ├── events/ # Event-driven communication
│ ├── platform/ # Platform detection utilities
│ └── store/ # Storage abstractions
├── multichain/ # Concrete implementations
└── store/ # Storage implementations
```
1 change: 1 addition & 0 deletions packages/sdk-multichain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@react-native-async-storage/async-storage": "^1.19.6",
"@vitest/coverage-v8": "^3.2.4",
"esbuild-plugin-umd-wrapper": "^3.0.0",
"jsdom": "^26.1.0",
"nock": "^14.0.4",
"prettier": "^3.3.3",
"tsup": "^8.5.0",
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk-multichain/src/domain/errors/base.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ErrorCodes } from "./types";

export abstract class BaseErr<C extends string, T extends ErrorCodes> extends Error {
constructor(
public readonly message: `${C}Err${T}: ${string}`,
public readonly code: T,
) {
super(message);
}
}
2 changes: 2 additions & 0 deletions packages/sdk-multichain/src/domain/errors/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export * from './rpc'
export * from './types';
53 changes: 53 additions & 0 deletions packages/sdk-multichain/src/domain/errors/rpc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@

import { BaseErr } from "./base";
import type { RPCErrorCodes } from "./types";

export class RPCHttpErr extends BaseErr<'RPC', RPCErrorCodes> {
static readonly code = 50;
constructor(
readonly rpcEndpoint: string,
readonly method: string,
readonly httpStatus: number,
) {
super(
`RPCErr${RPCHttpErr.code}: ${httpStatus} on ${rpcEndpoint} for method ${method}`,
RPCHttpErr.code
);
}
}

export class RPCReadonlyResponseErr extends BaseErr<'RPC', RPCErrorCodes> {
static readonly code = 51;
constructor(
public readonly reason: string,
) {
super(
`RPCErr${RPCReadonlyResponseErr.code}: RPC Client response reason ${reason}`,
RPCReadonlyResponseErr.code
);
}
}

export class RPCReadonlyRequestErr extends BaseErr<'RPC', RPCErrorCodes> {
static readonly code = 52;
constructor(
public readonly reason: string,
) {
super(
`RPCErr${RPCReadonlyRequestErr.code}: RPC Client fetch reason ${reason}`,
RPCReadonlyRequestErr.code
);
}
}

export class RPCInvokeMethodErr extends BaseErr<'RPC', RPCErrorCodes> {
static readonly code = 53;
constructor(
public readonly reason: string,
) {
super(
`RPCErr${RPCInvokeMethodErr.code}: RPC Client invoke method reason ${reason}`,
RPCInvokeMethodErr.code
);
}
}
45 changes: 45 additions & 0 deletions packages/sdk-multichain/src/domain/errors/storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@

import { BaseErr } from "./base";
import type { StorageErrorCodes } from "./types";

export class StorageGetErr extends BaseErr<'Storage', StorageErrorCodes> {
static readonly code = 60;
constructor(
public readonly platform: 'web' | 'rn' | 'node',
public readonly key: string,
public readonly reason: string,
) {
super(
`StorageErr${StorageGetErr.code}: ${platform} storage get error in key: ${key} - ${reason}`,
StorageGetErr.code
);
}
}

export class StorageSetErr extends BaseErr<'Storage', StorageErrorCodes> {
static readonly code = 61;
constructor(
public readonly platform: 'web' | 'rn' | 'node',
public readonly key: string,
public readonly reason: string,
) {
super(
`StorageErr${StorageSetErr.code}: ${platform} storage set error in key: ${key} - ${reason}`,
StorageSetErr.code
);
}
}

export class StorageDeleteErr extends BaseErr<'Storage', StorageErrorCodes> {
static readonly code = 62;
constructor(
public readonly platform: 'web' | 'rn' | 'node',
public readonly key: string,
public readonly reason: string,
) {
super(
`StorageErr${StorageDeleteErr.code}: ${platform} storage delete error in key: ${key} - ${reason}`,
StorageDeleteErr.code
);
}
}
14 changes: 14 additions & 0 deletions packages/sdk-multichain/src/domain/errors/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export type Enumerate<N extends number, Acc extends number[] = []> = Acc['length'] extends N
? Acc[number]
: Enumerate<N, [...Acc, Acc['length']]>;

export type ErrorCodeRange<F extends number, T extends number> = Exclude<Enumerate<T>, Enumerate<F>>;

export type DomainErrorCodes = ErrorCodeRange<1, 50>;
export type RPCErrorCodes = ErrorCodeRange<50, 60>;
export type StorageErrorCodes = ErrorCodeRange<60, 70>;

export type ErrorCodes =
| DomainErrorCodes
| RPCErrorCodes
| StorageErrorCodes;
4 changes: 1 addition & 3 deletions packages/sdk-multichain/src/domain/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
import * as t from 'vitest'
import Bowser from 'bowser';

import { EventEmitter,ExtensionEvents, SDKEvents, getPlatformType, PlatformType, createLogger, enableDebug, isEnabled } from './';


import { EventEmitter,type ExtensionEvents, type SDKEvents, getPlatformType, PlatformType, createLogger, enableDebug, isEnabled } from './';


const parseMock = t.vi.fn();
Expand Down
1 change: 1 addition & 0 deletions packages/sdk-multichain/src/domain/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './errors';
export * from './platform';
export * from './multichain';
export * from './events';
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* c8 ignore start */
import { RPC_URLS_MAP } from "./types";
import type { RPC_URLS_MAP } from "./types";

export const infuraRpcUrls: RPC_URLS_MAP = {
// ###### Ethereum ######
Expand Down
10 changes: 10 additions & 0 deletions packages/sdk-multichain/src/domain/multichain/api/infura.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { infuraRpcUrls } from "./constants";
import type { RPC_URLS_MAP } from "./types";

export function getInfuraRpcUrls(infuraAPIKey: string) {
return Object.keys(infuraRpcUrls).reduce((acc, key) => {
const typedKey = key as keyof typeof infuraRpcUrls;
acc[typedKey] = `${infuraRpcUrls[typedKey]}${infuraAPIKey}`;
return acc;
}, {} as RPC_URLS_MAP)
}
35 changes: 16 additions & 19 deletions packages/sdk-multichain/src/domain/multichain/api/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Json } from "@metamask/utils";
import type { Json } from "@metamask/utils";
import type EIP155 from "./eip155";

/**
Expand Down Expand Up @@ -41,22 +41,6 @@ export type RPCAPI = {
eip155: EIP155;
};

/**
* Represents a notification object for RPC communication.
*
* Notifications are one-way messages sent from the server to the client
* that don't require a response. They follow the JSON-RPC 2.0 specification
* for notification messages.
*/
export type Notification = {
/** The name of the method being called */
method: string;
/** Optional parameters for the method call */
params?: Json;
/** JSON-RPC version identifier (typically "2.0") */
jsonrpc?: string;
};

/**
* Callback function type for handling incoming notifications.
*
Expand All @@ -65,7 +49,7 @@ export type Notification = {
*
* @param notification - The notification object to handle
*/
export type NotificationCallback = (notification: Notification) => void;
export type NotificationCallback = (notification: unknown) => void;

/**
* Options for invoking RPC methods with specific scope and request parameters.
Expand Down Expand Up @@ -97,4 +81,17 @@ export type RPC_URLS_MAP = {
[chainId: `${string}:${string}`]: string;
};


/**
* Represents the structure of a JSON-RPC response.
*
* This type defines the expected format of JSON-RPC responses from RPC endpoints.
* It includes the unique identifier for the request, the JSON-RPC version used,
* and the result of the RPC call.
*
* @property id - The unique identifier for the request
* @property jsonrpc - The JSON-RPC version used
* @property result - The result of the RPC call JSON
*/
export type RPCResponse = {
id: number, jsonrpc: string, result: unknown
}
41 changes: 15 additions & 26 deletions packages/sdk-multichain/src/domain/multichain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,10 @@ import type { InvokeMethodOptions, NotificationCallback, RPC_URLS_MAP, Scope } f
* - Using a regular icon URL
* - Using a base64-encoded icon
*/
export type DappSettings =
| { name?: string; url?: string; iconUrl?: string }
| { name?: string; url?: string; base64Icon?: string };
export type DappSettings = {
name?: string;
url?: string;
} & ({ iconUrl?: string } | { base64Icon?: string });

/**
* Constructor options for creating a Multichain SDK instance.
Expand Down Expand Up @@ -53,39 +54,25 @@ export type MultichainSDKConstructor = {
*/
/* c8 ignore start */
export abstract class MultichainSDKBase {
public abstract isInitialized: boolean;
public abstract session: SessionData | undefined;

/**
* Establishes a connection to the multichain provider.
* Establishes a connection to the multichain provider, or re-use existing session
*
* @returns Promise that resolves to true if connection is successful, false otherwise
* @returns Promise that resolves to the session data
*/
abstract connect(): Promise<boolean>;
abstract connect(
scopes: Scope[],
caipAccountIds: CaipAccountId[],
): Promise<SessionData>;

/**
* Disconnects from the multichain provider.
*
* @returns Promise that resolves when disconnection is complete
*/
abstract disconnect(): Promise<void>;

/**
* Retrieves the current session data.
*
* @returns Promise that resolves to the current session data, or undefined if no session exists
*/
abstract getSession(): Promise<SessionData | undefined>;

/**
* Creates a new session with the specified scopes and account IDs.
*
* @param scopes - Array of blockchain scopes to request access to
* @param caipAccountIds - Array of CAIP account IDs to associate with the session
* @returns Promise that resolves to the created session data
*/
abstract createSession(
scopes: Scope[],
caipAccountIds: CaipAccountId[],
): Promise<SessionData>;

/**
* Revokes the current session.
*
Expand Down Expand Up @@ -143,3 +130,5 @@ export type MultichainSDKOptions = MultichainSDKBaseOptions & {
export type CreateMultichainFN = ( options: MultichainSDKBaseOptions ) => Promise<MultichainSDKBase>;

export type * from './api/types';
export * from './api/constants';
export * from './api/infura';
3 changes: 3 additions & 0 deletions packages/sdk-multichain/src/domain/store/adapter.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
/* c8 ignore start */
export type StoreOptions = Record<string, any>;



export abstract class StoreAdapter {
abstract platform: 'web' | 'rn' | 'node'
constructor(public options?: StoreOptions) {}

abstract getItem(key: string): Promise<string | null>;
Expand Down
7 changes: 7 additions & 0 deletions packages/sdk-multichain/src/globals.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,12 @@ declare global {
* TODO: Add types for the window object to manage connection with inApp browser, etc
*/
ReactNativeWebView?: any;
mmsdk?: any;
ethereum?: {
isMetaMask?: boolean;
request?: (request: { method: string; params?: any[] }) => Promise<any>;
on?: (eventName: string, handler: (...args: any[]) => void) => void;
removeListener?: (eventName: string, handler: (...args: any[]) => void) => void;
};
}
}
15 changes: 15 additions & 0 deletions packages/sdk-multichain/src/index.browser.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
import type { CreateMultichainFN } from './domain';
import { MultichainSDK } from './multichain';
import { Store } from './store';

export type * from './domain';

export const createMetamaskSDK: CreateMultichainFN = async (options) => {
const { StoreAdapterWeb } = await import('./store/adapters/web');
const adapter = new StoreAdapterWeb();
return MultichainSDK.create({
...options,
storage: new Store(adapter),
ui: {
headless: options.ui?.headless ?? false,
},
});
}
15 changes: 15 additions & 0 deletions packages/sdk-multichain/src/index.native.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
import type { CreateMultichainFN } from './domain';
import { MultichainSDK } from './multichain';
import { Store } from './store';

export type * from './domain';

export const createMetamaskSDK: CreateMultichainFN = async (options) => {
const { StoreAdapterRN } = await import('./store/adapters/rn');
const adapter = new StoreAdapterRN();
return MultichainSDK.create({
...options,
storage: new Store(adapter),
ui: {
headless: options.ui?.headless ?? false, // React Native can show UI
},
});
}
15 changes: 15 additions & 0 deletions packages/sdk-multichain/src/index.node.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,17 @@
import type { CreateMultichainFN } from './domain';
import { MultichainSDK } from './multichain';
import { Store } from './store';

export type * from './domain';

export const createMetamaskSDK: CreateMultichainFN = async (options) => {
const { StoreAdapterNode } = await import('./store/adapters/node');
const adapter = new StoreAdapterNode();
return MultichainSDK.create({
...options,
storage: new Store(adapter),
ui: {
headless: true, // Node.js defaults to headless
},
});
}
Loading
Loading