forked from ZyntariHQ/Invoisio
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsoroban.service.ts
More file actions
100 lines (87 loc) · 3.33 KB
/
Copy pathsoroban.service.ts
File metadata and controls
100 lines (87 loc) · 3.33 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
import { Injectable, Logger, OnModuleInit } from "@nestjs/common";
import { ConfigService } from "@nestjs/config";
import {
PaymentRecord,
SorobanContractError,
SorobanInvoiceClient,
} from "@invoisio/soroban-client";
import { RecordPaymentDto } from "./dto/soroban-payment.dto";
/**
* NestJS service wrapping the `@invoisio/soroban-client` library.
*
* A single `SorobanInvoiceClient` instance is created in `onModuleInit()` and
* reused for the lifetime of the process — the underlying RPC server connection
* and admin keypair are both initialised once rather than per-call.
*
* All Soroban logic (XDR codec, polling, error parsing) lives in the client
* library. This service is a thin adapter that maps NestJS config and DTOs
* to the library's typed API.
*/
@Injectable()
export class SorobanService implements OnModuleInit {
private readonly logger = new Logger(SorobanService.name);
private client!: SorobanInvoiceClient;
constructor(private readonly configService: ConfigService) {}
onModuleInit(): void {
const cfg = this.configService.get("stellar") as {
sorobanRpcUrl: string;
networkPassphrase: string;
contractId: string;
adminSecretKey: string;
merchantPublicKey: string;
};
this.client = new SorobanInvoiceClient({
rpcUrl: cfg.sorobanRpcUrl,
networkPassphrase: cfg.networkPassphrase,
contractId: cfg.contractId,
// signerSecretKey enables write operations; undefined when not configured.
signerSecretKey: cfg.adminSecretKey || undefined,
// merchantPublicKey serves as the source account for read-only simulation.
sourcePublicKey: cfg.merchantPublicKey || undefined,
});
this.logger.log(
`SorobanService ready — contract: ${cfg.contractId || "(not configured)"}`,
);
}
/**
* Record a verified invoice payment on-chain.
*
* Returns the confirmed transaction hash and ledger number.
* @throws {SorobanContractError} if the contract rejects the call
*/
async recordInvoicePayment(
dto: RecordPaymentDto,
): Promise<{ hash: string; ledger: number }> {
this.logger.log(`Recording on-chain payment for invoice: ${dto.invoiceId}`);
const result = await this.client.recordPayment({
invoiceId: dto.invoiceId,
payer: dto.payer,
assetCode: dto.assetCode,
assetIssuer: dto.assetIssuer,
amount: BigInt(dto.amount),
settlementRef: dto.settlementRef,
});
this.logger.log(
`Payment recorded — invoice: ${dto.invoiceId}, hash: ${result.hash}, ledger: ${result.ledger}`,
);
return result;
}
/**
* Fetch the full on-chain payment record for an invoice.
* @throws {SorobanContractError} with code `PaymentNotFound` if not recorded
*/
async getInvoicePayment(invoiceId: string): Promise<PaymentRecord> {
return this.client.getPayment(invoiceId);
}
/**
* Return `true` if a payment has been recorded on-chain for the invoice.
*
* Use this as an idempotency check before calling `recordInvoicePayment`
* to make reconciliation safe to retry after partial failures.
*/
async hasInvoicePayment(invoiceId: string): Promise<boolean> {
return this.client.hasPayment(invoiceId);
}
/** Re-export the typed error class so callers can `catch (e instanceof SorobanContractError)`. */
static readonly ContractError = SorobanContractError;
}