Skip to content

Commit b280ce1

Browse files
authored
Merge pull request #451 from therealjhay/Resolve-issue-#412
Resolve issue #412
2 parents 6782bb4 + 51c0dcf commit b280ce1

3 files changed

Lines changed: 245 additions & 0 deletions

File tree

sdk/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ export {
7979
} from "./methods/insurance.js";
8080
export type { InsurancePoolInfo } from "@invoice-liquidity/types";
8181
export { getDistributionAccrual } from "./methods/distribution.js";
82+
export { getReferralStats } from "./methods/referralStats.js";
8283
export { TokenRegistry, tokenRegistry } from "./utils/tokenRegistry.js";
8384
export type { TokenInfo, NetworkName } from "./utils/tokenRegistry.js";
8485
export {

sdk/src/methods/referralStats.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/**
2+
* getReferralStats — query referral statistics (referral count) for a given
3+
* referral code hash from the on-chain ILN contract.
4+
*
5+
* Wraps the `get_referral_stats(BytesN<32>) -> u64` view function.
6+
*/
7+
8+
import {
9+
Contract,
10+
SorobanRpc,
11+
TransactionBuilder,
12+
Account,
13+
BASE_FEE,
14+
scValToNative,
15+
xdr,
16+
Networks,
17+
} from "@stellar/stellar-sdk";
18+
import { retry } from "../utils/retry.js";
19+
20+
// ---------------------------------------------------------------------------
21+
// Hex validation
22+
// ---------------------------------------------------------------------------
23+
24+
const HEX64_RE = /^[0-9a-fA-F]{64}$/;
25+
26+
// ---------------------------------------------------------------------------
27+
// getReferralStats
28+
// ---------------------------------------------------------------------------
29+
30+
/**
31+
* Query the number of referrals attributed to a given referral code.
32+
*
33+
* Performs a read-only Soroban simulation — no on-chain mutation, no
34+
* transaction fees, and no signer required.
35+
*
36+
* @param server - Soroban RPC server for the target network
37+
* @param contractId - Deployed invoice-liquidity contract address
38+
* @param referralCodeHex - 64-character hex string (32 bytes) representing the referral code hash
39+
* @param networkPassphrase - Stellar network passphrase (default: TESTNET)
40+
* @returns Number of referrals for the given code
41+
*
42+
* @throws When `referralCodeHex` is not a valid 64-character hex string
43+
* @throws When the Soroban simulation fails (RPC unreachable, contract not found)
44+
*
45+
* @example
46+
* ```ts
47+
* const count = await getReferralStats(server, CONTRACT_ID, "ab12...cd34");
48+
* console.log(`Referrals: ${count}`);
49+
* ```
50+
*/
51+
export async function getReferralStats(
52+
server: SorobanRpc.Server,
53+
contractId: string,
54+
referralCodeHex: string,
55+
networkPassphrase: string = Networks.TESTNET
56+
): Promise<number> {
57+
// Strip optional 0x prefix
58+
const cleaned = referralCodeHex.startsWith("0x")
59+
? referralCodeHex.slice(2)
60+
: referralCodeHex;
61+
62+
if (!HEX64_RE.test(cleaned)) {
63+
throw new Error(
64+
`Invalid referral code: "${referralCodeHex}". Expected a 64-character hex string (32 bytes) optionally prefixed with "0x".`
65+
);
66+
}
67+
68+
// Decode hex to raw 32-byte buffer
69+
const referralCodeBytes = Buffer.from(cleaned, "hex");
70+
71+
const contract = new Contract(contractId);
72+
const op = contract.call(
73+
"get_referral_stats",
74+
xdr.ScVal.scvBytes(referralCodeBytes)
75+
);
76+
77+
const sourceAccount = new Account(
78+
"GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAWHF",
79+
"0"
80+
);
81+
82+
const simTx = new TransactionBuilder(sourceAccount, {
83+
fee: BASE_FEE,
84+
networkPassphrase,
85+
})
86+
.addOperation(op)
87+
.setTimeout(30)
88+
.build();
89+
90+
const sim = await retry(() => server.simulateTransaction(simTx));
91+
92+
if (SorobanRpc.Api.isSimulationError(sim)) {
93+
throw new Error(`get_referral_stats simulation failed: ${sim.error}`);
94+
}
95+
96+
if (!sim.result?.retval) {
97+
return 0;
98+
}
99+
100+
const raw = scValToNative(sim.result.retval);
101+
return Number(raw ?? 0n);
102+
}

sdk/tests/referralStats.test.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { vi, describe, it, expect, beforeEach } from "vitest";
2+
import { getReferralStats } from "../src/methods/referralStats.js";
3+
import {
4+
SorobanRpc,
5+
xdr,
6+
} from "@stellar/stellar-sdk";
7+
8+
function mockSimulationResult(retval: xdr.ScVal | null) {
9+
return {
10+
result: retval ? { retval } : undefined,
11+
};
12+
}
13+
14+
describe("getReferralStats", () => {
15+
const mockServer = {
16+
simulateTransaction: vi.fn(),
17+
} as unknown as SorobanRpc.Server;
18+
19+
const CONTRACT_ID = "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4";
20+
const VALID_HEX = "ab" + "01".repeat(31); // 64 hex chars (32 bytes)
21+
const VALID_HEX_WITH_PREFIX = "0x" + VALID_HEX;
22+
23+
beforeEach(() => {
24+
vi.clearAllMocks();
25+
});
26+
27+
// ---------------------------------------------------------------------------
28+
// Success path
29+
// ---------------------------------------------------------------------------
30+
31+
it("returns the referral count for a valid hex code", async () => {
32+
const retval = xdr.ScVal.scvU64(xdr.Uint64.fromString("42"));
33+
vi.mocked(mockServer.simulateTransaction).mockResolvedValue(
34+
mockSimulationResult(retval) as any
35+
);
36+
37+
const count = await getReferralStats(
38+
mockServer,
39+
CONTRACT_ID,
40+
VALID_HEX
41+
);
42+
43+
expect(count).toBe(42);
44+
expect(mockServer.simulateTransaction).toHaveBeenCalledTimes(1);
45+
});
46+
47+
it("strips the 0x prefix and returns the referral count", async () => {
48+
const retval = xdr.ScVal.scvU64(xdr.Uint64.fromString("7"));
49+
vi.mocked(mockServer.simulateTransaction).mockResolvedValue(
50+
mockSimulationResult(retval) as any
51+
);
52+
53+
const count = await getReferralStats(
54+
mockServer,
55+
CONTRACT_ID,
56+
VALID_HEX_WITH_PREFIX
57+
);
58+
59+
expect(count).toBe(7);
60+
});
61+
62+
// ---------------------------------------------------------------------------
63+
// Zero default
64+
// ---------------------------------------------------------------------------
65+
66+
it("returns 0 when the simulation returns no retval", async () => {
67+
vi.mocked(mockServer.simulateTransaction).mockResolvedValue(
68+
mockSimulationResult(null) as any
69+
);
70+
71+
const count = await getReferralStats(
72+
mockServer,
73+
CONTRACT_ID,
74+
VALID_HEX
75+
);
76+
77+
expect(count).toBe(0);
78+
});
79+
80+
// ---------------------------------------------------------------------------
81+
// Hex validation errors
82+
// ---------------------------------------------------------------------------
83+
84+
it("throws when the hex string is too short", async () => {
85+
await expect(
86+
getReferralStats(mockServer, CONTRACT_ID, "abc123")
87+
).rejects.toThrow(/invalid referral code/i);
88+
expect(mockServer.simulateTransaction).not.toHaveBeenCalled();
89+
});
90+
91+
it("throws when the hex string is too long", async () => {
92+
await expect(
93+
getReferralStats(mockServer, CONTRACT_ID, VALID_HEX + "ff")
94+
).rejects.toThrow(/invalid referral code/i);
95+
expect(mockServer.simulateTransaction).not.toHaveBeenCalled();
96+
});
97+
98+
it("throws when the hex string contains non-hex characters", async () => {
99+
await expect(
100+
getReferralStats(mockServer, CONTRACT_ID, "z" + "01".repeat(31) + "xy")
101+
).rejects.toThrow(/invalid referral code/i);
102+
expect(mockServer.simulateTransaction).not.toHaveBeenCalled();
103+
});
104+
105+
it("throws when the hex string is empty", async () => {
106+
await expect(
107+
getReferralStats(mockServer, CONTRACT_ID, "")
108+
).rejects.toThrow(/invalid referral code/i);
109+
expect(mockServer.simulateTransaction).not.toHaveBeenCalled();
110+
});
111+
112+
it("throws when given an invalid 0x-prefixed hex string", async () => {
113+
await expect(
114+
getReferralStats(mockServer, CONTRACT_ID, "0x123")
115+
).rejects.toThrow(/invalid referral code/i);
116+
expect(mockServer.simulateTransaction).not.toHaveBeenCalled();
117+
});
118+
119+
// ---------------------------------------------------------------------------
120+
// Simulation errors
121+
// ---------------------------------------------------------------------------
122+
123+
it("throws when the simulation returns an error", async () => {
124+
vi.mocked(mockServer.simulateTransaction).mockResolvedValue({
125+
error: "Contract error: not found",
126+
} as any);
127+
128+
await expect(
129+
getReferralStats(mockServer, CONTRACT_ID, VALID_HEX)
130+
).rejects.toThrow(/simulation failed/i);
131+
});
132+
133+
it("throws when the simulation fails with a SorobanRPC error", async () => {
134+
vi.mocked(mockServer.simulateTransaction).mockResolvedValue({
135+
error: "Contract error #4",
136+
} as any);
137+
138+
await expect(
139+
getReferralStats(mockServer, CONTRACT_ID, VALID_HEX)
140+
).rejects.toThrow(/get_referral_stats simulation failed/i);
141+
});
142+
});

0 commit comments

Comments
 (0)