diff --git a/src/jobs/transaction-status-checker/transaction-status-checker.service.ts b/src/jobs/transaction-status-checker/transaction-status-checker.service.ts index fcc8dee..54fb3dd 100644 --- a/src/jobs/transaction-status-checker/transaction-status-checker.service.ts +++ b/src/jobs/transaction-status-checker/transaction-status-checker.service.ts @@ -535,7 +535,7 @@ export class TransactionStatusCheckerService { }; } - if (functionName === 'repay_loan') { + if (functionName === 'repay_installment') { const loanId = nativeArgs[1] as string; const rawAmount = nativeArgs[2]; const amount = typeof rawAmount === 'bigint' ? Number(rawAmount) / 10_000_000 : diff --git a/src/modules/loans/dto/loan-payment-response.dto.ts b/src/modules/loans/dto/loan-payment-response.dto.ts index 7680ee2..62f68fb 100644 --- a/src/modules/loans/dto/loan-payment-response.dto.ts +++ b/src/modules/loans/dto/loan-payment-response.dto.ts @@ -27,7 +27,7 @@ export class LoanPaymentPreviewDto { */ export class LoanPaymentResponseDto { @ApiProperty({ - description: 'Unsigned XDR transaction for the repay_loan() Soroban call', + description: 'Unsigned XDR transaction for the repay_installment() Soroban call', example: 'AAAAAgAAAAA...', }) unsignedXdr: string; diff --git a/src/modules/loans/loans.controller.ts b/src/modules/loans/loans.controller.ts index 7b9d253..3837aaf 100644 --- a/src/modules/loans/loans.controller.ts +++ b/src/modules/loans/loans.controller.ts @@ -184,7 +184,7 @@ export class LoansController { @ApiOperation({ summary: 'Make a loan repayment', description: - 'Validates the payment, constructs an unsigned Soroban repay_loan() transaction, and returns it alongside a payment preview. The mobile app must sign the XDR and submit the signed transaction back to the network. Requires JWT authentication.', + 'Validates the payment, constructs an unsigned Soroban repay_installment() transaction, and returns it alongside a payment preview. The mobile app must sign the XDR and submit the signed transaction back to the network. Requires JWT authentication.', }) @ApiResponse({ status: 200, diff --git a/src/modules/loans/loans.service.ts b/src/modules/loans/loans.service.ts index 91b3377..e862e2e 100644 --- a/src/modules/loans/loans.service.ts +++ b/src/modules/loans/loans.service.ts @@ -213,7 +213,7 @@ export class LoansService { }); } - return this.creditLineContractClient.buildRepayLoanTx( + return this.creditLineContractClient.buildRepayInstallmentTx( wallet, loan.loan_id, amount, diff --git a/src/stellar/contracts/clients/creditline.client.ts b/src/stellar/contracts/clients/creditline.client.ts index ddd231d..4abc2c7 100644 --- a/src/stellar/contracts/clients/creditline.client.ts +++ b/src/stellar/contracts/clients/creditline.client.ts @@ -66,7 +66,7 @@ export class CreditLineContractClient { return prepared.toXDR(); } - async buildRepayLoanTx(userWallet: string, loanId: string, amount: number): Promise { + async buildRepayInstallmentTx(userWallet: string, loanId: string, amount: number): Promise { this.ensureConfigured(); try { @@ -97,7 +97,7 @@ export class CreditLineContractClient { if (error instanceof ContractNotConfiguredError) { throw error; } - this.logger.error(`Failed to build repay_loan transaction: ${error.message}`); + this.logger.error(`Failed to build repay_installment transaction: ${error.message}`); throw new ContractTxBuildError('repayment'); } } diff --git a/src/stellar/contracts/interfaces/creditline.interface.ts b/src/stellar/contracts/interfaces/creditline.interface.ts index 8a60d2d..8fa0c85 100644 --- a/src/stellar/contracts/interfaces/creditline.interface.ts +++ b/src/stellar/contracts/interfaces/creditline.interface.ts @@ -16,7 +16,7 @@ export interface ICreditLineClient { params: CreateLoanParams, ): Promise; - buildRepayLoanTx( + buildRepayInstallmentTx( userWallet: string, loanId: string, amount: number, diff --git a/src/stellar/contracts/mocks/creditline.mock.ts b/src/stellar/contracts/mocks/creditline.mock.ts index 525d0c6..d91d66e 100644 --- a/src/stellar/contracts/mocks/creditline.mock.ts +++ b/src/stellar/contracts/mocks/creditline.mock.ts @@ -9,7 +9,7 @@ export class MockCreditLineContractClient { }, ); - buildRepayLoanTx = jest.fn( + buildRepayInstallmentTx = jest.fn( async (_userWallet: string, _loanId: string, _amount: number): Promise => { return 'AAAAAgAAAQAAAAAAAAAAiZ3TgwAAAAAyMZyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='; }, diff --git a/test/e2e/helpers/test-setup.ts b/test/e2e/helpers/test-setup.ts index e916e3b..ca54eda 100644 --- a/test/e2e/helpers/test-setup.ts +++ b/test/e2e/helpers/test-setup.ts @@ -320,7 +320,7 @@ export async function buildTestApp(): Promise<{ buildCreateLoanTransaction: jest .fn() .mockResolvedValue('AAAAAgAAAQAAAAAAAAAAiZ3TgwAAAAAyMZyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), - buildRepayLoanTx: jest + buildRepayInstallmentTx: jest .fn() .mockResolvedValue('AAAAAgAAAQAAAAAAAAAAiZ3TgwAAAAAyMZyAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=='), getScore: jest.fn().mockResolvedValue(75), diff --git a/test/e2e/modules/loans/loan-lifecycle.e2e-spec.ts b/test/e2e/modules/loans/loan-lifecycle.e2e-spec.ts index 1394594..d1581c5 100644 --- a/test/e2e/modules/loans/loan-lifecycle.e2e-spec.ts +++ b/test/e2e/modules/loans/loan-lifecycle.e2e-spec.ts @@ -75,7 +75,7 @@ describe('Loan Lifecycle Flow (e2e)', () => { const mockCreditLineContract = { buildCreateLoanTransaction: jest.fn(), - buildRepayLoanTx: jest.fn(), + buildRepayInstallmentTx: jest.fn(), }; const mockTransactionsService = { @@ -288,7 +288,7 @@ describe('Loan Lifecycle Flow (e2e)', () => { return `xdr-loan-create-${payload.loanId}`; }); - mockCreditLineContract.buildRepayLoanTx.mockImplementation(async (_wallet, loanId, amount) => { + mockCreditLineContract.buildRepayInstallmentTx.mockImplementation(async (_wallet, loanId, amount) => { return `xdr-loan-repay-${loanId}-${amount}`; }); diff --git a/test/unit/modules/loans/loans.service.spec.ts b/test/unit/modules/loans/loans.service.spec.ts index 2f82922..129eea0 100644 --- a/test/unit/modules/loans/loans.service.spec.ts +++ b/test/unit/modules/loans/loans.service.spec.ts @@ -110,7 +110,7 @@ describe('LoansService', () => { mockSupabaseFrom.insert.mockResolvedValue({ error: null }); mockSupabaseFrom.update.mockReturnThis(); mockCreditLineContractClient.buildCreateLoanTransaction.mockResolvedValue('AAAAAgAAAAC...'); - mockCreditLineContractClient.buildRepayLoanTx.mockResolvedValue('AAAAAgAAAAA...'); + mockCreditLineContractClient.buildRepayInstallmentTx.mockResolvedValue('AAAAAgAAAAA...'); }); afterEach(() => { @@ -386,7 +386,7 @@ describe('LoansService', () => { willComplete: false, }, }); - expect(mockCreditLineContractClient.buildRepayLoanTx).toHaveBeenCalledWith( + expect(mockCreditLineContractClient.buildRepayInstallmentTx).toHaveBeenCalledWith( validWallet, 'chain-loan-1', 108.33, diff --git a/test/unit/stellar/contract-function-name.spec.ts b/test/unit/stellar/contract-function-name.spec.ts new file mode 100644 index 0000000..77e110e --- /dev/null +++ b/test/unit/stellar/contract-function-name.spec.ts @@ -0,0 +1,66 @@ +jest.unmock('stellar-sdk'); + +import * as StellarSdk from 'stellar-sdk'; +import { CreditLineContractClient } from '../../../src/stellar/contracts/clients/creditline.client'; +import { SorobanService } from '../../../src/blockchain/soroban/soroban.service'; +import { ConfigService } from '@nestjs/config'; + +describe('Repayment function name alignment', () => { + let client: CreditLineContractClient; + let mockServer: { prepareTransaction: jest.Mock; getAccount: jest.Mock }; + + const testContractId = 'CC4C3VI4FS6J5PP7OQN4ZC3WWBYMAISPRN4VYTCZ2J7CHI3255OM32LT'; + const testNetwork = 'Test SDF Network ; September 2015'; + const testWallet = 'GAZBSUBUXTD6YVYXECZRW2R6K7MOU5FGU2SVYXKYWOLT5H3JIRD54EZL'; + + beforeEach(() => { + mockServer = { + prepareTransaction: jest.fn(async (tx: StellarSdk.Transaction) => { + return { toXDR: () => tx.toXDR() }; + }), + getAccount: jest.fn().mockResolvedValue({}), + }; + + const mockSorobanService = { + getServer: jest.fn().mockReturnValue(mockServer), + getNetworkPassphrase: jest.fn().mockReturnValue(testNetwork), + }; + + const mockConfigService = { + get: jest.fn((key: string) => { + if (key === 'CREDIT_LINE_CONTRACT_ID' || key === 'CREDITLINE_CONTRACT_ID') { + return testContractId; + } + return undefined; + }), + }; + + client = new CreditLineContractClient( + mockSorobanService as unknown as SorobanService, + mockConfigService as unknown as ConfigService, + ); + }); + + it('XDR builder and transaction checker use the same function name: repay_installment with 3 args', async () => { + const xdr = await client.buildRepayInstallmentTx(testWallet, 'test-loan-123', 100); + + const tx = StellarSdk.TransactionBuilder.fromXDR(xdr, testNetwork); + const innerTx = + tx instanceof StellarSdk.FeeBumpTransaction ? tx.innerTransaction : tx; + const op = innerTx.operations[0]; + + expect(op.type).toBe('invokeHostFunction'); + + const invocation = ((op as unknown as { func: unknown }).func as { + _value?: { _attributes?: { functionName?: { toString?: () => string }; args?: unknown[] } }; + })?._value?._attributes; + + expect(invocation).toBeDefined(); + + const functionName = invocation!.functionName?.toString?.(); + expect(functionName).toBe('repay_installment'); + + const args = invocation!.args as unknown[]; + expect(args).toHaveLength(3); + }); +});