Skip to content

Commit f1e0995

Browse files
committed
feat: add deterministic transaction testing fixtures and documentation
1 parent cf8e166 commit f1e0995

4 files changed

Lines changed: 298 additions & 0 deletions

File tree

docs/testing-fixtures.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Transaction Testing Fixtures
2+
3+
Testing transaction flows involving offline preparation, signing, and network submission can be complex and prone to flakiness if reliant on live network dependencies (like fetching sequences or fee statistics).
4+
5+
To ensure SDK stability and provide consumers with reliable tools for their own testing, the PocketPay SDK exports deterministic fixture generators.
6+
7+
## Why Use Deterministic Fixtures?
8+
- **No Network Calls**: Fixture generation is entirely offline. No calls are made to Horizon.
9+
- **Repeatable State**: Timebounds, sequence numbers, and base fees are hardcoded. Tests will not break due to the passage of time or sequence advancement.
10+
- **Type Safety**: The generators return the exact interfaces used by the SDK (`PreparedTransaction`, `SignedTransaction`, etc.) or construct real `StellarSDK.Transaction` objects so that internal assertions pass perfectly.
11+
12+
## Available Generators
13+
14+
All fixture generators are exported from `src/transactions/index.ts`.
15+
16+
### 1. `createPreparedTransactionFixture`
17+
Simulates a transaction that has completed the offline preparation phase (`fetchNetworkState`) and is ready to be built.
18+
19+
```typescript
20+
import { createPreparedTransactionFixture } from 'stellar-pocketpay-sdk';
21+
22+
const prepared = createPreparedTransactionFixture({
23+
baseFee: '500' // You can override any field
24+
});
25+
26+
console.log(prepared.readyToBuild); // true
27+
console.log(prepared.timebounds); // Deterministic timebounds
28+
```
29+
30+
### 2. `createUnsignedTransactionFixture`
31+
Simulates a transaction that has been built into a `StellarSDK.Transaction` but has not yet been signed.
32+
33+
```typescript
34+
import { createUnsignedTransactionFixture } from 'stellar-pocketpay-sdk';
35+
36+
const unsigned = createUnsignedTransactionFixture({
37+
memo: 'Testing offline'
38+
});
39+
40+
// Provides access to a real StellarSDK.Transaction instance
41+
console.log(unsigned.transaction.memo.value.toString()); // "Testing offline"
42+
```
43+
44+
### 3. `createSignedTransactionFixture`
45+
Simulates a transaction that has been signed with a keypair. The SDK uses a deterministic dummy key to produce a valid signature format over the deterministic payload.
46+
47+
```typescript
48+
import { createSignedTransactionFixture } from 'stellar-pocketpay-sdk';
49+
50+
// Pass a specific secret key if you want to test your own validation logic
51+
const signed = createSignedTransactionFixture('SBU24Y4P...');
52+
53+
console.log(signed.xdr); // Base64 encoded XDR envelope
54+
```
55+
56+
### 4. `createSubmissionResultFixture`
57+
Simulates the outcome of `submitSignedTransaction` without actually broadcasting to the network.
58+
59+
```typescript
60+
import { createSubmissionResultFixture } from 'stellar-pocketpay-sdk';
61+
62+
const success = createSubmissionResultFixture('success');
63+
const failure = createSubmissionResultFixture('failed');
64+
const unknown = createSubmissionResultFixture('unknown');
65+
66+
// Useful for mocking the submission layer in tests
67+
```
68+
69+
## Best Practices
70+
When writing tests for your PocketPay integration, use `vi.mock` or `jest.mock` on the network submission and preparation layers, and use these fixtures as the resolved return values. This effectively stubs out Horizon, ensuring your business logic operates deterministically against valid transaction payloads.

