diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts index e338c481..80cb9d61 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts @@ -15,6 +15,7 @@ import { CreateNanoContractCreateTokenTxConfirmationPrompt, TriggerResponseTypes, RpcResponseTypes, + TokenVersionString, } from '../../src/types'; import { PromptRejectedError, InvalidParamsError, SendNanoContractTxError } from '../../src/errors'; @@ -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, @@ -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() + ); + }); }); diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts index 87ef5d08..8e3aba17 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts @@ -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, @@ -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' }; @@ -137,7 +166,7 @@ describe('createToken', () => { ...rpcRequest, params: { ...rpcRequest.params, - version: TokenVersion.FEE, + version: TokenVersionString.FEE, }, } as unknown as CreateTokenRpcRequest; @@ -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); @@ -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); + }); }); }); diff --git a/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts b/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts index 408bbd14..a856fc17 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts @@ -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 = { @@ -86,7 +89,8 @@ export async function createToken( allowExternalMeltAuthorityAddress: params.options.allowExternalMeltAuthorityAddress, data: params.options.data, address: params.options.address, - fee + fee, + deposit }, }; diff --git a/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts b/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts index 5d19bbaa..bdae02b9 100644 --- a/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts +++ b/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts @@ -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(), @@ -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), diff --git a/packages/hathor-rpc-handler/src/schemas/index.ts b/packages/hathor-rpc-handler/src/schemas/index.ts index 563bf7e9..3b174479 100644 --- a/packages/hathor-rpc-handler/src/schemas/index.ts +++ b/packages/hathor-rpc-handler/src/schemas/index.ts @@ -12,4 +12,5 @@ export { sendNanoContractTxConfirmationResponseSchema, createNanoContractCreateTokenTxConfirmationDataSchema, createNanoContractCreateTokenTxConfirmationResponseSchema, -} from './nanoContractResponseSchema'; \ No newline at end of file +} from './nanoContractResponseSchema'; +export { TokenVersionString, tokenVersionStringSchema } from './tokenVersionSchema'; \ No newline at end of file diff --git a/packages/hathor-rpc-handler/src/schemas/tokenVersionSchema.ts b/packages/hathor-rpc-handler/src/schemas/tokenVersionSchema.ts new file mode 100644 index 00000000..d7f32a13 --- /dev/null +++ b/packages/hathor-rpc-handler/src/schemas/tokenVersionSchema.ts @@ -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.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]); diff --git a/packages/hathor-rpc-handler/src/types/index.ts b/packages/hathor-rpc-handler/src/types/index.ts index 7ce2a881..d189ab6c 100644 --- a/packages/hathor-rpc-handler/src/types/index.ts +++ b/packages/hathor-rpc-handler/src/types/index.ts @@ -8,3 +8,4 @@ export * from './rpcRequest'; export * from './rpcResponse'; export * from './prompt'; +export { TokenVersionString } from '../schemas'; diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index 4017c734..5a96f668 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -213,6 +213,7 @@ export interface CreateTokenParams { allowExternalMeltAuthorityAddress: boolean, data: string[] | null, fee?: bigint, + deposit?: bigint, } // Extended type for nano contract token creation diff --git a/packages/hathor-rpc-handler/src/types/rpcRequest.ts b/packages/hathor-rpc-handler/src/types/rpcRequest.ts index d16fe84d..cfff5f71 100644 --- a/packages/hathor-rpc-handler/src/types/rpcRequest.ts +++ b/packages/hathor-rpc-handler/src/types/rpcRequest.ts @@ -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', @@ -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;