@anywalletkit/wallet-ton 0.2.4 → 0.3.0

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/README.md CHANGED
@@ -56,10 +56,18 @@ const tk = await WalletManagerTon.fromTonMnemonic(words, { tonClient })
56
56
  an aborted transaction, a failed action phase or a failed **first-hop**
57
57
  internal message (the operation would bounce) throws `SimulationError`
58
58
  **before signing**; failures deeper in the trace are `warnings`. tonapi's
59
- `risk` is returned as `risk` and a body that would carry the whole balance
60
- (send mode 128) is refused with `UnsafeTransactionError('drain')` unless
61
- `allowDrainingTransactions`. `emulate: false` keeps only `estimateFee`;
62
- `simulate: false` skips the preflight entirely.
59
+ `risk` is returned as `risk`. A message with send mode 128 (the whole
60
+ remaining balance) or 32 (the account destroyed once empty) is refused with
61
+ `UnsafeTransactionError('drain')` unless `allowDrainingTransactions` — checked
62
+ locally before any emulation, prebuilt bodies too, and again on tonapi's risk.
63
+ `describeTransaction` shows each message's `mode`. `emulate: false` keeps only
64
+ `estimateFee`; `simulate: false` skips the preflight entirely (not the mode check).
65
+ A jetton transfer's recipient and response destination get the same network
66
+ (test-only flag) and workchain checks as a native transfer.
67
+ A broadcast with no definitive answer (no response, a timeout, a non-JSON body)
68
+ throws `BroadcastUnknownError` (`maybeSent`, `hash` = the body hash): do not sign
69
+ again (the next seqno would pay twice); a node's `ok: false` stays a `NetworkError`.
70
+ A custom `TonRpc` should mark refusals `answered: true` (`isAnsweredRefusal`).
63
71
  4. noble ed25519 over the body hash; the 32-byte seed is the only key material
64
72
  and `dispose()` zeroizes it.
65
73
  5. `sendBoc` of the external (with StateInit when the wallet is not deployed).
@@ -68,7 +76,8 @@ const tk = await WalletManagerTon.fromTonMnemonic(words, { tonClient })
68
76
 
69
77
  The account never holds a key. Every signature is a `signer.sign(bytes, {scheme: 'ed25519', context})`
70
78
  call: `ton-external` over the hash of the V5R1 signing cell (after the preflight, which runs on a
71
- zero-signed body), `ton-message` over raw message bytes (`sign()`, ton_proof). Seed wallets get a
79
+ zero-signed body), `ton-message` over raw message bytes (`sign()`), `ton-proof` / `ton-sign-data`
80
+ over the TON Connect digests (`signTonProof`, `signTonConnectData`). Seed wallets get a
72
81
  `LocalSigner`; a hardware wallet, MPC node or backend is wrapped with `externalSigner()` from
73
82
  `@anywalletkit/core`:
74
83
 
@@ -82,6 +91,21 @@ A derivable `SeedSigner` at `m/44'/607'` yields `getAccount(i)` children; a leaf
82
91
  `fromTonMnemonic`) is one account. `WalletAccountTon.fromPrivateKey(priv, pub, path)` keeps the
83
92
  raw-key shape for callers that derive themselves.
84
93
 
94
+ ## Messages and TON Connect
95
+
96
+ `sign(message)` is a hex ed25519 signature over the raw bytes. A 32-byte message — bytes or a
97
+ string that encodes to 32 bytes — is refused with `MessageIsTransactionError`: the wallet contract
98
+ checks a transfer's signature over the 32-byte hash of its signing cell, so 32 raw bytes may be a
99
+ transfer. TON Connect has its own methods, which build the digest from the request and the
100
+ account's own address (0xffff ‖ "ton-connect…", never a cell hash):
101
+
102
+ ```ts
103
+ const proof = await account.signTonProof({ domain: 'app.example', timestamp, payload }) // the `proof` of a ton_proof reply
104
+ const reply = await account.signTonConnectData({ domain: 'app.example', timestamp, payload: { type: 'text', text } }) // signData (text | binary)
105
+ ```
106
+
107
+ `domain` is the host the wallet saw the request come from, never the dApp's own claim.
108
+
85
109
  ## Describe
