Skip to content
Open
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
27 changes: 27 additions & 0 deletions packages/sdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -71,11 +71,37 @@ try {
The lower-level primitives (`backoffWithJitter`, `CircuitBreaker`,
`parseRetryAfter`, `withRetry`) are exported directly for reuse.

### Dry-run status events

`TransactionQueue` emits status events via `queue.on("status", ...)`. When run
in **dry-run** mode (`run({ dryRun: true })` or a queue-level `dryRun: true`),
each step is simulated but never submitted. The queue still emits a `confirmed`
event at the end of each step to mark it complete, but that event:

- carries `dryRun: true`, and
- has **no** `hash` (nothing was broadcast).

Consumers listening for on-chain confirmations should check the `dryRun` flag to
avoid mistaking a simulated flow for a real submission:

```ts
queue.on("status", (e) => {
if (e.status === "confirmed") {
if (e.dryRun) {
// Simulated — do not show as "submitted to network".
} else {
// Real confirmation — e.hash is present.
}
}
});
```

## API Semantics

The SDK exposes two distinct paths for mutative (write) operations:

### 1. `prepare*Tx` (Submittable)

Methods like `prepareCreatePostTx`, `prepareFollowTx`, and `prepareDmKeyTx` are the **intended path for client-side applications**.
They fetch the actual account sequence from Horizon, simulate the transaction to discover footprint/fees, and return a base64-encoded `TransactionEnvelope` XDR that is fully ready to be signed (e.g. by Freighter) and submitted to the network.

Expand All @@ -85,6 +111,7 @@ const txXdr = await client.prepareCreatePostTx("GBFOY...", "Hello!");
```

### 2. Base Write Methods (Throwaway XDR)

Methods like `createPost`, `follow`, and `tip` **do not fetch sequence numbers** and return XDR built using a throwaway `Keypair`.
**These are not directly submittable.** They exist primarily to easily extract the Soroban `Operation` for batching (e.g., passing to `buildMultiOpTx`) or for server-side queueing where sequence management is handled by a background worker (like `TransactionQueue`).

Expand Down
21 changes: 20 additions & 1 deletion packages/sdk/src/__tests__/queue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,26 @@ describe("TransactionQueue", () => {

const statuses = events.map((e) => e.status);
expect(statuses).toEqual(["pending", "simulated", "confirmed"]);
expect(events.find((e) => e.status === "confirmed")?.resourceFee).toBe("2000");

const confirmedEvent = events.find((e) => e.status === "confirmed");
expect(confirmedEvent?.resourceFee).toBe("2000");
// Dry-run "confirmed" must be distinguishable from a real confirmation:
expect(confirmedEvent?.dryRun).toBe(true);
expect(confirmedEvent?.hash).toBeUndefined();
});

it("marks real confirmations with dryRun:false (or undefined) and a hash", async () => {
const rpc = makeRpc();
const queue = new TransactionQueue({ signer: makeSigner(), rpc, pollIntervalMs: 0 });
const events: TxStatusEvent[] = [];
queue.on("status", (e) => events.push(e));
queue.enqueue("XDR_LIVE");

await queue.run({ dryRun: false });

const confirmedEvent = events.find((e) => e.status === "confirmed");
expect(confirmedEvent?.dryRun).not.toBe(true);
expect(confirmedEvent?.hash).toBeDefined();
});

it("dryRun queue-level default is honoured", async () => {
Expand Down
21 changes: 18 additions & 3 deletions packages/sdk/src/queue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@
* every step is simulated but never submitted. This is useful for preflight
* checks and fee estimation without consuming sequence numbers or fees.
*
* In dry-run mode the queue still emits a `confirmed` event at the end of each
* step (as a completion marker), but the event carries `dryRun: true` and no
* `hash`. Consumers can therefore distinguish a simulated `confirmed` from a
* real on-chain confirmation by checking the `dryRun` flag.
*
* ### Per-step timeout
* `stepTimeoutMs` (config or per-`run()` override) caps the total wall-clock
* time spent on a single step (signing + submission + confirmation). When the
Expand All @@ -32,6 +37,13 @@ export interface TxStatusEvent {
error?: string;
/** Resource fee returned by simulation (present when status is "simulated" or later). */
resourceFee?: string;
/**
* When `true`, the event reflects a dry-run (simulate-only) execution rather
* than a real on-chain submission. A dry-run `confirmed` event carries no
* `hash`; consumers can use this flag to distinguish simulated success from an
* actual broadcast.
*/
dryRun?: boolean;
}

export type TxStatusListener = (event: TxStatusEvent) => void;
Expand Down Expand Up @@ -252,7 +264,8 @@ export class TransactionQueue {
* 2. Signs the XDR via the configured signer.
* 3. Simulates the signed transaction via `rpc.simulateTransaction` (unless
* `skipSimulation` is true). Emits `simulated` on success.
* 4. In `dryRun` mode, stops here and does not submit.
* 4. In `dryRun` mode, stops here and does not submit. Emits a `confirmed`
* event with `dryRun: true` and no hash to mark the step complete.
* 5. Submits via `rpc.sendTransaction`. Emits `submitted` with the hash.
* 6. Polls `rpc.getTransaction` until `SUCCESS` or failure. Emits `confirmed`.
*
Expand Down Expand Up @@ -365,8 +378,10 @@ export class TransactionQueue {

// ── 3. Dry-run exit ──────────────────────────────────────────────────────
if (isDryRun) {
// Simulation succeeded; mark as confirmed for tracking purposes (no hash).
this.emit({ index: i, xdr: step.xdr, status: "confirmed", resourceFee });
// Simulation succeeded; report a dry-run "confirmed" (no hash was
// produced) so callers can track the step without mistaking it for a
// real on-chain confirmation.
this.emit({ index: i, xdr: step.xdr, status: "confirmed", resourceFee, dryRun: true });
completed.push(i);
return;
}
Expand Down