Skip to content

Commit

Permalink
feat: sign oracle data (#17)
Browse files Browse the repository at this point in the history
* tests: added tests for signOracleData

* refactor: using default exports

* chore: updated metadata

* feat: added helper methods to allow dapps to create rpc requests

* fix: missing method from generic rpc

* refactor: using types for type imports

* chore: changed package name

* fix: broken tests and missing types

* fix: broken CI

* chore: fix lint and missing rpc calls in rpcHandler

* chore: removed version
  • Loading branch information
andreabadesso authored Sep 7, 2024
1 parent 22b37a9 commit 3f1eacd
Show file tree
Hide file tree
Showing 26 changed files with 457 additions and 113 deletions.
7 changes: 3 additions & 4 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,12 @@ 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
uses: cachix/install-nix-action@ba0dd844c9180cbf77aa72a116d6fbc515d0e87b
with:
nix_path: nixpkgs=channel:nixos-unstable
extra_nix_config: |
Expand All @@ -24,8 +23,8 @@ jobs:
- name: lint
run: |
nix develop . -c yarn workspace hathor-rpc-handler run lint
nix develop . -c yarn workspace @hathor/hathor-rpc-handler run lint
- name: tests
run: |
nix develop . -c yarn workspace hathor-rpc-handler run test
nix develop . -c yarn workspace @hathor/hathor-rpc-handler run test
20 changes: 10 additions & 10 deletions packages/hathor-rpc-handler/__tests__/mocks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,11 @@ import {
GetConnectedNetworkRpcRequest,
GetUtxosRpcRequest,
RpcMethods,
SignOracleDataRpcRequest,
SignWithAddressRpcRequest,
} from '../../src/types';

export const mockGetBalanceRequest: GetBalanceRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetBalance,
params: {
network: 'mainnet',
Expand All @@ -19,8 +18,6 @@ export const mockGetBalanceRequest: GetBalanceRpcRequest = {
};

export const mockGetAddressRequest: GetAddressRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetAddress,
params: {
network: 'mainnet',
Expand All @@ -30,8 +27,6 @@ export const mockGetAddressRequest: GetAddressRpcRequest = {
};

export const mockGetUtxosRequest: GetUtxosRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.GetUtxos,
params: {
network: 'mainnet',
Expand All @@ -46,8 +41,6 @@ export const mockGetUtxosRequest: GetUtxosRpcRequest = {
};

export const mockSignWithAddressRequest: SignWithAddressRpcRequest = {
id: '1',
jsonrpc: '2.0',
method: RpcMethods.SignWithAddress,
params: {
network: 'mainnet',
Expand All @@ -56,9 +49,16 @@ export const mockSignWithAddressRequest: SignWithAddressRpcRequest = {
},
};

export const mockSignOracleDataRequest: SignOracleDataRpcRequest = {
method: RpcMethods.SignOracleData,
params: {
network: 'mainnet',
oracle: 'address1',
data: 'Test oracle data',
},
};

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

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ import {
} from '../../src/types';
import { CreateTokenError, PromptRejectedError } from '../../src/errors';

function toCamelCase(params: Pick<CreateTokenRpcRequest, 'params'>['params']) {
return {
name: params.name,
symbol: params.symbol,
changeAddress: params.change_address,
address: params.address,
amount: params.amount,
createMint: params.create_mint,
mintAuthorityAddress: params.mint_authority_address,
allowExternalMintAuthorityAddress: params.allow_external_mint_authority_address,
createMelt: params.create_melt,
meltAuthorityAddress: params.melt_authority_address,
allowExternalMeltAuthorityAddress: params.allow_external_melt_authority_address,
data: params.data,
};
}

describe('createToken', () => {
let rpcRequest: CreateTokenRpcRequest;
let wallet: HathorWallet;
Expand All @@ -17,22 +34,20 @@ describe('createToken', () => {
beforeEach(() => {
rpcRequest = {
method: RpcMethods.CreateToken,
id: '1',
jsonrpc: '2.0',
params: {
name: 'myToken',
name: 'mytoken',
symbol: 'mtk',
amount: 1000,
address: 'address123',
changeAddress: 'changeAddress123',
createMint: true,
mintAuthorityAddress: null,
allowExternalMintAuthorityAddress: false,
createMelt: true,
meltAuthorityAddress: null,
allowExternalMeltAuthorityAddress: false,
change_address: 'changeAddress123',
create_mint: true,
mint_authority_address: null,
allow_external_mint_authority_address: false,
create_melt: true,
melt_authority_address: null,
allow_external_melt_authority_address: false,
data: null,
pushTx: true,
push_tx: true,
network: 'mainnet',
},
} as unknown as CreateTokenRpcRequest;
Expand Down Expand Up @@ -73,12 +88,12 @@ describe('createToken', () => {

const result = await createToken(rpcRequest, wallet, {}, triggerHandler);

expect(triggerHandler).toHaveBeenCalledTimes(2);
expect(triggerHandler).toHaveBeenCalledTimes(4);
expect(triggerHandler).toHaveBeenCalledWith(
{
type: TriggerTypes.CreateTokenConfirmationPrompt,
method: rpcRequest.method,
data: rpcRequest.params,
data: toCamelCase(rpcRequest.params),
},
{}
);
Expand All @@ -95,15 +110,10 @@ describe('createToken', () => {
rpcRequest.params.symbol,
rpcRequest.params.amount,
{
changeAddress: rpcRequest.params.change_address,
address: rpcRequest.params.address,
createMint: rpcRequest.params.create_mint,
mintAuthorityAddress: rpcRequest.params.mint_authority_address,
allowExternalMintAuthorityAddress: rpcRequest.params.allow_external_mint_authority_address,
createMelt: rpcRequest.params.create_melt,
meltAuthorityAddress: rpcRequest.params.melt_authority_address,
allowExternalMeltAuthorityAddress: rpcRequest.params.allow_external_melt_authority_address,
data: rpcRequest.params.data,
...toCamelCase(rpcRequest.params),
amount: undefined,
name: undefined,
symbol: undefined,
pinCode,
}
);
Expand All @@ -127,7 +137,7 @@ describe('createToken', () => {
{
type: TriggerTypes.CreateTokenConfirmationPrompt,
method: rpcRequest.method,
data: rpcRequest.params,
data: toCamelCase(rpcRequest.params),
},
{}
);
Expand Down Expand Up @@ -156,7 +166,7 @@ describe('createToken', () => {

await expect(createToken(rpcRequest, wallet, {}, triggerHandler)).rejects.toThrow(CreateTokenError);

expect(triggerHandler).toHaveBeenCalledTimes(2);
expect(triggerHandler).toHaveBeenCalledTimes(3);
});

it('should throw an error if the change address is not owned by the wallet', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,6 @@ describe('getAddress', () => {

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,
};
Expand All @@ -47,8 +45,6 @@ describe('getAddress', () => {

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,
};
Expand All @@ -58,8 +54,6 @@ describe('getAddress', () => {

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,
};
Expand All @@ -74,8 +68,6 @@ describe('getAddress', () => {

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,
};
Expand All @@ -93,8 +85,6 @@ describe('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,
};
Expand All @@ -106,8 +96,6 @@ describe('getAddress', () => {

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,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ describe('sendNanoContractTx', () => {

const result = await sendNanoContractTx(rpcRequest, wallet, {}, promptHandler);

expect(promptHandler).toHaveBeenCalledTimes(3);
expect(promptHandler).toHaveBeenCalledTimes(4);
expect(promptHandler).toHaveBeenCalledWith({
type: TriggerTypes.PinConfirmationPrompt,
method: rpcRequest.method,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**
* 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, Network, bufferUtils, nanoUtils } from '@hathor/wallet-lib';
import { TriggerTypes, TriggerResponseTypes, RpcResponseTypes } from '../../src/types';
import { mockPromptHandler, mockSignOracleDataRequest } from '../mocks';
import { signOracleData } from '../../src/rpcMethods/signOracleData';
import { PromptRejectedError } from '../../src/errors';

jest.mock('@hathor/wallet-lib', () => ({
...jest.requireActual('@hathor/wallet-lib'),
nanoUtils: {
getOracleBuffer: jest.fn().mockReturnValue(Buffer.from('oracle-data')),
getOracleInputData: jest.fn().mockResolvedValue(Buffer.from('oracle-data')),
},
NanoContractSerializer: jest.fn().mockImplementation(() => ({
serializeFromType: jest.fn(),
})),
}));

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

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

beforeEach(() => {
wallet = {
getNetwork: jest.fn().mockReturnValue('mainnet'),
getNetworkObject: jest.fn().mockReturnValue(new Network('mainnet')),
} as unknown as HathorWallet;
});

it('should throw PromptRejectedError if user rejects the sign oracle data trigger request', async () => {
mockPromptHandler.mockResolvedValueOnce(false);

await expect(signOracleData(mockSignOracleDataRequest, wallet, {}, mockPromptHandler)).rejects.toThrow(PromptRejectedError);

expect(mockPromptHandler).toHaveBeenNthCalledWith(1, {
type: TriggerTypes.SignOracleDataConfirmationPrompt,
method: mockSignOracleDataRequest.method,
data: {
oracle: mockSignOracleDataRequest.params.oracle,
data: mockSignOracleDataRequest.params.data,
},
}, {});

expect(nanoUtils.getOracleBuffer).not.toHaveBeenCalled();
});

it('should throw PromptRejectedError if user rejects the PIN prompt', async () => {
mockPromptHandler
.mockResolvedValueOnce({
type: TriggerResponseTypes.SignOracleDataConfirmationResponse,
data: true,
})
.mockResolvedValueOnce({
type: TriggerResponseTypes.PinRequestResponse,
data: {
accepted: false
},
});

await expect(signOracleData(mockSignOracleDataRequest, wallet, {}, mockPromptHandler)).rejects.toThrow(PromptRejectedError);

expect(mockPromptHandler).toHaveBeenNthCalledWith(1, {
type: TriggerTypes.SignOracleDataConfirmationPrompt,
method: mockSignOracleDataRequest.method,
data: {
oracle: mockSignOracleDataRequest.params.oracle,
data: mockSignOracleDataRequest.params.data,
},
}, {});

expect(mockPromptHandler).toHaveBeenNthCalledWith(2, {
type: TriggerTypes.PinConfirmationPrompt,
method: mockSignOracleDataRequest.method,
}, {});

expect(nanoUtils.getOracleBuffer).not.toHaveBeenCalled();
});

it('should return signed oracle data if user confirms and provides PIN', async () => {
mockPromptHandler
.mockResolvedValueOnce({
type: TriggerResponseTypes.SignOracleDataConfirmationResponse,
data: true,
})
.mockResolvedValueOnce({
type: TriggerResponseTypes.PinRequestResponse,
data: {
accepted: true,
pinCode: 'mock_pin',
},
});

const result = await signOracleData(mockSignOracleDataRequest, wallet, {}, mockPromptHandler);

expect(mockPromptHandler).toHaveBeenNthCalledWith(1, {
type: TriggerTypes.SignOracleDataConfirmationPrompt,
method: mockSignOracleDataRequest.method,
data: {
oracle: mockSignOracleDataRequest.params.oracle,
data: mockSignOracleDataRequest.params.data,
},
}, {});

expect(mockPromptHandler).toHaveBeenNthCalledWith(2, {
type: TriggerTypes.PinConfirmationPrompt,
method: mockSignOracleDataRequest.method,
}, {});

const oracleDataBuf = Buffer.from('oracle-data');
const signature = `${bufferUtils.bufferToHex(oracleDataBuf)},${mockSignOracleDataRequest.params.data},str`;

expect(result).toStrictEqual({
type: RpcResponseTypes.SignOracleDataResponse,
response: {
data: mockSignOracleDataRequest.params.data,
signature,
oracle: mockSignOracleDataRequest.params.oracle,
}
});
});
});
3 changes: 0 additions & 3 deletions packages/hathor-rpc-handler/eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ export default tseslint.config(
eslint.configs.recommended,
...tseslint.configs.recommended,
{
ignores: [
'dist/**/*.js',
],
rules: {
'@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }],
},
Expand Down
Loading

0 comments on commit 3f1eacd

Please sign in to comment.