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
70 changes: 0 additions & 70 deletions packages/sdk/src/PaymentStreamClient.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
import { Client as ContractClient } from './generated/payment-stream/src/index';
import { AssembledTransaction, ClientOptions as ContractClientOptions } from '@stellar/stellar-sdk/contract';
import { rpc as SorobanRpc } from '@stellar/stellar-sdk';
import { Stream, StreamMetrics, ProtocolMetrics, StreamStatus } from './generated/payment-stream/src/index';
import { Client as ContractClient } from "./generated/payment-stream/src/index";
import {
AssembledTransaction,
Expand All @@ -12,7 +8,7 @@
Stream,
StreamMetrics,
ProtocolMetrics,
StreamStatus,

Check warning on line 11 in packages/sdk/src/PaymentStreamClient.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

'StreamStatus' is defined but never used

Check warning on line 11 in packages/sdk/src/PaymentStreamClient.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

'StreamStatus' is defined but never used
} from "./generated/payment-stream/src/index";
import { executeWithErrorHandling } from "./utils/errors";
import {
Expand All @@ -34,16 +30,6 @@
return typeof address === "string" ? address : address.toString();
}

export type StreamEventType = 'created' | 'deposit' | 'withdraw' | 'paused' | 'resumed' | 'canceled' | 'completed' | 'delegate_set' | 'delegate_revoked' | 'fee_collected';

export interface StreamHistoryEvent {
type: StreamEventType;
streamId: bigint;
ledger: number;
timestamp: number;
data: Record<string, unknown>;
}

/**
* High-level client for interacting with the Payment Stream contract.
* Provides a type-safe and DX-optimized interface for all contract methods.
Expand All @@ -52,15 +38,6 @@
* and transaction result XDR to provide human-readable error messages.
*/
export class PaymentStreamClient {
private client: ContractClient;
private rpcUrl: string;
private contractId: string;

constructor(options: ContractClientOptions) {
this.client = new ContractClient(options);
this.rpcUrl = options.rpcUrl;
this.contractId = options.contractId;
}
private client: ContractClient;
private rpcUrl?: string;
private contractId?: string;
Expand Down Expand Up @@ -266,7 +243,7 @@
): Promise<AssembledTransaction<string | undefined>> {
// Option<string> is usually returned as string | undefined or similar in the generated client
return executeWithErrorHandling(
() => this.client.get_delegate({ stream_id: streamId }) as any,

Check warning on line 246 in packages/sdk/src/PaymentStreamClient.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected any. Specify a different type

Check warning on line 246 in packages/sdk/src/PaymentStreamClient.ts

View workflow job for this annotation

GitHub Actions / lint-and-format

Unexpected any. Specify a different type
"Get stream delegate"
);
}
Expand Down Expand Up @@ -382,53 +359,6 @@
);
}

