Skip to content

Commit

Permalink
feat: create token RPC (#15)
Browse files Browse the repository at this point in the history
* feat: added createdToken rpc

* feat: added create token req to rpcHandler

* refactor: improved create token rpc request

* feat: added loading trigger for create token

* feat: sending loading finished trigger on send nano contract and create token

* refactor: using CreateTokenTransaction
  • Loading branch information
andreabadesso authored Aug 26, 2024
1 parent 31b3f85 commit 22b37a9
Show file tree
Hide file tree
Showing 9 changed files with 414 additions and 5 deletions.
170 changes: 170 additions & 0 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
import { HathorWallet } from '@hathor/wallet-lib';
import { createToken } from '../../src/rpcMethods/createToken';
import {
TriggerTypes,
RpcMethods,
CreateTokenRpcRequest,
TriggerResponseTypes,
RpcResponseTypes,
} from '../../src/types';
import { CreateTokenError, PromptRejectedError } from '../../src/errors';

describe('createToken', () => {
let rpcRequest: CreateTokenRpcRequest;
let wallet: HathorWallet;
let triggerHandler = jest.fn();

beforeEach(() => {
rpcRequest = {
method: RpcMethods.CreateToken,
id: '1',
jsonrpc: '2.0',
params: {
name: 'myToken',
symbol: 'mtk',
amount: 1000,
address: 'address123',
changeAddress: 'changeAddress123',
createMint: true,
mintAuthorityAddress: null,
allowExternalMintAuthorityAddress: false,
createMelt: true,
meltAuthorityAddress: null,
allowExternalMeltAuthorityAddress: false,
data: null,
pushTx: true,
network: 'mainnet',
},
} as unknown as CreateTokenRpcRequest;

wallet = {
isAddressMine: jest.fn(),
createNewToken: jest.fn(),
} as unknown as HathorWallet;

triggerHandler = jest.fn();
});

it('should create a token successfully', async () => {
const pinCode = '1234';
const transaction = { tx_id: 'transaction-id' };
const rpcResponse = {
type: RpcResponseTypes.CreateTokenResponse,
response: transaction,
};

(wallet.isAddressMine as jest.Mock).mockResolvedValue(true);
triggerHandler
.mockResolvedValueOnce({
type: TriggerResponseTypes.CreateTokenConfirmationResponse,
data: {
accepted: true,
},
})
.mockResolvedValueOnce({
type: TriggerResponseTypes.PinRequestResponse,
data: {
accepted: true,
pinCode,
},
});

(wallet.createNewToken as jest.Mock).mockResolvedValue(transaction);

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

expect(triggerHandler).toHaveBeenCalledTimes(2);
expect(triggerHandler).toHaveBeenCalledWith(
{
type: TriggerTypes.CreateTokenConfirmationPrompt,
method: rpcRequest.method,
data: rpcRequest.params,
},
{}
);
expect(triggerHandler).toHaveBeenCalledWith(
{
type: TriggerTypes.PinConfirmationPrompt,
method: rpcRequest.method,
},
{}
);

expect(wallet.createNewToken).toHaveBeenCalledWith(
rpcRequest.params.name,
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,
pinCode,
}
);
expect(result).toEqual(rpcResponse);
});

it('should throw PromptRejectedError if the user rejects the confirmation prompt', async () => {
(wallet.isAddressMine as jest.Mock).mockResolvedValue(true);

triggerHandler.mockResolvedValueOnce({
type: TriggerResponseTypes.CreateTokenConfirmationResponse,
data: {
accepted: false,
},
});

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

expect(triggerHandler).toHaveBeenCalledTimes(1);
expect(triggerHandler).toHaveBeenCalledWith(
{
type: TriggerTypes.CreateTokenConfirmationPrompt,
method: rpcRequest.method,
data: rpcRequest.params,
},
{}
);
});

it('should throw CreateTokenError if the wallet transaction fails', async () => {
const pinCode = '1234';

(wallet.isAddressMine as jest.Mock).mockResolvedValue(true);
triggerHandler
.mockResolvedValueOnce({
type: TriggerResponseTypes.CreateTokenConfirmationResponse,
data: {
accepted: true,
},
})
.mockResolvedValueOnce({
type: TriggerResponseTypes.PinRequestResponse,
data: {
accepted: true,
pinCode,
},
});

(wallet.createNewToken as jest.Mock).mockRejectedValue(new Error('Transaction failed'));

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

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

it('should throw an error if the change address is not owned by the wallet', async () => {
(wallet.isAddressMine as jest.Mock).mockResolvedValue(false);

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

expect(wallet.isAddressMine).toHaveBeenCalledWith('changeAddress123');
});
});

2 changes: 1 addition & 1 deletion packages/hathor-rpc-handler/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
"scripts": {
"lint": "eslint .",
"test": "jest",
"build": "tsc",
"build": "tsc --declaration",
"watch": "tsc -w"
},
"author": "André Abadesso",
Expand Down
2 changes: 2 additions & 0 deletions packages/hathor-rpc-handler/src/errors/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export class PromptRejectedError extends Error {};

export class SendNanoContractTxError extends Error {};

export class CreateTokenError extends Error {};

export class InvalidRpcMethod extends Error {};

export class NotImplementedError extends Error {};
Expand Down
8 changes: 8 additions & 0 deletions packages/hathor-rpc-handler/src/rpcHandler/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@ import {
SendNanoContractRpcRequest,
SignWithAddressRpcRequest,
RpcResponse,
CreateTokenRpcRequest,
} from '../types';
import { signWithAddress } from '../rpcMethods/signWithAddress';
import { HathorWallet } from '@hathor/wallet-lib';
import { getAddress, getBalance, getUtxos, sendNanoContractTx } from '../rpcMethods';
import { getConnectedNetwork } from '../rpcMethods/getConnectedNetwork';
import { InvalidRpcMethod } from '../errors';
import { createToken } from '../rpcMethods/createToken';

export const handleRpcRequest = async (
request: RpcRequest,
Expand Down Expand Up @@ -67,6 +69,12 @@ export const handleRpcRequest = async (
requestMetadata,
promptHandler,
);
case RpcMethods.CreateToken: return createToken(
request as CreateTokenRpcRequest,
wallet,
requestMetadata,
promptHandler,
);
default: throw new InvalidRpcMethod();
}
};
144 changes: 144 additions & 0 deletions packages/hathor-rpc-handler/src/rpcMethods/createToken.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/**
* 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, Transaction } from '@hathor/wallet-lib';
import {
CreateTokenConfirmationPrompt,
CreateTokenConfirmationResponse,
CreateTokenLoadingFinishedTrigger,
CreateTokenLoadingTrigger,
CreateTokenRpcRequest,
PinConfirmationPrompt,
PinRequestResponse,
RequestMetadata,
RpcResponse,
RpcResponseTypes,
TriggerHandler,
TriggerTypes,
} from '../types';
import { CreateTokenError, PromptRejectedError } from '../errors';

/**
* Handles the creation of a new token on the Hathor blockchain.
*
* This function orchestrates the entire token creation process, including
* validating addresses, prompting for user confirmation and PIN code, and
* interacting with the wallet library to create the token. It supports
* optional parameters for mint and melt authorities, and allows for
* customization of the token creation process.
*
* @param {CreateTokenRpcRequest} rpcRequest - The RPC request object containing the token details, including
* the token name, symbol, amount, and various options related to minting and melting.
* @param wallet - The wallet instance that will be used to create the token.
* @param requestMetadata - Metadata associated with the request, such as the request ID
* and other contextual information.
* @param triggerHandler - A function that handles triggering user prompts, such as
* confirmations and PIN entry.
*
* @returns An object containing transaction details of the created token.
*/
export async function createToken(
rpcRequest: CreateTokenRpcRequest,
wallet: HathorWallet,
requestMetadata: RequestMetadata,
triggerHandler: TriggerHandler,
) {
const { name, symbol, amount } = rpcRequest.params;
const address = rpcRequest.params.address || null;
const changeAddress = rpcRequest.params.change_address || null;
const createMint = rpcRequest.params.create_mint ?? true;
const mintAuthorityAddress = rpcRequest.params.mint_authority_address || null;
const allowExternalMintAuthorityAddress = rpcRequest.params.allow_external_mint_authority_address || false;
const createMelt = rpcRequest.params.create_melt ?? true;
const meltAuthorityAddress = rpcRequest.params.melt_authority_address || null;
const allowExternalMeltAuthorityAddress = rpcRequest.params.allow_external_melt_authority_address || false;
const data = rpcRequest.params.data || null;

if (changeAddress && !await wallet.isAddressMine(changeAddress)) {
throw new Error('Change address is not from this wallet');
}

const pinPrompt: PinConfirmationPrompt = {
type: TriggerTypes.PinConfirmationPrompt,
method: rpcRequest.method,
};

const createTokenPrompt: CreateTokenConfirmationPrompt = {
type: TriggerTypes.CreateTokenConfirmationPrompt,
method: rpcRequest.method,
data: {
name,
symbol,
amount,
address,
changeAddress,
createMint,
mintAuthorityAddress,
allowExternalMintAuthorityAddress,
createMelt,
meltAuthorityAddress,
allowExternalMeltAuthorityAddress,
data,
},
};

const createTokeResponse = await triggerHandler(createTokenPrompt, requestMetadata) as CreateTokenConfirmationResponse;

if (!createTokeResponse.data.accepted) {
throw new PromptRejectedError();
}

const pinCodeResponse: PinRequestResponse = (await triggerHandler(pinPrompt, requestMetadata)) as PinRequestResponse;

if (!pinCodeResponse.data.accepted) {
throw new PromptRejectedError('Pin prompt rejected');
}

try {
const createTokenLoadingTrigger: CreateTokenLoadingTrigger = {
type: TriggerTypes.CreateTokenLoadingTrigger,
};

// No need to await as this is a fire-and-forget trigger
triggerHandler(createTokenLoadingTrigger, requestMetadata);

const response: Transaction = await wallet.createNewToken(
name,
symbol,
amount,
{
changeAddress,
address,
createMint,
mintAuthorityAddress,
allowExternalMintAuthorityAddress,
createMelt,
meltAuthorityAddress,
allowExternalMeltAuthorityAddress,
data,
pinCode: pinCodeResponse.data.pinCode,
}
);

const createTokenLoadingFinished: CreateTokenLoadingFinishedTrigger = {
type: TriggerTypes.CreateTokenLoadingFinishedTrigger,
};
triggerHandler(createTokenLoadingFinished, requestMetadata);

return {
type: RpcResponseTypes.CreateTokenResponse,
response,
} as RpcResponse;

} catch (err) {
if (err instanceof Error) {
throw new CreateTokenError(err.message);
} else {
throw new CreateTokenError('An unknown error occurred');
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
SendNanoContractTxLoadingTrigger,
RpcResponseTypes,
RpcResponse,
SendNanoContractTxLoadingFinishedTrigger,
} from '../types';
import { PromptRejectedError, SendNanoContractTxError } from '../errors';

Expand Down Expand Up @@ -114,6 +115,11 @@ export async function sendNanoContractTx(
}
);

const sendNanoContractLoadingFinishedTrigger: SendNanoContractTxLoadingFinishedTrigger = {
type: TriggerTypes.SendNanoContractTxLoadingFinishedTrigger,
};
triggerHandler(sendNanoContractLoadingFinishedTrigger, requestMetadata);

return {
type: RpcResponseTypes.SendNanoContractTxResponse,
response,
Expand Down
Loading

0 comments on commit 22b37a9

Please sign in to comment.