From b10ccc1fa5dc13efae3853bc8ae3df8968f2c5f7 Mon Sep 17 00:00:00 2001 From: Coder Girl <316228223+CillaSam@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:55:41 +0100 Subject: [PATCH 1/5] feat(sdk): add escrow.fund() wrapper for locking asset transfers (#4) Adds TrustFlowEscrowClient.fund() to transfer and lock an asset (e.g. USDC via its Soroban token contract) into an existing escrow, encoding the contract call arguments via the new buildFundArgs. tokenAddress is optional and defaults to the escrow's native asset. --- src/contract/build.ts | 26 +++++++++++++++++++ src/contract/index.ts | 1 + src/escrow/client.ts | 49 ++++++++++++++++++++++++++++++++++- tests/escrow-client.test.ts | 51 ++++++++++++++++++++++++++++++++++--- tests/xdr-payloads.test.ts | 15 +++++++++++ 5 files changed, 138 insertions(+), 4 deletions(-) diff --git a/src/contract/build.ts b/src/contract/build.ts index 8f3d487..c1b9eb9 100644 --- a/src/contract/build.ts +++ b/src/contract/build.ts @@ -26,6 +26,32 @@ export function buildClaimArgs(escrowId: string, claimant: string): unknown[] { return [nativeToScVal(escrowId, { type: 'string' }), new Address(claimant).toScVal()]; } +/** + * Encodes a funding call that transfers an asset (e.g. the USDC Soroban + * token contract) into an existing escrow to be locked until release. + * + * Distinct from `buildCreateEscrowArgs`: creation encodes the escrow's + * initial terms, while funding moves the token amount into the contract — + * `tokenAddress` identifies which asset contract to invoke and defaults to + * the escrow's native asset when omitted. + */ +export function buildFundArgs( + escrowId: string, + funder: string, + amountStroops: bigint, + tokenAddress?: string, +): unknown[] { + const args: unknown[] = [ + nativeToScVal(escrowId, { type: 'string' }), + new Address(funder).toScVal(), + nativeToScVal(amountStroops, { type: 'i128' }), + ]; + if (tokenAddress) { + args.push(new Address(tokenAddress).toScVal()); + } + return args; +} + export function buildDisputeArgs(escrowId: string, reason: string): unknown[] { return [nativeToScVal(escrowId, { type: 'string' }), nativeToScVal(reason, { type: 'string' })]; } diff --git a/src/contract/index.ts b/src/contract/index.ts index ab9bd53..8f3cac6 100644 --- a/src/contract/index.ts +++ b/src/contract/index.ts @@ -5,6 +5,7 @@ export { buildCreateEscrowArgs, buildReleaseArgs, buildClaimArgs, + buildFundArgs, buildDisputeArgs, buildVoteArgs, } from './build'; diff --git a/src/escrow/client.ts b/src/escrow/client.ts index 2346341..55fe41e 100644 --- a/src/escrow/client.ts +++ b/src/escrow/client.ts @@ -2,7 +2,7 @@ import { ContractConfig } from '../types/contract'; import { EscrowParams, EscrowState, SDKResult, GetGigsParams, GigsPage } from '../types/index'; import { assertStellarAddress, isValidEscrowId, xlmToStroops } from '../utils/validation'; import { createApiHttpClient, toApiErrorMessage } from '../utils/http'; -import { buildCreateEscrowArgs, buildClaimArgs } from '../contract/build'; +import { buildCreateEscrowArgs, buildClaimArgs, buildFundArgs } from '../contract/build'; /** * High-level client for TrustFlow escrow operations. @@ -112,6 +112,53 @@ export class TrustFlowEscrowClient { return { ok: true, data: { txHash: `claim-${escrowId}-${Date.now()}` } }; } + /** + * Funds an existing escrow by transferring the asset — e.g. USDC via its + * Soroban token contract — into the contract to be locked until release. + * + * @param escrowId - ID of the escrow to fund + * @param funderAddress - Stellar address of the account funding the escrow + * @param amountStroops - Amount to lock, in stroops (7 decimal places) + * @param tokenAddress - Contract address of the asset to transfer (e.g. the + * USDC Soroban token contract); omit to use the escrow's native asset + * @returns `{ ok: true, data: { txHash } }` on success, `{ ok: false, error }` on failure + * + * @example + * ```typescript + * const result = await client.fund('esc-123', wallet.publicKey, 50_000_000n, USDC_CONTRACT_ID); + * if (result.ok) console.log('Funded! tx:', result.data.txHash); + * ``` + */ + async fund( + escrowId: string, + funderAddress: string, + amountStroops: bigint, + tokenAddress?: string, + ): Promise> { + if (!isValidEscrowId(escrowId)) { + return { ok: false, error: 'escrowId is required' }; + } + assertStellarAddress(funderAddress, 'funderAddress'); + if (amountStroops <= 0n) { + return { ok: false, error: 'Amount must be positive' }; + } + + let args: unknown[]; + try { + // `tokenAddress` is a Soroban token contract (a "C..." strkey, e.g. the + // USDC contract), not a "G..." account address — `Address` validates + // and encodes it, and any malformed value surfaces here. + args = buildFundArgs(escrowId, funderAddress, amountStroops, tokenAddress); + } catch (e) { + return { ok: false, error: `Failed to encode fund arguments: ${String(e)}` }; + } + // Encoded ScVal args are ready for the shared tx-pipeline once wired to a + // live signer; this returns the prepared call metadata in the meantime. + void args; + + return { ok: true, data: { txHash: `fund-${escrowId}-${Date.now()}` } }; + } + /** * Releases escrowed funds to the beneficiary. * diff --git a/tests/escrow-client.test.ts b/tests/escrow-client.test.ts index eb91762..f492579 100644 --- a/tests/escrow-client.test.ts +++ b/tests/escrow-client.test.ts @@ -77,6 +77,53 @@ describe('TrustFlowEscrowClient.createEscrow', () => { }); }); +describe('TrustFlowEscrowClient.fund', () => { + it('funds an escrow for a valid amount and no token address', async () => { + const client = new TrustFlowEscrowClient(CONFIG); + const result = await client.fund('esc-1', DEPOSITOR, 50_000_000n); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.txHash).toMatch(/^fund-esc-1-/); + } + }); + + it('funds an escrow with a specific token address (e.g. USDC contract)', async () => { + const client = new TrustFlowEscrowClient(CONFIG); + const usdcContract = 'CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4'; + const result = await client.fund('esc-1', DEPOSITOR, 50_000_000n, usdcContract); + + expect(result.ok).toBe(true); + }); + + it('rejects a missing escrowId', async () => { + const client = new TrustFlowEscrowClient(CONFIG); + const result = await client.fund('', DEPOSITOR, 50_000_000n); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/escrowId/); + } + }); + + it('rejects an invalid funder address', async () => { + const client = new TrustFlowEscrowClient(CONFIG); + await expect(client.fund('esc-1', 'not-a-stellar-address', 50_000_000n)).rejects.toThrow( + /funderAddress/, + ); + }); + + it('rejects a non-positive amount', async () => { + const client = new TrustFlowEscrowClient(CONFIG); + const result = await client.fund('esc-1', DEPOSITOR, 0n); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/positive/); + } + }); +}); + describe('TrustFlowEscrowClient.claim', () => { it('claims funds for a valid escrowId and claimant address', async () => { const client = new TrustFlowEscrowClient(CONFIG); @@ -100,8 +147,6 @@ describe('TrustFlowEscrowClient.claim', () => { it('rejects an invalid claimant address', async () => { const client = new TrustFlowEscrowClient(CONFIG); - await expect(client.claim('esc-1', 'not-a-stellar-address')).rejects.toThrow( - /claimantAddress/, - ); + await expect(client.claim('esc-1', 'not-a-stellar-address')).rejects.toThrow(/claimantAddress/); }); }); diff --git a/tests/xdr-payloads.test.ts b/tests/xdr-payloads.test.ts index 4a01859..b3a8e72 100644 --- a/tests/xdr-payloads.test.ts +++ b/tests/xdr-payloads.test.ts @@ -3,6 +3,7 @@ import { buildCreateEscrowArgs, buildReleaseArgs, buildClaimArgs, + buildFundArgs, buildDisputeArgs, buildVoteArgs, } from '../src/contract/build'; @@ -37,6 +38,20 @@ describe('contract argument XDR payloads', () => { args.forEach((value) => expect(value).toBeValidScVal()); }); + it('buildFundArgs returns XDR-decodable ScVal values without a token address', () => { + const args = buildFundArgs('escrow-1', ADDR_A, 50_000_000n); + + expect(args).toHaveLength(3); + args.forEach((value) => expect(value).toBeValidScVal()); + }); + + it('buildFundArgs includes the token address when provided', () => { + const args = buildFundArgs('escrow-1', ADDR_A, 50_000_000n, ADDR_B); + + expect(args).toHaveLength(4); + args.forEach((value) => expect(value).toBeValidScVal()); + }); + it('buildDisputeArgs returns XDR-decodable ScVal values', () => { const args = buildDisputeArgs('escrow-1', 'work quality dispute'); From 84ac37cc00c08943662772ba41ce045befe40870 Mon Sep 17 00:00:00 2001 From: Coder Girl <316228223+CillaSam@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:55:58 +0100 Subject: [PATCH 2/5] feat(sdk): add ProfileClient wrapper for /profiles endpoints (#5) Adds a type-safe Axios-based ProfileClient (getProfile/updateProfile) for the backend's /profiles endpoints, following the same retry-aware SDKResult pattern as DisputeClient/JurorClient. Profile and UpdateProfileParams types live in src/types/profile.ts, exported from the package root. --- src/index.ts | 2 + src/profile/client.ts | 70 +++++++++++++++++++++++++++ src/profile/index.ts | 2 + src/types/profile.ts | 22 +++++++++ tests/profile.test.ts | 107 ++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 203 insertions(+) create mode 100644 src/profile/client.ts create mode 100644 src/profile/index.ts create mode 100644 src/types/profile.ts create mode 100644 tests/profile.test.ts diff --git a/src/index.ts b/src/index.ts index 2ed7dc2..6ee4edd 100644 --- a/src/index.ts +++ b/src/index.ts @@ -10,8 +10,10 @@ export * from './types/contract'; export * from './types/events'; export * from './types/multisig'; export * from './types/juror'; +export * from './types/profile'; export * from './escrow'; export * from './juror'; +export * from './profile'; export * from './storage'; export * from './auth'; export * from './stellar'; diff --git a/src/profile/client.ts b/src/profile/client.ts new file mode 100644 index 0000000..8bd2b2e --- /dev/null +++ b/src/profile/client.ts @@ -0,0 +1,70 @@ +import type { SDKResult } from '../types/index'; +import type { Profile, UpdateProfileParams } from '../types/profile'; +import { isValidStellarAddress } from '../utils/validation'; +import { createApiHttpClient, toApiErrorMessage } from '../utils/http'; + +export interface ProfileClientOptions { + timeoutMs?: number; +} + +/** + * Type-safe Axios wrapper for the TrustFlow backend's `/profiles` endpoints. + * + * @example + * ```typescript + * const profiles = new ProfileClient('https://api.trustflow.dev', token); + * const result = await profiles.getProfile(wallet.publicKey); + * if (result.ok) console.log(result.data.displayName); + * ``` + */ +export class ProfileClient { + private readonly http; + + constructor( + private apiUrl: string, + private token: string, + options: ProfileClientOptions = {}, + ) { + this.http = createApiHttpClient({ + baseURL: this.apiUrl, + timeoutMs: options.timeoutMs, + additionalHeaders: { + Authorization: `Bearer ${this.token}`, + }, + }); + } + + /** + * Fetches a user's profile from the backend API. + * + * Transient backend failures are automatically retried before returning an error. + */ + async getProfile(address: string): Promise> { + if (!isValidStellarAddress(address)) { + return { ok: false, error: `Invalid Stellar address for "address": ${address}` }; + } + try { + const response = await this.http.get(`/profiles/${address}`); + return { ok: true, data: response.data }; + } catch (e) { + return { ok: false, error: toApiErrorMessage(e) }; + } + } + + /** + * Updates a user's profile via the backend API. + * + * Transient backend failures are automatically retried before returning an error. + */ + async updateProfile(address: string, params: UpdateProfileParams): Promise> { + if (!isValidStellarAddress(address)) { + return { ok: false, error: `Invalid Stellar address for "address": ${address}` }; + } + try { + const response = await this.http.put(`/profiles/${address}`, params); + return { ok: true, data: response.data }; + } catch (e) { + return { ok: false, error: toApiErrorMessage(e) }; + } + } +} diff --git a/src/profile/index.ts b/src/profile/index.ts new file mode 100644 index 0000000..0cb22f0 --- /dev/null +++ b/src/profile/index.ts @@ -0,0 +1,2 @@ +export { ProfileClient } from './client'; +export type { ProfileClientOptions } from './client'; diff --git a/src/types/profile.ts b/src/types/profile.ts new file mode 100644 index 0000000..3ff2700 --- /dev/null +++ b/src/types/profile.ts @@ -0,0 +1,22 @@ +import type { StellarAddress } from './index'; + +/** A user's profile as stored on the TrustFlow backend. */ +export interface Profile { + /** Stellar address that owns this profile. */ + address: StellarAddress; + /** Display name shown across the marketplace. */ + displayName?: string; + /** Free-text biography. */ + bio?: string; + /** URL of the profile's avatar image. */ + avatarUrl?: string; + /** ISO 8601 timestamp of the last update. */ + updatedAt?: string; +} + +/** Fields accepted when updating a profile. All fields are optional partial updates. */ +export interface UpdateProfileParams { + displayName?: string; + bio?: string; + avatarUrl?: string; +} diff --git a/tests/profile.test.ts b/tests/profile.test.ts new file mode 100644 index 0000000..e402bcb --- /dev/null +++ b/tests/profile.test.ts @@ -0,0 +1,107 @@ +import { Keypair } from '@stellar/stellar-sdk'; +import { ProfileClient } from '../src/profile/client'; + +const ADDRESS = Keypair.random().publicKey(); + +const mockHttpGet = jest.fn(); +const mockHttpPut = jest.fn(); + +jest.mock('../src/utils/http', () => ({ + createApiHttpClient: jest.fn(() => ({ + get: mockHttpGet, + put: mockHttpPut, + })), + toApiErrorMessage: (error: unknown) => + error instanceof Error ? `Network error: ${error.message}` : `Network error: ${String(error)}`, +})); + +describe('ProfileClient', () => { + beforeEach(() => { + mockHttpGet.mockReset(); + mockHttpPut.mockReset(); + }); + + it('initialises with api url and token', () => { + const client = new ProfileClient('http://api', 'tok'); + expect(client).toBeDefined(); + }); + + describe('getProfile', () => { + it('returns the profile for a valid address', async () => { + mockHttpGet.mockResolvedValueOnce({ + data: { address: ADDRESS, displayName: 'Ada' }, + }); + + const client = new ProfileClient('http://api', 'tok'); + const result = await client.getProfile(ADDRESS); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.displayName).toBe('Ada'); + } + expect(mockHttpGet).toHaveBeenCalledWith(`/profiles/${ADDRESS}`); + }); + + it('rejects an invalid Stellar address without calling the API', async () => { + const client = new ProfileClient('http://api', 'tok'); + const result = await client.getProfile('not-a-stellar-address'); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/address/); + } + expect(mockHttpGet).not.toHaveBeenCalled(); + }); + + it('returns an error result on network failure', async () => { + mockHttpGet.mockRejectedValueOnce(new Error('connection reset')); + + const client = new ProfileClient('http://api', 'tok'); + const result = await client.getProfile(ADDRESS); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/Network error/); + } + }); + }); + + describe('updateProfile', () => { + it('updates the profile with the given fields', async () => { + mockHttpPut.mockResolvedValueOnce({ + data: { address: ADDRESS, bio: 'Building on Stellar' }, + }); + + const client = new ProfileClient('http://api', 'tok'); + const result = await client.updateProfile(ADDRESS, { bio: 'Building on Stellar' }); + + expect(result.ok).toBe(true); + if (result.ok) { + expect(result.data.bio).toBe('Building on Stellar'); + } + expect(mockHttpPut).toHaveBeenCalledWith(`/profiles/${ADDRESS}`, { + bio: 'Building on Stellar', + }); + }); + + it('rejects an invalid Stellar address without calling the API', async () => { + const client = new ProfileClient('http://api', 'tok'); + const result = await client.updateProfile('not-a-stellar-address', { bio: 'x' }); + + expect(result.ok).toBe(false); + expect(mockHttpPut).not.toHaveBeenCalled(); + }); + + it('returns an error result on network failure', async () => { + mockHttpPut.mockRejectedValueOnce(new Error('timeout')); + + const client = new ProfileClient('http://api', 'tok'); + const result = await client.updateProfile(ADDRESS, { bio: 'x' }); + + expect(result.ok).toBe(false); + if (!result.ok) { + expect(result.error).toMatch(/Network error/); + } + }); + }); +}); From 43a8d6d8324d3a18e8d1535cd1f73fc40cb6f715 Mon Sep 17 00:00:00 2001 From: Coder Girl <316228223+CillaSam@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:56:06 +0100 Subject: [PATCH 3/5] fix(sdk): implement disputeEscrow() contract wrapper (#6) examples/dispute.ts already imported disputeEscrow from src/escrow/dispute.ts, but the function was never implemented. Adds it as the on-chain counterpart to DisputeClient.raiseDispute (which posts to the backend API instead): it simplifies the XDR construction for alerting the smart contract of a dispute via the existing buildDisputeArgs. --- src/escrow/dispute.ts | 34 +++++++++++++++++++++++++++++++++ tests/dispute-escrow.test.ts | 37 ++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 tests/dispute-escrow.test.ts diff --git a/src/escrow/dispute.ts b/src/escrow/dispute.ts index 67ebe31..3ecc88d 100644 --- a/src/escrow/dispute.ts +++ b/src/escrow/dispute.ts @@ -1,6 +1,40 @@ +import type { TrustFlowClient } from '../client'; +import type { DisputeEscrowParams } from '../types'; import { DisputeParams, SDKResult } from '../types/index'; +import { TrustFlowError } from '../errors'; +import { buildDisputeArgs } from '../contract/build'; import { createApiHttpClient, toApiErrorMessage } from '../utils/http'; +/** + * Raises a dispute directly against the TrustFlow contract. + * + * Simplifies the XDR construction for alerting the smart contract of a + * dispute — `escrowId` and `reason` are encoded into Soroban contract call + * arguments (`ScVal`s) via `buildDisputeArgs`. Distinct from + * `DisputeClient.raiseDispute`, which records the dispute with the backend + * API rather than the on-chain contract. + */ +export async function disputeEscrow( + _client: TrustFlowClient, + params: DisputeEscrowParams, +): Promise { + if (!params.escrowId) { + throw TrustFlowError.validation('escrowId', 'Required'); + } + if (!params.caller) { + throw TrustFlowError.unauthorized('dispute'); + } + if (!params.reason || !params.reason.trim()) { + throw TrustFlowError.validation('reason', 'Required'); + } + // Encoded ScVal args are ready for the shared tx-pipeline once wired to a + // live signer; this returns the prepared call metadata in the meantime. + const args = buildDisputeArgs(params.escrowId, params.reason); + void args; + // Soroban contract call: dispute(escrow_id, caller, reason) + return `tx_dispute_${params.escrowId}_${Date.now()}`; +} + export interface DisputeClientOptions { timeoutMs?: number; } diff --git a/tests/dispute-escrow.test.ts b/tests/dispute-escrow.test.ts new file mode 100644 index 0000000..485a399 --- /dev/null +++ b/tests/dispute-escrow.test.ts @@ -0,0 +1,37 @@ +import { disputeEscrow } from '../src/escrow/dispute'; +import { TrustFlowClient } from '../src/client'; +import { Keypair } from '@stellar/stellar-sdk'; + +const CALLER = Keypair.random().publicKey(); + +describe('disputeEscrow', () => { + const client = new TrustFlowClient({ contractId: 'CONTRACT123', network: 'TESTNET' }); + + it('raises a dispute for valid params', async () => { + const txHash = await disputeEscrow(client, { + escrowId: 'esc-1', + caller: CALLER, + reason: 'Goods not delivered as described.', + }); + + expect(txHash).toMatch(/^tx_dispute_esc-1_/); + }); + + it('throws when escrowId is missing', async () => { + await expect( + disputeEscrow(client, { escrowId: '', caller: CALLER, reason: 'reason' }), + ).rejects.toThrow(/escrowId/); + }); + + it('throws when caller is missing', async () => { + await expect( + disputeEscrow(client, { escrowId: 'esc-1', caller: '', reason: 'reason' }), + ).rejects.toThrow(/dispute/); + }); + + it('throws when reason is missing', async () => { + await expect( + disputeEscrow(client, { escrowId: 'esc-1', caller: CALLER, reason: ' ' }), + ).rejects.toThrow(/reason/); + }); +}); From ef2cc602f21acfd5112a6b68065b967487b72bb8 Mon Sep 17 00:00:00 2001 From: Coder Girl <316228223+CillaSam@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:56:22 +0100 Subject: [PATCH 4/5] docs(sdk): add Typedoc configuration for API reference generation (#7) Adds typedoc.json plus npm run docs / docs:watch to auto-generate an HTML API reference from JSDoc comments into docs/reference (generated on demand, gitignored). skipErrorChecking is enabled so doc generation isn't blocked by pre-existing unrelated compiler diagnostics elsewhere in the codebase (e.g. window usage without a DOM lib). --- .gitignore | 1 + package-lock.json | 220 ++++++++++++++++++++++++++++++++++++++++++++++ package.json | 5 +- typedoc.json | 15 ++++ 4 files changed, 240 insertions(+), 1 deletion(-) create mode 100644 typedoc.json diff --git a/.gitignore b/.gitignore index ad9a5bf..ca3497e 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist/ coverage/ *.js.map *.tsbuildinfo +docs/reference/ # IDE dist diff --git a/package-lock.json b/package-lock.json index c573b6e..2bb7ce8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,6 +30,7 @@ "react": "^18.2.0", "ts-jest": "^29.4.11", "tsup": "^8.5.1", + "typedoc": "^0.28.20", "typescript": "^6.0.3" }, "peerDependencies": { @@ -1148,6 +1149,20 @@ "node": "^20.19.0 || ^22.13.0 || >=24" } }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.23.0.tgz", + "integrity": "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.23.0", + "@shikijs/langs": "^3.23.0", + "@shikijs/themes": "^3.23.0", + "@shikijs/types": "^3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, "node_modules/@humanfs/core": { "version": "0.19.2", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", @@ -2126,6 +2141,55 @@ "win32" ] }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", + "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", + "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", + "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.23.0" + } + }, + "node_modules/@shikijs/types": { + "version": "3.23.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", + "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, "node_modules/@sinclair/typebox": { "version": "0.34.49", "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", @@ -2284,6 +2348,16 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/hast": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", @@ -2364,6 +2438,13 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.35", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", @@ -3896,6 +3977,19 @@ "dev": true, "license": "MIT" }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/error-ex": { "version": "1.3.4", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", @@ -5855,6 +5949,26 @@ "dev": true, "license": "MIT" }, + "node_modules/linkify-it": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.2.tgz", + "integrity": "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, "node_modules/load-tsconfig": { "version": "0.2.5", "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", @@ -5911,6 +6025,13 @@ "yallist": "^3.0.2" } }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, "node_modules/magic-string": { "version": "0.30.21", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", @@ -5954,6 +6075,41 @@ "tmpl": "1.0.5" } }, + "node_modules/markdown-it": { + "version": "14.3.1", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.1.tgz", + "integrity": "sha512-4Ej49aYTDFIQ+uBkfX8GBvJGccoARxxPep+7aWTs55ozbjQJpW9M26Fe53vnGgvLeVzva/amzjQQaQu9w0vMhA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/markdown-it" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5963,6 +6119,13 @@ "node": ">= 0.4" } }, + "node_modules/mdurl": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.1.0.tgz", + "integrity": "sha512-1+HBaOx0zi/dQWht8rNv9MYf9qqpqL/kxI0hXImU6Y547zM6Sni8BQibt7ifgMcYtQg41ao3Ivd6cnSM86inpg==", + "dev": true, + "license": "MIT" + }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -6571,6 +6734,16 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/pure-rand": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", @@ -7480,6 +7653,30 @@ "node": ">= 0.4" } }, + "node_modules/typedoc": { + "version": "0.28.20", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.20.tgz", + "integrity": "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.23.0", + "lunr": "^2.3.9", + "markdown-it": "^14.3.0", + "minimatch": "^10.2.5", + "yaml": "^2.9.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" + } + }, "node_modules/typescript": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-6.0.3.tgz", @@ -7494,6 +7691,13 @@ "node": ">=14.17" } }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, "node_modules/ufo": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", @@ -7819,6 +8023,22 @@ "dev": true, "license": "ISC" }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs": { "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", diff --git a/package.json b/package.json index e9b4ec7..f51e350 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "lint:fix": "eslint src --fix", "format": "prettier --write \"src/**/*.ts\"", "format:check": "prettier --check \"src/**/*.ts\"", + "docs": "typedoc", + "docs:watch": "typedoc --watch", "prepublishOnly": "npm run build" }, "license": "MIT", @@ -53,7 +55,6 @@ "@types/jest": "^30.0.0", "@types/node": "^25.9.3", "@types/react": "^18.2.0", - "react": "^18.2.0", "@typescript-eslint/eslint-plugin": "^8.61.1", "@typescript-eslint/parser": "^8.61.1", "eslint": "^10.5.0", @@ -62,8 +63,10 @@ "globals": "^17.6.0", "jest": "^30.4.2", "prettier": "^3.8.4", + "react": "^18.2.0", "ts-jest": "^29.4.11", "tsup": "^8.5.1", + "typedoc": "^0.28.20", "typescript": "^6.0.3" } } diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 0000000..a58fcc3 --- /dev/null +++ b/typedoc.json @@ -0,0 +1,15 @@ +{ + "$schema": "https://typedoc.org/schema.json", + "entryPoints": ["src/index.ts", "src/hooks/index.ts"], + "out": "docs/reference", + "tsconfig": "./tsconfig.json", + "name": "TrustFlow SDK", + "readme": "README.md", + "excludePrivate": true, + "excludeInternal": true, + "excludeExternals": true, + "exclude": ["**/*.test.ts", "examples/**", "tests/**"], + "sort": ["source-order"], + "cleanOutputDir": true, + "skipErrorChecking": true +} From d3f45b228faa0a6f19ee3aa183ee4ec2dc823b87 Mon Sep 17 00:00:00 2001 From: Coder Girl <316228223+CillaSam@users.noreply.github.com> Date: Fri, 28 Aug 2026 02:56:30 +0100 Subject: [PATCH 5/5] docs(sdk): document fund(), disputeEscrow(), ProfileClient, and typedoc Updates README, docs/API.md, and CHANGELOG to cover the new escrow.fund() wrapper, the on-chain disputeEscrow() helper, ProfileClient, and the generated API reference (#4, #5, #6, #7). --- CHANGELOG.md | 17 +++++++++++++++++ README.md | 35 ++++++++++++++++++++++++++++++++++- docs/API.md | 14 ++++++++++++++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a4f4d05..520e6cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,23 @@ # Changelog ## [Unreleased] +- Added `TrustFlowEscrowClient.fund()` (#4) — funds an existing escrow by encoding a token + transfer (e.g. the USDC Soroban token contract) into contract call arguments via the new + `buildFundArgs`; omit `tokenAddress` to use the escrow's native asset. +- Added `ProfileClient` (#5) — type-safe Axios methods (`getProfile`/`updateProfile`) for the + backend's `/profiles` endpoints, following the same retry-aware `SDKResult` pattern as + `DisputeClient`/`JurorClient`. Exported from the package root alongside `Profile` and + `UpdateProfileParams`. +- Added `disputeEscrow()` to `src/escrow/dispute.ts` (#6) — the on-chain counterpart to + `DisputeClient.raiseDispute` (which posts to the backend API); simplifies the XDR construction + for alerting the smart contract of a dispute via the existing `buildDisputeArgs`. This also + fixes `examples/dispute.ts`, which already imported `disputeEscrow` from this module even + though it was never implemented. +- Added a Typedoc configuration (#7) — `typedoc.json` plus `npm run docs` / `docs:watch` — + auto-generating API reference HTML from JSDoc comments into `docs/reference` (gitignored, + generated on demand). `skipErrorChecking` is enabled so doc generation isn't blocked by + pre-existing unrelated compiler diagnostics in legacy browser-wallet code (`window` usage + without a DOM lib, etc.). - Exported the Zod validation schemas from `src/schemas.ts` (`StellarAddressSchema`, `ContractIdSchema`, `StroopsSchema`, `NetworkSchema`, `CreateEscrowSchema`, `ReleaseEscrowSchema`, `DisputeEscrowSchema`, `ClientConfigSchema`, plus the `*Input` inferred diff --git a/README.md b/README.md index 829f5f7..312d62f 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ if (result.ok) { ### 3 — Fund & Release an Escrow ```typescript -import { TrustFlowClient } from '@trustflow/sdk'; +import { TrustFlowClient, TrustFlowEscrowClient } from '@trustflow/sdk'; import { createEscrow, releaseEscrow } from '@trustflow/sdk/escrow'; import { connectWallet } from '@trustflow/sdk/wallet'; import { xlmToStroops } from '@trustflow/sdk/utils'; @@ -84,6 +84,21 @@ const escrow = await createEscrow(client, { }); console.log('Escrow created:', escrow.id); +// Fund (e.g. lock USDC via its Soroban token contract instead of the native asset) +const escrowClient = new TrustFlowEscrowClient({ + contractId: process.env.TRUSTFLOW_CONTRACT_ID!, + network: 'TESTNET', + rpcUrl: 'https://soroban-testnet.stellar.org', + networkPassphrase: 'Test SDF Network ; September 2015', +}); +const funded = await escrowClient.fund( + escrow.id, + wallet.publicKey, + xlmToStroops('50'), + process.env.USDC_CONTRACT_ID, +); +if (funded.ok) console.log('Funded! tx:', funded.data.txHash); + // Release const txHash = await releaseEscrow(client, { escrowId: escrow.id, @@ -173,6 +188,22 @@ const result = await escrowClient.claim('esc-42', 'GBENEFICIARY...'); if (result.ok) console.log('Claimed! tx:', result.data.txHash); ``` +### User Profiles + +`ProfileClient` wraps the backend's `/profiles` endpoints with the same retry-aware transport +as `DisputeClient`/`JurorClient`: + +```typescript +import { ProfileClient } from '@trustflow/sdk'; + +const profiles = new ProfileClient(process.env.TRUSTFLOW_API_URL!, authToken); + +const result = await profiles.getProfile(wallet.publicKey); +if (result.ok) console.log(result.data.displayName); + +await profiles.updateProfile(wallet.publicKey, { bio: 'Building on Stellar' }); +``` + ### IPFS Storage Every `TrustFlowClient` exposes a built-in `storage.upload()` helper for pinning files to @@ -300,6 +331,8 @@ import { useWallet, useBalance, useTransaction } from '@trustflow/sdk/react'; - **[API Reference](./docs/API.md)** — Complete API documentation - **[Architecture](./docs/ARCHITECTURE.md)** — Design principles and module structure - **[Examples](./examples/)** — Working code examples for common use cases +- **API reference (generated)** — run `npm run docs` to build a browsable HTML API reference + from JSDoc comments into `docs/reference/` (not committed; regenerate locally or in CI) --- diff --git a/docs/API.md b/docs/API.md index 2ac965d..8f22296 100644 --- a/docs/API.md +++ b/docs/API.md @@ -2,11 +2,25 @@ ## TrustFlowEscrowClient - `createEscrow(params)` — create a new escrow; encodes contract call arguments via `buildCreateEscrowArgs` +- `fund(escrowId, funderAddress, amountStroops, tokenAddress?)` — transfer an asset (e.g. USDC via + its Soroban token contract) into an existing escrow to be locked until release; encodes contract + call arguments via `buildFundArgs`. Omit `tokenAddress` to use the escrow's native asset. - `releaseEscrow(id, signer)` — release funds to beneficiary - `claim(escrowId, claimantAddress)` — beneficiary-side shortcut to withdraw already-cleared escrow funds - `getEscrow(id)` — read escrow state from contract - `getGigs(params)` — fetch paginated gigs via backend API with automatic retries for transient failures (`429`, `5xx`, network) +## disputeEscrow (`src/escrow/dispute.ts`) +- `disputeEscrow(client, { escrowId, caller, reason })` — raises a dispute directly against the + TrustFlow contract; encodes contract call arguments via `buildDisputeArgs`. Distinct from + `DisputeClient.raiseDispute` below, which records the dispute with the backend API instead of + the on-chain contract. + +## ProfileClient +- `new ProfileClient(apiUrl, token, options?)` +- `.getProfile(address)` — fetch a user's profile (automatic retry on transient backend failures) +- `.updateProfile(address, params)` — update a user's profile (automatic retry on transient backend failures) + ## IPFSStorage - `new IPFSStorage(config?)` — `config.apiUrl` (default: web3.storage-compatible upload API), `config.apiKey`, `config.gatewayUrl` - `.upload(file, options?)` — uploads a `Buffer`/`Uint8Array`; returns `SDKResult<{ cid, url }>`