diff --git a/package-lock.json b/package-lock.json index 7c3b894cfd6..8e4d8737c39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -252,6 +252,10 @@ "resolved": "packages/wasm-solana", "link": true }, + "node_modules/@bitgo/wasm-ton": { + "resolved": "packages/wasm-ton", + "link": true + }, "node_modules/@bitgo/wasm-utxo": { "resolved": "packages/wasm-utxo", "link": true @@ -22368,6 +22372,21 @@ "typescript-eslint": "^8.18.2" } }, + "packages/wasm-ton": { + "name": "@bitgo/wasm-ton", + "version": "0.0.1", + "license": "MIT", + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/mocha": "^10.0.7", + "@types/node": "^22.10.5", + "eslint": "^9.17.0", + "mocha": "^10.6.0", + "tsx": "4.20.6", + "typescript": "^5.5.3", + "typescript-eslint": "^8.18.2" + } + }, "packages/wasm-utxo": { "name": "@bitgo/wasm-utxo", "version": "0.0.2", diff --git a/packages/wasm-ton/.gitignore b/packages/wasm-ton/.gitignore new file mode 100644 index 00000000000..f2d18aa3467 --- /dev/null +++ b/packages/wasm-ton/.gitignore @@ -0,0 +1,10 @@ +target/ +node_modules/ +# we actually only track the .ts files +dist/ +test/*.js +test/*.d.ts +js/*.js +js/*.d.ts +js/wasm +.vscode diff --git a/packages/wasm-ton/.mocharc.json b/packages/wasm-ton/.mocharc.json new file mode 100644 index 00000000000..f585fb0eb49 --- /dev/null +++ b/packages/wasm-ton/.mocharc.json @@ -0,0 +1,5 @@ +{ + "extensions": ["ts", "tsx", "js", "jsx"], + "spec": ["test/**/*.ts"], + "node-option": ["import=tsx/esm", "experimental-wasm-modules"] +} diff --git a/packages/wasm-ton/Cargo.lock b/packages/wasm-ton/Cargo.lock new file mode 100644 index 00000000000..22a5c69dfc0 --- /dev/null +++ b/packages/wasm-ton/Cargo.lock @@ -0,0 +1,987 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anyhow" +version = "1.0.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c" + +[[package]] +name = "array-util" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e509844de8f09b90a2c3444684a2b6695f4071360e13d2fda0af9f749cc2ed6" +dependencies = [ + "arrayvec", +] + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "async-trait" +version = "0.1.89" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + +[[package]] +name = "base64ct" +version = "1.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bumpalo" +version = "3.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0dd1ca384932ff3641c8718a02769f1698e7563dc6974ffd03346116310423" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "chrono" +version = "0.4.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0" +dependencies = [ + "iana-time-zone", + "num-traits", + "windows-link", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "crc" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d" +dependencies = [ + "crc-catalog", +] + +[[package]] +name = "crc-catalog" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" + +[[package]] +name = "crypto-common" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "find-msvc-tools" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "wasi", + "wasm-bindgen", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.65" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "impl-tools" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ae314a99afb5821e2fda288387546d4a04aace674551e854e6216b892ec3208" +dependencies = [ + "autocfg", + "impl-tools-lib", + "proc-macro-error2", + "syn", +] + +[[package]] +name = "impl-tools-lib" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab699036df31c1f7d3561bfa6e9cb9bc3bb0fd2e2cd9bf121c31cb961d049ddf" +dependencies = [ + "proc-macro-error2", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "itoa" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" + +[[package]] +name = "js-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.183" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" + +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "memchr" +version = "2.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" + +[[package]] +name = "minicov" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4869b6a491569605d66d3952bcdf03df789e5b536e5f0cf7758a7f08a55ae24d" +dependencies = [ + "cc", + "walkdir", +] + +[[package]] +name = "nacl" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "30aefc44d813c51b5e7952950e87c17f2e0e1a3274d63c8281a701e05323d548" + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8302e169f0eddcc139c70f139d19d6467353af16f9fce27e8c30158036a1e16b" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "strum" +version = "0.25.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.25.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "rustversion", + "syn", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.117" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tlb" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "416f2233c655807eb4bc0575f0803843971f06ce98b0d3059b1eaa79a4236bd3" +dependencies = [ + "array-util", + "base64 0.21.7", + "bitvec", + "crc", + "digest", + "hex", + "impl-tools", + "sha2", + "tlbits", +] + +[[package]] +name = "tlb-ton" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e26b4014128b0ccc545f3d3bf2b7e2136ee9be3ed51a3447c082a30b20d5e85f" +dependencies = [ + "base64 0.21.7", + "chrono", + "crc", + "digest", + "hex", + "impl-tools", + "lazy_static", + "num-bigint", + "num-traits", + "sha2", + "strum", + "tlb", +] + +[[package]] +name = "tlbits" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0e5a456b59a6f89eb1200fbeebf837295e7dd729e52a206d75b65503acc0a" +dependencies = [ + "array-util", + "bitvec", + "either", + "impl-tools", + "num-bigint", + "num-traits", + "rustversion", + "thiserror", +] + +[[package]] +name = "ton-contracts" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6582842a68b2be6f4dce52e790fff1b6bbd4ce615cf728139621e073760f588" +dependencies = [ + "anyhow", + "bitvec", + "chrono", + "hmac", + "lazy_static", + "nacl", + "num-bigint", + "pbkdf2", + "sha2", + "tlb-ton", +] + +[[package]] +name = "typenum" +version = "1.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasm-bindgen" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9c5522b3a28661442748e09d40924dfb9ca614b21c00d3fd135720e48b67db8" +dependencies = [ + "cfg-if", + "futures-util", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "wasm-bindgen-test" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6311c867385cc7d5602463b31825d454d0837a3aba7cdb5e56d5201792a3f7fe" +dependencies = [ + "async-trait", + "cast", + "js-sys", + "libm", + "minicov", + "nu-ansi-term", + "num-traits", + "oorandom", + "serde", + "serde_json", + "wasm-bindgen", + "wasm-bindgen-futures", + "wasm-bindgen-test-macro", + "wasm-bindgen-test-shared", +] + +[[package]] +name = "wasm-bindgen-test-macro" +version = "0.3.64" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67008cdde4769831958536b0f11b3bdd0380bde882be17fff9c2f34bb4549abd" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "wasm-bindgen-test-shared" +version = "0.2.114" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe29135b180b72b04c74aa97b2b4a2ef275161eff9a6c7955ea9eaedc7e1d4e" + +[[package]] +name = "wasm-ton" +version = "0.1.0" +dependencies = [ + "base64 0.22.1", + "chrono", + "crc", + "getrandom", + "hex", + "js-sys", + "num-bigint", + "serde", + "serde-wasm-bindgen", + "sha2", + "tlb-ton", + "ton-contracts", + "wasm-bindgen", + "wasm-bindgen-test", +] + +[[package]] +name = "web-sys" +version = "0.3.91" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "winapi-util" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" +dependencies = [ + "windows-sys", +] + +[[package]] +name = "windows-core" +version = "0.62.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-interface" +version = "0.59.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-result" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "zmij" +version = "1.0.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/packages/wasm-ton/Cargo.toml b/packages/wasm-ton/Cargo.toml new file mode 100644 index 00000000000..0f391bcf759 --- /dev/null +++ b/packages/wasm-ton/Cargo.toml @@ -0,0 +1,34 @@ +[package] +name = "wasm-ton" +version = "0.1.0" +edition = "2021" + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false + +[lib] +crate-type = ["cdylib", "rlib"] + +[lints.clippy] +all = "warn" + +[dependencies] +wasm-bindgen = "0.2" +js-sys = "0.3" +serde = { version = "1.0", features = ["derive"] } +serde-wasm-bindgen = "0.6" +getrandom = { version = "0.2", features = ["js"] } +chrono = { version = "0.4", default-features = false, features = ["clock"] } +base64 = "0.22" +hex = "0.4" +crc = "3" +sha2 = "0.10" +num-bigint = "0.4" +tlb-ton = { version = "0.7.3", features = ["sha2"] } +ton-contracts = { version = "0.7.3", features = ["wallet"] } + +[dev-dependencies] +wasm-bindgen-test = "0.3" + +[profile.release] +strip = true diff --git a/packages/wasm-ton/Makefile b/packages/wasm-ton/Makefile new file mode 100644 index 00000000000..6b8f1f95965 --- /dev/null +++ b/packages/wasm-ton/Makefile @@ -0,0 +1,70 @@ +WASM_PACK = wasm-pack +WASM_OPT = wasm-opt +WASM_PACK_FLAGS = --no-pack --weak-refs + +ifdef WASM_PACK_DEV + WASM_PACK_FLAGS += --dev +endif + +# Auto-detect Mac and use Homebrew LLVM for WASM compilation +# Apple's Clang doesn't support wasm32-unknown-unknown target +UNAME_S := $(shell uname -s) + +ifeq ($(UNAME_S),Darwin) + # Mac detected - check for Homebrew LLVM installation + HOMEBREW_LLVM := $(shell brew --prefix llvm 2>/dev/null) + + ifdef HOMEBREW_LLVM + export CC = $(HOMEBREW_LLVM)/bin/clang + export AR = $(HOMEBREW_LLVM)/bin/llvm-ar + $(info Using Homebrew LLVM: $(HOMEBREW_LLVM)) + else + $(warning Homebrew LLVM not found. Install with: brew install llvm) + $(warning Continuing with system clang - may fail on Apple Silicon) + endif +endif + +define WASM_PACK_COMMAND + $(WASM_PACK) build --no-opt --out-dir $(1) $(WASM_PACK_FLAGS) --target $(2) +endef + +# run wasm-opt separately so we can pass `--enable-bulk-memory` +define WASM_OPT_COMMAND + $(WASM_OPT) --enable-bulk-memory --enable-nontrapping-float-to-int --enable-sign-ext -Oz $(1)/*.wasm -o $(1)/*.wasm +endef + +define REMOVE_GITIGNORE + find $(1) -name .gitignore -delete +endef + +define SHOW_WASM_SIZE + @find $(1) -name "*.wasm" -exec gzip -k {} \; + @find $(1) -name "*.wasm" -exec du -h {} \; + @find $(1) -name "*.wasm.gz" -exec du -h {} \; + @find $(1) -name "*.wasm.gz" -delete +endef + +define BUILD + rm -rf $(1) + $(call WASM_PACK_COMMAND,$(1),$(2)) + $(call WASM_OPT_COMMAND,$(1)) + $(call REMOVE_GITIGNORE,$(1)) + $(call SHOW_WASM_SIZE,$(1)) +endef + +.PHONY: js/wasm +js/wasm: + $(call BUILD,$@,bundler) + +.PHONY: dist/esm/js/wasm +dist/esm/js/wasm: + $(call BUILD,$@,bundler) + +.PHONY: dist/cjs/js/wasm +dist/cjs/js/wasm: + $(call BUILD,$@,nodejs) + +.PHONY: lint +lint: + cargo fmt --check + cargo clippy --all-targets --all-features -- -D warnings diff --git a/packages/wasm-ton/VALIDATION-ton.md b/packages/wasm-ton/VALIDATION-ton.md new file mode 100644 index 00000000000..adcda44dfac --- /dev/null +++ b/packages/wasm-ton/VALIDATION-ton.md @@ -0,0 +1,38 @@ +# wasm-ton Scratch Validation + +**Wallet:** EQDuPYX8CRn1vCwjaWaua8UCnV7H93kk17eDuCzICC6H0Agk +**Testnet:** TON testnet (toncenter v2 API) +**Date:** 2026-03-27 +**Public Key:** 0x71519a80b6034586824a2571f59b78e9d2258818091ae43151ce6fce217805f7 + +## Results + +| Intent | Status | TxHash | Seqno | +| ---------------------------- | ------ | -------------------------------------------- | ----- | +| payment (native, 0.01 TON) | pass | uPFLE1pBTeW/YAqchGkBmd+4Z0kDES7WtJW6FdaDrZs= | 0→1 | +| fillNonce (native) | pass | HrdOUp4XaT33ikzi9ZJ+L8jgEL/6MQIKnmpJQKZnqB8= | 1→2 | +| consolidate (native) | pass | vyFUjysPh8UgXy0GbvtnTe1f3vC1NBiic0LIACe+0yE= | 2→3 | +| delegate (TonWhales) | pass | Rj7ZE3npPJiP7phh4o+ApjUPaS6BF+Qi/zfQl2FQw2c= | 3→4 | +| undelegate (TonWhales) | pass | diYXhAYCMeXewaugrLtSQv5Ti2p2+2Zqp6F4c6jlbBk= | 4→5 | +| delegate (SingleNominator) | pass | KPH0m6VXfVdMBjmQwyjoF/0UPnHWJIn6Js4yiTN8wMc= | 5→6 | +| undelegate (SingleNominator) | pass | HuLjS4rn9K9BOZy8tPGGe5kqXN0/+0toDZPi72o1KKs= | 6→7 | + +## Parse Round-trip + +| Intent | Build -> Parse | Fields Verified | +| ---------------------------- | -------------- | ------------------------------------------------------------------------------ | +| payment | pass | recipient, amount (10000000), memo ("wasm-ton scratch test"), bounce (false) | +| fillNonce | pass | recipient (self), amount (1 nanoton) | +| consolidate | pass | recipient (self), amount (1 nanoton) | +| delegate (TonWhales) | pass | transactionType (WhalesDeposit), amount (1000000000), opcode (0x7bcd1fef) | +| undelegate (TonWhales) | pass | transactionType (WhalesWithdraw), amount (200000000), opcode (0xda803efd) | +| delegate (SingleNominator) | pass | transactionType (Transfer), amount (1000000000), bounce (true) | +| undelegate (SingleNominator) | pass | transactionType (SingleNominatorWithdraw), amount (200000000), opcode (0x1000) | + +## Notes + +- First transaction (payment) deployed the wallet contract via StateInit (BOC 980 bytes vs ~185 bytes for subsequent txs) +- Token intents (tokenPayment, tokenFillNonce, tokenConsolidate) not tested: require a deployed Jetton contract on testnet +- MultiNominator delegate/undelegate not tested separately (uses simple transfer with memo, same code path as SingleNominator delegate) +- Staking intents sent to own address as "validator" (no real validator contracts on testnet), but tx structure and opcodes are correct +- Explorer: https://testnet.tonviewer.com/EQDuPYX8CRn1vCwjaWaua8UCnV7H93kk17eDuCzICC6H0Agk diff --git a/packages/wasm-ton/eslint.config.js b/packages/wasm-ton/eslint.config.js new file mode 100644 index 00000000000..7ea533aafc9 --- /dev/null +++ b/packages/wasm-ton/eslint.config.js @@ -0,0 +1,47 @@ +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommendedTypeChecked, + { + languageOptions: { + parserOptions: { + project: ["./tsconfig.json", "./tsconfig.test.json"], + tsconfigRootDir: import.meta.dirname, + }, + }, + }, + { + ignores: ["dist/", "pkg/", "target/", "node_modules/", "js/wasm/", "bundler-test/"], + }, + { + files: ["**/*.js"], + ...tseslint.configs.disableTypeChecked, + }, + // Ban Node.js globals in production code + { + files: ["js/**/*.ts"], + rules: { + "no-restricted-globals": [ + "error", + { + name: "Buffer", + message: "Use Uint8Array instead of Buffer for ESM compatibility.", + }, + { + name: "process", + message: "Avoid Node.js process global for ESM compatibility.", + }, + { + name: "__dirname", + message: "Use import.meta.url instead of __dirname for ESM.", + }, + { + name: "__filename", + message: "Use import.meta.url instead of __filename for ESM.", + }, + ], + }, + }, +); diff --git a/packages/wasm-ton/js/address.ts b/packages/wasm-ton/js/address.ts new file mode 100644 index 00000000000..ec5f6ea5c5a --- /dev/null +++ b/packages/wasm-ton/js/address.ts @@ -0,0 +1,56 @@ +import { AddressNamespace } from "./wasm/wasm_ton.js"; + +/** + * Result of decoding a TON address + */ +export interface DecodedAddress { + workchainId: number; + addressHash: Uint8Array; + isBounceable: boolean; + isTestnet: boolean; +} + +/** + * Encode a raw Ed25519 public key to a TON user-friendly address. + * + * Computes the wallet v4r2 StateInit hash internally (workchain 0, default wallet ID). + * + * @param publicKey - 32-byte Ed25519 public key + * @param bounceable - Whether the address is bounceable (default: true) + * @returns User-friendly base64url address string (EQ for bounceable, UQ for non-bounceable) + */ +export function encodeAddress(publicKey: Uint8Array, bounceable = true): string { + return AddressNamespace.encodeAddress(publicKey, bounceable); +} + +/** + * Encode an address hash and workchain into a user-friendly TON address. + * + * @param workchainId - The workchain ID (0 for basechain) + * @param addressHash - 32-byte address hash + * @param bounceable - Whether the address is bounceable (default: true) + * @returns User-friendly base64url address string + */ +export function encode(workchainId: number, addressHash: Uint8Array, bounceable = true): string { + return AddressNamespace.encode(workchainId, addressHash, bounceable); +} + +/** + * Decode a TON address string into its components. + * + * @param address - TON address (user-friendly or raw hex) + * @returns Decoded address components + */ +export function decode(address: string): DecodedAddress { + return AddressNamespace.decode(address) as DecodedAddress; +} + +/** + * Validate a TON address string. + * + * @param address - TON address to validate + * @returns true if valid + */ +export function validate(address: string): boolean { + return AddressNamespace.validate(address); +} diff --git a/packages/wasm-ton/js/builder.ts b/packages/wasm-ton/js/builder.ts new file mode 100644 index 00000000000..4f1f9f3e47a --- /dev/null +++ b/packages/wasm-ton/js/builder.ts @@ -0,0 +1,109 @@ +/** + * Transaction building via intents. + * + * Intents and context are passed directly to WASM as JS objects. + * serde-wasm-bindgen handles deserialization, including BigInt to u64. + */ + +import { BuilderNamespace } from "./wasm/wasm_ton.js"; +import { Transaction } from "./transaction.js"; + +/** Staking provider types */ +export const TonStakingType = ["TonWhales", "SingleNominator", "MultiNominator"] as const; +export type TonStakingType = (typeof TonStakingType)[number]; + +/** Transaction type */ +export const TonTransactionType = [ + "Transfer", + "TokenTransfer", + "WhalesDeposit", + "WhalesVestingDeposit", + "WhalesWithdraw", + "WhalesVestingWithdraw", + "SingleNominatorWithdraw", + "Unknown", +] as const; +export type TonTransactionType = (typeof TonTransactionType)[number]; + +/** Build context for constructing transactions */ +export interface BuildContext { + sender: string; + seqno: number; + expireTime: bigint; + publicKey?: string; + walletVersion?: number; + isVestingContract?: boolean; + subWalletId?: bigint; +} + +/** Native TON payment intent */ +export interface PaymentIntent { + type: "payment"; + to: string; + amount: bigint; + bounceable?: boolean; + memo?: string; +} + +/** Token (jetton) payment intent */ +export interface TokenPaymentIntent { + type: "tokenPayment"; + to: string; + amount: bigint; + jettonAddress: string; + tonAmount?: bigint; + forwardTonAmount?: bigint; + memo?: string; +} + +/** Fill nonce intent */ +export interface FillNonceIntent { + type: "fillNonce"; + isToken?: boolean; + jettonAddress?: string; +} + +/** Consolidate intent */ +export interface ConsolidateIntent { + type: "consolidate"; + isToken?: boolean; + jettonAddress?: string; +} + +/** Delegate (staking) intent */ +export interface DelegateIntent { + type: "delegate"; + amount: bigint; + validatorAddress: string; + stakingType: TonStakingType; + queryId?: bigint; +} + +/** Undelegate (unstaking) intent */ +export interface UndelegateIntent { + type: "undelegate"; + amount?: bigint; + validatorAddress: string; + stakingType: TonStakingType; +} + +/** Union of all intent types */ +export type TonIntent = + | PaymentIntent + | TokenPaymentIntent + | FillNonceIntent + | ConsolidateIntent + | DelegateIntent + | UndelegateIntent; + +/** + * Build a transaction from an intent and context. + * + * @param intent - Transaction intent + * @param context - Build context (sender, seqno, expireTime, etc.) + * @returns A Transaction ready for signing + */ +export function buildTransaction(intent: TonIntent, context: BuildContext): Transaction { + const bytes = BuilderNamespace.buildTransaction(intent, context); + return Transaction.fromBytes(bytes); +} diff --git a/packages/wasm-ton/js/index.ts b/packages/wasm-ton/js/index.ts new file mode 100644 index 00000000000..8509eeab26a --- /dev/null +++ b/packages/wasm-ton/js/index.ts @@ -0,0 +1,35 @@ +import * as wasm from "./wasm/wasm_ton.js"; + +// Force webpack to include the WASM module +void wasm; + +// Namespace exports +export * as address from "./address.js"; +export * as transaction from "./transaction.js"; +export * as parser from "./parser.js"; +export * as builder from "./builder.js"; + +// Top-level function exports +export { encodeAddress, encode, decode, validate } from "./address.js"; +export { Transaction, transactionFromBytes } from "./transaction.js"; +export { parseTransaction } from "./parser.js"; +export { buildTransaction } from "./builder.js"; + +// Type exports +export type { DecodedAddress } from "./address.js"; +export type { ParsedTransaction, ParsedSendAction, JettonTransferFields } from "./parser.js"; +export type { + BuildContext, + PaymentIntent, + TokenPaymentIntent, + FillNonceIntent, + ConsolidateIntent, + DelegateIntent, + UndelegateIntent, + TonIntent, +} from "./builder.js"; + +export { TonStakingType, TonTransactionType } from "./builder.js"; + +// Constants +export { default_wallet_id as defaultWalletId } from "./wasm/wasm_ton.js"; diff --git a/packages/wasm-ton/js/parser.ts b/packages/wasm-ton/js/parser.ts new file mode 100644 index 00000000000..a8187938cbf --- /dev/null +++ b/packages/wasm-ton/js/parser.ts @@ -0,0 +1,61 @@ +/** + * High-level transaction parsing. + * + * All monetary amounts are returned as bigint directly from WASM. + */ + +import { ParserNamespace } from "./wasm/wasm_ton.js"; +import type { Transaction } from "./transaction.js"; + +/** Jetton transfer fields */ +export interface JettonTransferFields { + queryId: bigint; + amount: bigint; + destination: string; + responseDestination: string; + forwardTonAmount: bigint; +} + +/** A single send action from the transaction */ +export interface ParsedSendAction { + mode: number; + destination: string; + destinationBounceable: string; + amount: bigint; + bounce: boolean; + stateInit: boolean; + bodyOpcode?: number; + memo?: string; + jettonTransfer?: JettonTransferFields; +} + +/** A fully parsed TON transaction */ +export interface ParsedTransaction { + transactionType: string; + sender: string; + walletId: number; + seqno: number; + expireAt: bigint; + signature: string; + sendActions: ParsedSendAction[]; +} + +/** + * Parse a Transaction into structured data. + * + * @param tx - A Transaction instance + * @returns A ParsedTransaction with decoded actions + */ +export function parseTransaction(tx: Transaction): ParsedTransaction { + return ParserNamespace.parseFromTransaction(tx.wasm) as ParsedTransaction; +} + +/** + * Parse raw BOC bytes into structured data. + * + * @param bytes - Raw BOC bytes + * @returns A ParsedTransaction + */ +export function parseTransactionBytes(bytes: Uint8Array): ParsedTransaction { + return ParserNamespace.parseTransaction(bytes) as ParsedTransaction; +} diff --git a/packages/wasm-ton/js/transaction.ts b/packages/wasm-ton/js/transaction.ts new file mode 100644 index 00000000000..60da6d6aedd --- /dev/null +++ b/packages/wasm-ton/js/transaction.ts @@ -0,0 +1,86 @@ +import { TransactionNamespace, WasmTransaction } from "./wasm/wasm_ton.js"; + +/** + * TON Transaction wrapper for signing and serialization. + * + * Use `Transaction.fromBytes(bytes)` to create. + * Use `parseTransaction(tx)` from parser.ts to get decoded instruction data. + */ +export class Transaction { + private constructor(private _wasm: WasmTransaction) {} + + /** + * Deserialize a transaction from raw BOC bytes. + */ + static fromBytes(bytes: Uint8Array): Transaction { + const wasm = TransactionNamespace.fromBytes(bytes); + return new Transaction(wasm); + } + + /** + * Get the signable payload (SHA-256 hash of the sign body cell). + * @returns 32-byte hash as Uint8Array + */ + signablePayload(): Uint8Array { + return this._wasm.signablePayload(); + } + + /** + * Add a 64-byte Ed25519 signature. + */ + addSignature(signature: Uint8Array): void { + this._wasm.addSignature(signature); + } + + /** + * Serialize to BOC bytes. + */ + toBytes(): Uint8Array { + return this._wasm.toBytes(); + } + + /** + * Serialize to broadcast format (raw BOC bytes). + * Callers convert to base64 at serialization boundaries: + * Buffer.from(tx.toBroadcastFormat()).toString('base64') + */ + toBroadcastFormat(): Uint8Array { + return this._wasm.toBroadcastFormat(); + } + + /** + * Get the transaction ID. + */ + get id(): string { + return this._wasm.id; + } + + /** + * Get the destination address. + */ + get destination(): string | undefined { + return this._wasm.destination ?? undefined; + } + + /** + * Get the current signature as hex string. + */ + get signature(): string { + return this._wasm.signature; + } + + /** + * Get the underlying WASM instance (internal use only). + * @internal + */ + get wasm(): WasmTransaction { + return this._wasm; + } +} + +/** + * Convenience function to create a Transaction from bytes. + */ +export function transactionFromBytes(bytes: Uint8Array): Transaction { + return Transaction.fromBytes(bytes); +} diff --git a/packages/wasm-ton/package.json b/packages/wasm-ton/package.json new file mode 100644 index 00000000000..d5edd059fbb --- /dev/null +++ b/packages/wasm-ton/package.json @@ -0,0 +1,61 @@ +{ + "name": "@bitgo/wasm-ton", + "description": "WebAssembly wrapper for TON cryptographic operations", + "version": "0.0.1", + "private": true, + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/BitGo/BitGoWASM" + }, + "license": "MIT", + "files": [ + "dist" + ], + "exports": { + ".": { + "import": { + "types": "./dist/esm/js/index.d.ts", + "default": "./dist/esm/js/index.js" + }, + "require": { + "types": "./dist/cjs/js/index.d.ts", + "default": "./dist/cjs/js/index.js" + } + } + }, + "main": "./dist/cjs/js/index.js", + "module": "./dist/esm/js/index.js", + "types": "./dist/esm/js/index.d.ts", + "sideEffects": [ + "./dist/esm/js/wasm/wasm_ton.js", + "./dist/cjs/js/wasm/wasm_ton.js" + ], + "scripts": { + "test": "npm run test:mocha", + "test:mocha": "mocha --recursive test", + "build:wasm": "make js/wasm && make dist/esm/js/wasm && make dist/cjs/js/wasm", + "build:ts-esm": "tsc", + "build:ts-cjs": "tsc --project tsconfig.cjs.json", + "build:ts": "npm run build:ts-esm && npm run build:ts-cjs", + "build:package-json": "echo '{\"type\":\"commonjs\"}' > dist/cjs/package.json", + "build": "npm run build:wasm && npm run build:ts && npm run build:package-json", + "check-fmt": "prettier --check . && cargo fmt -- --check", + "lint": "eslint .", + "lint:fix": "eslint . --fix" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/mocha": "^10.0.7", + "@types/node": "^22.10.5", + "eslint": "^9.17.0", + "mocha": "^10.6.0", + "tsx": "4.20.6", + "typescript": "^5.5.3", + "typescript-eslint": "^8.18.2" + }, + "publishConfig": { + "access": "public", + "registry": "https://registry.npmjs.org/" + } +} diff --git a/packages/wasm-ton/src/address.rs b/packages/wasm-ton/src/address.rs new file mode 100644 index 00000000000..ed3ce0e86f3 --- /dev/null +++ b/packages/wasm-ton/src/address.rs @@ -0,0 +1,168 @@ +use tlb_ton::MsgAddress; +use ton_contracts::wallet::v4r2::V4R2; +use ton_contracts::wallet::WalletVersion; + +use crate::error::WasmTonError; + +/// Address format enum matching TON user-friendly formats +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum AddressFormat { + /// Bounceable (EQ prefix in base64url) + Bounceable, + /// Non-bounceable (UQ prefix in base64url) + NonBounceable, + /// Raw hex format: workchain:hex_address + RawHex, +} + +/// Encode a public key hash (32 bytes) and workchain to a TON address. +/// +/// The `address_hash` is the 32-byte address portion of MsgAddress, +/// NOT a public key. For wallets, this is the hash of the state init. +pub fn encode_address( + workchain_id: i32, + address_hash: &[u8], + format: AddressFormat, +) -> Result { + if address_hash.len() != 32 { + return Err(WasmTonError::new("address hash must be 32 bytes")); + } + let mut hash = [0u8; 32]; + hash.copy_from_slice(address_hash); + let addr = MsgAddress { + workchain_id, + address: hash, + }; + Ok(match format { + AddressFormat::Bounceable => addr.to_base64_url_flags(false, false), + AddressFormat::NonBounceable => addr.to_base64_url_flags(true, false), + AddressFormat::RawHex => addr.to_hex(), + }) +} + +/// Encode a raw Ed25519 public key to a TON user-friendly address. +/// +/// This computes the wallet v4r2 StateInit hash internally, using workchain 0 +/// and the default wallet ID. +pub fn encode_address_from_public_key( + public_key: &[u8], + bounceable: bool, +) -> Result { + if public_key.len() != 32 { + return Err(WasmTonError::new("public key must be 32 bytes")); + } + let mut pubkey = [0u8; 32]; + pubkey.copy_from_slice(public_key); + + let state_init = V4R2::state_init(V4R2::DEFAULT_WALLET_ID, pubkey); + let msg_addr = MsgAddress::derive(0, state_init) + .map_err(|e| WasmTonError::new(&format!("failed to derive address: {e}")))?; + + let non_bounceable = !bounceable; + Ok(msg_addr.to_base64_url_flags(non_bounceable, false)) +} + +/// Decode a TON address string into its components. +/// +/// Supports: +/// - User-friendly base64url (EQ/UQ prefix, 48 chars) +/// - Raw hex format (workchain:hex) +/// +/// Returns (workchain_id, address_hash, is_bounceable, is_testnet) +pub fn decode_address(addr: &str) -> Result<(i32, [u8; 32], bool, bool), WasmTonError> { + // Try raw hex first + if addr.contains(':') { + let parsed = MsgAddress::from_hex(addr) + .map_err(|e| WasmTonError::new(&format!("invalid hex address: {e}")))?; + // Raw hex doesn't carry bounce/testnet flags + return Ok((parsed.workchain_id, parsed.address, true, false)); + } + + // Try user-friendly base64url + let (parsed, non_bounceable, non_production) = MsgAddress::from_base64_url_flags(addr) + .or_else(|_| MsgAddress::from_base64_std_flags(addr)) + .map_err(|e| WasmTonError::new(&format!("invalid address: {e}")))?; + + Ok(( + parsed.workchain_id, + parsed.address, + !non_bounceable, + non_production, + )) +} + +/// Validate a TON address string. +pub fn validate_address(addr: &str) -> bool { + decode_address(addr).is_ok() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_roundtrip_bounceable() { + let addr = "EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG"; + let (wc, hash, bounceable, testnet) = decode_address(addr).unwrap(); + assert_eq!(wc, 0); + assert!(bounceable); + assert!(!testnet); + let encoded = encode_address(wc, &hash, AddressFormat::Bounceable).unwrap(); + assert_eq!(encoded, addr); + } + + #[test] + fn test_roundtrip_non_bounceable() { + let addr = "UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD"; + let (wc, hash, bounceable, testnet) = decode_address(addr).unwrap(); + assert_eq!(wc, 0); + assert!(!bounceable); + assert!(!testnet); + let encoded = encode_address(wc, &hash, AddressFormat::NonBounceable).unwrap(); + assert_eq!(encoded, addr); + } + + #[test] + fn test_raw_hex() { + let addr = "0:348bcf82746945fc38541c77fdd91d4e347eac200f6f2d9fd62dc08885f0415f"; + let (wc, hash, _, _) = decode_address(addr).unwrap(); + assert_eq!(wc, 0); + let encoded = encode_address(wc, &hash, AddressFormat::RawHex).unwrap(); + assert_eq!(encoded, addr); + } + + #[test] + fn test_invalid_addresses() { + assert!(!validate_address("randomString")); + assert!(!validate_address("0xc4173a804406a365e69dfb297ddfgsdcvf")); + assert!(!validate_address( + "5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen" + )); + } + + #[test] + fn test_encode_address_from_public_key() { + // Known test vector from ton-contracts doctest + let pubkey: [u8; 32] = [ + 0x7d, 0x6b, 0x1a, 0x21, 0x0b, 0x18, 0x0c, 0xa1, 0x41, 0x26, 0x7c, 0xea, 0x69, 0x56, + 0x8a, 0x6a, 0x4f, 0xf2, 0xd8, 0x49, 0xda, 0x9e, 0x6f, 0x47, 0x6d, 0x04, 0x10, 0x05, + 0xd4, 0x47, 0x6c, 0x6e, + ]; + let non_bounceable = encode_address_from_public_key(&pubkey, false).unwrap(); + assert_eq!( + non_bounceable, + "UQAHgNAYSdWyD3kl2RIl_oSo4lS0ECclh-FDjKETwGtSOZbW" + ); + + let bounceable = encode_address_from_public_key(&pubkey, true).unwrap(); + assert_eq!( + bounceable, + "EQAHgNAYSdWyD3kl2RIl_oSo4lS0ECclh-FDjKETwGtSOcsT" + ); + } + + #[test] + fn test_encode_address_from_public_key_invalid() { + assert!(encode_address_from_public_key(&[0u8; 16], true).is_err()); + } +} diff --git a/packages/wasm-ton/src/builder/build.rs b/packages/wasm-ton/src/builder/build.rs new file mode 100644 index 00000000000..15820d12a1c --- /dev/null +++ b/packages/wasm-ton/src/builder/build.rs @@ -0,0 +1,413 @@ +use chrono::DateTime; +use num_bigint::BigUint; +use tlb_ton::{ + action::SendMsgAction, + bits::ser::BitWriterExt, + currency::Grams, + message::{CommonMsgInfo, ExternalInMsgInfo, Message}, + ser::CellSerializeExt, + BagOfCells, BagOfCellsArgs, Cell, MsgAddress, +}; +use ton_contracts::wallet::v4r2::{WalletV4R2ExternalBody, WalletV4R2SignBody, V4R2}; +use ton_contracts::wallet::WalletVersion; + +use super::types::{BuildContext, TonIntent, TonStakingType}; +use crate::error::WasmTonError; + +// Opcodes for staking operations +const WHALES_DEPOSIT_OPCODE: u32 = 0x7bcd1fef; +const WHALES_WITHDRAW_OPCODE: u32 = 0xda803efd; +const SINGLE_NOMINATOR_WITHDRAW_OPCODE: u32 = 0x00001000; +// Jetton transfer opcode +const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; + +const BOC_ARGS: BagOfCellsArgs = BagOfCellsArgs { + has_idx: false, + has_crc32c: true, +}; + +/// Build a transaction from an intent and context. +/// Returns the serialized BOC bytes. +pub fn build_transaction( + context: &BuildContext, + intent: &TonIntent, +) -> Result, WasmTonError> { + intent.validate(context)?; + + let expire_at = DateTime::from_timestamp(context.expire_time as i64, 0) + .ok_or_else(|| WasmTonError::new("invalid expire_time"))?; + + let sender_addr = parse_address(&context.sender)?; + + let wallet_id = context + .sub_wallet_id + .map(|id| id as u32) + .unwrap_or(V4R2::DEFAULT_WALLET_ID); + + let send_actions = build_send_actions(context, intent, sender_addr)?; + + let sign_body = WalletV4R2SignBody { + wallet_id, + expire_at, + seqno: context.seqno, + op: ton_contracts::wallet::v4r2::WalletV4R2Op::Send(send_actions), + }; + + let external_body = WalletV4R2ExternalBody { + signature: [0u8; 64], // placeholder, to be filled by TSS + body: sign_body, + }; + + // When seqno == 0 and publicKey is provided, include StateInit to deploy the wallet contract + let init = if let (0, Some(public_key)) = (context.seqno, context.public_key.as_ref()) { + let pubkey_bytes = hex::decode(public_key) + .map_err(|e| WasmTonError::new(&format!("invalid publicKey hex: {e}")))?; + if pubkey_bytes.len() != 32 { + return Err(WasmTonError::new("publicKey must be 32 bytes")); + } + let mut pubkey = [0u8; 32]; + pubkey.copy_from_slice(&pubkey_bytes); + let typed_init = V4R2::state_init(wallet_id, pubkey); + // Serialize typed StateInit fields to plain Cells for Message<_> + let code_cell = typed_init.code.as_ref().map(|c| (**c).clone()); + let data_cell = typed_init + .data + .as_ref() + .map(|d| { + d.to_cell(()).map_err(|e| { + WasmTonError::new(&format!("failed to serialize StateInit data: {e}")) + }) + }) + .transpose()?; + Some(tlb_ton::state_init::StateInit { + code: code_cell, + data: data_cell, + ..Default::default() + }) + } else { + None + }; + + let message: Message = Message { + info: CommonMsgInfo::ExternalIn(ExternalInMsgInfo { + src: MsgAddress::NULL, + dst: sender_addr, + import_fee: BigUint::ZERO, + }), + init, + body: external_body, + }; + + let cell = message + .to_cell(()) + .map_err(|e| WasmTonError::new(&format!("failed to serialize message: {e}")))?; + let boc = BagOfCells::from_root(cell); + boc.serialize(BOC_ARGS) + .map_err(|e| WasmTonError::new(&format!("failed to serialize BOC: {e}"))) +} + +fn build_send_actions( + _context: &BuildContext, + intent: &TonIntent, + sender_addr: MsgAddress, +) -> Result, WasmTonError> { + match intent { + TonIntent::Payment { + to, + amount, + bounceable, + memo, + } => { + let dst = parse_address(to)?; + let bounce = bounceable.unwrap_or(false); + let body = build_comment_body(memo.as_deref())?; + Ok(vec![build_internal_send(dst, *amount, bounce, body, 3)]) + } + + TonIntent::TokenPayment { + to, + amount, + jetton_address, + ton_amount, + forward_ton_amount, + memo, + } => { + let jetton_wallet = parse_address(jetton_address)?; + let dst = parse_address(to)?; + + let body = build_jetton_transfer_body( + 0, // query_id + *amount, + dst, + sender_addr, + *forward_ton_amount, + memo.as_deref(), + )?; + + Ok(vec![build_internal_send( + jetton_wallet, + *ton_amount, + true, + body, + 3, + )]) + } + + TonIntent::FillNonce { + is_token, + jetton_address, + } => { + if *is_token { + let jetton_wallet = parse_address(jetton_address.as_deref().ok_or_else(|| { + WasmTonError::new("jetton_address required for token fillNonce") + })?)?; + let body = build_jetton_transfer_body(0, 1, sender_addr, sender_addr, 100, None)?; + Ok(vec![build_internal_send( + jetton_wallet, + 100_000_000, + true, + body, + 3, + )]) + } else { + let body = build_comment_body(None)?; + Ok(vec![build_internal_send(sender_addr, 1, false, body, 3)]) + } + } + + TonIntent::Consolidate { + is_token, + jetton_address, + } => { + if *is_token { + let jetton_wallet = parse_address(jetton_address.as_deref().ok_or_else(|| { + WasmTonError::new("jetton_address required for token consolidate") + })?)?; + let body = build_jetton_transfer_body(0, 1, sender_addr, sender_addr, 100, None)?; + Ok(vec![build_internal_send( + jetton_wallet, + 100_000_000, + true, + body, + 3, + )]) + } else { + let body = build_comment_body(None)?; + Ok(vec![build_internal_send(sender_addr, 1, false, body, 3)]) + } + } + + TonIntent::Delegate { + amount, + validator_address, + staking_type, + query_id, + } => { + let validator = parse_address(validator_address)?; + + match staking_type { + TonStakingType::TonWhales => { + let body = build_whales_deposit_body(query_id.unwrap_or(0))?; + Ok(vec![build_internal_send(validator, *amount, true, body, 3)]) + } + TonStakingType::SingleNominator => { + let body = build_comment_body(None)?; + Ok(vec![build_internal_send(validator, *amount, true, body, 3)]) + } + TonStakingType::MultiNominator => { + let body = build_comment_body(Some("d"))?; + Ok(vec![build_internal_send(validator, *amount, true, body, 3)]) + } + } + } + + TonIntent::Undelegate { + amount, + validator_address, + staking_type, + } => { + let validator = parse_address(validator_address)?; + + match staking_type { + TonStakingType::TonWhales => { + let withdraw_amount = amount.unwrap_or(0); + let body = build_whales_withdraw_body(0, withdraw_amount)?; + Ok(vec![build_internal_send( + validator, + 200_000_000, + true, + body, + 3, + )]) + } + TonStakingType::SingleNominator => { + let withdraw_amount = amount.unwrap_or(0); + let body = build_single_nominator_withdraw_body(0, withdraw_amount)?; + Ok(vec![build_internal_send( + validator, + 200_000_000, + true, + body, + 3, + )]) + } + TonStakingType::MultiNominator => { + let body = build_comment_body(Some("w"))?; + Ok(vec![build_internal_send( + validator, + 200_000_000, + true, + body, + 3, + )]) + } + } + } + } +} + +fn parse_address(addr: &str) -> Result { + addr.parse::() + .map_err(|e| WasmTonError::new(&format!("invalid address '{addr}': {e}"))) +} + +fn build_comment_body(memo: Option<&str>) -> Result { + let mut builder = Cell::builder(); + if let Some(text) = memo { + builder + .pack(0u32, ()) + .map_err(|e| WasmTonError::new(&format!("failed to write comment opcode: {e}")))?; + for byte in text.as_bytes() { + builder + .pack(*byte, ()) + .map_err(|e| WasmTonError::new(&format!("failed to write comment: {e}")))?; + } + } + Ok(builder.into_cell()) +} + +fn build_jetton_transfer_body( + query_id: u64, + amount: u64, + destination: MsgAddress, + response_destination: MsgAddress, + forward_ton_amount: u64, + memo: Option<&str>, +) -> Result { + let mut builder = Cell::builder(); + builder + .pack(JETTON_TRANSFER_OPCODE, ()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to write opcode: {e}")))?; + builder + .pack(query_id, ()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to write query_id: {e}")))?; + builder + .pack_as::<_, &Grams>(&BigUint::from(amount), ()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to write amount: {e}")))?; + builder + .pack(destination, ()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to write destination: {e}")))?; + builder.pack(response_destination, ()).map_err(|e| { + WasmTonError::new(&format!( + "jetton: failed to write response_destination: {e}" + )) + })?; + // custom_payload: Maybe ^Cell = None + builder.pack(false, ()).map_err(|e| { + WasmTonError::new(&format!("jetton: failed to write custom_payload flag: {e}")) + })?; + builder + .pack_as::<_, &Grams>(&BigUint::from(forward_ton_amount), ()) + .map_err(|e| { + WasmTonError::new(&format!("jetton: failed to write forward_ton_amount: {e}")) + })?; + + // forward_payload: Either Cell ^Cell + if let Some(text) = memo { + builder.pack(false, ()).map_err(|e| { + WasmTonError::new(&format!( + "jetton: failed to write forward_payload flag: {e}" + )) + })?; + builder + .pack(0u32, ()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to write memo opcode: {e}")))?; + for byte in text.as_bytes() { + builder.pack(*byte, ()).map_err(|e| { + WasmTonError::new(&format!("jetton: failed to write memo byte: {e}")) + })?; + } + } else { + builder.pack(false, ()).map_err(|e| { + WasmTonError::new(&format!( + "jetton: failed to write forward_payload flag: {e}" + )) + })?; + } + + Ok(builder.into_cell()) +} + +fn build_whales_deposit_body(query_id: u64) -> Result { + let mut builder = Cell::builder(); + builder + .pack(WHALES_DEPOSIT_OPCODE, ()) + .map_err(|e| WasmTonError::new(&format!("whales deposit: failed to write opcode: {e}")))?; + builder.pack(query_id, ()).map_err(|e| { + WasmTonError::new(&format!("whales deposit: failed to write query_id: {e}")) + })?; + Ok(builder.into_cell()) +} + +fn build_whales_withdraw_body(query_id: u64, amount: u64) -> Result { + let mut builder = Cell::builder(); + builder + .pack(WHALES_WITHDRAW_OPCODE, ()) + .map_err(|e| WasmTonError::new(&format!("whales withdraw: failed to write opcode: {e}")))?; + builder.pack(query_id, ()).map_err(|e| { + WasmTonError::new(&format!("whales withdraw: failed to write query_id: {e}")) + })?; + builder + .pack_as::<_, &Grams>(&BigUint::from(amount), ()) + .map_err(|e| WasmTonError::new(&format!("whales withdraw: failed to write amount: {e}")))?; + Ok(builder.into_cell()) +} + +fn build_single_nominator_withdraw_body(query_id: u64, amount: u64) -> Result { + let mut builder = Cell::builder(); + builder + .pack(SINGLE_NOMINATOR_WITHDRAW_OPCODE, ()) + .map_err(|e| { + WasmTonError::new(&format!( + "single nominator withdraw: failed to write opcode: {e}" + )) + })?; + builder.pack(query_id, ()).map_err(|e| { + WasmTonError::new(&format!( + "single nominator withdraw: failed to write query_id: {e}" + )) + })?; + builder + .pack_as::<_, &Grams>(&BigUint::from(amount), ()) + .map_err(|e| { + WasmTonError::new(&format!( + "single nominator withdraw: failed to write amount: {e}" + )) + })?; + Ok(builder.into_cell()) +} + +fn build_internal_send( + dst: MsgAddress, + amount: u64, + bounce: bool, + body: Cell, + mode: u8, +) -> SendMsgAction { + let msg = Message { + info: CommonMsgInfo::transfer(dst, BigUint::from(amount), bounce), + init: None, + body, + }; + SendMsgAction { mode, message: msg } +} diff --git a/packages/wasm-ton/src/builder/mod.rs b/packages/wasm-ton/src/builder/mod.rs new file mode 100644 index 00000000000..786c1705fa9 --- /dev/null +++ b/packages/wasm-ton/src/builder/mod.rs @@ -0,0 +1,5 @@ +pub mod build; +pub mod types; + +pub use build::build_transaction; +pub use types::*; diff --git a/packages/wasm-ton/src/builder/types.rs b/packages/wasm-ton/src/builder/types.rs new file mode 100644 index 00000000000..1e847552c4f --- /dev/null +++ b/packages/wasm-ton/src/builder/types.rs @@ -0,0 +1,114 @@ +use serde::Deserialize; + +use crate::error::WasmTonError; + +/// Build context for constructing transactions. +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct BuildContext { + pub sender: String, + pub seqno: u32, + pub expire_time: u64, + pub public_key: Option, + #[serde(default = "default_wallet_version")] + pub wallet_version: u32, + #[serde(default)] + pub is_vesting_contract: bool, + #[serde(default)] + pub sub_wallet_id: Option, +} + +fn default_wallet_version() -> u32 { + 4 +} + +/// Staking type for delegate/undelegate intents. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub enum TonStakingType { + TonWhales, + SingleNominator, + MultiNominator, +} + +/// Intent types for building TON transactions. +#[derive(Debug, Clone, Deserialize)] +#[serde(tag = "type", rename_all = "camelCase")] +pub enum TonIntent { + /// Native TON payment + #[serde(rename_all = "camelCase")] + Payment { + to: String, + amount: u64, + bounceable: Option, + memo: Option, + }, + + /// Token (jetton) payment + #[serde(rename = "tokenPayment", rename_all = "camelCase")] + TokenPayment { + to: String, + amount: u64, + jetton_address: String, + #[serde(default = "default_ton_amount")] + ton_amount: u64, + #[serde(default = "default_forward_ton_amount")] + forward_ton_amount: u64, + memo: Option, + }, + + /// Fill nonce (native self-send) + #[serde(rename = "fillNonce", rename_all = "camelCase")] + FillNonce { + #[serde(default)] + is_token: bool, + jetton_address: Option, + }, + + /// Consolidate + #[serde(rename_all = "camelCase")] + Consolidate { + #[serde(default)] + is_token: bool, + jetton_address: Option, + }, + + /// Delegate (staking) + #[serde(rename_all = "camelCase")] + Delegate { + amount: u64, + validator_address: String, + staking_type: TonStakingType, + #[serde(default)] + query_id: Option, + }, + + /// Undelegate (unstaking) + #[serde(rename_all = "camelCase")] + Undelegate { + #[serde(default)] + amount: Option, + validator_address: String, + staking_type: TonStakingType, + }, +} + +fn default_ton_amount() -> u64 { + 100_000_000 // 0.1 TON +} + +fn default_forward_ton_amount() -> u64 { + 100 // 100 nanoTON +} + +impl TonIntent { + pub fn validate(&self, context: &BuildContext) -> Result<(), WasmTonError> { + if let TonIntent::Consolidate { .. } = self { + if context.wallet_version == 1 { + return Err(WasmTonError::new( + "consolidate not supported for wallet version 1", + )); + } + } + Ok(()) + } +} diff --git a/packages/wasm-ton/src/error.rs b/packages/wasm-ton/src/error.rs new file mode 100644 index 00000000000..556cae3d20d --- /dev/null +++ b/packages/wasm-ton/src/error.rs @@ -0,0 +1,42 @@ +use core::fmt; +use wasm_bindgen::prelude::*; + +#[derive(Debug, Clone)] +pub enum WasmTonError { + StringError(String), +} + +impl std::error::Error for WasmTonError {} + +impl fmt::Display for WasmTonError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + WasmTonError::StringError(s) => write!(f, "{}", s), + } + } +} + +impl From<&str> for WasmTonError { + fn from(s: &str) -> Self { + WasmTonError::StringError(s.to_string()) + } +} + +impl From for WasmTonError { + fn from(s: String) -> Self { + WasmTonError::StringError(s) + } +} + +impl WasmTonError { + pub fn new(s: &str) -> WasmTonError { + WasmTonError::StringError(s.to_string()) + } +} + +// Required for wasm_bindgen to convert errors to JavaScript exceptions +impl From for JsValue { + fn from(err: WasmTonError) -> Self { + js_sys::Error::new(&err.to_string()).into() + } +} diff --git a/packages/wasm-ton/src/lib.rs b/packages/wasm-ton/src/lib.rs new file mode 100644 index 00000000000..72325140027 --- /dev/null +++ b/packages/wasm-ton/src/lib.rs @@ -0,0 +1,19 @@ +//! wasm-ton: WASM bindings for TON cryptographic operations. +//! +//! This crate wraps the `tlb-ton` and `ton-contracts` crates to provide +//! TON address encoding/decoding, transaction parsing, signing support, +//! and intent-based transaction building via WASM bindings. + +mod address; +pub mod builder; +mod error; +mod parser; +mod transaction; +pub mod wasm; + +pub use address::{decode_address, encode_address, validate_address}; +pub use error::WasmTonError; +pub use parser::{parse_transaction, ParsedTransaction, TransactionType}; +pub use transaction::Transaction; + +pub use wasm::{AddressNamespace, BuilderNamespace, ParserNamespace, TransactionNamespace}; diff --git a/packages/wasm-ton/src/parser.rs b/packages/wasm-ton/src/parser.rs new file mode 100644 index 00000000000..eac8c4a98e7 --- /dev/null +++ b/packages/wasm-ton/src/parser.rs @@ -0,0 +1,315 @@ +use num_bigint::BigUint; +use tlb_ton::{bits::de::BitReaderExt, currency::Grams, message::CommonMsgInfo, Cell, MsgAddress}; +use ton_contracts::wallet::v4r2::{WalletV4R2Op, WalletV4R2SignBody}; + +use crate::error::WasmTonError; +use crate::transaction::Transaction; + +/// Body parse result: (opcode, memo, jetton_transfer) +type BodyParseResult = (Option, Option, Option); + +/// Transaction type enum +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum TransactionType { + Transfer, + TokenTransfer, + WhalesDeposit, + WhalesVestingDeposit, + WhalesWithdraw, + WhalesVestingWithdraw, + SingleNominatorWithdraw, + Unknown, +} + +impl TransactionType { + pub fn as_str(&self) -> &'static str { + match self { + TransactionType::Transfer => "Transfer", + TransactionType::TokenTransfer => "TokenTransfer", + TransactionType::WhalesDeposit => "WhalesDeposit", + TransactionType::WhalesVestingDeposit => "WhalesVestingDeposit", + TransactionType::WhalesWithdraw => "WhalesWithdraw", + TransactionType::WhalesVestingWithdraw => "WhalesVestingWithdraw", + TransactionType::SingleNominatorWithdraw => "SingleNominatorWithdraw", + TransactionType::Unknown => "Unknown", + } + } +} + +// Opcodes +const JETTON_TRANSFER_OPCODE: u32 = 0x0f8a7ea5; +const WHALES_DEPOSIT_OPCODE: u32 = 0x7bcd1fef; +const WHALES_WITHDRAW_OPCODE: u32 = 0xda803efd; +const SINGLE_NOMINATOR_WITHDRAW_OPCODE: u32 = 0x00001000; // 4096 + +/// Parsed jetton transfer fields (manually parsed, not using ton_contracts JettonTransfer) +#[derive(Debug, Clone)] +pub struct JettonTransferFields { + pub query_id: u64, + pub amount: u64, + pub destination: String, + pub response_destination: String, + pub forward_ton_amount: u64, + pub forward_payload: Option>, +} + +/// A single send action parsed from the transaction +#[derive(Debug, Clone)] +pub struct ParsedSendAction { + pub mode: u8, + pub destination: String, + pub destination_bounceable: String, + pub amount: u64, + pub bounce: bool, + pub body_opcode: Option, + pub state_init: bool, + pub memo: Option, + pub jetton_transfer: Option, +} + +/// A fully parsed TON transaction +#[derive(Debug, Clone)] +pub struct ParsedTransaction { + pub transaction_type: TransactionType, + pub sender: String, + pub wallet_id: u32, + pub seqno: u32, + pub expire_at: u64, + pub signature: String, + pub send_actions: Vec, +} + +/// Parse a transaction from raw BOC bytes. +pub fn parse_transaction(bytes: &[u8]) -> Result { + let tx = Transaction::from_bytes(bytes)?; + parse_from_transaction(&tx) +} + +/// Parse a pre-deserialized Transaction. +pub fn parse_from_transaction(tx: &Transaction) -> Result { + let sender = match &tx.message.info { + CommonMsgInfo::ExternalIn(info) => { + if info.dst.is_null() { + "null".to_string() + } else { + info.dst.to_base64_url_flags(false, false) + } + } + _ => return Err(WasmTonError::new("expected external-in message")), + }; + + let sign_body = tx.sign_body(); + let signature = hex::encode(tx.signature()); + let expire_at = sign_body.expire_at.timestamp() as u64; + + let send_actions = parse_sign_body_actions(sign_body)?; + let transaction_type = determine_transaction_type(&send_actions); + + Ok(ParsedTransaction { + transaction_type, + sender, + wallet_id: sign_body.wallet_id, + seqno: sign_body.seqno, + expire_at, + signature, + send_actions, + }) +} + +fn parse_sign_body_actions( + sign_body: &WalletV4R2SignBody, +) -> Result, WasmTonError> { + match &sign_body.op { + WalletV4R2Op::Send(actions) => { + let mut parsed = Vec::new(); + for action in actions { + let msg = &action.message; + + let (destination_addr, amount, bounce) = match &msg.info { + CommonMsgInfo::Internal(info) => { + let amount = biguint_to_u64(&info.value.grams); + (info.dst, amount, info.bounce) + } + _ => { + return Err(WasmTonError::new( + "expected internal message in send action", + )) + } + }; + + let bounceable_str = destination_addr.to_base64_url_flags(false, false); + let non_bounceable_str = destination_addr.to_base64_url_flags(true, false); + + let state_init = msg.init.is_some(); + + // Parse body + let (body_opcode, memo, jetton_transfer) = parse_message_body(&msg.body)?; + + parsed.push(ParsedSendAction { + mode: action.mode, + destination: if bounce { + bounceable_str.clone() + } else { + non_bounceable_str + }, + destination_bounceable: bounceable_str, + amount, + bounce, + body_opcode, + state_init, + memo, + jetton_transfer, + }); + } + Ok(parsed) + } + _ => Err(WasmTonError::new("unsupported wallet op (not Send)")), + } +} + +fn parse_message_body(body: &Cell) -> Result { + let mut parser = body.parser(); + let bits_left = parser.bits_left(); + + // Empty body + if bits_left == 0 { + return Ok((None, None, None)); + } + + // Need at least 32 bits for opcode + if bits_left < 32 { + return Ok((None, None, None)); + } + + let opcode: u32 = parser + .unpack(()) + .map_err(|e| WasmTonError::new(&format!("failed to read opcode: {e}")))?; + + if opcode == 0 { + // Text comment - read remaining bytes as UTF-8 + let remaining = parser.bits_left() / 8; + let mut bytes = Vec::with_capacity(remaining); + for _ in 0..remaining { + match parser.unpack::(()) { + Ok(v) => bytes.push(v), + Err(_) => break, + }; + } + let memo = String::from_utf8_lossy(&bytes).to_string(); + return Ok((Some(0), Some(memo), None)); + } + + if opcode == JETTON_TRANSFER_OPCODE { + let jetton = parse_jetton_transfer_body(&mut parser)?; + return Ok((Some(opcode), None, Some(jetton))); + } + + // Other known opcodes + Ok((Some(opcode), None, None)) +} + +fn parse_jetton_transfer_body( + parser: &mut tlb_ton::de::CellParser<'_>, +) -> Result { + // query_id: uint64 + let query_id: u64 = parser + .unpack(()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to read query_id: {e}")))?; + + // amount: VarUInteger 16 (Grams encoding) + let amount_big: BigUint = parser + .unpack_as::<_, Grams>(()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to read amount: {e}")))?; + let amount = biguint_to_u64(&amount_big); + + // destination: MsgAddress + let dst: MsgAddress = parser + .unpack(()) + .map_err(|e| WasmTonError::new(&format!("jetton: failed to read destination: {e}")))?; + let destination = dst.to_base64_url_flags(false, false); + + // response_destination: MsgAddress + let response_dst: MsgAddress = parser.unpack(()).map_err(|e| { + WasmTonError::new(&format!("jetton: failed to read response_destination: {e}")) + })?; + let response_destination = if response_dst.is_null() { + "null".to_string() + } else { + response_dst.to_base64_url_flags(false, false) + }; + + // custom_payload: Maybe ^Cell (skip it) + let has_custom_payload: bool = parser.unpack(()).unwrap_or(false); + if has_custom_payload { + // Skip the ref + let _: Cell = parser.parse_as::<_, tlb_ton::Ref>(()).unwrap_or_default(); + } + + // forward_ton_amount: VarUInteger 16 + let forward_big: BigUint = parser.unpack_as::<_, Grams>(()).map_err(|e| { + WasmTonError::new(&format!("jetton: failed to read forward_ton_amount: {e}")) + })?; + let forward_ton_amount = biguint_to_u64(&forward_big); + + Ok(JettonTransferFields { + query_id, + amount, + destination, + response_destination, + forward_ton_amount, + forward_payload: None, + }) +} + +fn determine_transaction_type(actions: &[ParsedSendAction]) -> TransactionType { + if actions.is_empty() { + return TransactionType::Unknown; + } + + let first = &actions[0]; + + // Check for jetton transfer + if first.jetton_transfer.is_some() { + return TransactionType::TokenTransfer; + } + + // Check for known opcodes + if let Some(opcode) = first.body_opcode { + match opcode { + WHALES_DEPOSIT_OPCODE => { + return if first.state_init { + TransactionType::WhalesVestingDeposit + } else { + TransactionType::WhalesDeposit + }; + } + WHALES_WITHDRAW_OPCODE => { + return if first.state_init { + TransactionType::WhalesVestingWithdraw + } else { + TransactionType::WhalesWithdraw + }; + } + SINGLE_NOMINATOR_WITHDRAW_OPCODE => { + return TransactionType::SingleNominatorWithdraw; + } + _ => {} + } + } + + // Plain transfer + if first.body_opcode.is_none() || first.body_opcode == Some(0) { + return TransactionType::Transfer; + } + + TransactionType::Unknown +} + +fn biguint_to_u64(v: &BigUint) -> u64 { + let max = BigUint::from(u64::MAX); + if *v > max { + u64::MAX + } else { + v.to_u64_digits().first().copied().unwrap_or(0) + } +} diff --git a/packages/wasm-ton/src/transaction.rs b/packages/wasm-ton/src/transaction.rs new file mode 100644 index 00000000000..ae11f2f03cb --- /dev/null +++ b/packages/wasm-ton/src/transaction.rs @@ -0,0 +1,124 @@ +use base64::{engine::general_purpose::STANDARD, Engine}; +use sha2::{Digest, Sha256}; +use tlb_ton::{ + message::{CommonMsgInfo, Message}, + ser::CellSerializeExt, + BagOfCells, BagOfCellsArgs, +}; +use ton_contracts::wallet::v4r2::{WalletV4R2ExternalBody, WalletV4R2SignBody}; + +use crate::error::WasmTonError; + +/// A TON transaction (external message BOC). +#[derive(Debug, Clone)] +pub struct Transaction { + /// The raw BOC bytes + boc_bytes: Vec, + /// The parsed external message + pub message: Message, +} + +const BOC_ARGS: BagOfCellsArgs = BagOfCellsArgs { + has_idx: false, + has_crc32c: true, +}; + +impl Transaction { + /// Deserialize a transaction from raw BOC bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + let boc = BagOfCells::deserialize(bytes) + .map_err(|e| WasmTonError::new(&format!("failed to parse BOC: {e}")))?; + let root = boc + .single_root() + .ok_or_else(|| WasmTonError::new("BOC must have exactly one root cell"))?; + let message: Message = root + .parse_fully(()) + .map_err(|e| WasmTonError::new(&format!("failed to parse message: {e}")))?; + Ok(Transaction { + boc_bytes: bytes.to_vec(), + message, + }) + } + + /// Deserialize a transaction from base64-encoded BOC. + pub fn from_base64(b64: &str) -> Result { + let bytes = STANDARD + .decode(b64) + .map_err(|e| WasmTonError::new(&format!("invalid base64: {e}")))?; + Self::from_bytes(&bytes) + } + + /// Get the signable payload (SHA-256 hash of the SignBody cell). + /// This is what gets signed by Ed25519. + pub fn signable_payload(&self) -> Result, WasmTonError> { + let sign_body = &self.message.body.body; + let cell = sign_body + .to_cell(()) + .map_err(|e| WasmTonError::new(&format!("failed to serialize sign body: {e}")))?; + let hash = cell.hash(); + Ok(hash.to_vec()) + } + + /// Get the sign body for inspection. + pub fn sign_body(&self) -> &WalletV4R2SignBody { + &self.message.body.body + } + + /// Get the current signature. + pub fn signature(&self) -> &[u8; 64] { + &self.message.body.signature + } + + /// Get the destination address from the external message. + pub fn destination(&self) -> Option { + match &self.message.info { + CommonMsgInfo::ExternalIn(info) => { + if info.dst.is_null() { + None + } else { + Some(info.dst.to_base64_url()) + } + } + _ => None, + } + } + + /// Add a signature to this transaction, producing new BOC bytes. + pub fn add_signature(&mut self, signature: &[u8]) -> Result<(), WasmTonError> { + if signature.len() != 64 { + return Err(WasmTonError::new("signature must be 64 bytes")); + } + let mut sig = [0u8; 64]; + sig.copy_from_slice(signature); + self.message.body.signature = sig; + // Re-serialize + self.boc_bytes = self.serialize_boc()?; + Ok(()) + } + + /// Serialize the transaction to BOC bytes. + pub fn to_bytes(&self) -> Result, WasmTonError> { + Ok(self.boc_bytes.clone()) + } + + /// Serialize to base64 for broadcast. + pub fn to_broadcast_format(&self) -> Result { + Ok(STANDARD.encode(&self.boc_bytes)) + } + + /// Get the transaction ID (SHA-256 hash of the BOC, base64url encoded). + pub fn id(&self) -> String { + let hash = Sha256::digest(&self.boc_bytes); + base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(hash) + } + + fn serialize_boc(&self) -> Result, WasmTonError> { + let cell = self + .message + .to_cell(()) + .map_err(|e| WasmTonError::new(&format!("failed to serialize message: {e}")))?; + let boc = BagOfCells::from_root(cell); + boc.serialize(BOC_ARGS) + .map_err(|e| WasmTonError::new(&format!("failed to serialize BOC: {e}"))) + } +} diff --git a/packages/wasm-ton/src/wasm/address.rs b/packages/wasm-ton/src/wasm/address.rs new file mode 100644 index 00000000000..c131b55a2e7 --- /dev/null +++ b/packages/wasm-ton/src/wasm/address.rs @@ -0,0 +1,90 @@ +use crate::address; +use wasm_bindgen::prelude::*; + +/// Namespace for TON address operations. +#[wasm_bindgen] +pub struct AddressNamespace; + +#[wasm_bindgen] +impl AddressNamespace { + /// Encode an address hash and workchain into a user-friendly TON address. + /// + /// @param workchain_id - The workchain ID (0 for basechain) + /// @param address_hash - 32-byte address hash + /// @param bounceable - Whether the address is bounceable + /// @returns User-friendly base64url address string + #[wasm_bindgen] + pub fn encode( + workchain_id: i32, + address_hash: &[u8], + bounceable: bool, + ) -> Result { + let format = if bounceable { + address::AddressFormat::Bounceable + } else { + address::AddressFormat::NonBounceable + }; + address::encode_address(workchain_id, address_hash, format) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Encode a raw Ed25519 public key to a TON user-friendly address. + /// + /// Computes the wallet v4r2 StateInit hash internally (workchain 0, default wallet ID). + /// + /// @param public_key - 32-byte Ed25519 public key + /// @param bounceable - Whether the address is bounceable (default: true) + /// @returns User-friendly base64url address string + #[wasm_bindgen(js_name = encodeAddress)] + pub fn encode_address(public_key: &[u8], bounceable: bool) -> Result { + address::encode_address_from_public_key(public_key, bounceable) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Encode an address hash and workchain into raw hex format. + /// + /// @param workchain_id - The workchain ID + /// @param address_hash - 32-byte address hash + /// @returns Raw hex address (workchain:hex) + #[wasm_bindgen(js_name = encodeRawHex)] + pub fn encode_raw_hex(workchain_id: i32, address_hash: &[u8]) -> Result { + address::encode_address(workchain_id, address_hash, address::AddressFormat::RawHex) + .map_err(|e| JsValue::from_str(&e.to_string())) + } + + /// Decode a TON address string. + /// + /// Returns a JS object with: + /// - workchainId: number + /// - addressHash: Uint8Array (32 bytes) + /// - isBounceable: boolean + /// - isTestnet: boolean + /// + /// @param address - TON address (user-friendly or raw hex) + /// @returns Decoded address object + #[wasm_bindgen] + pub fn decode(addr: &str) -> Result { + let (wc, hash, bounceable, testnet) = + address::decode_address(addr).map_err(|e| JsValue::from_str(&e.to_string()))?; + + let obj = js_sys::Object::new(); + js_sys::Reflect::set(&obj, &"workchainId".into(), &JsValue::from(wc))?; + js_sys::Reflect::set( + &obj, + &"addressHash".into(), + &js_sys::Uint8Array::from(hash.as_slice()), + )?; + js_sys::Reflect::set(&obj, &"isBounceable".into(), &JsValue::from(bounceable))?; + js_sys::Reflect::set(&obj, &"isTestnet".into(), &JsValue::from(testnet))?; + Ok(obj.into()) + } + + /// Validate a TON address string. + /// + /// @param address - TON address to validate + /// @returns true if valid + #[wasm_bindgen] + pub fn validate(addr: &str) -> bool { + address::validate_address(addr) + } +} diff --git a/packages/wasm-ton/src/wasm/builder.rs b/packages/wasm-ton/src/wasm/builder.rs new file mode 100644 index 00000000000..4a0e5e1379b --- /dev/null +++ b/packages/wasm-ton/src/wasm/builder.rs @@ -0,0 +1,30 @@ +use crate::builder; +use wasm_bindgen::prelude::*; + +/// Namespace for transaction building operations. +#[wasm_bindgen] +pub struct BuilderNamespace; + +#[wasm_bindgen] +impl BuilderNamespace { + /// Build a transaction from an intent and context. + /// + /// @param intent - TonIntent JS object + /// @param context - BuildContext JS object + /// @returns Raw BOC bytes as Uint8Array + #[wasm_bindgen(js_name = buildTransaction)] + pub fn build_transaction( + intent: JsValue, + context: JsValue, + ) -> Result { + let intent: builder::TonIntent = serde_wasm_bindgen::from_value(intent) + .map_err(|e| JsValue::from_str(&format!("invalid intent: {e}")))?; + let context: builder::BuildContext = serde_wasm_bindgen::from_value(context) + .map_err(|e| JsValue::from_str(&format!("invalid context: {e}")))?; + + let bytes = builder::build_transaction(&context, &intent) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + + Ok(js_sys::Uint8Array::from(&bytes[..])) + } +} diff --git a/packages/wasm-ton/src/wasm/constants.rs b/packages/wasm-ton/src/wasm/constants.rs new file mode 100644 index 00000000000..3cf67761616 --- /dev/null +++ b/packages/wasm-ton/src/wasm/constants.rs @@ -0,0 +1,7 @@ +use wasm_bindgen::prelude::*; + +/// Default wallet ID for V4R2 +#[wasm_bindgen] +pub fn default_wallet_id() -> u32 { + 0x29a9a317 +} diff --git a/packages/wasm-ton/src/wasm/mod.rs b/packages/wasm-ton/src/wasm/mod.rs new file mode 100644 index 00000000000..9be72edd37c --- /dev/null +++ b/packages/wasm-ton/src/wasm/mod.rs @@ -0,0 +1,12 @@ +mod address; +mod builder; +mod constants; +mod parser; +mod transaction; +pub mod try_into_js_value; + +pub use address::AddressNamespace; +pub use builder::BuilderNamespace; +pub use constants::*; +pub use parser::ParserNamespace; +pub use transaction::TransactionNamespace; diff --git a/packages/wasm-ton/src/wasm/parser.rs b/packages/wasm-ton/src/wasm/parser.rs new file mode 100644 index 00000000000..7753116bdd0 --- /dev/null +++ b/packages/wasm-ton/src/wasm/parser.rs @@ -0,0 +1,37 @@ +use crate::parser; +use crate::wasm::transaction::WasmTransaction; +use crate::wasm::try_into_js_value::TryIntoJsValue; +use wasm_bindgen::prelude::*; + +/// Namespace for transaction parsing operations. +#[wasm_bindgen] +pub struct ParserNamespace; + +#[wasm_bindgen] +impl ParserNamespace { + /// Parse a serialized TON transaction (BOC bytes) into structured data. + /// + /// @param bytes - Raw BOC bytes + /// @returns ParsedTransaction object + #[wasm_bindgen(js_name = parseTransaction)] + pub fn parse_transaction(bytes: &[u8]) -> Result { + let parsed = + parser::parse_transaction(bytes).map_err(|e| JsValue::from_str(&e.to_string()))?; + parsed + .try_to_js_value() + .map_err(|e| JsValue::from_str(&format!("Conversion error: {e}"))) + } + + /// Parse a pre-deserialized WasmTransaction into structured data. + /// + /// @param tx - A WasmTransaction instance + /// @returns ParsedTransaction object + #[wasm_bindgen(js_name = parseFromTransaction)] + pub fn parse_from_transaction(tx: &WasmTransaction) -> Result { + let parsed = parser::parse_from_transaction(tx.inner()) + .map_err(|e| JsValue::from_str(&e.to_string()))?; + parsed + .try_to_js_value() + .map_err(|e| JsValue::from_str(&format!("Conversion error: {e}"))) + } +} diff --git a/packages/wasm-ton/src/wasm/transaction.rs b/packages/wasm-ton/src/wasm/transaction.rs new file mode 100644 index 00000000000..a25d15b353e --- /dev/null +++ b/packages/wasm-ton/src/wasm/transaction.rs @@ -0,0 +1,92 @@ +use crate::error::WasmTonError; +use crate::transaction::Transaction; +use wasm_bindgen::prelude::*; + +/// Namespace for TON transaction operations. +#[wasm_bindgen] +pub struct TransactionNamespace; + +#[wasm_bindgen] +impl TransactionNamespace { + /// Deserialize a transaction from raw BOC bytes. + /// + /// @param bytes - Raw BOC bytes + /// @returns Opaque transaction handle for further operations + #[wasm_bindgen(js_name = fromBytes)] + pub fn from_bytes(bytes: &[u8]) -> Result { + Transaction::from_bytes(bytes).map(|inner| WasmTransaction { inner }) + } +} + +/// WASM wrapper for a TON transaction. +#[wasm_bindgen] +pub struct WasmTransaction { + inner: Transaction, +} + +#[wasm_bindgen] +impl WasmTransaction { + /// Get the signable payload (SHA-256 of the sign body cell). + /// This is what needs to be signed by Ed25519. + /// + /// @returns 32-byte hash as Uint8Array + #[wasm_bindgen(js_name = signablePayload)] + pub fn signable_payload(&self) -> Result { + let bytes = self.inner.signable_payload()?; + Ok(js_sys::Uint8Array::from(&bytes[..])) + } + + /// Add a 64-byte Ed25519 signature to this transaction. + /// + /// @param signature - 64-byte signature + #[wasm_bindgen(js_name = addSignature)] + pub fn add_signature(&mut self, signature: &[u8]) -> Result<(), WasmTonError> { + self.inner.add_signature(signature) + } + + /// Serialize the transaction to BOC bytes. + /// + /// @returns Raw BOC bytes + #[wasm_bindgen(js_name = toBytes)] + pub fn to_bytes(&self) -> Result { + let bytes = self.inner.to_bytes()?; + Ok(js_sys::Uint8Array::from(&bytes[..])) + } + + /// Serialize to broadcast format (raw BOC bytes). + /// + /// @returns Raw BOC bytes as Uint8Array + #[wasm_bindgen(js_name = toBroadcastFormat)] + pub fn to_broadcast_format(&self) -> Result { + let bytes = self.inner.to_bytes()?; + Ok(js_sys::Uint8Array::from(&bytes[..])) + } + + /// Get the transaction ID (SHA-256 of BOC, base64url-encoded). + /// + /// @returns Transaction ID string + #[wasm_bindgen(getter)] + pub fn id(&self) -> String { + self.inner.id() + } + + /// Get the destination address of this external message. + /// + /// @returns Bounceable address string, or undefined + #[wasm_bindgen(getter)] + pub fn destination(&self) -> Option { + self.inner.destination() + } + + /// Get the current signature as hex string. + #[wasm_bindgen(getter)] + pub fn signature(&self) -> String { + hex::encode(self.inner.signature()) + } +} + +impl WasmTransaction { + pub fn inner(&self) -> &Transaction { + &self.inner + } +} diff --git a/packages/wasm-ton/src/wasm/try_into_js_value.rs b/packages/wasm-ton/src/wasm/try_into_js_value.rs new file mode 100644 index 00000000000..660e5b8bd6e --- /dev/null +++ b/packages/wasm-ton/src/wasm/try_into_js_value.rs @@ -0,0 +1,228 @@ +//! Trait for converting Rust types to JavaScript values. +//! +//! This module provides proper BigInt handling for u64 amounts. + +use wasm_bindgen::JsValue; + +/// Error type for JS value conversion failures. +#[derive(Debug)] +pub struct JsConversionError(String); + +impl JsConversionError { + pub fn new(msg: &str) -> Self { + JsConversionError(msg.to_string()) + } +} + +impl std::fmt::Display for JsConversionError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for JsValue { + fn from(err: JsConversionError) -> Self { + js_sys::Error::new(&err.to_string()).into() + } +} + +/// Trait for converting Rust types to JavaScript values. +pub trait TryIntoJsValue { + fn try_to_js_value(&self) -> Result; +} + +impl TryIntoJsValue for String { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_str(self)) + } +} + +impl TryIntoJsValue for str { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_str(self)) + } +} + +impl TryIntoJsValue for &str { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_str(self)) + } +} + +impl TryIntoJsValue for u8 { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_f64(*self as f64)) + } +} + +impl TryIntoJsValue for u32 { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_f64(*self as f64)) + } +} + +impl TryIntoJsValue for u64 { + fn try_to_js_value(&self) -> Result { + Ok(js_sys::BigInt::from(*self).into()) + } +} + +impl TryIntoJsValue for bool { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_bool(*self)) + } +} + +impl TryIntoJsValue for Option { + fn try_to_js_value(&self) -> Result { + match self { + Some(v) => v.try_to_js_value(), + None => Ok(JsValue::UNDEFINED), + } + } +} + +impl TryIntoJsValue for Vec { + fn try_to_js_value(&self) -> Result { + let arr = js_sys::Array::new(); + for item in self.iter() { + arr.push(&item.try_to_js_value()?); + } + Ok(arr.into()) + } +} + +/// Macro to create a JavaScript object from key-value pairs. +#[macro_export] +macro_rules! js_obj { + ( $( $key:expr => $value:expr ),* $(,)? ) => {{ + let obj = js_sys::Object::new(); + $( + js_sys::Reflect::set( + &obj, + &wasm_bindgen::JsValue::from_str($key), + &$crate::wasm::try_into_js_value::TryIntoJsValue::try_to_js_value(&$value)? + ).map_err(|_| $crate::wasm::try_into_js_value::JsConversionError::new( + concat!("Failed to set object property: ", $key) + ))?; + )* + Ok::(obj.into()) + }}; +} + +pub use js_obj; + +// ============================================================================ +// TryIntoJsValue implementations for parser types +// ============================================================================ + +use crate::parser::{JettonTransferFields, ParsedSendAction, ParsedTransaction, TransactionType}; + +impl TryIntoJsValue for TransactionType { + fn try_to_js_value(&self) -> Result { + Ok(JsValue::from_str(self.as_str())) + } +} + +impl TryIntoJsValue for JettonTransferFields { + fn try_to_js_value(&self) -> Result { + js_obj!( + "queryId" => self.query_id, + "amount" => self.amount, + "destination" => self.destination, + "responseDestination" => self.response_destination, + "forwardTonAmount" => self.forward_ton_amount + ) + } +} + +impl TryIntoJsValue for ParsedSendAction { + fn try_to_js_value(&self) -> Result { + let obj = js_sys::Object::new(); + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("mode"), + &TryIntoJsValue::try_to_js_value(&self.mode)?, + ) + .map_err(|_| JsConversionError::new("Failed to set mode"))?; + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("destination"), + &TryIntoJsValue::try_to_js_value(&self.destination)?, + ) + .map_err(|_| JsConversionError::new("Failed to set destination"))?; + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("destinationBounceable"), + &TryIntoJsValue::try_to_js_value(&self.destination_bounceable)?, + ) + .map_err(|_| JsConversionError::new("Failed to set destinationBounceable"))?; + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("amount"), + &TryIntoJsValue::try_to_js_value(&self.amount)?, + ) + .map_err(|_| JsConversionError::new("Failed to set amount"))?; + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("bounce"), + &TryIntoJsValue::try_to_js_value(&self.bounce)?, + ) + .map_err(|_| JsConversionError::new("Failed to set bounce"))?; + + js_sys::Reflect::set( + &obj, + &JsValue::from_str("stateInit"), + &TryIntoJsValue::try_to_js_value(&self.state_init)?, + ) + .map_err(|_| JsConversionError::new("Failed to set stateInit"))?; + + if let Some(opcode) = self.body_opcode { + js_sys::Reflect::set( + &obj, + &JsValue::from_str("bodyOpcode"), + &TryIntoJsValue::try_to_js_value(&opcode)?, + ) + .map_err(|_| JsConversionError::new("Failed to set bodyOpcode"))?; + } + + if let Some(ref memo) = self.memo { + js_sys::Reflect::set( + &obj, + &JsValue::from_str("memo"), + &TryIntoJsValue::try_to_js_value(memo)?, + ) + .map_err(|_| JsConversionError::new("Failed to set memo"))?; + } + + if let Some(ref jetton) = self.jetton_transfer { + js_sys::Reflect::set( + &obj, + &JsValue::from_str("jettonTransfer"), + &TryIntoJsValue::try_to_js_value(jetton)?, + ) + .map_err(|_| JsConversionError::new("Failed to set jettonTransfer"))?; + } + + Ok(obj.into()) + } +} + +impl TryIntoJsValue for ParsedTransaction { + fn try_to_js_value(&self) -> Result { + js_obj!( + "transactionType" => self.transaction_type, + "sender" => self.sender, + "walletId" => self.wallet_id, + "seqno" => self.seqno, + "expireAt" => self.expire_at, + "signature" => self.signature, + "sendActions" => self.send_actions + ) + } +} diff --git a/packages/wasm-ton/test/address.ts b/packages/wasm-ton/test/address.ts new file mode 100644 index 00000000000..0bac164a76b --- /dev/null +++ b/packages/wasm-ton/test/address.ts @@ -0,0 +1,106 @@ +import { strict as assert } from "assert"; +import { encodeAddress, encode, decode, validate } from "../js/index.js"; + +describe("Address", () => { + const validBounceable = "EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG"; + const validNonBounceable = "UQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBX1aD"; + + describe("validate", () => { + it("should validate correct bounceable addresses", () => { + assert.ok(validate(validBounceable)); + }); + + it("should validate correct non-bounceable addresses", () => { + assert.ok(validate(validNonBounceable)); + }); + + it("should validate raw hex addresses", () => { + assert.ok(validate("0:348bcf82746945fc38541c77fdd91d4e347eac200f6f2d9fd62dc08885f0415f")); + }); + + it("should reject invalid addresses", () => { + assert.ok(!validate("randomString")); + assert.ok(!validate("0xc4173a804406a365e69dfb297ddfgsdcvf")); + assert.ok(!validate("5ne7phA48Jrvpn39AtupB8ZkCCAy8gLTfpGihZPuDqen")); + }); + }); + + describe("decode", () => { + it("should decode bounceable address", () => { + const decoded = decode(validBounceable); + assert.equal(decoded.workchainId, 0); + assert.ok(decoded.isBounceable); + assert.ok(!decoded.isTestnet); + assert.equal(decoded.addressHash.length, 32); + }); + + it("should decode non-bounceable address", () => { + const decoded = decode(validNonBounceable); + assert.equal(decoded.workchainId, 0); + assert.ok(!decoded.isBounceable); + assert.ok(!decoded.isTestnet); + }); + + it("should decode raw hex address", () => { + const decoded = decode("0:348bcf82746945fc38541c77fdd91d4e347eac200f6f2d9fd62dc08885f0415f"); + assert.equal(decoded.workchainId, 0); + assert.equal(decoded.addressHash.length, 32); + }); + }); + + describe("encode/decode roundtrip", () => { + it("should roundtrip bounceable", () => { + const decoded = decode(validBounceable); + const encoded = encode(decoded.workchainId, decoded.addressHash, true); + assert.equal(encoded, validBounceable); + }); + + it("should roundtrip non-bounceable", () => { + const decoded = decode(validNonBounceable); + const encoded = encode(decoded.workchainId, decoded.addressHash, false); + assert.equal(encoded, validNonBounceable); + }); + }); + + describe("encodeAddress", () => { + const knownPublicKey = new Uint8Array([ + 0x7d, 0x6b, 0x1a, 0x21, 0x0b, 0x18, 0x0c, 0xa1, 0x41, 0x26, 0x7c, 0xea, 0x69, 0x56, 0x8a, + 0x6a, 0x4f, 0xf2, 0xd8, 0x49, 0xda, 0x9e, 0x6f, 0x47, 0x6d, 0x04, 0x10, 0x05, 0xd4, 0x47, + 0x6c, 0x6e, + ]); + const expectedBounceable = "EQAHgNAYSdWyD3kl2RIl_oSo4lS0ECclh-FDjKETwGtSOcsT"; + const expectedNonBounceable = "UQAHgNAYSdWyD3kl2RIl_oSo4lS0ECclh-FDjKETwGtSOZbW"; + + it("should encode a public key to a bounceable address", () => { + const addr = encodeAddress(knownPublicKey, true); + assert.equal(addr, expectedBounceable); + }); + + it("should encode a public key to a non-bounceable address", () => { + const addr = encodeAddress(knownPublicKey, false); + assert.equal(addr, expectedNonBounceable); + }); + + it("should default to bounceable", () => { + const addr = encodeAddress(knownPublicKey); + assert.equal(addr, expectedBounceable); + }); + + it("should produce a valid address", () => { + const addr = encodeAddress(knownPublicKey, true); + assert.ok(validate(addr)); + }); + + it("should reject invalid public key length", () => { + assert.throws(() => encodeAddress(new Uint8Array(16)), /public key must be 32 bytes/); + }); + + it("should roundtrip with decode", () => { + const addr = encodeAddress(knownPublicKey, true); + const decoded = decode(addr); + assert.equal(decoded.workchainId, 0); + assert.ok(decoded.isBounceable); + assert.ok(!decoded.isTestnet); + }); + }); +}); diff --git a/packages/wasm-ton/test/builder.ts b/packages/wasm-ton/test/builder.ts new file mode 100644 index 00000000000..74f8c4e4ef5 --- /dev/null +++ b/packages/wasm-ton/test/builder.ts @@ -0,0 +1,247 @@ +import { strict as assert } from "assert"; +import { + buildTransaction, + parseTransaction, + TonStakingType, + TonTransactionType, +} from "../js/index.js"; +import type { BuildContext, TonIntent } from "../js/index.js"; + +const TEST_SENDER = "EQBkD52LACNxGgaoAxm5Nhs0SN6gg8hNaceNYifev88Y7qoZ"; +const TEST_VALIDATOR = "EQDr9Sq482A6ikIUh5mUUjJaBUUJBrye13CJiDB-R31_lwIq"; +const TEST_RECIPIENT = "EQA0i8-CdGnF_DhUHHf92R1ONH6sIA9vLZ_WLcCIhfBBXwtG"; +const TEST_JETTON = "UQACn6WjuRU-_ZJToT7Aswd4BnLO3vuCEZH4LUQoQNiMc3SE"; + +function makeContext(overrides: Partial = {}): BuildContext { + return { + sender: TEST_SENDER, + seqno: 0, + expireTime: 1234567890n, + walletVersion: 4, + ...overrides, + }; +} + +describe("Builder", () => { + describe("payment (native)", () => { + it("should build and parse a native transfer", () => { + const intent: TonIntent = { + type: "payment", + to: TEST_RECIPIENT, + amount: 10_000_000n, + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "Transfer"); + assert.equal(parsed.sendActions.length, 1); + assert.equal(parsed.sendActions[0].amount, 10_000_000n); + }); + + it("should build a transfer with memo", () => { + const intent: TonIntent = { + type: "payment", + to: TEST_RECIPIENT, + amount: 10_000_000n, + memo: "test memo", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "Transfer"); + assert.equal(parsed.sendActions[0].memo, "test memo"); + }); + }); + + describe("payment (token)", () => { + it("should build and parse a token transfer", () => { + const intent: TonIntent = { + type: "tokenPayment", + to: TEST_RECIPIENT, + amount: 1_000_000_000n, + jettonAddress: TEST_JETTON, + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "TokenTransfer"); + assert.ok(parsed.sendActions[0].jettonTransfer); + assert.equal(parsed.sendActions[0].jettonTransfer?.amount, 1_000_000_000n); + }); + }); + + describe("fillNonce", () => { + it("should build a native fill nonce (self-send 1 nanoTON)", () => { + const intent: TonIntent = { + type: "fillNonce", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "Transfer"); + assert.equal(parsed.sendActions[0].amount, 1n); + }); + + it("should build a token fill nonce", () => { + const intent: TonIntent = { + type: "fillNonce", + isToken: true, + jettonAddress: TEST_JETTON, + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "TokenTransfer"); + }); + }); + + describe("consolidate", () => { + it("should build a native consolidate", () => { + const intent: TonIntent = { + type: "consolidate", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "Transfer"); + }); + + it("should reject consolidate for wallet version 1", () => { + const intent: TonIntent = { + type: "consolidate", + }; + + assert.throws(() => { + buildTransaction(intent, makeContext({ walletVersion: 1 })); + }); + }); + }); + + describe("delegate", () => { + it("should build TonWhales deposit", () => { + const intent: TonIntent = { + type: "delegate", + amount: 10_000_000_000n, + validatorAddress: TEST_VALIDATOR, + stakingType: "TonWhales", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "WhalesDeposit"); + }); + + it("should build SingleNominator delegate", () => { + const intent: TonIntent = { + type: "delegate", + amount: 10_000_000_000n, + validatorAddress: TEST_VALIDATOR, + stakingType: "SingleNominator", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + // SingleNominator delegate is just a plain transfer + assert.equal(parsed.transactionType, "Transfer"); + }); + + it("should build MultiNominator delegate", () => { + const intent: TonIntent = { + type: "delegate", + amount: 10_000_000_000n, + validatorAddress: TEST_VALIDATOR, + stakingType: "MultiNominator", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + // MultiNominator delegate has memo 'd' + assert.equal(parsed.transactionType, "Transfer"); + assert.equal(parsed.sendActions[0].memo, "d"); + }); + }); + + describe("undelegate", () => { + it("should build TonWhales withdraw", () => { + const intent: TonIntent = { + type: "undelegate", + amount: 10_000_000_000n, + validatorAddress: TEST_VALIDATOR, + stakingType: "TonWhales", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "WhalesWithdraw"); + }); + + it("should build TonWhales full withdraw (amount=0)", () => { + const intent: TonIntent = { + type: "undelegate", + amount: 0n, + validatorAddress: TEST_VALIDATOR, + stakingType: "TonWhales", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "WhalesWithdraw"); + }); + + it("should build SingleNominator withdraw", () => { + const intent: TonIntent = { + type: "undelegate", + amount: 123_400_000n, + validatorAddress: TEST_VALIDATOR, + stakingType: "SingleNominator", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + assert.equal(parsed.transactionType, "SingleNominatorWithdraw"); + }); + + it("should build MultiNominator withdraw", () => { + const intent: TonIntent = { + type: "undelegate", + validatorAddress: TEST_VALIDATOR, + stakingType: "MultiNominator", + }; + + const tx = buildTransaction(intent, makeContext()); + const parsed = parseTransaction(tx); + + // MultiNominator withdraw has memo 'w' + assert.equal(parsed.transactionType, "Transfer"); + assert.equal(parsed.sendActions[0].memo, "w"); + }); + }); + + describe("as const values", () => { + it("TonStakingType should contain all staking types", () => { + assert.ok(TonStakingType.includes("TonWhales")); + assert.ok(TonStakingType.includes("SingleNominator")); + assert.ok(TonStakingType.includes("MultiNominator")); + assert.equal(TonStakingType.length, 3); + }); + + it("TonTransactionType should contain all transaction types", () => { + assert.ok(TonTransactionType.includes("Transfer")); + assert.ok(TonTransactionType.includes("TokenTransfer")); + assert.ok(TonTransactionType.includes("WhalesDeposit")); + assert.equal(TonTransactionType.length, 8); + }); + }); +}); diff --git a/packages/wasm-ton/test/transaction.ts b/packages/wasm-ton/test/transaction.ts new file mode 100644 index 00000000000..434e128bff2 --- /dev/null +++ b/packages/wasm-ton/test/transaction.ts @@ -0,0 +1,64 @@ +import { strict as assert } from "assert"; +import { Transaction, parseTransaction } from "../js/index.js"; + +// Test fixtures from BitGoJS sdk-coin-ton +const signedSendTx = + "te6cckEBAgEAqQAB4YgBJAxo7vqHF++LJ4bC/kJ8A1uVRskrKlrKJZ8rIB0tF+gCadlSX+hPo2mmhZyi0p3zTVUYVRkcmrCm97cSUFSa2vzvCArM3APg+ww92r3IcklNjnzfKOgysJVQXiCvj9SAaU1NGLsotvRwAAAAMAAcAQBmQgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr5zEtAAAAAAAAAAAAAAAAAAAdfZO7w=="; + +const whalesDepositTx = + "te6cckEBAgEAvAAB4YgAyB87FgBG4jQNUAYzcmw2aJG9QQeQmtOPGsRPvX+eMdwFf6OLyGMsPoPXNPLUqMoUZTIrdu2maNNUK52q+Wa0BJhNq9e/qHXYsF9xU5TYbOsZt1EBGJf1GpkumdgXj0/4CU1NGLtKFdHwAAAC4AAcAQCLYgB1+pVcebAdRSEKQ8zKKRktAqKEg15Pa7hExBg/I76/y6gSoF8gAAAAAAAAAAAAAAAAAAB7zR/vAAAAAGlCugJDuaygCErRw2Y="; + +const singleNominatorWithdrawTx = + "te6cckECGAEAA8MAAuGIADZN0H0n1tz6xkYgWqJSRmkURKYajjEgXeawBo9cifPIGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACmpoxdJlgLSAAAAAAADgEXAgE0AhYBFP8A9KQT9LzyyAsDAgEgBBECAUgFCALm0AHQ0wMhcbCSXwTgItdJwSCSXwTgAtMfIYIQcGx1Z70ighBkc3RyvbCSXwXgA/pAMCD6RAHIygfL/8nQ7UTQgQFA1yH0BDBcgQEI9ApvoTGzkl8H4AXTP8glghBwbHVnupI4MOMNA4IQZHN0crqSXwbjDQYHAHgB+gD0BDD4J28iMFAKoSG+8uBQghBwbHVngx6xcIAYUATLBSbPFlj6Ahn0AMtpF8sfUmDLPyDJgED7AAYAilAEgQEI9Fkw7UTQgQFA1yDIAc8W9ADJ7VQBcrCOI4IQZHN0coMesXCAGFAFywVQA88WI/oCE8tqyx/LP8mAQPsAkl8D4gIBIAkQAgEgCg8CAVgLDAA9sp37UTQgQFA1yH0BDACyMoHy//J0AGBAQj0Cm+hMYAIBIA0OABmtznaiaEAga5Drhf/AABmvHfaiaEAQa5DrhY/AABG4yX7UTQ1wsfgAWb0kK29qJoQICga5D6AhhHDUCAhHpJN9KZEM5pA+n/mDeBKAG3gQFImHFZ8xhAT48oMI1xgg0x/TH9MfAvgju/Jk7UTQ0x/TH9P/9ATRUUO68qFRUbryogX5AVQQZPkQ8qP4ACSkyMsfUkDLH1Iwy/9SEPQAye1U+A8B0wchwACfbFGTINdKltMH1AL7AOgw4CHAAeMAIcAC4wABwAORMOMNA6TIyx8Syx/L/xITFBUAbtIH+gDU1CL5AAXIygcVy//J0Hd0gBjIywXLAiLPFlAF+gIUy2sSzMzJc/sAyEAUgQEI9FHypwIAcIEBCNcY+gDTP8hUIEeBAQj0UfKnghBub3RlcHSAGMjLBcsCUAbPFlAE+gIUy2oSyx/LP8lz+wACAGyBAQjXGPoA0z8wUiSBAQj0WfKnghBkc3RycHSAGMjLBcsCUAXPFlAD+gITy2rLHxLLP8lz+wAACvQAye1UAFEAAAAAKamjF8DDudwJkyEh7jUbJEjFCjriVxsSlRJFyF872V1eegb4QACPQgAaRefBOjTi/hwqDjv+7I6nGj9WEAe3ls/rFuBEQvggr6A613oAAAAAAAAAAAAAAAAAAAAAEAAAAAAAAAAAAHA0/PoUC5EIEyWuPg=="; + +function fromBase64(b64: string): Transaction { + return Transaction.fromBytes(Buffer.from(b64, "base64")); +} + +describe("Transaction", () => { + describe("fromBytes", () => { + it("should deserialize a signed send transaction", () => { + const tx = fromBase64(signedSendTx); + assert.ok(tx); + assert.ok(tx.id); + assert.ok(tx.destination); + }); + + it("should get signable payload", () => { + const tx = fromBase64(signedSendTx); + const payload = tx.signablePayload(); + assert.equal(payload.length, 32); + }); + + it("should serialize to broadcast format", () => { + const tx = fromBase64(signedSendTx); + const broadcast = tx.toBroadcastFormat(); + assert.ok(broadcast.length > 0); + }); + }); + + describe("parseTransaction", () => { + it("should parse a transfer transaction", () => { + const tx = fromBase64(signedSendTx); + const parsed = parseTransaction(tx); + assert.equal(parsed.transactionType, "Transfer"); + assert.ok(parsed.sendActions.length > 0); + assert.ok(parsed.sender); + assert.ok(parsed.signature); + }); + + it("should parse a whales deposit transaction", () => { + const tx = fromBase64(whalesDepositTx); + const parsed = parseTransaction(tx); + assert.equal(parsed.transactionType, "WhalesDeposit"); + assert.ok(parsed.sendActions.length > 0); + }); + + it("should parse a single nominator withdraw transaction", () => { + const tx = fromBase64(singleNominatorWithdrawTx); + const parsed = parseTransaction(tx); + assert.equal(parsed.transactionType, "SingleNominatorWithdraw"); + assert.ok(parsed.sendActions.length > 0); + }); + }); +}); diff --git a/packages/wasm-ton/tsconfig.cjs.json b/packages/wasm-ton/tsconfig.cjs.json new file mode 100644 index 00000000000..390db86d99f --- /dev/null +++ b/packages/wasm-ton/tsconfig.cjs.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "CommonJS", + "moduleResolution": "node", + "rootDir": ".", + "outDir": "./dist/cjs" + }, + "exclude": ["node_modules", "./js/wasm/**/*", "test/**/*"] +} diff --git a/packages/wasm-ton/tsconfig.json b/packages/wasm-ton/tsconfig.json new file mode 100644 index 00000000000..499b65c4266 --- /dev/null +++ b/packages/wasm-ton/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "ES2022", + "target": "ES2022", + "moduleResolution": "bundler", + "esModuleInterop": true, + "allowJs": true, + "skipLibCheck": true, + "declaration": true, + "composite": true, + "rootDir": ".", + "outDir": "./dist/esm", + "noUnusedLocals": true, + "noUnusedParameters": true + }, + "include": ["./js/**/*.ts"], + "exclude": ["node_modules", "./js/wasm/**/*", "test/**/*"] +} diff --git a/packages/wasm-ton/tsconfig.test.json b/packages/wasm-ton/tsconfig.test.json new file mode 100644 index 00000000000..7ec46d32502 --- /dev/null +++ b/packages/wasm-ton/tsconfig.test.json @@ -0,0 +1,9 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "types": ["node", "mocha"], + "noEmit": true + }, + "include": ["./js/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "./js/wasm/**/*"] +}