86
110
 
87
111
  `describeTransaction(tx)` / `account.describe(tx)` decode TON transfers (text
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,
@@ -27,6 +28,7 @@ __export(src_exports, {
27
28
  JETTON_TRANSFER_OP: () => JETTON_TRANSFER_OP,
28
29
  MAINNET_GLOBAL_ID: () => MAINNET_GLOBAL_ID,
29
30
  MAX_ACTIONS: () => MAX_ACTIONS,
31
+ MessageIsTransactionError: () => MessageIsTransactionError,
30
32
  SEND_MODE: () => SEND_MODE,
31
33
  SimulationError: () => SimulationError,
32
34
  TESTNET_GLOBAL_ID: () => TESTNET_GLOBAL_ID,
@@ -52,6 +54,7 @@ __export(src_exports, {
52
54
  emulateViaTonapi: () => emulateViaTonapi,
53
55
  encodeHistoryCursor: () => encodeCursor,
54
56
  installBufferGlobal: () => installBufferGlobal,
57
+ isAnsweredRefusal: () => isAnsweredRefusal,
55
58
  mapTonapiActions: () => mapActions,
56
59
  mapTonapiEvent: () => mapTonapiEvent,
57
60
  mapToncenterTransaction: () => mapToncenterTransaction,
@@ -65,6 +68,8 @@ __export(src_exports, {
65
68
  runEmulation: () => runEmulation,
66
69
  tonCenterEndpoint: () => tonCenterEndpoint,
67
70
  tonExplorerTxUrl: () => tonExplorerTxUrl,
71
+ tonProofDigest: () => tonProofDigest,
72
+ tonSignDataDigest: () => tonSignDataDigest,
68
73
  v5r1Address: () => v5r1Address,
69
74
  v5r1Code: () => v5r1Code,
70
75
  v5r1ExternalMessage: () => v5r1ExternalMessage,
@@ -98,6 +103,13 @@ var UnsafeTransactionError = class extends import_core.SignerError {
98
103
  this.code = code;
99
104
  }
100
105
  };
106
+ var MessageIsTransactionError = class extends import_core.SignerError {
107
+ code = "message-is-transaction";
108
+ constructor() {
109
+ super("This message has the shape of a transaction (a 32-byte TON hash, or bytes naming this key as a Solana signer) and is not signed; use signTonProof / signTonConnectData for TON Connect, or signTransaction so it can be reviewed.");
110
+ this.name = "MessageIsTransactionError";
111
+ }
112
+ };
101
113
  var SimulationError = class extends Error {
102
114
  exitCode;
103
115
  aborted;
@@ -108,6 +120,17 @@ var SimulationError = class extends Error {
108
120
  this.aborted = details.aborted ?? false;
109
121
  }
110
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
+ };
111
134
 
112
135
  // src/toncenter.ts
113
136
  var import_core2 = require("@anywalletkit/core");
@@ -145,12 +168,15 @@ function tonCenterEndpoint(endpoint, opts = {}) {
145
168
  }
146
169
  if (!res.ok || json.ok !== true) {
147
170
  const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
148
- 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 });
149
172
  }
150
173
  return json.result;
151
174
  }
152
175
  };
153
176
  }
177
+ function isAnsweredRefusal(e) {
178
+ return e?.answered === true;
179
+ }
154
180
  function createTonRpc(config) {
155
181
  const target = config.tonClient ?? config.provider;
156
182
  if (target === void 0) return void 0;
@@ -163,14 +189,17 @@ function createTonRpc(config) {
163
189
  return {
164
190
  async call(method, params) {
165
191
  let lastError;
192
+ let answered = true;
166
193
  for (let i = 0; i < attempts; i++) {
167
194
  try {
168
195
  return await list[i % list.length].call(method, params);
169
196
  } catch (e) {
170
197
  lastError = e;
198
+ answered &&= isAnsweredRefusal(e);
171
199
  }
172
200
  }
173
- 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;
174
203
  }
175
204
  };
176
205
  }
@@ -697,6 +726,19 @@ function describeOne(i, to, value, body, hasStateInit, bounceableOverride, warni
697
726
  return item;
698
727
  }
699
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
+ }
700
742
  function describeMessageRelaxed(i, m, warnings, ctx = {}) {
701
743
  if (m.info.type !== "internal") {
702
744
  warnings.push({ code: "unknown-body", message: "not an internal message", index: i });
@@ -718,7 +760,7 @@ function describeTransaction(tx, ctx = {}) {
718
760
  return { kind: "contract-call", items, warnings };
719
761
  }
720
762
  external2 = { seqno: parsed.seqno, validUntil: parsed.validUntil, walletId: parsed.walletId, signed: parsed.signed };
721
- 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)));
722
764
  } else {
723
765
  const list = Array.isArray(tx) ? tx : [tx];
724
766
  list.forEach((t, i) => {
@@ -736,7 +778,7 @@ function describeTransaction(tx, ctx = {}) {
736
778
  } catch {
737
779
  warnings.push({ code: "unknown-body", message: "bodyBoc is not a valid BoC", index: i });
738
780
  }
739
- 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));
740
782
  });
