@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.d.cts CHANGED
@@ -65,6 +65,12 @@ interface TonConfig {
65
65
  simulate?: boolean;
66
66
  /** Seconds a signed external stays valid (default 60, max 600). */
67
67
  validUntilSeconds?: number;
68
+ /**
69
+ * Most milliseconds a send waits for this account's previous transfer to move the wallet's seqno
70
+ * before signing the next one (default 60 000). A transfer signed at the same seqno would never
71
+ * execute; past this wait the send throws instead.
72
+ */
73
+ pendingSeqnoTimeoutMs?: number;
68
74
  /** Nanotons attached to a jetton transfer on top of `forwardTonAmount` (default 0.05 TON). */
69
75
  jettonAttachedValue?: bigint | number | string;
70
76
  /**
@@ -267,7 +273,7 @@ declare function tonCenterEndpoint(endpoint: TonEndpoint, opts?: {
267
273
  fetch?: typeof fetch;
268
274
  timeoutMs?: number;
269
275
  }): TonRpc;
270
- /** An endpoint answered with a refusal (`ok: false`): nothing was taken. A thrown fetch, a timeout or a non-JSON body may have been. */
276
+ /** An endpoint answered with a node refusal (toncenter's `ok: false` envelope): nothing was taken. A thrown fetch, a timeout, a non-JSON body or a gateway error may have been. */
271
277
  declare function isAnsweredRefusal(e: unknown): boolean;
272
278
  /** One RPC, or a failover RPC walking the configured list (`retries` extra rounds after the first full pass). */
273
279
  declare function createTonRpc(config: TonConfig): TonRpc | undefined;
@@ -638,7 +644,7 @@ declare class WalletAccountReadOnlyTon implements IWalletAccountReadOnly {
638
644
  protected toMessages(tx: TonTransaction | TonTransaction[]): V5R1Message[];
639
645
  protected validUntil(): number;
640
646
  /** The cell to sign for `messages` at the CURRENT seqno (fetched fresh every time). */
641
- protected signingCell(messages: V5R1Message[]): Promise<{
647
+ protected signingCell(messages: V5R1Message[], knownSeqno?: number): Promise<{
642
648
  signing: Cell;
643
649
  seqno: number;
644
650
  validUntil: number;
@@ -752,6 +758,14 @@ declare class WalletAccountTon extends WalletAccountReadOnlyTon implements IWall
752
758
  /** What holds the key: `seed`, `key` (one imported key) or `external`. */
753
759
  readonly signerKind: Signer['kind'];
754
760
  private _readOnly;
761
+ /**
762
+ * The last external this account broadcast (accepted, or maybe sent) and when it expires. Until
763
+ * the wallet's seqno moves past it, a new message signed at the chain seqno would carry the same
764
+ * seqno and silently never execute, so the next send waits for it first.
765
+ */
766
+ private _inFlight;
767
+ /** Tail of this account's send queue: seqno → sign → broadcast runs one send at a time. */
768
+ private _sendTail;
755
769
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
756
770
  constructor(signer: Signer, publicKey: Uint8Array, path: string, config?: TonConfig);
757
771
  /** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
package/dist/index.d.ts CHANGED
@@ -65,6 +65,12 @@ interface TonConfig {
65
65
  simulate?: boolean;
66
66
  /** Seconds a signed external stays valid (default 60, max 600). */
67
67
  validUntilSeconds?: number;
68
+ /**
69
+ * Most milliseconds a send waits for this account's previous transfer to move the wallet's seqno
70
+ * before signing the next one (default 60 000). A transfer signed at the same seqno would never
71
+ * execute; past this wait the send throws instead.
72
+ */
73
+ pendingSeqnoTimeoutMs?: number;
68
74
  /** Nanotons attached to a jetton transfer on top of `forwardTonAmount` (default 0.05 TON). */
69
75
  jettonAttachedValue?: bigint | number | string;
70
76
  /**
@@ -267,7 +273,7 @@ declare function tonCenterEndpoint(endpoint: TonEndpoint, opts?: {
267
273
  fetch?: typeof fetch;
268
274
  timeoutMs?: number;
269
275
  }): TonRpc;
270
- /** An endpoint answered with a refusal (`ok: false`): nothing was taken. A thrown fetch, a timeout or a non-JSON body may have been. */
276
+ /** An endpoint answered with a node refusal (toncenter's `ok: false` envelope): nothing was taken. A thrown fetch, a timeout, a non-JSON body or a gateway error may have been. */
271
277
  declare function isAnsweredRefusal(e: unknown): boolean;
272
278
  /** One RPC, or a failover RPC walking the configured list (`retries` extra rounds after the first full pass). */
273
279
  declare function createTonRpc(config: TonConfig): TonRpc | undefined;
@@ -638,7 +644,7 @@ declare class WalletAccountReadOnlyTon implements IWalletAccountReadOnly {
638
644
  protected toMessages(tx: TonTransaction | TonTransaction[]): V5R1Message[];
639
645
  protected validUntil(): number;
640
646
  /** The cell to sign for `messages` at the CURRENT seqno (fetched fresh every time). */
641
- protected signingCell(messages: V5R1Message[]): Promise<{
647
+ protected signingCell(messages: V5R1Message[], knownSeqno?: number): Promise<{
642
648
  signing: Cell;
643
649
  seqno: number;
644
650
  validUntil: number;
@@ -752,6 +758,14 @@ declare class WalletAccountTon extends WalletAccountReadOnlyTon implements IWall
752
758
  /** What holds the key: `seed`, `key` (one imported key) or `external`. */
753
759
  readonly signerKind: Signer['kind'];
754
760
  private _readOnly;
761
+ /**
762
+ * The last external this account broadcast (accepted, or maybe sent) and when it expires. Until
763
+ * the wallet's seqno moves past it, a new message signed at the chain seqno would carry the same
764
+ * seqno and silently never execute, so the next send waits for it first.
765
+ */
766
+ private _inFlight;
767
+ /** Tail of this account's send queue: seqno → sign → broadcast runs one send at a time. */
768
+ private _sendTail;
755
769
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
756
770
  constructor(signer: Signer, publicKey: Uint8Array, path: string, config?: TonConfig);
757
771
  /** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
package/dist/index.js CHANGED
@@ -10,7 +10,7 @@ import {
10
10
  rawTonAddress,
11
11
  tonExplorerTxUrl,
12
12
  valueFlowTransfers
13
- } from "./chunk-ZFENRKRW.js";
13
+ } from "./chunk-WU4RNRQJ.js";
14
14
 
15
15
  // src/buffer-global.ts
16
16
  import { Buffer } from "buffer";
@@ -95,12 +95,19 @@ function tonCenterEndpoint(endpoint, opts = {}) {
95
95
  }
96
96
  if (!res.ok || json.ok !== true) {
97
97
  const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
98
- throw Object.assign(new NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url }), { answered: true });
98
+ const err = new NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url });
99
+ throw isNodeRefusal(res.status, json) ? Object.assign(err, { answered: true }) : err;
99
100
  }
100
101
  return json.result;
101
102
  }
102
103
  };
103
104
  }
105
+ var UNKNOWN_STATUS = /* @__PURE__ */ new Set([408, 502, 503, 504]);
106
+ function isNodeRefusal(status, json) {
107
+ if (UNKNOWN_STATUS.has(status)) return false;
108
+ if (json.ok !== false || typeof json.error !== "string" || json.error.trim() === "") return false;
109
+ return !/time[ds]? ?out|timed out|deadline/i.test(json.error);
110
+ }
104
111
  function isAnsweredRefusal(e) {
105
112
  return e?.answered === true;
106
113
  }
@@ -950,8 +957,8 @@ var WalletAccountReadOnlyTon = class {
950
957
  return Math.floor(Date.now() / 1e3) + seconds;
951
958
  }
952
959
  /** The cell to sign for `messages` at the CURRENT seqno (fetched fresh every time). */
953
- async signingCell(messages) {
954
- const seqno = await this.getSeqno();
960
+ async signingCell(messages, knownSeqno) {
961
+ const seqno = knownSeqno ?? await this.getSeqno();
955
962
  const validUntil = this.validUntil();
956
963
  return { signing: v5r1SigningMessage({ walletId: this._walletId, validUntil, seqno, messages }), seqno, validUntil };
957
964
  }
@@ -1112,6 +1119,8 @@ function tonSignDataDigest(address, request) {
1112
1119
  }
1113
1120
 
1114
1121
  // src/account.ts
1122
+ var PENDING_SEQNO_TIMEOUT_MS = 6e4;
1123
+ var PENDING_SEQNO_POLL_MS = 1500;
1115
1124
  var toHex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
1116
1125
  var carriesKey = (bytes, key) => {
1117
1126
  outer: for (let i = 0; i + key.length <= bytes.length; i++) {
@@ -1134,6 +1143,14 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1134
1143
  /** What holds the key: `seed`, `key` (one imported key) or `external`. */
1135
1144
  signerKind;
1136
1145
  _readOnly;
1146
+ /**
1147
+ * The last external this account broadcast (accepted, or maybe sent) and when it expires. Until
1148
+ * the wallet's seqno moves past it, a new message signed at the chain seqno would carry the same
1149
+ * seqno and silently never execute, so the next send waits for it first.
1150
+ */
1151
+ _inFlight;
1152
+ /** Tail of this account's send queue: seqno → sign → broadcast runs one send at a time. */
1153
+ _sendTail = Promise.resolve();
1137
1154
  /** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
1138
1155
  constructor(signer, publicKey, path, config = {}) {
1139
1156
  super(publicKey, config);
@@ -1211,14 +1228,33 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1211
1228
  bodyModes(body) {
1212
1229
  return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
1213
1230
  }
1214
- async #signMessages(messages, limit, op) {
1231
+ /**
1232
+ * The seqno to sign the next send at: the chain's, once it has moved past this account's last
1233
+ * broadcast. While that external can still land (before its valid_until) the seqno is polled, up
1234
+ * to `pendingSeqnoTimeoutMs`; an expired one that never landed is forgotten.
1235
+ */
1236
+ async #nextSendSeqno() {
1237
+ const timeout = this._config.pendingSeqnoTimeoutMs ?? PENDING_SEQNO_TIMEOUT_MS;
1238
+ const deadline = Date.now() + timeout;
1239
+ for (; ; ) {
1240
+ const chain = await this.getSeqno();
1241
+ const prev = this._inFlight;
1242
+ if (!prev || chain > prev.seqno || Date.now() / 1e3 > prev.validUntil) {
1243
+ this._inFlight = void 0;
1244
+ return chain;
1245
+ }
1246
+ 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.`);
1247
+ await new Promise((resolve) => setTimeout(resolve, Math.min(PENDING_SEQNO_POLL_MS, Math.max(0, deadline - Date.now()))));
1248
+ }
1249
+ }
1250
+ async #signMessages(messages, limit, op, forSend = false) {
1215
1251
  this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
1216
1252
  const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
1217
1253
  if (want > 0n) {
1218
1254
  const balance = await this.getBalance();
1219
1255
  if (balance < want) throw new UnsafeTransactionError("insufficient-balance", `Insufficient TON balance: the messages carry ${want} nanotons, the wallet holds ${balance} (fees come on top).`);
1220
1256
  }
1221
- const { signing } = await this.signingCell(messages);
1257
+ const { signing } = await this.signingCell(messages, forSend ? await this.#nextSendSeqno() : void 0);
1222
1258
  const fee = await this.preflight(v5r1PackUnsigned(signing), limit, op, want);
1223
1259
  const signature = await this.#signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
1224
1260
  return { body: v5r1PackSigned(signing, signature), fee };
@@ -1255,26 +1291,47 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
1255
1291
  const rpc = this.requireRpc("send transactions");
1256
1292
  const init = await this.initIfNeeded();
1257
1293
  const hash = body.hash().toString("hex");
1294
+ const parsed = parseV5R1Body(body);
1295
+ const sent = () => {
1296
+ if (parsed) this._inFlight = { seqno: parsed.seqno, validUntil: parsed.validUntil };
1297
+ };
1258
1298
  try {
1259
1299
  await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
1260
1300
  } catch (e) {
1261
1301
  if (isAnsweredRefusal(e)) throw e;
1302
+ sent();
1262
1303
  throw new BroadcastUnknownError(hash, e);
1263
1304
  }
1305
+ sent();
1264
1306
  return hash;
1265
1307
  }
1308
+ /**
1309
+ * One send at a time per account: two sends started together would read the same seqno before
1310
+ * either is broadcast. A failed send does not block the next.
1311
+ */
1312
+ #serial(run) {
1313
+ const next = this._sendTail.then(run);
1314
+ this._sendTail = next.then(() => void 0, () => void 0);
1315
+ return next;
1316
+ }
1266
1317
  /** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
1267
- async sendTransaction(tx, options = {}) {
1318
+ sendTransaction(tx, options = {}) {
1319
+ return this.#serial(() => this.#sendTransactionNow(tx, options));
1320
+ }
1321
+ async #sendTransactionNow(tx, options) {
1268
1322
  this.requireRpc("send transactions");
1269
1323
  if (tx instanceof Cell5) this.assertNoDrain(this.bodyModes(tx));
1270
- const { body, fee } = tx instanceof Cell5 ? { body: tx, fee: await this.preflight(tx, feeLimit(this._config.transactionMaxFee, options), "transaction") } : await this.#signMessages(this.toMessages(tx), feeLimit(this._config.transactionMaxFee, options), "transaction");
1324
+ const { body, fee } = tx instanceof Cell5 ? { body: tx, fee: await this.preflight(tx, feeLimit(this._config.transactionMaxFee, options), "transaction") } : await this.#signMessages(this.toMessages(tx), feeLimit(this._config.transactionMaxFee, options), "transaction", true);
1271
1325
  return { hash: await this.broadcast(body), fee };
1272
1326
  }
1273
1327
  /** TON (no `token`) or jetton transfer. */
1274
- async transfer(o, writeOptions = {}) {
1328
+ transfer(o, writeOptions = {}) {
1329
+ return this.#serial(() => this.#transferNow(o, writeOptions));
1330
+ }
1331
+ async #transferNow(o, writeOptions) {
1275
1332
  this.requireRpc("transfer tokens");
1276
1333
  const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
1277
- const { body, fee } = await this.#signMessages([message], feeLimit(this._config.transferMaxFee, writeOptions), "transfer");
1334
+ const { body, fee } = await this.#signMessages([message], feeLimit(this._config.transferMaxFee, writeOptions), "transfer", true);
1278
1335
  return { hash: await this.broadcast(body), fee };
1279
1336
  }
1280
1337
  /** The same wallet as a {@link WalletAccountReadOnlyTon}. */