Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/client-security.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function createSafeToJSON(obj: any) {
* console.log(client); // Safe: keypair is redacted in output
*/
export function createSafeInspect() {
return function inspect(this: any, depth: number, opts: any) {
return function inspect(this: any, _depth?: number, _opts?: any) {
return 'VeriTixClient { ...config, keypair: [REDACTED], server: [REDACTED] }';
};
}
15 changes: 9 additions & 6 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,7 @@
* ```
*/

import { SorobanRpc, Keypair, Contract, xdr } from '@stellar/stellar-sdk';
import { SorobanRpc, Keypair, StrKey, xdr } from '@stellar/stellar-sdk';
import { SorobanRpc, Keypair, Contract, StrKey, xdr } from '@stellar/stellar-sdk';

import type {
NetworkConfig,
Expand Down Expand Up @@ -140,8 +139,8 @@ export class VeriTixClient extends EventEmitter {
}

/** Redacts the keypair when the client is logged via console/util.inspect. */
[Symbol.for('nodejs.util.inspect.custom')](): string {
return createSafeInspect()();
[Symbol.for('nodejs.util.inspect.custom')](): (depth: number, opts: object) => string {
return createSafeInspect();
}

// -------------------------------------------------------------------------
Expand Down Expand Up @@ -180,7 +179,10 @@ export class VeriTixClient extends EventEmitter {
static fromEnvironment(env: NodeJS.ProcessEnv = process.env): VeriTixClient {
// Guard against browser bundles: a statically-inlined secret key would
// end up shipped to every client. Require an explicit client in browsers.
if (typeof window !== 'undefined' || typeof document !== 'undefined') {
if (
typeof (globalThis as { window?: unknown }).window !== 'undefined' ||
typeof (globalThis as { document?: unknown }).document !== 'undefined'
) {
throw new VeriTixError(
VeriTixErrorCode.ReadOnlyClient,
'VeriTixClient.fromEnvironment is not available in browser contexts; construct a VeriTixClient explicitly and never inline a secret key',
Expand Down Expand Up @@ -340,7 +342,8 @@ export class VeriTixClient extends EventEmitter {
if (rpcReachable) {
try {
await this.server.getContractData(
new Contract(this.config.contractId).getAddress().toScVal(),
this.config.contractId,
new Contract(this.config.contractId).address().toScVal(),
);
contractFound = true;
} catch {
Expand Down
37 changes: 37 additions & 0 deletions src/modules/admin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -333,6 +333,43 @@ export class AdminModule {
return this.writeCall('unpause', []);
}

// -------------------------------------------------------------------------
// Whitelist management
// -------------------------------------------------------------------------

/**
* Enables the whitelist feature on the contract. Must be called by admin.
* When enabled, only whitelisted addresses can interact with the contract.
*
* @returns A {@link TransactionResult} on success.
* @throws {VeriTixError} With code `ADMIN_UNAUTHORIZED` if caller is not admin.
*
* @example
* ```ts
* await client.admin.enableWhitelist();
* ```
*/
async enableWhitelist(): Promise<TransactionResult> {
return this.writeCall('enable_whitelist', []);
}

/**
* Adds an address to the whitelist. Must be called by admin.
* The contract must have whitelist enabled for this to succeed.
*
* @param address - Stellar account address to whitelist.
* @returns A {@link TransactionResult} on success.
* @throws {VeriTixError} With code `ADMIN_UNAUTHORIZED` if caller is not admin.
*
* @example
* ```ts
* await client.admin.whitelistAddress('GABC…');
* ```
*/
async whitelistAddress(address: string): Promise<TransactionResult> {
return this.writeCall('whitelist_address', [addressToScVal(address)]);
}

// -------------------------------------------------------------------------
// Fee management
// -------------------------------------------------------------------------
Expand Down
87 changes: 59 additions & 28 deletions src/modules/dispute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -586,57 +586,89 @@ export class DisputeModule {
async expireDispute(disputeId: bigint): Promise<TransactionResult> {
if (!this.keypair) {
throw new Error('DisputeModule.expireDispute: signing keypair required');
}

const dispute = await this.getDispute(disputeId);
if (!dispute) {
throw new VeriTixError(VeriTixErrorCode.DisputeNotFound, 'Dispute not found');
}

if (dispute.status !== DisputeStatus.Open) {
throw new VeriTixError(
VeriTixErrorCode.DisputeAlreadyResolved,
'Dispute already resolved',
);
}

const admin = this.keypair.publicKey();

const tx = await buildContractCall(
this.server,
new Account(admin, '0'),
this.config.contractId,
'expire_dispute',
[addressToScVal(admin), bigintToScVal(disputeId, 'u64')],
this.config.networkPassphrase,
);

const raw = await this.server.simulateTransaction(tx);
if (SorobanRpc.Api.isSimulationError(raw)) {
throw parseSorobanError(raw.error);
}

const returnValue =
SorobanRpc.Api.isSimulationSuccess(raw) && raw.result ? raw.result.retval : undefined;

const assembled = SorobanRpc.assembleTransaction(tx, raw).build();
const result = await submitTransaction(this.server, assembled, this.keypair);

return {
...result,
returnValue,
};
}

/**
* Appeals a resolved dispute. Must be called by the original claimant.
*
* @param disputeId - The dispute ID to appeal.
* @param disputeId - The dispute ID to appeal.
* @param appealResolver - Stellar account address of the new resolver for the appeal.
* @returns A {@link TransactionResult} on success.
* @throws {Error} If no signing keypair is available.
* @throws {VeriTixError} With code `DISPUTE_NOT_FOUND` if dispute does not exist.
* @throws {VeriTixError} With code `DISPUTE_INVALID_STATE` if dispute is still open.
* @throws {VeriTixError} With code `DISPUTE_INVALID_STATE` if appealResolver equals the caller.
*
* @example
* ```ts
* await client.dispute.appealDispute(3n);
* await client.dispute.appealDispute(3n, 'GARB…');
* ```
*/
async appealDispute(disputeId: bigint): Promise<TransactionResult> {
async appealDispute(disputeId: bigint, appealResolver: string): Promise<TransactionResult> {
if (!this.keypair) {
throw new Error('DisputeModule.appealDispute: signing keypair required');
}

const dispute = await this.getDispute(disputeId);
if (!dispute) {
const claimant = this.keypair.publicKey();
if (appealResolver === claimant) {
throw new VeriTixError(
VeriTixErrorCode.DisputeNotFound,
'Dispute not found',
VeriTixErrorCode.DisputeInvalidState,
'DisputeModule.appealDispute: appeal resolver cannot be the claimant',
);
}

const dispute = await this.getDispute(disputeId);
if (!dispute) {
throw new VeriTixError(VeriTixErrorCode.DisputeNotFound, 'Dispute not found');
}

if (dispute.status !== DisputeStatus.Open) {
throw new VeriTixError(
VeriTixErrorCode.DisputeAlreadyResolved,
'Dispute already resolved',
);
}

const admin = this.keypair.publicKey();

const tx = await buildContractCall(
this.server,
new Account(admin, '0'),
this.config.contractId,
'expire_dispute',
[
addressToScVal(admin),
if (dispute.status === DisputeStatus.Open) {
throw new VeriTixError(
VeriTixErrorCode.InvalidAmount,
'DisputeModule.appealDispute: dispute is still open, cannot appeal',
);
}

const claimant = this.keypair.publicKey();

const tx = await buildContractCall(
this.server,
new Account(claimant, '0'),
Expand All @@ -645,6 +677,7 @@ export class DisputeModule {
[
addressToScVal(claimant),
bigintToScVal(disputeId, 'u64'),
addressToScVal(appealResolver),
],
this.config.networkPassphrase,
);
Expand All @@ -655,9 +688,7 @@ export class DisputeModule {
}

const returnValue =
SorobanRpc.Api.isSimulationSuccess(raw) && raw.result
? raw.result.retval
: undefined;
SorobanRpc.Api.isSimulationSuccess(raw) && raw.result ? raw.result.retval : undefined;

const assembled = SorobanRpc.assembleTransaction(tx, raw).build();
const result = await submitTransaction(this.server, assembled, this.keypair);
Expand Down
7 changes: 7 additions & 0 deletions src/modules/escrow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -673,6 +673,13 @@ export class EscrowModule {
);
}

if (escrow.released || escrow.refunded) {
throw new VeriTixError(
VeriTixErrorCode.EscrowAlreadySettled,
'Escrow has already been released or refunded',
);
}

const caller = this.keypair.publicKey();
if (caller !== escrow.depositor) {
throw new VeriTixError(
Expand Down
2 changes: 1 addition & 1 deletion src/modules/recurring.ts
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ export class RecurringModule {
throw new Error('RecurringModule.amendRecurring: at least one of amount or interval must be provided');
}
if (!this.keypair) {
throw new Error('RecurringModule.amendRecurring: signing keypair required');
throw new VeriTixError(VeriTixErrorCode.ReadOnlyClient, 'RecurringModule.amendRecurring: signing keypair required');
}

const args = [bigintToScVal(id, 'u64')];
Expand Down
2 changes: 2 additions & 0 deletions src/modules/splitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
* and the platform, with support for custom BPS configurations.
*/
import { SorobanRpc, Keypair, Account, xdr } from '@stellar/stellar-sdk';
import { addressToScVal, bigintToScVal, scValToBigint, scValToNumber, stringToScVal } from '../utils/scval';
import { buildContractCall, simulateTransaction, submitTransaction } from '../utils/transaction';
import { addressToScVal, scValToBigint, scValToNumber } from '../utils/scval';
import { buildContractCall } from '../utils/transaction';
import { parseSorobanError, VeriTixError, VeriTixErrorCode } from '../utils/errors';
Expand Down
2 changes: 2 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -227,6 +227,8 @@ export interface RecurringRecord {
interval: number;
/** Whether this recurring payment is still active */
active: boolean;
/** Whether this recurring payment is currently paused */
paused: boolean;
/** Ledger sequence number when the most recent charge was executed */
lastChargedLedger: number;
}
Expand Down
3 changes: 1 addition & 2 deletions src/utils/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -292,8 +292,7 @@ function buildMessage(code: VeriTixErrorCode, rawStr: string): string {
[VeriTixErrorCode.ConnectionFailed]: 'Failed to connect to the Soroban RPC endpoint.',
[VeriTixErrorCode.BatchTooLarge]: 'Batch request exceeded maximum allowed size.',
[VeriTixErrorCode.ReadOnlyClient]: 'This client is read-only. Provide a Keypair to enable write operations.',
[VeriTixErrorCode.WatchTimeout]: 'watchEscrow or watchTransaction timed out before the operation was confirmed.',
[VeriTixErrorCode.WatchTimeout]: 'Watch timed out before the escrow settled.',
[VeriTixErrorCode.WatchTimeout]: 'Watch timed out before the operation was confirmed.',
[VeriTixErrorCode.TransactionFailed]: 'Transaction was rejected by the Stellar network.',
};
return messages[code];
Expand Down
1 change: 1 addition & 0 deletions src/utils/parsers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ export function parseRecurringRecord(val: xdr.ScVal): RecurringRecord {
amount: scValToBigint(getField(map, 'amount')),
interval: scValToNumber(getField(map, 'interval')),
active: scValToBoolean(getField(map, 'active')),
paused: scValToBoolean(getField(map, 'paused')),
lastChargedLedger: scValToNumber(getField(map, 'last_charged_ledger')),
};
}
Expand Down
5 changes: 0 additions & 5 deletions src/utils/transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,11 +160,6 @@ export async function estimateFee(
args: xdr.ScVal[],
): Promise<FeeEstimate> {
// Use a throwaway source account — simulation does not require a funded account
const sourceAccount = new Account(
'GAAZI4TCR3TY5OJHCTJC2A4QSY6CJWJH5IAJTGKIN2ER7LBNVKOCCWN',
'0',
);
// Use a throwaway keypair — simulation does not require a funded account
const sourceAccount = new Account(DUMMY_PUBLIC_KEY, '0');

const tx = await buildContractCall(
Expand Down
Loading