741
783
  }
742
784
  const kinds = new Set(items.map((x) => x.kind));
@@ -842,25 +884,34 @@ var WalletAccountReadOnlyTon = class {
842
884
  if (src instanceof import_core8.Cell) return (0, import_core8.loadStateInit)(src.beginParse());
843
885
  return src;
844
886
  }
845
- transactionMessage(tx) {
846
- const value = big(tx.value);
847
- 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) {
848
893
  let address;
849
894
  let bounceable = true;
850
895
  let testOnly;
851
- if (import_core8.Address.isFriendly(tx.to)) {
852
- const f = import_core8.Address.parseFriendly(tx.to);
896
+ if (import_core8.Address.isFriendly(to)) {
897
+ const f = import_core8.Address.parseFriendly(to);
853
898
  address = f.address;
854
899
  bounceable = f.isBounceable;
855
900
  testOnly = f.isTestOnly;
856
901
  } else {
857
- address = import_core8.Address.parse(tx.to);
902
+ address = import_core8.Address.parse(to);
858
903
  }
859
904
  if (address.workChain !== 0 && address.workChain !== -1) throw new UnsafeTransactionError("exotic-workchain", `Refusing to send to workchain ${address.workChain}.`);
860
905
  const testnet = this._config.network === "testnet";
861
906
  if (testOnly !== void 0 && testOnly !== testnet) {
862
907
  throw new UnsafeTransactionError("network-mismatch", testOnly ? "Test-only address on a mainnet wallet." : "Mainnet address on a testnet wallet.");
863
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);
864
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;
865
916
  const init = this.stateInitOf(tx.stateInit);
866
917
  const message = (0, import_core8.internal)({ to: address, value, bounce: tx.bounceable ?? bounceable, ...body ? { body } : {}, ...init ? { init } : {} });
@@ -869,14 +920,15 @@ var WalletAccountReadOnlyTon = class {
869
920
  async jettonTransferMessage(o) {
870
921
  const amount = big(o.amount);
871
922
  if (amount <= 0n) throw new UnsafeTransactionError("zero-value", "Jetton amount must be positive.");
872
- 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;
873
925
  const jw = await this.getJettonWalletAddress(o.token);
874
926
  await this.jettonWalletData(o.token, jw);
875
927
  const forwardTonAmount = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
876
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.");
877
929
  const payload = o.forwardPayload ?? o.memo;
878
930
  const forwardPayload = payload === void 0 ? null : typeof payload === "string" ? commentCell(payload) : payload;
879
- 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();
880
932
  const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTonAmount;
881
933
  return { message: (0, import_core8.internal)({ to: jw, value: attached, bounce: true, body }) };
882
934
  }
@@ -994,8 +1046,75 @@ function randomQueryId() {
994
1046
 
995
1047
  // src/account.ts
996
1048
  var import_core9 = require("@anywalletkit/core");
1049
+ var import_buffer5 = require("buffer");
997
1050
  var import_core10 = require("@ton/core");
1051
+
1052
+ // src/tonconnect.ts
1053
+ var import_sha2 = require("@noble/hashes/sha2.js");
1054
+ var import_buffer4 = require("buffer");
1055
+ var utf82 = (s) => new TextEncoder().encode(s);
1056
+ var int32BE = (n) => {
1057
+ const b = new Uint8Array(4);
1058
+ new DataView(b.buffer).setInt32(0, n, false);
1059
+ return b;
1060
+ };
1061
+ var uint32 = (n, littleEndian) => {
1062
+ const b = new Uint8Array(4);
1063
+ new DataView(b.buffer).setUint32(0, n, littleEndian);
1064
+ return b;
1065
+ };
1066
+ var uint64 = (n, littleEndian) => {
1067
+ const b = new Uint8Array(8);
1068
+ new DataView(b.buffer).setBigUint64(0, BigInt(n), littleEndian);
1069
+ return b;
1070
+ };
1071
+ var concat = (...parts) => {
1072
+ const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
1073
+ let o = 0;
1074
+ for (const p of parts) {
1075
+ out.set(p, o);
1076
+ o += p.length;
1077
+ }
1078
+ return out;
1079
+ };
1080
+ function checkCommon(address, domain, timestamp) {
1081
+ if (address.hash.length !== 32) throw new RangeError("TON address hash must be 32 bytes.");
1082
+ if (!Number.isInteger(address.workChain)) throw new TypeError("TON address workChain must be an integer.");
1083
+ if (typeof domain !== "string" || domain.length === 0) throw new TypeError("TON Connect domain must be a non-empty string.");
1084
+ if (typeof timestamp !== "number" || !Number.isSafeInteger(timestamp) || timestamp < 0) throw new TypeError("TON Connect timestamp must be a non-negative integer (unix seconds).");
1085
+ }
1086
+ function base64Bytes(raw) {
1087
+ const s = raw.replace(/[\t\n\f\r ]/g, "");
1088
+ if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s) || s.replace(/=+$/, "").length % 4 === 1) throw new TypeError("signData binary payload must be base64.");
1089
+ return new Uint8Array(import_buffer4.Buffer.from(s, "base64"));
1090
+ }
1091
+ function tonProofDigest(address, request) {
1092
+ checkCommon(address, request.domain, request.timestamp);
1093
+ if (typeof request.payload !== "string") throw new TypeError("ton_proof payload must be a string.");
1094
+ const domain = utf82(request.domain);
1095
+ const inner = concat(utf82("ton-proof-item-v2/"), int32BE(address.workChain), address.hash, uint32(domain.length, true), domain, uint64(request.timestamp, true), utf82(request.payload));
1096
+ return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect"), (0, import_sha2.sha256)(inner)));
1097
+ }
1098
+ function tonSignDataDigest(address, request) {
1099
+ checkCommon(address, request.domain, request.timestamp);
1100
+ const p = request.payload;
1101
+ let payload;
1102
+ if (p?.type === "text" && typeof p.text === "string") payload = utf82(p.text);
1103
+ else if (p?.type === "binary" && typeof p.bytes === "string") payload = base64Bytes(p.bytes);
1104
+ else throw new TypeError('signData payload must be { type: "text", text } or { type: "binary", bytes }; cell payloads are not supported.');
1105
+ const domain = utf82(request.domain);
1106
+ return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect/sign-data/"), int32BE(address.workChain), address.hash, uint32(domain.length, false), domain, uint64(request.timestamp, false), utf82(p.type === "text" ? "txt" : "bin"), uint32(payload.length, false), payload));
1107
+ }
1108
+
1109
+ // src/account.ts
998
1110
  var toHex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
1111
+ var carriesKey = (bytes, key) => {
1112
+ outer: for (let i = 0; i + key.length <= bytes.length; i++) {
1113
+ for (let j = 0; j < key.length; j++) if (bytes[i + j] !== key[j]) continue outer;
1114
+ return true;
1115
+ }
1116
+ return false;
1117
+ };
999
1118
  var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon {
1000
1119
  /** seqno and fees come from the network before signing. */
1001
1120
  needsNetwork = true;
@@ -1019,11 +1138,46 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1019
1138
  const m = /^m\/44'\/607'\/(\d+)'$/.exec(this.path);
1020
1139
  return m ? Number(m[1]) : void 0;
1021
1140
  }
1022
- /** Hex ed25519 signature over the raw message bytes (ton_proof and the like). */
1141
+ /**
1142
+ * Hex ed25519 signature over the raw message bytes. A 32-byte message — string or bytes — is
1143
+ * refused with {@link MessageIsTransactionError}: the wallet contract checks a transfer's
1144
+ * signature over the 32-byte hash of its signing cell, so those bytes could be one. Bytes that
1145
+ * carry this account's public key are refused as well: one ed25519 key (an external signer) may
1146
+ * also be a Solana wallet, and a Solana transaction names its signer's key. TON Connect goes
1147
+ * through {@link signTonProof} and {@link signTonConnectData}.
1148
+ */
1023
1149
  async sign(message) {
1024
1150
  const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
1151
+ if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
1025
1152
  return toHex(await this.signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
1026
1153
  }
1154
+ /**
1155
+ * TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
1156
+ * digest is built here (0xffff ‖ "ton-connect" ‖ …), so no request can turn it into a transfer.
1157
+ * `domain` is the host the wallet saw the request come from.
1158
+ */
1159
+ async signTonProof(request) {
1160
+ const digest = tonProofDigest(this._address, request);
1161
+ const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
1162
+ return {
1163
+ timestamp: request.timestamp,
1164
+ domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
1165
+ payload: request.payload,
1166
+ signature: import_buffer5.Buffer.from(signature).toString("base64")
1167
+ };
1168
+ }
1169
+ /** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
1170
+ async signTonConnectData(request) {
1171
+ const digest = tonSignDataDigest(this._address, request);
1172
+ const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
1173
+ return {
1174
+ signature: import_buffer5.Buffer.from(signature).toString("base64"),
1175
+ address: this._address.toRawString(),
1176
+ timestamp: request.timestamp,
1177
+ domain: request.domain,
1178
+ payload: request.payload.type === "text" ? { type: "text", text: request.payload.text } : { type: "binary", bytes: request.payload.bytes }
1179
+ };
1180
+ }
1027
1181
  checkMaxFee(fee, limit, op) {
1028
1182
  if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
1029
1183
  }
@@ -1031,7 +1185,22 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1031
1185
  * Checks → seqno → preflight → signed body. The preflight runs on the
1032
1186
  * zero-signed body (ignore_chksig), so nothing is signed before it passes.
1033
1187
  */
1034
- async prepare(messages, limit, op) {
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
+ }
1202
+ async signMessages(messages, limit, op) {
1203
+ this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1035
1204
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1036
1205
  if (want > 0n) {
1037
1206
  const balance = await this.getBalance();
@@ -1063,29 +1232,37 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1063
1232
  /** Sign without broadcasting. Returns the external-message BODY as base64 BoC (pass it back to sendTransaction as a Cell). */
1064
1233
  async signTransaction(tx, options = {}) {
1065
1234
  if (tx instanceof import_core10.Cell) {
1235
+ this.assertNoDrain(this.bodyModes(tx));
1066
1236
  await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1067
1237
  return tx.toBoc().toString("base64");
1068
1238
  }
1069
- const { body } = await this.prepare(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1239
+ const { body } = await this.signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1070
1240
  return body.toBoc().toString("base64");
1071
1241
  }
1072
1242
  async broadcast(body) {
1073
1243
  const rpc = this.requireRpc("send transactions");
1074
1244
  const init = await this.initIfNeeded();
1075
- await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1076
- 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;
1077
1253
  }
1078
1254
  /** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
1079
1255
  async sendTransaction(tx, options = {}) {
1080
1256
  this.requireRpc("send transactions");
1081
- 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.prepare(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1257
+ if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
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");
1082
1259
  return { hash: await this.broadcast(body), fee };
1083
1260
  }
1084
1261
  /** TON (no `token`) or jetton transfer. */
1085
1262
  async transfer(o, writeOptions = {}) {
1086
1263
  this.requireRpc("transfer tokens");
1087
1264
  const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
1088
- const { body, fee } = await this.prepare([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1265
+ const { body, fee } = await this.signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1089
1266
  return { hash: await this.broadcast(body), fee };
1090
1267
  }
1091
1268
  /** The same wallet as a {@link WalletAccountReadOnlyTon}. */
@@ -1116,6 +1293,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
1116
1293
  static async fromTonMnemonic(words, config = {}) {
1117
1294
  const list = (Array.isArray(words) ? words : words.trim().split(/\s+/)).map((w) => w.toLowerCase());
1118
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).");
1119
1297
  const pair = await (0, import_crypto.mnemonicToPrivateKey)(list);
1120
1298
  const privateKey = new Uint8Array(pair.secretKey.subarray(0, 32));
1121
1299
  const publicKey = new Uint8Array(pair.publicKey);
@@ -1181,7 +1359,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
1181
1359
 
1182
1360
  // src/history.ts
1183
1361
  var import_core12 = require("@ton/core");
1184
- var import_buffer4 = require("buffer");
1362
+ var import_buffer6 = require("buffer");
1185
1363
  var HistoryUnavailableError = class extends Error {
1186
1364
  chain;
1187
1365
  reason;
@@ -1198,12 +1376,12 @@ var DEFAULT_LIMIT = 25;
1198
1376
  var MAX_LIMIT = 100;
1199
1377
  var DEFAULT_TIMEOUT_MS3 = 15e3;
1200
1378
  function encodeCursor(c) {
1201
- return import_buffer4.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
1379
+ return import_buffer6.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
1202
1380
  }
1203
1381
  function decodeCursor(s) {
1204
1382
  let parsed;
1205
1383
  try {
1206
- parsed = JSON.parse(import_buffer4.Buffer.from(s, "base64").toString("utf8"));
1384
+ parsed = JSON.parse(import_buffer6.Buffer.from(s, "base64").toString("utf8"));
1207
1385
  } catch {
1208
1386
  throw new Error("@anywalletkit/wallet-ton: malformed history cursor");
1209
1387
  }
@@ -1455,7 +1633,7 @@ function mapTonapiEvent(chain, address, own, event, valueFlow) {
1455
1633
  var TEXT_COMMENT_OP = "0x00000000";
1456
1634
  function mapToncenterTransaction(chain, address, row) {
1457
1635
  const own = row.account.toLowerCase();
1458
- const hash = import_buffer4.Buffer.from(row.hash, "base64").toString("hex");
1636
+ const hash = import_buffer6.Buffer.from(row.hash, "base64").toString("hex");
1459
1637
  const d = row.description;
1460
1638
  const failed = d?.aborted === true || d?.compute_ph?.skipped !== true && d?.compute_ph?.success === false || d?.action?.success === false;
1461
1639
  const transfers = [];