src/transactions/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -298,6 +298,7 @@ export async function safeGetPayments(
298298

299299
export * from './mapper';
300300
export * from './fixtures';
301+
export * from './test-fixtures';
301302

302303
// ─── Offline Transaction Preparation ───────────────────────────────────────────
303304
export {

src/transactions/test-fixtures.ts

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
import * as StellarSDK from '@stellar/stellar-sdk';
2+
import {
3+
PreparedTransaction,
4+
UnsignedTransaction,
5+
SignedTransaction,
6+
SubmissionResult
7+
} from './offline-preparation';
8+
9+
/** Deterministic mock source public key (G...) */
10+
export const FIXTURE_SOURCE_PK = 'GBEDH75BXETOM2UPESSCHDVGV7XRDKS74YUKXTC7LR627DQ3SUZIJG3Y';
11+
/** Deterministic mock destination public key (G...) */
12+
export const FIXTURE_DESTINATION_PK = 'GBAW5KWLZU5PEUMR4PWXJVGFXDT4FRYIOKKTU74O5CGB24RWJS36LICH';
13+
/** Deterministic mock secret key (S...) matching the source public key */
14+
export const FIXTURE_SOURCE_SK = 'SDUTB2DT5NIGRKSGUHEU3TSZ4UJ45CJ3KVDSAKOOMHZEI64F2M4EIJCD';
15+
/** We use Stellar Testnet as the deterministic network */
16+
export const FIXTURE_NETWORK = StellarSDK.Networks.TESTNET;
17+
18+
/**
19+
* Creates a deterministic PreparedTransaction fixture.
20+
*
21+
* @param overrides Optional overrides for the prepared transaction properties
22+
* @returns A deterministic PreparedTransaction
23+
*/
24+
export function createPreparedTransactionFixture(overrides?: Partial<PreparedTransaction>): PreparedTransaction {
25+
return {
26+
sourcePublicKey: FIXTURE_SOURCE_PK,
27+
networkPassphrase: FIXTURE_NETWORK,
28+
operations: [{
29+
destination: FIXTURE_DESTINATION_PK,
30+
amount: '10',
31+
asset: { code: 'XLM' }
32+
}],
33+
timebounds: { minTime: 1700000000, maxTime: 1700000300 },
34+
baseFee: '100',
35+
networkState: { sequence: '1234567890', fetchedAt: 1700000000 },
36+
readyToBuild: true,
37+
...overrides
38+
};
39+
}
40+
41+
/**
42+
* Creates a deterministic UnsignedTransaction fixture containing a real StellarSDK.Transaction.
43+
*
44+
* @param overrides Optional overrides for the PreparedTransaction used to build it
45+
* @returns A deterministic UnsignedTransaction
46+
*/
47+
export function createUnsignedTransactionFixture(overrides?: Partial<PreparedTransaction>): UnsignedTransaction {
48+
const prepared = createPreparedTransactionFixture(overrides);
49+
50+
const account = new StellarSDK.Account(
51+
prepared.sourcePublicKey,
52+
prepared.networkState.sequence
53+
);
54+
55+
const builder = new StellarSDK.TransactionBuilder(account, {
56+
fee: prepared.baseFee,
57+
networkPassphrase: prepared.networkPassphrase,
58+
timebounds: prepared.timebounds,
59+
});
60+
61+
for (const op of prepared.operations) {
62+
builder.addOperation(
63+
StellarSDK.Operation.payment({
64+
destination: op.destination,
65+
asset: StellarSDK.Asset.native(), // For simplicity in fixture, we assume native if XLM
66+
amount: op.amount,
67+
})
68+
);
69+
}
70+
71+
if (prepared.memo) {
72+
builder.addMemo(StellarSDK.Memo.text(prepared.memo));
73+
}
74+
75+
const transaction = builder.build();
76+
77+
return {
78+
transaction,
79+
networkPassphrase: prepared.networkPassphrase,
80+
sourcePublicKey: prepared.sourcePublicKey,
81+
hash: transaction.hash().toString('hex'),
82+
};
83+
}
84+
85+
/**
86+
* Creates a deterministic SignedTransaction fixture.
87+
* Note: Since we need a real signature, this uses a dummy keypair to sign the mock transaction.
88+
*
89+
* @param secretKey Optional valid secret key to sign with. If omitted, a deterministic dummy key is used.
90+
* @param overrides Optional overrides for the underlying transaction
91+
* @returns A deterministic SignedTransaction
92+
*/
93+
export function createSignedTransactionFixture(
94+
secretKey: string = FIXTURE_SOURCE_SK,
95+
overrides?: Partial<PreparedTransaction>
96+
): SignedTransaction {
97+
const unsigned = createUnsignedTransactionFixture(overrides);
98+
const keypair = StellarSDK.Keypair.fromSecret(secretKey);
99+
100+
unsigned.transaction.sign(keypair);
101+
102+
return {
103+
transaction: unsigned.transaction,
104+
networkPassphrase: unsigned.networkPassphrase,
105+
hash: unsigned.transaction.hash().toString('hex'),
106+
xdr: unsigned.transaction.toEnvelope().toXDR('base64'),
107+
};
108+
}
109+
110+
/**
111+
* Creates a deterministic SubmissionResult fixture.
112+
*
113+
* @param status The status of the submission ('success', 'failed', 'unknown')
114+
* @param overrides Optional overrides for the SubmissionResult properties
115+
* @returns A deterministic SubmissionResult
116+
*/
117+
export function createSubmissionResultFixture(
118+
status: 'success' | 'failed' | 'unknown',
119+
overrides?: Partial<SubmissionResult>
120+
): SubmissionResult {
121+
if (status === 'success') {
122+
return {
123+
success: true,
124+
hash: 'a1b2c3d4e5f678901234567890abcdef1234567890abcdef1234567890abcdef',
125+
ledger: 12345,
126+
fee: '100',
127+
...overrides
128+
};
129+
} else if (status === 'failed') {
130+
return {
131+
success: false,
132+
hash: 'f6e5d4c3b2a109876543210987fedcba0987654321fedcba0987654321fedcba',
133+
error: 'Transaction failed on network',
134+
errorCode: 'TX_FAILED',
135+
...overrides
136+
};
137+
} else {
138+
// unknown
139+
return {
140+
success: false,
141+
hash: '0000000000000000000000000000000000000000000000000000000000000000',
142+
error: 'Transaction status unknown due to timeout',
143+
errorCode: 'TX_STATUS_UNKNOWN',
144+
...overrides
145+
};
146+
}
147+
}
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { describe, it, expect } from 'vitest';
2+
import {
3+
createPreparedTransactionFixture,
4+
createUnsignedTransactionFixture,
5+
createSignedTransactionFixture,
6+
createSubmissionResultFixture,
7+
FIXTURE_SOURCE_PK,
8+
FIXTURE_DESTINATION_PK
9+
} from '../src/transactions/test-fixtures';
10+
import * as StellarSDK from '@stellar/stellar-sdk';
11+
12+
describe('Transaction Fixture Generators', () => {
13+
describe('createPreparedTransactionFixture', () => {
14+
it('should generate a deterministic prepared transaction', () => {
15+
const fixture = createPreparedTransactionFixture();
16+
17+
expect(fixture.sourcePublicKey).toBe(FIXTURE_SOURCE_PK);
18+
expect(fixture.readyToBuild).toBe(true);
19+
expect(fixture.baseFee).toBe('100');
20+
expect(fixture.operations[0].destination).toBe(FIXTURE_DESTINATION_PK);
21+
});
22+
23+
it('should allow overrides', () => {
24+
const fixture = createPreparedTransactionFixture({ baseFee: '500' });
25+
expect(fixture.baseFee).toBe('500');
26+
expect(fixture.sourcePublicKey).toBe(FIXTURE_SOURCE_PK);
27+
});
28+
});
29+
30+
describe('createUnsignedTransactionFixture', () => {
31+
it('should generate a deterministic unsigned transaction with a real Stellar transaction object', () => {
32+
const fixture = createUnsignedTransactionFixture();
33+
34+
expect(fixture.transaction).toBeInstanceOf(StellarSDK.Transaction);
35+
expect(fixture.sourcePublicKey).toBe(FIXTURE_SOURCE_PK);
36+
expect(fixture.hash).toBeDefined();
37+
});
38+
39+
it('should apply overrides to the transaction builder inputs', () => {
40+
const fixture = createUnsignedTransactionFixture({ memo: 'Test Memo' });
41+
42+
expect(fixture.transaction.memo.value?.toString()).toBe('Test Memo');
43+
});
44+
});
45+
46+
describe('createSignedTransactionFixture', () => {
47+
it('should generate a deterministic signed transaction with signatures', () => {
48+
const fixture = createSignedTransactionFixture();
49+
50+
expect(fixture.transaction.signatures.length).toBe(1);
51+
expect(fixture.xdr).toBeDefined();
52+
});
53+
});
54+
55+
describe('createSubmissionResultFixture', () => {
56+
it('should generate a success result', () => {
57+
const fixture = createSubmissionResultFixture('success');
58+
expect(fixture.success).toBe(true);
59+
if (fixture.success) {
60+
expect(fixture.ledger).toBeDefined();
61+
}
62+
});
63+
64+
it('should generate a failure result', () => {
65+
const fixture = createSubmissionResultFixture('failed');
66+
expect(fixture.success).toBe(false);
67+
if (!fixture.success) {
68+
expect(fixture.errorCode).toBe('TX_FAILED');
69+
}
70+
});
71+
72+
it('should generate an unknown result', () => {
73+
const fixture = createSubmissionResultFixture('unknown');
74+
expect(fixture.success).toBe(false);
75+
if (!fixture.success) {
76+
expect(fixture.errorCode).toBe('TX_STATUS_UNKNOWN');
77+
}
78+
});
79+
});
80+
});

0 commit comments

Comments
 (0)