@anywalletkit/wallet-ton 0.2.4 → 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 +29 -5
- package/dist/index.cjs +201 -23
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +121 -4
- package/dist/index.d.ts +121 -4
- package/dist/index.js +198 -20
- package/dist/index.js.map +1 -1
- package/package.json +7 -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).
|
|
@@ -68,7 +76,8 @@ const tk = await WalletManagerTon.fromTonMnemonic(words, { tonClient })
|
|
|
68
76
|
|
|
69
77
|
The account never holds a key. Every signature is a `signer.sign(bytes, {scheme: 'ed25519', context})`
|
|
70
78
|
call: `ton-external` over the hash of the V5R1 signing cell (after the preflight, which runs on a
|
|
71
|
-
zero-signed body), `ton-message` over raw message bytes (`sign()
|
|
79
|
+
zero-signed body), `ton-message` over raw message bytes (`sign()`), `ton-proof` / `ton-sign-data`
|
|
80
|
+
over the TON Connect digests (`signTonProof`, `signTonConnectData`). Seed wallets get a
|
|
72
81
|
`LocalSigner`; a hardware wallet, MPC node or backend is wrapped with `externalSigner()` from
|
|
73
82
|
`@anywalletkit/core`:
|
|
74
83
|
|
|
@@ -82,6 +91,21 @@ A derivable `SeedSigner` at `m/44'/607'` yields `getAccount(i)` children; a leaf
|
|
|
82
91
|
`fromTonMnemonic`) is one account. `WalletAccountTon.fromPrivateKey(priv, pub, path)` keeps the
|
|
83
92
|
raw-key shape for callers that derive themselves.
|
|
84
93
|
|
|
94
|
+
## Messages and TON Connect
|
|
95
|
+
|
|
96
|
+
`sign(message)` is a hex ed25519 signature over the raw bytes. A 32-byte message — bytes or a
|
|
97
|
+
string that encodes to 32 bytes — is refused with `MessageIsTransactionError`: the wallet contract
|
|
98
|
+
checks a transfer's signature over the 32-byte hash of its signing cell, so 32 raw bytes may be a
|
|
99
|
+
transfer. TON Connect has its own methods, which build the digest from the request and the
|
|
100
|
+
account's own address (0xffff ‖ "ton-connect…", never a cell hash):
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
const proof = await account.signTonProof({ domain: 'app.example', timestamp, payload }) // the `proof` of a ton_proof reply
|
|
104
|
+
const reply = await account.signTonConnectData({ domain: 'app.example', timestamp, payload: { type: 'text', text } }) // signData (text | binary)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`domain` is the host the wallet saw the request come from, never the dApp's own claim.
|
|
108
|
+
|
|
85
109
|
## Describe
|
|
86
110
|
|
|
87
111
|
`describeTransaction(tx)` / `account.describe(tx)` decode TON transfers (text
|
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,
|
|
@@ -27,6 +28,7 @@ __export(src_exports, {
|
|
|
27
28
|
JETTON_TRANSFER_OP: () => JETTON_TRANSFER_OP,
|
|
28
29
|
MAINNET_GLOBAL_ID: () => MAINNET_GLOBAL_ID,
|
|
29
30
|
MAX_ACTIONS: () => MAX_ACTIONS,
|
|
31
|
+
MessageIsTransactionError: () => MessageIsTransactionError,
|
|
30
32
|
SEND_MODE: () => SEND_MODE,
|
|
31
33
|
SimulationError: () => SimulationError,
|
|
32
34
|
TESTNET_GLOBAL_ID: () => TESTNET_GLOBAL_ID,
|
|
@@ -52,6 +54,7 @@ __export(src_exports, {
|
|
|
52
54
|
emulateViaTonapi: () => emulateViaTonapi,
|
|
53
55
|
encodeHistoryCursor: () => encodeCursor,
|
|
54
56
|
installBufferGlobal: () => installBufferGlobal,
|
|
57
|
+
isAnsweredRefusal: () => isAnsweredRefusal,
|
|
55
58
|
mapTonapiActions: () => mapActions,
|
|
56
59
|
mapTonapiEvent: () => mapTonapiEvent,
|
|
57
60
|
mapToncenterTransaction: () => mapToncenterTransaction,
|
|
@@ -65,6 +68,8 @@ __export(src_exports, {
|
|
|
65
68
|
runEmulation: () => runEmulation,
|
|
66
69
|
tonCenterEndpoint: () => tonCenterEndpoint,
|
|
67
70
|
tonExplorerTxUrl: () => tonExplorerTxUrl,
|
|
71
|
+
tonProofDigest: () => tonProofDigest,
|
|
72
|
+
tonSignDataDigest: () => tonSignDataDigest,
|
|
68
73
|
v5r1Address: () => v5r1Address,
|
|
69
74
|
v5r1Code: () => v5r1Code,
|
|
70
75
|
v5r1ExternalMessage: () => v5r1ExternalMessage,
|
|
@@ -98,6 +103,13 @@ var UnsafeTransactionError = class extends import_core.SignerError {
|
|
|
98
103
|
this.code = code;
|
|
99
104
|
}
|
|
100
105
|
};
|
|
106
|
+
var MessageIsTransactionError = class extends import_core.SignerError {
|
|
107
|
+
code = "message-is-transaction";
|
|
108
|
+
constructor() {
|
|
109
|
+
super("This message has the shape of a transaction (a 32-byte TON hash, or bytes naming this key as a Solana signer) and is not signed; use signTonProof / signTonConnectData for TON Connect, or signTransaction so it can be reviewed.");
|
|
110
|
+
this.name = "MessageIsTransactionError";
|
|
111
|
+
}
|
|
112
|
+
};
|
|
101
113
|
var SimulationError = class extends Error {
|
|
102
114
|
exitCode;
|
|
103
115
|
aborted;
|
|
@@ -108,6 +120,17 @@ var SimulationError = class extends Error {
|
|
|
108
120
|
this.aborted = details.aborted ?? false;
|
|
109
121
|
}
|
|
110
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
|
+
};
|
|
111
134
|
|
|
112
135
|
// src/toncenter.ts
|
|
113
136
|
var import_core2 = require("@anywalletkit/core");
|
|
@@ -145,12 +168,15 @@ function tonCenterEndpoint(endpoint, opts = {}) {
|
|
|
145
168
|
}
|
|
146
169
|
if (!res.ok || json.ok !== true) {
|
|
147
170
|
const detail = typeof json.error === "string" ? json.error : JSON.stringify(json.error ?? json);
|
|
148
|
-
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 });
|
|
149
172
|
}
|
|
150
173
|
return json.result;
|
|
151
174
|
}
|
|
152
175
|
};
|
|
153
176
|
}
|
|
177
|
+
function isAnsweredRefusal(e) {
|
|
178
|
+
return e?.answered === true;
|
|
179
|
+
}
|
|
154
180
|
function createTonRpc(config) {
|
|
155
181
|
const target = config.tonClient ?? config.provider;
|
|
156
182
|
if (target === void 0) return void 0;
|
|
@@ -163,14 +189,17 @@ function createTonRpc(config) {
|
|
|
163
189
|
return {
|
|
164
190
|
async call(method, params) {
|
|
165
191
|
let lastError;
|
|
192
|
+
let answered = true;
|
|
166
193
|
for (let i = 0; i < attempts; i++) {
|
|
167
194
|
try {
|
|
168
195
|
return await list[i % list.length].call(method, params);
|
|
169
196
|
} catch (e) {
|
|
170
197
|
lastError = e;
|
|
198
|
+
answered &&= isAnsweredRefusal(e);
|
|
171
199
|
}
|
|
172
200
|
}
|
|
173
|
-
|
|
201
|
+
const err = new import_core2.NetworkError(`All TON endpoints failed for ${method}`, { cause: lastError });
|
|
202
|
+
throw answered ? Object.assign(err, { answered: true }) : err;
|
|
174
203
|
}
|
|
175
204
|
};
|
|
176
205
|
}
|
|
@@ -697,6 +726,19 @@ function describeOne(i, to, value, body, hasStateInit, bounceableOverride, warni
|
|
|
697
726
|
return item;
|
|
698
727
|
}
|
|
699
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
|
+
}
|
|
700
742
|
function describeMessageRelaxed(i, m, warnings, ctx = {}) {
|
|
701
743
|
if (m.info.type !== "internal") {
|
|
702
744
|
warnings.push({ code: "unknown-body", message: "not an internal message", index: i });
|
|
@@ -718,7 +760,7 @@ function describeTransaction(tx, ctx = {}) {
|
|
|
718
760
|
return { kind: "contract-call", items, warnings };
|
|
719
761
|
}
|
|
720
762
|
external2 = { seqno: parsed.seqno, validUntil: parsed.validUntil, walletId: parsed.walletId, signed: parsed.signed };
|
|
721
|
-
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)));
|
|
722
764
|
} else {
|
|
723
765
|
const list = Array.isArray(tx) ? tx : [tx];
|
|
724
766
|
list.forEach((t, i) => {
|
|
@@ -736,7 +778,7 @@ function describeTransaction(tx, ctx = {}) {
|
|
|
736
778
|
} catch {
|
|
737
779
|
warnings.push({ code: "unknown-body", message: "bodyBoc is not a valid BoC", index: i });
|
|
738
780
|
}
|
|
739
|
-
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));
|
|
740
782
|
});
|
|
741
783
|
}
|
|
742
784
|
const kinds = new Set(items.map((x) => x.kind));
|
|
@@ -842,25 +884,34 @@ var WalletAccountReadOnlyTon = class {
|
|
|
842
884
|
if (src instanceof import_core8.Cell) return (0, import_core8.loadStateInit)(src.beginParse());
|
|
843
885
|
return src;
|
|
844
886
|
}
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
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) {
|
|
848
893
|
let address;
|
|
849
894
|
let bounceable = true;
|
|
850
895
|
let testOnly;
|
|
851
|
-
if (import_core8.Address.isFriendly(
|
|
852
|
-
const f = import_core8.Address.parseFriendly(
|
|
896
|
+
if (import_core8.Address.isFriendly(to)) {
|
|
897
|
+
const f = import_core8.Address.parseFriendly(to);
|
|
853
898
|
address = f.address;
|
|
854
899
|
bounceable = f.isBounceable;
|
|
855
900
|
testOnly = f.isTestOnly;
|
|
856
901
|
} else {
|
|
857
|
-
address = import_core8.Address.parse(
|
|
902
|
+
address = import_core8.Address.parse(to);
|
|
858
903
|
}
|
|
859
904
|
if (address.workChain !== 0 && address.workChain !== -1) throw new UnsafeTransactionError("exotic-workchain", `Refusing to send to workchain ${address.workChain}.`);
|
|
860
905
|
const testnet = this._config.network === "testnet";
|
|
861
906
|
if (testOnly !== void 0 && testOnly !== testnet) {
|
|
862
907
|
throw new UnsafeTransactionError("network-mismatch", testOnly ? "Test-only address on a mainnet wallet." : "Mainnet address on a testnet wallet.");
|
|
863
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);
|
|
864
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;
|
|
865
916
|
const init = this.stateInitOf(tx.stateInit);
|
|
866
917
|
const message = (0, import_core8.internal)({ to: address, value, bounce: tx.bounceable ?? bounceable, ...body ? { body } : {}, ...init ? { init } : {} });
|
|
@@ -869,14 +920,15 @@ var WalletAccountReadOnlyTon = class {
|
|
|
869
920
|
async jettonTransferMessage(o) {
|
|
870
921
|
const amount = big(o.amount);
|
|
871
922
|
if (amount <= 0n) throw new UnsafeTransactionError("zero-value", "Jetton amount must be positive.");
|
|
872
|
-
const recipient =
|
|
923
|
+
const recipient = this.checkedAddress(o.recipient).address;
|
|
924
|
+
const response = o.responseDestination ? this.checkedAddress(o.responseDestination).address : void 0;
|
|
873
925
|
const jw = await this.getJettonWalletAddress(o.token);
|
|
874
926
|
await this.jettonWalletData(o.token, jw);
|
|
875
927
|
const forwardTonAmount = o.forwardTonAmount !== void 0 ? big(o.forwardTonAmount) : 1n;
|
|
876
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.");
|
|
877
929
|
const payload = o.forwardPayload ?? o.memo;
|
|
878
930
|
const forwardPayload = payload === void 0 ? null : typeof payload === "string" ? commentCell(payload) : payload;
|
|
879
|
-
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();
|
|
880
932
|
const attached = o.attachedValue !== void 0 ? big(o.attachedValue) : (this._config.jettonAttachedValue !== void 0 ? big(this._config.jettonAttachedValue) : DEFAULT_JETTON_ATTACHED) + forwardTonAmount;
|
|
881
933
|
return { message: (0, import_core8.internal)({ to: jw, value: attached, bounce: true, body }) };
|
|
882
934
|
}
|
|
@@ -994,8 +1046,75 @@ function randomQueryId() {
|
|
|
994
1046
|
|
|
995
1047
|
// src/account.ts
|
|
996
1048
|
var import_core9 = require("@anywalletkit/core");
|
|
1049
|
+
var import_buffer5 = require("buffer");
|
|
997
1050
|
var import_core10 = require("@ton/core");
|
|
1051
|
+
|
|
1052
|
+
// src/tonconnect.ts
|
|
1053
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
1054
|
+
var import_buffer4 = require("buffer");
|
|
1055
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
1056
|
+
var int32BE = (n) => {
|
|
1057
|
+
const b = new Uint8Array(4);
|
|
1058
|
+
new DataView(b.buffer).setInt32(0, n, false);
|
|
1059
|
+
return b;
|
|
1060
|
+
};
|
|
1061
|
+
var uint32 = (n, littleEndian) => {
|
|
1062
|
+
const b = new Uint8Array(4);
|
|
1063
|
+
new DataView(b.buffer).setUint32(0, n, littleEndian);
|
|
1064
|
+
return b;
|
|
1065
|
+
};
|
|
1066
|
+
var uint64 = (n, littleEndian) => {
|
|
1067
|
+
const b = new Uint8Array(8);
|
|
1068
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(n), littleEndian);
|
|
1069
|
+
return b;
|
|
1070
|
+
};
|
|
1071
|
+
var concat = (...parts) => {
|
|
1072
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1073
|
+
let o = 0;
|
|
1074
|
+
for (const p of parts) {
|
|
1075
|
+
out.set(p, o);
|
|
1076
|
+
o += p.length;
|
|
1077
|
+
}
|
|
1078
|
+
return out;
|
|
1079
|
+
};
|
|
1080
|
+
function checkCommon(address, domain, timestamp) {
|
|
1081
|
+
if (address.hash.length !== 32) throw new RangeError("TON address hash must be 32 bytes.");
|
|
1082
|
+
if (!Number.isInteger(address.workChain)) throw new TypeError("TON address workChain must be an integer.");
|
|
1083
|
+
if (typeof domain !== "string" || domain.length === 0) throw new TypeError("TON Connect domain must be a non-empty string.");
|
|
1084
|
+
if (typeof timestamp !== "number" || !Number.isSafeInteger(timestamp) || timestamp < 0) throw new TypeError("TON Connect timestamp must be a non-negative integer (unix seconds).");
|
|
1085
|
+
}
|
|
1086
|
+
function base64Bytes(raw) {
|
|
1087
|
+
const s = raw.replace(/[\t\n\f\r ]/g, "");
|
|
1088
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s) || s.replace(/=+$/, "").length % 4 === 1) throw new TypeError("signData binary payload must be base64.");
|
|
1089
|
+
return new Uint8Array(import_buffer4.Buffer.from(s, "base64"));
|
|
1090
|
+
}
|
|
1091
|
+
function tonProofDigest(address, request) {
|
|
1092
|
+
checkCommon(address, request.domain, request.timestamp);
|
|
1093
|
+
if (typeof request.payload !== "string") throw new TypeError("ton_proof payload must be a string.");
|
|
1094
|
+
const domain = utf82(request.domain);
|
|
1095
|
+
const inner = concat(utf82("ton-proof-item-v2/"), int32BE(address.workChain), address.hash, uint32(domain.length, true), domain, uint64(request.timestamp, true), utf82(request.payload));
|
|
1096
|
+
return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect"), (0, import_sha2.sha256)(inner)));
|
|
1097
|
+
}
|
|
1098
|
+
function tonSignDataDigest(address, request) {
|
|
1099
|
+
checkCommon(address, request.domain, request.timestamp);
|
|
1100
|
+
const p = request.payload;
|
|
1101
|
+
let payload;
|
|
1102
|
+
if (p?.type === "text" && typeof p.text === "string") payload = utf82(p.text);
|
|
1103
|
+
else if (p?.type === "binary" && typeof p.bytes === "string") payload = base64Bytes(p.bytes);
|
|
1104
|
+
else throw new TypeError('signData payload must be { type: "text", text } or { type: "binary", bytes }; cell payloads are not supported.');
|
|
1105
|
+
const domain = utf82(request.domain);
|
|
1106
|
+
return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect/sign-data/"), int32BE(address.workChain), address.hash, uint32(domain.length, false), domain, uint64(request.timestamp, false), utf82(p.type === "text" ? "txt" : "bin"), uint32(payload.length, false), payload));
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
// src/account.ts
|
|
998
1110
|
var toHex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1111
|
+
var carriesKey = (bytes, key) => {
|
|
1112
|
+
outer: for (let i = 0; i + key.length <= bytes.length; i++) {
|
|
1113
|
+
for (let j = 0; j < key.length; j++) if (bytes[i + j] !== key[j]) continue outer;
|
|
1114
|
+
return true;
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
};
|
|
999
1118
|
var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon {
|
|
1000
1119
|
/** seqno and fees come from the network before signing. */
|
|
1001
1120
|
needsNetwork = true;
|
|
@@ -1019,11 +1138,46 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1019
1138
|
const m = /^m\/44'\/607'\/(\d+)'$/.exec(this.path);
|
|
1020
1139
|
return m ? Number(m[1]) : void 0;
|
|
1021
1140
|
}
|
|
1022
|
-
/**
|
|
1141
|
+
/**
|
|
1142
|
+
* Hex ed25519 signature over the raw message bytes. A 32-byte message — string or bytes — is
|
|
1143
|
+
* refused with {@link MessageIsTransactionError}: the wallet contract checks a transfer's
|
|
1144
|
+
* signature over the 32-byte hash of its signing cell, so those bytes could be one. Bytes that
|
|
1145
|
+
* carry this account's public key are refused as well: one ed25519 key (an external signer) may
|
|
1146
|
+
* also be a Solana wallet, and a Solana transaction names its signer's key. TON Connect goes
|
|
1147
|
+
* through {@link signTonProof} and {@link signTonConnectData}.
|
|
1148
|
+
*/
|
|
1023
1149
|
async sign(message) {
|
|
1024
1150
|
const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1151
|
+
if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
|
|
1025
1152
|
return toHex(await this.signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
|
|
1026
1153
|
}
|
|
1154
|
+
/**
|
|
1155
|
+
* TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
|
|
1156
|
+
* digest is built here (0xffff ‖ "ton-connect" ‖ …), so no request can turn it into a transfer.
|
|
1157
|
+
* `domain` is the host the wallet saw the request come from.
|
|
1158
|
+
*/
|
|
1159
|
+
async signTonProof(request) {
|
|
1160
|
+
const digest = tonProofDigest(this._address, request);
|
|
1161
|
+
const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
|
|
1162
|
+
return {
|
|
1163
|
+
timestamp: request.timestamp,
|
|
1164
|
+
domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
|
|
1165
|
+
payload: request.payload,
|
|
1166
|
+
signature: import_buffer5.Buffer.from(signature).toString("base64")
|
|
1167
|
+
};
|
|
1168
|
+
}
|
|
1169
|
+
/** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
|
|
1170
|
+
async signTonConnectData(request) {
|
|
1171
|
+
const digest = tonSignDataDigest(this._address, request);
|
|
1172
|
+
const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
|
|
1173
|
+
return {
|
|
1174
|
+
signature: import_buffer5.Buffer.from(signature).toString("base64"),
|
|
1175
|
+
address: this._address.toRawString(),
|
|
1176
|
+
timestamp: request.timestamp,
|
|
1177
|
+
domain: request.domain,
|
|
1178
|
+
payload: request.payload.type === "text" ? { type: "text", text: request.payload.text } : { type: "binary", bytes: request.payload.bytes }
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1027
1181
|
checkMaxFee(fee, limit, op) {
|
|
1028
1182
|
if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
|
|
1029
1183
|
}
|
|
@@ -1031,7 +1185,22 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1031
1185
|
* Checks → seqno → preflight → signed body. The preflight runs on the
|
|
1032
1186
|
* zero-signed body (ignore_chksig), so nothing is signed before it passes.
|
|
1033
1187
|
*/
|
|
1034
|
-
|
|
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
|
+
}
|
|
1202
|
+
async signMessages(messages, limit, op) {
|
|
1203
|
+
this.assertNoDrain(messages.map((m) => sendModeOf(m.mode)));
|
|
1035
1204
|
const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
|
|
1036
1205
|
if (want > 0n) {
|
|
1037
1206
|
const balance = await this.getBalance();
|
|
@@ -1063,29 +1232,37 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1063
1232
|
/** Sign without broadcasting. Returns the external-message BODY as base64 BoC (pass it back to sendTransaction as a Cell). */
|
|
1064
1233
|
async signTransaction(tx, options = {}) {
|
|
1065
1234
|
if (tx instanceof import_core10.Cell) {
|
|
1235
|
+
this.assertNoDrain(this.bodyModes(tx));
|
|
1066
1236
|
await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1067
1237
|
return tx.toBoc().toString("base64");
|
|
1068
1238
|
}
|
|
1069
|
-
const { body } = await this.
|
|
1239
|
+
const { body } = await this.signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1070
1240
|
return body.toBoc().toString("base64");
|
|
1071
1241
|
}
|
|
1072
1242
|
async broadcast(body) {
|
|
1073
1243
|
const rpc = this.requireRpc("send transactions");
|
|
1074
1244
|
const init = await this.initIfNeeded();
|
|
1075
|
-
|
|
1076
|
-
|
|
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;
|
|
1077
1253
|
}
|
|
1078
1254
|
/** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
|
|
1079
1255
|
async sendTransaction(tx, options = {}) {
|
|
1080
1256
|
this.requireRpc("send transactions");
|
|
1081
|
-
|
|
1257
|
+
if (tx instanceof import_core10.Cell) this.assertNoDrain(this.bodyModes(tx));
|
|
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");
|
|
1082
1259
|
return { hash: await this.broadcast(body), fee };
|
|
1083
1260
|
}
|
|
1084
1261
|
/** TON (no `token`) or jetton transfer. */
|
|
1085
1262
|
async transfer(o, writeOptions = {}) {
|
|
1086
1263
|
this.requireRpc("transfer tokens");
|
|
1087
1264
|
const message = o.token ? await this.jettonTransferMessage(o) : this.transactionMessage({ to: o.recipient, value: o.amount, ...o.memo !== void 0 ? { body: o.memo } : {} });
|
|
1088
|
-
const { body, fee } = await this.
|
|
1265
|
+
const { body, fee } = await this.signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
|
|
1089
1266
|
return { hash: await this.broadcast(body), fee };
|
|
1090
1267
|
}
|
|
1091
1268
|
/** The same wallet as a {@link WalletAccountReadOnlyTon}. */
|
|
@@ -1116,6 +1293,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
|
|
|
1116
1293
|
static async fromTonMnemonic(words, config = {}) {
|
|
1117
1294
|
const list = (Array.isArray(words) ? words : words.trim().split(/\s+/)).map((w) => w.toLowerCase());
|
|
1118
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).");
|
|
1119
1297
|
const pair = await (0, import_crypto.mnemonicToPrivateKey)(list);
|
|
1120
1298
|
const privateKey = new Uint8Array(pair.secretKey.subarray(0, 32));
|
|
1121
1299
|
const publicKey = new Uint8Array(pair.publicKey);
|
|
@@ -1181,7 +1359,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
|
|
|
1181
1359
|
|
|
1182
1360
|
// src/history.ts
|
|
1183
1361
|
var import_core12 = require("@ton/core");
|
|
1184
|
-
var
|
|
1362
|
+
var import_buffer6 = require("buffer");
|
|
1185
1363
|
var HistoryUnavailableError = class extends Error {
|
|
1186
1364
|
chain;
|
|
1187
1365
|
reason;
|
|
@@ -1198,12 +1376,12 @@ var DEFAULT_LIMIT = 25;
|
|
|
1198
1376
|
var MAX_LIMIT = 100;
|
|
1199
1377
|
var DEFAULT_TIMEOUT_MS3 = 15e3;
|
|
1200
1378
|
function encodeCursor(c) {
|
|
1201
|
-
return
|
|
1379
|
+
return import_buffer6.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
|
|
1202
1380
|
}
|
|
1203
1381
|
function decodeCursor(s) {
|
|
1204
1382
|
let parsed;
|
|
1205
1383
|
try {
|
|
1206
|
-
parsed = JSON.parse(
|
|
1384
|
+
parsed = JSON.parse(import_buffer6.Buffer.from(s, "base64").toString("utf8"));
|
|
1207
1385
|
} catch {
|
|
1208
1386
|
throw new Error("@anywalletkit/wallet-ton: malformed history cursor");
|
|
1209
1387
|
}
|
|
@@ -1455,7 +1633,7 @@ function mapTonapiEvent(chain, address, own, event, valueFlow) {
|
|
|
1455
1633
|
var TEXT_COMMENT_OP = "0x00000000";
|
|
1456
1634
|
function mapToncenterTransaction(chain, address, row) {
|
|
1457
1635
|
const own = row.account.toLowerCase();
|
|
1458
|
-
const hash =
|
|
1636
|
+
const hash = import_buffer6.Buffer.from(row.hash, "base64").toString("hex");
|
|
1459
1637
|
const d = row.description;
|
|
1460
1638
|
const failed = d?.aborted === true || d?.compute_ph?.skipped !== true && d?.compute_ph?.success === false || d?.action?.success === false;
|
|
1461
1639
|
const transfers = [];
|