forked from Movalabs-crew/mova-store
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheckout.ts
More file actions
177 lines (156 loc) · 5.22 KB
/
Copy pathcheckout.ts
File metadata and controls
177 lines (156 loc) · 5.22 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
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { rpc, TransactionBuilder } from "@stellar/stellar-sdk";
import {
CHECKOUT_CONTRACT_ID,
NETWORK_PASSPHRASE,
RPC_URL,
TokenConfig,
defaultToken,
USDC_DECIMALS,
} from "./config";
import { ensureNetwork, signWithFreighter, WalletError } from "./freighter";
import { decodePaymentEvent, PaymentReceipt, waitForTransaction } from "./events";
import {
addressToScVal,
bytes32ToScVal,
hashOrderId,
i128ToScVal,
} from "./scval";
import { assertPaymentReady } from "./account";
import { buildInvocationTransaction, budgetFee, prepareAndReport } from "./simulate";
export { fundTestnetAccount } from "./account";
export interface PayOptions {
/** Price in dollars (USD), converted to token raw units internally. */
amountUsd: number;
/** Human-readable order id (any string), hashed to 32 bytes for the contract. */
orderId: string;
/** Buyer's Freighter public key. */
publicKey: string;
/** Token to pay with (defaults to the first supported token, USDC). */
token?: TokenConfig;
/** Called with human-readable progress updates. */
onStatus?: (status: string) => void;
}
export interface PayResult {
hash: string;
status: string;
receipt: PaymentReceipt | null;
amountUsd: number;
amountRaw: bigint;
/** Pre-flight simulation details (see lib/stellar/simulate.ts). */
simulation: {
minResourceFeeStroops: string;
recommendedInclusionFeeStroops: string;
instructions: number;
};
}
function status(s: string): void {
console.log(`[stellar] ${s}`);
}
/**
* Convert a USD amount to raw token units (7 decimals).
* e.g. 12.34 -> 123_400_000
*/
export function usdToRawUnits(amountUsd: number): bigint {
if (!Number.isFinite(amountUsd) || amountUsd <= 0) {
throw new WalletError("Invalid amount to pay.", "INVALID_AMOUNT");
}
const raw = Math.round(amountUsd * 10 ** USDC_DECIMALS);
return BigInt(raw);
}
/**
* Round-trip an order id string through its 32-byte hash (hex) so callers can
* cross-check on-chain order ids with their own order numbers.
*/
export async function orderIdHash(orderId: string): Promise<string> {
return hexOf(await hashOrderId(orderId));
}
function hexOf(bytes: Uint8Array): string {
return Array.from(bytes)
.map((b) => b.toString(16).padStart(2, "0"))
.join("");
}
/**
* Main flow: connect wallet -> readiness checks -> simulate -> prepare ->
* sign -> submit -> wait -> decode event.
*/
export async function payWithStellar(options: PayOptions): Promise<PayResult> {
const { amountUsd, orderId, publicKey, onStatus = status } = options;
const token = options.token ?? defaultToken();
if (!CHECKOUT_CONTRACT_ID) {
throw new WalletError(
"Checkout contract is not configured. Set NEXT_PUBLIC_CHECKOUT_CONTRACT_ID in .env.local.",
"CONTRACT_NOT_CONFIGURED"
);
}
const amountRaw = usdToRawUnits(amountUsd);
const orderBytes = await hashOrderId(orderId);
// 1. Network guard.
onStatus("Checking Freighter network…");
await ensureNetwork();
const server = new rpc.Server(RPC_URL);
// 2. Account readiness: funded, trustline present, balance sufficient.
onStatus("Checking account readiness…");
const readiness = await assertPaymentReady(server, publicKey, {
token,
requiredRaw: amountRaw,
strict: true,
});
// 3. Build the invocation.
onStatus("Building payment transaction…");
const args = [
addressToScVal(token.contractId),
addressToScVal(publicKey),
bytes32ToScVal(orderBytes),
i128ToScVal(amountRaw),
];
const tx = buildInvocationTransaction(
readiness.account!,
CHECKOUT_CONTRACT_ID,
"pay",
args
);
// 4. Pre-flight simulation (surfaces errors early) + prepare.
onStatus("Simulating transaction…");
const { tx: prepared, report } = await prepareAndReport(server, tx);
if (!report.ok || !readiness.account) {
throw new WalletError(
`Transaction simulation failed: ${report.error?.message ?? "unknown error"}`,
"TX_SIMULATION_ERROR"
);
}
const fee = await budgetFee(server, report);
onStatus(
`Estimated fee: ${report.minResourceFee?.toString() ?? "?"} stroops resource + ${fee} stroops total`
);
// 5. Sign with Freighter.
onStatus("Waiting for Freighter signature…");
const signedXdr = await signWithFreighter(prepared.toXDR(), publicKey);
const signedTx = TransactionBuilder.fromXDR(signedXdr, NETWORK_PASSPHRASE);
// 6. Submit.
onStatus("Submitting transaction…");
const sendResponse = await server.sendTransaction(signedTx);
if (sendResponse.status === "ERROR") {
throw new WalletError(
`Transaction rejected: ${sendResponse.errorResult?.toXDR("base64") ?? "unknown error"}`,
"TX_SEND_ERROR"
);
}
if (sendResponse.status === "PENDING" || sendResponse.status === "DUPLICATE") {
onStatus("Confirming transaction…");
}
// 7. Wait for final state and decode the payment event.
const txResult = await waitForTransaction(sendResponse.hash);
const receipt = decodePaymentEvent(txResult);
return {
hash: sendResponse.hash,
status: txResult.status,
receipt,
amountUsd,
amountRaw,
simulation: {
minResourceFeeStroops: report.minResourceFee?.toString() ?? "0",
recommendedInclusionFeeStroops: fee,
instructions: report.instructions ?? 0,
},
};
}