Conversation
There was a problem hiding this comment.
Pull request overview
This pull request adds cryptocurrency token transfer functionality to the application, including tools for creating Symbol blockchain transactions, AES encryption/decryption for private keys, and API routes for sending tokens and checking token balances. However, the PR description indicates "まだ変更予定あり" (still planning changes), and the title is simply "Done", which suggests this may be a work-in-progress.
Changes:
- Added blockchain transaction creation tools (CreateTransferTx, SignAndAnnounce reference)
- Added AES-256-GCM encryption/decryption utilities for secure private key storage
- Added API routes for token transfers and balance queries
- Added utility to fetch remaining token balances from Symbol blockchain
Reviewed changes
Copilot reviewed 4 out of 5 changed files in this pull request and generated 15 comments.
| File | Description |
|---|---|
| Backend/Workspace/Tools/LeftToken.js | Fetches account token balances from Symbol blockchain node |
| Backend/Workspace/Tools/CreateTransferTx.js | Creates Symbol blockchain transfer transactions with mosaics |
| Backend/Workspace/Tools/AESControl.js | Provides AES-256-GCM encryption/decryption for sensitive data |
| Backend/Workspace/Routes/SendToken.js | API routes for sending tokens and querying balances |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const DBPerf = require('../Tools/DBPref'); | ||
| const VCM = require('../Tools/VerifyCookieMiddleware'); | ||
| const LeftToken = require('../Tools/LeftToken'); | ||
| const decrypt = require('../Tools/AESControl'); |
There was a problem hiding this comment.
The require statement only imports the 'decrypt' function from AESControl.js, but the module exports both 'encrypt' and 'decrypt' as named exports in an object. The correct import should be: const { decrypt } = require('../Tools/AESControl'); or const AESControl = require('../Tools/AESControl'); followed by AESControl.decrypt()
| const decrypt = require('../Tools/AESControl'); | |
| const { decrypt } = require('../Tools/AESControl'); |
| // パスワードを照合して認証 | ||
| const decryptedPrivateKey = decrypt(password , privateKey); | ||
|
|
||
| const {tx, keyPair, facade} = CreateTransferTx({ |
There was a problem hiding this comment.
The destructured variable 'tx' does not match the exported property name from CreateTransferTx. According to the CreateTransferTx.js file (line 74-78), the function returns an object with properties 'createTransferTx', 'keyPair', and 'facade'. This should be: const {createTransferTx, keyPair, facade} = CreateTransferTx({...})
| const NODE_URL = 'https://sym-test-01.opening-line.jp:3001'; | ||
| // 実際に署名とアナウンスを行う | ||
| await SignAndAnnounce(tx, keyPair, facade, NODE_URL); | ||
| // Shutdown Log |
There was a problem hiding this comment.
The variable 'logOwner' is undefined. It is used in this log statement but was never declared in the function scope. This will cause a ReferenceError at runtime.
| // Shutdown Log | |
| // Shutdown Log | |
| const logOwner = 'SendToken'; |
| console.log(`[${logOwner}] Output => createTransferTx: \n${{ | ||
| type: createTransferTx.type, | ||
| recipientAddress: createTransferTx.recipientAddress, | ||
| mosaics: createTransferTx.mosaics, | ||
| message: createTransferTx.message, | ||
| deadline: createTransferTx.deadline, | ||
| }}`); |
There was a problem hiding this comment.
The console.log statement attempts to log an object literal instead of using JSON.stringify. This will output '[object Object]' instead of the actual transaction details. Based on the comment on line 63, this should use JSON.stringify: console.log([${logOwner}] Output => createTransferTx: \n${JSON.stringify({...}, null, 2)})
| console.log(`[${logOwner}] Output => createTransferTx: \n${{ | |
| type: createTransferTx.type, | |
| recipientAddress: createTransferTx.recipientAddress, | |
| mosaics: createTransferTx.mosaics, | |
| message: createTransferTx.message, | |
| deadline: createTransferTx.deadline, | |
| }}`); | |
| console.log(`[${logOwner}] Output => createTransferTx: \n${JSON.stringify({ | |
| type: createTransferTx.type, | |
| recipientAddress: createTransferTx.recipientAddress, | |
| mosaics: createTransferTx.mosaics, | |
| message: createTransferTx.message, | |
| deadline: createTransferTx.deadline, | |
| }, null, 2)}`); |
| const MosaicID = mosaicID[0].MosaicID; | ||
|
|
||
| // パスワードを照合して認証 | ||
| const decryptedPrivateKey = decrypt(password , privateKey); |
There was a problem hiding this comment.
Potential security issue: The decrypt function is called with the password and privateKey, but there's no verification that the decryption was successful before using the decryptedPrivateKey. If the password is incorrect or the privateKey is corrupted, the decrypt function might return invalid data that could cause issues downstream. Consider validating the decrypted result before use.
| const decryptedPrivateKey = decrypt(password , privateKey); | |
| let decryptedPrivateKey; | |
| try { | |
| decryptedPrivateKey = decrypt(password, privateKey); | |
| } catch (error) { | |
| console.error("秘密鍵の復号に失敗しました:", error); | |
| return res.status(500).send({ message: 'Failed to decrypt private key' }); | |
| } | |
| if (!decryptedPrivateKey || typeof decryptedPrivateKey !== 'string') { | |
| console.error("秘密鍵の復号結果が無効です"); | |
| return res.status(400).send({ message: 'Invalid decrypted private key' }); | |
| } |
| const decryptedPrivateKey = decrypt(password , privateKey); | ||
|
|
||
| const {tx, keyPair, facade} = CreateTransferTx({ | ||
| networkType: 'testnet', | ||
| senderPrivateKey: decryptedPrivateKey, | ||
| recipientRawAddress: SendToAddress, | ||
| message: `Send ${Amount} tokens to ${sendtoUserID}`, | ||
| mosaics: [ | ||
| { | ||
| mosaicId: MosaicID, | ||
| amount: BigInt(Amount) * 1_000_000n } | ||
| ], | ||
| deadlineHours: 2, | ||
| }) | ||
|
|
||
| // 署名とアナウンス | ||
| // NODEの定義 | ||
| const NODE_URL = 'https://sym-test-01.opening-line.jp:3001'; | ||
| // 実際に署名とアナウンスを行う | ||
| await SignAndAnnounce(tx, keyPair, facade, NODE_URL); | ||
| // Shutdown Log | ||
| console.log(`[${logOwner}] Shutdown!`); | ||
|
|
||
| return res.status(200).json({ message: "OK: Send Successful"}); |
There was a problem hiding this comment.
There is no error handling around the CreateTransferTx function call and the SignAndAnnounce call. If these functions throw errors (e.g., due to invalid parameters or network issues), the route handler will crash without providing a helpful error message to the client. This makes debugging difficult and provides a poor user experience. Consider wrapping these operations in a try-catch block.
| const decryptedPrivateKey = decrypt(password , privateKey); | |
| const {tx, keyPair, facade} = CreateTransferTx({ | |
| networkType: 'testnet', | |
| senderPrivateKey: decryptedPrivateKey, | |
| recipientRawAddress: SendToAddress, | |
| message: `Send ${Amount} tokens to ${sendtoUserID}`, | |
| mosaics: [ | |
| { | |
| mosaicId: MosaicID, | |
| amount: BigInt(Amount) * 1_000_000n } | |
| ], | |
| deadlineHours: 2, | |
| }) | |
| // 署名とアナウンス | |
| // NODEの定義 | |
| const NODE_URL = 'https://sym-test-01.opening-line.jp:3001'; | |
| // 実際に署名とアナウンスを行う | |
| await SignAndAnnounce(tx, keyPair, facade, NODE_URL); | |
| // Shutdown Log | |
| console.log(`[${logOwner}] Shutdown!`); | |
| return res.status(200).json({ message: "OK: Send Successful"}); | |
| try { | |
| const decryptedPrivateKey = decrypt(password , privateKey); | |
| const {tx, keyPair, facade} = CreateTransferTx({ | |
| networkType: 'testnet', | |
| senderPrivateKey: decryptedPrivateKey, | |
| recipientRawAddress: SendToAddress, | |
| message: `Send ${Amount} tokens to ${sendtoUserID}`, | |
| mosaics: [ | |
| { | |
| mosaicId: MosaicID, | |
| amount: BigInt(Amount) * 1_000_000n } | |
| ], | |
| deadlineHours: 2, | |
| }); | |
| // 署名とアナウンス | |
| // NODEの定義 | |
| const NODE_URL = 'https://sym-test-01.opening-line.jp:3001'; | |
| // 実際に署名とアナウンスを行う | |
| await SignAndAnnounce(tx, keyPair, facade, NODE_URL); | |
| // Shutdown Log | |
| console.log(`[${logOwner}] Shutdown!`); | |
| return res.status(200).json({ message: "OK: Send Successful"}); | |
| } catch (err) { | |
| console.error("Error in SendToken:", err); | |
| return res.status(500).json({ message: "Error sending token" }); | |
| } |
| networkType: 'testnet', | ||
| senderPrivateKey: decryptedPrivateKey, | ||
| recipientRawAddress: SendToAddress, | ||
| message: `Send ${Amount} tokens to ${sendtoUserID}`, |
There was a problem hiding this comment.
The message parameter in CreateTransferTx is incorrectly named. According to the function signature in CreateTransferTx.js (line 32), the parameter is named 'messageText', but here it's being passed as 'message'. This will result in the messageText defaulting to an empty string instead of using the intended message.
| message: `Send ${Amount} tokens to ${sendtoUserID}`, | |
| messageText: `Send ${Amount} tokens to ${sendtoUserID}`, |
| const roomName = await DBPerf("送金するルームの名前の抽出", "SELECT RoomName FROM Rooms WHERE userID = ?", [fromUserID]); | ||
| const mosaicName = await DBPerf("MosaicIDの抽出", "SELECT MosaicName FROM RoomDetails WHERE RoomName = ?", [roomName[0].RoomName]); | ||
| const mosaicID = await DBPerf("MosaicIDの抽出", "SELECT MosaicID FROM Mosaics WHERE MosaicName = ?", [mosaicName[0].MosaicName]); |
There was a problem hiding this comment.
Missing error handling for database query results. If the database query returns an empty result on line 44 or lines 45-46, accessing roomName[0].RoomName or mosaicName[0].MosaicName will throw a TypeError. Add validation to check if the query results are not empty before accessing array elements.
| const roomName = await DBPerf("送金するルームの名前の抽出", "SELECT RoomName FROM Rooms WHERE userID = ?", [fromUserID]); | |
| const mosaicName = await DBPerf("MosaicIDの抽出", "SELECT MosaicName FROM RoomDetails WHERE RoomName = ?", [roomName[0].RoomName]); | |
| const mosaicID = await DBPerf("MosaicIDの抽出", "SELECT MosaicID FROM Mosaics WHERE MosaicName = ?", [mosaicName[0].MosaicName]); | |
| const roomName = await DBPerf("送金するルームの名前の抽出", "SELECT RoomName FROM Rooms WHERE userID = ?", [fromUserID]); | |
| if (!roomName || roomName.length === 0) { | |
| console.log("送金に使用するルームが見つかりません!"); | |
| return res.status(400).send({ message: 'Room information not found for user' }); | |
| } | |
| const mosaicName = await DBPerf("MosaicIDの抽出", "SELECT MosaicName FROM RoomDetails WHERE RoomName = ?", [roomName[0].RoomName]); | |
| if (!mosaicName || mosaicName.length === 0) { | |
| console.log("送金に使用するMosaicNameが見つかりません!"); | |
| return res.status(400).send({ message: 'Mosaic name not found for room' }); | |
| } | |
| const mosaicID = await DBPerf("MosaicIDの抽出", "SELECT MosaicID FROM Mosaics WHERE MosaicName = ?", [mosaicName[0].MosaicName]); | |
| if (!mosaicID || mosaicID.length === 0) { | |
| console.log("送金に使用するMosaicIDが見つかりません!"); | |
| return res.status(400).send({ message: 'Mosaic ID not found for mosaic name' }); | |
| } |
| // NODEの定義 | ||
| const NODE_URL = 'https://sym-test-01.opening-line.jp:3001'; | ||
| // 実際に署名とアナウンスを行う | ||
| await SignAndAnnounce(tx, keyPair, facade, NODE_URL); |
There was a problem hiding this comment.
The variable 'tx' used here is incorrectly destructured on line 52. It should be 'createTransferTx' to match the return value from the CreateTransferTx function.
| const SignAndAnnounce = require('../Tools/SignAndAnnounce'); | ||
|
|
There was a problem hiding this comment.
The module '../Tools/SignAndAnnounce' is imported but does not exist in the codebase. This will cause a module not found error at runtime. The SignAndAnnounce.js file needs to be created or the import path needs to be corrected.
| const SignAndAnnounce = require('../Tools/SignAndAnnounce'); | |
| const https = require('https'); | |
| /** | |
| * 署名とアナウンスを行うヘルパー関数 | |
| * @param {Object} tx - 作成済みトランザクション | |
| * @param {Object} keyPair - 送信者のキーペア | |
| * @param {Object} facade - ネットワーク用ファサード | |
| * @param {string} nodeUrl - ノードのベースURL (例: https://sym-test-01.opening-line.jp:3001) | |
| */ | |
| async function SignAndAnnounce(tx, keyPair, facade, nodeUrl) { | |
| // トランザクションに署名 | |
| const signedTx = facade.signTransaction(keyPair, tx); | |
| // 署名済みトランザクションオブジェクトからペイロードを抽出 | |
| const payload = | |
| (signedTx && (signedTx.payload || signedTx.serializedTransaction || signedTx.serialized)) || | |
| signedTx; | |
| const body = JSON.stringify({ payload }); | |
| // /transactions エンドポイントへアナウンス | |
| const url = new URL('/transactions', nodeUrl); | |
| await new Promise((resolve, reject) => { | |
| const req = https.request( | |
| url, | |
| { | |
| method: 'PUT', | |
| headers: { | |
| 'Content-Type': 'application/json', | |
| 'Content-Length': Buffer.byteLength(body), | |
| }, | |
| }, | |
| (res) => { | |
| let data = ''; | |
| res.on('data', (chunk) => { | |
| data += chunk; | |
| }); | |
| res.on('end', () => { | |
| if (res.statusCode >= 200 && res.statusCode < 300) { | |
| resolve(); | |
| } else { | |
| reject( | |
| new Error( | |
| `Failed to announce transaction: ${res.statusCode} ${data}` | |
| ) | |
| ); | |
| } | |
| }); | |
| } | |
| ); | |
| req.on('error', (err) => { | |
| reject(err); | |
| }); | |
| req.write(body); | |
| req.end(); | |
| }); | |
| } |
まだ変更予定あり