@anywalletkit/wallet-ton 0.2.5 → 0.3.1

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.cjs CHANGED
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.ts
21
21
  var src_exports = {};
22
22
  __export(src_exports, {
23
+ BroadcastUnknownError: () => BroadcastUnknownError,
23
24
  CAIP2: () => CAIP2,
24
25
  DEFAULT_SEND_MODE: () => DEFAULT_SEND_MODE,
25
26
  DEFAULT_TONAPI: () => DEFAULT_TONAPI,
@@ -53,6 +54,7 @@ __export(src_exports, {
53
54
  emulateViaTonapi: () => emulateViaTonapi,
54
55
  encodeHistoryCursor: () => encodeCursor,
55
56
  installBufferGlobal: () => installBufferGlobal,
57
+ isAnsweredRefusal: () => isAnsweredRefusal,
56
58
  mapTonapiActions: () => mapActions,
57
59
  mapTonapiEvent: () => mapTonapiEvent,
58
60
  mapToncenterTransaction: () => mapToncenterTransaction,
@@ -118,6 +120,17 @@ var SimulationError = class extends Error {
118
120
  this.aborted = details.aborted ?? false;
119
121
  }
120
122
  };
123
+ var BroadcastUnknownError = class extends Error {
124
+ /** Always true: the transaction may be on its way. */
125
+ maybeSent = true;
126
+ /** Hex hash of the signed body. */
127
+ hash;
128
+ constructor(hash, cause) {
129
+ super(`Broadcast outcome unknown for ${hash}: ${cause instanceof Error ? cause.message.slice(0, 200) : String(cause).slice(0, 200)}`, { cause });
130
+ this.name = "BroadcastUnknownError";
131
+ this.hash = hash;
132
+ }
133
+ };
121
134
 
122
135
  // src/toncenter.ts
123
136
  var import_core2 = require("@anywalletkit/core");
@@ -155,12 +168,15 @@ function tonCenterEndpoint(endpoint, opts = {}) {
155
168
  }
156
169
  if (!res.ok || json.ok !== true) {
157
170
  const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
158
- throw new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url });
171
+ throw Object.assign(new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url }), { answered: true });
159
172
  }
160
173
  return json.result;
161
174
  }
162
175
  };
163
176
  }
