feat: add tbank tokens
This commit is contained in:
@@ -2,3 +2,8 @@ DATABASE_URL="file:./prisma/dev.db"
|
||||
FRONTEND_ORIGIN="http://localhost:5173"
|
||||
PORT=3000
|
||||
JWT_SECRET="replace-with-a-secret-at-least-32-characters-long"
|
||||
TOKEN_ENCRYPTION_KEY="replace-with-base64-encoded-32-byte-key"
|
||||
TBANK_GRPC_ENDPOINT="invest-public-api.tbank.ru:443"
|
||||
TBANK_RATE_LIMIT_PER_SECOND=5
|
||||
# Set when the network uses the Russian Trusted Root CA or TLS interception.
|
||||
# TBANK_CA_CERT_PATH="certs/russian-trusted-root-ca.pem"
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFwjCCA6qgAwIBAgICEAAwDQYJKoZIhvcNAQELBQAwcDELMAkGA1UEBhMCUlUx
|
||||
PzA9BgNVBAoMNlRoZSBNaW5pc3RyeSBvZiBEaWdpdGFsIERldmVsb3BtZW50IGFu
|
||||
ZCBDb21tdW5pY2F0aW9uczEgMB4GA1UEAwwXUnVzc2lhbiBUcnVzdGVkIFJvb3Qg
|
||||
Q0EwHhcNMjIwMzAxMjEwNDE1WhcNMzIwMjI3MjEwNDE1WjBwMQswCQYDVQQGEwJS
|
||||
VTE/MD0GA1UECgw2VGhlIE1pbmlzdHJ5IG9mIERpZ2l0YWwgRGV2ZWxvcG1lbnQg
|
||||
YW5kIENvbW11bmljYXRpb25zMSAwHgYDVQQDDBdSdXNzaWFuIFRydXN0ZWQgUm9v
|
||||
dCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMfFOZ8pUAL3+r2n
|
||||
qqE0Zp52selXsKGFYoG0GM5bwz1bSFtCt+AZQMhkWQheI3poZAToYJu69pHLKS6Q
|
||||
XBiwBC1cvzYmUYKMYZC7jE5YhEU2bSL0mX7NaMxMDmH2/NwuOVRj8OImVa5s1F4U
|
||||
zn4Kv3PFlDBjjSjXKVY9kmjUBsXQrIHeaqmUIsPIlNWUnimXS0I0abExqkbdrXbX
|
||||
YwCOXhOO2pDUx3ckmJlCMUGacUTnylyQW2VsJIyIGA8V0xzdaeUXg0VZ6ZmNUr5Y
|
||||
Ber/EAOLPb8NYpsAhJe2mXjMB/J9HNsoFMBFJ0lLOT/+dQvjbdRZoOT8eqJpWnVD
|
||||
U+QL/qEZnz57N88OWM3rabJkRNdU/Z7x5SFIM9FrqtN8xewsiBWBI0K6XFuOBOTD
|
||||
4V08o4TzJ8+Ccq5XlCUW2L48pZNCYuBDfBh7FxkB7qDgGDiaftEkZZfApRg2E+M9
|
||||
G8wkNKTPLDc4wH0FDTijhgxR3Y4PiS1HL2Zhw7bD3CbslmEGgfnnZojNkJtcLeBH
|
||||
BLa52/dSwNU4WWLubaYSiAmA9IUMX1/RpfpxOxd4Ykmhz97oFbUaDJFipIggx5sX
|
||||
ePAlkTdWnv+RWBxlJwMQ25oEHmRguNYf4Zr/Rxr9cS93Y+mdXIZaBEE0KS2iLRqa
|
||||
OiWBki9IMQU4phqPOBAaG7A+eP8PAgMBAAGjZjBkMB0GA1UdDgQWBBTh0YHlzlpf
|
||||
BKrS6badZrHF+qwshzAfBgNVHSMEGDAWgBTh0YHlzlpfBKrS6badZrHF+qwshzAS
|
||||
BgNVHRMBAf8ECDAGAQH/AgEEMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG9w0BAQsF
|
||||
AAOCAgEAALIY1wkilt/urfEVM5vKzr6utOeDWCUczmWX/RX4ljpRdgF+5fAIS4vH
|
||||
tmXkqpSCOVeWUrJV9QvZn6L227ZwuE15cWi8DCDal3Ue90WgAJJZMfTshN4OI8cq
|
||||
W9E4EG9wglbEtMnObHlms8F3CHmrw3k6KmUkWGoa+/ENmcVl68u/cMRl1JbW2bM+
|
||||
/3A+SAg2c6iPDlehczKx2oa95QW0SkPPWGuNA/CE8CpyANIhu9XFrj3RQ3EqeRcS
|
||||
AQQod1RNuHpfETLU/A2gMmvn/w/sx7TB3W5BPs6rprOA37tutPq9u6FTZOcG1Oqj
|
||||
C/B7yTqgI7rbyvox7DEXoX7rIiEqyNNUguTk/u3SZ4VXE2kmxdmSh3TQvybfbnXV
|
||||
4JbCZVaqiZraqc7oZMnRoWrXRG3ztbnbes/9qhRGI7PqXqeKJBztxRTEVj8ONs1d
|
||||
WN5szTwaPIvhkhO3CO5ErU2rVdUr89wKpNXbBODFKRtgxUT70YpmJ46VVaqdAhOZ
|
||||
D9EUUn4YaeLaS8AjSF/h7UkjOibNc4qVDiPP+rkehFWM66PVnP1Msh93tc+taIfC
|
||||
EYVMxjh8zNbFuoc7fzvvrFILLe7ifvEIUqSVIC/AzplM/Jxw7buXFeGP1qVCBEHq
|
||||
391d/9RAfaZ12zkwFsl+IKwE/OZxW8AHa9i1p4GO0YSNuczzEm4=
|
||||
-----END CERTIFICATE-----
|
||||
+4
-1
@@ -3,6 +3,9 @@
|
||||
"collection": "@nestjs/schematics",
|
||||
"sourceRoot": "src",
|
||||
"compilerOptions": {
|
||||
"deleteOutDir": true
|
||||
"deleteOutDir": true,
|
||||
"assets": [
|
||||
{ "include": "tbank/proto/contracts/**/*.proto", "watchAssets": true }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
Generated
+129
-17
@@ -9,6 +9,8 @@
|
||||
"version": "0.0.1",
|
||||
"license": "UNLICENSED",
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.4",
|
||||
"@grpc/proto-loader": "^0.8.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.4",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
@@ -950,6 +952,37 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/grpc-js": {
|
||||
"version": "1.14.4",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.4.tgz",
|
||||
"integrity": "sha512-k9Dj3DV/itK9D06Y8f190Qgop7/Ui+D0njFV3LHMPwPT75DpXLQohE9Wmz0QElrJnzsjB7KPWiKJbOl7IPDArQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@grpc/proto-loader": "^0.8.0",
|
||||
"@js-sdsl/ordered-map": "^4.4.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@grpc/proto-loader": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.1.tgz",
|
||||
"integrity": "sha512-wtF6h+DY6M3YaDBPAmvuuA6jV8Sif9MjtOI5euKFWRgCDl5PeDpPsHR9u2l6St5ceY8AZgoNDww5+HvEsXFsGg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"lodash.camelcase": "^4.3.0",
|
||||
"long": "^5.0.0",
|
||||
"protobufjs": "^7.5.5",
|
||||
"yargs": "^17.7.2"
|
||||
},
|
||||
"bin": {
|
||||
"proto-loader-gen-types": "build/bin/proto-loader-gen-types.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/@humanfs/core": {
|
||||
"version": "0.19.2",
|
||||
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
|
||||
@@ -2060,6 +2093,16 @@
|
||||
"@jridgewell/sourcemap-codec": "^1.4.14"
|
||||
}
|
||||
},
|
||||
"node_modules/@js-sdsl/ordered-map": {
|
||||
"version": "4.4.2",
|
||||
"resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz",
|
||||
"integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/js-sdsl"
|
||||
}
|
||||
},
|
||||
"node_modules/@lukeed/csprng": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz",
|
||||
@@ -2889,6 +2932,63 @@
|
||||
"react-dom": "^18.0.0 || ^19.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/aspromise": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
|
||||
"integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/base64": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz",
|
||||
"integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/codegen": {
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz",
|
||||
"integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/eventemitter": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz",
|
||||
"integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/fetch": {
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz",
|
||||
"integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.1"
|
||||
}
|
||||
},
|
||||
"node_modules/@protobufjs/float": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz",
|
||||
"integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/path": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz",
|
||||
"integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/pool": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz",
|
||||
"integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@protobufjs/utf8": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz",
|
||||
"integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==",
|
||||
"license": "BSD-3-Clause"
|
||||
},
|
||||
"node_modules/@radix-ui/primitive": {
|
||||
"version": "1.1.3",
|
||||
"resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.3.tgz",
|
||||
@@ -4383,7 +4483,6 @@
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
|
||||
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -4393,7 +4492,6 @@
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-convert": "^2.0.1"
|
||||
@@ -5145,7 +5243,6 @@
|
||||
"version": "8.0.1",
|
||||
"resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
|
||||
"integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"string-width": "^4.2.0",
|
||||
@@ -5160,7 +5257,6 @@
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
|
||||
"integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-styles": "^4.0.0",
|
||||
@@ -5206,7 +5302,6 @@
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"color-name": "~1.1.4"
|
||||
@@ -5219,7 +5314,6 @@
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
|
||||
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
@@ -5888,7 +5982,6 @@
|
||||
"version": "8.0.0",
|
||||
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
|
||||
"integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/empathic": {
|
||||
@@ -6013,7 +6106,6 @@
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
|
||||
"integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
@@ -6841,7 +6933,6 @@
|
||||
"version": "2.0.5",
|
||||
"resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
|
||||
"integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": "6.* || 8.* || >= 10.*"
|
||||
@@ -7329,7 +7420,6 @@
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
|
||||
"integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
@@ -8514,6 +8604,12 @@
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.camelcase": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz",
|
||||
"integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.includes": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
|
||||
@@ -8591,7 +8687,6 @@
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
|
||||
"integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==",
|
||||
"devOptional": true,
|
||||
"license": "Apache-2.0"
|
||||
},
|
||||
"node_modules/lru-cache": {
|
||||
@@ -9858,6 +9953,29 @@
|
||||
"devOptional": true,
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/protobufjs": {
|
||||
"version": "7.6.5",
|
||||
"resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz",
|
||||
"integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==",
|
||||
"hasInstallScript": true,
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"@protobufjs/aspromise": "^1.1.2",
|
||||
"@protobufjs/base64": "^1.1.2",
|
||||
"@protobufjs/codegen": "^2.0.5",
|
||||
"@protobufjs/eventemitter": "^1.1.1",
|
||||
"@protobufjs/fetch": "^1.1.1",
|
||||
"@protobufjs/float": "^1.0.2",
|
||||
"@protobufjs/path": "^1.1.2",
|
||||
"@protobufjs/pool": "^1.1.0",
|
||||
"@protobufjs/utf8": "^1.1.1",
|
||||
"@types/node": ">=13.7.0",
|
||||
"long": "^5.3.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/proxy-addr": {
|
||||
"version": "2.0.7",
|
||||
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
|
||||
@@ -10076,7 +10194,6 @@
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
|
||||
"integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
@@ -10628,7 +10745,6 @@
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
"integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"emoji-regex": "^8.0.0",
|
||||
@@ -10659,7 +10775,6 @@
|
||||
"version": "6.0.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
|
||||
"integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ansi-regex": "^5.0.1"
|
||||
@@ -11963,7 +12078,6 @@
|
||||
"version": "5.0.8",
|
||||
"resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
|
||||
"integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=10"
|
||||
@@ -11980,7 +12094,6 @@
|
||||
"version": "17.7.3",
|
||||
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
|
||||
"integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"cliui": "^8.0.1",
|
||||
@@ -11999,7 +12112,6 @@
|
||||
"version": "21.1.1",
|
||||
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
|
||||
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
|
||||
"dev": true,
|
||||
"license": "ISC",
|
||||
"engines": {
|
||||
"node": ">=12"
|
||||
|
||||
@@ -20,6 +20,8 @@
|
||||
"test:e2e": "DATABASE_URL=file:/tmp/wealth-ledger-e2e.db prisma migrate deploy && NODE_ENV=development NODE_OPTIONS=--experimental-vm-modules jest --config ./test/jest-e2e.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@grpc/grpc-js": "^1.14.4",
|
||||
"@grpc/proto-loader": "^0.8.1",
|
||||
"@nestjs/common": "^11.0.1",
|
||||
"@nestjs/config": "^4.0.4",
|
||||
"@nestjs/core": "^11.0.1",
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
-- CreateTable
|
||||
CREATE TABLE "TbankConnection" (
|
||||
"userId" TEXT NOT NULL,
|
||||
"encryptedToken" TEXT NOT NULL,
|
||||
"connectedAt" DATETIME NOT NULL,
|
||||
CONSTRAINT "TbankConnection_userId_fkey" FOREIGN KEY ("userId") REFERENCES "User" ("id") ON DELETE CASCADE ON UPDATE CASCADE
|
||||
);
|
||||
|
||||
-- CreateIndex
|
||||
CREATE UNIQUE INDEX "TbankConnection_userId_key" ON "TbankConnection"("userId");
|
||||
+14
-6
@@ -9,12 +9,20 @@ datasource db {
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id
|
||||
email String @unique
|
||||
passwordHash String
|
||||
sessions Session[]
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
id String @id
|
||||
email String @unique
|
||||
passwordHash String
|
||||
sessions Session[]
|
||||
tbankConnection TbankConnection?
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
}
|
||||
|
||||
model TbankConnection {
|
||||
userId String @unique
|
||||
encryptedToken String
|
||||
connectedAt DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
}
|
||||
|
||||
model Session {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { ConfigModule } from '@nestjs/config';
|
||||
import { AuthModule } from './auth/auth.module';
|
||||
import { validateEnv } from './config/env.validation';
|
||||
import { PrismaModule } from './prisma/prisma.module';
|
||||
import { TbankConnectionModule } from './tbank/tbank-connection.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -12,6 +13,7 @@ import { PrismaModule } from './prisma/prisma.module';
|
||||
}),
|
||||
PrismaModule,
|
||||
AuthModule,
|
||||
TbankConnectionModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -7,12 +7,16 @@ describe('validateEnv', () => {
|
||||
DATABASE_URL: 'file:./data.db',
|
||||
FRONTEND_ORIGIN: 'http://localhost:5173/',
|
||||
JWT_SECRET: 'a-secret-that-is-at-least-thirty-two-characters',
|
||||
TOKEN_ENCRYPTION_KEY: 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
||||
}),
|
||||
).toEqual({
|
||||
DATABASE_URL: 'file:./data.db',
|
||||
FRONTEND_ORIGIN: 'http://localhost:5173',
|
||||
PORT: 3000,
|
||||
JWT_SECRET: 'a-secret-that-is-at-least-thirty-two-characters',
|
||||
TOKEN_ENCRYPTION_KEY: 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
||||
TBANK_GRPC_ENDPOINT: 'invest-public-api.tbank.ru:443',
|
||||
TBANK_RATE_LIMIT_PER_SECOND: 5,
|
||||
});
|
||||
});
|
||||
|
||||
@@ -35,6 +39,7 @@ describe('validateEnv', () => {
|
||||
DATABASE_URL: 'file:./data.db',
|
||||
FRONTEND_ORIGIN: origin,
|
||||
JWT_SECRET: 'a-secret-that-is-at-least-thirty-two-characters',
|
||||
TOKEN_ENCRYPTION_KEY: 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
||||
}),
|
||||
).toThrow('FRONTEND_ORIGIN must be a valid HTTP origin');
|
||||
},
|
||||
|
||||
@@ -3,6 +3,10 @@ export interface Environment {
|
||||
FRONTEND_ORIGIN: string;
|
||||
JWT_SECRET: string;
|
||||
PORT: number;
|
||||
TOKEN_ENCRYPTION_KEY: string;
|
||||
TBANK_CA_CERT_PATH?: string;
|
||||
TBANK_GRPC_ENDPOINT: string;
|
||||
TBANK_RATE_LIMIT_PER_SECOND: number;
|
||||
}
|
||||
|
||||
export function validateEnv(config: Record<string, unknown>): Environment {
|
||||
@@ -10,6 +14,11 @@ export function validateEnv(config: Record<string, unknown>): Environment {
|
||||
const frontendOrigin = config['FRONTEND_ORIGIN'];
|
||||
const port = config['PORT'] ?? 3000;
|
||||
const jwtSecret = config['JWT_SECRET'];
|
||||
const tokenEncryptionKey = config['TOKEN_ENCRYPTION_KEY'];
|
||||
const tbankEndpoint =
|
||||
config['TBANK_GRPC_ENDPOINT'] ?? 'invest-public-api.tbank.ru:443';
|
||||
const tbankRateLimit = config['TBANK_RATE_LIMIT_PER_SECOND'] ?? 5;
|
||||
const tbankCaCertPath = config['TBANK_CA_CERT_PATH'];
|
||||
|
||||
if (typeof databaseUrl !== 'string' || !databaseUrl.startsWith('file:')) {
|
||||
throw new Error('DATABASE_URL must be a SQLite file URL');
|
||||
@@ -44,10 +53,39 @@ export function validateEnv(config: Record<string, unknown>): Environment {
|
||||
throw new Error('JWT_SECRET must be at least 32 characters long');
|
||||
}
|
||||
|
||||
if (
|
||||
typeof tokenEncryptionKey !== 'string' ||
|
||||
!/^[A-Za-z0-9+/]+={0,2}$/.test(tokenEncryptionKey) ||
|
||||
Buffer.from(tokenEncryptionKey, 'base64').length !== 32
|
||||
) {
|
||||
throw new Error(
|
||||
'TOKEN_ENCRYPTION_KEY must be a base64-encoded 32-byte key',
|
||||
);
|
||||
}
|
||||
|
||||
if (typeof tbankEndpoint !== 'string' || !tbankEndpoint) {
|
||||
throw new Error('TBANK_GRPC_ENDPOINT must be a non-empty endpoint');
|
||||
}
|
||||
|
||||
const parsedTbankRateLimit = Number(tbankRateLimit);
|
||||
if (!Number.isInteger(parsedTbankRateLimit) || parsedTbankRateLimit < 1) {
|
||||
throw new Error('TBANK_RATE_LIMIT_PER_SECOND must be a positive integer');
|
||||
}
|
||||
|
||||
if (tbankCaCertPath !== undefined && typeof tbankCaCertPath !== 'string') {
|
||||
throw new Error('TBANK_CA_CERT_PATH must be a file path');
|
||||
}
|
||||
|
||||
return {
|
||||
DATABASE_URL: databaseUrl,
|
||||
FRONTEND_ORIGIN: parsedOrigin.origin,
|
||||
JWT_SECRET: jwtSecret,
|
||||
PORT: parsedPort,
|
||||
TOKEN_ENCRYPTION_KEY: tokenEncryptionKey,
|
||||
...(typeof tbankCaCertPath === 'string' && tbankCaCertPath
|
||||
? { TBANK_CA_CERT_PATH: tbankCaCertPath }
|
||||
: {}),
|
||||
TBANK_GRPC_ENDPOINT: tbankEndpoint,
|
||||
TBANK_RATE_LIMIT_PER_SECOND: parsedTbankRateLimit,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,144 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package tinkoff.public.invest.api.contract.v1;
|
||||
|
||||
option go_package = "./;investapi";
|
||||
option java_package = "ru.tinkoff.piapi.contract.v1";
|
||||
option java_multiple_files = true;
|
||||
option csharp_namespace = "Tinkoff.InvestApi.V1";
|
||||
option objc_class_prefix = "TIAPI";
|
||||
option php_namespace = "Tinkoff\\Invest\\V1";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
|
||||
//Тип инструмента.
|
||||
enum InstrumentType {
|
||||
INSTRUMENT_TYPE_UNSPECIFIED = 0;
|
||||
INSTRUMENT_TYPE_BOND = 1; //Облигация.
|
||||
INSTRUMENT_TYPE_SHARE = 2; //Акция.
|
||||
INSTRUMENT_TYPE_CURRENCY = 3; //Валюта.
|
||||
INSTRUMENT_TYPE_ETF = 4; //Exchange-traded fund. Фонд.
|
||||
INSTRUMENT_TYPE_FUTURES = 5; //Фьючерс.
|
||||
INSTRUMENT_TYPE_SP = 6; //Структурная нота.
|
||||
INSTRUMENT_TYPE_OPTION = 7; //Опцион.
|
||||
INSTRUMENT_TYPE_CLEARING_CERTIFICATE = 8; //Clearing certificate.
|
||||
INSTRUMENT_TYPE_INDEX = 9; //Индекс.
|
||||
INSTRUMENT_TYPE_COMMODITY = 10; //Товар.
|
||||
INSTRUMENT_TYPE_DFA = 11; //Цифровой актив.
|
||||
}
|
||||
|
||||
//Статус запрашиваемых инструментов.
|
||||
enum InstrumentStatus {
|
||||
INSTRUMENT_STATUS_UNSPECIFIED = 0; //Значение не определено.
|
||||
INSTRUMENT_STATUS_BASE = 1; //По умолчанию — базовый список инструментов, которыми можно торговать через T-Invest API. Сейчас списки доступных бумаг в API и других интерфейсах совпадают — кроме внебиржевых бумаг, но в будущем списки могут различаться.
|
||||
INSTRUMENT_STATUS_ALL = 2; //Список всех инструментов.
|
||||
}
|
||||
|
||||
//Денежная сумма в определенной валюте.
|
||||
message MoneyValue {
|
||||
|
||||
// Строковый ISO-код валюты.
|
||||
string currency = 1;
|
||||
|
||||
// Целая часть суммы, может быть отрицательным числом.
|
||||
int64 units = 2;
|
||||
|
||||
// Дробная часть суммы, может быть отрицательным числом.
|
||||
int32 nano = 3;
|
||||
}
|
||||
|
||||
//Котировка — денежная сумма без указания валюты.
|
||||
message Quotation {
|
||||
|
||||
// Целая часть суммы, может быть отрицательным числом.
|
||||
int64 units = 1;
|
||||
|
||||
// Дробная часть суммы, может быть отрицательным числом.
|
||||
int32 nano = 2;
|
||||
}
|
||||
|
||||
//Режим торгов инструмента
|
||||
enum SecurityTradingStatus {
|
||||
SECURITY_TRADING_STATUS_UNSPECIFIED = 0; //Торговый статус не определен.
|
||||
SECURITY_TRADING_STATUS_NOT_AVAILABLE_FOR_TRADING = 1; //Недоступен для торгов.
|
||||
SECURITY_TRADING_STATUS_OPENING_PERIOD = 2; //Период открытия торгов.
|
||||
SECURITY_TRADING_STATUS_CLOSING_PERIOD = 3; //Период закрытия торгов.
|
||||
SECURITY_TRADING_STATUS_BREAK_IN_TRADING = 4; //Перерыв в торговле.
|
||||
SECURITY_TRADING_STATUS_NORMAL_TRADING = 5; //Нормальная торговля.
|
||||
SECURITY_TRADING_STATUS_CLOSING_AUCTION = 6; //Аукцион закрытия.
|
||||
SECURITY_TRADING_STATUS_DARK_POOL_AUCTION = 7; //Аукцион крупных пакетов.
|
||||
SECURITY_TRADING_STATUS_DISCRETE_AUCTION = 8; //Дискретный аукцион.
|
||||
SECURITY_TRADING_STATUS_OPENING_AUCTION_PERIOD = 9; //Аукцион открытия.
|
||||
SECURITY_TRADING_STATUS_TRADING_AT_CLOSING_AUCTION_PRICE = 10; //Период торгов по цене аукциона закрытия.
|
||||
SECURITY_TRADING_STATUS_SESSION_ASSIGNED = 11; //Сессия назначена.
|
||||
SECURITY_TRADING_STATUS_SESSION_CLOSE = 12; //Сессия закрыта.
|
||||
SECURITY_TRADING_STATUS_SESSION_OPEN = 13; //Сессия открыта.
|
||||
SECURITY_TRADING_STATUS_DEALER_NORMAL_TRADING = 14; //Доступна торговля в режиме внутренней ликвидности брокера.
|
||||
SECURITY_TRADING_STATUS_DEALER_BREAK_IN_TRADING = 15; //Перерыв торговли в режиме внутренней ликвидности брокера.
|
||||
SECURITY_TRADING_STATUS_DEALER_NOT_AVAILABLE_FOR_TRADING = 16; //Недоступна торговля в режиме внутренней ликвидности брокера.
|
||||
SECURITY_TRADING_STATUS_STABILIZATION_AUCTION = 17; //Аукцион обновления цен.
|
||||
}
|
||||
|
||||
message PingRequest {
|
||||
optional google.protobuf.Timestamp time = 1; //Время формирования запроса.
|
||||
}
|
||||
|
||||
message PingDelaySettings {
|
||||
optional int32 ping_delay_ms = 15; //Задержка (пинг) сообщений: 5000–180 000 миллисекунд. Значение по умолчанию — 120 000.
|
||||
}
|
||||
|
||||
//Проверка активности стрима.
|
||||
message Ping {
|
||||
google.protobuf.Timestamp time = 1; //Время проверки.
|
||||
string stream_id = 2; //Идентификатор соединения.
|
||||
optional google.protobuf.Timestamp ping_request_time = 4; //Время формирования запроса.
|
||||
}
|
||||
|
||||
//Тип цены.
|
||||
enum PriceType {
|
||||
PRICE_TYPE_UNSPECIFIED = 0; //Значение не определено.
|
||||
PRICE_TYPE_POINT = 1; //Цена в пунктах (только для фьючерсов и облигаций).
|
||||
PRICE_TYPE_CURRENCY = 2; //Цена в валюте расчетов по инструменту.
|
||||
}
|
||||
|
||||
message Page {
|
||||
int32 limit = 1; //Максимальное число возвращаемых записей.
|
||||
int32 page_number = 2; //Порядковый номер страницы, начиная с 0.
|
||||
}
|
||||
|
||||
message PageResponse {
|
||||
int32 limit = 1; //Максимальное число возвращаемых записей.
|
||||
int32 page_number = 2; //Порядковый номер страницы, начиная с 0.
|
||||
int32 total_count = 3; //Общее количество записей.
|
||||
}
|
||||
|
||||
message ResponseMetadata {
|
||||
string tracking_id = 42; //Идентификатор трекинга.
|
||||
google.protobuf.Timestamp server_time = 43; //Серверное время.
|
||||
}
|
||||
|
||||
message BrandData {
|
||||
string logo_name = 1; // Логотип инструмента. Имя файла для получения логотипа.
|
||||
string logo_base_color = 2; // Цвет бренда.
|
||||
string text_color = 3; // Цвет текста для цвета логотипа бренда.
|
||||
}
|
||||
|
||||
enum ResultSubscriptionStatus {
|
||||
RESULT_SUBSCRIPTION_STATUS_UNSPECIFIED = 0; //Статус подписки не определен.
|
||||
RESULT_SUBSCRIPTION_STATUS_OK = 1; //Подписка успешно установлена.
|
||||
RESULT_SUBSCRIPTION_STATUS_ERROR = 13; //Ошибка подписки
|
||||
}
|
||||
|
||||
message ErrorDetail {
|
||||
string code = 1; //Код ошибки.
|
||||
string message = 3; //Описание ошибки.
|
||||
}
|
||||
|
||||
//Реальная площадка исполнения расчетов.
|
||||
enum RealExchange {
|
||||
REAL_EXCHANGE_UNSPECIFIED = 0; //Тип не определен.
|
||||
REAL_EXCHANGE_MOEX = 1; //Московская биржа.
|
||||
REAL_EXCHANGE_RTS = 2; //Санкт-Петербургская биржа.
|
||||
REAL_EXCHANGE_OTC = 3; //Внебиржевой инструмент.
|
||||
REAL_EXCHANGE_DEALER = 4; //Инструмент, торгуемый на площадке брокера.
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
// Copyright 2026 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
syntax = "proto3";
|
||||
|
||||
package google.api;
|
||||
|
||||
import "google/protobuf/descriptor.proto";
|
||||
|
||||
option go_package = "google.golang.org/genproto/googleapis/api/annotations;annotations";
|
||||
option java_multiple_files = true;
|
||||
option java_outer_classname = "FieldBehaviorProto";
|
||||
option java_package = "com.google.api";
|
||||
option objc_class_prefix = "GAPI";
|
||||
|
||||
extend google.protobuf.FieldOptions {
|
||||
// A designation of a specific field behavior (required, output only, etc.)
|
||||
// in protobuf messages.
|
||||
//
|
||||
// Examples:
|
||||
//
|
||||
// string name = 1 [(google.api.field_behavior) = REQUIRED];
|
||||
// State state = 1 [(google.api.field_behavior) = OUTPUT_ONLY];
|
||||
// google.protobuf.Duration ttl = 1
|
||||
// [(google.api.field_behavior) = INPUT_ONLY];
|
||||
// google.protobuf.Timestamp expire_time = 1
|
||||
// [(google.api.field_behavior) = OUTPUT_ONLY,
|
||||
// (google.api.field_behavior) = IMMUTABLE];
|
||||
repeated google.api.FieldBehavior field_behavior = 1052 [packed = false];
|
||||
}
|
||||
|
||||
// An indicator of the behavior of a given field (for example, that a field
|
||||
// is required in requests, or given as output but ignored as input).
|
||||
// This **does not** change the behavior in protocol buffers itself; it only
|
||||
// denotes the behavior and may affect how API tooling handles the field.
|
||||
//
|
||||
// Note: This enum **may** receive new values in the future.
|
||||
enum FieldBehavior {
|
||||
// Conventional default for enums. Do not use this.
|
||||
FIELD_BEHAVIOR_UNSPECIFIED = 0;
|
||||
|
||||
// Specifically denotes a field as optional.
|
||||
// While all fields in protocol buffers are optional, this may be specified
|
||||
// for emphasis if appropriate.
|
||||
OPTIONAL = 1;
|
||||
|
||||
// Denotes a field as required.
|
||||
// This indicates that the field **must** be provided as part of the request,
|
||||
// and failure to do so will cause an error (usually `INVALID_ARGUMENT`).
|
||||
REQUIRED = 2;
|
||||
|
||||
// Denotes a field as output only.
|
||||
// This indicates that the field is provided in responses, but including the
|
||||
// field in a request does nothing (the server *must* ignore it and
|
||||
// *must not* throw an error as a result of the field's presence).
|
||||
OUTPUT_ONLY = 3;
|
||||
|
||||
// Denotes a field as input only.
|
||||
// This indicates that the field is provided in requests, and the
|
||||
// corresponding field is not included in output.
|
||||
INPUT_ONLY = 4;
|
||||
|
||||
// Denotes a field as immutable.
|
||||
// This indicates that the field may be set once in a request to create a
|
||||
// resource, but may not be changed thereafter.
|
||||
IMMUTABLE = 5;
|
||||
|
||||
// Denotes that a (repeated) field is an unordered list.
|
||||
// This indicates that the service may provide the elements of the list
|
||||
// in any arbitrary order, rather than the order the user originally
|
||||
// provided. Additionally, the list's order may or may not be stable.
|
||||
UNORDERED_LIST = 6;
|
||||
|
||||
// Denotes that this field returns a non-empty default value if not set.
|
||||
// This indicates that if the user provides the empty value in a request,
|
||||
// a non-empty value will be returned. The user will not be aware of what
|
||||
// non-empty value to expect.
|
||||
NON_EMPTY_DEFAULT = 7;
|
||||
|
||||
// Denotes that the field in a resource (a message annotated with
|
||||
// google.api.resource) is used in the resource name to uniquely identify the
|
||||
// resource. For AIP-compliant APIs, this should only be applied to the
|
||||
// `name` field on the resource.
|
||||
//
|
||||
// This behavior should not be applied to references to other resources within
|
||||
// the message.
|
||||
//
|
||||
// The identifier field of resources often have different field behavior
|
||||
// depending on the request it is embedded in (e.g. for Create methods name
|
||||
// is optional and unused, while for Update methods it is required). Instead
|
||||
// of method-specific annotations, only `IDENTIFIER` is required.
|
||||
IDENTIFIER = 8;
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,648 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package tinkoff.public.invest.api.contract.v1;
|
||||
|
||||
option go_package = "./;investapi";
|
||||
option java_package = "ru.tinkoff.piapi.contract.v1";
|
||||
option java_multiple_files = true;
|
||||
option csharp_namespace = "Tinkoff.InvestApi.V1";
|
||||
option objc_class_prefix = "TIAPI";
|
||||
option php_namespace = "Tinkoff\\Invest\\V1";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "common.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
|
||||
service OperationsService {/*С помощью методов сервиса можно получить:<br/><br/> **1**. Список операций по счету.<br/> **2**.
|
||||
Портфель по счету.<br/> **3**. Позиции ценных бумаг на счете.<br/> **4**.
|
||||
Доступный остаток для вывода средств.<br/> **5**. Различные отчеты.*/
|
||||
//Deprecated GetOperations — список операций по счету
|
||||
//При работе с методом учитывайте [особенности взаимодействия](/invest/services/operations/operations_problems).
|
||||
rpc GetOperations(OperationsRequest) returns (OperationsResponse);
|
||||
|
||||
//GetPortfolio — портфель по счету
|
||||
rpc GetPortfolio(PortfolioRequest) returns (PortfolioResponse);
|
||||
|
||||
//GetPositions — список позиций по счету
|
||||
rpc GetPositions(PositionsRequest) returns (PositionsResponse);
|
||||
|
||||
//GetWithdrawLimits — доступный остаток для вывода средств
|
||||
rpc GetWithdrawLimits(WithdrawLimitsRequest) returns (WithdrawLimitsResponse);
|
||||
|
||||
//GetBrokerReport — брокерский отчет.
|
||||
rpc GetBrokerReport(BrokerReportRequest) returns (BrokerReportResponse);
|
||||
|
||||
//GetDividendsForeignIssuer — отчет «Справка о доходах за пределами РФ»
|
||||
rpc GetDividendsForeignIssuer(GetDividendsForeignIssuerRequest) returns (GetDividendsForeignIssuerResponse);
|
||||
|
||||
//GetOperationsByCursor — список операций по счету с пагинацией
|
||||
//При работе с методом учитывайте [особенности взаимодействия](/invest/services/operations/operations_problems).
|
||||
rpc GetOperationsByCursor(GetOperationsByCursorRequest) returns (GetOperationsByCursorResponse);
|
||||
}
|
||||
|
||||
service OperationsStreamService {
|
||||
//PortfolioStream — стрим обновлений портфеля
|
||||
rpc PortfolioStream(PortfolioStreamRequest) returns (stream PortfolioStreamResponse);
|
||||
|
||||
//PositionsStream — стрим обновлений информации по изменению позиций портфеля
|
||||
rpc PositionsStream(PositionsStreamRequest) returns (stream PositionsStreamResponse);
|
||||
|
||||
//OperationsStream — стрим обновлений операций
|
||||
rpc OperationsStream(OperationsStreamRequest) returns (stream OperationsStreamResponse);
|
||||
}
|
||||
|
||||
//Запрос получения списка операций по счету.
|
||||
message OperationsRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета клиента.
|
||||
optional google.protobuf.Timestamp from = 2; //Начало периода по UTC.
|
||||
optional google.protobuf.Timestamp to = 3; //Окончание периода по UTC.
|
||||
optional OperationState state = 4; //Статус запрашиваемых операций.
|
||||
optional string figi = 5; //FIGI-идентификатор инструмента для фильтрации.
|
||||
}
|
||||
|
||||
//Список операций.
|
||||
message OperationsResponse {
|
||||
repeated Operation operations = 1; //Массив операций.
|
||||
}
|
||||
|
||||
//Данные по операции.
|
||||
message Operation {
|
||||
string id = 1; //Идентификатор операции.
|
||||
string parent_operation_id = 2; //Идентификатор родительской операции.
|
||||
string currency = 3; //Валюта операции.
|
||||
MoneyValue payment = 4; //Сумма операции.
|
||||
MoneyValue price = 5; //Цена операции за 1 инструмент. Чтобы получить стоимость лота, нужно умножить на лотность инструмента.
|
||||
OperationState state = 6; //Статус операции.
|
||||
int64 quantity = 7; //Количество единиц инструмента.
|
||||
int64 quantity_rest = 8; //Неисполненный остаток по сделке.
|
||||
string figi = 9; //FIGI-идентификатор инструмента, связанного с операцией.
|
||||
string instrument_type = 10; //Тип инструмента. Возможные значения: <br/><br/>`bond` — облигация; <br/>`share` — акция; <br/>`currency` — валюта; <br/>`etf` — фонд; <br/>`futures` — фьючерс.
|
||||
google.protobuf.Timestamp date = 11; //Дата и время операции в формате часовом поясе UTC.
|
||||
string type = 12; //Текстовое описание типа операции.
|
||||
OperationType operation_type = 13; //Тип операции.
|
||||
repeated OperationTrade trades = 14; //Массив сделок.
|
||||
string asset_uid = 16; //Идентификатор актива
|
||||
string position_uid = 17; //Уникальный идентификатор позиции.
|
||||
string instrument_uid = 18; //Уникальный идентификатор инструмента.
|
||||
repeated ChildOperationItem child_operations = 19; //Массив дочерних операций.
|
||||
}
|
||||
|
||||
//Сделка по операции.
|
||||
message OperationTrade {
|
||||
string trade_id = 1; //Идентификатор сделки.
|
||||
google.protobuf.Timestamp date_time = 2; //Дата и время сделки по UTC.
|
||||
int64 quantity = 3; //Количество инструментов.
|
||||
MoneyValue price = 4; //Цена за 1 инструмент. Чтобы получить стоимость лота, нужно умножить на лотность инструмента.
|
||||
}
|
||||
|
||||
//Запрос получения текущего портфеля по счету.
|
||||
message PortfolioRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета пользователя.
|
||||
optional CurrencyRequest currency = 2; //Валюта, в которой нужно рассчитать портфель.
|
||||
enum CurrencyRequest {
|
||||
RUB = 0; //Рубли
|
||||
USD = 1; //Доллары
|
||||
EUR = 2; //Евро
|
||||
}
|
||||
}
|
||||
|
||||
//Текущий портфель по счету.
|
||||
message PortfolioResponse {
|
||||
MoneyValue total_amount_shares = 1; //Общая стоимость акций в портфеле.
|
||||
MoneyValue total_amount_bonds = 2; //Общая стоимость облигаций в портфеле.
|
||||
MoneyValue total_amount_etf = 3; //Общая стоимость фондов в портфеле.
|
||||
MoneyValue total_amount_currencies = 4; //Общая стоимость валют в портфеле.
|
||||
MoneyValue total_amount_futures = 5; //Общая стоимость фьючерсов в портфеле.
|
||||
Quotation expected_yield = 6; //Текущая относительная доходность портфеля в %.
|
||||
repeated PortfolioPosition positions = 7; //Список позиций портфеля.
|
||||
string account_id = 8; //Идентификатор счета пользователя.
|
||||
|
||||
MoneyValue total_amount_options = 9; //Общая стоимость опционов в портфеле.
|
||||
MoneyValue total_amount_sp = 10; //Общая стоимость структурных нот в портфеле.
|
||||
MoneyValue total_amount_portfolio = 11; //Общая стоимость портфеля.
|
||||
repeated VirtualPortfolioPosition virtual_positions = 12; //Массив виртуальных позиций портфеля.
|
||||
MoneyValue daily_yield = 15; // Рассчитанная доходность портфеля за день в рублях.
|
||||
Quotation daily_yield_relative = 16; //Относительная доходность в день в %.
|
||||
MoneyValue total_amount_dfa = 17; //Общая стоимость смарт-активов в портфеле в рублях.
|
||||
}
|
||||
|
||||
//Запрос позиций портфеля по счету.
|
||||
message PositionsRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета пользователя.
|
||||
}
|
||||
|
||||
//Список позиций по счету.
|
||||
message PositionsResponse {
|
||||
repeated MoneyValue money = 1; //Массив валютных позиций портфеля.
|
||||
repeated MoneyValue blocked = 2; //Массив заблокированных валютных позиций портфеля.
|
||||
repeated PositionsSecurities securities = 3; //Список ценно-бумажных позиций портфеля.
|
||||
bool limits_loading_in_progress = 4; //Признак идущей выгрузки лимитов в данный момент.
|
||||
repeated PositionsFutures futures = 5; //Список фьючерсов портфеля.
|
||||
repeated PositionsOptions options = 6; //Список опционов портфеля.
|
||||
string account_id = 15; //Идентификатор счёта пользователя.
|
||||
}
|
||||
|
||||
//Запрос доступного остатка для вывода.
|
||||
message WithdrawLimitsRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета пользователя.
|
||||
}
|
||||
|
||||
//Доступный остаток для вывода.
|
||||
message WithdrawLimitsResponse {
|
||||
repeated MoneyValue money = 1; //Массив валютных позиций портфеля.
|
||||
repeated MoneyValue blocked = 2; //Массив заблокированных валютных позиций портфеля.
|
||||
repeated MoneyValue blocked_guarantee = 3; //Заблокировано под гарантийное обеспечение фьючерсов.
|
||||
}
|
||||
|
||||
//Позиции портфеля.
|
||||
message PortfolioPosition {
|
||||
string figi = 1; //FIGI-идентификатор инструмента.
|
||||
string instrument_type = 2; //Тип инструмента.
|
||||
Quotation quantity = 3; //Количество инструмента в портфеле в штуках.
|
||||
MoneyValue average_position_price = 4; //Средневзвешенная цена позиции. Для пересчета возможна задержка до одной секунды.
|
||||
Quotation expected_yield = 5; //Текущая рассчитанная доходность позиции.
|
||||
MoneyValue current_nkd = 6; // Текущий НКД.
|
||||
Quotation average_position_price_pt = 7 [ deprecated = true ]; // Deprecated Средняя цена позиции в пунктах (для фьючерсов). Для пересчета возможна задержка до одной секунды.
|
||||
MoneyValue current_price = 8; //Текущая цена за 1 инструмент. Чтобы получить стоимость лота, нужно умножить на лотность инструмента.
|
||||
MoneyValue average_position_price_fifo = 9; //Средняя цена позиции по методу FIFO. Для пересчета возможна задержка до одной секунды.
|
||||
Quotation quantity_lots = 10 [ deprecated = true ]; //Deprecated Количество лотов в портфеле.
|
||||
bool blocked = 21; //Заблокировано на бирже.
|
||||
Quotation blocked_lots = 22; //Количество бумаг, заблокированных выставленными заявками.
|
||||
string position_uid = 24; //Уникальный идентификатор позиции.
|
||||
string instrument_uid = 25; //Уникальный идентификатор инструмента.
|
||||
MoneyValue var_margin = 26; //Вариационная маржа.
|
||||
Quotation expected_yield_fifo = 27; //Текущая рассчитанная доходность позиции.
|
||||
MoneyValue daily_yield = 31; // Рассчитанная доходность портфеля за день.
|
||||
string ticker = 32; //Тикер инструмента.
|
||||
string class_code = 33; //Класс-код (секция торгов).
|
||||
optional MoneyValue var_margin_settled = 34; //Вариационная маржа (расчетная).
|
||||
}
|
||||
|
||||
message VirtualPortfolioPosition {
|
||||
string position_uid = 1; //Уникальный идентификатор позиции.
|
||||
string instrument_uid = 2; //Уникальный идентификатор инструмента.
|
||||
string figi = 3; //FIGI-идентификатор инструмента.
|
||||
string instrument_type = 4; //Тип инструмента.
|
||||
Quotation quantity = 5; //Количество инструмента в портфеле в штуках.
|
||||
MoneyValue average_position_price = 6; //Средневзвешенная цена позиции. Для пересчета возможна задержка до одной секунды.
|
||||
Quotation expected_yield = 7; //Текущая рассчитанная доходность позиции.
|
||||
Quotation expected_yield_fifo = 8; //Текущая рассчитанная доходность позиции.
|
||||
google.protobuf.Timestamp expire_date = 9; //Дата, до которой нужно продать виртуальные бумаги. После этой даты виртуальная позиция «сгораетт».
|
||||
MoneyValue current_price = 10; //Текущая цена за 1 инструмент. Чтобы получить стоимость лота, нужно умножить на лотность инструмента.
|
||||
MoneyValue average_position_price_fifo = 11; //Средняя цена позиции по методу FIFO. Для пересчета возможна задержка до одной секунды.
|
||||
MoneyValue daily_yield = 31; // Рассчитанная доходность портфеля за день.
|
||||
string ticker = 32; //Тикер инструмента.
|
||||
string class_code = 33; //Класс-код (секция торгов).
|
||||
}
|
||||
|
||||
//Баланс позиции ценной бумаги.
|
||||
message PositionsSecurities {
|
||||
string figi = 1; //FIGI-идентификатор бумаги.
|
||||
int64 blocked = 2; //Количество бумаг, заблокированных выставленными заявками.
|
||||
int64 balance = 3; //Текущий незаблокированный баланс.
|
||||
string position_uid = 4; //Уникальный идентификатор позиции.
|
||||
string instrument_uid = 5; //Уникальный идентификатор инструмента.
|
||||
string ticker = 6; //Тикер инструмента.
|
||||
string class_code = 7; //Класс-код (секция торгов).
|
||||
bool exchange_blocked = 11; //Заблокировано на бирже.
|
||||
string instrument_type = 16; //Тип инструмента.
|
||||
}
|
||||
|
||||
//Баланс фьючерса.
|
||||
message PositionsFutures {
|
||||
string figi = 1; //FIGI-идентификатор фьючерса.
|
||||
int64 blocked = 2; //Количество бумаг, заблокированных выставленными заявками.
|
||||
int64 balance = 3; //Текущий незаблокированный баланс.
|
||||
string position_uid = 4; //Уникальный идентификатор позиции.
|
||||
string instrument_uid = 5; //Уникальный идентификатор инструмента.
|
||||
string ticker = 6; //Тикер инструмента.
|
||||
string class_code = 7; //Класс-код (секция торгов).
|
||||
}
|
||||
|
||||
//Баланс опциона.
|
||||
message PositionsOptions {
|
||||
string position_uid = 1; //Уникальный идентификатор позиции опциона.
|
||||
string instrument_uid = 2; //Уникальный идентификатор инструмента.
|
||||
string ticker = 3; //Тикер инструмента.
|
||||
string class_code = 4; //Класс-код (секция торгов).
|
||||
int64 blocked = 11; //Количество бумаг, заблокированных выставленными заявками.
|
||||
int64 balance = 21; //Текущий незаблокированный баланс.
|
||||
}
|
||||
|
||||
message BrokerReportRequest {
|
||||
oneof payload {
|
||||
GenerateBrokerReportRequest generate_broker_report_request = 1;
|
||||
GetBrokerReportRequest get_broker_report_request = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message BrokerReportResponse {
|
||||
oneof payload {
|
||||
GenerateBrokerReportResponse generate_broker_report_response = 1;
|
||||
GetBrokerReportResponse get_broker_report_response = 2;
|
||||
}
|
||||
}
|
||||
|
||||
message GenerateBrokerReportRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета клиента.
|
||||
google.protobuf.Timestamp from = 2 [(google.api.field_behavior) = REQUIRED]; //Начало периода по UTC.
|
||||
google.protobuf.Timestamp to = 3 [(google.api.field_behavior) = REQUIRED]; //Окончание периода по UTC.
|
||||
}
|
||||
|
||||
message GenerateBrokerReportResponse {
|
||||
string task_id = 1; //Идентификатор задачи формирования брокерского отчета.
|
||||
}
|
||||
|
||||
message GetBrokerReportRequest {
|
||||
string task_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор задачи формирования брокерского отчета.
|
||||
optional int32 page = 2; //Номер страницы отчета, начинается с 1. Значение по умолчанию — 0.
|
||||
}
|
||||
|
||||
message GetBrokerReportResponse {
|
||||
repeated BrokerReport broker_report = 1;
|
||||
int32 itemsCount = 2; //Количество записей в отчете.
|
||||
int32 pagesCount = 3; //Количество страниц с данными отчета, начинается с 0.
|
||||
int32 page = 4; //Текущая страница, начинается с 0.
|
||||
string task_id = 5; //Идентификатор задачи формирования брокерского отчета.
|
||||
}
|
||||
|
||||
message BrokerReport {
|
||||
string trade_id = 1;//Номер сделки.
|
||||
string order_id = 2; //Номер поручения.
|
||||
string figi = 3; //FIGI-идентификаторинструмента.
|
||||
string execute_sign = 4; //Признак исполнения.
|
||||
google.protobuf.Timestamp trade_datetime = 5; //Дата и время заключения по UTC.
|
||||
string exchange = 6; //Торговая площадка.
|
||||
string class_code = 7; //Режим торгов.
|
||||
string direction = 8; //Вид сделки.
|
||||
string name = 9; //Сокращенное наименование актива.
|
||||
string ticker = 10; //Код актива.
|
||||
MoneyValue price = 11; //Цена за единицу.
|
||||
int64 quantity = 12; //Количество.
|
||||
MoneyValue order_amount = 13; //Сумма без НКД.
|
||||
Quotation aci_value = 14; //НКД.
|
||||
MoneyValue total_order_amount = 15; //Сумма сделки.
|
||||
MoneyValue broker_commission = 16; //Комиссия брокера.
|
||||
MoneyValue exchange_commission = 17; //Комиссия биржи.
|
||||
MoneyValue exchange_clearing_commission = 18; //Комиссия клирингового центра.
|
||||
Quotation repo_rate = 19; //Ставка РЕПО, %.
|
||||
string party = 20; //Контрагент или брокерарокер.
|
||||
google.protobuf.Timestamp clear_value_date = 21; //Дата расчетов по UTC.
|
||||
google.protobuf.Timestamp sec_value_date = 22; //Дата поставки по UTC.
|
||||
string broker_status = 23; //Статус брокера.
|
||||
string separate_agreement_type = 24; //Тип договора.
|
||||
string separate_agreement_number = 25; //Номер договора.
|
||||
string separate_agreement_date = 26; //Дата договора.
|
||||
string delivery_type = 27; //Тип расчета по сделке.
|
||||
}
|
||||
|
||||
//Статус запрашиваемых операций.
|
||||
enum OperationState {
|
||||
OPERATION_STATE_UNSPECIFIED = 0; //Статус операции не определен.
|
||||
OPERATION_STATE_EXECUTED = 1; //Исполнена частично или полностью.
|
||||
OPERATION_STATE_CANCELED = 2; //Отменена.
|
||||
OPERATION_STATE_PROGRESS = 3; //Исполняется.
|
||||
}
|
||||
|
||||
//Тип операции.
|
||||
enum OperationType {
|
||||
OPERATION_TYPE_UNSPECIFIED = 0; //Тип операции не определен.
|
||||
OPERATION_TYPE_INPUT = 1; //Пополнение брокерского счета.
|
||||
OPERATION_TYPE_BOND_TAX = 2; //Удержание НДФЛ по купонам.
|
||||
OPERATION_TYPE_OUTPUT_SECURITIES = 3; //Вывод ЦБ.
|
||||
OPERATION_TYPE_OVERNIGHT = 4; //Доход по сделке РЕПО овернайт.
|
||||
OPERATION_TYPE_TAX = 5; //Удержание налога.
|
||||
OPERATION_TYPE_BOND_REPAYMENT_FULL = 6; //Полное погашение облигаций.
|
||||
OPERATION_TYPE_SELL_CARD = 7; //Продажа ЦБ с карты.
|
||||
OPERATION_TYPE_DIVIDEND_TAX = 8; //Удержание налога по дивидендам.
|
||||
OPERATION_TYPE_OUTPUT = 9; //Вывод денежных средств.
|
||||
OPERATION_TYPE_BOND_REPAYMENT = 10; //Частичное погашение облигаций.
|
||||
OPERATION_TYPE_TAX_CORRECTION = 11; //Корректировка налога.
|
||||
OPERATION_TYPE_SERVICE_FEE = 12; //Удержание комиссии за обслуживание брокерского счета.
|
||||
OPERATION_TYPE_BENEFIT_TAX = 13; //Удержание налога за материальную выгоду.
|
||||
OPERATION_TYPE_MARGIN_FEE = 14; //Удержание комиссии за непокрытую позицию.
|
||||
OPERATION_TYPE_BUY = 15; //Покупка ЦБ.
|
||||
OPERATION_TYPE_BUY_CARD = 16; //Покупка ЦБ с карты.
|
||||
OPERATION_TYPE_INPUT_SECURITIES = 17; //Перевод ценных бумаг из другого депозитария.
|
||||
OPERATION_TYPE_SELL_MARGIN = 18; //Продажа в результате Margin-call.
|
||||
OPERATION_TYPE_BROKER_FEE = 19; //Удержание комиссии за операцию.
|
||||
OPERATION_TYPE_BUY_MARGIN = 20; //Покупка в результате Margin-call.
|
||||
OPERATION_TYPE_DIVIDEND = 21; //Выплата дивидендов.
|
||||
OPERATION_TYPE_SELL = 22; //Продажа ЦБ.
|
||||
OPERATION_TYPE_COUPON = 23; //Выплата купонов.
|
||||
OPERATION_TYPE_SUCCESS_FEE = 24; //Удержание комиссии SuccessFee.
|
||||
OPERATION_TYPE_DIVIDEND_TRANSFER = 25; //Передача дивидендного дохода.
|
||||
OPERATION_TYPE_ACCRUING_VARMARGIN = 26; //Зачисление вариационной маржи.
|
||||
OPERATION_TYPE_WRITING_OFF_VARMARGIN = 27; //Списание вариационной маржи.
|
||||
OPERATION_TYPE_DELIVERY_BUY = 28; //Покупка в рамках экспирации фьючерсного контракта.
|
||||
OPERATION_TYPE_DELIVERY_SELL = 29; //Продажа в рамках экспирации фьючерсного контракта.
|
||||
OPERATION_TYPE_TRACK_MFEE = 30; //Комиссия за управление по счету автоследования.
|
||||
OPERATION_TYPE_TRACK_PFEE = 31; //Комиссия за результат по счету автоследования.
|
||||
OPERATION_TYPE_TAX_PROGRESSIVE = 32; //Удержание налога по ставке 15%.
|
||||
OPERATION_TYPE_BOND_TAX_PROGRESSIVE = 33; //Удержание налога по купонам по ставке 15%.
|
||||
OPERATION_TYPE_DIVIDEND_TAX_PROGRESSIVE = 34; //Удержание налога по дивидендам по ставке 15%.
|
||||
OPERATION_TYPE_BENEFIT_TAX_PROGRESSIVE = 35; //Удержание налога за материальную выгоду по ставке 15%.
|
||||
OPERATION_TYPE_TAX_CORRECTION_PROGRESSIVE = 36; //Корректировка налога по ставке 15%.
|
||||
OPERATION_TYPE_TAX_REPO_PROGRESSIVE = 37; //Удержание налога за возмещение по сделкам РЕПО по ставке 15%.
|
||||
OPERATION_TYPE_TAX_REPO = 38; //Удержание налога за возмещение по сделкам РЕПО.
|
||||
OPERATION_TYPE_TAX_REPO_HOLD = 39; //Удержание налога по сделкам РЕПО.
|
||||
OPERATION_TYPE_TAX_REPO_REFUND = 40; //Возврат налога по сделкам РЕПО.
|
||||
OPERATION_TYPE_TAX_REPO_HOLD_PROGRESSIVE = 41; //Удержание налога по сделкам РЕПО по ставке 15%.
|
||||
OPERATION_TYPE_TAX_REPO_REFUND_PROGRESSIVE = 42; //Возврат налога по сделкам РЕПО по ставке 15%.
|
||||
OPERATION_TYPE_DIV_EXT = 43; //Выплата дивидендов на карту.
|
||||
OPERATION_TYPE_TAX_CORRECTION_COUPON = 44; //Корректировка налога по купонам.
|
||||
OPERATION_TYPE_CASH_FEE = 45; //Комиссия за валютный остаток.
|
||||
OPERATION_TYPE_OUT_FEE = 46; //Комиссия за вывод валюты с брокерского счета.
|
||||
OPERATION_TYPE_OUT_STAMP_DUTY = 47; //Гербовый сбор.
|
||||
OPERATION_TYPE_OUTPUT_SWIFT = 50; // SWIFT-перевод.
|
||||
OPERATION_TYPE_INPUT_SWIFT = 51; // SWIFT-перевод.
|
||||
OPERATION_TYPE_OUTPUT_ACQUIRING = 53; // Перевод на карту.
|
||||
OPERATION_TYPE_INPUT_ACQUIRING = 54; // Перевод с карты.
|
||||
OPERATION_TYPE_OUTPUT_PENALTY = 55; // Комиссия за вывод средств.
|
||||
OPERATION_TYPE_ADVICE_FEE = 56; // Списание оплаты за сервис Советов.
|
||||
OPERATION_TYPE_TRANS_IIS_BS = 57; // Перевод ценных бумаг с ИИС на брокерский счет.
|
||||
OPERATION_TYPE_TRANS_BS_BS = 58; // Перевод ценных бумаг с одного брокерского счета на другой.
|
||||
OPERATION_TYPE_OUT_MULTI = 59; // Вывод денежных средств со счета.
|
||||
OPERATION_TYPE_INP_MULTI = 60; // Пополнение денежных средств со счета.
|
||||
OPERATION_TYPE_OVER_PLACEMENT = 61; // Размещение биржевого овернайта.
|
||||
OPERATION_TYPE_OVER_COM = 62; // Списание комиссии.
|
||||
OPERATION_TYPE_OVER_INCOME = 63; // Доход от оверанайта.
|
||||
OPERATION_TYPE_OPTION_EXPIRATION = 64; // Экспирация опциона.
|
||||
OPERATION_TYPE_FUTURE_EXPIRATION = 65; // Экспирация фьючерса.
|
||||
OPERATION_TYPE_OTHER_FEE = 66; // Прочие комиссии;
|
||||
OPERATION_TYPE_OTHER = 67; // Операция по счету;
|
||||
OPERATION_TYPE_DFA_REDEMPTION = 68; // погашение ЦФА-токена;
|
||||
OPERATION_TYPE_PRIMARY_ORDER =69; // отмена заявки на первичное размещение по ЦФА;
|
||||
OPERATION_TYPE_FUNDING = 70; //Списание фандинга.
|
||||
}
|
||||
|
||||
message GetDividendsForeignIssuerRequest {
|
||||
oneof payload {
|
||||
GenerateDividendsForeignIssuerReportRequest generate_div_foreign_issuer_report = 1; //Объект запроса формирования отчета.
|
||||
GetDividendsForeignIssuerReportRequest get_div_foreign_issuer_report = 2; //Объект запроса сформированного отчета.
|
||||
}
|
||||
}
|
||||
|
||||
message GetDividendsForeignIssuerResponse {
|
||||
oneof payload {
|
||||
GenerateDividendsForeignIssuerReportResponse generate_div_foreign_issuer_report_response = 1; //Объект результата задачи запуска формирования отчета.
|
||||
GetDividendsForeignIssuerReportResponse div_foreign_issuer_report = 2; //Отчет «Справка о доходах за пределами РФ».
|
||||
}
|
||||
}
|
||||
|
||||
//Объект запроса формирования отчета «Справка о доходах за пределами РФ».
|
||||
message GenerateDividendsForeignIssuerReportRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета клиента.
|
||||
google.protobuf.Timestamp from = 2 [(google.api.field_behavior) = REQUIRED]; //Начало периода по UTC.
|
||||
google.protobuf.Timestamp to = 3 [(google.api.field_behavior) = REQUIRED]; //Окончание периода по UTC. Как правило, можно сформировать отчет по дату на несколько дней меньше текущей. Начало и окончание периода должны быть в рамках одного календарного года.
|
||||
}
|
||||
|
||||
// Объект запроса сформированного отчета «Справка о доходах за пределами РФ».
|
||||
message GetDividendsForeignIssuerReportRequest {
|
||||
string task_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор задачи формирования отчета.
|
||||
optional int32 page = 2; //Номер страницы отчета (начинается с 0), значение по умолчанию: 0.
|
||||
}
|
||||
|
||||
// Объект результата задачи запуска формирования отчета «Справка о доходах за пределами РФ».
|
||||
message GenerateDividendsForeignIssuerReportResponse {
|
||||
string task_id = 1; //Идентификатор задачи формирования отчета.
|
||||
}
|
||||
|
||||
message GetDividendsForeignIssuerReportResponse {
|
||||
repeated DividendsForeignIssuerReport dividends_foreign_issuer_report = 1;
|
||||
int32 itemsCount = 2; //Количество записей в отчете.
|
||||
int32 pagesCount = 3; //Количество страниц с данными отчета, начинается с 0.
|
||||
int32 page = 4; //Текущая страница, начинается с 0.
|
||||
}
|
||||
|
||||
// Отчет «Справка о доходах за пределами РФ».
|
||||
message DividendsForeignIssuerReport {
|
||||
google.protobuf.Timestamp record_date = 1; //Дата фиксации реестра.
|
||||
google.protobuf.Timestamp payment_date = 2; //Дата выплаты.
|
||||
string security_name = 3; //Наименование ценной бумаги.
|
||||
string isin = 4; //ISIN-идентификатор ценной бумаги.
|
||||
string issuer_country = 5; //Страна эмитента. Для депозитарных расписок указывается страна эмитента базового актива.
|
||||
int64 quantity = 6; //Количество ценных бумаг.
|
||||
Quotation dividend = 7; //Выплаты на одну бумагу
|
||||
Quotation external_commission = 8; //Комиссия внешних платежных агентов.
|
||||
Quotation dividend_gross = 9; //Сумма до удержания налога.
|
||||
Quotation tax = 10; //Сумма налога, удержанного агентом.
|
||||
Quotation dividend_amount = 11; //Итоговая сумма выплаты.
|
||||
string currency = 12; //Валюта.
|
||||
}
|
||||
|
||||
//Запрос установки stream-соединения.
|
||||
message PortfolioStreamRequest {
|
||||
repeated string accounts = 1; //Массив идентификаторов счетов пользователя.
|
||||
PingDelaySettings ping_settings = 15; //Запрос настройки пинга.
|
||||
}
|
||||
|
||||
//Информация по позициям и доходностям портфелей.
|
||||
message PortfolioStreamResponse {
|
||||
oneof payload {
|
||||
PortfolioSubscriptionResult subscriptions = 1; //Объект результата подписки.
|
||||
PortfolioResponse portfolio = 2; //Объект стриминга портфеля.
|
||||
Ping ping = 3; //Проверка активности стрима.
|
||||
}
|
||||
}
|
||||
|
||||
//Объект результата подписки.
|
||||
message PortfolioSubscriptionResult {
|
||||
repeated AccountSubscriptionStatus accounts = 1; //Массив счетов клиента.
|
||||
string tracking_id = 7; //Уникальный идентификатор запроса, подробнее: [tracking_id](/invest/intro/developer/protocols/grpc#tracking-id).
|
||||
string stream_id = 8; //Идентификатор открытого соединения
|
||||
}
|
||||
|
||||
//Счет клиента.
|
||||
message AccountSubscriptionStatus {
|
||||
string account_id = 1; //Идентификатор счета.
|
||||
PortfolioSubscriptionStatus subscription_status = 6; //Результат подписки.
|
||||
}
|
||||
|
||||
//Результат подписки.
|
||||
enum PortfolioSubscriptionStatus {
|
||||
PORTFOLIO_SUBSCRIPTION_STATUS_UNSPECIFIED = 0; //Тип не определен.
|
||||
PORTFOLIO_SUBSCRIPTION_STATUS_SUCCESS = 1; //Успешно.
|
||||
PORTFOLIO_SUBSCRIPTION_STATUS_ACCOUNT_NOT_FOUND = 2; //Счет не найден или недостаточно прав.
|
||||
PORTFOLIO_SUBSCRIPTION_STATUS_INTERNAL_ERROR = 3; //Произошла ошибка.
|
||||
}
|
||||
|
||||
//Запрос списка операций по счету с пагинацией.
|
||||
message GetOperationsByCursorRequest {
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED]; //Идентификатор счета клиента, обязательный параметр. Остальные параметры опциональны.
|
||||
optional string instrument_id = 2; //Идентификатор инструмента. Принимает значение `figi`, `instrument_uid` или `ticker + '_' + class_code`.
|
||||
optional google.protobuf.Timestamp from = 6; //Начало периода по UTC.
|
||||
optional google.protobuf.Timestamp to = 7; //Окончание периода по UTC.
|
||||
optional string cursor = 11; //Идентификатор элемента, с которого начать формировать ответ.
|
||||
optional int32 limit = 12; //Лимит количества операций. По умолчанию — `100`, максимальное значение — `1000`.
|
||||
repeated OperationType operation_types = 13; //Тип операции. Принимает значение из списка `OperationType`.
|
||||
optional OperationState state = 14; //Статус запрашиваемых операций. Возможные значения указаны в `OperationState`.
|
||||
optional bool without_commissions = 15; //Флаг возврата комиссии. По умолчанию — `false`.
|
||||
optional bool without_trades = 16; //Флаг получения ответа без массива сделок.
|
||||
optional bool without_overnights = 17; //Флаг показа overnight операций.
|
||||
}
|
||||
|
||||
//Список операций по счету с пагинацией.
|
||||
message GetOperationsByCursorResponse {
|
||||
bool has_next = 1; //Признак, есть ли следующий элемент.
|
||||
string next_cursor = 2; //Следующий курсор.
|
||||
repeated OperationItem items = 6; //Список операций.
|
||||
}
|
||||
|
||||
//Данные об операции.
|
||||
message OperationItem {
|
||||
string cursor = 1; //Курсор.
|
||||
string broker_account_id = 6; //Номер счета клиента.
|
||||
string id = 16; //Идентификатор операции, может меняться с течением времени.
|
||||
string parent_operation_id = 17; //Идентификатор родительской операции. Может измениться, если изменился ID родительской операции.
|
||||
string name = 18; //Название операции.
|
||||
google.protobuf.Timestamp date = 21; //Дата поручения.
|
||||
OperationType type = 22; //Тип операции.
|
||||
string description = 23; //Описание операции.
|
||||
OperationState state = 24; //Статус поручения.
|
||||
string instrument_uid = 31; //Уникальный идентификатор инструмента.
|
||||
string figi = 32; //FIGI.
|
||||
string instrument_type = 33; //Тип инструмента.
|
||||
InstrumentType instrument_kind = 34; //Тип инструмента.
|
||||
string position_uid = 35; //Уникальный идентификатор позиции.
|
||||
string ticker = 36; //Тикер инструмента.
|
||||
string class_code = 37; //Класс-код (секция торгов).
|
||||
MoneyValue payment = 41; //Сумма операции.
|
||||
MoneyValue price = 42; //Цена операции за 1 инструмент.
|
||||
MoneyValue commission = 43; //Комиссия.
|
||||
MoneyValue yield = 44; //Доходность.
|
||||
Quotation yield_relative = 45; //Относительная доходность.
|
||||
MoneyValue accrued_int = 46; //Накопленный купонный доход.
|
||||
int64 quantity = 51; //Количество единиц инструмента.
|
||||
int64 quantity_rest = 52; //Неисполненный остаток по сделке.
|
||||
int64 quantity_done = 53; //Исполненный остаток.
|
||||
google.protobuf.Timestamp cancel_date_time = 56; //Дата и время снятия заявки.
|
||||
string cancel_reason = 57; //Причина отмены операции.
|
||||
OperationItemTrades trades_info = 61; //Массив сделок.
|
||||
string asset_uid = 64; //Идентификатор актива.
|
||||
repeated ChildOperationItem child_operations = 65; //Массив дочерних операций.
|
||||
}
|
||||
|
||||
//Массив с информацией о сделках.
|
||||
message OperationItemTrades {
|
||||
repeated OperationItemTrade trades = 6;
|
||||
}
|
||||
|
||||
//Сделка по операции.
|
||||
message OperationItemTrade {
|
||||
string num = 1; //Номер сделки.
|
||||
google.protobuf.Timestamp date = 6; //Дата сделки.
|
||||
int64 quantity = 11; //Количество в единицах.
|
||||
MoneyValue price = 16; //Цена.
|
||||
MoneyValue yield = 21; //Доходность.
|
||||
Quotation yield_relative = 22; //Относительная доходность.
|
||||
}
|
||||
|
||||
//Запрос установки stream-соединения позиций.
|
||||
message PositionsStreamRequest {
|
||||
repeated string accounts = 1; //Массив идентификаторов счетов пользователя.
|
||||
bool with_initial_positions = 3; //Получение состояния позиций на момент подключения.
|
||||
PingDelaySettings ping_settings = 15; //Запрос настройки пинга.
|
||||
}
|
||||
|
||||
//Информация по изменению позиций портфеля.
|
||||
message PositionsStreamResponse {
|
||||
oneof payload {
|
||||
PositionsSubscriptionResult subscriptions = 1; //Объект результата подписки.
|
||||
PositionData position = 2; //Объект стриминга позиций.
|
||||
Ping ping = 3; //Проверка активности стрима.
|
||||
PositionsResponse initial_positions = 5; //Текущие позиции.
|
||||
}
|
||||
}
|
||||
|
||||
//Объект результата подписки.
|
||||
message PositionsSubscriptionResult {
|
||||
repeated PositionsSubscriptionStatus accounts = 1; //Массив счетов клиента.
|
||||
string tracking_id = 7; //Уникальный идентификатор запроса, подробнее: [tracking_id](/invest/intro/developer/protocols/grpc#tracking-id).
|
||||
string stream_id = 8; //Идентификатор открытого соединения
|
||||
}
|
||||
|
||||
//Счет клиента.
|
||||
message PositionsSubscriptionStatus {
|
||||
string account_id = 1; //Идентификатор счета.
|
||||
PositionsAccountSubscriptionStatus subscription_status = 6; //Результат подписки.
|
||||
}
|
||||
|
||||
//Результат подписки.
|
||||
enum PositionsAccountSubscriptionStatus {
|
||||
POSITIONS_SUBSCRIPTION_STATUS_UNSPECIFIED = 0; //Тип не определен.
|
||||
POSITIONS_SUBSCRIPTION_STATUS_SUCCESS = 1; //Успешно.
|
||||
POSITIONS_SUBSCRIPTION_STATUS_ACCOUNT_NOT_FOUND = 2; //Счет не найден или недостаточно прав.
|
||||
POSITIONS_SUBSCRIPTION_STATUS_INTERNAL_ERROR = 3; //Произошла ошибка.
|
||||
}
|
||||
|
||||
//Данные о позиции портфеля.
|
||||
message PositionData {
|
||||
string account_id = 1; //Идентификатор счета.
|
||||
repeated PositionsMoney money = 2; //Массив валютных позиций портфеля.
|
||||
repeated PositionsSecurities securities = 3; //Список ценно-бумажных позиций портфеля.
|
||||
repeated PositionsFutures futures = 4; //Список фьючерсов портфеля.
|
||||
repeated PositionsOptions options = 5; //Список опционов портфеля.
|
||||
google.protobuf.Timestamp date = 6; //Дата и время операции в формате UTC.
|
||||
}
|
||||
|
||||
//Валютная позиция портфеля.
|
||||
message PositionsMoney {
|
||||
MoneyValue available_value = 1; //Доступное количество валютный позиций.
|
||||
MoneyValue blocked_value = 2; //Заблокированное количество валютных позиций.
|
||||
}
|
||||
|
||||
message ChildOperationItem {
|
||||
string instrument_uid = 1; //Уникальный идентификатор инструмента.
|
||||
MoneyValue payment = 2; //Сумма операции.
|
||||
}
|
||||
|
||||
//Запрос установки stream-соединения операций.
|
||||
message OperationsStreamRequest {
|
||||
repeated string accounts = 1; //Массив идентификаторов счетов пользователя.
|
||||
PingDelaySettings ping_settings = 15; //Запрос настройки пинга.
|
||||
}
|
||||
|
||||
//Информация по операциям.
|
||||
message OperationsStreamResponse {
|
||||
oneof payload {
|
||||
OperationsSubscriptionResult subscriptions = 1; //Объект результата подписки.
|
||||
OperationData operation = 2; //Объект стриминга операций.
|
||||
Ping ping = 3; //Проверка активности стрима.
|
||||
}
|
||||
}
|
||||
|
||||
//Объект результата подписки.
|
||||
message OperationsSubscriptionResult {
|
||||
repeated string accounts = 1; //Массив счетов клиента.
|
||||
OperationsAccountSubscriptionStatus subscription_status = 2; //Результат подписки.
|
||||
string tracking_id = 7; //Уникальный идентификатор запроса, подробнее: [tracking_id](/invest/intro/developer/protocols/grpc#tracking-id).
|
||||
string stream_id = 8; //Идентификатор открытого соединения
|
||||
}
|
||||
|
||||
//Результат подписки.
|
||||
enum OperationsAccountSubscriptionStatus {
|
||||
OPERATIONS_SUBSCRIPTION_STATUS_UNSPECIFIED = 0; //Тип не определен.
|
||||
OPERATIONS_SUBSCRIPTION_STATUS_SUCCESS = 1; //Успешно.
|
||||
OPERATIONS_SUBSCRIPTION_STATUS_ACCOUNT_NOT_FOUND = 2; //Счет не найден или недостаточно прав.
|
||||
OPERATIONS_SUBSCRIPTION_STATUS_INTERNAL_ERROR = 3; //Произошла ошибка.
|
||||
}
|
||||
|
||||
//Данные об операции.
|
||||
message OperationData {
|
||||
string broker_account_id = 1; //Идентификатор счета.
|
||||
string id = 2; //Номер поручения.
|
||||
string parent_operation_id = 3; //Номер родительского поручения.
|
||||
string name = 4; //Название инструмента.
|
||||
google.protobuf.Timestamp date = 5; //Дата.
|
||||
OperationType type = 6; //Тип операции.
|
||||
OperationState state = 7; //Статус поручения.
|
||||
string instrument_uid = 8; //Уникальный идентификатор инструмента.
|
||||
string figi = 9; //FIGI-идентификатор инструмента.
|
||||
string instrument_type = 10; //Тип инструмента.
|
||||
InstrumentType instrument_kind = 11; //Тип инструмента.
|
||||
string position_uid = 12; //Идентификатор позиции.
|
||||
string ticker = 13; //Тикер инструмента.
|
||||
string class_code = 14; //Класс-код (секция торгов).
|
||||
MoneyValue payment = 15; //Сумма операции.
|
||||
}
|
||||
@@ -0,0 +1,249 @@
|
||||
syntax = "proto3";
|
||||
|
||||
package tinkoff.public.invest.api.contract.v1;
|
||||
|
||||
option go_package = "./;investapi";
|
||||
option java_package = "ru.tinkoff.piapi.contract.v1";
|
||||
option java_multiple_files = true;
|
||||
option csharp_namespace = "Tinkoff.InvestApi.V1";
|
||||
option objc_class_prefix = "TIAPI";
|
||||
option php_namespace = "Tinkoff\\Invest\\V1";
|
||||
|
||||
import "google/protobuf/timestamp.proto";
|
||||
import "google/api/field_behavior.proto";
|
||||
import "common.proto";
|
||||
|
||||
service UsersService { /*С помощью сервиса можно получить: <br/> 1.
|
||||
список счетов пользователя; <br/> 2. маржинальные показатели по счeту.*/
|
||||
|
||||
//GetAccounts — счета пользователя
|
||||
//Получить список счетов.
|
||||
rpc GetAccounts (GetAccountsRequest) returns (GetAccountsResponse);
|
||||
|
||||
//GetMarginAttributes — маржинальные показатели по счeту
|
||||
//Метод позволяет получить маржинальные показатели и ликвидность по заданному счeту.
|
||||
rpc GetMarginAttributes (GetMarginAttributesRequest) returns (GetMarginAttributesResponse);
|
||||
|
||||
//GetUserTariff — тариф пользователя
|
||||
//Получить информацию о текущих лимитах на подклчение, согласно текущему тарифу пользователя.
|
||||
rpc GetUserTariff (GetUserTariffRequest) returns (GetUserTariffResponse);
|
||||
|
||||
//GetInfo — информация о пользователе
|
||||
//Получить информацию о пользователе: тариф, признак квалификации, пройденные тесты и др.
|
||||
rpc GetInfo (GetInfoRequest) returns (GetInfoResponse);
|
||||
|
||||
//GetBankAccounts — банковские счета пользователя
|
||||
//Получить список счетов пользователя, в том числе и банковских.
|
||||
rpc GetBankAccounts(GetBankAccountsRequest) returns (GetBankAccountsResponse);
|
||||
|
||||
//CurrencyTransfer — перевод денежных средств между счетами
|
||||
//Перевести денежные средства между брокерскими счетами
|
||||
rpc CurrencyTransfer(CurrencyTransferRequest) returns (CurrencyTransferResponse);
|
||||
|
||||
//PayIn — пополнение брокерского счета
|
||||
//Пополнить брокерский счёт с банковского
|
||||
rpc PayIn(PayInRequest) returns (PayInResponse);
|
||||
|
||||
//GetAccountValues — дополнительные показатели счетов
|
||||
//Метод предназначен для получения дополнительных показателей счетов
|
||||
rpc GetAccountValues(GetAccountValuesRequest) returns (GetAccountValuesResponse);
|
||||
}
|
||||
|
||||
//Запрос получения счетов пользователя.
|
||||
message GetAccountsRequest {
|
||||
optional AccountStatus status = 1; //Статус счета.
|
||||
}
|
||||
|
||||
//Список счетов пользователя.
|
||||
message GetAccountsResponse {
|
||||
// Массив счетов клиента.
|
||||
repeated Account accounts = 1;
|
||||
}
|
||||
|
||||
//Информация о счeте.
|
||||
message Account {
|
||||
|
||||
// Идентификатор счeта.
|
||||
string id = 1;
|
||||
|
||||
// Тип счeта.
|
||||
AccountType type = 2;
|
||||
|
||||
// Название счeта.
|
||||
string name = 3;
|
||||
|
||||
// Статус счeта.
|
||||
AccountStatus status = 4;
|
||||
|
||||
// Дата открытия счeта в часовом поясе UTC.
|
||||
google.protobuf.Timestamp opened_date = 5;
|
||||
|
||||
// Дата закрытия счeта в часовом поясе UTC.
|
||||
google.protobuf.Timestamp closed_date = 6;
|
||||
|
||||
// Уровень доступа к текущему счeту (определяется токеном).
|
||||
AccessLevel access_level = 7;
|
||||
}
|
||||
|
||||
//Тип счeта.
|
||||
enum AccountType {
|
||||
ACCOUNT_TYPE_UNSPECIFIED = 0; //Тип аккаунта не определeн.
|
||||
ACCOUNT_TYPE_TINKOFF = 1; //Брокерский счeт Т-Инвестиций.
|
||||
ACCOUNT_TYPE_TINKOFF_IIS = 2; //ИИС.
|
||||
ACCOUNT_TYPE_INVEST_BOX = 3; //Инвесткопилка.
|
||||
ACCOUNT_TYPE_INVEST_FUND = 4; //Фонд денежного рынка.
|
||||
ACCOUNT_TYPE_DEBIT = 5; //Дебетовый карточный счeт.
|
||||
ACCOUNT_TYPE_SAVING = 6; //Накопительный счeт.
|
||||
ACCOUNT_TYPE_DFA = 7; //Смарт-счет.
|
||||
}
|
||||
|
||||
//Статус счeта.
|
||||
enum AccountStatus {
|
||||
ACCOUNT_STATUS_UNSPECIFIED = 0; //Статус счeта не определeн.
|
||||
ACCOUNT_STATUS_NEW = 1; //Новый, в процессе открытия.
|
||||
ACCOUNT_STATUS_OPEN = 2; //Открытый и активный счeт.
|
||||
ACCOUNT_STATUS_CLOSED = 3; //Закрытый счeт.
|
||||
ACCOUNT_STATUS_ALL = 4; //Все счета.
|
||||
}
|
||||
|
||||
//Запрос маржинальных показателей по счeту.
|
||||
message GetMarginAttributesRequest {
|
||||
|
||||
// Идентификатор счeта пользователя.
|
||||
string account_id = 1 [(google.api.field_behavior) = REQUIRED];
|
||||
}
|
||||
|
||||
//Маржинальные показатели по счeту.
|
||||
message GetMarginAttributesResponse {
|
||||
|
||||
// Ликвидная стоимость портфеля. [Подробнее про ликвидный портфель](https://www.tbank.ru/invest/help/brokerage/account/margin/about/#q4).
|
||||
MoneyValue liquid_portfolio = 1;
|
||||
|
||||
// Начальная маржа — начальное обеспечение для совершения новой сделки. [Подробнее про начальную и минимальную маржу](https://www.tbank.ru/invest/help/brokerage/account/margin/about/#q6).
|
||||
MoneyValue starting_margin = 2;
|
||||
|
||||
// Минимальная маржа — это минимальное обеспечение для поддержания позиции, которую вы уже открыли. [Подробнее про начальную и минимальную маржу](https://www.tbank.ru/invest/help/brokerage/account/margin/about/#q6).
|
||||
MoneyValue minimal_margin = 3;
|
||||
|
||||
// Уровень достаточности средств. Соотношение стоимости ликвидного портфеля к начальной марже.
|
||||
Quotation funds_sufficiency_level = 4;
|
||||
|
||||
// Объем недостающих средств. Разница между стартовой маржой и ликвидной стоимости портфеля.
|
||||
MoneyValue amount_of_missing_funds = 5;
|
||||
|
||||
// Скорректированная маржа. Начальная маржа, в которой плановые позиции рассчитываются с учeтом активных заявок на покупку позиций лонг или продажу позиций шорт.
|
||||
MoneyValue corrected_margin = 6;
|
||||
|
||||
// Размер гарантийного обеспечения, заблокированного под фьючерсы.
|
||||
MoneyValue guarantee_for_futures = 7;
|
||||
}
|
||||
|
||||
//Запрос текущих лимитов пользователя.
|
||||
message GetUserTariffRequest {
|
||||
}
|
||||
|
||||
//Текущие лимиты пользователя.
|
||||
message GetUserTariffResponse {
|
||||
repeated UnaryLimit unary_limits = 1; //Массив лимитов пользователя по unary-запросам.
|
||||
repeated StreamLimit stream_limits = 2; //Массив лимитов пользователей для stream-соединений.
|
||||
}
|
||||
|
||||
//Лимит unary-методов.
|
||||
message UnaryLimit {
|
||||
int32 limit_per_minute = 1; //Количество unary-запросов в минуту.
|
||||
repeated string methods = 2; //Названия методов.
|
||||
optional int32 limit_per_second = 3; //Количество unary-запросов в секунду.
|
||||
}
|
||||
|
||||
//Лимит stream-соединений.
|
||||
message StreamLimit {
|
||||
int32 limit = 1; //Максимальное количество stream-соединений.
|
||||
repeated string streams = 2; //Названия stream-методов.
|
||||
int32 open = 3; //Текущее количество открытых stream-соединений.
|
||||
}
|
||||
|
||||
//Запрос информации о пользователе.
|
||||
message GetInfoRequest {
|
||||
}
|
||||
|
||||
//Информация о пользователе.
|
||||
message GetInfoResponse {
|
||||
bool prem_status = 1; //Признак премиум клиента.
|
||||
bool qual_status = 2; //Признак квалифицированного инвестора.
|
||||
repeated string qualified_for_work_with = 3; //Набор требующих тестирования инструментов и возможностей, с которыми может работать пользователь. [Подробнее](/invest/services/accounts/faq_users).
|
||||
string tariff = 4; //Наименование тарифа пользователя.
|
||||
string user_id = 9; //Идентификатор пользователя.
|
||||
string risk_level_code = 12; // Категория риска.
|
||||
}
|
||||
|
||||
//Уровень доступа к счeту.
|
||||
enum AccessLevel {
|
||||
ACCOUNT_ACCESS_LEVEL_UNSPECIFIED = 0; //Уровень доступа не определeн.
|
||||
ACCOUNT_ACCESS_LEVEL_FULL_ACCESS = 1; //Полный доступ к счeту.
|
||||
ACCOUNT_ACCESS_LEVEL_READ_ONLY = 2; //Доступ с уровнем прав «только чтение».
|
||||
ACCOUNT_ACCESS_LEVEL_NO_ACCESS = 3; //Доступа нет.
|
||||
}
|
||||
|
||||
//Запрос списка банковских счетов пользователя.
|
||||
message GetBankAccountsRequest {
|
||||
}
|
||||
|
||||
//Список банковских счетов пользователя.
|
||||
message GetBankAccountsResponse {
|
||||
repeated BankAccount bank_accounts = 1; //Массив банковских счетов.
|
||||
}
|
||||
|
||||
//Банковский счeт.
|
||||
message BankAccount {
|
||||
string id = 1; //Идентификатор счeта.
|
||||
string name = 2; //Название счeта.
|
||||
repeated MoneyValue money = 3; //Список валютных позиций на счeте.
|
||||
google.protobuf.Timestamp opened_date = 4; //Дата открытия счeта в часовом поясе UTC.
|
||||
AccountType type = 5; //Тип счeта.
|
||||
}
|
||||
|
||||
message CurrencyTransferRequest {
|
||||
string from_account_id = 1 [(google.api.field_behavior) = REQUIRED]; // Номер счета списания.
|
||||
string to_account_id = 2 [(google.api.field_behavior) = REQUIRED]; // Номер счета зачисления.
|
||||
MoneyValue amount = 3 [(google.api.field_behavior) = REQUIRED]; // Сумма перевода с указанием валюты.
|
||||
string transaction_id = 4; // Идентификатор запроса выставления поручения для целей идемпотентности в формате UUID.
|
||||
}
|
||||
|
||||
message CurrencyTransferResponse {
|
||||
|
||||
}
|
||||
|
||||
message PayInRequest {
|
||||
string from_account_id = 1 [(google.api.field_behavior) = REQUIRED]; // Номер счета списания.
|
||||
string to_account_id = 2 [(google.api.field_behavior) = REQUIRED]; // Номер брокерского счета зачисления.
|
||||
MoneyValue amount = 3 [(google.api.field_behavior) = REQUIRED]; // Сумма перевода с указанием валюты.
|
||||
}
|
||||
|
||||
message PayInResponse {
|
||||
|
||||
}
|
||||
|
||||
enum AccountValue {
|
||||
ACCOUNT_VALUE_UNSPECIFIED = 0; // Не определён.
|
||||
ACCOUNT_VALUE_MARGIN_FEE = 1; // Размер комиссии за маржинальное кредитование.
|
||||
ACCOUNT_VALUE_AMOUNT_WITHOUT_EXTRA_FEE = 2; // Остаток доступного лимита с текущей комиссией.
|
||||
}
|
||||
|
||||
message GetAccountValuesRequest {
|
||||
repeated string accounts = 1; // Массив счетов пользователя.
|
||||
repeated AccountValue values = 2; // Массив запрашиваемых параметров.
|
||||
}
|
||||
|
||||
message GetAccountValuesResponse {
|
||||
repeated AccountValuesWithParameters accounts = 1; // Массив счетов с параметрами.
|
||||
}
|
||||
|
||||
message AccountValuesWithParameters {
|
||||
string account_id = 1; // Номер счета.
|
||||
repeated InstrumentParameter values = 2; // Массив параметров инструмента.
|
||||
}
|
||||
|
||||
message InstrumentParameter {
|
||||
AccountValue name = 1; // Тип запрашиваемого параметра.
|
||||
MoneyValue value = 2; // Значение запрашиваемого параметра.
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
HttpCode,
|
||||
Put,
|
||||
Req,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiBearerAuth,
|
||||
ApiNoContentResponse,
|
||||
ApiOkResponse,
|
||||
ApiTags,
|
||||
} from '@nestjs/swagger';
|
||||
import type { Request } from 'express';
|
||||
import { AccessPayload } from '../auth/auth.types';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import {
|
||||
PutTbankConnectionDto,
|
||||
TbankConnectionStatusDto,
|
||||
} from './tbank-connection.dto';
|
||||
import { TbankConnectionService } from './tbank-connection.service';
|
||||
|
||||
@ApiTags('tbank-connection')
|
||||
@ApiBearerAuth()
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Controller('tbank-connection')
|
||||
export class TbankConnectionController {
|
||||
constructor(private readonly connection: TbankConnectionService) {}
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({ type: TbankConnectionStatusDto })
|
||||
status(@Req() request: Request & { user: AccessPayload }) {
|
||||
return this.connection.status(request.user.sub);
|
||||
}
|
||||
|
||||
@Put()
|
||||
@ApiOkResponse({ type: TbankConnectionStatusDto })
|
||||
put(
|
||||
@Req() request: Request & { user: AccessPayload },
|
||||
@Body() body: PutTbankConnectionDto,
|
||||
) {
|
||||
return this.connection.put(request.user.sub, body.token);
|
||||
}
|
||||
|
||||
@Delete()
|
||||
@HttpCode(204)
|
||||
@ApiNoContentResponse()
|
||||
delete(@Req() request: Request & { user: AccessPayload }): Promise<void> {
|
||||
return this.connection.delete(request.user.sub);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { IsNotEmpty, IsString, MaxLength } from 'class-validator';
|
||||
|
||||
export class PutTbankConnectionDto {
|
||||
@ApiProperty({ writeOnly: true })
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@MaxLength(4096)
|
||||
token: string;
|
||||
}
|
||||
|
||||
export class TbankConnectionStatusDto {
|
||||
@ApiProperty()
|
||||
connected: boolean;
|
||||
|
||||
@ApiProperty({ nullable: true, format: 'date-time' })
|
||||
connectedAt: string | null;
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TbankClient } from './tbank.client';
|
||||
import { TbankConnectionController } from './tbank-connection.controller';
|
||||
import { TbankConnectionService } from './tbank-connection.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TbankConnectionController],
|
||||
providers: [TbankClient, TbankConnectionService],
|
||||
})
|
||||
export class TbankConnectionModule {}
|
||||
@@ -0,0 +1,45 @@
|
||||
import { BadRequestException } from '@nestjs/common';
|
||||
|
||||
jest.mock('../prisma/prisma.service', () => ({
|
||||
PrismaService: class PrismaService {},
|
||||
}));
|
||||
|
||||
import { TbankConnectionService } from './tbank-connection.service';
|
||||
|
||||
describe('TbankConnectionService', () => {
|
||||
const prisma = {
|
||||
tbankConnection: {
|
||||
findUnique: jest.fn(),
|
||||
upsert: jest.fn(),
|
||||
deleteMany: jest.fn(),
|
||||
},
|
||||
};
|
||||
const config = {
|
||||
getOrThrow: jest.fn(() => 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY='),
|
||||
};
|
||||
const tbank = { validateToken: jest.fn() };
|
||||
const service = new TbankConnectionService(
|
||||
prisma as never,
|
||||
config as never,
|
||||
tbank,
|
||||
);
|
||||
|
||||
beforeEach(() => jest.clearAllMocks());
|
||||
|
||||
it('does not persist an invalid token', async () => {
|
||||
tbank.validateToken.mockRejectedValue(new Error('INVALID_TBANK_TOKEN'));
|
||||
|
||||
await expect(service.put('user-1', ' token ')).rejects.toEqual(
|
||||
expect.any(BadRequestException),
|
||||
);
|
||||
expect(prisma.tbankConnection.upsert).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('deletes a connection idempotently', async () => {
|
||||
await service.delete('user-1');
|
||||
|
||||
expect(prisma.tbankConnection.deleteMany).toHaveBeenCalledWith({
|
||||
where: { userId: 'user-1' },
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,64 @@
|
||||
import { BadRequestException, Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { Environment } from '../config/env.validation';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { TbankClient } from './tbank.client';
|
||||
import { TbankCrypto } from './tbank.crypto';
|
||||
|
||||
@Injectable()
|
||||
export class TbankConnectionService {
|
||||
private readonly crypto: TbankCrypto;
|
||||
|
||||
constructor(
|
||||
private readonly prisma: PrismaService,
|
||||
config: ConfigService<Environment, true>,
|
||||
private readonly tbank: TbankClient,
|
||||
) {
|
||||
this.crypto = TbankCrypto.fromBase64(
|
||||
config.getOrThrow('TOKEN_ENCRYPTION_KEY'),
|
||||
);
|
||||
}
|
||||
|
||||
async status(userId: string) {
|
||||
const connection = await this.prisma.tbankConnection.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
return {
|
||||
connected: Boolean(connection),
|
||||
connectedAt: connection?.connectedAt.toISOString() ?? null,
|
||||
};
|
||||
}
|
||||
|
||||
async put(userId: string, value: string) {
|
||||
const token = value.trim();
|
||||
if (!token) throw new BadRequestException({ code: 'VALIDATION_ERROR' });
|
||||
try {
|
||||
await this.tbank.validateToken(token);
|
||||
} catch (error) {
|
||||
if (error instanceof Error && error.message === 'INVALID_TBANK_TOKEN') {
|
||||
throw new BadRequestException({ code: 'INVALID_TBANK_TOKEN' });
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const connection = await this.prisma.tbankConnection.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
encryptedToken: this.crypto.encrypt(token),
|
||||
connectedAt: new Date(),
|
||||
},
|
||||
update: {
|
||||
encryptedToken: this.crypto.encrypt(token),
|
||||
connectedAt: new Date(),
|
||||
},
|
||||
});
|
||||
return {
|
||||
connected: true,
|
||||
connectedAt: connection.connectedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
async delete(userId: string): Promise<void> {
|
||||
await this.prisma.tbankConnection.deleteMany({ where: { userId } });
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
import { Injectable, Logger, ServiceUnavailableException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import {
|
||||
ChannelCredentials,
|
||||
Client,
|
||||
ClientUnaryCall,
|
||||
loadPackageDefinition,
|
||||
Metadata,
|
||||
ServiceError,
|
||||
status,
|
||||
} from '@grpc/grpc-js';
|
||||
import { loadSync } from '@grpc/proto-loader';
|
||||
import { existsSync, readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Environment } from '../config/env.validation';
|
||||
|
||||
type GetAccounts = (
|
||||
request: { status: string },
|
||||
metadata: Metadata,
|
||||
options: { deadline: Date },
|
||||
callback: (error: ServiceError | null) => void,
|
||||
) => ClientUnaryCall;
|
||||
type UsersClient = Client & { getAccounts: GetAccounts };
|
||||
type TbankNamespace = {
|
||||
tinkoff: {
|
||||
public: {
|
||||
invest: {
|
||||
api: {
|
||||
contract: { v1: { UsersService: unknown } };
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
class RateLimiter {
|
||||
private readonly waiting: (() => void)[] = [];
|
||||
private starts = 0;
|
||||
private windowStartedAt = Date.now();
|
||||
|
||||
constructor(private readonly perSecond: number) {}
|
||||
|
||||
get size(): number {
|
||||
return this.waiting.length;
|
||||
}
|
||||
|
||||
async run<T>(operation: () => Promise<T>): Promise<T> {
|
||||
await new Promise<void>((resolve) => {
|
||||
this.waiting.push(resolve);
|
||||
this.drain();
|
||||
});
|
||||
try {
|
||||
return await operation();
|
||||
} finally {
|
||||
this.drain();
|
||||
}
|
||||
}
|
||||
|
||||
private drain(): void {
|
||||
const now = Date.now();
|
||||
if (now - this.windowStartedAt >= 1000) {
|
||||
this.windowStartedAt = now;
|
||||
this.starts = 0;
|
||||
}
|
||||
while (this.starts < this.perSecond && this.waiting.length) {
|
||||
this.starts += 1;
|
||||
this.waiting.shift()?.();
|
||||
}
|
||||
if (this.waiting.length && this.starts === this.perSecond) {
|
||||
setTimeout(
|
||||
() => this.drain(),
|
||||
Math.max(1, 1000 - (now - this.windowStartedAt)),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class TbankClient {
|
||||
private readonly logger = new Logger(TbankClient.name);
|
||||
private readonly queue: RateLimiter;
|
||||
private readonly client: UsersClient;
|
||||
|
||||
constructor(config: ConfigService<Environment, true>) {
|
||||
const sourceProtoRoot = join(process.cwd(), 'src/tbank/proto/contracts');
|
||||
const builtProtoRoot = join(process.cwd(), 'dist/tbank/proto/contracts');
|
||||
const protoRoot = existsSync(sourceProtoRoot)
|
||||
? sourceProtoRoot
|
||||
: builtProtoRoot;
|
||||
const definition = loadSync(join(protoRoot, 'users.proto'), {
|
||||
includeDirs: [protoRoot],
|
||||
keepCase: false,
|
||||
longs: String,
|
||||
enums: String,
|
||||
defaults: true,
|
||||
oneofs: true,
|
||||
});
|
||||
const packageDefinition = loadPackageDefinition(
|
||||
definition,
|
||||
) as unknown as TbankNamespace;
|
||||
const Service = packageDefinition.tinkoff.public.invest.api.contract.v1
|
||||
.UsersService as new (
|
||||
address: string,
|
||||
credentials: ChannelCredentials,
|
||||
) => UsersClient;
|
||||
const caPath = config.get<string | undefined>('TBANK_CA_CERT_PATH');
|
||||
const credentials = caPath
|
||||
? ChannelCredentials.createSsl(readFileSync(caPath))
|
||||
: ChannelCredentials.createSsl();
|
||||
this.client = new Service(
|
||||
config.getOrThrow('TBANK_GRPC_ENDPOINT'),
|
||||
credentials,
|
||||
);
|
||||
this.queue = new RateLimiter(
|
||||
config.getOrThrow('TBANK_RATE_LIMIT_PER_SECOND'),
|
||||
);
|
||||
this.logger.log({
|
||||
event: 'client_configured',
|
||||
endpoint: config.getOrThrow('TBANK_GRPC_ENDPOINT'),
|
||||
rateLimitPerSecond: config.getOrThrow('TBANK_RATE_LIMIT_PER_SECOND'),
|
||||
customCa: Boolean(caPath),
|
||||
});
|
||||
}
|
||||
|
||||
async validateToken(token: string): Promise<void> {
|
||||
await this.call(token, true);
|
||||
}
|
||||
|
||||
private async call(token: string, retry: boolean): Promise<void> {
|
||||
if (this.queue.size >= 50) {
|
||||
this.logger.warn({ event: 'queue_full', waiting: this.queue.size });
|
||||
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
|
||||
}
|
||||
try {
|
||||
this.logger.debug({ event: 'get_accounts_started', retry: !retry });
|
||||
await this.queue.run(() => this.request(token));
|
||||
this.logger.debug({ event: 'get_accounts_succeeded', retry: !retry });
|
||||
} catch (error) {
|
||||
const code = (error as ServiceError).code;
|
||||
this.logger.warn({
|
||||
event: 'get_accounts_failed',
|
||||
grpcCode: code,
|
||||
retry: !retry,
|
||||
retryable: [
|
||||
status.UNAVAILABLE,
|
||||
status.DEADLINE_EXCEEDED,
|
||||
status.RESOURCE_EXHAUSTED,
|
||||
].includes(code),
|
||||
});
|
||||
if (
|
||||
retry &&
|
||||
[
|
||||
status.UNAVAILABLE,
|
||||
status.DEADLINE_EXCEEDED,
|
||||
status.RESOURCE_EXHAUSTED,
|
||||
].includes(code)
|
||||
) {
|
||||
this.logger.log({ event: 'get_accounts_retry_scheduled', delayMs: 1000 });
|
||||
await new Promise((resolve) => setTimeout(resolve, 1000));
|
||||
return this.call(token, false);
|
||||
}
|
||||
if ([status.UNAUTHENTICATED, status.PERMISSION_DENIED].includes(code)) {
|
||||
throw new Error('INVALID_TBANK_TOKEN');
|
||||
}
|
||||
throw new ServiceUnavailableException({ code: 'TBANK_UNAVAILABLE' });
|
||||
}
|
||||
}
|
||||
|
||||
private request(token: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const metadata = new Metadata();
|
||||
metadata.set('Authorization', `Bearer ${token}`);
|
||||
this.client.getAccounts(
|
||||
{ status: 'ACCOUNT_STATUS_OPEN' },
|
||||
metadata,
|
||||
{ deadline: new Date(Date.now() + 10_000) },
|
||||
(error) => (error ? reject(error) : resolve()),
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { TbankCrypto } from './tbank.crypto';
|
||||
|
||||
describe('TbankCrypto', () => {
|
||||
it('round-trips a token and rejects a malformed key', () => {
|
||||
const crypto = TbankCrypto.fromBase64(
|
||||
'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=',
|
||||
);
|
||||
const encrypted = crypto.encrypt('secret-token');
|
||||
|
||||
expect(encrypted).not.toContain('secret-token');
|
||||
expect(crypto.decrypt(encrypted)).toBe('secret-token');
|
||||
expect(() => TbankCrypto.fromBase64('invalid')).toThrow(
|
||||
'Invalid token encryption key',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,45 @@
|
||||
import { createCipheriv, createDecipheriv, randomBytes } from 'node:crypto';
|
||||
|
||||
const VERSION = 1;
|
||||
const NONCE_SIZE = 12;
|
||||
const TAG_SIZE = 16;
|
||||
|
||||
export class TbankCrypto {
|
||||
constructor(private readonly key: Buffer) {}
|
||||
|
||||
static fromBase64(key: string): TbankCrypto {
|
||||
const decoded = Buffer.from(key, 'base64');
|
||||
if (decoded.length !== 32) throw new Error('Invalid token encryption key');
|
||||
return new TbankCrypto(decoded);
|
||||
}
|
||||
|
||||
encrypt(value: string): string {
|
||||
const nonce = randomBytes(NONCE_SIZE);
|
||||
const cipher = createCipheriv('aes-256-gcm', this.key, nonce);
|
||||
const ciphertext = Buffer.concat([
|
||||
cipher.update(value, 'utf8'),
|
||||
cipher.final(),
|
||||
]);
|
||||
return Buffer.concat([
|
||||
Buffer.from([VERSION]),
|
||||
nonce,
|
||||
ciphertext,
|
||||
cipher.getAuthTag(),
|
||||
]).toString('base64');
|
||||
}
|
||||
|
||||
decrypt(payload: string): string {
|
||||
const bytes = Buffer.from(payload, 'base64');
|
||||
if (bytes[0] !== VERSION || bytes.length < 1 + NONCE_SIZE + TAG_SIZE) {
|
||||
throw new Error('Invalid encrypted token');
|
||||
}
|
||||
const nonce = bytes.subarray(1, 1 + NONCE_SIZE);
|
||||
const tag = bytes.subarray(-TAG_SIZE);
|
||||
const decipher = createDecipheriv('aes-256-gcm', this.key, nonce);
|
||||
decipher.setAuthTag(tag);
|
||||
return Buffer.concat([
|
||||
decipher.update(bytes.subarray(1 + NONCE_SIZE, -TAG_SIZE)),
|
||||
decipher.final(),
|
||||
]).toString('utf8');
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,7 @@ describe('Authentication (e2e)', () => {
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
await app.init();
|
||||
await app.get(PrismaService).session.deleteMany();
|
||||
await app.get(PrismaService).tbankConnection.deleteMany();
|
||||
await app.get(PrismaService).user.deleteMany();
|
||||
});
|
||||
|
||||
@@ -82,4 +83,23 @@ describe('Authentication (e2e)', () => {
|
||||
expect((body as Problem).code).toBe('INVALID_CREDENTIALS'),
|
||||
);
|
||||
});
|
||||
|
||||
it('guards T-Bank connection and never returns a token', async () => {
|
||||
const registration = await request(app.getHttpServer())
|
||||
.post('/api/v1/auth/register')
|
||||
.send({ email: 'connection@example.com', password: 'a secure password' })
|
||||
.expect(201);
|
||||
const accessToken = (registration.body as Authentication).accessToken;
|
||||
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/v1/tbank-connection')
|
||||
.expect(401);
|
||||
await request(app.getHttpServer())
|
||||
.get('/api/v1/tbank-connection')
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.expect(200)
|
||||
.expect(({ body }: { body: unknown }) =>
|
||||
expect(body).toEqual({ connected: false, connectedAt: null }),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
process.env.DATABASE_URL = 'file:/tmp/wealth-ledger-e2e.db';
|
||||
process.env.FRONTEND_ORIGIN = 'http://localhost:5173';
|
||||
process.env.JWT_SECRET = 'e2e-secret-that-is-at-least-thirty-two-characters';
|
||||
process.env.TOKEN_ENCRYPTION_KEY = 'MDEyMzQ1Njc4OWFiY2RlZjAxMjM0NTY3ODlhYmNkZWY=';
|
||||
|
||||
Reference in New Issue
Block a user