diff --git a/sdk/src/index.ts b/sdk/src/index.ts index f2d8410c..86950236 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -44,7 +44,7 @@ export type { Unsubscribe, } from "./events/types.js"; -export { getInvoice, listInvoicesBySubmitter, listInvoicesByLP } from "./methods/queries.js"; +export { getInvoice, listInvoicesBySubmitter, listInvoicesByLP, getSubmitterInvoices } from "./methods/queries.js"; export { getNftMetadata, getNftOwner } from "./methods/nft.js"; export { submitInvoice } from "./methods/submitInvoice.js"; export { transferLPPosition } from "./methods/transferLPPosition.js"; diff --git a/sdk/src/methods/queries.ts b/sdk/src/methods/queries.ts index 92029dff..1ddf6ee1 100644 --- a/sdk/src/methods/queries.ts +++ b/sdk/src/methods/queries.ts @@ -121,6 +121,60 @@ export async function listInvoicesBySubmitter( } +/** + * Get invoices submitted by a specific submitter address with pagination. + * @param server Soroban RPC server instance + * @param contractAddress The contract's address + * @param submitter The submitter's address + * @param sourceAccount Account used for simulation + * @param networkPassphrase The network passphrase + * @param page The page number (0-indexed) + * @param pageSize The number of items per page + * @returns Array of invoices + * @throws {ILNError} On simulation errors + * @example + * ```ts + * const invoices = await getSubmitterInvoices(server, contractAddress, "G...", sourceAccount, Networks.TESTNET, 0, 10); + * ``` + */ +export async function getSubmitterInvoices( + server: SorobanRpc.Server, + contractAddress: string, + submitter: string, + sourceAccount: Account, + networkPassphrase: string, + page: number = 0, + pageSize: number = 50 +): Promise { + const contract = new Contract(contractAddress); + const op = contract.call( + "list_invoices_by_submitter", + nativeToScVal(submitter, { type: "address" }), + nativeToScVal(page, { type: "u32" }), + nativeToScVal(pageSize, { type: "u32" }) + ); + + const tx = new TransactionBuilder(sourceAccount, { + fee: BASE_FEE, + networkPassphrase, + }) + .addOperation(op) + .setTimeout(30) + .build(); + + const sim = await retry(() => server.simulateTransaction(tx)); + + if (SorobanRpc.Api.isSimulationError(sim)) { + throw ILNError.fromError(sim.error); + } + if (!sim.result?.retval) { + return []; + } + + const rawArr = scValToNative(sim.result.retval) as Record[]; + return rawArr.map(raw => decodeInvoice(raw as Record)); +} + /** * List invoices funded by a specific LP address. * @param server Soroban RPC server instance diff --git a/sdk/tests/queries.test.ts b/sdk/tests/queries.test.ts index bfa5ce02..9965a56d 100644 --- a/sdk/tests/queries.test.ts +++ b/sdk/tests/queries.test.ts @@ -1,5 +1,5 @@ import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest'; -import { getInvoice, listInvoicesBySubmitter, listInvoicesByLP } from '../src/methods/queries.js'; +import { getInvoice, listInvoicesBySubmitter, listInvoicesByLP, getSubmitterInvoices } from '../src/methods/queries.js'; import { ILNError } from '../src/errors.js'; import { Account, SorobanRpc } from '@stellar/stellar-sdk'; @@ -12,4 +12,24 @@ describe('queries', () => { mockServer.simulateTransaction.mockResolvedValue({ error: 'NotFound' }); await expect(getInvoice(mockServer, "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", 1n, mockAccount, 'pass')).rejects.toThrow(ILNError.InvoiceNotFound); }); + + it('getSubmitterInvoices returns empty array when retval is null', async () => { + // @ts-ignore + mockServer.simulateTransaction.mockResolvedValue({ result: { retval: null } }); + const result = await getSubmitterInvoices(mockServer, "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", "GAGZSXAR7P7PASD2PGYISBMEZCMSI35TRJXYZTZNNCAUZRDEMHQM2XJS", mockAccount, 'pass', 0, 10); + expect(result).toEqual([]); + }); + + it('getSubmitterInvoices throws error on simulation error', async () => { + // @ts-ignore + mockServer.simulateTransaction.mockResolvedValue({ error: 'ContractError' }); + await expect(getSubmitterInvoices(mockServer, "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", "GAGZSXAR7P7PASD2PGYISBMEZCMSI35TRJXYZTZNNCAUZRDEMHQM2XJS", mockAccount, 'pass', 0, 10)).rejects.toThrow(); + }); + + it('getSubmitterInvoices uses default pagination values', async () => { + // @ts-ignore + mockServer.simulateTransaction.mockResolvedValue({ result: { retval: null } }); + const result = await getSubmitterInvoices(mockServer, "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", "GAGZSXAR7P7PASD2PGYISBMEZCMSI35TRJXYZTZNNCAUZRDEMHQM2XJS", mockAccount, 'pass'); + expect(result).toEqual([]); + }); });