@mystars-tg/faas-wallet 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MyStars.tg
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # @mystars-tg/faas-wallet
2
+
3
+ [![npm](https://img.shields.io/npm/v/@mystars-tg/faas-wallet.svg)](https://www.npmjs.com/package/@mystars-tg/faas-wallet) [![license](https://img.shields.io/npm/l/@mystars-tg/faas-wallet.svg)](LICENSE)
4
+
5
+ Opt-in TON wallet + payer for [`@mystars-tg/faas-sdk`](https://www.npmjs.com/package/@mystars-tg/faas-sdk).
6
+ **Node only.** Generate or import a TON wallet, read balances, and pay a FaaS order invoice (TON or
7
+ USDT) from **your own** wallet.
8
+
9
+ ๐Ÿ“– API reference: **[mystars.tg/docs](https://mystars.tg/docs)** ยท core client: [`@mystars-tg/faas-sdk`](https://www.npmjs.com/package/@mystars-tg/faas-sdk).
10
+
11
+ > **Key custody:** keys live only in process memory. This package never writes a mnemonic or secret
12
+ > key to disk, never logs it, and never sends it anywhere. `generate()` returns the mnemonic exactly
13
+ > once โ€” storing it securely is your responsibility. Payments move **your** funds from **your** wallet
14
+ > to the MyStars treasury invoice; the SDK never touches your keys beyond signing in memory.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install @mystars-tg/faas-sdk @mystars-tg/faas-wallet
20
+ ```
21
+
22
+ ## One-call fulfilment
23
+
24
+ ```ts
25
+ import { MyStarsClient } from "@mystars-tg/faas-sdk";
26
+ import { TonWallet, ToncenterRpc, fulfill } from "@mystars-tg/faas-wallet";
27
+
28
+ // 1. Create (or import) a funding wallet and fund its address with TON/USDT.
29
+ const { wallet, mnemonic } = await TonWallet.generate(); // STORE `mnemonic` securely
30
+ console.log("Fund this address:", wallet.address);
31
+
32
+ // 2. Wire a client + an RPC.
33
+ const client = MyStarsClient.production(process.env.MYSTARS_API_KEY!);
34
+ const rpc = new ToncenterRpc({ endpoint: "https://toncenter.com/api/v2/jsonRPC", apiKey: process.env.TONCENTER_KEY });
35
+
36
+ // 3. create โ†’ pay โ†’ wait, in one call.
37
+ // A STABLE idempotencyKey is REQUIRED โ€” see the retry-hazard note below.
38
+ const order = await fulfill(
39
+ client, wallet,
40
+ { type: "stars", recipient: { username: "durov" }, quantity: 100 },
41
+ { rpc, idempotencyKey: `order-${myOrderId}` },
42
+ );
43
+ console.log(order.status, order.purchase_tx);
44
+ ```
45
+
46
+ ## โš ๏ธ Retry hazard โ€” `fulfill()` moves real money
47
+
48
+ `fulfill()` broadcasts a **real on-chain payment**. A naive retry of a `fulfill` that already paid
49
+ would create a SECOND order and pay it AGAIN (double-spend). Two safeguards make retries safe, and the
50
+ first is **mandatory**:
51
+
52
+ 1. **A stable `idempotencyKey` is required** (`opts.idempotencyKey`, or `createOptions.idempotencyKey`).
53
+ Derive it from your own order id so re-running `fulfill` reuses the same key โ€” the server returns the
54
+ SAME order (idempotent replay) instead of minting a duplicate. `fulfill` throws a
55
+ `MyStarsValidationError` before creating or paying if you omit it.
56
+ 2. **It only broadcasts when the order still needs payment.** If the (replayed) order has already
57
+ advanced past `awaiting_payment`, `fulfill` skips the payment and just waits โ€” so a retry never
58
+ double-pays an order whose first payment already landed.
59
+
60
+ If anything throws after the order exists, the error carries `order_id` โ€” read it with the type-safe
61
+ `orderIdFromError(err)` accessor and re-attach instead of re-running `fulfill`:
62
+
63
+ ```ts
64
+ import { fulfill, orderIdFromError } from "@mystars-tg/faas-wallet";
65
+
66
+ try {
67
+ await fulfill(client, wallet, params, { rpc, idempotencyKey: `order-${myOrderId}` });
68
+ } catch (err) {
69
+ const orderId = orderIdFromError(err);
70
+ if (orderId) {
71
+ // Payment may have been broadcast โ€” re-attach, do NOT re-run fulfill.
72
+ await client.waitForOrder(orderId); // resolves once the order is terminal
73
+ }
74
+ throw err;
75
+ }
76
+ ```
77
+
78
+ ## Step by step
79
+
80
+ ```ts
81
+ import { TonWallet, OrderPayer, ToncenterRpc } from "@mystars-tg/faas-wallet";
82
+
83
+ const wallet = await TonWallet.fromMnemonic(mnemonic);
84
+ const rpc = new ToncenterRpc({ endpoint: "https://toncenter.com/api/v2/jsonRPC" });
85
+
86
+ const balance = await wallet.getBalance(rpc); // nanoTON
87
+ const usdt = await wallet.getJettonBalance(rpc, USDT_MASTER); // micro-USDT
88
+
89
+ const order = await client.createOrder({ type: "stars", recipient: { username: "durov" }, quantity: 100, payment_currency: "usdt_ton" });
90
+ await new OrderPayer(wallet).payOrder(order, { rpc }); // signs once + broadcasts
91
+ const final = await client.waitForOrder(order.order_id);
92
+ ```
93
+
94
+ `TonWallet` currently creates **WalletContractV4** wallets โ€” fund the `address` it produces. For a
95
+ custom signer (keys in an HSM, never in the SDK), build the TON Connect message with the core SDK's
96
+ `buildTonConnectMessages` instead and sign it yourself.
package/dist/index.cjs ADDED
@@ -0,0 +1,291 @@
1
+ 'use strict';
2
+
3
+ var crypto = require('@ton/crypto');
4
+ var ton = require('@ton/ton');
5
+ var core = require('@ton/core');
6
+ var faasSdk = require('@mystars-tg/faas-sdk');
7
+
8
+ // src/wallet.ts
9
+ var WalletError = class extends Error {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = new.target.name;
13
+ Object.setPrototypeOf(this, new.target.prototype);
14
+ }
15
+ };
16
+ var InsufficientBalanceError = class extends WalletError {
17
+ };
18
+ var TonWallet = class _TonWallet {
19
+ /** The in-memory key pair. Treat `secretKey` as a secret โ€” never log or persist it. */
20
+ keyPair;
21
+ contract;
22
+ constructor(keyPair) {
23
+ const contract = ton.WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
24
+ Object.defineProperty(this, "keyPair", { value: keyPair, enumerable: false });
25
+ Object.defineProperty(this, "contract", { value: contract, enumerable: false });
26
+ }
27
+ /** Redacted serialization โ€” NEVER exposes the secret key. */
28
+ toJSON() {
29
+ return { address: this.address, publicKey: this.keyPair.publicKey.toString("hex") };
30
+ }
31
+ /** Redacted console.log/util.inspect output. */
32
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
33
+ return `TonWallet<${this.address}>`;
34
+ }
35
+ /**
36
+ * Generate a NEW wallet. Returns the 24-word mnemonic ONCE โ€” store it securely;
37
+ * it is never persisted.
38
+ *
39
+ * @returns the in-memory `wallet` and its `mnemonic` (the only time you see it)
40
+ * @example
41
+ * ```ts
42
+ * const { wallet, mnemonic } = await TonWallet.generate();
43
+ * await secureVault.store(mnemonic); // YOUR job โ€” it is never written to disk by the SDK
44
+ * console.log("fund this:", wallet.address);
45
+ * ```
46
+ */
47
+ static async generate() {
48
+ const mnemonic = await crypto.mnemonicNew();
49
+ return { wallet: await _TonWallet.fromMnemonic(mnemonic), mnemonic };
50
+ }
51
+ /** Import a wallet from its 24-word mnemonic. */
52
+ static async fromMnemonic(words) {
53
+ if (!await crypto.mnemonicValidate(words)) throw new WalletError("invalid TON mnemonic");
54
+ const keyPair = await crypto.mnemonicToPrivateKey(words);
55
+ return new _TonWallet(keyPair);
56
+ }
57
+ /** Import a wallet from a raw Ed25519 key pair. */
58
+ static fromKeyPair(publicKey, secretKey) {
59
+ return new _TonWallet({ publicKey: Buffer.from(publicKey), secretKey: Buffer.from(secretKey) });
60
+ }
61
+ /** Friendly, non-bounceable (`UQโ€ฆ`) address โ€” FUND THIS to pay invoices. */
62
+ get address() {
63
+ return this.contract.address.toString({ bounceable: false });
64
+ }
65
+ /** Raw `0:hex` address form. */
66
+ get rawAddress() {
67
+ return this.contract.address.toRawString();
68
+ }
69
+ /** @internal The contract's StateInit, needed for the first (deploying) transfer. */
70
+ get init() {
71
+ return this.contract.init;
72
+ }
73
+ /** @internal The contract address as a TON `Address`. */
74
+ get tonAddress() {
75
+ return this.contract.address;
76
+ }
77
+ /** @internal Sign a transfer body. */
78
+ createTransfer(args) {
79
+ return this.contract.createTransfer({ ...args, secretKey: this.keyPair.secretKey });
80
+ }
81
+ /**
82
+ * This wallet's native TON balance.
83
+ *
84
+ * @param rpc - the RPC to query through
85
+ * @returns the balance in nanoTON
86
+ */
87
+ getBalance(rpc) {
88
+ return rpc.getBalance(this.address);
89
+ }
90
+ /**
91
+ * This wallet's current sequence number.
92
+ *
93
+ * @param rpc - the RPC to query through
94
+ * @returns the seqno (`0` when the wallet contract isn't deployed yet)
95
+ */
96
+ getSeqno(rpc) {
97
+ return rpc.getSeqno(this.address);
98
+ }
99
+ /**
100
+ * This wallet's balance of a given jetton (e.g. USDT).
101
+ *
102
+ * @param rpc - the RPC to query through
103
+ * @param jettonMaster - the jetton master address (e.g. `DEFAULT_USDT_MASTER` from `@mystars-tg/faas-wallet`)
104
+ * @returns the token balance in the jetton's smallest unit (micro-USDT for USDT)
105
+ */
106
+ async getJettonBalance(rpc, jettonMaster) {
107
+ const jettonWallet = await rpc.resolveJettonWallet(this.address, jettonMaster);
108
+ return rpc.getJettonBalance(jettonWallet);
109
+ }
110
+ };
111
+ var DEFAULT_USDT_MASTER = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs";
112
+ var DEFAULT_JETTON_GAS_TON = "0.05";
113
+ var NETWORK_FEE_RESERVE = core.toNano("0.02");
114
+ function commentCell(memo) {
115
+ return core.beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
116
+ }
117
+ var OrderPayer = class {
118
+ wallet;
119
+ /** @param wallet - the funded {@link TonWallet} whose own funds will pay invoices */
120
+ constructor(wallet) {
121
+ this.wallet = wallet;
122
+ }
123
+ /**
124
+ * Plan the internal message(s) for an order WITHOUT signing or broadcasting.
125
+ * `ton` is pure; `usdt_ton` resolves the payer's jetton wallet via `rpc`.
126
+ */
127
+ async planMessages(payment, opts) {
128
+ if (!payment.pay_to_address) throw new WalletError("payment.pay_to_address is missing");
129
+ if (!payment.memo) throw new WalletError("payment.memo is missing");
130
+ if (payment.currency === "ton") {
131
+ const value = core.toNano(payment.amount);
132
+ if (value <= 0n) {
133
+ throw new faasSdk.MyStarsValidationError(`payment amount must be positive, got "${payment.amount}"`);
134
+ }
135
+ return [{ to: payment.pay_to_address, value, body: commentCell(payment.memo), bounce: false }];
136
+ }
137
+ const microAmount = faasSdk.toMicro(payment.amount);
138
+ if (microAmount <= 0n) {
139
+ throw new faasSdk.MyStarsValidationError(`payment amount must be positive, got "${payment.amount}"`);
140
+ }
141
+ const jettonMaster = opts.jettonMaster ?? DEFAULT_USDT_MASTER;
142
+ const jettonWallet = await opts.rpc.resolveJettonWallet(this.wallet.address, jettonMaster);
143
+ const body = core.beginCell().storeUint(faasSdk.JETTON_TRANSFER_OP, 32).storeUint(0n, 64).storeCoins(microAmount).storeAddress(core.Address.parse(payment.pay_to_address)).storeAddress(this.wallet.tonAddress).storeBit(false).storeCoins(0n).storeBit(true).storeRef(commentCell(payment.memo)).endCell();
144
+ return [
145
+ { to: jettonWallet, value: core.toNano(opts.jettonGasTon ?? DEFAULT_JETTON_GAS_TON), body, bounce: true }
146
+ ];
147
+ }
148
+ /** Throw `InsufficientBalanceError` if the wallet can't cover the planned payment + gas. */
149
+ async assertFunded(payment, planned, rpc) {
150
+ const tonBalance = await rpc.getBalance(this.wallet.address);
151
+ const first = planned[0];
152
+ if (payment.currency === "ton") {
153
+ const need = first.value + NETWORK_FEE_RESERVE;
154
+ if (tonBalance < need) {
155
+ throw new InsufficientBalanceError(`insufficient TON: balance ${tonBalance} < required ~${need} (nanoTON)`);
156
+ }
157
+ return;
158
+ }
159
+ const gasNeed = first.value + NETWORK_FEE_RESERVE;
160
+ if (tonBalance < gasNeed) {
161
+ throw new InsufficientBalanceError(`insufficient TON for jetton gas: balance ${tonBalance} < required ~${gasNeed} (nanoTON)`);
162
+ }
163
+ const jettonBalance = await rpc.getJettonBalance(first.to);
164
+ const micro = faasSdk.toMicro(payment.amount);
165
+ if (jettonBalance < micro) {
166
+ throw new InsufficientBalanceError(`insufficient USDT: balance ${jettonBalance} < required ${micro} (micro-USDT)`);
167
+ }
168
+ }
169
+ /**
170
+ * Build, sign, and broadcast the payment for an order. Signs exactly ONCE.
171
+ *
172
+ * MONEY: this broadcasts a real on-chain transfer of the wallet's OWN funds.
173
+ * It is not idempotent โ€” calling it twice for the same order pays twice. Guard
174
+ * retries at the order layer (a stable `Idempotency-Key` on `createOrder`, or
175
+ * use `fulfill` which only pays an order still `awaiting_payment`).
176
+ *
177
+ * @param order - a `CreateOrderResult`, or any object carrying a `payment` block (and optional `order_id`)
178
+ * @param opts - the {@link PayOrderOptions} (at minimum `rpc`)
179
+ * @returns the {@link PayOrderResult} โ€” `from`/`to` addresses and the smallest-unit amount sent
180
+ * @throws `WalletError` if the order's `payment` block is missing `pay_to_address`/`memo`
181
+ * @throws `MyStarsValidationError` if the amount is non-positive
182
+ * @throws {@link InsufficientBalanceError} if the wallet can't cover the payment + gas (unless `skipBalanceCheck`)
183
+ */
184
+ async payOrder(order, opts) {
185
+ const payment = order.payment;
186
+ const planned = await this.planMessages(payment, opts);
187
+ if (!opts.skipBalanceCheck) await this.assertFunded(payment, planned, opts.rpc);
188
+ const seqno = await opts.rpc.getSeqno(this.wallet.address);
189
+ const messages = planned.map(
190
+ (m) => core.internal({ to: core.Address.parse(m.to), value: m.value, body: m.body, bounce: m.bounce })
191
+ );
192
+ const timeout = Math.floor((opts.now ?? Date.now()) / 1e3) + (opts.validForSeconds ?? 120);
193
+ const transfer = this.wallet.createTransfer({ seqno, messages, sendMode: core.SendMode.PAY_GAS_SEPARATELY, timeout });
194
+ const ext = core.external({
195
+ to: this.wallet.tonAddress,
196
+ init: seqno === 0 ? this.wallet.init : void 0,
197
+ body: transfer
198
+ });
199
+ const boc = core.beginCell().store(core.storeMessage(ext)).endCell().toBoc();
200
+ await opts.rpc.sendBoc(boc);
201
+ const first = planned[0];
202
+ return {
203
+ orderId: order.order_id,
204
+ from: this.wallet.address,
205
+ to: first.to,
206
+ amountSmallestUnit: (payment.currency === "ton" ? core.toNano(payment.amount) : faasSdk.toMicro(payment.amount)).toString()
207
+ };
208
+ }
209
+ };
210
+ var ToncenterRpc = class {
211
+ client;
212
+ /** @param opts - the {@link ToncenterRpcOptions} (endpoint + optional API key) */
213
+ constructor(opts) {
214
+ this.client = new ton.TonClient(opts.apiKey ? { endpoint: opts.endpoint, apiKey: opts.apiKey } : { endpoint: opts.endpoint });
215
+ }
216
+ /** {@inheritDoc TonRpc.getBalance} */
217
+ getBalance(address) {
218
+ return this.client.getBalance(core.Address.parse(address));
219
+ }
220
+ /** {@inheritDoc TonRpc.getSeqno} */
221
+ async getSeqno(address) {
222
+ const addr = core.Address.parse(address);
223
+ if (!await this.client.isContractDeployed(addr)) return 0;
224
+ const res = await this.client.runMethod(addr, "seqno");
225
+ return res.stack.readNumber();
226
+ }
227
+ /** {@inheritDoc TonRpc.resolveJettonWallet} */
228
+ async resolveJettonWallet(owner, jettonMaster) {
229
+ const res = await this.client.runMethod(core.Address.parse(jettonMaster), "get_wallet_address", [
230
+ { type: "slice", cell: core.beginCell().storeAddress(core.Address.parse(owner)).endCell() }
231
+ ]);
232
+ return res.stack.readAddress().toString();
233
+ }
234
+ /** {@inheritDoc TonRpc.getJettonBalance} */
235
+ async getJettonBalance(jettonWallet) {
236
+ const res = await this.client.runMethod(core.Address.parse(jettonWallet), "get_wallet_data");
237
+ return res.stack.readBigNumber();
238
+ }
239
+ /** {@inheritDoc TonRpc.sendBoc} */
240
+ async sendBoc(boc) {
241
+ await this.client.sendFile(Buffer.from(boc));
242
+ }
243
+ };
244
+ function withOrderId(err, orderId) {
245
+ if (err && typeof err === "object" && err.order_id === void 0) {
246
+ try {
247
+ err.order_id = orderId;
248
+ } catch {
249
+ }
250
+ }
251
+ return err;
252
+ }
253
+ function orderIdFromError(err) {
254
+ if (err && typeof err === "object") {
255
+ const id = err.order_id;
256
+ if (typeof id === "string") return id;
257
+ }
258
+ return void 0;
259
+ }
260
+ async function fulfill(client, wallet, params, opts) {
261
+ const idempotencyKey = opts.idempotencyKey ?? opts.createOptions?.idempotencyKey;
262
+ if (!idempotencyKey) {
263
+ throw new faasSdk.MyStarsValidationError(
264
+ "fulfill() requires a stable idempotencyKey (opts.idempotencyKey or createOptions.idempotencyKey), derived from your own order id \u2014 without it a retry would create a duplicate order and broadcast a second payment"
265
+ );
266
+ }
267
+ const createOptions = { ...opts.createOptions, idempotencyKey };
268
+ const order = await client.createOrder(params, createOptions);
269
+ try {
270
+ if (order.status === "awaiting_payment") {
271
+ await new OrderPayer(wallet).payOrder(order, opts);
272
+ }
273
+ } catch (err) {
274
+ throw withOrderId(err, order.order_id);
275
+ }
276
+ try {
277
+ return await client.waitForOrder(order.order_id, opts.wait);
278
+ } catch (err) {
279
+ throw withOrderId(err, order.order_id);
280
+ }
281
+ }
282
+
283
+ exports.DEFAULT_JETTON_GAS_TON = DEFAULT_JETTON_GAS_TON;
284
+ exports.DEFAULT_USDT_MASTER = DEFAULT_USDT_MASTER;
285
+ exports.InsufficientBalanceError = InsufficientBalanceError;
286
+ exports.OrderPayer = OrderPayer;
287
+ exports.TonWallet = TonWallet;
288
+ exports.ToncenterRpc = ToncenterRpc;
289
+ exports.WalletError = WalletError;
290
+ exports.fulfill = fulfill;
291
+ exports.orderIdFromError = orderIdFromError;