@anywalletkit/wallet-ton 0.4.0 → 0.7.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
@@ -170,12 +170,19 @@ function tonCenterEndpoint(endpoint, opts = {}) {
170
170
  }
171
171
  if (!res.ok || json.ok !== true) {
172
172
  const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
173
- throw Object.assign(new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url }), { answered: true });
173
+ const err = new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url });
174
+ throw isNodeRefusal(res.status, json) ? Object.assign(err, { answered: true }) : err;
174
175
  }
175
176
  return json.result;
176
177
  }
177
178
  };
178
179
  }
180
+ var UNKNOWN_STATUS = /* @__PURE__ */ new Set([408, 502, 503, 504]);
181
+ function isNodeRefusal(status, json) {
182
+ if (UNKNOWN_STATUS.has(status)) return false;
183
+ if (json.ok !== false || typeof json.error !== "string" || json.error.trim() === "") return false;
184
+ return !/time[ds]? ?out|timed out|deadline/i.test(json.error);
185
+ }
179
186
  function isAnsweredRefusal(e) {
180
187
  return e?.answered === true;
181
188
  }
@@ -1016,8 +1023,8 @@ var WalletAccountReadOnlyTon = class {
1016
1023
  return Math.floor(Date.now() / 1e3) + seconds;
1017
1024
  }
1018
1025
  /** The cell to sign for `messages` at the CURRENT seqno (fetched fresh every time). */
