diff --git a/guidebook/toncoin-processing/.env.example b/guidebook/toncoin-processing/.env.example new file mode 100644 index 0000000..a10ea24 --- /dev/null +++ b/guidebook/toncoin-processing/.env.example @@ -0,0 +1,15 @@ +# Toncoin Payment Processing Configuration + +# Network Configuration +# Set to "false" for mainnet, leave as "true" or omit for testnet +IS_TESTNET=true + +# TonCenter API Key +# Get your API key at https://toncenter.com (mainnet) or https://testnet.toncenter.com (testnet) +# Or run your own API: https://github.com/toncenter/ton-http-api +API_KEY=your_api_key_here + +# Wallet Address +# Your wallet address that will receive deposits (for single-wallet examples) +# Or your HOT wallet address (for multi-wallet examples) +WALLET_ADDRESS=UQB22lH8P_P2OitCe8UYRxpuDF5GVqCfYTL7PDz3OzbuHebu diff --git a/guidebook/toncoin-processing/.gitignore b/guidebook/toncoin-processing/.gitignore new file mode 100644 index 0000000..cf94e93 --- /dev/null +++ b/guidebook/toncoin-processing/.gitignore @@ -0,0 +1,23 @@ +node_modules +temp +build +dist +.DS_Store +package.ts + +# VS Code +.vscode/* +.history/ +*.vsix + +# IDEA files +.idea + +# VIM +Session.vim +.vim/ + +# Other private editor folders +.nvim/ +.emacs/ +.helix/ diff --git a/guidebook/toncoin-processing/.prettierignore b/guidebook/toncoin-processing/.prettierignore new file mode 100644 index 0000000..378eac2 --- /dev/null +++ b/guidebook/toncoin-processing/.prettierignore @@ -0,0 +1 @@ +build diff --git a/guidebook/toncoin-processing/.prettierrc b/guidebook/toncoin-processing/.prettierrc new file mode 100644 index 0000000..24a6660 --- /dev/null +++ b/guidebook/toncoin-processing/.prettierrc @@ -0,0 +1,7 @@ +{ + "printWidth": 120, + "tabWidth": 4, + "singleQuote": true, + "bracketSpacing": true, + "semi": true +} diff --git a/guidebook/toncoin-processing/README.md b/guidebook/toncoin-processing/README.md new file mode 100644 index 0000000..af4660b --- /dev/null +++ b/guidebook/toncoin-processing/README.md @@ -0,0 +1,60 @@ +# Toncoin Payment Processing Examples + +Educational TypeScript examples demonstrating Toncoin payment processing on the TON blockchain. + +## Libraries Used + +- [@ton/ton](https://github.com/ton-org/ton) - High-level TON blockchain API client +- [@ton/core](https://github.com/ton-org/ton-core) - Core primitives for TON blockchain +- [@ton/crypto](https://github.com/ton-org/ton-crypto) - Cryptographic primitives for TON + +## Examples + +### 1. Single Wallet with Invoices (`src/deposits/invoices.ts`) + +Demonstrates accepting Toncoin deposits to a single wallet using unique text comments (UUIDs) to identify each payment. + +**Use case**: Payment processing where each payment is tracked by a unique identifier in the transaction comment. + +### 2. Multi-Wallet Deposits (`src/deposits/unique-addresses.ts`) + +Demonstrates accepting Toncoin deposits where each user has their own unique deposit wallet that forwards funds to a master HOT wallet. + +**Use case**: Exchange or service where users need permanent deposit addresses. + +## Setup + +1. Install dependencies: +```bash +npm install +``` + +2. Configure your environment: + - Copy `.env.example` to `.env` + - Set your API key and wallet address + - Choose mainnet or testnet + +3. Run an example: +```bash +# Single wallet invoices example +npm start + +# Multi-wallet example +npm run start:unique +``` + +## Development Scripts + +- `npm start` - Run the single-wallet invoices example +- `npm run start:unique` - Run the multi-wallet deposits example +- `npm run build` - Type-check the project (does not produce executable output) +- `npm run format` - Format code with Prettier + +## ⚠️ Educational Use Only + +These examples are for learning purposes. Do not deploy to production without: +- Thorough security review +- Proper error handling +- Database persistence +- Monitoring and alerting +- Rate limiting and retry strategies diff --git a/guidebook/toncoin-processing/package-lock.json b/guidebook/toncoin-processing/package-lock.json new file mode 100644 index 0000000..261fdd4 --- /dev/null +++ b/guidebook/toncoin-processing/package-lock.json @@ -0,0 +1,627 @@ +{ + "name": "toncoin-processing", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "toncoin-processing", + "version": "1.0.0", + "dependencies": { + "@ton/core": "^0.62.0", + "@ton/crypto": "^3.3.0", + "@ton/ton": "^15.4.0" + }, + "devDependencies": { + "@types/node": "^22.17.2", + "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@ton/core": { + "version": "0.62.0", + "resolved": "https://registry.npmjs.org/@ton/core/-/core-0.62.0.tgz", + "integrity": "sha512-GCYlzzx11rSESKkiHvNy9tL8zWth+ZtUbvV29WH478FvBp8xTw24AyoigwXWNV+OLCAcnwlGhZpTpxjD3wzCwA==", + "license": "MIT", + "dependencies": { + "symbol.inspect": "1.0.1" + }, + "peerDependencies": { + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@ton/crypto": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@ton/crypto/-/crypto-3.3.0.tgz", + "integrity": "sha512-/A6CYGgA/H36OZ9BbTaGerKtzWp50rg67ZCH2oIjV1NcrBaCK9Z343M+CxedvM7Haf3f/Ee9EhxyeTp0GKMUpA==", + "license": "MIT", + "dependencies": { + "@ton/crypto-primitives": "2.1.0", + "jssha": "3.2.0", + "tweetnacl": "1.0.3" + } + }, + "node_modules/@ton/crypto-primitives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@ton/crypto-primitives/-/crypto-primitives-2.1.0.tgz", + "integrity": "sha512-PQesoyPgqyI6vzYtCXw4/ZzevePc4VGcJtFwf08v10OevVJHVfW238KBdpj1kEDQkxWLeuNHEpTECNFKnP6tow==", + "license": "MIT", + "dependencies": { + "jssha": "3.2.0" + } + }, + "node_modules/@ton/ton": { + "version": "15.4.0", + "resolved": "https://registry.npmjs.org/@ton/ton/-/ton-15.4.0.tgz", + "integrity": "sha512-f19y2Rez88KZK+lv3CT3ghXi07LcToJtJhlgRSfK+3GzjdIcoW/wbmXG1ffi6fkc8W2LO5z6Q3gZaIEvNGnz6w==", + "license": "MIT", + "dependencies": { + "axios": "^1.6.7", + "dataloader": "^2.0.0", + "symbol.inspect": "1.0.1", + "teslabot": "^1.3.0", + "zod": "^3.21.4" + }, + "peerDependencies": { + "@ton/core": ">=0.62.0 <1.0.0", + "@ton/crypto": ">=3.2.0" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.1.tgz", + "integrity": "sha512-LCCV0HdSZZZb34qifBsyWlUmok6W7ouER+oQIGBScS8EsZsQbrtFTUrDX4hOl+CS6p7cnNC4td+qrSVGSCTUfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/axios": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.2.tgz", + "integrity": "sha512-VPk9ebNqPcy5lRGuSlKx752IlDatOjT9paPlm8A7yOuW2Fbvp4X3JznJtT4f0GzGLLiWE9W8onz51SqLYwzGaA==", + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.6", + "form-data": "^4.0.4", + "proxy-from-env": "^1.1.0" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/dataloader": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/dataloader/-/dataloader-2.2.3.tgz", + "integrity": "sha512-y2krtASINtPFS1rSDjacrFgn1dcUuoREVabwlOGOe4SdxenREqwjwjElAdwvbGM7kgZz9a3KVicWR7vcz8rnzA==", + "license": "MIT" + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", + "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jssha": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jssha/-/jssha-3.2.0.tgz", + "integrity": "sha512-QuruyBENDWdN4tZwJbQq7/eAK85FqrI4oDbXjy5IBhYD+2pTJyBUWZe8ctWaCkrV0gy6AaelgOZZBMeswEa/6Q==", + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "license": "MIT" + }, + "node_modules/symbol.inspect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol.inspect/-/symbol.inspect-1.0.1.tgz", + "integrity": "sha512-YQSL4duoHmLhsTD1Pw8RW6TZ5MaTX5rXJnqacJottr2P2LZBF/Yvrc3ku4NUpMOm8aM0KOCqM+UAkMA5HWQCzQ==", + "license": "ISC" + }, + "node_modules/teslabot": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/teslabot/-/teslabot-1.5.0.tgz", + "integrity": "sha512-e2MmELhCgrgZEGo7PQu/6bmYG36IDH+YrBI1iGm6jovXkeDIGa3pZ2WSqRjzkuw2vt1EqfkZoV5GpXgqL8QJVg==", + "license": "MIT" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==", + "license": "Unlicense" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/guidebook/toncoin-processing/package.json b/guidebook/toncoin-processing/package.json new file mode 100644 index 0000000..3ea9f90 --- /dev/null +++ b/guidebook/toncoin-processing/package.json @@ -0,0 +1,22 @@ +{ + "name": "toncoin-processing", + "version": "1.0.0", + "description": "Toncoin Payment Processing Examples with TypeScript", + "scripts": { + "start": "ts-node src/deposits/invoices.ts", + "start:unique": "ts-node src/deposits/unique-addresses.ts", + "build": "tsc", + "format": "prettier --write \"src/**/*.ts\"" + }, + "dependencies": { + "@ton/core": "^0.62.0", + "@ton/ton": "^15.4.0", + "@ton/crypto": "^3.3.0" + }, + "devDependencies": { + "@types/node": "^22.17.2", + "prettier": "^3.6.2", + "ts-node": "^10.9.2", + "typescript": "^5.9.2" + } +} diff --git a/guidebook/toncoin-processing/src/deposits/invoices.ts b/guidebook/toncoin-processing/src/deposits/invoices.ts new file mode 100644 index 0000000..72adbbd --- /dev/null +++ b/guidebook/toncoin-processing/src/deposits/invoices.ts @@ -0,0 +1,198 @@ +/** + * Single-wallet deposits with invoice tracking + * + * This example demonstrates how to accept Toncoin deposits to a single wallet + * where each payment is identified by a unique text comment (UUID). + * + * Flow: + * 1. Generate a wallet address once for your service + * 2. On each payment request: + * a. Generate a unique UUID for the payment + * b. Store the payment record in your database with expected amount and UUID + * c. Provide the payment details to the user (wallet address, amount, UUID) + * 3. The backend monitors all transactions to the wallet address + * 4. On detecting an incoming transaction: + * a. Extract the UUID from the transaction comment + * b. Lookup the payment in the database + * c. Verify the payment hasn't been processed + * d. Verify the amount matches expectations + * e. Mark as processed and credit the user's account + */ + +import { TonClient, Transaction } from '@ton/ton'; +import { Cell, fromNano } from '@ton/core'; +import { createAccountSubscription } from '../subscription/account-subscription'; +import { loadConfig } from '../utils/config'; + +// Resume cursor - load last processed transaction LT + hash from your database +// If undefined, the subscriber will process recent transactions returned by the node +const LAST_PROCESSED_LT: string | undefined = undefined; +const LAST_PROCESSED_HASH: string | undefined = undefined; + +interface DepositInfo { + readonly amount: bigint; + readonly senderAddress: string; + readonly comment: string | undefined; + readonly txHash: string; + readonly txLt: string; + readonly timestamp: Date; +} + +/** + * Parses message body to extract text comment if present + * @param body - Message body cell + * @returns Text comment or undefined if not present or parsing fails + */ +function parseComment(body: Cell): string | undefined { + try { + const slice = body.beginParse(); + const op = slice.loadUint(32); + + // op === 0 indicates UTF-8 text payload + if (op === 0) { + return slice.loadStringTail(); + } + } catch (error) { + if (error instanceof Error && error.message.includes('slice')) { + // Expected: insufficient bits or malformed cell + return undefined; + } + // Unexpected error - log and rethrow for debugging + console.error('Unexpected error parsing comment:', error); + throw error; + } + + return undefined; +} + +/** + * Extracts deposit information from a transaction + * @param tx - Transaction to process + * @returns Deposit information or null if transaction should be skipped + */ +function extractDepositInfo(tx: Transaction): DepositInfo | null { + // Check if this is an incoming transaction with a source address + const inMessage = tx.inMessage; + if (!inMessage || inMessage.info.type !== 'internal' || !inMessage.info.src) { + return null; + } + + // CRITICAL: Always verify there are no outgoing messages + // This ensures the coins didn't bounce back due to an error + if (tx.outMessages.size > 0) { + return null; + } + + const comment = inMessage.body ? parseComment(inMessage.body) : undefined; + + return { + amount: inMessage.info.value.coins, + senderAddress: inMessage.info.src.toString(), + comment, + txHash: tx.hash().toString('base64'), + txLt: tx.lt.toString(), + timestamp: new Date(tx.now * 1000), + }; +} + +/** + * Transaction handler - processes each transaction + * @param tx - Transaction to process + */ +async function onTransaction(tx: Transaction): Promise { + const depositInfo = extractDepositInfo(tx); + + if (!depositInfo) { + return; + } + + if (!depositInfo.comment) { + console.log('Transaction without comment - skipping'); + return; + } + + console.log('\n=== New Deposit Detected ==='); + console.log(`Amount: ${fromNano(depositInfo.amount)} TON`); + console.log(`From: ${depositInfo.senderAddress}`); + console.log(`Comment/UUID: ${depositInfo.comment}`); + console.log(`Transaction Hash: ${depositInfo.txHash}`); + console.log(`Transaction LT: ${depositInfo.txLt}`); + console.log(`Timestamp: ${depositInfo.timestamp.toISOString()}`); + console.log('===========================\n'); + + // In production: + // 1. Find the payment in your database by the UUID (comment) + // 2. Verify that the payment hasn't been processed yet + // 3. Check that the amount matches what was expected + // 4. Mark the payment as processed in your database + // 5. Credit the user's account + // + // Example with database: + // const payment = await db.findPaymentByUUID(depositInfo.comment); + // if (!payment) { + // console.log('Unknown payment UUID'); + // return; + // } + // if (payment.processed) { + // console.log('Payment already processed'); + // return; + // } + // if (payment.expectedAmount !== depositInfo.amount) { + // console.log('Amount mismatch'); + // return; + // } + // await db.markPaymentAsProcessed(payment.id, depositInfo.txHash); + // await db.creditUserAccount(payment.userId, depositInfo.amount); +} + +/** + * Main function - sets up the deposit monitoring + */ +async function main(): Promise { + const config = loadConfig(); + + console.log('Starting deposit monitoring...'); + console.log(`Network: ${config.isTestnet ? 'TESTNET' : 'MAINNET'}`); + console.log(`Wallet: ${config.walletAddress}`); + if (LAST_PROCESSED_LT && LAST_PROCESSED_HASH) { + console.log(`Resume from lt:${LAST_PROCESSED_LT} hash:${LAST_PROCESSED_HASH}`); + } else { + console.log('Resume from latest transactions (no cursor saved)'); + } + console.log(''); + + // Initialize TON client + const client = new TonClient({ + endpoint: config.apiUrl, + apiKey: config.apiKey, + }); + + // Create and start the subscription + const accountSub = createAccountSubscription(client, config.walletAddress, onTransaction, { + limit: 10, + lastLt: LAST_PROCESSED_LT, + lastHash: LAST_PROCESSED_HASH, + }); + + const unsubscribe = await accountSub.start(10_000); // Poll every 10 seconds + + console.log('Monitoring started. Press Ctrl+C to stop.\n'); + + // Handle graceful shutdown + // Note: This won't catch SIGKILL or power loss. In production, + // persist the cursor after each processed transaction. + process.on('SIGINT', () => { + console.log('\nStopping deposit monitoring...'); + const cursor = accountSub.getLastProcessed(); + console.log('Last processed cursor:', cursor); + console.log('Persist this cursor to your database for safe resumption.'); + unsubscribe(); + // process.exit(0) is not needed - Node will exit naturally after cleanup + }); +} + +// Run the application +main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); +}); diff --git a/guidebook/toncoin-processing/src/deposits/unique-addresses.ts b/guidebook/toncoin-processing/src/deposits/unique-addresses.ts new file mode 100644 index 0000000..c5b2bb9 --- /dev/null +++ b/guidebook/toncoin-processing/src/deposits/unique-addresses.ts @@ -0,0 +1,318 @@ +/** + * Multi-wallet deposits with unique addresses per user + * + * This example demonstrates how to accept Toncoin deposits where each user has their own deposit wallet. + * All deposits are automatically forwarded to a single HOT wallet. + * + * Flow: + * 1. Generate a HOT wallet once (master wallet that receives all deposits) + * 2. For each user, generate a unique deposit wallet and store keys in your database + * 3. User sends Toncoin to their deposit wallet address + * 4. The backend monitors all blockchain blocks for transactions + * 5. When a deposit is detected on a user's wallet: + * - Verify the transaction + * - Forward all balance to HOT wallet + * - Destroy the deposit wallet to avoid storage fees + * - Credit the user's account + * + * Advantages of this approach: + * - No need for text comments/memos + * - Each user has a permanent, unique deposit address + * - Easier to track deposits per user + * + * Note: Deposit wallets are destroyed after each transfer to avoid storage fees. + * They can be redeployed automatically on the next deposit. + */ + +import { TonClient, WalletContractV5R1, Transaction } from '@ton/ton'; +import { Address, internal, fromNano, SendMode } from '@ton/core'; +import { mnemonicNew, mnemonicToPrivateKey } from '@ton/crypto'; +import { createBlockSubscription } from '../subscription/block-subscription'; +import { loadConfig } from '../utils/config'; + +/** + * Represents a user's deposit wallet + * In production, store this data in a real database (PostgreSQL, MongoDB, etc.) + */ +interface DepositWallet { + readonly userId: number; + readonly address: string; + readonly publicKey: Buffer; + readonly secretKey: Buffer; // Store securely in production (HSM, KMS, etc.) +} + +/** + * In-memory storage for deposit wallets + * Maps wallet address (non-bounceable string) -> DepositWallet + * In production, replace this with actual database queries + */ +type WalletAddressToDepositWallet = ReadonlyMap; + +/** + * Creates a new deposit wallet for a user + * @param userId - Unique user identifier + * @param networkGlobalId - Network ID (-3 for testnet, -239 for mainnet) + * @returns DepositWallet record to persist in your database + */ +async function createDepositWallet(userId: number, networkGlobalId: number): Promise { + // Generate new key pair + const mnemonic = await mnemonicNew(); + const keyPair = await mnemonicToPrivateKey(mnemonic); + + // Create wallet contract V5R1 + const wallet = WalletContractV5R1.create({ + walletId: { + networkGlobalId, + }, + publicKey: keyPair.publicKey, + workchain: 0, + }); + const address = wallet.address.toString({ bounceable: false }); + + const depositWallet: DepositWallet = { + userId, + address, + publicKey: keyPair.publicKey, + secretKey: keyPair.secretKey, + }; + + console.log(`[DB] Created deposit wallet for user ${userId}: ${address}`); + return depositWallet; +} + +/** + * Looks up a deposit wallet by its address + * @param walletMap - Map of wallet addresses to deposit wallet records + * @param address - Wallet address to look up + * @returns DepositWallet or undefined if not found + */ +function getDepositWallet(walletMap: WalletAddressToDepositWallet, address: string): DepositWallet | undefined { + try { + const addr = Address.parse(address); + const normalized = addr.toString({ bounceable: false }); + return walletMap.get(normalized); + } catch { + return undefined; + } +} + +interface DepositInfo { + readonly amount: bigint; + readonly depositAddress: string; + readonly sender: string; + readonly txHash: string; + readonly txLt: string; + readonly timestamp: Date; + readonly block: { readonly workchain: number; readonly shard: string; readonly seqno: number }; +} + +/** + * Extracts deposit information from a transaction + */ +function extractDepositInfo( + tx: Transaction, + block: { workchain: number; shard: string; seqno: number }, +): DepositInfo | null { + const inMessage = tx.inMessage; + if (!inMessage || inMessage.info.type !== 'internal' || !inMessage.info.src) { + return null; + } + + return { + amount: inMessage.info.value.coins, + depositAddress: inMessage.info.dest.toString({ bounceable: false }), + sender: inMessage.info.src.toString({ bounceable: false }), + txHash: tx.hash().toString('base64'), + txLt: tx.lt.toString(), + timestamp: new Date(tx.now * 1000), + block, + }; +} + +/** + * Forwards all balance from deposit wallet to HOT wallet + */ +async function forwardToHotWallet( + client: TonClient, + depositWallet: DepositWallet, + hotWalletAddress: string, + networkGlobalId: number, +): Promise { + const wallet = WalletContractV5R1.create({ + walletId: { + networkGlobalId, + }, + publicKey: depositWallet.publicKey, + workchain: 0, + }); + + const contract = client.open(wallet); + const balance = await contract.getBalance(); + + if (balance === 0n) { + console.log(`Deposit wallet ${depositWallet.address} has zero balance, skipping`); + return 0n; + } + + console.log(`Forwarding ${fromNano(balance)} TON to HOT wallet...`); + + const seqno = await contract.getSeqno(); + + // SendMode.CARRY_ALL_REMAINING_BALANCE (128) + SendMode.DESTROY_ACCOUNT_IF_ZERO (32) + // forwards the full balance and removes the wallet to avoid future storage fees + await contract.sendTransfer({ + seqno, + secretKey: depositWallet.secretKey, + messages: [ + internal({ + to: hotWalletAddress, + value: 0n, // Will be replaced by CARRY_ALL_REMAINING_BALANCE + bounce: false, + }), + ], + sendMode: SendMode.CARRY_ALL_REMAINING_BALANCE + SendMode.DESTROY_ACCOUNT_IF_ZERO, + }); + + // In production: monitor this transfer and retry/validate if it fails + console.log(`✓ Transfer sent from ${depositWallet.address} to HOT wallet`); + console.log(` User: ${depositWallet.userId}`); + console.log(` Amount: ${fromNano(balance)} TON`); + + return balance; +} + +/** + * Processes a deposit transaction + */ +async function processDeposit( + client: TonClient, + tx: Transaction, + depositWallet: DepositWallet, + block: { workchain: number; shard: string; seqno: number }, + hotWalletAddress: string, + networkGlobalId: number, +): Promise { + // Skip outgoing transactions (bounced messages) + if (tx.outMessages.size > 0) { + return; + } + + const depositInfo = extractDepositInfo(tx, block); + if (!depositInfo) { + return; + } + + console.log('\n=== Deposit Detected ==='); + console.log(`Block: ${depositInfo.block.workchain}:${depositInfo.block.shard}:${depositInfo.block.seqno}`); + console.log(`Deposit wallet: ${depositInfo.depositAddress}`); + console.log(`User: ${depositWallet.userId}`); + console.log(`Amount: ${fromNano(depositInfo.amount)} TON`); + console.log(`Sender: ${depositInfo.sender}`); + console.log(`Transaction hash: ${depositInfo.txHash}`); + console.log(`Transaction LT: ${depositInfo.txLt}`); + console.log(`Timestamp: ${depositInfo.timestamp.toISOString()}`); + console.log('======================\n'); + + try { + await forwardToHotWallet(client, depositWallet, hotWalletAddress, networkGlobalId); + + // In production: + // 1. Mark transaction as processed in your database + // 2. Credit user's account + // 3. Send notification to user + console.log(`✅ Deposit processed successfully for user ${depositWallet.userId}\n`); + } catch (error) { + console.error('Error forwarding to HOT wallet:', error); + // In production: implement retry logic with exponential backoff + } +} + +/** + * Main function + */ +async function main(): Promise { + const config = loadConfig(); + const hotWalletAddress = config.walletAddress; + const networkGlobalId = config.isTestnet ? -3 : -239; + + console.log('=== Toncoin Multi-Wallet Deposits Demo ===\n'); + console.log(`Network: ${config.isTestnet ? 'TESTNET' : 'MAINNET'}`); + console.log(`HOT Wallet: ${hotWalletAddress}\n`); + + const client = new TonClient({ + endpoint: config.apiUrl, + apiKey: config.apiKey, + }); + + // Create example deposit wallets + // In production, create these when users requests deposit address + console.log('Creating example deposit wallets...\n'); + const wallet1 = await createDepositWallet(101, networkGlobalId); + const wallet2 = await createDepositWallet(102, networkGlobalId); + const wallet3 = await createDepositWallet(103, networkGlobalId); + + // Build in-memory map (in production, query from database) + const walletMap: WalletAddressToDepositWallet = new Map([ + [wallet1.address, wallet1], + [wallet2.address, wallet2], + [wallet3.address, wallet3], + ]); + + console.log('\n=== Deposit Wallets ==='); + for (const wallet of walletMap.values()) { + console.log(`User ${wallet.userId}: ${wallet.address}`); + } + console.log('=======================\n'); + + console.log('Users can now send Toncoin to their deposit addresses.'); + console.log('Funds will be automatically forwarded to the HOT wallet.\n'); + + // Get starting block + const masterchainInfo = await client.getMasterchainInfo(); + const startBlock = masterchainInfo.latestSeqno; + console.log(`Starting from masterchain block ${startBlock}\n`); + + // Transaction handler + const handleTransaction = async ( + tx: Transaction, + block: { workchain: number; shard: string; seqno: number }, + ): Promise => { + const inMessage = tx.inMessage; + if (!inMessage || inMessage.info.type !== 'internal') { + return; + } + + const destination = inMessage.info.dest.toString({ bounceable: false }); + const depositWallet = getDepositWallet(walletMap, destination); + + if (!depositWallet) { + return; + } + + await processDeposit(client, tx, depositWallet, block, hotWalletAddress, networkGlobalId); + }; + + // Start block subscription + const blockSub = createBlockSubscription(client, startBlock, handleTransaction); + const unsubscribe = await blockSub.start(1000); + + console.log('Monitoring blockchain for deposits...'); + console.log('Press Ctrl+C to stop\n'); + + // Handle graceful shutdown + process.on('SIGINT', () => { + console.log('\n\nShutting down...'); + unsubscribe(); + console.log(`Last processed block: ${blockSub.getLastProcessedBlock()}`); + console.log('Persist this block number to resume safely after restart.'); + console.log('Goodbye!'); + }); +} + +// Run the application +if (require.main === module) { + main().catch((error) => { + console.error('Fatal error:', error); + process.exit(1); + }); +} diff --git a/guidebook/toncoin-processing/src/subscription/account-subscription.ts b/guidebook/toncoin-processing/src/subscription/account-subscription.ts new file mode 100644 index 0000000..809a0f7 --- /dev/null +++ b/guidebook/toncoin-processing/src/subscription/account-subscription.ts @@ -0,0 +1,130 @@ +/** + * Account subscription (functional API) for monitoring wallet transactions. + * Polls a specific account for new transactions and invokes a callback in order. + */ + +import { TonClient, Transaction } from '@ton/ton'; +import { Address } from '@ton/core'; + +const wait = (milliseconds: number): Promise => new Promise((r) => setTimeout(r, milliseconds)); + +export interface AccountSubscriptionOptions { + readonly limit?: number; + readonly lastLt?: string; + readonly lastHash?: string; + readonly archival?: boolean; +} + +export type Unsubscribe = () => void; + +export function createAccountSubscription( + client: TonClient, + accountAddress: string, + onTransaction: (tx: Transaction) => Promise, + options: AccountSubscriptionOptions = {}, +) { + const address = Address.parse(accountAddress); + const limit = options.limit ?? 10; + const useArchival = options.archival ?? true; + + let lastProcessedLt = options.lastLt; + let lastProcessedHash = options.lastHash; + let isProcessing = false; + let intervalId: NodeJS.Timeout | undefined; + + const getLastProcessed = (): { readonly lt?: string; readonly hash?: string } => ({ + lt: lastProcessedLt, + hash: lastProcessedHash, + }); + + const isAlreadyProcessed = (tx: Transaction): boolean => { + if (!lastProcessedLt || !lastProcessedHash) return false; + const currentLt = tx.lt.toString(); + const currentHash = tx.hash().toString('base64'); + return currentLt === lastProcessedLt && currentHash === lastProcessedHash; + }; + + const fetchNewTransactions = async ( + offsetLt?: string, + offsetHash?: string, + attemptNumber: number = 1, + ): Promise => { + if (offsetLt && offsetHash) { + console.log(`Fetching ${limit} transactions before LT:${offsetLt} Hash:${offsetHash}`); + } else { + console.log(`Fetching last ${limit} transactions`); + } + + let transactions: Transaction[]; + try { + transactions = await client.getTransactions(address, { + limit, + lt: offsetLt, + hash: offsetHash, + archival: useArchival, + }); + } catch (error) { + console.error(`API error (attempt ${attemptNumber}/3):`, error); + if (attemptNumber < 3) { + const delayMs = Math.min(1000 * 2 ** (attemptNumber - 1), 10000); + console.log(`Retrying in ${delayMs}ms...`); + await wait(delayMs); + return fetchNewTransactions(offsetLt, offsetHash, attemptNumber + 1); + } + console.error('Failed to fetch transactions after 3 attempts, will retry on next poll cycle'); + return []; + } + + console.log(`Received ${transactions.length} transactions`); + if (transactions.length === 0) return []; + + const newTransactions: Transaction[] = []; + for (const tx of transactions) { + if (isAlreadyProcessed(tx)) return newTransactions; + newTransactions.push(tx); + } + + if (transactions.length === limit) { + const lastTx = transactions[transactions.length - 1]; + const older = await fetchNewTransactions(lastTx.lt.toString(), lastTx.hash().toString('base64'), 1); + return [...newTransactions, ...older]; + } + return newTransactions; + }; + + const tick = async () => { + if (isProcessing) return; + isProcessing = true; + try { + const newTransactions = await fetchNewTransactions(); + if (newTransactions.length > 0) { + const ordered = newTransactions.reverse(); + for (const tx of ordered) { + await onTransaction(tx); + lastProcessedLt = tx.lt.toString(); + lastProcessedHash = tx.hash().toString('base64'); + console.log(`Updated cursor to lt:${lastProcessedLt} hash:${lastProcessedHash}`); + } + } + } catch (error) { + console.error('Error in transaction polling:', error); + } finally { + isProcessing = false; + } + }; + + const stop: Unsubscribe = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = undefined; + } + }; + + const start = async (intervalMs: number = 10000): Promise => { + await tick(); + intervalId = setInterval(tick, intervalMs); + return stop; + }; + + return { start, stop, getLastProcessed }; +} diff --git a/guidebook/toncoin-processing/src/subscription/block-subscription.ts b/guidebook/toncoin-processing/src/subscription/block-subscription.ts new file mode 100644 index 0000000..c112dff --- /dev/null +++ b/guidebook/toncoin-processing/src/subscription/block-subscription.ts @@ -0,0 +1,88 @@ +/** + * Block subscription (functional API) for monitoring blockchain transactions + */ + +import { TonClient, Transaction } from '@ton/ton'; + +export type Unsubscribe = () => void; + +export function createBlockSubscription( + client: TonClient, + startBlock: number, + onTransaction: (tx: Transaction, block: { workchain: number; shard: string; seqno: number }) => Promise, +) { + let lastProcessedBlock = startBlock; + let isProcessing = false; + let intervalId: NodeJS.Timeout | undefined; + + const getLastProcessedBlock = (): number => lastProcessedBlock; + + const processShard = async (block: { workchain: number; shard: string; seqno: number }): Promise => { + const blockKey = `${block.workchain}:${block.shard}:${block.seqno}`; + console.log(` Processing block ${blockKey}`); + try { + const transactions = await client.getShardTransactions(block.workchain, block.seqno, block.shard); + for (const shortTx of transactions) { + const fullTx = await client.getTransaction(shortTx.account, shortTx.lt, shortTx.hash); + if (fullTx) { + await onTransaction(fullTx, block); + } + } + return transactions.length; + } catch (error) { + console.error(`Error processing block ${blockKey}:`, error); + return 0; + } + }; + + const processBlock = async (seqno: number): Promise => { + const masterchainShard = '8000000000000000'; + let processedCount = 0; + processedCount += await processShard({ workchain: -1, shard: masterchainShard, seqno }); + const shards = await client.getWorkchainShards(seqno); + for (const shard of shards) { + processedCount += await processShard(shard); + } + return processedCount; + }; + + const processNextBlock = async (): Promise => { + const masterchainInfo = await client.getMasterchainInfo(); + const targetSeqno = masterchainInfo.latestSeqno; + if (targetSeqno <= lastProcessedBlock) { + return; + } + for (let nextSeqno = lastProcessedBlock + 1; nextSeqno <= targetSeqno; nextSeqno += 1) { + const totalCount = await processBlock(nextSeqno); + console.log(`✓ Processed masterchain block ${nextSeqno} (${totalCount} transactions)`); + lastProcessedBlock = nextSeqno; + } + }; + + const tick = async () => { + if (isProcessing) return; + isProcessing = true; + try { + await processNextBlock(); + } catch (error) { + console.error('Error processing block:', error); + } finally { + isProcessing = false; + } + }; + + const stop: Unsubscribe = () => { + if (intervalId) { + clearInterval(intervalId); + intervalId = undefined; + } + }; + + const start = async (intervalMs: number = 1000): Promise => { + await tick(); + intervalId = setInterval(tick, intervalMs); + return stop; + }; + + return { start, stop, getLastProcessedBlock }; +} diff --git a/guidebook/toncoin-processing/src/utils/config.ts b/guidebook/toncoin-processing/src/utils/config.ts new file mode 100644 index 0000000..3668116 --- /dev/null +++ b/guidebook/toncoin-processing/src/utils/config.ts @@ -0,0 +1,42 @@ +/** + * Configuration loader for Toncoin payment processing + * + * Loads configuration from environment variables. + * Throws errors if required configuration is missing. + */ + +export interface Config { + readonly isTestnet: boolean; + readonly apiKey: string; + readonly walletAddress: string; + readonly apiUrl: string; +} + +/** + * Loads and validates configuration from environment variables + * @throws {Error} if required configuration is missing + */ +export function loadConfig(): Config { + const isTestnet = process.env.IS_TESTNET !== 'false'; + const apiKey = process.env.API_KEY; + const walletAddress = process.env.WALLET_ADDRESS; + + if (!apiKey) { + throw new Error('API_KEY environment variable is required. Get your key at https://toncenter.com'); + } + + if (!walletAddress) { + throw new Error('WALLET_ADDRESS environment variable is required'); + } + + const apiUrl = isTestnet + ? 'https://testnet.toncenter.com/api/v2/jsonRPC' + : 'https://toncenter.com/api/v2/jsonRPC'; + + return { + isTestnet, + apiKey, + walletAddress, + apiUrl, + }; +} diff --git a/guidebook/toncoin-processing/src/utils/payment-link.ts b/guidebook/toncoin-processing/src/utils/payment-link.ts new file mode 100644 index 0000000..d9b2298 --- /dev/null +++ b/guidebook/toncoin-processing/src/utils/payment-link.ts @@ -0,0 +1,84 @@ +/** + * Payment Link Generator + * + * Utility functions to generate payment deeplinks + * for users to easily send deposits with the correct UUID. + */ + +import { randomUUID } from 'node:crypto'; +import { toNano } from '@ton/core'; +import { loadConfig } from './config'; + +export interface PaymentRequest { + readonly walletAddress: string; + readonly amount: bigint; + readonly comment: string; +} + +/** + * Generates a TON deeplink for payment + * + * @param request - Payment request details + * @returns Deeplink string that opens user's wallet app + * + * @example + * const link = generatePaymentLink({ + * walletAddress: 'UQB7AhB4fP7SWtnfnIMcVUkwIgVLKqijlcpjNEPUVontys5I', + * amount: toNano('1.5'), + * comment: 'payment-uuid-123' + * }); + * // Returns: ton://transfer/UQB7Ah...?amount=1500000000&text=payment-uuid-123 + */ +export function generatePaymentLink(request: PaymentRequest): string { + const { walletAddress, amount, comment } = request; + + const params = new URLSearchParams({ + amount: amount.toString(), + text: comment, + }); + + return `ton://transfer/${walletAddress}?${params.toString()}`; +} + +/** + * Generates a UUID for payment tracking using Node.js built-in crypto + */ +export function generatePaymentUUID(): string { + return randomUUID(); +} + +/** + * Example usage demonstrating payment link generation + */ +export function createPaymentExample(): void { + const config = loadConfig(); + const amount = toNano('1.5'); // 1.5 TON + const uuid = generatePaymentUUID(); + + console.log('=== Payment Request ==='); + console.log(`Network: ${config.isTestnet ? 'TESTNET' : 'MAINNET'}`); + console.log(`Wallet: ${config.walletAddress}`); + console.log(`UUID: ${uuid}`); + console.log(`Amount: 1.5 TON`); + console.log(); + + const deeplink = generatePaymentLink({ + walletAddress: config.walletAddress, + amount, + comment: uuid, + }); + + console.log('Deeplink (opens wallet app):'); + console.log(deeplink); + console.log(); + + console.log('User instructions:'); + console.log('1. Click the link above or scan QR code (create from the link)'); + console.log('2. Confirm the transaction in your wallet'); + console.log('3. Wait for confirmation'); +} + +// Run example if executed directly +if (require.main === module) { + createPaymentExample(); +} diff --git a/guidebook/toncoin-processing/tsconfig.json b/guidebook/toncoin-processing/tsconfig.json new file mode 100644 index 0000000..c5df979 --- /dev/null +++ b/guidebook/toncoin-processing/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "ES2020", + "outDir": "dist", + "rootDir": "src", + "module": "commonjs", + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "strict": true, + "skipLibCheck": true, + "resolveJsonModule": true, + "moduleResolution": "node", + "lib": ["ES2020"] + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist"] +}