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
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CreateNanoContractCreateTokenTxConfirmationPrompt,
TriggerResponseTypes,
RpcResponseTypes,
TokenVersionString,
} from '../../src/types';
import { PromptRejectedError, InvalidParamsError, SendNanoContractTxError } from '../../src/errors';

Expand All @@ -37,7 +38,7 @@ describe('createNanoContractCreateTokenTx', () => {
name: 'TestToken',
symbol: 'TT',
amount: '100',
version: 1, // TokenVersion.DEPOSIT - tests that version is forwarded
version: TokenVersionString.DEPOSIT,
mintAddress: 'wallet1',
changeAddress: 'wallet1',
createMint: true,
Expand Down Expand Up @@ -630,4 +631,56 @@ describe('createNanoContractCreateTokenTx', () => {
await expect(createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler))
.rejects.toThrow(SendNanoContractTxError);
});

it('should default to DEPOSIT version when createTokenOptions.version is not provided', async () => {
const pinCode = '1234';

const requestWithoutVersion = {
...rpcRequest,
params: {
...rpcRequest.params,
createTokenOptions: {
...createTokenOptions,
version: undefined,
},
},
};

promptHandler
.mockResolvedValueOnce({
type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse,
data: {
accepted: true,
nano: {
blueprintId: 'blueprint123',
ncId: 'nc123',
actions: nanoActions,
args: [],
method: 'initialize',
pushTx: true,
caller: 'wallet1',
fee: 100n,
parsedArgs: [],
},
token: { ...createTokenOptions, version: undefined },
},
})
.mockResolvedValueOnce({
type: TriggerResponseTypes.PinRequestResponse,
data: { accepted: true, pinCode },
});

await createNanoContractCreateTokenTx(requestWithoutVersion, wallet, {}, promptHandler);

// Verify the pre-build was called with DEPOSIT version (default)
expect(wallet.createNanoContractCreateTokenTransaction).toHaveBeenCalledWith(
requestWithoutVersion.params.method,
requestWithoutVersion.params.address,
expect.anything(),
expect.objectContaining({
version: 1, // TokenVersion.DEPOSIT
}),
expect.anything()
);
});
});
127 changes: 123 additions & 4 deletions packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { type IHathorWallet, tokensUtils } from '@hathor/wallet-lib';
import { TokenVersion } from '@hathor/wallet-lib/lib/models/enum';
import { type IHathorWallet, tokensUtils, TokenVersion } from '@hathor/wallet-lib';
import { FEE_PER_OUTPUT } from '@hathor/wallet-lib/lib/constants';
import { TokenVersionString } from '../../src/types';
import { createToken } from '../../src/rpcMethods/createToken';
import {
TriggerTypes,
Expand Down Expand Up @@ -118,13 +118,42 @@ describe('createToken', () => {
amount: undefined,
name: undefined,
symbol: undefined,
tokenVersion: null,
tokenVersion: TokenVersion.DEPOSIT,
pinCode,
}
);
expect(result).toEqual(rpcResponse);
});

it('should default to DEPOSIT version when version is not provided', async () => {
const pinCode = '1234';
const transaction = { tx_id: 'transaction-id' };

(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);

await createToken(rpcRequest, wallet, {}, triggerHandler);

expect(wallet.createNewToken).toHaveBeenCalledWith(
rpcRequest.params.name,
rpcRequest.params.symbol,
BigInt(rpcRequest.params.amount),
expect.objectContaining({
tokenVersion: TokenVersion.DEPOSIT,
})
);
});

it('should create a token with FEE version and calculate fee correctly', async () => {
const pinCode = '1234';
const transaction = { tx_id: 'transaction-id' };
Expand All @@ -137,7 +166,7 @@ describe('createToken', () => {
...rpcRequest,
params: {
...rpcRequest.params,
version: TokenVersion.FEE,
version: TokenVersionString.FEE,
},
} as unknown as CreateTokenRpcRequest;

Expand Down Expand Up @@ -187,6 +216,66 @@ describe('createToken', () => {
expect(result).toEqual(rpcResponse);
});

it('should create a token with DEPOSIT version and calculate deposit correctly', async () => {
const pinCode = '1234';
const transaction = { tx_id: 'transaction-id' };
const rpcResponse = {
type: RpcResponseTypes.CreateTokenResponse,
response: transaction,
};

const depositVersionRequest = {
...rpcRequest,
params: {
...rpcRequest.params,
amount: '1000',
version: TokenVersionString.DEPOSIT,
},
} as unknown as CreateTokenRpcRequest;

(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(depositVersionRequest, wallet, {}, triggerHandler);

expect(triggerHandler).toHaveBeenCalledWith(
expect.objectContaining({
type: TriggerTypes.CreateTokenConfirmationPrompt,
data: expect.objectContaining({
version: TokenVersion.DEPOSIT,
deposit: 10n,
}),
}),
{}
);

expect(wallet.createNewToken).toHaveBeenCalledWith(
depositVersionRequest.params.name,
depositVersionRequest.params.symbol,
1000n,
expect.objectContaining({
tokenVersion: TokenVersion.DEPOSIT,
pinCode,
})
);
expect(result).toEqual(rpcResponse);
});

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

Expand Down Expand Up @@ -327,6 +416,36 @@ describe('createToken', () => {
await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when version is an invalid string', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'Test Token',
symbol: 'TT',
amount: '100',
version: 'invalid' as unknown,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});

it('should reject when version is an integer', async () => {
const invalidRequest = {
method: RpcMethods.CreateToken,
params: {
name: 'Test Token',
symbol: 'TT',
amount: '100',
version: 1 as unknown,
},
} as CreateTokenRpcRequest;

await expect(createToken(invalidRequest, wallet, {}, triggerHandler))
.rejects.toThrow(InvalidParamsError);
});
});
});