1019
- async signingCell(messages) {
1020
- const seqno = await this.getSeqno();
1026
+ async signingCell(messages, knownSeqno) {
1027
+ const seqno = knownSeqno ?? await this.getSeqno();
1021
1028
  const validUntil = this.validUntil();
1022
1029
  return { signing: v5r1SigningMessage({ walletId: this._walletId, validUntil, seqno, messages }), seqno, validUntil };
1023
1030
  }
@@ -1178,6 +1185,8 @@ function tonSignDataDigest(address, request) {
1178
1185
  }
1179
1186
 
1180
1187
  // src/account.ts
1188
+ var PENDING_SEQNO_TIMEOUT_MS = 6e4;
1189
+ var PENDING_SEQNO_POLL_MS = 1500;
1181
1190
  var toHex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
1182
1191
  var carriesKey = (bytes, key) => {
1183
1192
  outer: for (let i = 0; i + key.length <= bytes.length; i++) {
@@ -1191,13 +1200,28 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1191
1200
  needsNetwork = true;
1192
1201
  /** Derivation path, `ton-mnemonic`, or `external`. */
1193
1202
  path;
1194
- /** The signer that holds the key. */
1195
- signer;
1203
+ /**
1204
+ * The signer that holds the key, in a private field: not a property of the account, so no code
1205
+ * the account is handed to (a protocol module, a logger) can reach it and sign around the
1206
+ * account's checks (audit 2026-10-29 A1).
1207
+ */
1208
+ #signer;
1209
+ /** What holds the key: `seed`, `key` (one imported key) or `external`. */
1210
+ signerKind;
1196
1211
  _readOnly;
1212
+ /**
1213
+ * The last external this account broadcast (accepted, or maybe sent) and when it expires. Until
1214
+ * the wallet's seqno moves past it, a new message signed at the chain seqno would carry the same
1215
+ * seqno and silently never execute, so the next send waits for it first.
1216
+ */
1217
+ _inFlight;
1218
+ /** Tail of this account's send queue: seqno → sign → broadcast runs one send at a time. */
1219
+ _sendTail = Promise.resolve();
1197
1220
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
1198
1221
  constructor(signer, publicKey, path, config = {}) {
1199
1222
  super(publicKey, config);
1200
- this.signer = (0, import_core9.requireCurve)(signer, "ed25519");
1223
+ this.#signer = (0, import_core9.requireCurve)(signer, "ed25519");
1224
+ this.signerKind = this.#signer.kind;
1201
1225
  this.path = path;
1202
1226
  }
1203
1227
  /** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
@@ -1220,7 +1244,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1220
1244
  async sign(message) {
1221
1245
  const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
1222
1246
  if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
1223
- return toHex(await this.signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
1247
+ return toHex(await this.#signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
1224
1248
  }
1225
1249
  /**
1226
1250
  * TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
@@ -1229,7 +1253,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1229
1253
  */
1230
1254
  async signTonProof(request) {
1231
1255
  const digest = tonProofDigest(this._address, request);
1232
- const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
1256
+ const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
1233
1257
  return {
1234
1258
  timestamp: request.timestamp,
1235
1259
  domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
@@ -1240,7 +1264,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1240
1264
  /** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
1241
1265
  async signTonConnectData(request) {
1242
1266
  const digest = tonSignDataDigest(this._address, request);
1243
- const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
1267
+ const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
1244
1268
  return {
1245
1269
  signature: import_buffer6.Buffer.from(signature).toString("base64"),
1246
1270
  address: this._address.toRawString(),
@@ -1270,16 +1294,35 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1270
1294
  bodyModes(body) {
1271
1295
  return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
1272
1296
  }
1273
- async #signMessages(messages, limit, op) {
1297
+ /**
1298
+ * The seqno to sign the next send at: the chain's, once it has moved past this account's last
1299
+ * broadcast. While that external can still land (before its valid_until) the seqno is polled, up
1300
+ * to `pendingSeqnoTimeoutMs`; an expired one that never landed is forgotten.
1301
+ */
1302
+ async #nextSendSeqno() {
1303
+ const timeout = this._config.pendingSeqnoTimeoutMs ?? PENDING_SEQNO_TIMEOUT_MS;
1304
+ const deadline = Date.now() + timeout;
1305
+ for (; ; ) {
1306
+ const chain = await this.getSeqno();
1307
+ const prev = this._inFlight;
1308
+ if (!prev || chain > prev.seqno || Date.now() / 1e3 > prev.validUntil) {
1309
+ this._inFlight = void 0;
1310
+ return chain;
1311
+ }
1312
+ if (Date.now() >= deadline) throw new Error(`The previous transfer is still pending (seqno ${prev.seqno} has not moved): wait for it to land, then send again.`);
1313
+ await new Promise((resolve) => setTimeout(resolve, Math.min(PENDING_SEQNO_POLL_MS, Math.max(0, deadline - Date.now()))));
1314
+ }
1315
+ }
1316
+ async #signMessages(messages, limit, op, forSend = false) {
1274
1317
  this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1275
1318
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1276
1319
  if (want > 0n) {
1277
1320
  const balance = await this.getBalance();
1278
1321
  if (balance < want) throw new UnsafeTransactionError("insufficient-balance", `Insufficient TON balance: the messages carry ${want} nanotons, the wallet holds ${balance} (fees come on top).`);
1279
1322
  }
1280
- const { signing } = await this.signingCell(messages);
1323
+ const { signing } = await this.signingCell(messages, forSend ? await this.#nextSendSeqno() : void 0);
1281
1324
  const fee = await this.preflight(v5r1PackUnsigned(signing), limit, op, want);
1282
- const signature = await this.signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1325
+ const signature = await this.#signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1283
1326
  return { body: v5r1PackSigned(signing, signature), fee };
1284
1327
  }
1285
1328
  async preflight(body, limit, op, expectedTonOut) {
@@ -1314,26 +1357,47 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1314
1357
  const rpc = this.requireRpc("send transactions");
1315
1358
  const init = await this.initIfNeeded();
1316
1359
  const hash = body.hash().toString("hex");
1360
+ const parsed = parseV5R1Body(body);
1361
+ const sent = () => {
1362
+ if (parsed) this._inFlight = { seqno: parsed.seqno, validUntil: parsed.validUntil };
1363
+ };
1317
1364
  try {
1318
1365
  await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1319
1366
  } catch (e) {
1320
1367
  if (isAnsweredRefusal(e)) throw e;
1368
+ sent();
1321
1369
  throw new BroadcastUnknownError(hash, e);
1322
1370
  }
1371
+ sent();
1323
1372
  return hash;
1324
1373
  }
1374
+ /**
1375
+ * One send at a time per account: two sends started together would read the same seqno before
1376
+ * either is broadcast. A failed send does not block the next.
1377
+ */
1378
+ #serial(run) {
1379
+ const next = this._sendTail.then(run);
1380
+ this._sendTail = next.then(() => void 0, () => void 0);
1381
+ return next;
1382
+ }
1325
1383
  /** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
1326
- async sendTransaction(tx, options = {}) {
1384
+ sendTransaction(tx, options = {}) {
1385
+ return this.#serial(() => this.#sendTransactionNow(tx, options));
1386
+ }
1387
+ async #sendTransactionNow(tx, options) {
1327
1388
  this.requireRpc("send transactions");
1328
1389
  if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
1329
- 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");
1390
+ 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", true);
1330
1391
  return { hash: await this.broadcast(body), fee };
1331
1392
  }
1332
1393
  /** TON (no `token`) or jetton transfer. */
1333
- async transfer(o, writeOptions = {}) {
1394
+ transfer(o, writeOptions = {}) {
1395
+ return this.#serial(() => this.#transferNow(o, writeOptions));
1396
+ }
1397
+ async #transferNow(o, writeOptions) {
1334
1398
  this.requireRpc("transfer tokens");
1335
1399
  const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
1336
- const { body, fee } = await this.#signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
1400
+ const { body, fee } = await this.#signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer", true);
1337
1401
  return { hash: await this.broadcast(body), fee };
1338
1402
  }
1339
1403
  /** The same wallet as a {@link WalletAccountReadOnlyTon}. */
@@ -1343,7 +1407,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1343
1407
  }
1344
1408
  /** Disposes the signer: a local key is zeroized, an external one is released. */
1345
1409
  dispose() {
1346
- this.signer.dispose();
1410
+ this.#signer.dispose();
1347
1411
  }
1348
1412
  };
1349
1413