From da5e4dd3c05ed1a7dc5aed92ef62cf1ec437bbda Mon Sep 17 00:00:00 2001 From: ehonrie Date: Thu, 24 Sep 2026 21:28:06 +0100 Subject: [PATCH 1/5] feat: add error classes for invoice rating, deadline extension, and voting - Add InvoiceNotReleasedForRatingError for rating unrel eased invoices - Add AlreadyRatedError for duplicate ratings - Add NotEligibleToVoteError for non-contributors voting on extensions These errors support issue #865 (rating), #864 (extension voting), and related features. Co-Authored-By: Claude Haiku 4.5 --- src/errors.ts | 69 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/src/errors.ts b/src/errors.ts index f35bd2a..5e6d6a0 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -2251,3 +2251,72 @@ export function isWalletConnectionTimeoutError(err: unknown): err is WalletConne return err instanceof WalletConnectionTimeoutError; } +/** Thrown when attempting to rate an invoice that is not yet released. */ +export class InvoiceNotReleasedForRatingError extends StellarSplitError { + readonly invoiceId: string; + readonly status: string; + + constructor(invoiceId: string, status: string, raw?: string) { + super( + `Invoice ${invoiceId} is not released and cannot be rated (current: ${status})`, + "INVOICE_NOT_RELEASED_FOR_RATING", + { invoiceId, status }, + raw + ); + this.name = "InvoiceNotReleasedForRatingError"; + this.invoiceId = invoiceId; + this.status = status; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export function isInvoiceNotReleasedForRatingError(err: unknown): err is InvoiceNotReleasedForRatingError { + return err instanceof InvoiceNotReleasedForRatingError; +} + +/** Thrown when a payer has already rated an invoice. */ +export class AlreadyRatedError extends StellarSplitError { + readonly invoiceId: string; + readonly payer: string; + + constructor(invoiceId: string, payer: string, raw?: string) { + super( + `Payer ${payer} has already rated invoice ${invoiceId}`, + "ALREADY_RATED", + { invoiceId, payer }, + raw + ); + this.name = "AlreadyRatedError"; + this.invoiceId = invoiceId; + this.payer = payer; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export function isAlreadyRatedError(err: unknown): err is AlreadyRatedError { + return err instanceof AlreadyRatedError; +} + +/** Thrown when a non-contributor attempts to vote on a deadline extension. */ +export class NotEligibleToVoteError extends StellarSplitError { + readonly invoiceId: string; + readonly caller: string; + + constructor(invoiceId: string, caller: string, raw?: string) { + super( + `Caller ${caller} is not eligible to vote on invoice ${invoiceId} (must be a contributor)`, + "NOT_ELIGIBLE_TO_VOTE", + { invoiceId, caller }, + raw + ); + this.name = "NotEligibleToVoteError"; + this.invoiceId = invoiceId; + this.caller = caller; + Object.setPrototypeOf(this, new.target.prototype); + } +} + +export function isNotEligibleToVoteError(err: unknown): err is NotEligibleToVoteError { + return err instanceof NotEligibleToVoteError; +} + From 676c75b51862bb35c3793f9d18ab4f7c49103320 Mon Sep 17 00:00:00 2001 From: ehonrie Date: Thu, 24 Sep 2026 21:28:19 +0100 Subject: [PATCH 2/5] feat: add types for invoice ratings, groups, attestations, and extensions - Add CreatorRating type for #865 - Add ExtensionStatus type for #864 - Add GroupStats type for #863 - Add Attestation type for #862 Co-Authored-By: Claude Haiku 4.5 --- src/types.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/src/types.ts b/src/types.ts index b60a6ec..b2b011b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -2150,3 +2150,67 @@ export interface ClaimableBalanceRecord { /** Ledger sequence after which the predicate expires (optional). */ predicateExpiryLedger?: number; } + +// --------------------------------------------------------------------------- +// Invoice Rating Types (Issue #865) +// --------------------------------------------------------------------------- + +/** Creator rating information. */ +export interface CreatorRating { + /** Total number of ratings received by the creator. */ + totalRatings: bigint; + /** Average star rating as a float (e.g. 4.3). */ + averageStars: number; +} + +// --------------------------------------------------------------------------- +// Deadline Extension Types (Issue #864) +// --------------------------------------------------------------------------- + +/** Extension status for an invoice deadline. */ +export interface ExtensionStatus { + /** Current number of votes for extension. */ + voteCount: bigint; + /** Minimum number of votes required (quorum). */ + quorumRequired: bigint; + /** Number of times the deadline has been extended. */ + extensionCount: bigint; + /** Maximum allowed extensions. */ + maxExtensions: bigint; + /** Current deadline timestamp. */ + currentDeadline: bigint; +} + +// --------------------------------------------------------------------------- +// Group Management Types (Issue #863) +// --------------------------------------------------------------------------- + +/** Statistics for an invoice group. */ +export interface GroupStats { + /** Group name. */ + name: string; + /** Total target amount for all invoices in the group. */ + totalTarget: bigint; + /** Total funded amount for all invoices in the group. */ + totalFunded: bigint; + /** Number of invoices in the group. */ + invoiceCount: bigint; + /** Number of fully funded invoices in the group. */ + fullyFundedCount: bigint; +} + +// --------------------------------------------------------------------------- +// Attestation Types (Issue #862) +// --------------------------------------------------------------------------- + +/** Invoice attestation record. */ +export interface Attestation { + /** Address of the attester. */ + attester: string; + /** Attestation statement (max 256 chars). */ + statement: string; + /** Unix timestamp when the attestation was created. */ + timestamp: bigint; + /** Whether the attestation has been revoked. */ + revoked: boolean; +} From 8f73f3cddc03bffb16ab9f9b9d2d36b908396edd Mon Sep 17 00:00:00 2001 From: ehonrie Date: Thu, 24 Sep 2026 21:28:52 +0100 Subject: [PATCH 3/5] feat: implement attestation, group, extension voting, and rating methods Issue #862: Add attestation management - attestInvoice(invoiceId, statement, payer) - revokeAttestation(invoiceId, payer) - getAttestations(invoiceId) Issue #863: Add campaign group management - createGroup(creator, name, description) - addInvoiceToGroup(creator, groupId, invoiceId) - getGroupStats(groupId) - getGroupInvoices(groupId) Issue #864: Add deadline extension voting - voteExtendDeadline(invoiceId, payer) - getExtensionStatus(invoiceId) Issue #865: Add invoice rating system - rateInvoice(invoiceId, stars, payer) - getCreatorRating(creator) Co-Authored-By: Claude Haiku 4.5 --- src/client.ts | 254 +++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 253 insertions(+), 1 deletion(-) diff --git a/src/client.ts b/src/client.ts index 8b44892..c422ab9 100644 --- a/src/client.ts +++ b/src/client.ts @@ -94,7 +94,13 @@ import type { CircuitBreakerStateSnapshot, } from "./resilience/CircuitBreaker.js"; import type { WaterfallPlan } from "./types/routing.js"; -import { WaterfallInsufficientFundsError } from "./errors.js"; +import { + WaterfallInsufficientFundsError, + InvalidAttestationError, + AlreadyRatedError, + InvoiceNotReleasedForRatingError, + NotEligibleToVoteError, +} from "./errors.js"; import { OptimisticCache } from "./cache/OptimisticCache.js"; import type { CommitFn, RollbackFn } from "./cache/OptimisticCache.js"; import { getOptimisticInvoice } from "./optimistic.js"; @@ -156,6 +162,10 @@ import type { BridgePaymentParams, BridgePaymentRequest, SignedBridgeProof, + Attestation, + CreatorRating, + ExtensionStatus, + GroupStats, } from "./types.js"; import { estimateBridgeFee as _estimateBridgeFee, @@ -4968,6 +4978,248 @@ export class StellarSplitClient extends TypedEventEmitter { return { txHash: result.txHash }; } + /** + * Attest an invoice with a statement. + * @param invoiceId - Invoice ID to attest + * @param statement - Attestation statement (max 256 chars) + * @param payer - Payer address + * @returns Transaction hash + * @throws {InvalidAttestationError} if statement exceeds 256 chars + * @throws {Error} If the method fails. + */ + async attestInvoice( + invoiceId: string, + statement: string, + payer: string + ): Promise { + if (statement.length > 256) { + throw new InvalidAttestationError("Statement must not exceed 256 characters"); + } + + const operation = this.contract.call( + "attest_invoice", + nativeToScVal(BigInt(invoiceId), { type: "u64" }), + nativeToScVal(statement, { type: "string" }), + nativeToScVal(payer, { type: "address" }) + ); + + const result = await this._submitTx(payer, operation); + return { txHash: result.txHash }; + } + + /** + * Revoke an attestation on an invoice. + * @param invoiceId - Invoice ID + * @param payer - Payer address + * @returns Transaction hash + * @throws {Error} If the method fails. + */ + async revokeAttestation(invoiceId: string, payer: string): Promise { + const operation = this.contract.call( + "revoke_attestation", + nativeToScVal(BigInt(invoiceId), { type: "u64" }), + nativeToScVal(payer, { type: "address" }) + ); + + const result = await this._submitTx(payer, operation); + return { txHash: result.txHash }; + } + + /** + * Get all attestations for an invoice. + * @param invoiceId - Invoice ID + * @returns Array of attestations + * @throws {Error} If the method fails. + */ + async getAttestations(invoiceId: string): Promise { + const operation = this.contract.call( + "get_attestations", + nativeToScVal(BigInt(invoiceId), { type: "u64" }) + ); + + const raw = (await this._simulateView(operation)) as Array>; + return raw.map((a) => ({ + attester: a.attester as string, + statement: a.statement as string, + timestamp: BigInt(a.timestamp as string | number), + revoked: Boolean(a.revoked), + })); + } + + /** + * Create a campaign group. + * @param creator - Creator address + * @param name - Group name + * @param description - Group description + * @returns Group ID and transaction hash + * @throws {Error} If the method fails. + */ + async createGroup( + creator: string, + name: string, + description: string + ): Promise<{ groupId: string; txHash: string }> { + const operation = this.contract.call( + "create_group", + nativeToScVal(creator, { type: "address" }), + nativeToScVal(name, { type: "string" }), + nativeToScVal(description, { type: "string" }) + ); + + const result = await this._submitTx(creator, operation); + const groupId = scValToNative(result.returnValue).toString(); + return { groupId, txHash: result.txHash }; + } + + /** + * Add an invoice to a group. + * @param creator - Creator address + * @param groupId - Group ID + * @param invoiceId - Invoice ID to add + * @returns Transaction hash + * @throws {Error} If caller is not the group owner or other errors. + */ + async addInvoiceToGroup( + creator: string, + groupId: string, + invoiceId: string + ): Promise { + const operation = this.contract.call( + "add_invoice_to_group", + nativeToScVal(creator, { type: "address" }), + nativeToScVal(BigInt(groupId), { type: "u64" }), + nativeToScVal(BigInt(invoiceId), { type: "u64" }) + ); + + const result = await this._submitTx(creator, operation); + return { txHash: result.txHash }; + } + + /** + * Get statistics for a group. + * @param groupId - Group ID + * @returns Group statistics + * @throws {Error} If the method fails. + */ + async getGroupStats(groupId: string): Promise { + const operation = this.contract.call( + "get_group_stats", + nativeToScVal(BigInt(groupId), { type: "u64" }) + ); + + const raw = (await this._simulateView(operation)) as Record; + return { + name: raw.name as string, + totalTarget: BigInt(raw.totalTarget as string | number), + totalFunded: BigInt(raw.totalFunded as string | number), + invoiceCount: BigInt(raw.invoiceCount as string | number), + fullyFundedCount: BigInt(raw.fullyFundedCount as string | number), + }; + } + + /** + * Get invoices in a group. + * @param groupId - Group ID + * @returns Array of invoice IDs in the group + * @throws {Error} If the method fails. + */ + async getGroupInvoices(groupId: string): Promise { + const operation = this.contract.call( + "get_group_invoices", + nativeToScVal(BigInt(groupId), { type: "u64" }) + ); + + const raw = (await this._simulateView(operation)) as (string | number)[]; + return raw.map((id) => BigInt(id)); + } + + /** + * Vote to extend a deadline. + * @param invoiceId - Invoice ID + * @param payer - Payer address (must be a contributor) + * @returns Transaction hash + * @throws {NotEligibleToVoteError} if caller has not contributed + * @throws {Error} If the method fails. + */ + async voteExtendDeadline(invoiceId: string, payer: string): Promise { + const operation = this.contract.call( + "vote_extend_deadline", + nativeToScVal(BigInt(invoiceId), { type: "u64" }), + nativeToScVal(payer, { type: "address" }) + ); + + const result = await this._submitTx(payer, operation); + return { txHash: result.txHash }; + } + + /** + * Get deadline extension status for an invoice. + * @param invoiceId - Invoice ID + * @returns Extension status + * @throws {Error} If the method fails. + */ + async getExtensionStatus(invoiceId: string): Promise { + const operation = this.contract.call( + "get_extension_status", + nativeToScVal(BigInt(invoiceId), { type: "u64" }) + ); + + const raw = (await this._simulateView(operation)) as Record; + return { + voteCount: BigInt(raw.voteCount as string | number), + quorumRequired: BigInt(raw.quorumRequired as string | number), + extensionCount: BigInt(raw.extensionCount as string | number), + maxExtensions: BigInt(raw.maxExtensions as string | number), + currentDeadline: BigInt(raw.currentDeadline as string | number), + }; + } + + /** + * Rate an invoice. + * @param invoiceId - Invoice ID to rate + * @param stars - Star rating (1-5) + * @param payer - Payer address + * @returns Transaction hash + * @throws {InvoiceNotReleasedForRatingError} if invoice is not released + * @throws {AlreadyRatedError} if caller has already rated + * @throws {Error} If the method fails. + */ + async rateInvoice(invoiceId: string, stars: 1 | 2 | 3 | 4 | 5, payer: string): Promise { + const operation = this.contract.call( + "rate_invoice", + nativeToScVal(BigInt(invoiceId), { type: "u64" }), + nativeToScVal(BigInt(stars), { type: "u32" }), + nativeToScVal(payer, { type: "address" }) + ); + + const result = await this._submitTx(payer, operation); + return { txHash: result.txHash }; + } + + /** + * Get the creator's rating information. + * @param creator - Creator address + * @returns Creator rating with total ratings and average stars + * @throws {Error} If the method fails. + */ + async getCreatorRating(creator: string): Promise { + const operation = this.contract.call( + "get_creator_rating", + nativeToScVal(creator, { type: "address" }) + ); + + const raw = (await this._simulateView(operation)) as Record; + const totalRatings = BigInt(raw.totalRatings as string | number); + const totalStars = BigInt(raw.totalStars as string | number); + const averageStars = + totalRatings > 0n ? Number(totalStars) / Number(totalRatings) : 0; + + return { + totalRatings, + averageStars, + }; + } + /** * Calculate the protocol fee for a given amount. * From 113cfe63402c65b51dbe18241a3662d78070bb63 Mon Sep 17 00:00:00 2001 From: ehonrie Date: Thu, 24 Sep 2026 21:29:26 +0100 Subject: [PATCH 4/5] feat: export new types and errors for attestations, groups, extensions, and ratings Export Attestation, GroupStats, ExtensionStatus, and CreatorRating types. Export NotEligibleToVoteError, AlreadyRatedError, and InvoiceNotReleasedForRatingError. Co-Authored-By: Claude Haiku 4.5 --- src/index.ts | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/src/index.ts b/src/index.ts index f9cb56b..33b32d3 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1427,3 +1427,37 @@ export type { SubmitTransactionOptions, SubmitServer, } from "./transaction/submit.js"; + +// --------------------------------------------------------------------------- +// #862 — Invoice Attestation System +// --------------------------------------------------------------------------- + +export type { Attestation } from "./types.js"; + +// --------------------------------------------------------------------------- +// #863 — Campaign Group Management +// --------------------------------------------------------------------------- + +export type { GroupStats } from "./types.js"; + +// --------------------------------------------------------------------------- +// #864 — Deadline Extension Voting +// --------------------------------------------------------------------------- + +export type { ExtensionStatus } from "./types.js"; +export { + NotEligibleToVoteError, + isNotEligibleToVoteError, +} from "./errors.js"; + +// --------------------------------------------------------------------------- +// #865 — Invoice Rating System +// --------------------------------------------------------------------------- + +export type { CreatorRating } from "./types.js"; +export { + AlreadyRatedError, + isAlreadyRatedError, + InvoiceNotReleasedForRatingError, + isInvoiceNotReleasedForRatingError, +} from "./errors.js"; From 9074d1098c308295a6b4d77aae5c85fca7176def Mon Sep 17 00:00:00 2001 From: ehonrie Date: Thu, 24 Sep 2026 21:31:01 +0100 Subject: [PATCH 5/5] test: add comprehensive tests for all 4 new features Tests cover: - Issue #862: attestInvoice, revokeAttestation, getAttestations - Issue #863: createGroup, addInvoiceToGroup, getGroupStats, getGroupInvoices - Issue #864: voteExtendDeadline, getExtensionStatus - Issue #865: rateInvoice, getCreatorRating with average calculation Co-Authored-By: Claude Haiku 4.5 --- test/new-features.test.ts | 231 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 231 insertions(+) create mode 100644 test/new-features.test.ts diff --git a/test/new-features.test.ts b/test/new-features.test.ts new file mode 100644 index 0000000..28f3c94 --- /dev/null +++ b/test/new-features.test.ts @@ -0,0 +1,231 @@ +import { describe, it, expect, beforeEach, vi } from "vitest"; +import { StellarSplitClient } from "../src/client.js"; +import { + Attestation, + CreatorRating, + ExtensionStatus, + GroupStats, +} from "../src/types.js"; +import { + AlreadyRatedError, + NotEligibleToVoteError, + InvoiceNotReleasedForRatingError, +} from "../src/errors.js"; + +describe("Issue #862: Attestation System", () => { + let mockClient: any; + + beforeEach(() => { + mockClient = { + _submitTx: vi.fn().mockResolvedValue({ txHash: "mock-tx-hash" }), + _simulateView: vi.fn(), + contract: { call: vi.fn() }, + }; + }); + + it("should attest an invoice with a valid statement", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + const result = await client.attestInvoice("123", "Great work!", "payer-addr"); + expect(result.txHash).toBe("mock-tx-hash"); + }); + + it("should reject statements exceeding 256 characters", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + const longStatement = "x".repeat(257); + await expect( + client.attestInvoice("123", longStatement, "payer-addr") + ).rejects.toThrow(); + }); + + it("should revoke an attestation", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + const result = await client.revokeAttestation("123", "payer-addr"); + expect(result.txHash).toBe("mock-tx-hash"); + }); + + it("should get attestations for an invoice", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue([ + { attester: "addr1", statement: "Good", timestamp: "1000", revoked: false }, + { attester: "addr2", statement: "Great", timestamp: "2000", revoked: true }, + ]); + + const attestations = await client.getAttestations("123"); + expect(attestations).toHaveLength(2); + expect(attestations[0].statement).toBe("Good"); + expect(attestations[1].revoked).toBe(true); + }); +}); + +describe("Issue #863: Campaign Group Management", () => { + let mockClient: any; + + beforeEach(() => { + mockClient = { + _submitTx: vi.fn().mockResolvedValue({ + txHash: "mock-tx-hash", + returnValue: { _value: "456" }, + }), + _simulateView: vi.fn(), + contract: { call: vi.fn() }, + }; + }); + + it("should create a group", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + // Mock scValToNative + vi.stubGlobal("scValToNative", (val: any) => val._value); + + const result = await client.createGroup("creator", "My Group", "Description"); + expect(result.groupId).toBe("456"); + expect(result.txHash).toBe("mock-tx-hash"); + }); + + it("should add invoice to a group", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + const result = await client.addInvoiceToGroup("creator", "456", "789"); + expect(result.txHash).toBe("mock-tx-hash"); + }); + + it("should get group statistics", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue({ + name: "Group 1", + totalTarget: "1000000000", + totalFunded: "500000000", + invoiceCount: "5", + fullyFundedCount: "2", + }); + + const stats = await client.getGroupStats("456"); + expect(stats.name).toBe("Group 1"); + expect(stats.invoiceCount).toBe(5n); + expect(stats.fullyFundedCount).toBe(2n); + }); + + it("should get group invoices", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue(["1", "2", "3"]); + + const invoices = await client.getGroupInvoices("456"); + expect(invoices).toEqual([1n, 2n, 3n]); + }); +}); + +describe("Issue #864: Deadline Extension Voting", () => { + let mockClient: any; + + beforeEach(() => { + mockClient = { + _submitTx: vi.fn().mockResolvedValue({ txHash: "mock-tx-hash" }), + _simulateView: vi.fn(), + contract: { call: vi.fn() }, + }; + }); + + it("should vote to extend deadline", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + const result = await client.voteExtendDeadline("123", "payer-addr"); + expect(result.txHash).toBe("mock-tx-hash"); + }); + + it("should get extension status", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue({ + voteCount: "3", + quorumRequired: "5", + extensionCount: "1", + maxExtensions: "3", + currentDeadline: "1700000000", + }); + + const status = await client.getExtensionStatus("123"); + expect(status.voteCount).toBe(3n); + expect(status.quorumRequired).toBe(5n); + expect(status.extensionCount).toBe(1n); + expect(status.maxExtensions).toBe(3n); + }); +}); + +describe("Issue #865: Invoice Rating System", () => { + let mockClient: any; + + beforeEach(() => { + mockClient = { + _submitTx: vi.fn().mockResolvedValue({ txHash: "mock-tx-hash" }), + _simulateView: vi.fn(), + contract: { call: vi.fn() }, + }; + }); + + it("should rate an invoice with valid stars (1-5)", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + for (const stars of [1, 2, 3, 4, 5] as const) { + const result = await client.rateInvoice("123", stars, "payer-addr"); + expect(result.txHash).toBe("mock-tx-hash"); + } + }); + + it("should get creator rating", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue({ + totalRatings: "10", + totalStars: "43", + }); + + const rating = await client.getCreatorRating("creator-addr"); + expect(rating.totalRatings).toBe(10n); + expect(rating.averageStars).toBeCloseTo(4.3, 1); + }); + + it("should compute average stars correctly", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue({ + totalRatings: "3", + totalStars: "14", + }); + + const rating = await client.getCreatorRating("creator-addr"); + expect(rating.averageStars).toBeCloseTo(4.67, 1); + }); + + it("should handle zero ratings gracefully", async () => { + const client = Object.create(StellarSplitClient.prototype); + Object.assign(client, mockClient); + + mockClient._simulateView.mockResolvedValue({ + totalRatings: "0", + totalStars: "0", + }); + + const rating = await client.getCreatorRating("creator-addr"); + expect(rating.totalRatings).toBe(0n); + expect(rating.averageStars).toBe(0); + }); +});