6 changes: 5 additions & 1 deletion packages/hathor-rpc-handler/src/rpcMethods/createToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,10 +63,13 @@ export async function createToken(
};

let fee = tokensUtils.getDataFee(params.options.data?.length ?? 0);
let deposit = 0n;
// This is a particular case where we know that wallet-lib is returning only one output
// so we don't need to prepare the tx to know the fee amount for this tx
if (params.options.tokenVersion === TokenVersion.FEE) {
fee += FEE_PER_OUTPUT;
} else if (params.options.tokenVersion === TokenVersion.DEPOSIT) {
deposit += tokensUtils.getDepositAmount(params.amount);
}

const createTokenPrompt: CreateTokenConfirmationPrompt = {
Expand All @@ -86,7 +89,8 @@ export async function createToken(
allowExternalMeltAuthorityAddress: params.options.allowExternalMeltAuthorityAddress,
data: params.options.data,
address: params.options.address,
fee
fee,
deposit
},
};

Expand Down
6 changes: 3 additions & 3 deletions packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
* LICENSE file in the root directory of this source tree.
*/

import { TokenVersion } from '@hathor/wallet-lib/lib/models/enum';
import { z } from 'zod';
import { tokenVersionStringSchema } from './tokenVersionSchema';

export const createTokenBaseSchema = z.object({
name: z.string().min(1).max(30),
symbol: z.string().min(2).max(5),
amount: z.union([z.string(), z.bigint()]),
version: z.nativeEnum(TokenVersion).optional(),
version: tokenVersionStringSchema,
changeAddress: z.string().nullable().optional(),
createMint: z.boolean().optional(),
mintAuthorityAddress: z.string().nullable().optional(),
Expand All @@ -30,7 +30,7 @@ export const createTokenRpcSchema = z.object({
symbol: z.string().min(2).max(5),
amount: z.string().regex(/^\d+$/)
.pipe(z.coerce.bigint().positive()),
version: z.nativeEnum(TokenVersion).nullish().default(null),
version: tokenVersionStringSchema,
address: z.string().nullish().default(null),
change_address: z.string().nullish().default(null),
create_mint: z.boolean().default(true),
Expand Down
3 changes: 2 additions & 1 deletion packages/hathor-rpc-handler/src/schemas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,4 +12,5 @@ export {
sendNanoContractTxConfirmationResponseSchema,
createNanoContractCreateTokenTxConfirmationDataSchema,
createNanoContractCreateTokenTxConfirmationResponseSchema,
} from './nanoContractResponseSchema';
} from './nanoContractResponseSchema';
export { TokenVersionString, tokenVersionStringSchema } from './tokenVersionSchema';
36 changes: 36 additions & 0 deletions packages/hathor-rpc-handler/src/schemas/tokenVersionSchema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* 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 { TokenVersion } from '@hathor/wallet-lib';
import { z } from 'zod';

/**
* String representation of token versions for RPC API.
* Maps to internal TokenVersion enum values.
*/
export enum TokenVersionString {
DEPOSIT = 'deposit',
FEE = 'fee',
}

/**
* Maps TokenVersionString to internal TokenVersion enum.
*/
const tokenVersionMap: Record<TokenVersionString, TokenVersion> = {
[TokenVersionString.DEPOSIT]: TokenVersion.DEPOSIT,
[TokenVersionString.FEE]: TokenVersion.FEE,
};

/**
* Zod schema that accepts TokenVersionString and transforms to TokenVersion.
* Defaults to DEPOSIT when not provided.
*/
export const tokenVersionStringSchema = z
.nativeEnum(TokenVersionString)
.nullish()
.default(TokenVersionString.DEPOSIT)
.transform(val => tokenVersionMap[val as TokenVersionString]);
1 change: 1 addition & 0 deletions packages/hathor-rpc-handler/src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export * from './rpcRequest';
export * from './rpcResponse';
export * from './prompt';
export { TokenVersionString } from '../schemas';
1 change: 1 addition & 0 deletions packages/hathor-rpc-handler/src/types/prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ export interface CreateTokenParams {
allowExternalMeltAuthorityAddress: boolean,
data: string[] | null,
fee?: bigint,
deposit?: bigint,
}

// Extended type for nano contract token creation
Expand Down
4 changes: 2 additions & 2 deletions packages/hathor-rpc-handler/src/types/rpcRequest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
* LICENSE file in the root directory of this source tree.
*/

import { TokenVersion } from '@hathor/wallet-lib';
import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types';
import { TokenVersionString } from '../schemas';

export enum RpcMethods {
CreateToken = 'htr_createToken',
Expand All @@ -32,7 +32,7 @@ export interface CreateTokenRpcRequest {
name: string;
symbol: string;
amount: string;
version?: TokenVersion
version?: TokenVersionString;
address?: string;
change_address?: string;
create_mint: boolean;
Expand Down
Loading