@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/dist/index.js ADDED
@@ -0,0 +1,281 @@
1
+ import { mnemonicNew, mnemonicValidate, mnemonicToPrivateKey } from '@ton/crypto';
2
+ import { WalletContractV4, TonClient } from '@ton/ton';
3
+ import { toNano, beginCell, Address, internal, SendMode, external, storeMessage } from '@ton/core';
4
+ import { MyStarsValidationError, toMicro, JETTON_TRANSFER_OP } from '@mystars-tg/faas-sdk';
5
+
6
+ // src/wallet.ts
7
+ var WalletError = class extends Error {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = new.target.name;
11
+ Object.setPrototypeOf(this, new.target.prototype);
12
+ }
13
+ };
14
+ var InsufficientBalanceError = class extends WalletError {
15
+ };
16
+ var TonWallet = class _TonWallet {
17
+ /** The in-memory key pair. Treat `secretKey` as a secret — never log or persist it. */
18
+ keyPair;
19
+ contract;
20
+ constructor(keyPair) {
21
+ const contract = WalletContractV4.create({ workchain: 0, publicKey: keyPair.publicKey });
22
+ Object.defineProperty(this, "keyPair", { value: keyPair, enumerable: false });
23
+ Object.defineProperty(this, "contract", { value: contract, enumerable: false });
24
+ }
25
+ /** Redacted serialization — NEVER exposes the secret key. */
26
+ toJSON() {
27
+ return { address: this.address, publicKey: this.keyPair.publicKey.toString("hex") };
28
+ }
29
+ /** Redacted console.log/util.inspect output. */
30
+ [/* @__PURE__ */ Symbol.for("nodejs.util.inspect.custom")]() {
31
+ return `TonWallet<${this.address}>`;
32
+ }
33
+ /**
34
+ * Generate a NEW wallet. Returns the 24-word mnemonic ONCE — store it securely;
35
+ * it is never persisted.
36
+ *
37
+ * @returns the in-memory `wallet` and its `mnemonic` (the only time you see it)
38
+ * @example
39
+ * ```ts
40
+ * const { wallet, mnemonic } = await TonWallet.generate();
41
+ * await secureVault.store(mnemonic); // YOUR job — it is never written to disk by the SDK
42
+ * console.log("fund this:", wallet.address);
43
+ * ```
44
+ */
45
+ static async generate() {
46
+ const mnemonic = await mnemonicNew();
47
+ return { wallet: await _TonWallet.fromMnemonic(mnemonic), mnemonic };
48
+ }
49
+ /** Import a wallet from its 24-word mnemonic. */
50
+ static async fromMnemonic(words) {
51
+ if (!await mnemonicValidate(words)) throw new WalletError("invalid TON mnemonic");
52
+ const keyPair = await mnemonicToPrivateKey(words);
53
+ return new _TonWallet(keyPair);
54
+ }
55
+ /** Import a wallet from a raw Ed25519 key pair. */
56
+ static fromKeyPair(publicKey, secretKey) {
57
+ return new _TonWallet({ publicKey: Buffer.from(publicKey), secretKey: Buffer.from(secretKey) });
58
+ }
59
+ /** Friendly, non-bounceable (`UQ…`) address — FUND THIS to pay invoices. */
60
+ get address() {
61
+ return this.contract.address.toString({ bounceable: false });
62
+ }
63
+ /** Raw `0:hex` address form. */
64
+ get rawAddress() {
65
+ return this.contract.address.toRawString();
66
+ }
67
+ /** @internal The contract's StateInit, needed for the first (deploying) transfer. */
68
+ get init() {
69
+ return this.contract.init;
70
+ }
71
+ /** @internal The contract address as a TON `Address`. */
72
+ get tonAddress() {
73
+ return this.contract.address;
74
+ }
75
+ /** @internal Sign a transfer body. */
76
+ createTransfer(args) {
77
+ return this.contract.createTransfer({ ...args, secretKey: this.keyPair.secretKey });
78
+ }
79
+ /**
80
+ * This wallet's native TON balance.
81
+ *
82
+ * @param rpc - the RPC to query through
83
+ * @returns the balance in nanoTON
84
+ */
85
+ getBalance(rpc) {
86
+ return rpc.getBalance(this.address);
87
+ }
88
+ /**
89
+ * This wallet's current sequence number.
90
+ *
91
+ * @param rpc - the RPC to query through
92
+ * @returns the seqno (`0` when the wallet contract isn't deployed yet)
93
+ */
94
+ getSeqno(rpc) {
95
+ return rpc.getSeqno(this.address);
96
+ }
97
+ /**
98
+ * This wallet's balance of a given jetton (e.g. USDT).
99
+ *
100
+ * @param rpc - the RPC to query through
101
+ * @param jettonMaster - the jetton master address (e.g. `DEFAULT_USDT_MASTER` from `@mystars-tg/faas-wallet`)
102
+ * @returns the token balance in the jetton's smallest unit (micro-USDT for USDT)
103
+ */
104
+ async getJettonBalance(rpc, jettonMaster) {
105
+ const jettonWallet = await rpc.resolveJettonWallet(this.address, jettonMaster);
106
+ return rpc.getJettonBalance(jettonWallet);
107
+ }
108
+ };
109
+ var DEFAULT_USDT_MASTER = "EQCxE6mUtQJKFnGfaROTKOt1lZbDiiX1kCixRv7Nw2Id_sDs";
110
+ var DEFAULT_JETTON_GAS_TON = "0.05";
111
+ var NETWORK_FEE_RESERVE = toNano("0.02");
112
+ function commentCell(memo) {
113
+ return beginCell().storeUint(0, 32).storeStringTail(memo).endCell();
114
+ }
115
+ var OrderPayer = class {
116
+ wallet;
117
+ /** @param wallet - the funded {@link TonWallet} whose own funds will pay invoices */
118
+ constructor(wallet) {
119
+ this.wallet = wallet;
120
+ }
121
+ /**
122
+ * Plan the internal message(s) for an order WITHOUT signing or broadcasting.
123
+ * `ton` is pure; `usdt_ton` resolves the payer's jetton wallet via `rpc`.
124
+ */
125
+ async planMessages(payment, opts) {
126
+ if (!payment.pay_to_address) throw new WalletError("payment.pay_to_address is missing");
127
+ if (!payment.memo) throw new WalletError("payment.memo is missing");
128
+ if (payment.currency === "ton") {
129
+ const value = toNano(payment.amount);
130
+ if (value <= 0n) {
131
+ throw new MyStarsValidationError(`payment amount must be positive, got "${payment.amount}"`);
132
+ }
133
+ return [{ to: payment.pay_to_address, value, body: commentCell(payment.memo), bounce: false }];
134
+ }
135
+ const microAmount = toMicro(payment.amount);
136
+ if (microAmount <= 0n) {
137
+ throw new MyStarsValidationError(`payment amount must be positive, got "${payment.amount}"`);
138
+ }
139
+ const jettonMaster = opts.jettonMaster ?? DEFAULT_USDT_MASTER;
140
+ const jettonWallet = await opts.rpc.resolveJettonWallet(this.wallet.address, jettonMaster);
141
+ const body = beginCell().storeUint(JETTON_TRANSFER_OP, 32).storeUint(0n, 64).storeCoins(microAmount).storeAddress(Address.parse(payment.pay_to_address)).storeAddress(this.wallet.tonAddress).storeBit(false).storeCoins(0n).storeBit(true).storeRef(commentCell(payment.memo)).endCell();
142
+ return [
143
+ { to: jettonWallet, value: toNano(opts.jettonGasTon ?? DEFAULT_JETTON_GAS_TON), body, bounce: true }
144
+ ];
145
+ }
146
+ /** Throw `InsufficientBalanceError` if the wallet can't cover the planned payment + gas. */
147
+ async assertFunded(payment, planned, rpc) {
148
+ const tonBalance = await rpc.getBalance(this.wallet.address);
149
+ const first = planned[0];
150
+ if (payment.currency === "ton") {
151
+ const need = first.value + NETWORK_FEE_RESERVE;
152
+ if (tonBalance < need) {
153
+ throw new InsufficientBalanceError(`insufficient TON: balance ${tonBalance} < required ~${need} (nanoTON)`);
154
+ }
155
+ return;
156
+ }
157
+ const gasNeed = first.value + NETWORK_FEE_RESERVE;
158
+ if (tonBalance < gasNeed) {
159
+ throw new InsufficientBalanceError(`insufficient TON for jetton gas: balance ${tonBalance} < required ~${gasNeed} (nanoTON)`);
160
+ }
161
+ const jettonBalance = await rpc.getJettonBalance(first.to);
162
+ const micro = toMicro(payment.amount);
163
+ if (jettonBalance < micro) {
164
+ throw new InsufficientBalanceError(`insufficient USDT: balance ${jettonBalance} < required ${micro} (micro-USDT)`);
165
+ }
166
+ }
167
+ /**
168
+ * Build, sign, and broadcast the payment for an order. Signs exactly ONCE.
169
+ *
170
+ * MONEY: this broadcasts a real on-chain transfer of the wallet's OWN funds.
171
+ * It is not idempotent — calling it twice for the same order pays twice. Guard
172
+ * retries at the order layer (a stable `Idempotency-Key` on `createOrder`, or
173
+ * use `fulfill` which only pays an order still `awaiting_payment`).
174
+ *
175
+ * @param order - a `CreateOrderResult`, or any object carrying a `payment` block (and optional `order_id`)
176
+ * @param opts - the {@link PayOrderOptions} (at minimum `rpc`)
177
+ * @returns the {@link PayOrderResult} — `from`/`to` addresses and the smallest-unit amount sent
178
+ * @throws `WalletError` if the order's `payment` block is missing `pay_to_address`/`memo`
179
+ * @throws `MyStarsValidationError` if the amount is non-positive
180
+ * @throws {@link InsufficientBalanceError} if the wallet can't cover the payment + gas (unless `skipBalanceCheck`)
181
+ */
182
+ async payOrder(order, opts) {
183
+ const payment = order.payment;
184
+ const planned = await this.planMessages(payment, opts);
185
+ if (!opts.skipBalanceCheck) await this.assertFunded(payment, planned, opts.rpc);
186
+ const seqno = await opts.rpc.getSeqno(this.wallet.address);
187
+ const messages = planned.map(
188
+ (m) => internal({ to: Address.parse(m.to), value: m.value, body: m.body, bounce: m.bounce })
189
+ );
190
+ const timeout = Math.floor((opts.now ?? Date.now()) / 1e3) + (opts.validForSeconds ?? 120);
191
+ const transfer = this.wallet.createTransfer({ seqno, messages, sendMode: SendMode.PAY_GAS_SEPARATELY, timeout });
192
+ const ext = external({
193
+ to: this.wallet.tonAddress,
194
+ init: seqno === 0 ? this.wallet.init : void 0,
195
+ body: transfer
196
+ });
197
+ const boc = beginCell().store(storeMessage(ext)).endCell().toBoc();
198
+ await opts.rpc.sendBoc(boc);
199
+ const first = planned[0];
200
+ return {
201
+ orderId: order.order_id,
202
+ from: this.wallet.address,
203
+ to: first.to,
204
+ amountSmallestUnit: (payment.currency === "ton" ? toNano(payment.amount) : toMicro(payment.amount)).toString()
205
+ };
206
+ }
207
+ };
208
+ var ToncenterRpc = class {
209
+ client;
210
+ /** @param opts - the {@link ToncenterRpcOptions} (endpoint + optional API key) */
211
+ constructor(opts) {
212
+ this.client = new TonClient(opts.apiKey ? { endpoint: opts.endpoint, apiKey: opts.apiKey } : { endpoint: opts.endpoint });
213
+ }
214
+ /** {@inheritDoc TonRpc.getBalance} */
215
+ getBalance(address) {
216
+ return this.client.getBalance(Address.parse(address));
217
+ }
218
+ /** {@inheritDoc TonRpc.getSeqno} */
219
+ async getSeqno(address) {
220
+ const addr = Address.parse(address);
221
+ if (!await this.client.isContractDeployed(addr)) return 0;
222
+ const res = await this.client.runMethod(addr, "seqno");
223
+ return res.stack.readNumber();
224
+ }
225
+ /** {@inheritDoc TonRpc.resolveJettonWallet} */
226
+ async resolveJettonWallet(owner, jettonMaster) {
227
+ const res = await this.client.runMethod(Address.parse(jettonMaster), "get_wallet_address", [
228
+ { type: "slice", cell: beginCell().storeAddress(Address.parse(owner)).endCell() }
229
+ ]);
230
+ return res.stack.readAddress().toString();
231
+ }
232
+ /** {@inheritDoc TonRpc.getJettonBalance} */
233
+ async getJettonBalance(jettonWallet) {
234
+ const res = await this.client.runMethod(Address.parse(jettonWallet), "get_wallet_data");
235
+ return res.stack.readBigNumber();
236
+ }
237
+ /** {@inheritDoc TonRpc.sendBoc} */
238
+ async sendBoc(boc) {
239
+ await this.client.sendFile(Buffer.from(boc));
240
+ }
241
+ };
242
+ function withOrderId(err, orderId) {
243
+ if (err && typeof err === "object" && err.order_id === void 0) {
244
+ try {
245
+ err.order_id = orderId;
246
+ } catch {
247
+ }
248
+ }
249
+ return err;
250
+ }
251
+ function orderIdFromError(err) {
252
+ if (err && typeof err === "object") {
253
+ const id = err.order_id;
254
+ if (typeof id === "string") return id;
255
+ }
256
+ return void 0;
257
+ }
258
+ async function fulfill(client, wallet, params, opts) {
259
+ const idempotencyKey = opts.idempotencyKey ?? opts.createOptions?.idempotencyKey;
260
+ if (!idempotencyKey) {
261
+ throw new MyStarsValidationError(
262
+ "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"
263
+ );
264
+ }
265
+ const createOptions = { ...opts.createOptions, idempotencyKey };
266
+ const order = await client.createOrder(params, createOptions);
267
+ try {
268
+ if (order.status === "awaiting_payment") {
269
+ await new OrderPayer(wallet).payOrder(order, opts);
270
+ }
271
+ } catch (err) {
272
+ throw withOrderId(err, order.order_id);
273
+ }
274
+ try {
275
+ return await client.waitForOrder(order.order_id, opts.wait);
276
+ } catch (err) {
277
+ throw withOrderId(err, order.order_id);
278
+ }
279
+ }
280
+
281
+ export { DEFAULT_JETTON_GAS_TON, DEFAULT_USDT_MASTER, InsufficientBalanceError, OrderPayer, TonWallet, ToncenterRpc, WalletError, fulfill, orderIdFromError };
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "@mystars-tg/faas-wallet",
3
+ "version": "0.1.2",
4
+ "description": "Opt-in TON wallet + payer for @mystars-tg/faas-sdk — generate/import a wallet and pay a FaaS order invoice (TON or USDT) from your OWN wallet. Node only. Holds keys only in memory; never persists/logs/transmits them.",
5
+ "license": "MIT",
6
+ "author": "MyStars.tg",
7
+ "publishConfig": {
8
+ "access": "public"
9
+ },
10
+ "type": "module",
11
+ "sideEffects": false,
12
+ "engines": {
13
+ "node": ">=18"
14
+ },
15
+ "keywords": ["ton", "usdt", "telegram", "stars", "mystars", "faas", "wallet", "payments"],
16
+ "homepage": "https://mystars.tg/docs",
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "https://github.com/mystars-tg/faas-ts.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/mystars-tg/faas-ts/issues"
23
+ },
24
+ "main": "./dist/index.cjs",
25
+ "module": "./dist/index.js",
26
+ "types": "./dist/index.d.ts",
27
+ "exports": {
28
+ ".": {
29
+ "types": "./dist/index.d.ts",
30
+ "import": "./dist/index.js",
31
+ "require": "./dist/index.cjs"
32
+ },
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": ["dist", "README.md", "LICENSE"],
36
+ "scripts": {
37
+ "build": "tsup",
38
+ "typecheck": "tsc --noEmit",
39
+ "test": "vitest run --root ../..",
40
+ "prepublishOnly": "npm run build"
41
+ },
42
+ "dependencies": {
43
+ "@mystars-tg/faas-sdk": "^0.1.2",
44
+ "@ton/core": "^0.62.0",
45
+ "@ton/crypto": "^3.3.0",
46
+ "@ton/ton": "^15.1.0"
47
+ }
48
+ }