forked from veridatum-labs/earnproof-backend
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathstellar.service.ts
More file actions
102 lines (93 loc) · 3.45 KB
/
Copy pathstellar.service.ts
File metadata and controls
102 lines (93 loc) · 3.45 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import {
HttpStatus,
Injectable,
Optional,
ServiceUnavailableException,
} from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
HorizonClient,
HorizonCancelledError,
HorizonReadOptions,
HorizonReadResult,
} from "./horizon-client";
import { HorizonTransactionRecord, NormalizedPayment } from "./stellar.types";
import { HorizonException } from "../common/exceptions/domain.exceptions";
@Injectable()
export class StellarService {
private readonly horizonUrl: string;
private readonly horizon: HorizonClient;
constructor(
configService: ConfigService,
/**
* Injected only by tests, which supply a scripted transport and a no-op
* sleep. Production builds its own client from configuration.
*/
@Optional() horizonClient?: HorizonClient,
) {
this.horizonUrl = configService
.getOrThrow<string>("stellar.horizonUrl")
.replace(/\/$/, "");
this.horizon = horizonClient ?? new HorizonClient({ horizonUrl: this.horizonUrl });
}
/**
* Incoming payments for a wallet, newest first.
*
* Delegates to {@link HorizonClient}, which walks Horizon's cursors, retries
* only transient faults, and stops at an explicit page, record, or time
* bound. Every Horizon failure is collapsed to one dependency error here: the
* caller is an HTTP handler that can do nothing differently for a 429 than
* for a 503, and the fault taxonomy that distinguishes them has already done
* its work inside the retry loop.
*/
async fetchIncomingPayments(
walletAddress: string,
options: HorizonReadOptions = {},
): Promise<NormalizedPayment[]> {
const result = await this.readIncomingPayments(walletAddress, options);
return result.payments;
}
/**
* The full read result, including the resume cursor and what stopped the walk.
*
* Separate from {@link fetchIncomingPayments} so the common caller keeps its
* simple array contract while a caller that wants to resume, or to alert on
* malformed records, can reach the detail.
*/
async readIncomingPayments(
walletAddress: string,
options: HorizonReadOptions = {},
): Promise<HorizonReadResult> {
try {
return await this.horizon.listIncomingPayments(walletAddress, options);
} catch (error) {
// Cancellation is the caller's own decision, not a dependency failure;
// reporting it as one would make a client disconnect look like a Horizon
// outage on the dashboards.
if (error instanceof HorizonCancelledError) throw error;
throw new ServiceUnavailableException(
"Stellar Horizon is temporarily unavailable",
);
}
}
async fetchTransaction(
transactionHash: string,
): Promise<HorizonTransactionRecord | null> {
const response = await fetch(
`${this.horizonUrl}/transactions/${encodeURIComponent(transactionHash)}`,
);
if (!response.ok) {
// Never echo the response body — Horizon errors can include the raw
// request path/params, which for this endpoint includes the
// transaction hash but could carry more in other Horizon error shapes.
throw new HorizonException(
"Stellar Horizon is temporarily unavailable",
response.status === 404
? HttpStatus.NOT_FOUND
: HttpStatus.SERVICE_UNAVAILABLE,
);
}
const transaction = (await response.json()) as HorizonTransactionRecord;
return transaction && typeof transaction === "object" ? transaction : null;
}
}