Skip to content

Commit

Permalink
feat: rpc implementation [part 2/5] (#2)
Browse files Browse the repository at this point in the history
* chore: added initial yarn + typescript structure

* chore: added hathor-rpc-handler

* chore: update flake inputs

* chore: added github PR templates

* chore: added tsconfig and eslint.config

* chore: opt-out of yarn's PnP

* chore: removed release candidate and release PR templates

* chore: removed gitattributes

* feat: added rpc handler and rpc method initial implementation

* chore: added jest

* feat: implemented getAddress method

* feat: added getUtxos method and improved other methods

* feat: added getUtxos method and tests

* feat: added signWithAddress

* feat: getConnectedNetwork rpc

* feat: sendNanoContractTx and tests

* refactor: better rpc handling

* feat: getAddress accepting all parameters and throwing if invalid network

* feat: passing tests for getAddress

* refactor: organize prompt types

* refactor: getBalance according to RPC document

* tests: most tests passing

* tests: tests passing

* refactor: better prompt response for pin code

* refator: removed unused log

* chore: updated package.json and gitignore

* chore: eslint

* chore: added CI

* refactor: prompt -> trigger

* tests: updated tests to use new methods

* chore: updated wallet-lib to v1.8.0

* refactor: use NATIVE_TOKEN_UID

* refactor: SignMessageFailure -> SignMessageError

* chore: updated actions/checkout to v4 and cachix/install-nix to latest version

* refactor: using getNetwork() instead of getNetworkObject()

* fix: added missing exports

* refactor: using NATIVE_TOKEN_UID

* chore: update magic-nix-cache-action to v7

* refactor: removed sendTx

* feat: sending method to prompt handler in sendNanoContractTx

* refactor: better response for signWithAddress

* fix: unused export

* refactor: pass requestMetadata and promptHandler to GetConnectedNetwork RPC

* refactor: receiving, but ignoring requestMetadata and promptHandler in getConnectedNetwork

* feat: accepting authorities in getUtxos

* docs: added requestMetadata to docstrings

* refactor: removed unused logic to set blueprintId to the ncId

* refactor: SendNanoContractTxFailure -> SendNanoContractTxError

* refactor: tests and invalid await in getNetwork

* feat: normalized rpc responses

* refactor: removed query utxos filters

* fix: added missing types from RpcRequest

* refactor: returning RpcResponse on handleRpcRequest type
  • Loading branch information
andreabadesso authored Aug 8, 2024
1 parent f0d2dd5 commit 31b3f85
Show file tree
Hide file tree
Showing 28 changed files with 6,115 additions and 630 deletions.
31 changes: 31 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
name: CI
on: [push]
jobs:
test:
runs-on: ubuntu-latest
services:
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Install Nix
uses: cachix/install-nix-action/ba0dd844c9180cbf77aa72a116d6fbc515d0e87b
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
experimental-features = nix-command flakes
- name: Cache Nix
uses: DeterminateSystems/magic-nix-cache-action@v7

- name: Install dependencies
run: |
nix develop . -c yarn install
- name: lint
run: |
nix develop . -c yarn workspace hathor-rpc-handler run lint
- name: tests
run: |
nix develop . -c yarn workspace hathor-rpc-handler run test
65 changes: 65 additions & 0 deletions packages/hathor-rpc-handler/__tests__/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { constants } from '@hathor/wallet-lib';
import {
GetAddressRpcRequest,
GetBalanceRpcRequest,
GetConnectedNetworkRpcRequest,
GetUtxosRpcRequest,
RpcMethods,
SignWithAddressRpcRequest,
} from '../../src/types';

export const mockGetBalanceRequest: GetBalanceRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetBalance,
params: {
network: 'mainnet',
tokens: [constants.NATIVE_TOKEN_UID],
},
};

export const mockGetAddressRequest: GetAddressRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetAddress,
params: {
network: 'mainnet',
type: 'index',
index: 1,
}
};

export const mockGetUtxosRequest: GetUtxosRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetUtxos,
params: {
network: 'mainnet',
token: 'mock_token',
maxUtxos: 10,
filterAddress: 'mock_address',
amountSmallerThan: 1000,
amountBiggerThan: 10,
maximumAmount: 10000,
onlyAvailableUtxos: true,
},
};

export const mockSignWithAddressRequest: SignWithAddressRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.SignWithAddress,
params: {
network: 'mainnet',
addressIndex: 0,
message: 'Test message',
},
};

export const mockGetConnectedNetworkRequest: GetConnectedNetworkRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetConnectedNetwork,
};

export const mockPromptHandler = jest.fn();
126 changes: 126 additions & 0 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/getAddress.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { NotImplementedError, PromptRejectedError } from '../../src/errors';
import { getAddress } from '../../src/rpcMethods/getAddress';
import { HathorWallet } from '@hathor/wallet-lib';
import { TriggerTypes, GetAddressRpcRequest, RpcMethods } from '../../src/types';

export const mockPromptHandler = jest.fn();

describe('getAddress', () => {
let promptHandler: jest.Mock;
let mockWallet: jest.Mocked<HathorWallet>;

beforeEach(() => {
promptHandler = jest.fn();
mockWallet = {
getAddressAtIndex: jest.fn().mockReturnValue('mocked_address'),
getCurrentAddress: jest.fn().mockReturnValue({
address: 'address1',
index: 0,
addressPath: `m/44'/280'/0'/0/10`,
}),
getNetwork: jest.fn().mockReturnValue('mainnet')
} as unknown as HathorWallet;
});

it('should return the current address for type "first_empty"', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'first_empty', network: 'mainnet' },
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockReturnValueOnce(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);

expect(address.response).toBe('current-address');
expect(mockWallet.getCurrentAddress).toHaveBeenCalled();
});

