Skip to content

Commit

Permalink
feat: implement SDK with provider injection and connection management (
Browse files Browse the repository at this point in the history
  • Loading branch information
jinoosss authored Sep 6, 2024
1 parent 6d21f41 commit b9d9957
Show file tree
Hide file tree
Showing 58 changed files with 1,673 additions and 472 deletions.
15 changes: 15 additions & 0 deletions jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"projects": ["<rootDir>/packages/*"],
"watchPathIgnorePatterns": ["<rootDir>/node_modules/", "<rootDir>/bin/"],
"testPathIgnorePatterns": [
"<rootDir>/node_modules/",
"<rootDir>/bin/",
"<rootDir>/**/node_modules/",
"<rootDir>/**/bin/",
"<rootDir>/**/__mocks__/",
"<rootDir>/**/coverage/"
],
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
"url": "https://github.com/onbloc/adena-wallet-sdk"
},
"scripts": {
"build": "yarn workspaces foreach -vptR run build",
"build": "yarn workspaces foreach -vA run build",
"lint": "yarn workspaces foreach -vA run lint",
"lint:fix": "yarn workspaces foreach -vA run lint:fix",
"test": "yarn workspaces foreach -vA run test",
Expand Down
8 changes: 7 additions & 1 deletion packages/sdk/jest.config.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
{
"preset": "ts-jest",
"testEnvironment": "node",
"modulePathIgnorePatterns": ["bin", "node_modules"]
"transform": {
"^.+\\.(ts|tsx)$": "ts-jest"
},
"testMatch": ["<rootDir>/**/*.test.(ts|tsx)"],
"moduleFileExtensions": ["ts", "tsx", "js", "jsx"],
"moduleDirectories": ["node_modules", "<rootDir>/packages"],
"modulePathIgnorePatterns": ["bin", "node_modules", "coverage", "__mocks__"]
}
3 changes: 2 additions & 1 deletion packages/sdk/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@
"test:ci": "jest --coverage --passWithNoTests "
},
"dependencies": {
"@gnolang/gno-js-client": "^1.3.0"
"@gnolang/gno-js-client": "^1.3.0",
"@gnolang/tm2-js-client": "^1.2.1"
},
"devDependencies": {
"@types/eslint": "^9",
Expand Down
11 changes: 11 additions & 0 deletions packages/sdk/src/core/__mocks__/mock-global.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { mockSessionStorage } from './mock-storage';

export const defineGlobalMock = () => {
Object.defineProperty(global, 'sessionStorage', {
value: mockSessionStorage,
});
};

export const clearGlobalMock = () => {
Object.defineProperty(global, 'sessionStorage', {});
};
18 changes: 18 additions & 0 deletions packages/sdk/src/core/__mocks__/mock-storage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
export const mockSessionStorage = (() => {
let store: Record<string, string> = {};

return {
getItem(key: string) {
return store[key] || null;
},
setItem(key: string, value: string) {
store[key] = value.toString();
},
removeItem(key: string) {
delete store[key];
},
clear() {
store = {};
},
};
})();
164 changes: 164 additions & 0 deletions packages/sdk/src/core/__mocks__/mock-wallet-provider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
import { WalletProvider } from '../providers';
import { WalletResponseFailureType, WalletResponseStatus, WalletResponseSuccessType } from '../types';

export const mockWalletProvider: jest.Mocked<WalletProvider> = {
isConnected: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: 'Wallet is connected',
data: null,
}),
addEstablish: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: 'Connection established',
data: null,
}),
getAccount: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.GET_ACCOUNT_SUCCESS,
message: 'Account retrieved',
data: { address: 'mock-address', connected: true },
}),
switchNetwork: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SWITCH_NETWORK_SUCCESS,
message: 'Network switched',
data: null,
}),
addNetwork: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.ADD_NETWORK_SUCCESS,
message: 'Network added',
data: null,
}),
signTransaction: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SIGN_SUCCESS,
message: 'Transaction signed',
data: 'mock-signed-transaction',
}),
broadcastTransaction: jest.fn().mockResolvedValue({
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.TRANSACTION_SUCCESS,
message: 'Transaction broadcasted',
data: { result: 'mock-result' },
}),
onChangeAccount: jest.fn().mockImplementation(() => {
return {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: 'Account change listener added',
data: null,
};
}),
onChangeNetwork: jest.fn().mockImplementation(() => {
return {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SWITCH_NETWORK_SUCCESS,
message: 'Network change listener added',
data: null,
};
}),
};

jest.mock('../providers', () => ({
WalletProvider: jest.fn(() => mockWalletProvider),
}));

export const isConnectedSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: '',
data: true,
};

export const isConnectedFailureMock = {
code: 4000,
status: WalletResponseStatus.FAILURE,
type: WalletResponseFailureType.ALREADY_CONNECTED,
message: '',
data: false,
};

export const addEstablishSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: '',
data: null,
};

export const addEstablishFailureMock = {
code: 4000,
status: WalletResponseStatus.FAILURE,
type: WalletResponseFailureType.ALREADY_CONNECTED,
message: '',
data: null,
};

export const getAccountSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.GET_ACCOUNT_SUCCESS,
message: '',
data: { address: '' },
};

export const switchNetworkSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SWITCH_NETWORK_SUCCESS,
message: '',
data: null,
};

