@anywalletkit/wallet-ton 0.5.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++) {
@@ -1200,6 +1209,14 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1200
1209
  /** What holds the key: `seed`, `key` (one imported key) or `external`. */
1201
1210
  signerKind;
1202
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();
1203
1220
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
1204
1221
  constructor(signer, publicKey, path, config = {}) {
1205
1222
  super(publicKey, config);
@@ -1277,14 +1294,33 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1277
1294
  bodyModes(body) {
1278
1295
  return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
1279
1296
  }
1280
- 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) {
1281
1317
  this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1282
1318
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1283
1319
  if (want > 0n) {
1284
1320
  const balance = await this.getBalance();
1285
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).`);
1286
1322
  }
1287
- const { signing } = await this.signingCell(messages);
1323
+ const { signing } = await this.signingCell(messages, forSend ? await this.#nextSendSeqno() : void 0);
1288
1324
  const fee = await this.preflight(v5r1PackUnsigned(signing), limit, op, want);
1289
1325
  const signature = await this.#signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1290
1326
  return { body: v5r1PackSigned(signing, signature), fee };
@@ -1321,26 +1357,47 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1321
1357
  const rpc = this.requireRpc("send transactions");
1322
1358
  const init = await this.initIfNeeded();
1323
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
+ };
1324
1364
  try {
1325
1365
  await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1326
1366
  } catch (e) {
1327
1367
  if (isAnsweredRefusal(e)) throw e;
1368
+ sent();
1328
1369
  throw new BroadcastUnknownError(hash, e);
1329
1370
  }
1371
+ sent();
1330
1372
  return hash;
1331
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
+ }
1332
1383
  /** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
1333
- async sendTransaction(tx, options = {}) {
1384
+ sendTransaction(tx, options = {}) {
1385
+ return this.#serial(() => this.#sendTransactionNow(tx, options));
1386
+ }
1387
+ async #sendTransactionNow(tx, options) {
1334
1388
  this.requireRpc("send transactions");
1335
1389
  if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
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");
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);
1337
1391
  return { hash: await this.broadcast(body), fee };
1338
1392
  }
1339
1393
  /** TON (no `token`) or jetton transfer. */
1340
- async transfer(o, writeOptions = {}) {
1394
+ transfer(o, writeOptions = {}) {
1395
+ return this.#serial(() => this.#transferNow(o, writeOptions));
1396
+ }
1397
+ async #transferNow(o, writeOptions) {
1341
1398
  this.requireRpc("transfer tokens");
1342
1399
  const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
1343
- 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);
1344
1401
  return { hash: await this.broadcast(body), fee };
1345
1402
  }
1346
1403
  /** The same wallet as a {@link WalletAccountReadOnlyTon}. */