@anywalletkit/wallet-ton 0.2.4 → 0.2.5
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 +17 -1
- package/dist/index.cjs +121 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +84 -3
- package/dist/index.d.ts +84 -3
- package/dist/index.js +117 -5
- package/dist/index.js.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -68,7 +68,8 @@ const tk = await WalletManagerTon.fromTonMnemonic(words, { tonClient })
|
|
|
68
68
|
|
|
69
69
|
The account never holds a key. Every signature is a `signer.sign(bytes, {scheme: 'ed25519', context})`
|
|
70
70
|
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()
|
|
71
|
+
zero-signed body), `ton-message` over raw message bytes (`sign()`), `ton-proof` / `ton-sign-data`
|
|
72
|
+
over the TON Connect digests (`signTonProof`, `signTonConnectData`). Seed wallets get a
|
|
72
73
|
`LocalSigner`; a hardware wallet, MPC node or backend is wrapped with `externalSigner()` from
|
|
73
74
|
`@anywalletkit/core`:
|
|
74
75
|
|
|
@@ -82,6 +83,21 @@ A derivable `SeedSigner` at `m/44'/607'` yields `getAccount(i)` children; a leaf
|
|
|
82
83
|
`fromTonMnemonic`) is one account. `WalletAccountTon.fromPrivateKey(priv, pub, path)` keeps the
|
|
83
84
|
raw-key shape for callers that derive themselves.
|
|
84
85
|
|
|
86
|
+
## Messages and TON Connect
|
|
87
|
+
|
|
88
|
+
`sign(message)` is a hex ed25519 signature over the raw bytes. A 32-byte message — bytes or a
|
|
89
|
+
string that encodes to 32 bytes — is refused with `MessageIsTransactionError`: the wallet contract
|
|
90
|
+
checks a transfer's signature over the 32-byte hash of its signing cell, so 32 raw bytes may be a
|
|
91
|
+
transfer. TON Connect has its own methods, which build the digest from the request and the
|
|
92
|
+
account's own address (0xffff ‖ "ton-connect…", never a cell hash):
|
|
93
|
+
|
|
94
|
+
```ts
|
|
95
|
+
const proof = await account.signTonProof({ domain: 'app.example', timestamp, payload }) // the `proof` of a ton_proof reply
|
|
96
|
+
const reply = await account.signTonConnectData({ domain: 'app.example', timestamp, payload: { type: 'text', text } }) // signData (text | binary)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
`domain` is the host the wallet saw the request come from, never the dApp's own claim.
|
|
100
|
+
|
|
85
101
|
## Describe
|
|
86
102
|
|
|
87
103
|
`describeTransaction(tx)` / `account.describe(tx)` decode TON transfers (text
|
package/dist/index.cjs
CHANGED
|
@@ -27,6 +27,7 @@ __export(src_exports, {
|
|
|
27
27
|
JETTON_TRANSFER_OP: () => JETTON_TRANSFER_OP,
|
|
28
28
|
MAINNET_GLOBAL_ID: () => MAINNET_GLOBAL_ID,
|
|
29
29
|
MAX_ACTIONS: () => MAX_ACTIONS,
|
|
30
|
+
MessageIsTransactionError: () => MessageIsTransactionError,
|
|
30
31
|
SEND_MODE: () => SEND_MODE,
|
|
31
32
|
SimulationError: () => SimulationError,
|
|
32
33
|
TESTNET_GLOBAL_ID: () => TESTNET_GLOBAL_ID,
|
|
@@ -65,6 +66,8 @@ __export(src_exports, {
|
|
|
65
66
|
runEmulation: () => runEmulation,
|
|
66
67
|
tonCenterEndpoint: () => tonCenterEndpoint,
|
|
67
68
|
tonExplorerTxUrl: () => tonExplorerTxUrl,
|
|
69
|
+
tonProofDigest: () => tonProofDigest,
|
|
70
|
+
tonSignDataDigest: () => tonSignDataDigest,
|
|
68
71
|
v5r1Address: () => v5r1Address,
|
|
69
72
|
v5r1Code: () => v5r1Code,
|
|
70
73
|
v5r1ExternalMessage: () => v5r1ExternalMessage,
|
|
@@ -98,6 +101,13 @@ var UnsafeTransactionError = class extends import_core.SignerError {
|
|
|
98
101
|
this.code = code;
|
|
99
102
|
}
|
|
100
103
|
};
|
|
104
|
+
var MessageIsTransactionError = class extends import_core.SignerError {
|
|
105
|
+
code = "message-is-transaction";
|
|
106
|
+
constructor() {
|
|
107
|
+
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.");
|
|
108
|
+
this.name = "MessageIsTransactionError";
|
|
109
|
+
}
|
|
110
|
+
};
|
|
101
111
|
var SimulationError = class extends Error {
|
|
102
112
|
exitCode;
|
|
103
113
|
aborted;
|
|
@@ -994,8 +1004,75 @@ function randomQueryId() {
|
|
|
994
1004
|
|
|
995
1005
|
// src/account.ts
|
|
996
1006
|
var import_core9 = require("@anywalletkit/core");
|
|
1007
|
+
var import_buffer5 = require("buffer");
|
|
997
1008
|
var import_core10 = require("@ton/core");
|
|
1009
|
+
|
|
1010
|
+
// src/tonconnect.ts
|
|
1011
|
+
var import_sha2 = require("@noble/hashes/sha2.js");
|
|
1012
|
+
var import_buffer4 = require("buffer");
|
|
1013
|
+
var utf82 = (s) => new TextEncoder().encode(s);
|
|
1014
|
+
var int32BE = (n) => {
|
|
1015
|
+
const b = new Uint8Array(4);
|
|
1016
|
+
new DataView(b.buffer).setInt32(0, n, false);
|
|
1017
|
+
return b;
|
|
1018
|
+
};
|
|
1019
|
+
var uint32 = (n, littleEndian) => {
|
|
1020
|
+
const b = new Uint8Array(4);
|
|
1021
|
+
new DataView(b.buffer).setUint32(0, n, littleEndian);
|
|
1022
|
+
return b;
|
|
1023
|
+
};
|
|
1024
|
+
var uint64 = (n, littleEndian) => {
|
|
1025
|
+
const b = new Uint8Array(8);
|
|
1026
|
+
new DataView(b.buffer).setBigUint64(0, BigInt(n), littleEndian);
|
|
1027
|
+
return b;
|
|
1028
|
+
};
|
|
1029
|
+
var concat = (...parts) => {
|
|
1030
|
+
const out = new Uint8Array(parts.reduce((n, p) => n + p.length, 0));
|
|
1031
|
+
let o = 0;
|
|
1032
|
+
for (const p of parts) {
|
|
1033
|
+
out.set(p, o);
|
|
1034
|
+
o += p.length;
|
|
1035
|
+
}
|
|
1036
|
+
return out;
|
|
1037
|
+
};
|
|
1038
|
+
function checkCommon(address, domain, timestamp) {
|
|
1039
|
+
if (address.hash.length !== 32) throw new RangeError("TON address hash must be 32 bytes.");
|
|
1040
|
+
if (!Number.isInteger(address.workChain)) throw new TypeError("TON address workChain must be an integer.");
|
|
1041
|
+
if (typeof domain !== "string" || domain.length === 0) throw new TypeError("TON Connect domain must be a non-empty string.");
|
|
1042
|
+
if (typeof timestamp !== "number" || !Number.isSafeInteger(timestamp) || timestamp < 0) throw new TypeError("TON Connect timestamp must be a non-negative integer (unix seconds).");
|
|
1043
|
+
}
|
|
1044
|
+
function base64Bytes(raw) {
|
|
1045
|
+
const s = raw.replace(/[\t\n\f\r ]/g, "");
|
|
1046
|
+
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(s) || s.replace(/=+$/, "").length % 4 === 1) throw new TypeError("signData binary payload must be base64.");
|
|
1047
|
+
return new Uint8Array(import_buffer4.Buffer.from(s, "base64"));
|
|
1048
|
+
}
|
|
1049
|
+
function tonProofDigest(address, request) {
|
|
1050
|
+
checkCommon(address, request.domain, request.timestamp);
|
|
1051
|
+
if (typeof request.payload !== "string") throw new TypeError("ton_proof payload must be a string.");
|
|
1052
|
+
const domain = utf82(request.domain);
|
|
1053
|
+
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));
|
|
1054
|
+
return (0, import_sha2.sha256)(concat(new Uint8Array([255, 255]), utf82("ton-connect"), (0, import_sha2.sha256)(inner)));
|
|
1055
|
+
}
|
|
1056
|
+
function tonSignDataDigest(address, request) {
|
|
1057
|
+
checkCommon(address, request.domain, request.timestamp);
|
|
1058
|
+
const p = request.payload;
|
|
1059
|
+
let payload;
|
|
1060
|
+
if (p?.type === "text" && typeof p.text === "string") payload = utf82(p.text);
|
|
1061
|
+
else if (p?.type === "binary" && typeof p.bytes === "string") payload = base64Bytes(p.bytes);
|
|
1062
|
+
else throw new TypeError('signData payload must be { type: "text", text } or { type: "binary", bytes }; cell payloads are not supported.');
|
|
1063
|
+
const domain = utf82(request.domain);
|
|
1064
|
+
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));
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/account.ts
|
|
998
1068
|
var toHex = (u8) => Array.from(u8, (b) => b.toString(16).padStart(2, "0")).join("");
|
|
1069
|
+
var carriesKey = (bytes, key) => {
|
|
1070
|
+
outer: for (let i = 0; i + key.length <= bytes.length; i++) {
|
|
1071
|
+
for (let j = 0; j < key.length; j++) if (bytes[i + j] !== key[j]) continue outer;
|
|
1072
|
+
return true;
|
|
1073
|
+
}
|
|
1074
|
+
return false;
|
|
1075
|
+
};
|
|
999
1076
|
var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon {
|
|
1000
1077
|
/** seqno and fees come from the network before signing. */
|
|
1001
1078
|
needsNetwork = true;
|
|
@@ -1019,11 +1096,46 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1019
1096
|
const m = /^m\/44'\/607'\/(\d+)'$/.exec(this.path);
|
|
1020
1097
|
return m ? Number(m[1]) : void 0;
|
|
1021
1098
|
}
|
|
1022
|
-
/**
|
|
1099
|
+
/**
|
|
1100
|
+
* Hex ed25519 signature over the raw message bytes. A 32-byte message — string or bytes — is
|
|
1101
|
+
* refused with {@link MessageIsTransactionError}: the wallet contract checks a transfer's
|
|
1102
|
+
* signature over the 32-byte hash of its signing cell, so those bytes could be one. Bytes that
|
|
1103
|
+
* carry this account's public key are refused as well: one ed25519 key (an external signer) may
|
|
1104
|
+
* also be a Solana wallet, and a Solana transaction names its signer's key. TON Connect goes
|
|
1105
|
+
* through {@link signTonProof} and {@link signTonConnectData}.
|
|
1106
|
+
*/
|
|
1023
1107
|
async sign(message) {
|
|
1024
1108
|
const bytes = typeof message === "string" ? new TextEncoder().encode(message) : message;
|
|
1109
|
+
if (bytes.length === 32 || carriesKey(bytes, this.publicKey)) throw new MessageIsTransactionError();
|
|
1025
1110
|
return toHex(await this.signer.sign(bytes, { scheme: "ed25519", context: "ton-message" }));
|
|
1026
1111
|
}
|
|
1112
|
+
/**
|
|
1113
|
+
* TON Connect `ton_proof` for this wallet's address: the `proof` object of the connect reply. The
|
|
1114
|
+
* digest is built here (0xffff ‖ "ton-connect" ‖ …), so no request can turn it into a transfer.
|
|
1115
|
+
* `domain` is the host the wallet saw the request come from.
|
|
1116
|
+
*/
|
|
1117
|
+
async signTonProof(request) {
|
|
1118
|
+
const digest = tonProofDigest(this._address, request);
|
|
1119
|
+
const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-proof" });
|
|
1120
|
+
return {
|
|
1121
|
+
timestamp: request.timestamp,
|
|
1122
|
+
domain: { lengthBytes: new TextEncoder().encode(request.domain).length, value: request.domain },
|
|
1123
|
+
payload: request.payload,
|
|
1124
|
+
signature: import_buffer5.Buffer.from(signature).toString("base64")
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
/** TON Connect `signData` (text / binary) for this wallet's address: the reply the dApp gets. */
|
|
1128
|
+
async signTonConnectData(request) {
|
|
1129
|
+
const digest = tonSignDataDigest(this._address, request);
|
|
1130
|
+
const signature = await this.signer.sign(digest, { scheme: "ed25519", context: "ton-sign-data" });
|
|
1131
|
+
return {
|
|
1132
|
+
signature: import_buffer5.Buffer.from(signature).toString("base64"),
|
|
1133
|
+
address: this._address.toRawString(),
|
|
1134
|
+
timestamp: request.timestamp,
|
|
1135
|
+
domain: request.domain,
|
|
1136
|
+
payload: request.payload.type === "text" ? { type: "text", text: request.payload.text } : { type: "binary", bytes: request.payload.bytes }
|
|
1137
|
+
};
|
|
1138
|
+
}
|
|
1027
1139
|
checkMaxFee(fee, limit, op) {
|
|
1028
1140
|
if (limit !== void 0 && fee > BigInt(limit)) throw new Error(`Exceeded maximum fee cost for ${op} operation.`);
|
|
1029
1141
|
}
|
|
@@ -1031,7 +1143,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1031
1143
|
* Checks → seqno → preflight → signed body. The preflight runs on the
|
|
1032
1144
|
* zero-signed body (ignore_chksig), so nothing is signed before it passes.
|
|
1033
1145
|
*/
|
|
1034
|
-
async
|
|
1146
|
+
async signMessages(messages, limit, op) {
|
|
1035
1147
|
const want = messages.reduce((sum, m) => sum + (m.message.info.type === "internal" ? m.message.info.value.coins : 0n), 0n);
|
|
1036
1148
|
if (want > 0n) {
|
|
1037
1149
|
const balance = await this.getBalance();
|
|
@@ -1066,7 +1178,7 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1066
1178
|
await this.preflight(tx, (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1067
1179
|
return tx.toBoc().toString("base64");
|
|
1068
1180
|
}
|
|
1069
|
-
const { body } = await this.
|
|
1181
|
+
const { body } = await this.signMessages(this.toMessages(tx), (0, import_core9.feeLimit)(this._config.transactionMaxFee, options), "transaction");
|
|
1070
1182
|
return body.toBoc().toString("base64");
|
|
1071
1183
|
}
|
|
1072
1184
|
async broadcast(body) {
|
|
@@ -1078,14 +1190,14 @@ var WalletAccountTon = class _WalletAccountTon extends WalletAccountReadOnlyTon
|
|
|
1078
1190
|
/** Sign and broadcast; `hash` is the body hash (hex) — resolve it with getTransactionReceipt. */
|
|
1079
1191
|
async sendTransaction(tx, options = {}) {
|
|
1080
1192
|
this.requireRpc("send transactions");
|
|
1081
|
-
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.
|
|
1193
|
+
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
1194
|
return { hash: await this.broadcast(body), fee };
|
|
1083
1195
|
}
|
|
1084
1196
|
/** TON (no `token`) or jetton transfer. */
|
|
1085
1197
|
async transfer(o, writeOptions = {}) {
|
|
1086
1198
|
this.requireRpc("transfer tokens");
|
|
1087
1199
|
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.
|
|
1200
|
+
const { body, fee } = await this.signMessages([message], (0, import_core9.feeLimit)(this._config.transferMaxFee, writeOptions), "transfer");
|
|
1089
1201
|
return { hash: await this.broadcast(body), fee };
|
|
1090
1202
|
}
|
|
1091
1203
|
/** The same wallet as a {@link WalletAccountReadOnlyTon}. */
|
|
@@ -1181,7 +1293,7 @@ var WalletManagerTon = class _WalletManagerTon extends import_core11.WalletManag
|
|
|
1181
1293
|
|
|
1182
1294
|
// src/history.ts
|
|
1183
1295
|
var import_core12 = require("@ton/core");
|
|
1184
|
-
var
|
|
1296
|
+
var import_buffer6 = require("buffer");
|
|
1185
1297
|
var HistoryUnavailableError = class extends Error {
|
|
1186
1298
|
chain;
|
|
1187
1299
|
reason;
|
|
@@ -1198,12 +1310,12 @@ var DEFAULT_LIMIT = 25;
|
|
|
1198
1310
|
var MAX_LIMIT = 100;
|
|
1199
1311
|
var DEFAULT_TIMEOUT_MS3 = 15e3;
|
|
1200
1312
|
function encodeCursor(c) {
|
|
1201
|
-
return
|
|
1313
|
+
return import_buffer6.Buffer.from(JSON.stringify(c), "utf8").toString("base64");
|
|
1202
1314
|
}
|
|
1203
1315
|
function decodeCursor(s) {
|
|
1204
1316
|
let parsed;
|
|
1205
1317
|
try {
|
|
1206
|
-
parsed = JSON.parse(
|
|
1318
|
+
parsed = JSON.parse(import_buffer6.Buffer.from(s, "base64").toString("utf8"));
|
|
1207
1319
|
} catch {
|
|
1208
1320
|
throw new Error("@anywalletkit/wallet-ton: malformed history cursor");
|
|
1209
1321
|
}
|
|
@@ -1455,7 +1567,7 @@ function mapTonapiEvent(chain, address, own, event, valueFlow) {
|
|
|
1455
1567
|
var TEXT_COMMENT_OP = "0x00000000";
|
|
1456
1568
|
function mapToncenterTransaction(chain, address, row) {
|
|
1457
1569
|
const own = row.account.toLowerCase();
|
|
1458
|
-
const hash =
|
|
1570
|
+
const hash = import_buffer6.Buffer.from(row.hash, "base64").toString("hex");
|
|
1459
1571
|
const d = row.description;
|
|
1460
1572
|
const failed = d?.aborted === true || d?.compute_ph?.skipped !== true && d?.compute_ph?.success === false || d?.action?.success === false;
|
|
1461
1573
|
const transfers = [];
|