@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/{chunk-ZFENRKRW.js → chunk-WU4RNRQJ.js} +1 -1
- package/dist/{chunk-ZFENRKRW.js.map → chunk-WU4RNRQJ.js.map} +1 -1
- package/dist/history.cjs.map +1 -1
- package/dist/history.js +1 -1
- package/dist/index.cjs +66 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +16 -2
- package/dist/index.d.ts +16 -2
- package/dist/index.js +67 -10
- package/dist/index.js.map +1 -1
- package/package.json +5 -5
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
|
|
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
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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}. */
|