-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathstellar.service.ts
More file actions
147 lines (128 loc) 路 4.33 KB
/
Copy pathstellar.service.ts
File metadata and controls
147 lines (128 loc) 路 4.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
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
import StellarSdk from "@stellar/stellar-sdk";
import BigNumber from "bignumber.js";
import { AssetConfig } from "../types/asset.types";
const server = new StellarSdk.Horizon.Server(
"https://horizon-testnet.stellar.org"
);
const { Keypair, TransactionBuilder, Networks, Operation, Asset } = StellarSdk;
function toStellarAsset(config: AssetConfig): InstanceType<typeof Asset> {
if (config.network !== "stellar") {
throw new Error("Only stellar network supported for now");
}
if (config.type === "native") {
return Asset.native();
}
if (config.type === "credit") {
return new Asset(config.code, config.issuer);
}
throw new Error("Unsupported stellar asset type");
}
function assetsMatch(
a: InstanceType<typeof Asset>,
b: InstanceType<typeof Asset>
): boolean {
if (a.isNative() && b.isNative()) return true;
if (a.isNative() || b.isNative()) return false;
return a.getCode() === b.getCode() && a.getIssuer() === b.getIssuer();
}
function settlementSourceAssetFromEnv(): InstanceType<typeof Asset> {
const raw = (process.env.PASSPAY_SETTLEMENT_SOURCE ?? process.env.MIGO_SETTLEMENT_SOURCE)?.trim();
if (!raw || raw.toLowerCase() === "native") {
return Asset.native();
}
const colon = raw.indexOf(":");
if (colon < 1 || colon === raw.length - 1) {
throw new Error(
"PASSPAY_SETTLEMENT_SOURCE must be \"native\" or ASSET_CODE:ISSUER (e.g. USDC:GBBD...)"
);
}
return new Asset(raw.slice(0, colon), raw.slice(colon + 1));
}
function horizonAssetFromPathStep(step: {
asset_type: string;
asset_code?: string;
asset_issuer?: string;
}): InstanceType<typeof Asset> {
if (step.asset_type === "native") {
return Asset.native();
}
if (!step.asset_code || !step.asset_issuer) {
throw new Error("Invalid path hop from Horizon");
}
return new Asset(step.asset_code, step.asset_issuer);
}
export async function sendSettlementPayment(
amount: string,
settlementAsset: AssetConfig
) {
const PASSPAY_SECRET = process.env.PASSPAY_SECRET ?? process.env.MIGO_SECRET;
const MERCHANT_PUBLIC = process.env.MERCHANT_PUBLIC!;
if (!PASSPAY_SECRET || !MERCHANT_PUBLIC) {
throw new Error("Stellar env vars not loaded");
}
const sourceKeypair = Keypair.fromSecret(PASSPAY_SECRET);
const sourcePublic = sourceKeypair.publicKey();
const account = await server.loadAccount(sourcePublic);
const destAsset = toStellarAsset(settlementAsset);
const sourceAsset = settlementSourceAssetFromEnv();
let operation: InstanceType<typeof Operation>;
if (assetsMatch(sourceAsset, destAsset)) {
operation = Operation.payment({
destination: MERCHANT_PUBLIC,
asset: destAsset,
amount,
});
} else {
const pathCall = server.strictReceivePaths(
[sourceAsset],
destAsset,
amount
);
const { records } = await pathCall.call();
const record = records[0];
if (!record) {
const src = sourceAsset.isNative()
? "XLM"
: `${sourceAsset.getCode()}:${sourceAsset.getIssuer()}`;
const dest = destAsset.isNative()
? "XLM"
: `${destAsset.getCode()}:${destAsset.getIssuer()}`;
throw new Error(
`No Stellar DEX path found to deliver ${amount} of destination asset to merchant (source=${src}, dest=${dest}). Check liquidity and trustlines.`
);
}
const sendMax = new BigNumber(record.source_amount)
.times(1.01)
.toFixed(7, BigNumber.ROUND_UP);
const path: InstanceType<typeof Asset>[] = (record.path || []).map(
(hop: { asset_type: string; asset_code?: string; asset_issuer?: string }) =>
horizonAssetFromPathStep(hop)
);
operation = Operation.pathPaymentStrictReceive({
destination: MERCHANT_PUBLIC,
sendAsset: sourceAsset,
sendMax,
destAsset,
destAmount: amount,
path,
});
}
const transaction = new TransactionBuilder(account, {
fee: StellarSdk.BASE_FEE,
networkPassphrase: Networks.TESTNET,
})
.addOperation(operation)
.setTimeout(0)
.build();
transaction.sign(sourceKeypair);
try {
const result = await server.submitTransaction(transaction);
return result.hash;
} catch (err: any) {
const resultCodes = err?.response?.data?.extras?.result_codes;
if (resultCodes) {
console.error("馃敟 Horizon rechaz贸 el settlement. Motivo:", JSON.stringify(resultCodes));
}
throw err;
}
}