@anywalletkit/wallet-ton 0.3.1 → 0.5.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/dist/index.cjs CHANGED
@@ -41,6 +41,7 @@ __export(src_exports, {
41
41
  WalletAccountReadOnlyTon: () => WalletAccountReadOnlyTon,
42
42
  WalletAccountTon: () => WalletAccountTon,
43
43
  WalletManagerTon: () => WalletManagerTon,
44
+ canonicalBocCell: () => canonicalBocCell,
44
45
  commentCell: () => commentCell,
45
46
  createTonClient: () => createTonRpc,
46
47
  createTonHistory: () => createTonHistory,
@@ -58,6 +59,7 @@ __export(src_exports, {
58
59
  mapTonapiActions: () => mapActions,
59
60
  mapTonapiEvent: () => mapTonapiEvent,
60
61
  mapToncenterTransaction: () => mapToncenterTransaction,
62
+ messageBody: () => messageBody,
61
63
  parseEmulation: () => parseEmulation,
62
64
  parseTonapiConsequences: () => parseTonapiConsequences,
63
65
  parseTonapiRisk: () => parseTonapiRisk,
@@ -623,6 +625,7 @@ function parseV5R1Body(body) {
623
625
  }
624
626
 
625
627
  // src/describe.ts
628
+ var import_buffer4 = require("buffer");
626
629
  var import_core6 = require("@ton/core");
627
630
  var JETTON_TRANSFER_OP = 260734629;
628
631
  var LARGE_FORWARD_NANOTON = 500000000n;
@@ -682,11 +685,67 @@ function parseTo(to) {
682
685
  const a = import_core6.Address.parse(to);
683
686
  return { to: a.toRawString(), bounceable: true, workchain: a.workChain };
684
687
  }
685
- function bodyOf(tx) {
688
+ function canonicalBocCell(b64, what = "bodyBoc") {
689
+ if (!/^[A-Za-z0-9+/_-]+={0,2}$/.test(b64)) throw new UnsafeTransactionError("non-canonical-boc", `${what} is not a base64 string.`);
690
+ const bytes = import_buffer4.Buffer.from(b64, "base64");
691
+ const layout = plainBocLayout(bytes);
692
+ if (layout !== null) throw new UnsafeTransactionError("non-canonical-boc", `${what} is not a plain single-root BoC (${layout}), so what was reviewed may not be what is signed.`);
693
+ let roots;
694
+ try {
695
+ roots = import_core6.Cell.fromBoc(bytes);
696
+ } catch {
697
+ throw new UnsafeTransactionError("non-canonical-boc", `${what} is not a valid BoC.`);
698
+ }
699
+ if (roots.length !== 1) throw new UnsafeTransactionError("non-canonical-boc", `${what} has ${roots.length} roots.`);
700
+ return roots[0];
701
+ }
702
+ function plainBocLayout(b) {
703
+ if (b.length < 6 || b[0] !== 181 || b[1] !== 238 || b[2] !== 156 || b[3] !== 114) return "not the generic BoC magic";
704
+ const flags = b[4];
705
+ const size = flags & 7;
706
+ const offBytes = b[5];
707
+ if (size < 1 || size > 4 || offBytes < 1 || offBytes > 8) return "bad size fields";
708
+ let i = 6;
709
+ const int = (n) => {
710
+ if (i + n > b.length) throw new RangeError("truncated");
711
+ let v = 0;
712
+ for (let k = 0; k < n; k++) v = v * 256 + b[i++];
713
+ return v;
714
+ };
715
+ try {
716
+ const cells = int(size);
717
+ const roots = int(size);
718
+ const absent = int(size);
719
+ int(offBytes);
720
+ if (roots !== 1) return `${roots} roots`;
721
+ if (absent !== 0) return "absent cells";
722
+ i += roots * size;
723
+ if (flags & 128) i += cells * offBytes;
724
+ for (let c = 0; c < cells; c++) {
725
+ if (i + 2 > b.length) return "truncated";
726
+ const d1 = b[i++];
727
+ const d2 = b[i++];
728
+ if (d1 & 16) return "a cell stores its hashes";
729
+ if (d1 >> 5 !== 0) return "a cell has a level mask";
730
+ if (d1 & 8) return "an exotic cell";
731
+ if ((d1 & 7) > 4) return "a cell with more than 4 refs";
732
+ i += Math.ceil(d2 / 2) + (d1 & 7) * size;
733
+ }
734
+ if (i + (flags & 64 ? 4 : 0) !== b.length) return "trailing or missing bytes";
735
+ } catch {
736
+ return "truncated";
737
+ }
738
+ return null;
739
+ }
740
+ function messageBody(tx) {
686
741
  if (tx.body instanceof import_core6.Cell) return tx.body;
687
742
  if (typeof tx.body === "string") return tx.body === "" ? void 0 : commentCell(tx.body);
688
- if (tx.bodyBoc) return import_core6.Cell.fromBase64(tx.bodyBoc);
689
- return void 0;
743
+ if (tx.body !== void 0) throw new UnsafeTransactionError("body", "A message body must be a Cell or a string (a text comment).");
744
+ if (tx.bodyBoc !== void 0 && typeof tx.bodyBoc !== "string") throw new UnsafeTransactionError("body", "bodyBoc must be a base64 string.");
745
+ return tx.bodyBoc ? canonicalBocCell(tx.bodyBoc) : void 0;
746
+ }
747
+ function bodyOf(tx) {
748
+ return messageBody(tx);
690
749
  }
691
750
  function commentCell(text) {
692
751
  return (0, import_core6.beginCell)().storeUint(0, 32).storeStringTail(text).endCell();
@@ -792,6 +851,8 @@ var import_ed25519 = require("@noble/curves/ed25519.js");
792
851
  var import_core8 = require("@ton/core");
793
852
  var JETTON_TRANSFER_OP2 = 260734629;
794
853
  var DEFAULT_JETTON_ATTACHED = 50000000n;
854
+ var DEFAULT_MAX_JETTON_ATTACHED = 1000000000n;
855
+ var RAW_ADDRESS = /^-?\d+:[0-9a-fA-F]{64}$/;
795
856
  var DEFAULT_INDEXER = "https://toncenter.com/api/v3";
796
857
  var DEFAULT_VALID_SECONDS = 60;
797
858
  var MAX_VALID_SECONDS = 600;
@@ -893,13 +954,15 @@ var WalletAccountReadOnlyTon = class {
893
954
  let address;
894
955
  let bounceable = true;
895
956
  let testOnly;
896
- if (import_core8.Address.isFriendly(to)) {
957
+ if (typeof to === "string" && import_core8.Address.isFriendly(to)) {
897
958
  const f = import_core8.Address.parseFriendly(to);
898
959
  address = f.address;
899
960
  bounceable = f.isBounceable;
900
961
  testOnly = f.isTestOnly;
901
- } else {
962
+ } else if (typeof to === "string" && RAW_ADDRESS.test(to)) {
902
963
  address = import_core8.Address.parse(to);
964
+ } else {
965
+ throw new UnsafeTransactionError("address", `${JSON.stringify(to)} is not a TON address in a strict form (raw wc:hex, or a friendly address with its checksum).`);
903
966
  }
904
967
  if (address.workChain !== 0 && address.workChain !== -1) throw new UnsafeTransactionError("exotic-workchain", `Refusing to send to workchain ${address.workChain}.`);
905
968
  const testnet = this._config.network === "testnet";
@@ -912,7 +975,7 @@ var WalletAccountReadOnlyTon = class {
912
975
  const value = big(tx.value);
913
976
  if (value <= 0n) throw new UnsafeTransactionError("zero-value", "TON message value must be positive.");
914
977
  const { address, bounceable } = this.checkedAddress(tx.to);
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;
978
+ const body = messageBody(tx);
916
979
  const init = this.stateInitOf(tx.stateInit);
917
980
  const message = (0, import_core8.internal)({ to: address, value, bounce: tx.bounceable ?? bounceable, ...body ? { body } : {}, ...init ? { init } : {} });
918
981
  return { message, ...tx.sendMode !== void 0 ? { mode: tx.sendMode } : {} };
@@ -922,14 +985,22 @@ var WalletAccountReadOnlyTon = class {
922
985
  if (amount <= 0n) throw new UnsafeTransactionError("zero-value", "Jetton amount must be positive.");
923
986
  const recipient = this.checkedAddress(o.recipient).address;
924
987
  const response = o.responseDestination ? this.checkedAddress(o.responseDestination).address : void 0;
988
+ if (response && !response.equals(this._address) && o.allowForeignResponseDestination !== true) {
989
+ throw new UnsafeTransactionError("foreign-response-destination", `The excess TON of this jetton transfer would go to ${response.toString()}, not to this wallet; pass allowForeignResponseDestination to allow it.`);
990
+ }
991
+ const forwardTon = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
992
+ const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTon;
993
+ const maxAttached = this._config.maxJettonAttachedValue !== void 0 ? big(this._config.maxJettonAttachedValue) : DEFAULT_MAX_JETTON_ATTACHED;
994
+ if (attached > maxAttached || forwardTon > attached) {
995
+ throw new UnsafeTransactionError("attached-value", `A jetton transfer may attach at most ${maxAttached} nanotons, forwardTonAmount included (asked: ${attached}, forward ${forwardTon}); raise config.maxJettonAttachedValue if this is intended.`);
996
+ }
925
997
  const jw = await this.getJettonWalletAddress(o.token);
926
998
  await this.jettonWalletData(o.token, jw);
927
- const forwardTonAmount = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
999
+ const forwardTonAmount = forwardTon;
928
1000
  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.");
929
1001
  const payload = o.forwardPayload ?? o.memo;
930
1002
  const forwardPayload = payload === void 0 ? null : typeof payload === "string" ? commentCell(payload) : payload;
931
1003
  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();
932
- const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTonAmount;
933
1004
  return { message: (0, import_core8.internal)({ to: jw, value: attached, bounce: true, body }) };
934
1005
  }
935
1006
  toMessages(tx) {
@@ -1046,12 +1117,12 @@ function randomQueryId() {
1046
1117
 
1047
1118
  // src/account.ts
1048
1119
  var import_core9 = require("@anywalletkit/core");
1049
- var import_buffer5 = require("buffer");
1120
+ var import_buffer6 = require("buffer");
1050
1121
  var import_core10 = require("@ton/core");
1051
1122
 
1052
1123
  // src/tonconnect.ts
1053
1124
  var import_sha2 = require("@noble/hashes/sha2.js");
1054
- var import_buffer4 = require("buffer");
1125
+ var import_buffer5 = require("buffer");
1055
1126
  var utf82 = (s) => new TextEncoder().encode(s);
1056
1127
  var int32BE = (n) => {
1057
1128
  const b = new Uint8Array(4);
@@ -1086,7 +1157,7 @@ function checkCommon(address, domain, timestamp) {
1086
1157
  function base64Bytes(raw) {
1087
1158
  const s = raw.replace(/[\t\n\f\r ]/g, "");
1088
1159
  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"));
1160
+ return new Uint8Array(import_buffer5.Buffer.from(s, "base64"));
1090
1161
  }
1091
1162
  function tonProofDigest(address, request) {
1092
1163
  checkCommon(address, request.domain, request.timestamp);
@@ -1120,13 +1191,20 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1120
1191
  needsNetwork = true;
1121
1192
  /** Derivation path, `ton-mnemonic`, or `external`. */
1122
1193
  path;
1123
- /** The signer that holds the key. */
1124
- signer;
1194
+ /**
1195
+ * The signer that holds the key, in a private field: not a property of the account, so no code
1196
+ * the account is handed to (a protocol module, a logger) can reach it and sign around the
1197
+ * account's checks (audit 2026-10-29 A1).
1198
+ */
1199
+ #signer;
1200
+ /** What holds the key: `seed`, `key` (one imported key) or `external`. */
1201
+ signerKind;
1125
1202
  _readOnly;
1126
1203
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
1127
1204
  constructor(signer, publicKey, path, config = {}) {
1128
1205
  super(publicKey, config);
1129
- this.signer = (0, import_core9.requireCurve)(signer, "ed25519");
1206
+ this.#signer = (0, import_core9.requireCurve)(signer, "ed25519");
1207
+ this.signerKind = this.#signer.kind;
1130
1208
  this.path = path;
1131
1209
  }
1132
1210
  /** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
@@ -1149,7 +1227,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1149
1227
  async sign(message) {
1150
1228
  const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
1151
1229
  if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
1152
- return toHex(await this.signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
1230
+ return toHex(await this.#signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
1153
1231
  }
1154
1232
  /**
1155
1233
  * TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
@@ -1158,20 +1236,20 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1158
1236
  */
1159
1237
  async signTonProof(request) {
1160
1238
  const digest = tonProofDigest(this._address, request);
1161
- const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
1239
+ const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
1162
1240
  return {
1163
1241
  timestamp: request.timestamp,
1164
1242
  domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
1165
1243
  payload: request.payload,
1166
- signature: import_buffer5.Buffer.from(signature).toString("base64")
1244
+ signature: import_buffer6.Buffer.from(signature).toString("base64")
1167
1245
  };
1168
1246
  }
1169
1247
  /** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
1170
1248
  async signTonConnectData(request) {
1171
1249
  const digest = tonSignDataDigest(this._address, request);
1172
- const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
1250
+ const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
1173
1251
  return {
1174
- signature: import_buffer5.Buffer.from(signature).toString("base64"),
1252
+ signature: import_buffer6.Buffer.from(signature).toString("base64"),
1175
1253
  address: this._address.toRawString(),
1176
1254
  timestamp: request.timestamp,
1177
1255
  domain: request.domain,
@@ -1199,7 +1277,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1199
1277
  bodyModes(body) {
1200
1278
  return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
1201
1279
  }
1202
- async signMessages(messages, limit, op) {
1280
+ async #signMessages(messages, limit, op) {
1203
1281
  this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1204
1282
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1205
1283
  if (want > 0n) {
@@ -1208,7 +1286,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1208
1286
  }
1209
1287
  const { signing } = await this.signingCell(messages);
1210
1288
  const fee = await this.preflight(v5r1PackUnsigned(signing), limit, op, want);
1211
- const signature = await this.signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1289
+ const signature = await this.#signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1212
1290
  return { body: v5r1PackSigned(signing, signature), fee };
1213
1291
  }
1214
1292
  async preflight(body, limit, op, expectedTonOut) {
@@ -1236,7 +1314,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1236
1314
  await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1237
1315
  return tx.toBoc().toString("base64");
1238
1316
  }
1239
- const { body } = await this.signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1317
+ const { body } = await this.#signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
1240
1318
  return body.toBoc().toString("base64");
1241
1319
  }
1242
1320
  async broadcast(body) {
@@ -1255,14 +1333,14 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1255
1333
  async sendTransaction(tx, options = {}) {
1256
1334
  this.requireRpc("send transactions");
1257
1335
  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");
1336
+ 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");
1259
1337
  return { hash: await this.broadcast(body), fee };
1260
1338
  }
1261
1339
  /** TON (no `token`) or jetton transfer. */
1262
1340
  async transfer(o, writeOptions = {}) {
1263
1341
  this.requireRpc("transfer tokens");
1264
1342
  const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
1265
- const { body, fee } = await this.signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1343
+ const { body, fee } = await this.#signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1266
1344
  return { hash: await this.broadcast(body), fee };
1267
1345
  }
1268
1346
  /** The same wallet as a {@link WalletAccountReadOnlyTon}. */
@@ -1272,7 +1350,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1272
1350
  }
1273
1351
  /** Disposes the signer: a local key is zeroized, an external one is released. */
1274
1352
  dispose() {
1275
- this.signer.dispose();
1353
+ this.#signer.dispose();
1276
1354
  }
1277
1355
  };
1278
1356
 
@@ -1359,7 +1437,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
1359
1437
 
1360
1438
  // src/history.ts
1361
1439
  var import_core12 = require("@ton/core");
1362
- var import_buffer6 = require("buffer");
1440
+ var import_buffer7 = require("buffer");
1363
1441
  var HistoryUnavailableError = class extends Error {
1364
1442
  chain;
1365
1443
  reason;
@@ -1376,12 +1454,12 @@ var DEFAULT_LIMIT = 25;
1376
1454
  var MAX_LIMIT = 100;
1377
1455
  var DEFAULT_TIMEOUT_MS3 = 15e3;
1378
1456
  function encodeCursor(c) {
1379
- return import_buffer6.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
1457
+ return import_buffer7.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
1380
1458
  }
1381
1459
  function decodeCursor(s) {
1382
1460
  let parsed;
1383
1461
  try {
1384
- parsed = JSON.parse(import_buffer6.Buffer.from(s, "base64").toString("utf8"));
1462
+ parsed = JSON.parse(import_buffer7.Buffer.from(s, "base64").toString("utf8"));
1385
1463
  } catch {
1386
1464
  throw new Error("@anywalletkit/wallet-ton: malformed history cursor");
1387
1465
  }
@@ -1633,7 +1711,7 @@ function mapTonapiEvent(chain, address, own, event, valueFlow) {
1633
1711
  var TEXT_COMMENT_OP = "0x00000000";
1634
1712
  function mapToncenterTransaction(chain, address, row) {
1635
1713
  const own = row.account.toLowerCase();
1636
- const hash = import_buffer6.Buffer.from(row.hash, "base64").toString("hex");
1714
+ const hash = import_buffer7.Buffer.from(row.hash, "base64").toString("hex");
1637
1715
  const d = row.description;
1638
1716
  const failed = d?.aborted === true || d?.compute_ph?.skipped !== true && d?.compute_ph?.success === false || d?.action?.success === false;
1639
1717
  const transfers = [];