@anywalletkit/wallet-ton 0.2.5 → 0.3.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/README.md +12 -4
- package/dist/index.cjs +80 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +38 -2
- package/dist/index.d.ts +38 -2
- package/dist/index.js +81 -15
- package/dist/index.js.map +1 -1
- package/package.json +6 -6
package/README.md
CHANGED
|
@@ -56,10 +56,18 @@ const tk = await WalletManagerTon.fromTonMnemonic(words, { tonClient })
|
|
|
56
56
|
an aborted transaction, a failed action phase or a failed **first-hop**
|
|
57
57
|
internal message (the operation would bounce) throws `SimulationError`
|
|
58
58
|
**before signing**; failures deeper in the trace are `warnings`. tonapi's
|
|
59
|
-
`risk` is returned as `risk
|
|
60
|
-
(
|
|
61
|
-
`
|
|
62
|
-
|
|
59
|
+
`risk` is returned as `risk`. A message with send mode 128 (the whole
|
|
60
|
+
remaining balance) or 32 (the account destroyed once empty) is refused with
|
|
61
|
+
`UnsafeTransactionError('drain')` unless `allowDrainingTransactions` — checked
|
|
62
|
+
locally before any emulation, prebuilt bodies too, and again on tonapi's risk.
|
|
63
|
+
`describeTransaction` shows each message's `mode`. `emulate: false` keeps only
|
|
64
|
+
`estimateFee`; `simulate: false` skips the preflight entirely (not the mode check).
|
|
65
|
+
A jetton transfer's recipient and response destination get the same network
|
|
66
|
+
(test-only flag) and workchain checks as a native transfer.
|
|
67
|
+
A broadcast with no definitive answer (no response, a timeout, a non-JSON body)
|
|
68
|
+
throws `BroadcastUnknownError` (`maybeSent`, `hash` = the body hash): do not sign
|
|
69
|
+
again (the next seqno would pay twice); a node's `ok: false` stays a `NetworkError`.
|
|
70
|
+
A custom `TonRpc` should mark refusals `answered: true` (`isAnsweredRefusal`).
|
|
63
71
|
4. noble ed25519 over the body hash; the 32-byte seed is the only key material
|
|
64
72
|
and `dispose()` zeroizes it.
|
|
65
73
|
5. `sendBoc` of the external (with StateInit when the wallet is not deployed).
|
package/dist/index.cjs
CHANGED
|
@@ -20,6 +20,7 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var src_exports = {};
|
|
22
22
|
__export(src_exports, {
|
|
23
|
+
BroadcastUnknownError: () => BroadcastUnknownError,
|
|
23
24
|
CAIP2: () => CAIP2,
|
|
24
25
|
DEFAULT_SEND_MODE: () => DEFAULT_SEND_MODE,
|
|
25
26
|
DEFAULT_TONAPI: () => DEFAULT_TONAPI,
|
|
@@ -53,6 +54,7 @@ __export(src_exports, {
|
|
|
53
54
|
emulateViaTonapi: () => emulateViaTonapi,
|
|
54
55
|
encodeHistoryCursor: () => encodeCursor,
|
|
55
56
|
installBufferGlobal: () => installBufferGlobal,
|
|
57
|
+
isAnsweredRefusal: () => isAnsweredRefusal,
|
|
56
58
|
mapTonapiActions: () => mapActions,
|
|
57
59
|
mapTonapiEvent: () => mapTonapiEvent,
|
|
58
60
|
mapToncenterTransaction: () => mapToncenterTransaction,
|
|
@@ -118,6 +120,17 @@ var SimulationError = class extends Error {
|
|
|
118
120
|
this.aborted = details.aborted ?? false;
|
|
119
121
|
}
|
|
120
122
|
};
|
|
123
|
+
var BroadcastUnknownError = class extends Error {
|
|
124
|
+
/** Always true: the transaction may be on its way. */
|
|
125
|
+
maybeSent = true;
|
|
126
|
+
/** Hex hash of the signed body. */
|
|
127
|
+
hash;
|
|
128
|
+
constructor(hash, cause) {
|
|
129
|
+
super(`Broadcast outcome unknown for ${hash}: ${cause instanceof Error ? cause.message.slice(0, 200) : String(cause).slice(0, 200)}`, { cause });
|
|
130
|
+
this.name = "BroadcastUnknownError";
|
|
131
|
+
this.hash = hash;
|
|
132
|
+
}
|
|
133
|
+
};
|
|
121
134
|
|
|
122
135
|
// src/toncenter.ts
|
|
123
136
|
var import_core2 = require("@anywalletkit/core");
|
|
@@ -155,12 +168,15 @@ function tonCenterEndpoint(endpoint, opts = {}) {
|
|
|
155
168
|
}
|
|
156
169
|
if (!res.ok || json.ok !== true) {
|
|
157
170
|
const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
|
|
158
|
-
throw new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url });
|
|
171
|
+
throw Object.assign(new import_core2.NetworkError(`${method}: ${detail} (HTTP ${res.status})`, { provider: url }), { answered: true });
|
|
159
172
|
}
|
|
160
173
|
return json.result;
|
|
161
174
|
}
|
|
162
175
|
};
|
|
163
176
|
}
|
|
177
|
+
function isAnsweredRefusal(e) {
|
|
178
|
+
return e?.answered === true;
|
|
179
|
+
}
|
|
164
180
|
function createTonRpc(config) {
|
|
165
181
|
const target = config.tonClient ?? config.provider;
|
|
166
182
|
if (target === void 0) return void 0;
|
|
@@ -173,14 +189,17 @@ function createTonRpc(config) {
|
|
|
173
189
|
return {
|
|
174
190
|
async call(method, params) {
|
|
175
191
|
let lastError;
|
|
192
|
+
let answered = true;
|
|
176
193
|
for (let i = 0; i < attempts; i++) {
|
|
177
194
|
try {
|
|
178
195
|
return await list[i % list.length].call(method, params);
|
|
179
196
|
} catch (e) {
|
|
180
197
|
lastError = e;
|
|
198
|
+
answered &&= isAnsweredRefusal(e);
|
|
181
199
|
}
|
|
182
200
|
}
|
|
183
|
-
|
|
201
|
+
const err = new import_core2.NetworkError(`All TON endpoints failed for ${method}`, { cause: lastError });
|
|
202
|
+
throw answered ? Object.assign(err, { answered: true }) : err;
|
|
184
203
|
}
|
|
185
204
|
};
|
|
186
205
|
}
|
|
@@ -707,6 +726,19 @@ function describeOne(i, to, value, body, hasStateInit, bounceableOverride, warni
|
|
|
707
726
|
return item;
|
|
708
727
|
}
|
|
709
728
|
var hex = (n) => `0x${n.toString(16).padStart(8, "0")}`;
|
|
729
|
+
var IGNORE_ERRORS = 2;
|
|
730
|
+
function sendModeOf(sendMode) {
|
|
731
|
+
return (sendMode ?? DEFAULT_SEND_MODE) | IGNORE_ERRORS;
|
|
732
|
+
}
|
|
733
|
+
function drainModes(mode) {
|
|
734
|
+
return [...(mode & 128) !== 0 ? ["carries-all-balance"] : [], ...(mode & 32) !== 0 ? ["destroys-account"] : []];
|
|
735
|
+
}
|
|
736
|
+
function withMode(i, item, mode, warnings) {
|
|
737
|
+
for (const code of drainModes(mode)) {
|
|
738
|
+
warnings.push({ code, message: code === "carries-all-balance" ? "send mode 128: the message carries the whole remaining balance" : "send mode 32: the wallet account is destroyed once it is empty", index: i });
|
|
739
|
+
}
|
|
740
|
+
return { ...item, mode };
|
|
741
|
+
}
|
|
710
742
|
function describeMessageRelaxed(i, m, warnings, ctx = {}) {
|
|
711
743
|
if (m.info.type !== "internal") {
|
|
712
744
|
warnings.push({ code: "unknown-body", message: "not an internal message", index: i });
|
|
@@ -728,7 +760,7 @@ function describeTransaction(tx, ctx = {}) {
|
|
|
728
760
|
return { kind: "contract-call", items, warnings };
|
|
729
761
|
}
|
|
730
762
|
external2 = { seqno: parsed.seqno, validUntil: parsed.validUntil, walletId: parsed.walletId, signed: parsed.signed };
|
|
731
|
-
parsed.messages.forEach((m, i) => items.push(describeMessageRelaxed(i, m.message, warnings, ctx)));
|
|
763
|
+
parsed.messages.forEach((m, i) => items.push(withMode(i, describeMessageRelaxed(i, m.message, warnings, ctx), m.mode, warnings)));
|
|
732
764
|
} else {
|
|
733
765
|
const list = Array.isArray(tx) ? tx : [tx];
|
|
734
766
|
list.forEach((t, i) => {
|
|
@@ -746,7 +778,7 @@ function describeTransaction(tx, ctx = {}) {
|
|
|
746
778
|
} catch {
|
|
747
779
|
warnings.push({ code: "unknown-body", message: "bodyBoc is not a valid BoC", index: i });
|
|
748
780
|
}
|
|
749
|
-
items.push(describeOne(i, to, BigInt(t.value), body, t.stateInit !== void 0, t.bounceable, warnings, ctx));
|
|
781
|
+
items.push(withMode(i, describeOne(i, to, BigInt(t.value), body, t.stateInit !== void 0, t.bounceable, warnings, ctx), sendModeOf(t.sendMode), warnings));
|
|
750
782
|
});
|
|
751
783
|
}
|
|
752
784
|
const kinds = new Set(items.map((x) => x.kind));
|
|
@@ -852,25 +884,34 @@ var WalletAccountReadOnlyTon = class {
|
|
|
852
884
|
if (src instanceof import_core8.Cell) return (0, import_core8.loadStateInit)(src.beginParse());
|
|
853
885
|
return src;
|
|
854
886
|
}
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
887
|
+
/**
|
|
888
|
+
* A destination address with the checks every payment gets: workchain 0 or -1, and a friendly
|
|
889
|
+
* address's test-only flag matching the wallet's network. Used for a native message's `to` and a
|
|
890
|
+
* jetton transfer's recipient and response destination (round 3, L8).
|
|
891
|
+
*/
|
|
892
|
+
checkedAddress(to) {
|
|
858
893
|
let address;
|
|
859
894
|
let bounceable = true;
|
|
860
895
|
let testOnly;
|
|
861
|
-
if (import_core8.Address.isFriendly(
|
|
862
|
-
const f = import_core8.Address.parseFriendly(
|
|
896
|
+
if (import_core8.Address.isFriendly(to)) {
|
|
897
|
+
const f = import_core8.Address.parseFriendly(to);
|
|
863
898
|
address = f.address;
|
|
864
899
|
bounceable = f.isBounceable;
|
|
865
900
|
testOnly = f.isTestOnly;
|
|
866
901
|
} else {
|
|
867
|
-
address = import_core8.Address.parse(
|
|
902
|
+
address = import_core8.Address.parse(to);
|
|
868
903
|
}
|
|
869
904
|
if (address.workChain !== 0 && address.workChain !== -1) throw new UnsafeTransactionError("exotic-workchain", `Refusing to send to workchain ${address.workChain}.`);
|
|
870
905
|
const testnet = this._config.network === "testnet";
|
|
871
906
|
if (testOnly !== void 0 && testOnly !== testnet) {
|
|
872
907
|
throw new UnsafeTransactionError("network-mismatch", testOnly ? "Test-only address on a mainnet wallet." : "Mainnet address on a testnet wallet.");
|
|
873
908
|
}
|
|
909
|
+
return { address, bounceable };
|
|
910
|
+
}
|
|
911
|
+
transactionMessage(tx) {
|
|
912
|
+
const value = big(tx.value);
|
|
913
|
+
if (value <= 0n) throw new UnsafeTransactionError("zero-value", "TON message value must be positive.");
|
|
914
|
+
const { address, bounceable } = this.checkedAddress(tx.to);
|
|
874
915
|
const body = tx.body instanceof import_core8.Cell ? tx.body : typeof tx.body === "string" ? tx.body === "" ? void 0 : commentCell(tx.body) : tx.bodyBoc ? import_core8.Cell.fromBase64(tx.bodyBoc) : void 0;
|
|
875
916
|
const init = this.stateInitOf(tx.stateInit);
|
|
876
917
|
const message = (0, import_core8.internal)({ to: address, value, bounce: tx.bounceable ?? bounceable, ...body ? { body } : {}, ...init ? { init } : {} });
|
|
@@ -879,14 +920,15 @@ var WalletAccountReadOnlyTon = class {
|
|
|
879
920
|
async jettonTransferMessage(o) {
|
|
880
921
|
const amount = big(o.amount);
|
|
881
922
|
if (amount <= 0n) throw new UnsafeTransactionError("zero-value", "Jetton amount must be positive.");
|
|
882
|
-
const recipient =
|
|
923
|
+
const recipient = this.checkedAddress(o.recipient).address;
|
|
924
|
+
const response = o.responseDestination ? this.checkedAddress(o.responseDestination).address : void 0;
|
|
883
925
|
const jw = await this.getJettonWalletAddress(o.token);
|
|
884
926
|
await this.jettonWalletData(o.token, jw);
|
|
885
927
|
const forwardTonAmount = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
|
|
886
928
|
if (o.memo !== void 0 && o.forwardPayload !== void 0) throw new UnsafeTransactionError("memo-and-payload", "Pass either memo or forwardPayload, not both: a jetton transfer carries one forward payload.");
|
|
887
929
|
const payload = o.forwardPayload ?? o.memo;
|
|
888
930
|
const forwardPayload = payload === void 0 ? null : typeof payload === "string" ? commentCell(payload) : payload;
|
|
889
|
-
const body = (0, import_core8.beginCell)().storeUint(JETTON_TRANSFER_OP2, 32).storeUint(o.queryId !== void 0 ? BigInt(o.queryId) : randomQueryId(), 64).storeCoins(amount).storeAddress(recipient).storeAddress(
|
|
931
|
+
const body = (0, import_core8.beginCell)().storeUint(JETTON_TRANSFER_OP2, 32).storeUint(o.queryId !== void 0 ? BigInt(o.queryId) : randomQueryId(), 64).storeCoins(amount).storeAddress(recipient).storeAddress(response ?? this._address).storeMaybeRef(o.customPayload ?? null).storeCoins(forwardTonAmount).storeMaybeRef(forwardPayload).endCell();
|
|
890
932
|
const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTonAmount;
|
|
891
933
|
return { message: (0, import_core8.internal)({ to: jw, value: attached, bounce: true, body }) };
|
|
892
934
|
}
|
|
@@ -1143,7 +1185,22 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1143
1185
|
* Checks → seqno → preflight → signed body. The preflight runs on the
|
|
1144
1186
|
* zero-signed body (ignore_chksig), so nothing is signed before it passes.
|
|
1145
1187
|
*/
|
|
1188
|
+
/**
|
|
1189
|
+
* Refuse send modes that drain the wallet — 128 (the whole remaining balance) and 32 (the account
|
|
1190
|
+
* destroyed once empty) — unless `allowDrainingTransactions`. Checked locally, before any seqno or
|
|
1191
|
+
* emulation, so it holds with `simulate: false` too (round 3, M11); the emulation check stays.
|
|
1192
|
+
*/
|
|
1193
|
+
assertNoDrain(modes) {
|
|
1194
|
+
if (this._config.allowDrainingTransactions === true) return;
|
|
1195
|
+
const found = modes.flatMap(drainModes);
|
|
1196
|
+
if (found.length) throw new UnsafeTransactionError("drain", `Refusing a message with send mode ${found.includes("carries-all-balance") ? "128 (it carries the whole remaining balance)" : "32 (it destroys the wallet account once empty)"}; set allowDrainingTransactions to override.`);
|
|
1197
|
+
}
|
|
1198
|
+
/** The send modes of a prebuilt V5R1 body; an unreadable body is left to the emulation and the node. */
|
|
1199
|
+
bodyModes(body) {
|
|
1200
|
+
return parseV5R1Body(body)?.messages.map((m) => m.mode) ?? [];
|
|
1201
|
+
}
|
|
1146
1202
|
async signMessages(messages, limit, op) {
|
|
1203
|
+
this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
|
|
1147
1204
|
const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
|
|
1148
1205
|
if (want > 0n) {
|
|
1149
1206
|
const balance = await this.getBalance();
|
|
@@ -1175,6 +1232,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1175
1232
|
/** Sign without broadcasting. Returns the external-message BODY as base64 BoC (pass it back to sendTransaction as a Cell). */
|
|
1176
1233
|
async signTransaction(tx, options = {}) {
|
|
1177
1234
|
if (tx instanceof import_core10.Cell) {
|
|
1235
|
+
this.assertNoDrain(this.bodyModes(tx));
|
|
1178
1236
|
await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1179
1237
|
return tx.toBoc().toString("base64");
|
|
1180
1238
|
}
|
|
@@ -1184,12 +1242,19 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1184
1242
|
async broadcast(body) {
|
|
1185
1243
|
const rpc = this.requireRpc("send transactions");
|
|
1186
1244
|
const init = await this.initIfNeeded();
|
|
1187
|
-
|
|
1188
|
-
|
|
1245
|
+
const hash = body.hash().toString("hex");
|
|
1246
|
+
try {
|
|
1247
|
+
await rpc.sendBoc(v5r1ExternalMessage(this._address, body, init));
|
|
1248
|
+
} catch (e) {
|
|
1249
|
+
if (isAnsweredRefusal(e)) throw e;
|
|
1250
|
+
throw new BroadcastUnknownError(hash, e);
|
|
1251
|
+
}
|
|
1252
|
+
return hash;
|
|
1189
1253
|
}
|
|
1190
1254
|
/** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
|
|
1191
1255
|
async sendTransaction(tx, options = {}) {
|
|
1192
1256
|
this.requireRpc("send transactions");
|
|
1257
|
+
if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
|
|
1193
1258
|
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");
|
|
1194
1259
|
return { hash: await this.broadcast(body), fee };
|
|
1195
1260
|
}
|
|
@@ -1228,6 +1293,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
|
|
|
1228
1293
|
static async fromTonMnemonic(words, config = {}) {
|
|
1229
1294
|
const list = (Array.isArray(words) ? words : words.trim().split(/\s+/)).map((w) => w.toLowerCase());
|
|
1230
1295
|
if (list.length !== 24) throw new Error("A TON mnemonic has 24 words.");
|
|
1296
|
+
if (!await (0, import_crypto.mnemonicValidate)(list)) throw new Error("Not a valid TON mnemonic: check the words (a typo would open a different, empty wallet).");
|
|
1231
1297
|
const pair = await (0, import_crypto.mnemonicToPrivateKey)(list);
|
|
1232
1298
|
const privateKey = new Uint8Array(pair.secretKey.subarray(0, 32));
|
|
1233
1299
|
const publicKey = new Uint8Array(pair.publicKey);
|