/**
* Fetch and parse history events for a specific stream from the ledger.
* @param streamId The ID of the stream.
* @param opts Optional pagination: startLedger and limit (default 100).
*/
public async getStreamHistory(
streamId: bigint,
opts: { startLedger?: number; limit?: number } = {}
): Promise<StreamHistoryEvent[]> {
const server = new SorobanRpc.Server(this.rpcUrl);
const { startLedger = 0, limit = 100 } = opts;

const response = await server.getEvents({
startLedger,
filters: [
{
type: 'contract',
contractIds: [this.contractId],
topics: [['*', `u64:${streamId}`]],
},
],
limit,
});

return response.events.map((event) => {
const topics = event.topic.map((t) => t.value());
const eventName = (topics[0] as string).toLowerCase().replace('_event', '') as StreamEventType;
const data: Record<string, unknown> = {};

try {
const val = event.value.value();
if (val && typeof val === 'object') {
Object.assign(data, val);
}
} catch {
// non-critical, leave data empty
}

return {
type: eventName,
streamId,
ledger: event.ledger,
timestamp: event.ledgerClosedAt ? new Date(event.ledgerClosedAt).getTime() / 1000 : 0,
data,
};
});
}
return getAllStreamHistory(
{
rpcUrl: this.rpcUrl,
Expand Down
61 changes: 0 additions & 61 deletions packages/sdk/src/__tests__/PaymentStreamClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,6 @@ const mockTx = (result: unknown = undefined) => ({
signAndSend: mockSignAndSend,
});

const mockGetEvents = vi.fn();

vi.mock('@stellar/stellar-sdk', () => ({
rpc: {
Server: vi.fn().mockImplementation(() => ({
getEvents: mockGetEvents,
})),
},
}));

const mockContractClient = {
create_stream: vi.fn(),
deposit: vi.fn(),
Expand Down Expand Up @@ -506,55 +496,4 @@ describe("PaymentStreamClient", () => {
expect(mockContractClient.initialize).toHaveBeenCalledWith(params);
});
});

// ── getStreamHistory ───────────────────────────────────────────────────────
describe('getStreamHistory', () => {
const makeEvent = (name: string, streamId: bigint) => ({
topic: [{ value: () => name }, { value: () => `u64:${streamId}` }],
value: { value: () => ({ amount: 500n }) },
ledger: 1000,
ledgerClosedAt: '2024-01-01T00:00:00Z',
});

it('queries getEvents with correct contract filter', async () => {
mockGetEvents.mockResolvedValue({ events: [] });
await client.getStreamHistory(STREAM_ID);
expect(mockGetEvents).toHaveBeenCalledWith(
expect.objectContaining({
filters: [expect.objectContaining({ contractIds: [VALID_OPTIONS.contractId] })],
})
);
});

it('returns parsed events for the stream', async () => {
mockGetEvents.mockResolvedValue({
events: [makeEvent('withdraw_event', STREAM_ID), makeEvent('paused_event', STREAM_ID)],
});
const history = await client.getStreamHistory(STREAM_ID);
expect(history).toHaveLength(2);
expect(history[0].type).toBe('withdraw');
expect(history[0].streamId).toBe(STREAM_ID);
expect(history[0].ledger).toBe(1000);
expect(history[1].type).toBe('paused');
});

it('returns empty array when no events exist', async () => {
mockGetEvents.mockResolvedValue({ events: [] });
const history = await client.getStreamHistory(STREAM_ID);
expect(history).toEqual([]);
});

it('respects startLedger and limit options', async () => {
mockGetEvents.mockResolvedValue({ events: [] });
await client.getStreamHistory(STREAM_ID, { startLedger: 500, limit: 50 });
expect(mockGetEvents).toHaveBeenCalledWith(
expect.objectContaining({ startLedger: 500, limit: 50 })
);
});

it('propagates RPC errors', async () => {
mockGetEvents.mockRejectedValue(new Error('RPC error'));
await expect(client.getStreamHistory(STREAM_ID)).rejects.toThrow('RPC error');
});
});
});
92 changes: 4 additions & 88 deletions packages/sdk/src/deployer/ContractDeployer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import {
Keypair,
Transaction,
TransactionBuilder,
Operation,
Networks,
Expand All @@ -11,7 +10,6 @@ import {
Transaction,
} from '@stellar/stellar-sdk';
import { Server, Api } from '@stellar/stellar-sdk/rpc';
import type { DeployerConfig, WasmUploadResult, ContractDeployResult, FeeEstimate, Signer } from './types';
import type {
DeployerConfig,
WasmUploadResult,
Expand Down Expand Up @@ -136,14 +134,6 @@ export class ContractDeployer {
* Simulate a WASM upload transaction and return the estimated fee and
* resource consumption without submitting anything to the network.
*
* @param wasm - Compiled contract WASM as a Buffer or Uint8Array.
* @param deployer - Keypair or public key string of the account that will pay.
*/
async estimateUploadFee(wasm: Buffer | Uint8Array, deployer: Keypair | string): Promise<FeeEstimate> {
this.assertValidWasm(wasm);
const address = typeof deployer === 'string' ? deployer : deployer.publicKey();
const account = await this.loadAccount(address);
const tx = this.buildUploadTx(wasm, account);
* @param wasm - Compiled contract WASM as a Buffer or Uint8Array.
* @param deployer - Keypair or account address that will pay for the upload.
*/
Expand All @@ -159,18 +149,6 @@ export class ContractDeployer {
* Simulate a contract instantiation transaction and return the estimated
* fee and resource consumption.
*
* @param wasmHash - Hex or base64 hash returned by `uploadWasm`.
* @param deployerOrAddr - Keypair or public key string of the paying account.
* @param salt - Optional 32-byte salt for deterministic contract IDs.
*/
async estimateDeployFee(
wasmHash: string,
deployerOrAddr: Keypair | string,
salt?: Buffer,
): Promise<FeeEstimate> {
const address = typeof deployerOrAddr === 'string' ? deployerOrAddr : deployerOrAddr.publicKey();
const account = await this.loadAccount(address);
const tx = this.buildDeployTx(wasmHash, address, account, salt);
* @param wasmHash - Hex or base64 hash returned by `uploadWasm`.
* @param deployer - Keypair or account address that will pay for the deploy.
* @param salt - Optional 32-byte salt for deterministic contract IDs.
Expand All @@ -194,15 +172,6 @@ export class ContractDeployer {
* contract address yet.
*
* @param wasm - Compiled contract WASM as a Buffer or Uint8Array.
* @param signer - Keypair, array of Keypairs (multi-sig), or signing callback.
* @returns `WasmUploadResult` containing the `wasmHash` needed for deployment.
*/
async uploadWasm(wasm: Buffer | Uint8Array, signer: Signer): Promise<WasmUploadResult> {
this.assertValidWasm(wasm);

const address = this.signerAddress(signer);
const account = await this.loadAccount(address);
const tx = this.buildUploadTx(wasm, account);
* @param deployer - Keypair or multi-sig config that signs and pays for the transaction.
* @returns `WasmUploadResult` containing the `wasmHash` needed for deployment.
*/
Expand All @@ -213,15 +182,14 @@ export class ContractDeployer {
const account = await this.loadAccount(deployerAddress);
const tx = await this.buildUploadTx(wasm, account);

// Simulate to get resource footprint, then rebuild with correct fee
const estimate = await this.simulate(tx);
const preparedTx = this.buildUploadTx(wasm, account, estimate.fee);
const signedXdr = await this.signTx(preparedTx, signer);
const preparedTx = await this.buildUploadTx(wasm, account, estimate.fee);

await this.signTransaction(preparedTx, deployer);

try {
const result = await this.submitAndWait(signedXdr);
const result = await this.submitAndWait(preparedTx.toEnvelope().toXDR('base64'));
return {
wasmHash: this.deriveWasmHash(wasm),
txHash: result.txHash,
Expand All @@ -244,25 +212,12 @@ export class ContractDeployer {
* Instantiate a previously uploaded WASM as a new contract.
*
* @param wasmHash - Hex hash returned by `uploadWasm`.
* @param signer - Keypair, array of Keypairs (multi-sig), or signing callback.
* @param deployer - Keypair or multi-sig config that signs and pays for the transaction.
* @param salt - Optional 32-byte salt for deterministic contract IDs.
* @returns `ContractDeployResult` containing the new `contractId`.
*/
async deployContract(
wasmHash: string,
signer: Signer,
salt?: Buffer,
): Promise<ContractDeployResult> {
const address = this.signerAddress(signer);
const account = await this.loadAccount(address);
const estimate = await this.estimateDeployFee(wasmHash, address, salt);
const tx = this.buildDeployTx(wasmHash, address, account, salt, estimate.fee);
const signedXdr = await this.signTx(tx, signer);

try {
const result = await this.submitAndWait(signedXdr);
const contractId = this.deriveContractId(address, salt ?? result.txHash);
deployer: Deployer,
salt?: Buffer,
): Promise<ContractDeployResult> {
Expand Down Expand Up @@ -302,13 +257,6 @@ export class ContractDeployer {
* Upload the WASM and immediately deploy the contract in two sequential
* transactions. Returns both results.
*
* @param wasm - Compiled contract WASM.
* @param signer - Keypair, array of Keypairs (multi-sig), or signing callback.
* @param salt - Optional salt for deterministic contract ID.
*/
async uploadAndDeploy(
wasm: Buffer | Uint8Array,
signer: Signer,
* @param wasm - Compiled contract WASM.
* @param deployer - Keypair or multi-sig config that signs both transactions.
* @param salt - Optional salt for deterministic contract ID.
Expand All @@ -318,45 +266,13 @@ export class ContractDeployer {
deployer: Deployer,
salt?: Buffer,
): Promise<{ upload: WasmUploadResult; deploy: ContractDeployResult }> {
const upload = await this.uploadWasm(wasm, signer);
const deploy = await this.deployContract(upload.wasmHash, signer, salt);
const upload = await this.uploadWasm(wasm, deployer);
const deploy = await this.deployContract(upload.wasmHash, deployer, salt);
return { upload, deploy };
}

// ─── Private: transaction builders ────────────────────────────────────────

/** Resolve the source account address from any supported signer type. */
private signerAddress(signer: Signer): string {
if (typeof signer === 'function') {
throw new DeployerError(
'A SigningCallback cannot provide a public key. Pass the deployer address separately or use a Keypair.',
'SIGNER_ADDRESS_UNKNOWN',
);
}
return Array.isArray(signer) ? signer[0].publicKey() : signer.publicKey();
}

/**
* Sign a built transaction with the provided signer and return the signed XDR.
* - Single Keypair: signs directly.
* - Keypair[]: each keypair signs in order (multi-sig).
* - SigningCallback: delegates to the callback (e.g. browser wallet).
*/
private async signTx(
tx: ReturnType<TransactionBuilder['build']>,
signer: Signer,
): Promise<string> {
if (typeof signer === 'function') {
return signer(tx.toEnvelope().toXDR('base64'));
}
const keypairs = Array.isArray(signer) ? signer : [signer];
for (const kp of keypairs) {
tx.sign(kp);
}
return tx.toEnvelope().toXDR('base64');
}

private buildUploadTx(
private async buildUploadTx(
wasm: Buffer | Uint8Array,
account: { id: string; sequenceNumber: () => string },
Expand Down
14 changes: 0 additions & 14 deletions packages/sdk/src/deployer/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,20 +66,6 @@ export type Deployer = Keypair | DeployerAccount;
*/
export type StellarNetwork = 'testnet' | 'mainnet' | 'custom';

/**
* A callback that receives a transaction XDR string, signs it externally
* (e.g. via a browser wallet), and returns the signed XDR string.
*/
export type SigningCallback = (txXdr: string) => Promise<string>;

/**
* Accepted signer types for upload/deploy operations:
* - A single `Keypair` (original behaviour)
* - An array of `Keypair`s for multi-sig (each one signs in order)
* - A `SigningCallback` for wallet-based or custom signing flows
*/
export type Signer = import('@stellar/stellar-sdk').Keypair | import('@stellar/stellar-sdk').Keypair[] | SigningCallback;

/**
* Configuration options for the ContractDeployer.
*
Expand Down
3 changes: 0 additions & 3 deletions packages/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@ export {
} from "./generated/distributor/src/index";

// Export high-level clients
export * from './PaymentStreamClient';
export type { StreamHistoryEvent, StreamEventType } from './PaymentStreamClient';
export * from './DistributorClient';
export * from "./PaymentStreamClient";
export * from "./DistributorClient";

Expand Down
Loading