From 63937cfb3ac9bc8c02fecd93fba8ebea4faa5131 Mon Sep 17 00:00:00 2001 From: raul-oliveira Date: Thu, 12 Jun 2025 10:50:22 -0300 Subject: [PATCH] feat: fee token creation fix: wallet-lib new version props feat: createNanoContactTransaction and createNanoContractCreateTokenTransaction tests: nano contract without sign feat: contract pays fees chore: adds yalc to gitignore review changes: change fee return and signTx method review changes: code rabbit review test: check for preparedTx in the prompt fix: remove yalc references to wallet-lib review changes: change error objetc fix: remove prepared tx from confirmation responses and tests for ensure default values fix: remove duplicated sign call feat: add extra guard to safe caller fix: removes unecessary filter review changes: remove options prop from rpc interface feat: fee for create token tx refact: removed unecessary object spread operator review changes: add caller validation with zod schema review changes: use fixed version review changes: validating entire user response fix: zod schemas to validate the confirmation response --- .gitignore | 3 + .../createNanoContractCreateTokenTx.test.ts | 356 ++++++++++++- .../__tests__/rpcMethods/createToken.test.ts | 67 ++- .../rpcMethods/sendNanoContractTx.test.ts | 471 +++++++++++++++--- packages/hathor-rpc-handler/package.json | 4 +- .../src/rpcHandler/index.ts | 4 +- .../createNanoContractCreateTokenTx.ts | 142 ++++-- .../src/rpcMethods/createToken.ts | 12 +- .../src/rpcMethods/sendNanoContractTx.ts | 108 ++-- .../src/rpcRequest/sendNanoContractTx.ts | 8 +- .../src/schemas/createTokenSchema.ts | 4 + .../hathor-rpc-handler/src/schemas/index.ts | 9 +- .../src/schemas/nanoContractResponseSchema.ts | 63 +++ .../hathor-rpc-handler/src/types/prompt.ts | 17 +- .../src/types/rpcRequest.ts | 20 +- yarn.lock | 20 +- 16 files changed, 1088 insertions(+), 220 deletions(-) create mode 100644 packages/hathor-rpc-handler/src/schemas/nanoContractResponseSchema.ts diff --git a/.gitignore b/.gitignore index 4c45f2e4..d5046309 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,6 @@ coverage/ *.swo .claude playwright-report/ + +.yalc/ +yalc.lock \ No newline at end of file diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts index b1eb726b..e338c481 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/createNanoContractCreateTokenTx.test.ts @@ -12,10 +12,11 @@ import { TriggerTypes, RpcMethods, CreateNanoContractCreateTokenTxRpcRequest, + CreateNanoContractCreateTokenTxConfirmationPrompt, TriggerResponseTypes, RpcResponseTypes, } from '../../src/types'; -import { PromptRejectedError, InvalidParamsError } from '../../src/errors'; +import { PromptRejectedError, InvalidParamsError, SendNanoContractTxError } from '../../src/errors'; describe('createNanoContractCreateTokenTx', () => { let rpcRequest: CreateNanoContractCreateTokenTxRpcRequest; @@ -36,6 +37,7 @@ describe('createNanoContractCreateTokenTx', () => { name: 'TestToken', symbol: 'TT', amount: '100', + version: 1, // TokenVersion.DEPOSIT - tests that version is forwarded mintAddress: 'wallet1', changeAddress: 'wallet1', createMint: true, @@ -55,7 +57,26 @@ describe('createNanoContractCreateTokenTx', () => { args?: unknown[]; }; + // Mock fees returned by pre-build transaction + const mockFees = [{ tokenIndex: 0, amount: 100n }]; + + // Create mock transaction object + const createMockTransaction = (fees = mockFees) => ({ + getFeeHeader: jest.fn().mockReturnValue({ entries: fees }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('mock-tx-hex'), + }); + + // Create mock SendTransaction result + const createMockSendTransactionResult = (mockTransaction: ReturnType) => ({ + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }); + beforeEach(() => { + jest.clearAllMocks(); + rpcRequest = { method: RpcMethods.CreateNanoContractCreateTokenTx, params: { @@ -72,10 +93,19 @@ describe('createNanoContractCreateTokenTx', () => { }, }; + const mockTransaction = createMockTransaction(); + const mockSendTxResult = createMockSendTransactionResult(mockTransaction); + wallet = { - createAndSendNanoContractCreateTokenTransaction: jest.fn(), - createNanoContractCreateTokenTransaction: jest.fn(), - } as Partial as IHathorWallet; + createNanoContractCreateTokenTransaction: jest.fn().mockResolvedValue(mockSendTxResult), + getNanoHeaderSeqnum: jest.fn().mockResolvedValue(1), + getNetworkObject: jest.fn().mockReturnValue({ name: 'testnet' }), + setNanoHeaderCaller: jest.fn().mockResolvedValue(undefined), + signTx: jest.fn().mockResolvedValue(undefined), + storage: { + // Mock storage for signTransaction + }, + } as unknown as IHathorWallet; promptHandler = jest.fn(); }); @@ -101,6 +131,8 @@ describe('createNanoContractCreateTokenTx', () => { method: rpcRequest.params.method, pushTx: true, caller: 'wallet1', + fee: 100n, + parsedArgs: [], }, token: createTokenOptions, }, @@ -113,8 +145,6 @@ describe('createNanoContractCreateTokenTx', () => { }, }); - (wallet.createAndSendNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(response); - const result = await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); expect(promptHandler).toHaveBeenCalledTimes(4); // Confirmation, PIN, Loading, LoadingFinished @@ -142,19 +172,18 @@ describe('createNanoContractCreateTokenTx', () => { }), {} ); - expect(wallet.createAndSendNanoContractCreateTokenTransaction).toHaveBeenCalledWith( + + // Verify the pre-build was called with signTx: false + expect(wallet.createNanoContractCreateTokenTransaction).toHaveBeenCalledWith( rpcRequest.params.method, rpcRequest.params.address, expect.objectContaining({ blueprintId: 'blueprint123', ncId: 'nc123', - actions: nanoActions, - method: rpcRequest.params.method, args: [], - pushTx: true, }), - createTokenOptions, - expect.objectContaining({ pinCode }) + expect.anything(), + expect.objectContaining({ signTx: false }) ); expect(result).toEqual(rpcResponse); }); @@ -175,6 +204,8 @@ describe('createNanoContractCreateTokenTx', () => { method: rpcRequest.params.method, pushTx: false, caller: 'wallet1', + fee: 100n, + parsedArgs: [], }, token: createTokenOptions, }, @@ -186,11 +217,7 @@ describe('createNanoContractCreateTokenTx', () => { pinCode, }, }); - (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue({ - transaction: { - toHex: jest.fn().mockReturnValue('mock-tx-hex'), - }, - }); + const result = await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); expect(promptHandler).toHaveBeenCalledTimes(4); // Confirmation, PIN, Loading, LoadingFinished @@ -206,25 +233,22 @@ describe('createNanoContractCreateTokenTx', () => { }), {} ); - + + // Verify the pre-build was called with signTx: false expect(wallet.createNanoContractCreateTokenTransaction).toHaveBeenCalledWith( rpcRequest.params.method, rpcRequest.params.address, expect.objectContaining({ blueprintId: 'blueprint123', ncId: 'nc123', - actions: nanoActions, - method: rpcRequest.params.method, args: [], - pushTx: false, }), expect.objectContaining({ name: createTokenOptions.name, symbol: createTokenOptions.symbol, - mintAddress: createTokenOptions.mintAddress, contractPaysTokenDeposit: createTokenOptions.contractPaysTokenDeposit, }), - expect.objectContaining({ pinCode }) + expect.objectContaining({ signTx: false }) ); expect(result).toHaveProperty('type', RpcResponseTypes.CreateNanoContractCreateTokenTxResponse); expect(result).toHaveProperty('response', 'mock-tx-hex'); @@ -252,6 +276,8 @@ describe('createNanoContractCreateTokenTx', () => { method: rpcRequest.params.method, pushTx: true, caller: 'wallet1', + fee: 100n, + parsedArgs: [], }, token: createTokenOptions, }, @@ -322,4 +348,286 @@ describe('createNanoContractCreateTokenTx', () => { await expect(createNanoContractCreateTokenTx(invalidTokenRequest, wallet, {}, promptHandler)).rejects.toThrow(InvalidParamsError); }); -}); + + it('should include fees and preparedTx in confirmation prompt', async () => { + const feesForTest = [{ tokenIndex: 0, amount: 200n }]; + const mockTransaction = createMockTransaction(feesForTest); + const mockSendTxResult = createMockSendTransactionResult(mockTransaction); + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + // Capture the prompt data + let capturedPrompt: CreateNanoContractCreateTokenTxConfirmationPrompt | undefined; + promptHandler.mockImplementation((prompt: CreateNanoContractCreateTokenTxConfirmationPrompt) => { + if (prompt.type === TriggerTypes.CreateNanoContractCreateTokenTxConfirmationPrompt) { + capturedPrompt = prompt; + return { + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { accepted: false }, + }; // Reject to end early + } + }); + + await expect(createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError); + + // Verify fees and preparedTx are included in the prompt + expect(capturedPrompt).toBeDefined(); + expect(capturedPrompt!.type).toBe(TriggerTypes.CreateNanoContractCreateTokenTxConfirmationPrompt); + expect(capturedPrompt!.data.nano.fee).toBe(200n); + expect(capturedPrompt!.data.nano.preparedTx).toBe(mockTransaction); + }); + + it('should handle transaction without fee header', async () => { + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue(null), // No fee header + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('mock-tx-hex'), + }; + const mockSendTxResult = { + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }; + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + let capturedPrompt: CreateNanoContractCreateTokenTxConfirmationPrompt | undefined; + promptHandler.mockImplementation((prompt: CreateNanoContractCreateTokenTxConfirmationPrompt) => { + if (prompt.type === TriggerTypes.CreateNanoContractCreateTokenTxConfirmationPrompt) { + capturedPrompt = prompt; + return { + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { accepted: false }, + }; // Reject to end early + } + }); + + await expect(createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(PromptRejectedError); + + // Should default to empty array when no fee header + expect(capturedPrompt!.data.nano.fee).toBe(0n); + }); + + it('should call wallet.signTx with correct parameters', async () => { + const pinCode = '1234'; + const mockTransaction = createMockTransaction(); + const mockSendTxResult = createMockSendTransactionResult(mockTransaction); + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + promptHandler + .mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: (rpcRequest.params.data as NanoData).blueprint_id, + ncId: (rpcRequest.params.data as NanoData).nc_id, + actions: (rpcRequest.params.data as NanoData).actions, + args: (rpcRequest.params.data as NanoData).args, + method: rpcRequest.params.method, + pushTx: true, + caller: 'wallet1', + fee: 100n, + parsedArgs: [], + }, + token: createTokenOptions, + }, + }) + .mockResolvedValueOnce({ + type: TriggerResponseTypes.PinRequestResponse, + data: { + accepted: true, + pinCode, + }, + }); + + await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); + + expect(wallet.signTx).toHaveBeenCalledWith( + mockTransaction, + { pinCode } + ); + }); + + it('should call runFromMining when push_tx is true', async () => { + const pinCode = '1234'; + const mockTransaction = createMockTransaction(); + const mockSendTxResult = createMockSendTransactionResult(mockTransaction); + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + promptHandler + .mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: (rpcRequest.params.data as NanoData).blueprint_id, + ncId: (rpcRequest.params.data as NanoData).nc_id, + actions: (rpcRequest.params.data as NanoData).actions, + args: (rpcRequest.params.data as NanoData).args, + method: rpcRequest.params.method, + pushTx: true, + caller: 'wallet1', + fee: 100n, + parsedArgs: [], + }, + token: createTokenOptions, + }, + }) + .mockResolvedValueOnce({ + type: TriggerResponseTypes.PinRequestResponse, + data: { + accepted: true, + pinCode, + }, + }); + + await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); + + expect(mockSendTxResult.runFromMining).toHaveBeenCalled(); + expect(mockTransaction.toHex).not.toHaveBeenCalled(); + }); + + it('should call toHex when push_tx is false', async () => { + rpcRequest.params.push_tx = false; + const pinCode = '1234'; + const mockTransaction = createMockTransaction(); + const mockSendTxResult = createMockSendTransactionResult(mockTransaction); + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + promptHandler + .mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: (rpcRequest.params.data as NanoData).blueprint_id, + ncId: (rpcRequest.params.data as NanoData).nc_id, + actions: (rpcRequest.params.data as NanoData).actions, + args: (rpcRequest.params.data as NanoData).args, + method: rpcRequest.params.method, + pushTx: false, + caller: 'wallet1', + fee: 100n, + parsedArgs: [], + }, + token: createTokenOptions, + }, + }) + .mockResolvedValueOnce({ + type: TriggerResponseTypes.PinRequestResponse, + data: { + accepted: true, + pinCode, + }, + }); + + await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); + + expect(mockTransaction.toHex).toHaveBeenCalled(); + expect(mockSendTxResult.runFromMining).not.toHaveBeenCalled(); + }); + + it('should update caller when changed in confirmation', async () => { + const pinCode = '1234'; + const newCaller = 'wallet2'; + + // Create a mock transaction with trackable nanoHeaders + const mockNanoHeaders = [{ address: null, seqnum: 0 }]; + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: mockFees }), + getNanoHeaders: jest.fn().mockReturnValue(mockNanoHeaders), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('mock-tx-hex'), + }; + const mockSendTxResult = { + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }; + + (wallet.createNanoContractCreateTokenTransaction as jest.Mock).mockResolvedValue(mockSendTxResult); + + promptHandler + .mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: 'blueprint123', + ncId: 'nc123', + actions: nanoActions, + args: [], + method: 'initialize', + pushTx: true, + caller: newCaller, // Different from original address 'wallet1' + parsedArgs: [], + fee: 100n, + }, + token: createTokenOptions, + }, + }) + .mockResolvedValueOnce({ + type: TriggerResponseTypes.PinRequestResponse, + data: { accepted: true, pinCode }, + }); + + await createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler); + + // Verify getNanoHeaders was called to get the headers to update + expect(mockTransaction.getNanoHeaders).toHaveBeenCalled(); + + // Verify setNanoHeaderCaller was called with the header and new caller + expect(wallet.setNanoHeaderCaller).toHaveBeenCalledWith(mockNanoHeaders[0], newCaller); + }); + + it('should throw SendNanoContractTxError when caller is missing in confirmation response', async () => { + promptHandler.mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: 'blueprint123', + ncId: 'nc123', + actions: nanoActions, + args: [], + method: 'initialize', + pushTx: true, + // caller is missing + parsedArgs: [], + fee: 100n, + }, + token: createTokenOptions, + }, + }); + + await expect(createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler)) + .rejects.toThrow(SendNanoContractTxError); + }); + + it('should throw SendNanoContractTxError when caller is empty string in confirmation response', async () => { + promptHandler.mockResolvedValueOnce({ + type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse, + data: { + accepted: true, + nano: { + blueprintId: 'blueprint123', + ncId: 'nc123', + actions: nanoActions, + args: [], + method: 'initialize', + pushTx: true, + caller: '', // Empty string + parsedArgs: [], + fee: 100n, + }, + token: createTokenOptions, + }, + }); + + await expect(createNanoContractCreateTokenTx(rpcRequest, wallet, {}, promptHandler)) + .rejects.toThrow(SendNanoContractTxError); + }); +}); diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts index 4e03cabb..87ef5d08 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/createToken.test.ts @@ -1,4 +1,6 @@ -import type { IHathorWallet } from '@hathor/wallet-lib'; +import { type IHathorWallet, tokensUtils } from '@hathor/wallet-lib'; +import { TokenVersion } from '@hathor/wallet-lib/lib/models/enum'; +import { FEE_PER_OUTPUT } from '@hathor/wallet-lib/lib/constants'; import { createToken } from '../../src/rpcMethods/createToken'; import { TriggerTypes, @@ -116,12 +118,75 @@ describe('createToken', () => { amount: undefined, name: undefined, symbol: undefined, + tokenVersion: null, pinCode, } ); expect(result).toEqual(rpcResponse); }); + it('should create a token with FEE version and calculate fee correctly', async () => { + const pinCode = '1234'; + const transaction = { tx_id: 'transaction-id' }; + const rpcResponse = { + type: RpcResponseTypes.CreateTokenResponse, + response: transaction, + }; + + const feeVersionRequest = { + ...rpcRequest, + params: { + ...rpcRequest.params, + version: TokenVersion.FEE, + }, + } 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(feeVersionRequest, wallet, {}, triggerHandler); + + const expectedDataFee = tokensUtils.getDataFee(rpcRequest.params.data?.length ?? 0); + const expectedFee = FEE_PER_OUTPUT + expectedDataFee; + + expect(triggerHandler).toHaveBeenCalledWith( + expect.objectContaining({ + type: TriggerTypes.CreateTokenConfirmationPrompt, + data: expect.objectContaining({ + version: TokenVersion.FEE, + fee: expectedFee, + }), + }), + {} + ); + + expect(wallet.createNewToken).toHaveBeenCalledWith( + feeVersionRequest.params.name, + feeVersionRequest.params.symbol, + BigInt(feeVersionRequest.params.amount), + expect.objectContaining({ + tokenVersion: TokenVersion.FEE, + pinCode, + }) + ); + expect(result).toEqual(rpcResponse); + }); + it('should throw PromptRejectedError if the user rejects the confirmation prompt', async () => { (wallet.isAddressMine as jest.Mock).mockResolvedValue(true); diff --git a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts index a6c056e6..f77010ad 100644 --- a/packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts +++ b/packages/hathor-rpc-handler/__tests__/rpcMethods/sendNanoContractTx.test.ts @@ -8,9 +8,20 @@ import { nanoUtils, ncApi, type IHathorWallet } from '@hathor/wallet-lib'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; import { sendNanoContractTx, NanoContractActionWithStringAmount } from '../../src/rpcMethods/sendNanoContractTx'; -import { TriggerTypes, RpcMethods, SendNanoContractRpcRequest, TriggerResponseTypes, RpcResponseTypes } from '../../src/types'; +import { TriggerTypes, RpcMethods, SendNanoContractRpcRequest, SendNanoContractTxConfirmationPrompt, TriggerResponseTypes, RpcResponseTypes } from '../../src/types'; import { SendNanoContractTxError, InvalidParamsError } from '../../src/errors'; +// Mock transactionUtils.signTransaction +jest.mock('@hathor/wallet-lib', () => { + const actual = jest.requireActual('@hathor/wallet-lib'); + return { + ...actual, + transactionUtils: { + ...actual.transactionUtils, + signTransaction: jest.fn().mockResolvedValue(undefined), + }, + }; +}); jest.spyOn(nanoUtils, 'validateAndParseBlueprintMethodArgs').mockResolvedValue([]); jest.spyOn(nanoUtils, 'getBlueprintId').mockResolvedValue('test-blueprint'); @@ -53,9 +64,19 @@ describe('sendNanoContractTx', () => { } } as SendNanoContractRpcRequest; + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('tx-hex'), + }; + wallet = { createAndSendNanoContractTransaction: jest.fn(), - createNanoContractTransaction: jest.fn(), + createNanoContractTransaction: jest.fn().mockResolvedValue({ + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }), getServerUrl: jest.fn(), getTokenDetails: jest.fn().mockResolvedValue({ tokenInfo: { @@ -64,7 +85,13 @@ describe('sendNanoContractTx', () => { uid: 'test-token-uid', }, }), - } as Partial as IHathorWallet; + getAddressAtIndex: jest.fn().mockResolvedValue('temp-address'), + getNanoHeaderSeqnum: jest.fn().mockResolvedValue(1), + getNetworkObject: jest.fn().mockReturnValue({ name: 'mainnet' }), + setNanoHeaderCaller: jest.fn().mockResolvedValue(undefined), + signTx: jest.fn().mockResolvedValue(undefined), + storage: {}, + } as unknown as IHathorWallet; promptHandler = jest.fn(); }); @@ -72,17 +99,20 @@ describe('sendNanoContractTx', () => { it('should send a nano contract transaction successfully', async () => { const pinCode = '1234'; const address = 'address123'; - const response = { - id: 'mock-id', - method: 'mock-method', - args: [], - pubkey: Buffer.from('pubkey'), - signature: Buffer.from('signature'), + const response = { tx_id: 'mock-tx-id' }; + + // Create mock transaction with all required methods + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('tx-hex'), }; - const rpcResponse = { - type: RpcResponseTypes.SendNanoContractTxResponse, - response, + const mockSendTx = { + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue(response), }; + (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); // Expected action after transformation const expectedActions = [ @@ -99,10 +129,14 @@ describe('sendNanoContractTx', () => { accepted: true, nc: { caller: address, + method: rpcRequest.params.method, blueprintId: rpcRequest.params.blueprint_id, ncId: rpcRequest.params.nc_id, args: rpcRequest.params.args, - actions: expectedActions, // Using the transformed actions + parsedArgs: [], + actions: expectedActions, + pushTx: true, + fee: 0n, }, } }) @@ -114,8 +148,6 @@ describe('sendNanoContractTx', () => { } }); - (wallet.createAndSendNanoContractTransaction as jest.Mock).mockResolvedValue(response); - const result = await sendNanoContractTx(rpcRequest, wallet, {}, promptHandler); expect(promptHandler).toHaveBeenCalledTimes(4); @@ -124,18 +156,23 @@ describe('sendNanoContractTx', () => { type: TriggerTypes.PinConfirmationPrompt, }, {}); - expect(wallet.createAndSendNanoContractTransaction).toHaveBeenCalledWith( + // Verify pre-build was called with signTx: false + expect(wallet.createNanoContractTransaction).toHaveBeenCalledWith( rpcRequest.params.method, - address, - { + 'temp-address', // Uses temp address for pre-build + expect.objectContaining({ blueprintId: rpcRequest.params.blueprint_id, - actions: expectedActions, // Using the transformed actions - args: rpcRequest.params.args, ncId: rpcRequest.params.nc_id, - }, - { pinCode } + }), + expect.objectContaining({ signTx: false }) ); - expect(result).toEqual(rpcResponse); + + // Verify transaction was signed and sent + expect(mockSendTx.runFromMining).toHaveBeenCalled(); + expect(result).toEqual({ + type: RpcResponseTypes.SendNanoContractTxResponse, + response, + }); }); it('should transform string amounts to BigInt in actions', async () => { @@ -175,6 +212,19 @@ describe('sendNanoContractTx', () => { }, ]; + // Create mock transaction with all required methods + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('tx-hex'), + }; + const mockSendTx = { + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }; + (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); + promptHandler .mockResolvedValueOnce({ type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, @@ -182,10 +232,14 @@ describe('sendNanoContractTx', () => { accepted: true, nc: { caller: 'address123', + method: requestWithStringAmount.params.method, blueprintId: requestWithStringAmount.params.blueprint_id, ncId: requestWithStringAmount.params.nc_id, args: requestWithStringAmount.params.args, + parsedArgs: [], actions: expectedActions, // Using transformed actions + pushTx: true, + fee: 0n, }, } }) @@ -197,18 +251,16 @@ describe('sendNanoContractTx', () => { } }); - (wallet.createAndSendNanoContractTransaction as jest.Mock).mockResolvedValue({}); - await sendNanoContractTx(requestWithStringAmount, wallet, {}, promptHandler); - // Verify the wallet was called with the right parameters (including transformed actions) - expect(wallet.createAndSendNanoContractTransaction).toHaveBeenCalledWith( + // Verify the pre-build was called (the actions are transformed before prompt) + expect(wallet.createNanoContractTransaction).toHaveBeenCalledWith( requestWithStringAmount.params.method, - 'address123', + 'temp-address', expect.objectContaining({ actions: expectedActions, // Expect BigInt conversions }), - expect.anything() + expect.objectContaining({ signTx: false }) ); }); @@ -240,6 +292,19 @@ describe('sendNanoContractTx', () => { } ]; + // Create mock transaction with all required methods + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('tx-hex'), + }; + const mockSendTx = { + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + }; + (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue(mockSendTx); + promptHandler .mockResolvedValueOnce({ type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, @@ -247,10 +312,14 @@ describe('sendNanoContractTx', () => { accepted: true, nc: { caller: 'address123', + method: requestWithLargeAmount.params.method, blueprintId: requestWithLargeAmount.params.blueprint_id, ncId: requestWithLargeAmount.params.nc_id, args: requestWithLargeAmount.params.args, + parsedArgs: [], actions: expectedActions, // Using transformed actions + pushTx: true, + fee: 0n, }, } }) @@ -262,35 +331,48 @@ describe('sendNanoContractTx', () => { } }); - (wallet.createAndSendNanoContractTransaction as jest.Mock).mockResolvedValue({}); - await sendNanoContractTx(requestWithLargeAmount, wallet, {}, promptHandler); - // Verify the wallet was called with the correct parameters - expect(wallet.createAndSendNanoContractTransaction).toHaveBeenCalledWith( + // Verify the pre-build was called with large BigInt amounts + expect(wallet.createNanoContractTransaction).toHaveBeenCalledWith( requestWithLargeAmount.params.method, - 'address123', + 'temp-address', expect.objectContaining({ actions: expectedActions, // Expect the large BigInt conversion }), - expect.anything() + expect.objectContaining({ signTx: false }) ); }); it('should throw SendNanoContractTxFailure if the transaction fails', async () => { const pinCode = '1234'; const originalAction = rpcRequest.params.actions[0] as unknown as NanoContractActionWithStringAmount; - + const ncData = { method: 'initialize', blueprintId: rpcRequest.params.blueprint_id, ncId: rpcRequest.params.nc_id, args: rpcRequest.params.args, + parsedArgs: [], actions: [{ ...originalAction, amount: 100n, // Convert amount to BigInt }], + pushTx: true, + fee: 0n, + }; + + // Mock createNanoContractTransaction to return a result that throws on runFromMining + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + toHex: jest.fn().mockReturnValue('tx-hex'), }; + (wallet.createNanoContractTransaction as jest.Mock).mockResolvedValue({ + transaction: mockTransaction, + runFromMining: jest.fn().mockRejectedValue(new Error('Transaction failed')), + }); promptHandler .mockResolvedValueOnce({ @@ -299,7 +381,7 @@ describe('sendNanoContractTx', () => { accepted: true, nc: { ...ncData, - address: 'address123', + caller: 'address123', } } }) @@ -310,25 +392,20 @@ describe('sendNanoContractTx', () => { pinCode, } }); - (wallet.createAndSendNanoContractTransaction as jest.Mock).mockRejectedValue(new Error('Transaction failed')); await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(SendNanoContractTxError); expect(promptHandler).toHaveBeenCalledTimes(3); - expect(promptHandler).toHaveBeenNthCalledWith(1, { - ...rpcRequest, - type: TriggerTypes.SendNanoContractTxConfirmationPrompt, - data: { - actions: expect.any(Array), - args: expect.any(Array), - parsedArgs: expect.any(Array), - blueprintId: expect.any(String), - method: expect.any(String), - ncId: expect.any(String), - pushTx: expect.any(Boolean), - tokenDetails: expect.any(Map), - }, - }, {}); + expect(promptHandler).toHaveBeenNthCalledWith(1, + expect.objectContaining({ + type: TriggerTypes.SendNanoContractTxConfirmationPrompt, + data: expect.objectContaining({ + fee: 0n, + contractPaysFees: false, + }), + }), + {} + ); expect(promptHandler).toHaveBeenNthCalledWith(2, { ...rpcRequest, type: TriggerTypes.PinConfirmationPrompt, @@ -339,30 +416,164 @@ describe('sendNanoContractTx', () => { }); }); +describe('fee pre-calculation', () => { + let wallet: IHathorWallet; + let promptHandler: jest.Mock; + + beforeEach(() => { + jest.clearAllMocks(); + promptHandler = jest.fn(); + }); + + it('should include fees in confirmation prompt', async () => { + const mockFees = [{ tokenIndex: 0, amount: 100n }]; + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: mockFees }), + toHex: jest.fn().mockReturnValue('tx-hex'), + }; + + wallet = { + createAndSendNanoContractTransaction: jest.fn(), + createNanoContractTransaction: jest.fn().mockResolvedValue({ + transaction: mockTransaction, + }), + getServerUrl: jest.fn(), + getTokenDetails: jest.fn().mockResolvedValue({ + tokenInfo: { name: 'Test Token', symbol: 'TST', uid: 'test-token-uid' }, + }), + getAddressAtIndex: jest.fn().mockResolvedValue('temp-address'), + } as Partial as IHathorWallet; + + let capturedPrompt: SendNanoContractTxConfirmationPrompt | undefined; + promptHandler.mockImplementation((prompt: SendNanoContractTxConfirmationPrompt) => { + if (prompt.type === TriggerTypes.SendNanoContractTxConfirmationPrompt) { + capturedPrompt = prompt; + return { data: { accepted: false } }; // Reject to end early + } + }); + + const rpcRequest = { + method: RpcMethods.SendNanoContractTx, + id: '1', + jsonrpc: '2.0', + params: { + network: 'mainnet', + method: 'initialize', + blueprint_id: 'blueprint123', + nc_id: 'nc123', + actions: [{ type: 'deposit', address: 'test-address', token: '00', amount: '100' }] as unknown as NanoContractAction[], + args: [], + push_tx: true, + } + } as SendNanoContractRpcRequest; + + await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); + + expect(capturedPrompt!.data.fee).toBe(100n); + expect(wallet.createNanoContractTransaction).toHaveBeenCalledWith( + expect.anything(), + 'temp-address', + expect.anything(), + expect.objectContaining({ signTx: false }) + ); + }); + + it('should pre-build with temporary caller and signTx=false', async () => { + const mockTransaction = { + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + toHex: jest.fn().mockReturnValue('tx-hex'), + }; + + wallet = { + createAndSendNanoContractTransaction: jest.fn(), + createNanoContractTransaction: jest.fn().mockResolvedValue({ + transaction: mockTransaction, + }), + getServerUrl: jest.fn(), + getTokenDetails: jest.fn().mockResolvedValue({ + tokenInfo: { name: 'Test Token', symbol: 'TST', uid: 'test-token-uid' }, + }), + getAddressAtIndex: jest.fn().mockResolvedValue('temp-caller-address'), + } as Partial as IHathorWallet; + + promptHandler.mockResolvedValueOnce({ + type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, + data: { accepted: false }, + }); + + const rpcRequest = { + method: RpcMethods.SendNanoContractTx, + id: '1', + jsonrpc: '2.0', + params: { + network: 'mainnet', + method: 'initialize', + blueprint_id: 'blueprint123', + nc_id: null, + actions: [] as NanoContractAction[], + args: [], + push_tx: true, + } + } as SendNanoContractRpcRequest; + + await expect(sendNanoContractTx(rpcRequest, wallet, {}, promptHandler)).rejects.toThrow(); + + // Verify pre-build was called with temp caller and signTx: false + expect(wallet.getAddressAtIndex).toHaveBeenCalledWith(0); + expect(wallet.createNanoContractTransaction).toHaveBeenCalledWith( + 'initialize', + 'temp-caller-address', + expect.objectContaining({ + ncId: null, + blueprintId: 'blueprint123', + }), + expect.objectContaining({ + signTx: false, + }) + ); + }); +}); + describe('sendNanoContractTx parameter validation', () => { - const mockWallet = { - createAndSendNanoContractTransaction: jest.fn(), - createNanoContractTransaction: jest.fn().mockImplementation(() => ({ - transaction: { - toHex: jest.fn().mockReturnValue('tx-hex'), - }, - })), - getServerUrl: jest.fn(), - getFullTxById: jest.fn().mockImplementation(() => ({ - tx: { - nc_id: 'nc-id' - }, - })), - } as Partial as IHathorWallet; + const createMockWallet = () => { + const mockTransaction = { + toHex: jest.fn().mockReturnValue('tx-hex'), + getFeeHeader: jest.fn().mockReturnValue({ entries: [] }), + getNanoHeaders: jest.fn().mockReturnValue([{ address: null, seqnum: 0 }]), + prepareToSend: jest.fn(), + }; + return { + createAndSendNanoContractTransaction: jest.fn(), + createNanoContractTransaction: jest.fn().mockImplementation(() => ({ + transaction: mockTransaction, + runFromMining: jest.fn().mockResolvedValue({ tx_id: 'mock-tx-id' }), + })), + getServerUrl: jest.fn(), + getFullTxById: jest.fn().mockImplementation(() => ({ + tx: { + nc_id: 'nc-id' + }, + })), + getAddressAtIndex: jest.fn().mockResolvedValue('temp-address'), + getNanoHeaderSeqnum: jest.fn().mockResolvedValue(1), + getNetworkObject: jest.fn().mockReturnValue({ name: 'mainnet' }), + setNanoHeaderCaller: jest.fn().mockResolvedValue(undefined), + signTx: jest.fn().mockResolvedValue(undefined), + storage: {}, + } as unknown as IHathorWallet; + }; + + let mockWallet: IHathorWallet; let mockTriggerHandler: jest.Mock; beforeEach(() => { jest.clearAllMocks(); + mockWallet = createMockWallet(); mockTriggerHandler = jest.fn() .mockResolvedValueOnce({ type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, - data: { + data: { accepted: true, nc: { caller: 'test-caller', @@ -370,8 +581,10 @@ describe('sendNanoContractTx parameter validation', () => { ncId: null, actions: [] as NanoContractAction[], args: [] as unknown[], + parsedArgs: [] as unknown[], method: 'test-method', pushTx: true, + fee: 0n, }, } }) @@ -528,8 +741,10 @@ describe('sendNanoContractTx parameter validation', () => { ncId: null, actions: [] as NanoContractAction[], args: [] as unknown[], + parsedArgs: [] as unknown[], method: 'test-method', pushTx: true, + fee: 0n, }, } }) @@ -563,20 +778,17 @@ describe('sendNanoContractTx parameter validation', () => { } as SendNanoContractRpcRequest; await sendNanoContractTx(validRequest, mockWallet, {}, promptHandler); - expect(promptHandler).toHaveBeenCalledWith({ - ...validRequest, - type: TriggerTypes.SendNanoContractTxConfirmationPrompt, - data: { - actions: expect.any(Array), - args: expect.any(Array), - parsedArgs: expect.any(Array), - blueprintId: 'test-blueprint', // make sure we added the blueprint id in the data object - method: expect.any(String), - ncId: expect.any(String), - pushTx: expect.any(Boolean), - tokenDetails: expect.any(Map), - } - }, {}); + expect(promptHandler).toHaveBeenCalledWith( + expect.objectContaining({ + type: TriggerTypes.SendNanoContractTxConfirmationPrompt, + data: expect.objectContaining({ + blueprintId: 'test-blueprint', // make sure we added the blueprint id in the data object + fee: 0n, + contractPaysFees: false, + }), + }), + {} + ); expect(nanoUtils.getBlueprintId).toHaveBeenCalled(); }); @@ -600,12 +812,13 @@ describe('sendNanoContractTx parameter validation', () => { nc_id: null, actions: validActions as unknown as NanoContractAction[], args: [] as unknown[], - push_tx: true, + // push_tx intentionally omitted to test default value (true) }, } as SendNanoContractRpcRequest; await sendNanoContractTx(validRequest, mockWallet, {}, mockTriggerHandler); - expect(mockWallet.createAndSendNanoContractTransaction).toHaveBeenCalled(); + // Implementation now uses createNanoContractTransaction with signTx: false then runFromMining + expect(mockWallet.createNanoContractTransaction).toHaveBeenCalled(); }); it('should call createNanoContractTransaction when push_tx is false', async () => { @@ -665,4 +878,96 @@ describe('sendNanoContractTx parameter validation', () => { response: 'tx-hex', }); }); + + it('should throw SendNanoContractTxError when caller is missing in confirmation response', async () => { + const validActions = [ + { + type: 'deposit', + address: 'test-address', + token: '00', + amount: '100', + } as NanoContractActionWithStringAmount + ]; + + const validRequest = { + method: RpcMethods.SendNanoContractTx, + params: { + network: 'mainnet', + method: 'test-method', + blueprint_id: 'test-blueprint', + nc_id: null, + actions: validActions as unknown as NanoContractAction[], + args: [] as unknown[], + push_tx: true, + }, + } as SendNanoContractRpcRequest; + + const promptHandler = jest.fn().mockResolvedValueOnce({ + type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, + data: { + accepted: true, + nc: { + // caller is missing + blueprintId: 'test-blueprint', + ncId: null, + actions: [] as NanoContractAction[], + args: [] as unknown[], + parsedArgs: [] as unknown[], + method: 'test-method', + pushTx: true, + fee: 0n, + }, + } + }); + + await expect( + sendNanoContractTx(validRequest, mockWallet, {}, promptHandler) + ).rejects.toThrow(SendNanoContractTxError); + }); + + it('should throw SendNanoContractTxError when caller is empty string in confirmation response', async () => { + const validActions = [ + { + type: 'deposit', + address: 'test-address', + token: '00', + amount: '100', + } as NanoContractActionWithStringAmount + ]; + + const validRequest = { + method: RpcMethods.SendNanoContractTx, + params: { + network: 'mainnet', + method: 'test-method', + blueprint_id: 'test-blueprint', + nc_id: null, + actions: validActions as unknown as NanoContractAction[], + args: [] as unknown[], + push_tx: true, + }, + } as SendNanoContractRpcRequest; + + const promptHandler = jest.fn().mockResolvedValueOnce({ + type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse, + data: { + accepted: true, + nc: { + caller: '', // Empty string + blueprintId: 'test-blueprint', + ncId: null, + actions: [] as NanoContractAction[], + args: [] as unknown[], + parsedArgs: [] as unknown[], + method: 'test-method', + pushTx: true, + fee: 0n, + }, + } + }); + + await expect( + sendNanoContractTx(validRequest, mockWallet, {}, promptHandler) + ).rejects.toThrow(SendNanoContractTxError); + }); }); diff --git a/packages/hathor-rpc-handler/package.json b/packages/hathor-rpc-handler/package.json index d5af075b..58d2cbc8 100644 --- a/packages/hathor-rpc-handler/package.json +++ b/packages/hathor-rpc-handler/package.json @@ -9,7 +9,7 @@ "src" ], "engines": { - "node": ">=20" + "node": ">=22" }, "scripts": { "lint": "eslint . --ignore-pattern 'dist/*'", @@ -22,7 +22,7 @@ "@eslint/js": "9.4.0", "@types/eslint__js": "8.42.3", "@types/jest": "29.5.12", - "@types/node": "20.14.2", + "@types/node": "22.0.0", "eslint": "9.4.0", "jest": "29.7.0", "ts-jest": "29.1.4", diff --git a/packages/hathor-rpc-handler/src/rpcHandler/index.ts b/packages/hathor-rpc-handler/src/rpcHandler/index.ts index cc3a2c49..56d92550 100644 --- a/packages/hathor-rpc-handler/src/rpcHandler/index.ts +++ b/packages/hathor-rpc-handler/src/rpcHandler/index.ts @@ -29,9 +29,9 @@ import { import { getAddress, getBalance, + getConnectedNetwork, getUtxos, sendNanoContractTx, - getConnectedNetwork, signOracleData, signWithAddress, createToken, @@ -47,7 +47,7 @@ export const handleRpcRequest = async ( request: RpcRequest, wallet: IHathorWallet, requestMetadata: RequestMetadata, - promptHandler: TriggerHandler, + promptHandler: TriggerHandler ): Promise => { switch (request.method) { case RpcMethods.SignWithAddress: return signWithAddress( diff --git a/packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts b/packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts index 84854345..af897f6d 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/createNanoContractCreateTokenTx.ts @@ -7,7 +7,6 @@ import { z } from 'zod'; import type { IHathorWallet, Transaction } from '@hathor/wallet-lib'; -import type { CreateTokenOptionsInput } from '@hathor/wallet-lib/lib/wallet/types'; import { TriggerTypes, PinConfirmationPrompt, @@ -18,15 +17,14 @@ import { RpcResponseTypes, CreateNanoContractCreateTokenTxResponse, CreateNanoContractCreateTokenTxConfirmationPrompt, - CreateNanoContractCreateTokenTxConfirmationResponse, CreateNanoContractCreateTokenTxLoadingTrigger, CreateNanoContractCreateTokenTxLoadingFinishedTrigger, NanoContractParams, - NanoContractCreateTokenParams, } from '../types'; -import { PromptRejectedError, InvalidParamsError } from '../errors'; +import { PromptRejectedError, InvalidParamsError, SendNanoContractTxError } from '../errors'; import { INanoContractActionSchema } from '@hathor/wallet-lib'; -import { createTokenBaseSchema } from '../schemas'; +import { bigIntCoercibleSchema } from '@hathor/wallet-lib/lib/utils/bigint'; +import { createTokenBaseSchema, createNanoContractCreateTokenTxConfirmationResponseSchema } from '../schemas'; const createNanoContractCreateTokenTxSchema = z.object({ method: z.string().min(1), @@ -40,8 +38,14 @@ const createNanoContractCreateTokenTxSchema = z.object({ createTokenOptions: createTokenBaseSchema.extend({ contractPaysTokenDeposit: z.boolean(), }).optional(), + max_fee: bigIntCoercibleSchema.optional(), + contract_pays_fees: z.boolean().optional(), push_tx: z.boolean().default(true), -}); +}).transform(data => ({ + ...data, + ...(data.max_fee !== undefined && { maxFee: data.max_fee }), + ...(data.contract_pays_fees !== undefined && { contractPaysFees: data.contract_pays_fees }), +})); /** * Creates and optionally sends a nano contract transaction that creates a new token. @@ -68,24 +72,16 @@ export async function createNanoContractCreateTokenTx( if (!validationResult.success) { throw new InvalidParamsError(validationResult.error.errors.map(e => `${e.path.join('.')}: ${e.message}`).join(', ')); } - const { method, address, data, createTokenOptions, push_tx } = validationResult.data; + const { method, address, data, createTokenOptions, maxFee, contractPaysFees, push_tx } = validationResult.data; - // Prepare nano and token params for the confirmation prompt - const nanoParams: NanoContractParams = { - blueprintId: data?.blueprint_id ?? null, - ncId: data?.nc_id ?? null, - actions: data?.actions ?? [], - method, - args: data?.args ?? [], - parsedArgs: [], - pushTx: push_tx, - }; // Only pass CreateTokenParams fields, fallback to null/empty for missing - const tokenParams: NanoContractCreateTokenParams = { + // Prepare createTokenOptions for pre-building transaction + const preBuildTokenOptions = { name: createTokenOptions?.name ?? '', symbol: createTokenOptions?.symbol ?? '', amount: typeof createTokenOptions?.amount === 'string' ? BigInt(createTokenOptions.amount) : (createTokenOptions?.amount ?? 0n), - mintAddress: createTokenOptions?.mintAddress ?? null, + version: createTokenOptions?.version ?? null, + mintAddress: createTokenOptions?.mintAddress ?? address, changeAddress: createTokenOptions?.changeAddress ?? null, createMint: createTokenOptions?.createMint ?? true, mintAuthorityAddress: createTokenOptions?.mintAuthorityAddress ?? null, @@ -97,28 +93,80 @@ export async function createNanoContractCreateTokenTx( contractPaysTokenDeposit: createTokenOptions?.contractPaysTokenDeposit ?? false, }; + // Pre-build transaction without signing to calculate fees + const preBuildData = { + blueprintId: data?.blueprint_id ?? null, + ncId: data?.nc_id ?? null, + actions: data?.actions ?? [], + args: data?.args ?? [], + }; + + const preBuildResult = await wallet.createNanoContractCreateTokenTransaction( + method, + address, + preBuildData, + preBuildTokenOptions, + { + maxFee, + contractPaysFees, + signTx: false, + } + ); + + if (!preBuildResult.transaction) { + throw new SendNanoContractTxError('Unable to create transaction object'); + } + + // Extract fee from pre-built transaction + const feeHeader = preBuildResult.transaction.getFeeHeader?.(); + if (feeHeader && feeHeader.entries.some(entry => entry.tokenIndex !== 0)) { + throw new InvalidParamsError('Unexpected fee entry with non-HTR token index'); + } + // Sum all fee entries for HTR token (index 0) + const fee = feeHeader + ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n) + : 0n; + + // Prepare nano and token params for the confirmation prompt + const nanoParams: NanoContractParams = { + blueprintId: data?.blueprint_id ?? null, + ncId: data?.nc_id ?? null, + actions: data?.actions ?? [], + method, + args: data?.args ?? [], + parsedArgs: [], + pushTx: push_tx, + fee, + contractPaysFees: contractPaysFees ?? false, + preparedTx: preBuildResult.transaction, + }; + const confirmationPrompt: CreateNanoContractCreateTokenTxConfirmationPrompt = { ...rpcRequest, type: TriggerTypes.CreateNanoContractCreateTokenTxConfirmationPrompt, data: { nano: nanoParams, - token: tokenParams, + token: { + ...preBuildTokenOptions, + fee, + }, }, }; - const confirmationResponse = await promptHandler( - confirmationPrompt, requestMetadata, - ) as CreateNanoContractCreateTokenTxConfirmationResponse; + const rawResponse = await promptHandler(confirmationPrompt, requestMetadata); + + // Parse and validate the entire response with Zod + const responseValidation = createNanoContractCreateTokenTxConfirmationResponseSchema.safeParse(rawResponse); + if (!responseValidation.success) { + throw new SendNanoContractTxError(responseValidation.error.errors.map(e => e.message).join(', ')); + } + + const confirmationResponse = responseValidation.data; + if (!confirmationResponse.data.accepted) { throw new PromptRejectedError('User rejected nano contract create token transaction prompt'); } - const { nano, token } = confirmationResponse.data; - - // Ensure mintAddress has a value (required by wallet-lib's CreateTokenOptionsInput) - const tokenOptions: CreateTokenOptionsInput = { - ...token, - mintAddress: token.mintAddress ?? address, - }; + const confirmedCaller = confirmationResponse.data.nano.caller; // Prompt for PIN const pinPrompt: PinConfirmationPrompt = { @@ -136,29 +184,25 @@ export async function createNanoContractCreateTokenTx( }; promptHandler(loadingTrigger, requestMetadata); - // Call the wallet method + // If caller changed, update the pre-built transaction + if (confirmedCaller !== address) { + const nanoHeaders = preBuildResult.transaction.getNanoHeaders(); + if (!nanoHeaders || nanoHeaders.length === 0) { + throw new SendNanoContractTxError('No nano headers found in the transaction'); + } + await wallet.setNanoHeaderCaller(nanoHeaders[0], confirmedCaller); + } + + await wallet.signTx(preBuildResult.transaction, { pinCode: pinResponse.data.pinCode }); + + // Send or return hex based on push_tx flag let response: Transaction | string; if (push_tx) { - response = await wallet.createAndSendNanoContractCreateTokenTransaction( - nano.method, - address, - nano, - tokenOptions, - { pinCode: pinResponse.data.pinCode } - ); + // Send the transaction + response = await preBuildResult.runFromMining(); } else { - const sendTransactionObj = await wallet.createNanoContractCreateTokenTransaction( - nano.method, - address, - nano, - tokenOptions, - { pinCode: pinResponse.data.pinCode } - ); // Convert to hex format for the response when not pushing to network - if (!sendTransactionObj.transaction) { - throw new Error('Failed to create transaction'); - } - response = sendTransactionObj.transaction.toHex(); + response = preBuildResult.transaction.toHex(); } // Emit loading finished trigger diff --git a/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts b/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts index 427d1cf7..408bbd14 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/createToken.ts @@ -5,7 +5,7 @@ * LICENSE file in the root directory of this source tree. */ -import { IHathorWallet, Transaction } from '@hathor/wallet-lib'; +import { IHathorWallet, tokensUtils, TokenVersion, Transaction } from '@hathor/wallet-lib'; import { CreateTokenConfirmationPrompt, CreateTokenConfirmationResponse, @@ -23,6 +23,7 @@ import { import { CreateTokenError, PromptRejectedError, InvalidParamsError } from '../errors'; import { z } from 'zod'; import { createTokenRpcSchema } from '../schemas'; +import { FEE_PER_OUTPUT } from '@hathor/wallet-lib/lib/constants'; /** * Handles the creation of a new token on the Hathor blockchain. @@ -61,6 +62,13 @@ export async function createToken( type: TriggerTypes.PinConfirmationPrompt, }; + let fee = tokensUtils.getDataFee(params.options.data?.length ?? 0); + // 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; + } + const createTokenPrompt: CreateTokenConfirmationPrompt = { ...rpcRequest, type: TriggerTypes.CreateTokenConfirmationPrompt, @@ -68,6 +76,7 @@ export async function createToken( name: params.name, symbol: params.symbol, amount: params.amount, + version: params.options.tokenVersion, changeAddress: params.options.changeAddress, createMint: params.options.createMint, mintAuthorityAddress: params.options.mintAuthorityAddress, @@ -77,6 +86,7 @@ export async function createToken( allowExternalMeltAuthorityAddress: params.options.allowExternalMeltAuthorityAddress, data: params.options.data, address: params.options.address, + fee }, }; diff --git a/packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts b/packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts index b68823d3..e08914ae 100644 --- a/packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts +++ b/packages/hathor-rpc-handler/src/rpcMethods/sendNanoContractTx.ts @@ -15,7 +15,6 @@ import { RequestMetadata, SendNanoContractRpcRequest, SendNanoContractTxConfirmationPrompt, - SendNanoContractTxConfirmationResponse, SendNanoContractTxLoadingTrigger, RpcResponseTypes, RpcResponse, @@ -23,7 +22,10 @@ import { } from '../types'; import { PromptRejectedError, SendNanoContractTxError, InvalidParamsError } from '../errors'; import { INanoContractActionSchema, NanoContractAction, ncApi, nanoUtils, Network, config, HathorWallet } from '@hathor/wallet-lib'; +import { bigIntCoercibleSchema } from '@hathor/wallet-lib/lib/utils/bigint'; import { fetchTokenDetails } from '../helpers'; +import { sendNanoContractTxConfirmationResponseSchema } from '../schemas'; + export type NanoContractActionWithStringAmount = Omit & { amount: string, @@ -36,12 +38,16 @@ const sendNanoContractSchema = z.object({ nc_id: z.string().nullish(), actions: z.array(INanoContractActionSchema), args: z.array(z.unknown()).default([]), + max_fee: bigIntCoercibleSchema.optional(), + contract_pays_fees: z.boolean().optional(), push_tx: z.boolean().default(true), }).transform(data => ({ ...data, blueprintId: data.blueprint_id || null, ncId: data.nc_id || null, pushTx: data.push_tx, + ...(data.max_fee !== undefined && { maxFee: data.max_fee }), + ...(data.contract_pays_fees !== undefined && { contractPaysFees: data.contract_pays_fees }), })).refine( (data) => data.blueprintId || data.ncId, "Either blueprint_id or nc_id must be provided" @@ -112,6 +118,43 @@ export async function sendNanoContractTx( .map(action => action.token); const tokenDetails = await fetchTokenDetails(wallet, tokenUids); + // Pre-build transaction to calculate fees + const tempCallerAddress = await wallet.getAddressAtIndex(0); + if (!tempCallerAddress || tempCallerAddress.trim() === '') { + throw new SendNanoContractTxError('Unable to get wallet address at index 0'); + } + const preBuildTxData = { + ncId: params.ncId, + blueprintId, + actions: params.actions, + args: params.args, + }; + + const preBuildSendTx = await wallet.createNanoContractTransaction( + params.method, + tempCallerAddress, + preBuildTxData, + { + ...(params.maxFee !== undefined && { maxFee: params.maxFee }), + ...(params.contractPaysFees !== undefined && { contractPaysFees: params.contractPaysFees }), + signTx: false, + }, + ); + + if (!preBuildSendTx.transaction) { + throw new SendNanoContractTxError('Unable to create transaction object'); + } + + // Extract fee from pre-built transaction + const feeHeader = preBuildSendTx.transaction?.getFeeHeader?.(); + if (feeHeader && feeHeader.entries.some(entry => entry.tokenIndex !== 0)) { + throw new SendNanoContractTxError('Unexpected fee entry with non-HTR token index'); + } + // Sum all fee entries for HTR token (index 0) + const fee = feeHeader + ? feeHeader.entries.reduce((sum, entry) => sum + entry.amount, 0n) + : 0n; + const sendNanoContractTxPrompt: SendNanoContractTxConfirmationPrompt = { ...rpcRequest, type: TriggerTypes.SendNanoContractTxConfirmationPrompt, @@ -124,21 +167,27 @@ export async function sendNanoContractTx( parsedArgs, pushTx: params.pushTx, tokenDetails, + fee, + contractPaysFees: params.contractPaysFees ?? false, + preparedTx: preBuildSendTx.transaction, }, }; - const sendNanoContractTxResponse = await triggerHandler(sendNanoContractTxPrompt, requestMetadata) as SendNanoContractTxConfirmationResponse; + const rawResponse = await triggerHandler(sendNanoContractTxPrompt, requestMetadata); + + // Parse and validate the entire response with Zod + const responseValidation = sendNanoContractTxConfirmationResponseSchema.safeParse(rawResponse); + if (!responseValidation.success) { + throw new SendNanoContractTxError(responseValidation.error.errors.map(e => e.message).join(', ')); + } + + const sendNanoContractTxResponse = responseValidation.data; if (!sendNanoContractTxResponse.data.accepted) { throw new PromptRejectedError(); } - const { - caller, - blueprintId: confirmedBluePrintId, - actions: confirmedActions, - args: confirmedArgs, - } = sendNanoContractTxResponse.data.nc; + const confirmedCaller = sendNanoContractTxResponse.data.nc.caller; const pinCodeResponse: PinRequestResponse = (await triggerHandler(pinPrompt, requestMetadata)) as PinRequestResponse; @@ -152,42 +201,25 @@ export async function sendNanoContractTx( }; triggerHandler(sendNanoContractLoadingTrigger, requestMetadata); - const txData = { - ncId: params.ncId, - blueprintId: confirmedBluePrintId, - actions: confirmedActions, - args: confirmedArgs, - }; let response: Transaction | string; + + // If caller changed, update the pre-built transaction + if (confirmedCaller !== tempCallerAddress) { + const nanoHeaders = preBuildSendTx.transaction.getNanoHeaders(); + if (!nanoHeaders || nanoHeaders.length === 0) { + throw new SendNanoContractTxError('No nano headers found in the transaction'); + } + await wallet.setNanoHeaderCaller(nanoHeaders[0], confirmedCaller); + } + + await wallet.signTx(preBuildSendTx.transaction, { pinCode: pinCodeResponse.data.pinCode }); if (params.pushTx) { - // If pushTx is true, create and send the transaction directly - response = await wallet.createAndSendNanoContractTransaction( - params.method, - caller, - txData, - { - pinCode: pinCodeResponse.data.pinCode, - }, - ); + response = await preBuildSendTx.runFromMining(); } else { - // Otherwise, just create the transaction object - const sendTransactionObj = await wallet.createNanoContractTransaction( - params.method, - caller, - txData, - { - pinCode: pinCodeResponse.data.pinCode, - }, - ); - - if (!sendTransactionObj.transaction) { - // This should never happen, but we'll check anyway - throw new SendNanoContractTxError('Unable to create transaction object'); - } // Convert the transaction object to hex format for the response - response = sendTransactionObj.transaction.toHex(); + response = preBuildSendTx.transaction.toHex(); } const sendNanoContractLoadingFinishedTrigger: SendNanoContractTxLoadingFinishedTrigger = { diff --git a/packages/hathor-rpc-handler/src/rpcRequest/sendNanoContractTx.ts b/packages/hathor-rpc-handler/src/rpcRequest/sendNanoContractTx.ts index 82360d11..66791838 100644 --- a/packages/hathor-rpc-handler/src/rpcRequest/sendNanoContractTx.ts +++ b/packages/hathor-rpc-handler/src/rpcRequest/sendNanoContractTx.ts @@ -12,22 +12,28 @@ import { } from '../types'; export function sendNanoContractTxRpcRequest( + network: string, method: string, - blueprintId: string, + blueprintId: string | null, actions: NanoContractAction[], args: unknown[], pushTx: boolean, ncId: string | null, + maxFee?: string, + contractPaysFees?: boolean, ): SendNanoContractRpcRequest { return { method: RpcMethods.SendNanoContractTx, params: { + network, method, blueprint_id: blueprintId, actions, args, push_tx: pushTx, nc_id: ncId, + ...(maxFee !== undefined && { max_fee: maxFee }), + ...(contractPaysFees !== undefined && { contract_pays_fees: contractPaysFees }), } }; } diff --git a/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts b/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts index 393025ee..5d19bbaa 100644 --- a/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts +++ b/packages/hathor-rpc-handler/src/schemas/createTokenSchema.ts @@ -5,12 +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'; 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(), changeAddress: z.string().nullable().optional(), createMint: z.boolean().optional(), mintAuthorityAddress: z.string().nullable().optional(), @@ -28,6 +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), address: z.string().nullish().default(null), change_address: z.string().nullish().default(null), create_mint: z.boolean().default(true), @@ -51,5 +54,6 @@ export const createTokenRpcSchema = z.object({ allowExternalMeltAuthorityAddress: data.allow_external_melt_authority_address, data: data.data, address: data.address, + tokenVersion: data.version, } })); \ No newline at end of file diff --git a/packages/hathor-rpc-handler/src/schemas/index.ts b/packages/hathor-rpc-handler/src/schemas/index.ts index 58021964..563bf7e9 100644 --- a/packages/hathor-rpc-handler/src/schemas/index.ts +++ b/packages/hathor-rpc-handler/src/schemas/index.ts @@ -5,4 +5,11 @@ * LICENSE file in the root directory of this source tree. */ -export { createTokenBaseSchema, createTokenRpcSchema } from './createTokenSchema'; \ No newline at end of file +export { createTokenBaseSchema, createTokenRpcSchema } from './createTokenSchema'; +export { + nanoContractResponseWithCallerSchema, + sendNanoContractTxConfirmationDataSchema, + sendNanoContractTxConfirmationResponseSchema, + createNanoContractCreateTokenTxConfirmationDataSchema, + createNanoContractCreateTokenTxConfirmationResponseSchema, +} from './nanoContractResponseSchema'; \ No newline at end of file diff --git a/packages/hathor-rpc-handler/src/schemas/nanoContractResponseSchema.ts b/packages/hathor-rpc-handler/src/schemas/nanoContractResponseSchema.ts new file mode 100644 index 00000000..4c1d1053 --- /dev/null +++ b/packages/hathor-rpc-handler/src/schemas/nanoContractResponseSchema.ts @@ -0,0 +1,63 @@ +/** + * 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 { z } from 'zod'; +import { INanoContractActionSchema } from '@hathor/wallet-lib'; +import { bigIntCoercibleSchema } from '@hathor/wallet-lib/lib/utils/bigint'; +import { createTokenBaseSchema } from './createTokenSchema'; +import { TriggerResponseTypes } from '../types'; + +// Shared schema for rejected confirmation responses +const rejectedDataSchema = z.object({ accepted: z.literal(false) }); + +/** Validates nano contract params with required non-empty caller and all NC parameters. */ +export const nanoContractResponseWithCallerSchema = z.object({ + caller: z.string().min(1, 'Missing or empty nano caller in confirmation response'), + method: z.string().min(1, 'Missing or empty nano method in confirmation response'), + blueprintId: z.string().nullable(), + ncId: z.string().nullable(), + actions: z.array(INanoContractActionSchema), + args: z.array(z.unknown()), + parsedArgs: z.array(z.unknown()), + pushTx: z.boolean(), + fee: bigIntCoercibleSchema, + contractPaysFees: z.boolean().optional(), +}).passthrough(); + +/** Validates token params for nano contract create token responses. */ +export const nanoContractCreateTokenParamsSchema = createTokenBaseSchema.extend({ + contractPaysTokenDeposit: z.boolean(), +}).passthrough(); + +export const sendNanoContractTxConfirmationDataSchema = z.object({ + accepted: z.literal(true), + nc: nanoContractResponseWithCallerSchema, +}); + +export const sendNanoContractTxConfirmationResponseSchema = z.object({ + type: z.literal(TriggerResponseTypes.SendNanoContractTxConfirmationResponse), + data: z.discriminatedUnion('accepted', [ + sendNanoContractTxConfirmationDataSchema, + rejectedDataSchema, + ]), +}); + +// --- CreateNanoContractCreateTokenTx --- + +export const createNanoContractCreateTokenTxConfirmationDataSchema = z.object({ + accepted: z.literal(true), + nano: nanoContractResponseWithCallerSchema, + token: nanoContractCreateTokenParamsSchema, +}); + +export const createNanoContractCreateTokenTxConfirmationResponseSchema = z.object({ + type: z.literal(TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse), + data: z.discriminatedUnion('accepted', [ + createNanoContractCreateTokenTxConfirmationDataSchema, + rejectedDataSchema, + ]), +}); diff --git a/packages/hathor-rpc-handler/src/types/prompt.ts b/packages/hathor-rpc-handler/src/types/prompt.ts index ab43b142..4017c734 100644 --- a/packages/hathor-rpc-handler/src/types/prompt.ts +++ b/packages/hathor-rpc-handler/src/types/prompt.ts @@ -6,8 +6,8 @@ */ import { AddressInfoObject, GetBalanceObject, TokenDetailsObject } from '@hathor/wallet-lib/lib/wallet/types'; import { NanoContractAction } from '@hathor/wallet-lib/lib/nano_contracts/types'; -import type { Transaction } from '@hathor/wallet-lib'; import { RequestMetadata, RpcRequest } from './rpcRequest'; +import { TokenVersion, Transaction } from '@hathor/wallet-lib'; export enum TriggerTypes { GetBalanceConfirmationPrompt, @@ -186,13 +186,23 @@ export interface NanoContractParams { parsedArgs: unknown[]; pushTx: boolean; tokenDetails?: Map; + fee: bigint; + contractPaysFees?: boolean; + preparedTx: Transaction; } +/** + * NanoContractParams without preparedTx for use in response payloads. + * Clients don't need to echo back the full Transaction object when confirming. + */ +export type NanoContractResponseParams = Omit; + export interface CreateTokenParams { name: string, symbol: string, amount: bigint, address?: string | null, + version: TokenVersion | null, mintAddress?: string | null, changeAddress: string | null, createMint: boolean, @@ -202,6 +212,7 @@ export interface CreateTokenParams { meltAuthorityAddress: string | null, allowExternalMeltAuthorityAddress: boolean, data: string[] | null, + fee?: bigint, } // Extended type for nano contract token creation @@ -235,7 +246,7 @@ export interface SendNanoContractTxConfirmationResponse { type: TriggerResponseTypes.SendNanoContractTxConfirmationResponse; data: { accepted: true; - nc: NanoContractParams & { + nc: NanoContractResponseParams & { caller: string; } } | { @@ -338,7 +349,7 @@ export interface CreateNanoContractCreateTokenTxConfirmationResponse { type: TriggerResponseTypes.CreateNanoContractCreateTokenTxConfirmationResponse; data: { accepted: true; - nano: NanoContractParams & { caller: string }; + nano: NanoContractResponseParams & { caller: string }; token: NanoContractCreateTokenParams; } | { accepted: false; diff --git a/packages/hathor-rpc-handler/src/types/rpcRequest.ts b/packages/hathor-rpc-handler/src/types/rpcRequest.ts index 8e2d97ec..d16fe84d 100644 --- a/packages/hathor-rpc-handler/src/types/rpcRequest.ts +++ b/packages/hathor-rpc-handler/src/types/rpcRequest.ts @@ -5,6 +5,7 @@ * 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'; export enum RpcMethods { @@ -31,6 +32,7 @@ export interface CreateTokenRpcRequest { name: string; symbol: string; amount: string; + version?: TokenVersion address?: string; change_address?: string; create_mint: boolean; @@ -101,12 +103,17 @@ export interface SignOracleDataRpcRequest { export interface SendNanoContractRpcRequest { method: RpcMethods.SendNanoContractTx, params: { + network: string; method: string; - blueprint_id: string; - nc_id: string | null; + blueprint_id?: string | null; + nc_id?: string | null; actions: NanoContractAction[], - args: unknown[]; - push_tx: boolean; + args?: unknown[]; + push_tx?: boolean; + /** Maximum fee the user is willing to pay */ + max_fee?: string; + /** Whether the contract pays the transaction fees */ + contract_pays_fees?: boolean; } } @@ -139,7 +146,10 @@ export interface CreateNanoContractCreateTokenTxRpcRequest { address: string; data?: unknown; createTokenOptions?: unknown; - options?: unknown; + /** Maximum fee the user is willing to pay */ + max_fee?: string; + /** Whether the contract pays the transaction fees */ + contract_pays_fees?: boolean; push_tx: boolean; } } diff --git a/yarn.lock b/yarn.lock index f42b8311..60f4e075 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2459,7 +2459,7 @@ __metadata: "@hathor/wallet-lib": "npm:2.16.0" "@types/eslint__js": "npm:8.42.3" "@types/jest": "npm:29.5.12" - "@types/node": "npm:20.14.2" + "@types/node": "npm:22.0.0" eslint: "npm:9.4.0" jest: "npm:29.7.0" ts-jest: "npm:29.1.4" @@ -5634,12 +5634,12 @@ __metadata: languageName: node linkType: hard -"@types/node@npm:20.14.2": - version: 20.14.2 - resolution: "@types/node@npm:20.14.2" +"@types/node@npm:22.0.0": + version: 22.0.0 + resolution: "@types/node@npm:22.0.0" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10/c38e47b190fa0a8bdfde24b036dddcf9401551f2fb170a90ff33625c7d6f218907e81c74e0fa6e394804a32623c24c60c50e249badc951007830f0d02c48ee0f + undici-types: "npm:~6.11.1" + checksum: 10/7142a13ef1f884fde38f1e1499cbebcfe72755e8cb8657c4cb1ba1c2c91a3ae8656a72eb6e0a7d8189b0124c23c30e7c115324375d9c593435166da7a292e80e languageName: node linkType: hard @@ -17461,10 +17461,10 @@ __metadata: languageName: node linkType: hard -"undici-types@npm:~5.26.4": - version: 5.26.5 - resolution: "undici-types@npm:5.26.5" - checksum: 10/0097779d94bc0fd26f0418b3a05472410408877279141ded2bd449167be1aed7ea5b76f756562cb3586a07f251b90799bab22d9019ceba49c037c76445f7cddd +"undici-types@npm:~6.11.1": + version: 6.11.1 + resolution: "undici-types@npm:6.11.1" + checksum: 10/bdee4c3d67626bf45f1502b817b96e328ff9c3c006ecafa3708bc39ba66d6cecc2d5d69d3148667bb833d3fb457c0e715bfeed0b7b6767fa4d3044f5c1036ba9 languageName: node linkType: hard