export const addNetworkSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.ADD_NETWORK_SUCCESS,
message: '',
data: null,
};

export const signTransactionSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SIGN_SUCCESS,
message: '',
data: '',
};

export const broadcastTransactionSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.TRANSACTION_SUCCESS,
message: '',
data: { result: '' },
};

export const onChangeAccountSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.CONNECTION_SUCCESS,
message: '',
data: null,
};

export const onChangeNetworkSuccessMock = {
code: 0,
status: WalletResponseStatus.SUCCESS,
type: WalletResponseSuccessType.SWITCH_NETWORK_SUCCESS,
message: '',
data: null,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import { clearGlobalMock, defineGlobalMock } from '../../__mocks__/mock-global';
import {
addEstablishFailureMock,
addEstablishSuccessMock,
isConnectedFailureMock,
isConnectedSuccessMock,
mockWalletProvider,
} from '../../__mocks__/mock-wallet-provider';
import { ConnectionManager, ConnectionState } from '../../connection';
import { SDKConnectionConfigure } from '../../types/config.types';

describe('ConnectionManager', () => {
let connectionManager: ConnectionManager;
let config: SDKConnectionConfigure;

beforeEach(() => {
defineGlobalMock();
config = { isSession: true };

// Initialize ConnectionManager with a real instance of ConnectionStateManager
connectionManager = new ConnectionManager(mockWalletProvider, config);
});

afterEach(() => {
jest.clearAllMocks();
clearGlobalMock();
});

test('should initialize and load state if isSession is true', () => {
// Initially, loadState should set the state to CONNECTED if it was not saved
connectionManager = new ConnectionManager(mockWalletProvider, config);
expect(connectionManager.getConnectionState()).toBe(ConnectionState.CONNECTED);
});

test('should set state to CONNECTING when connecting wallet', async () => {
mockWalletProvider.isConnected.mockResolvedValue(isConnectedSuccessMock);
mockWalletProvider.addEstablish.mockResolvedValue(addEstablishSuccessMock);

await connectionManager.connectWallet();

expect(connectionManager.getConnectionState()).toBe(ConnectionState.CONNECTED);
});

test('should connect wallet if already connected', async () => {
mockWalletProvider.isConnected.mockResolvedValue(isConnectedSuccessMock);

await connectionManager.connectWallet();

expect(connectionManager.getConnectionState()).toBe(ConnectionState.CONNECTED);
});

test('should set state to DISCONNECTED if connection fails', async () => {
connectionManager = new ConnectionManager(mockWalletProvider, { isSession: false });
mockWalletProvider.isConnected.mockResolvedValue(isConnectedFailureMock);
mockWalletProvider.addEstablish.mockResolvedValue(addEstablishFailureMock);

await connectionManager.connectWallet();

expect(connectionManager.getConnectionState()).toBe(ConnectionState.DISCONNECTED);
});

test('should set state to ERROR if an exception is thrown', async () => {
mockWalletProvider.isConnected.mockRejectedValueOnce(new Error('connection error'));

await expect(connectionManager.connectWallet()).rejects.toThrow('connection error');
expect(connectionManager.getConnectionState()).toBe(ConnectionState.ERROR);
});

test('should trigger connection event when connected', () => {
const listener = jest.fn();
connectionManager.on(listener);

connectionManager['connect']();

expect(connectionManager.getConnectionState()).toBe(ConnectionState.CONNECTED);
expect(listener).toHaveBeenCalledWith('connect');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import { clearGlobalMock, defineGlobalMock } from '../../__mocks__/mock-global';
import { ConnectionState, ConnectionStateManager } from '../../connection';
import { getSessionStorageItem, setSessionStorageItem } from '../../utils/storage.utils';

jest.mock('../../utils/storage.utils', () => ({
getSessionStorageItem: jest.fn(),
setSessionStorageItem: jest.fn(),
}));

describe('ConnectionStateManager', () => {
let manager: ConnectionStateManager;

beforeEach(() => {
defineGlobalMock();
manager = new ConnectionStateManager();
});

afterEach(() => {
jest.clearAllMocks();
clearGlobalMock();
});

it('should initialize with DISCONNECTED state', () => {
expect(manager.getState()).toBe(ConnectionState.DISCONNECTED);
});

it('should save state to session storage when setState is called', () => {
manager.setState(ConnectionState.CONNECTED);

expect(setSessionStorageItem).toHaveBeenCalledWith(
'adena-sdk-connection-state',
ConnectionState.CONNECTED.toString()
);
expect(manager.getState()).toBe(ConnectionState.CONNECTED);
});

it('should load state from session storage when loadState is called', () => {
(getSessionStorageItem as jest.Mock).mockReturnValue(ConnectionState.CONNECTED.toString());

manager.loadState();

expect(getSessionStorageItem).toHaveBeenCalledWith('adena-sdk-connection-state');
expect(manager.getState()).toBe(ConnectionState.CONNECTED);
});

it('should not change state if the loaded state is not CONNECTED', () => {
(getSessionStorageItem as jest.Mock).mockReturnValue(ConnectionState.DISCONNECTED.toString());

manager.loadState();

expect(getSessionStorageItem).toHaveBeenCalledWith('adena-sdk-connection-state');
expect(manager.getState()).toBe(ConnectionState.DISCONNECTED);
});
});
Loading

0 comments on commit b9d9957

Please sign in to comment.