@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/{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 +81 -17
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +18 -4
- package/dist/index.d.ts +18 -4
- package/dist/index.js +82 -18
- 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;
|
|
@@ -749,9 +755,17 @@ declare class WalletAccountTon extends WalletAccountReadOnlyTon implements IWall
|
|
|
749
755
|
readonly needsNetwork = true;
|
|
750
756
|
/** Derivation path, `ton-mnemonic`, or `external`. */
|
|
751
757
|
readonly path: string;
|
|
752
|
-
/**
|
|
753
|
-
readonly
|
|
758
|
+
/** What holds the key: `seed`, `key` (one imported key) or `external`. */
|
|
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;
|
|
@@ -749,9 +755,17 @@ declare class WalletAccountTon extends WalletAccountReadOnlyTon implements IWall
|
|
|
749
755
|
readonly needsNetwork = true;
|
|
750
756
|
/** Derivation path, `ton-mnemonic`, or `external`. */
|
|
751
757
|
readonly path: string;
|
|
752
|
-
/**
|
|
753
|
-
readonly
|
|
758
|
+
/** What holds the key: `seed`, `key` (one imported key) or `external`. */
|
|
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++) {
|
|
@@ -1125,13 +1134,28 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1125
1134
|
needsNetwork = true;
|
|
1126
1135
|
/** Derivation path, `ton-mnemonic`, or `external`. */
|
|
1127
1136
|
path;
|
|
1128
|
-
/**
|
|
1129
|
-
|
|
1137
|
+
/**
|
|
1138
|
+
* The signer that holds the key, in a private field: not a property of the account, so no code
|
|
1139
|
+
* the account is handed to (a protocol module, a logger) can reach it and sign around the
|
|
1140
|
+
* account's checks (audit 2026-10-29 A1).
|
|
1141
|
+
*/
|
|
1142
|
+
#signer;
|
|
1143
|
+
/** What holds the key: `seed`, `key` (one imported key) or `external`. */
|
|
1144
|
+
signerKind;
|
|
1130
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();
|
|
1131
1154
|
/** `publicKey` is the signer's 32-byte ed25519 key; the V5R1 address derives from it synchronously. */
|
|
1132
1155
|
constructor(signer, publicKey, path, config = {}) {
|
|
1133
1156
|
super(publicKey, config);
|
|
1134
|
-
this
|
|
1157
|
+
this.#signer = requireCurve(signer, "ed25519");
|
|
1158
|
+
this.signerKind = this.#signer.kind;
|
|
1135
1159
|
this.path = path;
|
|
1136
1160
|
}
|
|
1137
1161
|
/** Back-compat: wrap a raw derived key in a {@link LocalSigner} (the account takes ownership; it cannot derive). */
|
|
@@ -1154,7 +1178,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1154
1178
|
async sign(message) {
|
|
1155
1179
|
const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1156
1180
|
if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
|
|
1157
|
-
return toHex(await this
|
|
1181
|
+
return toHex(await this.#signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
|
|
1158
1182
|
}
|
|
1159
1183
|
/**
|
|
1160
1184
|
* TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
|
|
@@ -1163,7 +1187,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1163
1187
|
*/
|
|
1164
1188
|
async signTonProof(request) {
|
|
1165
1189
|
const digest = tonProofDigest(this._address, request);
|
|
1166
|
-
const signature = await this
|
|
1190
|
+
const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
|
|
1167
1191
|
return {
|
|
1168
1192
|
timestamp: request.timestamp,
|
|
1169
1193
|
domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
|
|
@@ -1174,7 +1198,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1174
1198
|
/** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
|
|
1175
1199
|
async signTonConnectData(request) {
|
|
1176
1200
|
const digest = tonSignDataDigest(this._address, request);
|
|
1177
|
-
const signature = await this
|
|
1201
|
+
const signature = await this.#signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
|
|
1178
1202
|
return {
|
|
1179
1203
|
signature: Buffer6.from(signature).toString("base64"),
|
|
1180
1204
|
address: this._address.toRawString(),
|
|
@@ -1204,16 +1228,35 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1204
1228
|
bodyModes(body) {
|
|
1205
1229
|
return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
|
|
1206
1230
|
}
|
|
1207
|
-
|
|
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) {
|
|
1208
1251
|
this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
|
|
1209
1252
|
const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
|
|
1210
1253
|
if (want > 0n) {
|
|
1211
1254
|
const balance = await this.getBalance();
|
|
1212
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).`);
|
|
1213
1256
|
}
|
|
1214
|
-
const { signing } = await this.signingCell(messages);
|
|
1257
|
+
const { signing } = await this.signingCell(messages, forSend ? await this.#nextSendSeqno() : void 0);
|
|
1215
1258
|
const fee = await this.preflight(v5r1PackUnsigned(signing), limit, op, want);
|
|
1216
|
-
const signature = await this
|
|
1259
|
+
const signature = await this.#signer.sign(new Uint8Array(signing.hash()), { scheme: "ed25519", context: "ton-external" });
|
|
1217
1260
|
return { body: v5r1PackSigned(signing, signature), fee };
|
|
1218
1261
|
}
|
|
1219
1262
|
async preflight(body, limit, op, expectedTonOut) {
|
|
@@ -1248,26 +1291,47 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1248
1291
|
const rpc = this.requireRpc("send transactions");
|
|
1249
1292
|
const init = await this.initIfNeeded();
|
|
1250
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
|
+
};
|
|
1251
1298
|
try {
|
|
1252
1299
|
await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
|
|
1253
1300
|
} catch (e) {
|
|
1254
1301
|
if (isAnsweredRefusal(e)) throw e;
|
|
1302
|
+
sent();
|
|
1255
1303
|
throw new BroadcastUnknownError(hash, e);
|
|
1256
1304
|
}
|
|
1305
|
+
sent();
|
|
1257
1306
|
return hash;
|
|
1258
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
|
+
}
|
|
1259
1317
|
/** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
|
|
1260
|
-
|
|
1318
|
+
sendTransaction(tx, options = {}) {
|
|
1319
|
+
return this.#serial(() => this.#sendTransactionNow(tx, options));
|
|
1320
|
+
}
|
|
1321
|
+
async #sendTransactionNow(tx, options) {
|
|
1261
1322
|
this.requireRpc("send transactions");
|
|
1262
1323
|
if (tx instanceof Cell5) this.assertNoDrain(this.bodyModes(tx));
|
|
1263
|
-
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);
|
|
1264
1325
|
return { hash: await this.broadcast(body), fee };
|
|
1265
1326
|
}
|
|
1266
1327
|
/** TON (no `token`) or jetton transfer. */
|
|
1267
|
-
|
|
1328
|
+
transfer(o, writeOptions = {}) {
|
|
1329
|
+
return this.#serial(() => this.#transferNow(o, writeOptions));
|
|
1330
|
+
}
|
|
1331
|
+
async #transferNow(o, writeOptions) {
|
|
1268
1332
|
this.requireRpc("transfer tokens");
|
|
1269
1333
|
const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
|
|
1270
|
-
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);
|
|
1271
1335
|
return { hash: await this.broadcast(body), fee };
|
|
1272
1336
|
}
|
|
1273
1337
|
/** The same wallet as a {@link WalletAccountReadOnlyTon}. */
|
|
@@ -1277,7 +1341,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1277
1341
|
}
|
|
1278
1342
|
/** Disposes the signer: a local key is zeroized, an external one is released. */
|
|
1279
1343
|
dispose() {
|
|
1280
|
-
this
|
|
1344
|
+
this.#signer.dispose();
|
|
1281
1345
|
}
|
|
1282
1346
|
};
|
|
1283
1347
|
|