it('should throw NotImplementedError for type "full_path"', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'full_path', network: 'mainnet' },
method: RpcMethods.GetAddress,
};

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(NotImplementedError);
});

it('should return the address at index for type "index"', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'index', index: 5, network: 'mainnet' },
method: RpcMethods.GetAddress,
};
mockWallet.getAddressAtIndex.mockResolvedValue('address-at-index');
promptHandler.mockReturnValueOnce(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);

expect(address.response).toBe('address-at-index');
expect(mockWallet.getAddressAtIndex).toHaveBeenCalledWith(5);
});

it('should return the client address for type "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'client', network: 'mainnet' },
method: RpcMethods.GetAddress,
};
const clientPromptResponse = { data: { address: 'client-address' } };
promptHandler.mockResolvedValue(clientPromptResponse);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);

expect(address.response).toBe('client-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestClientPrompt,
method: RpcMethods.GetAddress,
}, {});
});

it('should throw PromptRejectedError if address confirmation is rejected', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'first_empty', network: 'mainnet' },
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValueOnce(false);

await expect(getAddress(rpcRequest, mockWallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);
});

it('should confirm the address if type is not "client"', async () => {
const rpcRequest: GetAddressRpcRequest = {
id: '3',
jsonrpc: '2.0',
params: { type: 'first_empty', network: 'mainnet' },
method: RpcMethods.GetAddress,
};
mockWallet.getCurrentAddress.mockResolvedValue('current-address');
promptHandler.mockResolvedValue(true);

const address = await getAddress(rpcRequest, mockWallet, {}, promptHandler);

expect(address.response).toBe('current-address');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.AddressRequestPrompt,
method: RpcMethods.GetAddress,
data: { address: 'current-address' },
}, {});
});
});
105 changes: 105 additions & 0 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/getBalance.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { GetBalanceObject } from '@hathor/wallet-lib/lib/wallet/types';
import { NotImplementedError, PromptRejectedError } from '../../src/errors';
import { getBalance } from '../../src/rpcMethods/getBalance';
import { HathorWallet } from '@hathor/wallet-lib';
import { TriggerTypes, GetBalanceRpcRequest, RpcMethods } from '../../src/types';

const mockedTokenBalance: GetBalanceObject[] = [{
token: {
id: 'moon-id',
name: 'MOON TOKEN',
symbol: 'MOON',
},
balance: {
unlocked: 0,
locked: 0,
},
tokenAuthorities: {
unlocked: {
mint: false,
melt: false,
},
locked: {
mint: false,
melt: false,
}
},
transactions: 0,
lockExpires: null,
}];

const BaseRpcCall = {
jsonrpc: '2.0',
id: '3',
};

describe('getBalance', () => {
let wallet: jest.Mocked<HathorWallet>;
let promptHandler: jest.Mock;

beforeEach(() => {
wallet = {
getBalance: jest.fn().mockReturnValue(Promise.resolve(mockedTokenBalance)),
getNetwork: jest.fn().mockReturnValue('mainnet')
} as unknown as HathorWallet;
promptHandler = jest.fn();
});

it('should throw NotImplementedError if addressIndexes are specified', async () => {
const rpcRequest: GetBalanceRpcRequest = {
...BaseRpcCall,
params: {
network: 'mainnet',
tokens: ['token1'],
addressIndexes: [0],
},
method: RpcMethods.GetBalance,
};

await expect(getBalance(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(NotImplementedError);
});

it('should return balances of specified tokens', async () => {
const rpcRequest: GetBalanceRpcRequest = {
...BaseRpcCall,
params: { network: 'mainnet', tokens: ['token1', 'token2'], addressIndexes: undefined },
method: RpcMethods.GetBalance,
};

promptHandler.mockResolvedValue(true);

const balances = await getBalance(rpcRequest, wallet, {}, promptHandler);

expect(balances.response).toEqual([mockedTokenBalance, mockedTokenBalance]);
expect(wallet.getBalance).toHaveBeenCalledWith('token1');
expect(wallet.getBalance).toHaveBeenCalledWith('token2');
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.GetBalanceConfirmationPrompt,
method: RpcMethods.GetBalance,
data: [mockedTokenBalance, mockedTokenBalance],
}, {});
});

it('should throw PromptRejectedError if balance confirmation is rejected', async () => {
const rpcRequest: GetBalanceRpcRequest = {
...BaseRpcCall,
params: {
network: 'mainnet',
tokens: ['token1'],
addressIndexes: undefined,
},
method: RpcMethods.GetBalance,
};
wallet.getBalance.mockResolvedValue({ token: 'token1', balance: 100 });
promptHandler.mockResolvedValue(false);

await expect(getBalance(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Copyright (c) Hathor Labs and its affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/

import { HathorWallet } from "@hathor/wallet-lib";
import { getConnectedNetwork } from "../../src/rpcMethods/getConnectedNetwork";
import { mockGetConnectedNetworkRequest } from "../mocks";

export const mockWallet = {
getNetwork: jest.fn().mockReturnValue('mainnet')
} as unknown as HathorWallet;

describe('getConnectedNetwork', () => {
afterEach(() => {
jest.clearAllMocks();
});

it('should return network information', async () => {
const result = await getConnectedNetwork(
mockGetConnectedNetworkRequest,
mockWallet,
{},
jest.fn(),
);

expect(mockWallet.getNetwork).toHaveBeenCalled();
expect(result.response).toStrictEqual({
network: 'mainnet',
genesisHash: '', // TODO: Update when logic to retrieve genesisHash is implemented
});
});
});
Loading

0 comments on commit 31b3f85

Please sign in to comment.