177
+ function isAnsweredRefusal(e) {
178
+ return e?.answered === true;
179
+ }
164
180
  function createTonRpc(config) {
165
181
  const target = config.tonClient ?? config.provider;
166
182
  if (target === void 0) return void 0;
@@ -173,14 +189,17 @@ function createTonRpc(config) {
173
189
  return {
174
190
  async call(method, params) {
175
191
  let lastError;
192
+ let answered = true;
176
193
  for (let i = 0; i < attempts; i++) {
177
194
  try {
178
195
  return await list[i % list.length].call(method, params);
179
196
  } catch (e) {
180
197
  lastError = e;
198
+ answered &&= isAnsweredRefusal(e);
181
199
  }
182
200
  }
183
- throw new import_core2.NetworkError(`All TON endpoints failed for ${method}`, { cause: lastError });
201
+ const err = new import_core2.NetworkError(`All TON endpoints failed for ${method}`, { cause: lastError });
202
+ throw answered ? Object.assign(err, { answered: true }) : err;
184
203
  }
185
204
  };
186
205
  }
@@ -707,6 +726,19 @@ function describeOne(i, to, value, body, hasStateInit, bounceableOverride, warni
707
726
  return item;
708
727
  }
709
728
  var hex = (n) => `0x${n.toString(16).padStart(8, "0")}`;
729
+ var IGNORE_ERRORS = 2;
730
+ function sendModeOf(sendMode) {
731
+ return (sendMode ?? DEFAULT_SEND_MODE) | IGNORE_ERRORS;
732
+ }
733
+ function drainModes(mode) {
734
+ return [...(mode & 128) !== 0 ? ["carries-all-balance"] : [], ...(mode & 32) !== 0 ? ["destroys-account"] : []];
735
+ }
736
+ function withMode(i, item, mode, warnings) {
737
+ for (const code of drainModes(mode)) {
738
+ warnings.push({ code, message: code === "carries-all-balance" ? "send mode 128: the message carries the whole remaining balance" : "send mode 32: the wallet account is destroyed once it is empty", index: i });
739
+ }
740
+ return { ...item, mode };
741
+ }
710
742
  function describeMessageRelaxed(i, m, warnings, ctx = {}) {
711
743
  if (m.info.type !== "internal") {
712
744
  warnings.push({ code: "unknown-body", message: "not an internal message", index: i });
@@ -728,7 +760,7 @@ function describeTransaction(tx, ctx = {}) {
728
760
  return { kind: "contract-call", items, warnings };
729
761
  }
730
762
  external2 = { seqno: parsed.seqno, validUntil: parsed.validUntil, walletId: parsed.walletId, signed: parsed.signed };
731
- parsed.messages.forEach((m, i) => items.push(describeMessageRelaxed(i, m.message, warnings, ctx)));
763
+ parsed.messages.forEach((m, i) => items.push(withMode(i, describeMessageRelaxed(i, m.message, warnings, ctx), m.mode, warnings)));
732
764
  } else {
733
765
  const list = Array.isArray(tx) ? tx : [tx];
734
766
  list.forEach((t, i) => {
@@ -746,7 +778,7 @@ function describeTransaction(tx, ctx = {}) {
746
778
  } catch {
747
779
  warnings.push({ code: "unknown-body", message: "bodyBoc is not a valid BoC", index: i });
748
780
  }
749
- items.push(describeOne(i, to, BigInt(t.value), body, t.stateInit !== void 0, t.bounceable, warnings, ctx));
781
+ items.push(withMode(i, describeOne(i, to, BigInt(t.value), body, t.stateInit !== void 0, t.bounceable, warnings, ctx), sendModeOf(t.sendMode), warnings));
750
782
  });
751
783
  }
752
784
  const kinds = new Set(items.map((x) => x.kind));
@@ -852,25 +884,34 @@ var WalletAccountReadOnlyTon = class {
852
884
  if (src instanceof import_core8.Cell) return (0, import_core8.loadStateInit)(src.beginParse());
853
885
  return src;
854
886
  }
855
- transactionMessage(tx) {
856
- const value = big(tx.value);
857
- if (value <= 0n) throw new UnsafeTransactionError("zero-value", "TON message value must be positive.");
887
+ /**
888
+ * A destination address with the checks every payment gets: workchain 0 or -1, and a friendly
889
+ * address's test-only flag matching the wallet's network. Used for a native message's `to` and a
890
+ * jetton transfer's recipient and response destination (round 3, L8).
891
+ */
892
+ checkedAddress(to) {
858
893
  let address;
859
894
  let bounceable = true;
860
895
  let testOnly;
861
- if (import_core8.Address.isFriendly(tx.to)) {
862
- const f = import_core8.Address.parseFriendly(tx.to);
896
+ if (import_core8.Address.isFriendly(to)) {
897
+ const f = import_core8.Address.parseFriendly(to);
863
898
  address = f.address;
864
899
  bounceable = f.isBounceable;
865
900
  testOnly = f.isTestOnly;
866
901
  } else {
867
- address = import_core8.Address.parse(tx.to);
902
+ address = import_core8.Address.parse(to);
868
903
  }
869
904
  if (address.workChain !== 0 && address.workChain !== -1) throw new UnsafeTransactionError("exotic-workchain", `Refusing to send to workchain ${address.workChain}.`);
870
905
  const testnet = this._config.network === "testnet";
871
906
  if (testOnly !== void 0 && testOnly !== testnet) {
872
907
  throw new UnsafeTransactionError("network-mismatch", testOnly ? "Test-only address on a mainnet wallet." : "Mainnet address on a testnet wallet.");
873
908
  }
909
+ return { address, bounceable };
910
+ }
911
+ transactionMessage(tx) {
912
+ const value = big(tx.value);
913
+ if (value <= 0n) throw new UnsafeTransactionError("zero-value", "TON message value must be positive.");
914
+ const { address, bounceable } = this.checkedAddress(tx.to);
874
915
  const body = tx.body instanceof import_core8.Cell ? tx.body : typeof tx.body === "string" ? tx.body === "" ? void 0 : commentCell(tx.body) : tx.bodyBoc ? import_core8.Cell.fromBase64(tx.bodyBoc) : void 0;
875
916
  const init = this.stateInitOf(tx.stateInit);
876
917
  const message = (0, import_core8.internal)({ to: address, value, bounce: tx.bounceable ?? bounceable, ...body ? { body } : {}, ...init ? { init } : {} });
@@ -879,14 +920,15 @@ var WalletAccountReadOnlyTon = class {
879
920
  async jettonTransferMessage(o) {
880
921
  const amount = big(o.amount);
881
922
  if (amount <= 0n) throw new UnsafeTransactionError("zero-value", "Jetton amount must be positive.");
882
- const recipient = import_core8.Address.parse(o.recipient);
923
+ const recipient = this.checkedAddress(o.recipient).address;
924
+ const response = o.responseDestination ? this.checkedAddress(o.responseDestination).address : void 0;
883
925
  const jw = await this.getJettonWalletAddress(o.token);
884
926
  await this.jettonWalletData(o.token, jw);
885
927
  const forwardTonAmount = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
886
928
  if (o.memo !== void 0 && o.forwardPayload !== void 0) throw new UnsafeTransactionError("memo-and-payload", "Pass either memo or forwardPayload, not both: a jetton transfer carries one forward payload.");
887
929
  const payload = o.forwardPayload ?? o.memo;
888
930
  const forwardPayload = payload === void 0 ? null : typeof payload === "string" ? commentCell(payload) : payload;
889
- const body = (0, import_core8.beginCell)().storeUint(JETTON_TRANSFER_OP2, 32).storeUint(o.queryId !== void 0 ? BigInt(o.queryId) : randomQueryId(), 64).storeCoins(amount).storeAddress(recipient).storeAddress(o.responseDestination ? import_core8.Address.parse(o.responseDestination) : this._address).storeMaybeRef(o.customPayload ?? null).storeCoins(forwardTonAmount).storeMaybeRef(forwardPayload).endCell();
931
+ const body = (0, import_core8.beginCell)().storeUint(JETTON_TRANSFER_OP2, 32).storeUint(o.queryId !== void 0 ? BigInt(o.queryId) : randomQueryId(), 64).storeCoins(amount).storeAddress(recipient).storeAddress(response ?? this._address).storeMaybeRef(o.customPayload ?? null).storeCoins(forwardTonAmount).storeMaybeRef(forwardPayload).endCell();
890
932
  const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTonAmount;
891
933
  return { message: (0, import_core8.internal)({ to: jw, value: attached, bounce: true, body }) };
892
934
  }
@@ -1143,7 +1185,22 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1143
1185
  * Checks → seqno → preflight → signed body. The preflight runs on the
1144
1186
  * zero-signed body (ignore_chksig), so nothing is signed before it passes.
1145
1187
  */
1188
+ /**
1189
+ * Refuse send modes that drain the wallet — 128 (the whole remaining balance) and 32 (the account
1190
+ * destroyed once empty) — unless `allowDrainingTransactions`. Checked locally, before any seqno or
1191
+ * emulation, so it holds with `simulate: false` too (round 3, M11); the emulation check stays.
1192
+ */
1193
+ assertNoDrain(modes) {
1194
+ if (this._config.allowDrainingTransactions === true) return;
1195
+ const found = modes.flatMap(drainModes);
1196
+ if (found.length) throw new UnsafeTransactionError("drain", `Refusing a message with send mode ${found.includes("carries-all-balance") ? "128 (it carries the whole remaining balance)" : "32 (it destroys the wallet account once empty)"}; set allowDrainingTransactions to override.`);
1197
+ }
1198
+ /** The send modes of a prebuilt V5R1 body; an unreadable body is left to the emulation and the node. */
1199
+ bodyModes(body) {
1200
+ return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
1201
+ }
1146
1202
  async signMessages(messages, limit, op) {
1203
+ this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1147
1204
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1148
1205
  if (want > 0n) {
1149
1206
  const balance = await this.getBalance();
@@ -1175,6 +1232,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1175
1232
  /** Sign without broadcasting. Returns the external-message BODY as base64 BoC (pass it back to sendTransaction as a Cell). */
1176
1233
  async signTransaction(tx, options = {}) {
1177
1234
  if (tx instanceof import_core10.Cell) {
1235
+ this.assertNoDrain(this.bodyModes(tx));
1178
1236
  await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1179
1237
  return tx.toBoc().toString("base64");
1180
1238
  }
@@ -1184,12 +1242,19 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1184
1242
  async broadcast(body) {
1185
1243
  const rpc = this.requireRpc("send transactions");
1186
1244
  const init = await this.initIfNeeded();
1187
- await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1188
- return body.hash().toString("hex");
1245
+ const hash = body.hash().toString("hex");
1246
+ try {
1247
+ await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1248
+ } catch (e) {
1249
+ if (isAnsweredRefusal(e)) throw e;
1250
+ throw new BroadcastUnknownError(hash, e);
1251
+ }
1252
+ return hash;
1189
1253
  }
1190
1254
  /** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
1191
1255
  async sendTransaction(tx, options = {}) {
1192
1256
  this.requireRpc("send transactions");
1257
+ if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
1193
1258
  const { body, fee } = tx instanceof import_core10.Cell ? { body: tx, fee: await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction") } : await this.signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1194
1259
  return { hash: await this.broadcast(body), fee };
1195
1260
  }
@@ -1228,6 +1293,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
1228
1293
  static async fromTonMnemonic(words, config = {}) {
1229
1294
  const list = (Array.isArray(words) ? words : words.trim().split(/\s+/)).map((w) => w.toLowerCase());
1230
1295
  if (list.length !== 24) throw new Error("A TON mnemonic has 24 words.");
1296
+ if (!await (0, import_crypto.mnemonicValidate)(list)) throw new Error("Not a valid TON mnemonic: check the words (a typo would open a different, empty wallet).");
1231
1297
  const pair = await (0, import_crypto.mnemonicToPrivateKey)(list);
1232
1298
  const privateKey = new Uint8Array(pair.secretKey.subarray(0, 32));
1233
1299
  const publicKey = new Uint8Array(pair.publicKey);