From 719f6ba8be95a00742a7894a7f2b6429fa271103 Mon Sep 17 00:00:00 2001 From: Michael Kim Date: Fri, 1 Nov 2024 15:44:47 +0900 Subject: [PATCH] Add endpoints (settlement, agent) --- .../contracts/controllers/LoyaltyBridge.sol | 73 +- .../contracts/controllers/LoyaltyProvider.sol | 30 +- .../contracts/interfaces/ILedger.sol | 6 +- .../contracts/contracts/ledger/Ledger.sol | 63 +- .../contracts/ledger/LedgerStorage.sol | 4 +- packages/contracts/contracts/shop/Shop.sol | 141 +- .../deploy/side_chain_devnet/deploy.ts | 8 +- packages/contracts/package.json | 4 +- packages/contracts/src/utils/ContractUtils.ts | 13 +- packages/contracts/test/04-Ledger.test.ts | 577 ++++++- packages/contracts/test/05-Bridge.test.ts | 221 ++- .../contracts/test/08-Ledger-Provider.test.ts | 22 +- packages/library/package.json | 2 +- packages/relay/package.json | 6 +- packages/relay/scripts/provider/send.ts | 4 +- .../relay/scripts/provider/send_to_phone.ts | 4 +- packages/relay/src/DefaultServer.ts | 13 + packages/relay/src/routers/AgentRouter.ts | 318 ++++ packages/relay/src/routers/BridgeRouter.ts | 4 +- packages/relay/src/routers/HistoryRouter.ts | 3 +- packages/relay/src/routers/LedgerRouter.ts | 27 +- packages/relay/src/routers/PaymentRouter.ts | 5 +- packages/relay/src/routers/ProviderRouter.ts | 11 +- packages/relay/src/routers/ShopRouter.ts | 399 ++++- .../relay/src/routers/StorePurchaseRouter.ts | 2 - packages/relay/src/routers/TaskRouter.ts | 1 - packages/relay/src/routers/TokenRouter.ts | 253 +++- .../scheduler/DelegatorApprovalScheduler.ts | 1 - .../relay/src/scheduler/WatchScheduler.ts | 1 - packages/relay/src/storage/GraphStorage.ts | 62 + packages/relay/src/storage/graph/shop.xml | 42 + packages/relay/src/types/index.ts | 16 + packages/relay/src/utils/ContractUtils.ts | 66 +- packages/relay/test/Agent.test.ts | 173 +++ packages/relay/test/LoyaltyProvider.test.ts | 2 +- packages/relay/test/LoyaltyProvider2.test.ts | 341 +++++ packages/relay/test/ShopWithdraw.test.ts | 1348 ++++++++++++++++- packages/relay/tspec/02_Shop.ts | 315 ++++ packages/relay/tspec/07_Summary.ts | 451 ++++++ packages/relay/tspec/10_Provider.ts | 10 +- packages/relay/tspec/11_Agent.ts | 293 ++++ .../manifest/subgraph.placeholder.yaml | 3 + packages/subgraph-sidechain/schema.graphql | 17 + packages/subgraph-sidechain/src/shop.ts | 23 +- yarn.lock | 14 +- 45 files changed, 5156 insertions(+), 236 deletions(-) create mode 100644 packages/relay/src/routers/AgentRouter.ts create mode 100644 packages/relay/test/Agent.test.ts create mode 100644 packages/relay/test/LoyaltyProvider2.test.ts create mode 100644 packages/relay/tspec/11_Agent.ts diff --git a/packages/contracts/contracts/controllers/LoyaltyBridge.sol b/packages/contracts/contracts/controllers/LoyaltyBridge.sol index 0d52a537..fd9ec6c8 100644 --- a/packages/contracts/contracts/controllers/LoyaltyBridge.sol +++ b/packages/contracts/contracts/controllers/LoyaltyBridge.sol @@ -88,29 +88,64 @@ contract LoyaltyBridge is LoyaltyBridgeStorage, Initializable, OwnableUpgradeabl require(_tokenId == tokenId, "1713"); require(_account != systemAccount, "1053"); - bytes32 dataHash = keccak256( - abi.encode( - block.chainid, - address(tokenContract), - _account, - address(this), - _amount, - ledgerContract.nonceOf(_account), - _expiry - ) + address account = _account; + uint256 amount = _amount; + uint256 expiry = _expiry; + address signer; + address recurve1 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + block.chainid, + address(tokenContract), + account, + address(this), + amount, + ledgerContract.nonceOf(account), + expiry + ) + ) + ), + _signature ); - require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "1501"); - require(_expiry > block.timestamp, "1506"); - require(ledgerContract.tokenBalanceOf(_account) >= _amount, "1511"); - require(_amount % 1 gwei == 0, "1030"); - require(_amount > protocolFee, "1031"); - ledgerContract.transferToken(_account, address(this), _amount); - ledgerContract.increaseNonce(_account); + if (recurve1 == account) { + signer = account; + } else { + address agent = ledgerContract.withdrawalAgentOf(account); + require(agent != address(0x0), "1501"); + + address recurve2 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + block.chainid, + address(tokenContract), + account, + address(this), + amount, + ledgerContract.nonceOf(agent), + expiry + ) + ) + ), + _signature + ); + require(recurve2 == agent, "1501"); + signer = agent; + } + + require(expiry > block.timestamp, "1506"); + require(ledgerContract.tokenBalanceOf(account) >= amount, "1511"); + require(amount % 1 gwei == 0, "1030"); + require(amount > protocolFee, "1031"); + + ledgerContract.transferToken(account, address(this), amount); + ledgerContract.increaseNonce(account); - DepositData memory data = DepositData({ tokenId: _tokenId, account: _account, amount: _amount }); + DepositData memory data = DepositData({ tokenId: _tokenId, account: account, amount: amount }); deposits[_depositId] = data; - emit BridgeDeposited(_tokenId, _depositId, _account, _amount, ledgerContract.tokenBalanceOf(_account)); + emit BridgeDeposited(_tokenId, _depositId, account, amount, ledgerContract.tokenBalanceOf(account)); } /// @notice 브리지에서 자금을 인출합니다. 검증자들의 합의가 완료되면 인출이 됩니다. diff --git a/packages/contracts/contracts/controllers/LoyaltyProvider.sol b/packages/contracts/contracts/controllers/LoyaltyProvider.sol index 7358b35a..0cfd8be3 100644 --- a/packages/contracts/contracts/controllers/LoyaltyProvider.sol +++ b/packages/contracts/contracts/controllers/LoyaltyProvider.sol @@ -190,9 +190,9 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad ) ); address recover = ECDSA.recover(ECDSA.toEthSignedMessageHash(purchaseDataHash), data.signature); - address assistant = ledgerContract.assistantOf(data.sender); - if ((assistant == address(0x0)) && (recover != data.sender)) continue; - if ((assistant != address(0x0)) && (recover != assistant)) continue; + address agent = ledgerContract.provisionAgentOf(data.sender); + if ((agent == address(0x0)) && (recover != data.sender)) continue; + if ((agent != address(0x0)) && (recover != agent)) continue; uint256 loyaltyValue = data.loyalty; uint256 loyaltyPoint = currencyRateContract.convertCurrencyToPoint(loyaltyValue, data.currency); @@ -285,19 +285,17 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad if (recurve1 == _provider) { sender = _provider; } else { - address assistant = ledgerContract.assistantOf(_provider); - require(assistant != address(0x0), "1501"); + address agent = ledgerContract.provisionAgentOf(_provider); + require(agent != address(0x0), "1501"); address recurve2 = ECDSA.recover( ECDSA.toEthSignedMessageHash( - keccak256( - abi.encode(_provider, _receiver, _point, block.chainid, ledgerContract.nonceOf(assistant)) - ) + keccak256(abi.encode(_provider, _receiver, _point, block.chainid, ledgerContract.nonceOf(agent))) ), _signature ); - require(recurve2 == assistant, "1501"); - sender = assistant; + require(recurve2 == agent, "1501"); + sender = agent; } ledgerContract.providePoint( @@ -333,19 +331,17 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad if (recurve1 == _provider) { sender = _provider; } else { - address assistant = ledgerContract.assistantOf(_provider); - require(assistant != address(0x0), "1501"); + address agent = ledgerContract.provisionAgentOf(_provider); + require(agent != address(0x0), "1501"); address recurve2 = ECDSA.recover( ECDSA.toEthSignedMessageHash( - keccak256( - abi.encode(_provider, _phoneHash, _point, block.chainid, ledgerContract.nonceOf(assistant)) - ) + keccak256(abi.encode(_provider, _phoneHash, _point, block.chainid, ledgerContract.nonceOf(agent))) ), _signature ); - require(recurve2 == assistant, "1501"); - sender = assistant; + require(recurve2 == agent, "1501"); + sender = agent; } address receiver = linkContract.toAddress(_phoneHash); diff --git a/packages/contracts/contracts/interfaces/ILedger.sol b/packages/contracts/contracts/interfaces/ILedger.sol index f66a7fb7..3b593044 100644 --- a/packages/contracts/contracts/interfaces/ILedger.sol +++ b/packages/contracts/contracts/interfaces/ILedger.sol @@ -71,5 +71,9 @@ interface ILedger { function isProvider(address _account) external view returns (bool); - function assistantOf(address _account) external view returns (address); + function provisionAgentOf(address _account) external view returns (address); + + function refundAgentOf(address _account) external view returns (address); + + function withdrawalAgentOf(address _account) external view returns (address); } diff --git a/packages/contracts/contracts/ledger/Ledger.sol b/packages/contracts/contracts/ledger/Ledger.sol index 4f922653..00c77f5e 100644 --- a/packages/contracts/contracts/ledger/Ledger.sol +++ b/packages/contracts/contracts/ledger/Ledger.sol @@ -67,7 +67,9 @@ contract Ledger is LedgerStorage, Initializable, OwnableUpgradeable, UUPSUpgrade event RegisteredProvider(address provider); event UnregisteredProvider(address provider); - event RegisteredAssistant(address provider, address assistant); + event RegisteredProvisionAgent(address account, address agent); + event RegisteredRefundAgent(address account, address agent); + event RegisteredWithdrawalAgent(address account, address agent); struct ManagementAddresses { address system; @@ -497,33 +499,58 @@ contract Ledger is LedgerStorage, Initializable, OwnableUpgradeable, UUPSUpgrade protocolFeeAccount = _account; } - function registerProvider(address _provider) external { + function registerProvider(address _account) external { require(_msgSender() == owner(), "1050"); - providers[_provider] = true; - emit RegisteredProvider(_provider); + providers[_account] = true; + emit RegisteredProvider(_account); } - function unregisterProvider(address _provider) external { + function unregisterProvider(address _account) external { require(_msgSender() == owner(), "1050"); - providers[_provider] = false; - emit UnregisteredProvider(_provider); + providers[_account] = false; + emit UnregisteredProvider(_account); } - function registerAssistant(address _provider, address _assistant, bytes calldata _signature) external { - require(providers[_provider], "1054"); - bytes32 dataHash = keccak256(abi.encode(_provider, _assistant, block.chainid, nonce[_provider])); - require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _provider, "1501"); - assistants[_provider] = _assistant; - nonce[_provider]++; + function isProvider(address _account) external view override returns (bool) { + return providers[_account]; + } + + function registerProvisionAgent(address _account, address _agent, bytes calldata _signature) external { + bytes32 dataHash = keccak256(abi.encode(_account, _agent, block.chainid, nonce[_account])); + require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "1501"); + provisionAgents[_account] = _agent; + nonce[_account]++; - emit RegisteredAssistant(_provider, _assistant); + emit RegisteredProvisionAgent(_account, _agent); } - function isProvider(address _account) external view override returns (bool) { - return providers[_account]; + function provisionAgentOf(address _account) external view override returns (address) { + return provisionAgents[_account]; + } + + function registerRefundAgent(address _account, address _agent, bytes calldata _signature) external { + bytes32 dataHash = keccak256(abi.encode(_account, _agent, block.chainid, nonce[_account])); + require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "1501"); + refundAgents[_account] = _agent; + nonce[_account]++; + + emit RegisteredRefundAgent(_account, _agent); + } + + function refundAgentOf(address _account) external view override returns (address) { + return refundAgents[_account]; + } + + function registerWithdrawalAgent(address _account, address _agent, bytes calldata _signature) external { + bytes32 dataHash = keccak256(abi.encode(_account, _agent, block.chainid, nonce[_account])); + require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "1501"); + withdrawalAgents[_account] = _agent; + nonce[_account]++; + + emit RegisteredWithdrawalAgent(_account, _agent); } - function assistantOf(address _account) external view override returns (address) { - return assistants[_account]; + function withdrawalAgentOf(address _account) external view override returns (address) { + return withdrawalAgents[_account]; } } diff --git a/packages/contracts/contracts/ledger/LedgerStorage.sol b/packages/contracts/contracts/ledger/LedgerStorage.sol index 682eacd9..8dda7105 100644 --- a/packages/contracts/contracts/ledger/LedgerStorage.sol +++ b/packages/contracts/contracts/ledger/LedgerStorage.sol @@ -19,7 +19,9 @@ contract LedgerStorage { mapping(address => uint256) internal liquidity; mapping(address => bool) internal providers; - mapping(address => address) internal assistants; + mapping(address => address) internal provisionAgents; + mapping(address => address) internal refundAgents; + mapping(address => address) internal withdrawalAgents; address public systemAccount; address public paymentFeeAccount; diff --git a/packages/contracts/contracts/shop/Shop.sol b/packages/contracts/contracts/shop/Shop.sol index 81b30cc0..106b3093 100644 --- a/packages/contracts/contracts/shop/Shop.sol +++ b/packages/contracts/contracts/shop/Shop.sol @@ -367,13 +367,31 @@ contract Shop is ShopStorage, Initializable, OwnableUpgradeable, UUPSUpgradeable /// @param _shopId 상점아이디 /// @param _amount 인출금 /// @dev 중계서버를 통해서 상점주의 서명을 가지고 호출됩니다. - function refund(bytes32 _shopId, address _account, uint256 _amount, bytes calldata _signature) external virtual { + function refund(bytes32 _shopId, uint256 _amount, bytes calldata _signature) external virtual { require(shops[_shopId].status == ShopStatus.ACTIVE, "1202"); - bytes32 dataHash = keccak256(abi.encode(_shopId, _account, _amount, block.chainid, nonce[_account])); - require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == _account, "1501"); - require(shops[_shopId].account == _account, "1050"); require(_amount % 1 gwei == 0, "1030"); - require(settlements[_shopId].manager == bytes32(0x0), "1552"); + + address signer; + address shopOwner = shops[_shopId].account; + + address recurve1 = ECDSA.recover( + ECDSA.toEthSignedMessageHash(keccak256(abi.encode(_shopId, _amount, block.chainid, nonce[shopOwner]))), + _signature + ); + + if (recurve1 == shopOwner) { + signer = shopOwner; + } else { + address agent = ledgerContract.refundAgentOf(shopOwner); + require(agent != address(0x0), "1501"); + + address recurve2 = ECDSA.recover( + ECDSA.toEthSignedMessageHash(keccak256(abi.encode(_shopId, _amount, block.chainid, nonce[agent]))), + _signature + ); + require(recurve2 == agent, "1501"); + signer = agent; + } ShopData memory shop = shops[_shopId]; uint256 settlementAmount = (shop.collectedAmount + shop.usedAmount > shop.providedAmount) @@ -385,16 +403,16 @@ contract Shop is ShopStorage, Initializable, OwnableUpgradeable, UUPSUpgradeable require(_amount <= refundableAmount, "1220"); - uint256 amountToken = currencyRate.convertCurrencyToToken(_amount, shops[_shopId].currency); - ledgerContract.refund(_account, _amount, shops[_shopId].currency, amountToken, _shopId); + uint256 amountToken = currencyRate.convertCurrencyToToken(_amount, shop.currency); + ledgerContract.refund(shopOwner, _amount, shop.currency, amountToken, shop.shopId); - shops[_shopId].refundedAmount += _amount; - nonce[_account]++; + shops[shop.shopId].refundedAmount += _amount; + nonce[signer]++; - uint256 balanceToken = ledgerContract.tokenBalanceOf(_account); - uint256 refundedTotal = shops[_shopId].refundedAmount; - string memory currency = shops[_shopId].currency; - emit Refunded(_shopId, _account, _amount, refundedTotal, currency, amountToken, balanceToken); + uint256 balanceToken = ledgerContract.tokenBalanceOf(shopOwner); + uint256 refundedTotal = shops[shop.shopId].refundedAmount; + string memory currency = shop.currency; + emit Refunded(_shopId, shopOwner, _amount, refundedTotal, currency, amountToken, balanceToken); } /// @notice nonce 를 리턴한다 @@ -523,13 +541,48 @@ contract Shop is ShopStorage, Initializable, OwnableUpgradeable, UUPSUpgradeable "1554" ); - address account = shops[_managerShopId].account; - bytes32 dataHash = keccak256( - abi.encode("CollectSettlementAmount", _managerShopId, _clientShopId, block.chainid, nonce[account]) + address sender; + address shopOwner = shops[_managerShopId].account; + address recurve1 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + "CollectSettlementAmount", + _managerShopId, + _clientShopId, + block.chainid, + nonce[shopOwner] + ) + ) + ), + _signature ); - require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == account, "1501"); - nonce[account]++; + if (recurve1 == shopOwner) { + sender = shopOwner; + } else { + address agent = ledgerContract.refundAgentOf(shopOwner); + require(agent != address(0x0), "1501"); + + address recurve2 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + "CollectSettlementAmount", + _managerShopId, + _clientShopId, + block.chainid, + nonce[agent] + ) + ) + ), + _signature + ); + require(recurve2 == agent, "1501"); + sender = agent; + } + + nonce[sender]++; _collectSettlementAmount(_managerShopId, _clientShopId); } @@ -542,19 +595,49 @@ contract Shop is ShopStorage, Initializable, OwnableUpgradeable, UUPSUpgradeable require(_managerShopId != bytes32(0x0), "1223"); require(shops[_managerShopId].status != ShopStatus.INVALID, "1201"); - address account = shops[_managerShopId].account; - bytes32 dataHash = keccak256( - abi.encode( - "CollectSettlementAmountMultiClient", - _managerShopId, - _clientShopIds, - block.chainid, - nonce[account] - ) + address sender; + address shopOwner = shops[_managerShopId].account; + + address recurve1 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + "CollectSettlementAmountMultiClient", + _managerShopId, + _clientShopIds, + block.chainid, + nonce[shopOwner] + ) + ) + ), + _signature ); - require(ECDSA.recover(ECDSA.toEthSignedMessageHash(dataHash), _signature) == account, "1501"); - nonce[account]++; + if (recurve1 == shopOwner) { + sender = shopOwner; + } else { + address agent = ledgerContract.refundAgentOf(shopOwner); + require(agent != address(0x0), "1501"); + + address recurve2 = ECDSA.recover( + ECDSA.toEthSignedMessageHash( + keccak256( + abi.encode( + "CollectSettlementAmountMultiClient", + _managerShopId, + _clientShopIds, + block.chainid, + nonce[agent] + ) + ) + ), + _signature + ); + require(recurve2 == agent, "1501"); + sender = agent; + } + + nonce[sender]++; for (uint256 idx = 0; idx < _clientShopIds.length; idx++) { bytes32 clientShopId = _clientShopIds[idx]; diff --git a/packages/contracts/deploy/side_chain_devnet/deploy.ts b/packages/contracts/deploy/side_chain_devnet/deploy.ts index 1d7f7b19..d4421f07 100644 --- a/packages/contracts/deploy/side_chain_devnet/deploy.ts +++ b/packages/contracts/deploy/side_chain_devnet/deploy.ts @@ -907,7 +907,7 @@ async function deployLedger(accounts: IAccount, deployment: Deployments) { { const nonce = await contract.nonceOf(accounts.system.address); - const message = ContractUtils.getRegisterAssistanceMessage( + const message = ContractUtils.getRegisterAgentMessage( accounts.system.address, accounts.publisher.address, nonce, @@ -916,11 +916,11 @@ async function deployLedger(accounts: IAccount, deployment: Deployments) { const signature = await ContractUtils.signMessage(accounts.system, message); const tx = await contract .connect(accounts.certifiers[0]) - .registerAssistant(accounts.system.address, accounts.publisher.address, signature); - console.log(`Register assistant address of system (tx: ${tx.hash})...`); + .registerProvisionAgent(accounts.system.address, accounts.publisher.address, signature); + console.log(`Register agent address of system (tx: ${tx.hash})...`); // await tx.wait(); - const value = await contract.assistantOf(accounts.system.address); + const value = await contract.provisionAgentOf(accounts.system.address); console.log("Assistance of System Account: ", value); } } diff --git a/packages/contracts/package.json b/packages/contracts/package.json index 78fdff7e..bf767794 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "acc-contracts-v2", - "version": "2.9.0", + "version": "2.10.0", "description": "Smart contracts that decentralized loyalty systems", "files": [ "**/*.sol" @@ -36,6 +36,8 @@ }, "homepage": "https://github.com/acc-coin/acc-osx#readme", "devDependencies": { + "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", "@nomiclabs/hardhat-ethers": "^2.2.3", "@nomiclabs/hardhat-waffle": "^2.0.2", diff --git a/packages/contracts/src/utils/ContractUtils.ts b/packages/contracts/src/utils/ContractUtils.ts index f977cedd..1644943a 100644 --- a/packages/contracts/src/utils/ContractUtils.ts +++ b/packages/contracts/src/utils/ContractUtils.ts @@ -173,14 +173,13 @@ export class ContractUtils { public static getShopRefundMessage( shopId: BytesLike, - account: string, amount: BigNumberish, nonce: BigNumberish, chainId?: BigNumberish ): Uint8Array { const encodedResult = defaultAbiCoder.encode( - ["bytes32", "address", "uint256", "uint256", "uint256"], - [shopId, account, amount, chainId ? chainId : hre.ethers.provider.network.chainId, nonce] + ["bytes32", "uint256", "uint256", "uint256"], + [shopId, amount, chainId ? chainId : hre.ethers.provider.network.chainId, nonce] ); return arrayify(keccak256(encodedResult)); } @@ -663,15 +662,15 @@ export class ContractUtils { return arrayify(keccak256(encodedResult)); } - public static getRegisterAssistanceMessage( - provider: string, - assistance: string, + public static getRegisterAgentMessage( + account: string, + agent: string, nonce: BigNumberish, chainId: BigNumberish ): Uint8Array { const encodedResult = defaultAbiCoder.encode( ["address", "address", "uint256", "uint256"], - [provider, assistance, chainId, nonce] + [account, agent, chainId, nonce] ); return arrayify(keccak256(encodedResult)); } diff --git a/packages/contracts/test/04-Ledger.test.ts b/packages/contracts/test/04-Ledger.test.ts index a89f82fd..365e1a95 100644 --- a/packages/contracts/test/04-Ledger.test.ts +++ b/packages/contracts/test/04-Ledger.test.ts @@ -2080,17 +2080,12 @@ describe("Test for Ledger", () => { it("refund", async () => { const nonce = await shopContract.nonceOf(shopData[shopIndex].wallet.address); - const message = ContractUtils.getShopRefundMessage( - shopData[shopIndex].shopId, - shopData[shopIndex].wallet.address, - amount2, - nonce - ); + const message = ContractUtils.getShopRefundMessage(shopData[shopIndex].shopId, amount2, nonce); const signature = await ContractUtils.signMessage(shopData[shopIndex].wallet, message); await expect( shopContract .connect(shopData[shopIndex].wallet.connect(hre.ethers.provider)) - .refund(shop.shopId, shopData[shopIndex].wallet.address, amount2, signature) + .refund(shop.shopId, amount2, signature) ) .to.emit(shopContract, "Refunded") .withNamedArgs({ @@ -2395,17 +2390,12 @@ describe("Test for Ledger", () => { it("Open Withdrawal", async () => { const nonce = await shopContract.nonceOf(shopData[shopIndex].wallet.address); - const message = ContractUtils.getShopRefundMessage( - shopData[shopIndex].shopId, - shopData[shopIndex].wallet.address, - amount2, - nonce - ); + const message = ContractUtils.getShopRefundMessage(shopData[shopIndex].shopId, amount2, nonce); const signature = await ContractUtils.signMessage(shopData[shopIndex].wallet, message); await expect( shopContract .connect(shopData[shopIndex].wallet.connect(hre.ethers.provider)) - .refund(shop.shopId, shopData[shopIndex].wallet.address, amount2, signature) + .refund(shop.shopId, amount2, signature) ) .to.emit(shopContract, "Refunded") .withNamedArgs({ @@ -3114,7 +3104,6 @@ describe("Test for Ledger", () => { const nonce = await shopContract.nonceOf(shopData[shopIndex].wallet.address); const message = ContractUtils.getShopRefundMessage( shopData[shopIndex].shopId, - shopData[shopIndex].wallet.address, expected[shopIndex], nonce ); @@ -3122,7 +3111,7 @@ describe("Test for Ledger", () => { await expect( shopContract .connect(deployments.accounts.certifiers[0]) - .refund(shop.shopId, shopData[shopIndex].wallet.address, expected[shopIndex], signature) + .refund(shop.shopId, expected[shopIndex], signature) ) .to.emit(shopContract, "Refunded") .withNamedArgs({ @@ -3647,17 +3636,12 @@ describe("Test for Ledger", () => { it("refund", async () => { const nonce = await shopContract.nonceOf(managerShop.wallet.address); - const message = ContractUtils.getShopRefundMessage( - managerShop.shopId, - managerShop.wallet.address, - sumExpected, - nonce - ); + const message = ContractUtils.getShopRefundMessage(managerShop.shopId, sumExpected, nonce); const signature = await ContractUtils.signMessage(managerShop.wallet, message); await expect( shopContract .connect(deployments.accounts.certifiers[0]) - .refund(managerShop.shopId, managerShop.wallet.address, sumExpected, signature) + .refund(managerShop.shopId, sumExpected, signature) ) .to.emit(shopContract, "Refunded") .withNamedArgs({ @@ -4160,17 +4144,550 @@ describe("Test for Ledger", () => { it("refund", async () => { const nonce = await shopContract.nonceOf(managerShop.wallet.address); - const message = ContractUtils.getShopRefundMessage( - managerShop.shopId, - managerShop.wallet.address, - sumExpected, - nonce - ); + const message = ContractUtils.getShopRefundMessage(managerShop.shopId, sumExpected, nonce); const signature = await ContractUtils.signMessage(managerShop.wallet, message); await expect( shopContract .connect(deployments.accounts.certifiers[0]) - .refund(managerShop.shopId, managerShop.wallet.address, sumExpected, signature) + .refund(managerShop.shopId, sumExpected, signature) + ) + .to.emit(shopContract, "Refunded") + .withNamedArgs({ + shopId: managerShop.shopId, + account: managerShop.wallet.address, + refundAmount: sumExpected, + amountToken, + }); + }); + + it("Check balance of ledger", async () => { + const balance = await ledgerContract.tokenBalanceOf(managerShop.wallet.address); + expect(balance).to.equal(amountToken); + }); + }); + }); + + context("Clearing for shops - Use settlement manager -- settlement agent", () => { + const userData: IUserData[] = [ + { + phone: "08201012341001", + address: deployments.accounts.users[0].address, + privateKey: deployments.accounts.users[0].privateKey, + }, + { + phone: "08201012341002", + address: deployments.accounts.users[1].address, + privateKey: deployments.accounts.users[1].privateKey, + }, + { + phone: "08201012341003", + address: deployments.accounts.users[2].address, + privateKey: deployments.accounts.users[2].privateKey, + }, + { + phone: "08201012341004", + address: deployments.accounts.users[3].address, + privateKey: deployments.accounts.users[3].privateKey, + }, + { + phone: "08201012341005", + address: deployments.accounts.users[4].address, + privateKey: deployments.accounts.users[4].privateKey, + }, + ]; + + const purchaseData: IPurchaseData[] = [ + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 0, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 1, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 1, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 2, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 2, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000, + providePercent: 1, + currency: "krw", + shopIndex: 2, + userIndex: 0, + }, + { + purchaseId: getPurchaseId(), + amount: 10000000, + providePercent: 1, + currency: "krw", + shopIndex: 5, + userIndex: 0, + }, + ]; + + const shopData: IShopData[] = [ + { + shopId: "F000100", + name: "Shop1", + currency: "krw", + wallet: deployments.accounts.shops[0], + }, + { + shopId: "F000200", + name: "Shop2", + currency: "krw", + wallet: deployments.accounts.shops[1], + }, + { + shopId: "F000300", + name: "Shop3", + currency: "krw", + wallet: deployments.accounts.shops[2], + }, + { + shopId: "F000400", + name: "Shop4", + currency: "krw", + wallet: deployments.accounts.shops[3], + }, + { + shopId: "F000500", + name: "Shop5", + currency: "krw", + wallet: deployments.accounts.shops[4], + }, + { + shopId: "F000500", + name: "Shop6", + currency: "krw", + wallet: deployments.accounts.shops[5], + }, + ]; + + before("Set Shop ID", async () => { + for (const elem of shopData) { + elem.shopId = ContractUtils.getShopId(elem.wallet.address, LoyaltyNetworkID.ACC_TESTNET); + } + }); + + before("Deploy", async () => { + await deployAllContract(shopData); + }); + + context("Save Purchase Data", () => { + it("Save Purchase Data", async () => { + for (const purchase of purchaseData) { + const phoneHash = ContractUtils.getPhoneHash(userData[purchase.userIndex].phone); + const purchaseAmount = Amount.make(purchase.amount, 18).value; + const loyaltyAmount = purchaseAmount.mul(purchase.providePercent).div(100); + const amt = purchaseAmount.mul(purchase.providePercent).div(100); + const userAccount = + userData[purchase.userIndex].address.trim() !== "" + ? userData[purchase.userIndex].address.trim() + : AddressZero; + const purchaseParam = { + purchaseId: getPurchaseId(), + amount: purchaseAmount, + loyalty: loyaltyAmount, + currency: purchase.currency.toLowerCase(), + shopId: shopData[purchase.shopIndex].shopId, + account: userAccount, + phone: phoneHash, + sender: deployments.accounts.system.address, + signature: "", + }; + purchaseParam.signature = await ContractUtils.getPurchaseSignature( + deployments.accounts.system, + purchaseParam + ); + const purchaseMessage = ContractUtils.getPurchasesMessage(0, [purchaseParam]); + const signatures = await Promise.all( + deployments.accounts.validators.map((m) => ContractUtils.signMessage(m, purchaseMessage)) + ); + const proposeMessage = ContractUtils.getPurchasesProposeMessage(0, [purchaseParam], signatures); + const proposerSignature = await ContractUtils.signMessage( + deployments.accounts.validators[0], + proposeMessage + ); + await expect( + providerContract + .connect(deployments.accounts.certifiers[0]) + .savePurchase(0, [purchaseParam], signatures, proposerSignature) + ) + .to.emit(providerContract, "SavedPurchase") + .withArgs( + purchaseParam.purchaseId, + purchaseParam.amount, + purchaseParam.loyalty, + purchaseParam.currency, + purchaseParam.shopId, + purchaseParam.account, + purchaseParam.phone, + purchaseParam.sender + ) + .emit(ledgerContract, "ProvidedPoint") + .withNamedArgs({ + account: userAccount, + providedPoint: amt, + providedValue: amt, + purchaseId: purchaseParam.purchaseId, + }); + } + }); + + it("Check balances", async () => { + const expected: Map = new Map(); + for (const purchase of purchaseData) { + const purchaseAmount = Amount.make(purchase.amount, 18).value; + const key = + userData[purchase.userIndex].address.trim() !== "" + ? userData[purchase.userIndex].address.trim() + : ContractUtils.getPhoneHash(userData[purchase.userIndex].phone.trim()); + const oldValue = expected.get(key); + + const point = purchaseAmount.mul(purchase.providePercent).div(100); + + if (oldValue !== undefined) expected.set(key, oldValue.add(point)); + else expected.set(key, point); + } + for (const key of expected.keys()) { + if (key.match(/^0x[A-Fa-f0-9]{64}$/i)) { + expect(await ledgerContract.unPayablePointBalanceOf(key)).to.deep.equal(expected.get(key)); + } else { + expect(await ledgerContract.pointBalanceOf(key)).to.deep.equal(expected.get(key)); + } + } + }); + + it("Check shop data", async () => { + const shopInfo1 = await shopContract.shopOf(shopData[0].shopId); + expect(shopInfo1.providedAmount).to.equal( + Amount.make(10000 * 1, 18) + .value.mul(1) + .div(100) + ); + + const shopInfo2 = await shopContract.shopOf(shopData[1].shopId); + expect(shopInfo2.providedAmount).to.equal( + Amount.make(10000 * 2, 18) + .value.mul(1) + .div(100) + ); + const shopInfo3 = await shopContract.shopOf(shopData[2].shopId); + expect(shopInfo3.providedAmount).to.equal( + Amount.make(10000 * 3, 18) + .value.mul(1) + .div(100) + ); + const shopInfo4 = await shopContract.shopOf(shopData[3].shopId); + expect(shopInfo4.providedAmount).to.equal(Amount.make(0, 18).value); + }); + }); + + context("Pay point", () => { + it("Pay point - Success", async () => { + const providedAmount = [100, 200, 300, 0].map((m) => Amount.make(m, 18).value); + const usedAmount = [500, 500, 500, 500].map((m) => Amount.make(m, 18).value); + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const purchase = { + purchaseId: getPurchaseId(), + amount: 500, + providePercent: 1, + currency: "krw", + shopIndex, + userIndex: 0, + }; + + const paymentId = ContractUtils.getPaymentId( + deployments.accounts.users[purchase.userIndex].address, + 0 + ); + const purchaseAmount = Amount.make(purchase.amount, 18).value; + const shop = shopData[purchase.shopIndex]; + const nonce = await ledgerContract.nonceOf(deployments.accounts.users[purchase.userIndex].address); + const signature = await ContractUtils.signLoyaltyNewPayment( + deployments.accounts.users[purchase.userIndex], + paymentId, + purchase.purchaseId, + purchaseAmount, + purchase.currency, + shop.shopId, + nonce + ); + + [secret, secretLock] = ContractUtils.getSecret(); + await expect( + consumerContract.connect(deployments.accounts.certifiers[0]).openNewLoyaltyPayment({ + paymentId, + purchaseId: purchase.purchaseId, + amount: purchaseAmount, + currency: purchase.currency.toLowerCase(), + shopId: shop.shopId, + account: deployments.accounts.users[purchase.userIndex].address, + signature, + secretLock, + }) + ).to.emit(consumerContract, "LoyaltyPaymentEvent"); + + await expect( + consumerContract + .connect(deployments.accounts.certifiers[0]) + .closeNewLoyaltyPayment(paymentId, secret, true) + ).to.emit(consumerContract, "LoyaltyPaymentEvent"); + + const shopInfo = await shopContract.shopOf(shop.shopId); + expect(shopInfo.providedAmount).to.equal(providedAmount[shopIndex]); + expect(shopInfo.usedAmount).to.equal(usedAmount[shopIndex]); + } + }); + }); + + context("setSettlementManager/removeSettlementManager", () => { + const managerShop = shopData[4]; + const clients: BytesLike[] = []; + + it("prepare", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + clients.push(shopData[shopIndex].shopId); + } + }); + + it("setSettlementManager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const nonce = await shopContract.nonceOf(shop.wallet.address); + const message = ContractUtils.getSetSettlementManagerMessage( + shop.shopId, + managerShop.shopId, + nonce + ); + const signature = ContractUtils.signMessage(shop.wallet, message); + await expect( + shopContract + .connect(deployments.accounts.certifiers[0]) + .setSettlementManager(shop.shopId, managerShop.shopId, signature) + ) + .to.emit(shopContract, "SetSettlementManager") + .withNamedArgs({ + shopId: shop.shopId, + managerShopId: managerShop.shopId, + }); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + expect(await shopContract.settlementManagerOf(shop.shopId)).to.be.equal(managerShop.shopId); + } + }); + + it("check client", async () => { + expect(await shopContract.getSettlementClientLength(managerShop.shopId)).to.be.equal(clients.length); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 0, 2)).to.deep.equal( + clients.slice(0, 2) + ); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 1, 3)).to.deep.equal( + clients.slice(1, 3) + ); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 0, 4)).to.deep.equal( + clients.slice(0, 4) + ); + }); + + it("removeSettlementManager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const nonce = await shopContract.nonceOf(shop.wallet.address); + const message = ContractUtils.getRemoveSettlementManagerMessage(shop.shopId, nonce); + const signature = ContractUtils.signMessage(shop.wallet, message); + await expect( + shopContract + .connect(deployments.accounts.certifiers[0]) + .removeSettlementManager(shop.shopId, signature) + ) + .to.emit(shopContract, "RemovedSettlementManager") + .withNamedArgs({ + shopId: shop.shopId, + managerShopId: managerShop.shopId, + }); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + expect(await shopContract.settlementManagerOf(shop.shopId)).to.be.equal(HashZero); + } + }); + + it("check client", async () => { + expect(await shopContract.getSettlementClientLength(managerShop.shopId)).to.be.equal(0); + }); + + it("setSettlementManager again", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const nonce = await shopContract.nonceOf(shop.wallet.address); + const message = ContractUtils.getSetSettlementManagerMessage( + shop.shopId, + managerShop.shopId, + nonce + ); + const signature = ContractUtils.signMessage(shop.wallet, message); + await expect( + shopContract + .connect(deployments.accounts.certifiers[0]) + .setSettlementManager(shop.shopId, managerShop.shopId, signature) + ) + .to.emit(shopContract, "SetSettlementManager") + .withNamedArgs({ + shopId: shop.shopId, + managerShopId: managerShop.shopId, + }); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + expect(await shopContract.settlementManagerOf(shop.shopId)).to.be.equal(managerShop.shopId); + } + }); + + it("check client", async () => { + expect(await shopContract.getSettlementClientLength(managerShop.shopId)).to.be.equal(clients.length); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 0, 2)).to.deep.equal( + clients.slice(0, 2) + ); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 1, 3)).to.deep.equal( + clients.slice(1, 3) + ); + expect(await shopContract.getSettlementClientList(managerShop.shopId, 0, 4)).to.deep.equal( + clients.slice(0, 4) + ); + }); + }); + + context("refund", () => { + const managerShop = shopData[4]; + const expected = [400, 300, 200, 500].map((m) => Amount.make(m, 18).value); + const sumExpected = Amount.make(1400, 18).value; + let amountToken: BigNumber; + + it("register refund agent", async () => { + const refundAgent = deployments.accounts.users[0]; + const nonce = await ledgerContract.nonceOf(managerShop.wallet.address); + const message = ContractUtils.getRegisterAgentMessage( + managerShop.wallet.address, + refundAgent.address, + nonce, + hre.ethers.provider.network.chainId + ); + const signature = await ContractUtils.signMessage(managerShop.wallet, message); + await expect( + ledgerContract + .connect(deployments.accounts.certifiers[0]) + .registerRefundAgent(managerShop.wallet.address, refundAgent.address, signature) + ) + .to.emit(ledgerContract, "RegisteredRefundAgent") + .withNamedArgs({ + account: managerShop.wallet.address, + agent: refundAgent.address, + }); + }); + + it("Check refund agent", async () => { + const refundAgent = deployments.accounts.users[0]; + const refundAgentAddress = await ledgerContract.refundAgentOf(managerShop.wallet.address); + expect(refundAgentAddress).to.be.equal(refundAgent.address); + }); + + it("Check refundable amount", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const { refundableAmount, refundableToken } = await shopContract.refundableOf(shop.shopId); + expect(refundableAmount).to.equal(expected[shopIndex]); + } + }); + + it("collectSettlementAmountMultiClient by agent", async () => { + const refundAgent = deployments.accounts.users[0]; + const clientLength = await shopContract.getSettlementClientLength(managerShop.shopId); + const clients = await shopContract.getSettlementClientList(managerShop.shopId, 0, clientLength); + const nonce = await shopContract.nonceOf(refundAgent.address); + const message = ContractUtils.getCollectSettlementAmountMultiClientMessage( + managerShop.shopId, + clients, + nonce + ); + const signature = await ContractUtils.signMessage(refundAgent, message); + await expect( + shopContract + .connect(deployments.accounts.certifiers[0]) + .collectSettlementAmountMultiClient(managerShop.shopId, clients, signature) + ) + .to.emit(shopContract, "CollectedSettlementAmount") + .withNamedArgs({ + managerId: managerShop.shopId, + managerAccount: managerShop.wallet.address, + managerCurrency: managerShop.currency, + }); + }); + + it("Check refundable amount", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const { refundableAmount } = await shopContract.refundableOf(shop.shopId); + expect(refundableAmount).to.equal(0); + } + }); + + it("Check refundable amount of settlement manager", async () => { + const { refundableAmount, refundableToken } = await shopContract.refundableOf(managerShop.shopId); + expect(refundableAmount).to.equal(sumExpected); + amountToken = BigNumber.from(refundableToken); + }); + + it("refund by agent", async () => { + const refundAgent = deployments.accounts.users[0]; + const nonce = await shopContract.nonceOf(refundAgent.address); + const message = ContractUtils.getShopRefundMessage(managerShop.shopId, sumExpected, nonce); + const signature = await ContractUtils.signMessage(refundAgent, message); + await expect( + shopContract + .connect(deployments.accounts.certifiers[0]) + .refund(managerShop.shopId, sumExpected, signature) ) .to.emit(shopContract, "Refunded") .withNamedArgs({ diff --git a/packages/contracts/test/05-Bridge.test.ts b/packages/contracts/test/05-Bridge.test.ts index cca520e9..c87b1419 100644 --- a/packages/contracts/test/05-Bridge.test.ts +++ b/packages/contracts/test/05-Bridge.test.ts @@ -17,7 +17,6 @@ import { LoyaltyTransfer, PhoneLinkCollection, Shop, - TestLYT, Validator, } from "../typechain-types"; import { Deployments } from "./helper/Deployments"; @@ -38,19 +37,11 @@ interface IShopData { wallet: Wallet; } -describe("Test for Ledger", () => { +describe("Test for LoyaltyBridge", () => { const deployments = new Deployments(); - let validatorContract: Validator; let tokenContract: BIP20DelegatedTransfer; let ledgerContract: Ledger; - let linkContract: PhoneLinkCollection; - let currencyContract: CurrencyRate; let shopContract: Shop; - let providerContract: LoyaltyProvider; - let consumerContract: LoyaltyConsumer; - let exchangerContract: LoyaltyExchanger; - let burnerContract: LoyaltyBurner; - let transferContract: LoyaltyTransfer; let bridgeContract: Bridge; let loyaltyBridgeContract: LoyaltyBridge; @@ -73,17 +64,8 @@ describe("Test for Ledger", () => { await deployments.doDeployAll(); tokenContract = deployments.getContract("TestLYT") as BIP20DelegatedTransfer; - validatorContract = deployments.getContract("Validator") as Validator; - currencyContract = deployments.getContract("CurrencyRate") as CurrencyRate; - ledgerContract = deployments.getContract("Ledger") as Ledger; - linkContract = deployments.getContract("PhoneLinkCollection") as PhoneLinkCollection; shopContract = deployments.getContract("Shop") as Shop; - providerContract = deployments.getContract("LoyaltyProvider") as LoyaltyProvider; - consumerContract = deployments.getContract("LoyaltyConsumer") as LoyaltyConsumer; - exchangerContract = deployments.getContract("LoyaltyExchanger") as LoyaltyExchanger; - burnerContract = deployments.getContract("LoyaltyBurner") as LoyaltyBurner; - transferContract = deployments.getContract("LoyaltyTransfer") as LoyaltyTransfer; bridgeContract = deployments.getContract("Bridge") as Bridge; loyaltyBridgeContract = deployments.getContract("LoyaltyBridge") as LoyaltyBridge; tokenId = ContractUtils.getTokenId(await tokenContract.name(), await tokenContract.symbol()); @@ -225,3 +207,204 @@ describe("Test for Ledger", () => { ); }); }); + +describe("Test for LoyaltyBridge - withdrawal agent", () => { + const deployments = new Deployments(); + let tokenContract: BIP20DelegatedTransfer; + let ledgerContract: Ledger; + let shopContract: Shop; + let bridgeContract: Bridge; + let loyaltyBridgeContract: LoyaltyBridge; + + let tokenId: string; + let amount = Amount.make(100_000, 18).value; + const fee = Amount.make(0.1, 18).value; + + const addShopData = async (shopData: IShopData[]) => { + for (const elem of shopData) { + const nonce = await shopContract.nonceOf(elem.wallet.address); + const message = ContractUtils.getShopAccountMessage(elem.shopId, elem.wallet.address, nonce); + const signature = await ContractUtils.signMessage(elem.wallet, message); + await shopContract + .connect(deployments.accounts.certifiers[0]) + .add(elem.shopId, elem.name, elem.currency, elem.wallet.address, signature); + } + }; + + const deployAllContract = async (shopData: IShopData[]) => { + await deployments.doDeployAll(); + + tokenContract = deployments.getContract("TestLYT") as BIP20DelegatedTransfer; + ledgerContract = deployments.getContract("Ledger") as Ledger; + shopContract = deployments.getContract("Shop") as Shop; + bridgeContract = deployments.getContract("Bridge") as Bridge; + loyaltyBridgeContract = deployments.getContract("LoyaltyBridge") as LoyaltyBridge; + tokenId = ContractUtils.getTokenId(await tokenContract.name(), await tokenContract.symbol()); + await addShopData(shopData); + }; + + let depositId: string; + it("Deploy", async () => { + await deployAllContract([]); + }); + + it("Register withdrawal agent", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const nonce = await ledgerContract.nonceOf(user.address); + const message = ContractUtils.getRegisterAgentMessage( + user.address, + agent.address, + nonce, + hre.ethers.provider.network.chainId + ); + const signature = await ContractUtils.signMessage(user, message); + await expect( + ledgerContract + .connect(deployments.accounts.certifiers[0]) + .registerWithdrawalAgent(user.address, agent.address, signature) + ) + .to.emit(ledgerContract, "RegisteredWithdrawalAgent") + .withNamedArgs({ + account: user.address, + agent: agent.address, + }); + }); + + it("Check withdrawal agent", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const withdrawalAgentAddress = await ledgerContract.withdrawalAgentOf(user.address); + expect(withdrawalAgentAddress).to.be.equal(agent.address); + }); + + it("Deposit to Main Bridge", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const oldLiquidity = await tokenContract.balanceOf(bridgeContract.address); + const oldTokenBalance = await tokenContract.balanceOf(user.address); + const nonce = await tokenContract.nonceOf(user.address); + const expiry = ContractUtils.getTimeStamp() + 3600; + const message = ContractUtils.getTransferMessage( + hre.ethers.provider.network.chainId, + tokenContract.address, + user.address, + bridgeContract.address, + amount, + nonce, + expiry + ); + depositId = ContractUtils.getRandomId(user.address); + const signature = await ContractUtils.signMessage(user, message); + await expect( + bridgeContract + .connect(deployments.accounts.certifiers[0]) + .depositToBridge(tokenId, depositId, user.address, amount, expiry, signature) + ) + .to.emit(bridgeContract, "BridgeDeposited") + .withNamedArgs({ + depositId, + account: user.address, + amount, + }); + expect(await tokenContract.balanceOf(user.address)).to.deep.equal(oldTokenBalance.sub(amount)); + expect(await tokenContract.balanceOf(bridgeContract.address)).to.deep.equal(oldLiquidity.add(amount)); + }); + + it("Withdraw from LoyaltyBridge", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const oldLiquidity = await ledgerContract.tokenBalanceOf(loyaltyBridgeContract.address); + const oldTokenBalance = await ledgerContract.tokenBalanceOf(user.address); + const oldFeeBalance = await ledgerContract.tokenBalanceOf(deployments.accounts.protocolFee.address); + + await loyaltyBridgeContract + .connect(deployments.accounts.bridgeValidators[0]) + .withdrawFromBridge(tokenId, depositId, user.address, amount); + await expect( + loyaltyBridgeContract + .connect(deployments.accounts.bridgeValidators[1]) + .withdrawFromBridge(tokenId, depositId, user.address, amount) + ) + .to.emit(loyaltyBridgeContract, "BridgeWithdrawn") + .withNamedArgs({ + withdrawId: depositId, + account: user.address, + amount: amount.sub(fee), + }); + + expect(await ledgerContract.tokenBalanceOf(loyaltyBridgeContract.address)).to.deep.equal( + oldLiquidity.sub(amount) + ); + expect(await ledgerContract.tokenBalanceOf(user.address)).to.deep.equal(oldTokenBalance.add(amount.sub(fee))); + expect(await ledgerContract.tokenBalanceOf(deployments.accounts.protocolFee.address)).to.deep.equal( + oldFeeBalance.add(fee) + ); + }); + + it("Deposit to Loyalty Bridge", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + amount = Amount.make(50_000, 18).value; + const oldLiquidity = await ledgerContract.tokenBalanceOf(loyaltyBridgeContract.address); + const oldTokenBalance = await ledgerContract.tokenBalanceOf(user.address); + + const nonce = await ledgerContract.nonceOf(agent.address); + const expiry = ContractUtils.getTimeStamp() + 3600; + const message = ContractUtils.getTransferMessage( + hre.ethers.provider.network.chainId, + tokenContract.address, + user.address, + loyaltyBridgeContract.address, + amount, + nonce, + expiry + ); + depositId = ContractUtils.getRandomId(user.address); + const signature = await ContractUtils.signMessage(agent, message); + await expect( + loyaltyBridgeContract + .connect(deployments.accounts.certifiers[0]) + .depositToBridge(tokenId, depositId, user.address, amount, expiry, signature) + ) + .to.emit(loyaltyBridgeContract, "BridgeDeposited") + .withNamedArgs({ + depositId, + account: user.address, + amount, + }); + expect(await ledgerContract.tokenBalanceOf(user.address)).to.deep.equal(oldTokenBalance.sub(amount)); + expect(await ledgerContract.tokenBalanceOf(loyaltyBridgeContract.address)).to.deep.equal( + oldLiquidity.add(amount) + ); + }); + + it("Withdraw from Main Bridge", async () => { + const user = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const oldLiquidity = await tokenContract.balanceOf(bridgeContract.address); + const oldTokenBalance = await tokenContract.balanceOf(user.address); + const oldFeeBalance = await tokenContract.balanceOf(deployments.accounts.protocolFee.address); + + await bridgeContract + .connect(deployments.accounts.bridgeValidators[0]) + .withdrawFromBridge(tokenId, depositId, user.address, amount); + await expect( + bridgeContract + .connect(deployments.accounts.bridgeValidators[1]) + .withdrawFromBridge(tokenId, depositId, user.address, amount) + ) + .to.emit(bridgeContract, "BridgeWithdrawn") + .withNamedArgs({ + withdrawId: depositId, + account: user.address, + amount: amount.sub(fee), + }); + + expect(await tokenContract.balanceOf(bridgeContract.address)).to.deep.equal(oldLiquidity.sub(amount)); + expect(await tokenContract.balanceOf(user.address)).to.deep.equal(oldTokenBalance.add(amount.sub(fee))); + expect(await tokenContract.balanceOf(deployments.accounts.protocolFee.address)).to.deep.equal( + oldFeeBalance.add(fee) + ); + }); +}); diff --git a/packages/contracts/test/08-Ledger-Provider.test.ts b/packages/contracts/test/08-Ledger-Provider.test.ts index d407f8f1..ceeb1a77 100644 --- a/packages/contracts/test/08-Ledger-Provider.test.ts +++ b/packages/contracts/test/08-Ledger-Provider.test.ts @@ -356,9 +356,9 @@ describe("Test for Ledger", () => { }); it("Register Assistance", async () => { - expect(await ledgerContract.assistantOf(deployments.accounts.users[0].address)).equal(AddressZero); + expect(await ledgerContract.provisionAgentOf(deployments.accounts.users[0].address)).equal(AddressZero); const nonce = await ledgerContract.nonceOf(deployments.accounts.users[0].address); - const message = ContractUtils.getRegisterAssistanceMessage( + const message = ContractUtils.getRegisterAgentMessage( deployments.accounts.users[0].address, deployments.accounts.users[2].address, nonce, @@ -368,29 +368,29 @@ describe("Test for Ledger", () => { await expect( ledgerContract .connect(deployments.accounts.deployer) - .registerAssistant( + .registerProvisionAgent( deployments.accounts.users[0].address, deployments.accounts.users[2].address, signature ) ) - .emit(ledgerContract, "RegisteredAssistant") + .emit(ledgerContract, "RegisteredProvisionAgent") .withNamedArgs({ - provider: deployments.accounts.users[0].address, - assistant: deployments.accounts.users[2].address, + account: deployments.accounts.users[0].address, + agent: deployments.accounts.users[2].address, }); - expect(await ledgerContract.assistantOf(deployments.accounts.users[0].address)).equal( + expect(await ledgerContract.provisionAgentOf(deployments.accounts.users[0].address)).equal( deployments.accounts.users[2].address ); }); - it("Provide point - assistance", async () => { + it("Provide point - agent", async () => { const providePoint = Amount.make(100, 18).value; - const assistance = deployments.accounts.users[2]; + const agent = deployments.accounts.users[2]; const provider = deployments.accounts.users[0]; const receiver = deployments.accounts.users[3]; - const nonce = await ledgerContract.nonceOf(assistance.address); + const nonce = await ledgerContract.nonceOf(agent.address); const message = ContractUtils.getProvidePointToAddressMessage( provider.address, receiver.address, @@ -398,7 +398,7 @@ describe("Test for Ledger", () => { nonce, hre.ethers.provider.network.chainId ); - const signature = await ContractUtils.signMessage(assistance, message); + const signature = await ContractUtils.signMessage(agent, message); await expect( providerContract .connect(deployments.accounts.certifiers[0]) diff --git a/packages/library/package.json b/packages/library/package.json index be2d95b3..dfc387e6 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "acc-contracts-lib-v2", - "version": "2.8.0", + "version": "2.10.0", "description": "", "main": "dist/bundle-cjs.js", "module": "dist/bundle-esm.js", diff --git a/packages/relay/package.json b/packages/relay/package.json index aed17e43..ecbc410c 100644 --- a/packages/relay/package.json +++ b/packages/relay/package.json @@ -14,6 +14,7 @@ "start": "TESTING=false NODE_ENV=production hardhat run src/main.ts", "formatting:check": "prettier '**/*.{json,sol,ts,js}' -c", "formatting:write": "prettier '**/*.{json,sol,ts,js}' --write", + "test:Agent": "TESTING=true hardhat test test/Agent.test.ts", "test:Endpoints": "TESTING=true hardhat test test/Endpoints.test.ts", "test:Config": "TESTING=true hardhat test test/Config.test.ts", "test:Shop": "TESTING=true hardhat test test/Shop.test.ts", @@ -30,6 +31,7 @@ "test:LoyaltyBridge": "TESTING=true hardhat test test/LoyaltyBridge.test.ts", "test:LoyaltyExchanger": "TESTING=true hardhat test test/LoyaltyExchanger.test.ts", "test:LoyaltyProvider": "TESTING=true hardhat test test/LoyaltyProvider.test.ts", + "test:LoyaltyProvider2": "TESTING=true hardhat test test/LoyaltyProvider2.test.ts", "test:LoyaltyTransfer": "TESTING=true hardhat test test/LoyaltyTransfer.test.ts" }, "repository": { @@ -59,7 +61,9 @@ }, "dependencies": { "@ethersproject/bignumber": "^5.7.0", + "@ethersproject/bytes": "^5.7.0", "@ethersproject/constants": "^5.7.0", + "@ethersproject/contracts": "^5.7.0", "@ethersproject/wallet": "^5.7.0", "@ethersproject/experimental": "^5.7.0", "@nomiclabs/hardhat-ethers": "^2.2.3", @@ -70,7 +74,7 @@ "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.2", "acc-bridge-contracts-v2": "~2.5.0", - "acc-contracts-v2": "~2.8.0", + "acc-contracts-v2": "~2.10.0", "argparse": "^2.0.1", "assert": "^2.0.0", "axios": "^1.6.7", diff --git a/packages/relay/scripts/provider/send.ts b/packages/relay/scripts/provider/send.ts index 2408e46d..2fb435b1 100644 --- a/packages/relay/scripts/provider/send.ts +++ b/packages/relay/scripts/provider/send.ts @@ -35,8 +35,8 @@ async function main() { const provider = new Wallet("0x70438bc3ed02b5e4b76d496625cb7c06d6b7bf4362295b16fdfe91a046d4586c"); // 0x64D111eA9763c93a003cef491941A011B8df5a49 const receiver = new Wallet("0x595f911dcf0845cb1f2d0e5cec9f1ccfd62fa199ebeae215a72aa56014edbb32"); // 0xB6f69F0e9e70034ba0578C542476cC13eF739269 - const assistant = await sideLedgerContract.assistantOf(provider.address); - console.log(`assistant: ${assistant}`); + const agent = await sideLedgerContract.provisionAgentOf(provider.address); + console.log(`agent: ${agent}`); const balance1 = await sideLedgerContract.pointBalanceOf(receiver.address); const pointAmount = Amount.make(100, 18).value; diff --git a/packages/relay/scripts/provider/send_to_phone.ts b/packages/relay/scripts/provider/send_to_phone.ts index 45d1ab49..4670357e 100644 --- a/packages/relay/scripts/provider/send_to_phone.ts +++ b/packages/relay/scripts/provider/send_to_phone.ts @@ -35,8 +35,8 @@ async function main() { const provider = new Wallet("0x70438bc3ed02b5e4b76d496625cb7c06d6b7bf4362295b16fdfe91a046d4586c"); // 0x64D111eA9763c93a003cef491941A011B8df5a49 const receiver = ContractUtils.getPhoneHash("+82 10-9000-2000"); - const assistant = await sideLedgerContract.assistantOf(provider.address); - console.log(`assistant: ${assistant}`); + const agent = await sideLedgerContract.provisionAgentOf(provider.address); + console.log(`agent: ${agent}`); const balance1 = await sideLedgerContract.unPayablePointBalanceOf(receiver); const pointAmount = Amount.make(100, 18).value; diff --git a/packages/relay/src/DefaultServer.ts b/packages/relay/src/DefaultServer.ts index 0eaee816..9d24346c 100644 --- a/packages/relay/src/DefaultServer.ts +++ b/packages/relay/src/DefaultServer.ts @@ -14,6 +14,7 @@ import { ContractManager } from "./contract/ContractManager"; import { RelaySigners } from "./contract/Signers"; import { INotificationEventHandler, INotificationSender, NotificationSender } from "./delegator/NotificationSender"; import { Metrics } from "./metrics/Metrics"; +import { AgentRouter } from "./routers/AgentRouter"; import { BridgeRouter } from "./routers/BridgeRouter"; import { HistoryRouter } from "./routers/HistoryRouter"; import { PhoneLinkRouter } from "./routers/PhoneLinkRouter"; @@ -42,6 +43,7 @@ export class DefaultServer extends WebService { public readonly purchaseRouter: StorePurchaseRouter; public readonly tokenRouter: TokenRouter; public readonly taskRouter: TaskRouter; + public readonly agentRouter: AgentRouter; public readonly phoneLinkRouter: PhoneLinkRouter; public readonly providerRouter: ProviderRouter; public readonly bridgeRouter: BridgeRouter; @@ -159,6 +161,16 @@ export class DefaultServer extends WebService { this.graph_mainchain, this.relaySigners ); + this.agentRouter = new AgentRouter( + this, + this.config, + this.contractManager, + this.metrics, + this.storage, + this.graph_sidechain, + this.graph_mainchain, + this.relaySigners + ); this.providerRouter = new ProviderRouter( this, this.config, @@ -232,6 +244,7 @@ export class DefaultServer extends WebService { await this.purchaseRouter.registerRoutes(); await this.tokenRouter.registerRoutes(); await this.phoneLinkRouter.registerRoutes(); + await this.agentRouter.registerRoutes(); await this.providerRouter.registerRoutes(); await this.bridgeRouter.registerRoutes(); await this.historyRouter.registerRoutes(); diff --git a/packages/relay/src/routers/AgentRouter.ts b/packages/relay/src/routers/AgentRouter.ts new file mode 100644 index 00000000..e2737773 --- /dev/null +++ b/packages/relay/src/routers/AgentRouter.ts @@ -0,0 +1,318 @@ +import { Config } from "../common/Config"; +import { logger } from "../common/Logger"; +import { ContractManager } from "../contract/ContractManager"; +import { ISignerItem, RelaySigners } from "../contract/Signers"; +import { Metrics } from "../metrics/Metrics"; +import { WebService } from "../service/WebService"; +import { GraphStorage } from "../storage/GraphStorage"; +import { RelayStorage } from "../storage/RelayStorage"; +import { ContractUtils } from "../utils/ContractUtils"; +import { ResponseMessage } from "../utils/Errors"; + +import { ethers } from "ethers"; +import express from "express"; +import { body, param, validationResult } from "express-validator"; + +export class AgentRouter { + private web_service: WebService; + private readonly config: Config; + private readonly contractManager: ContractManager; + private readonly metrics: Metrics; + private readonly relaySigners: RelaySigners; + private storage: RelayStorage; + private graph_sidechain: GraphStorage; + private graph_mainchain: GraphStorage; + + constructor( + service: WebService, + config: Config, + contractManager: ContractManager, + metrics: Metrics, + storage: RelayStorage, + graph_sidechain: GraphStorage, + graph_mainchain: GraphStorage, + relaySigners: RelaySigners + ) { + this.web_service = service; + this.config = config; + this.contractManager = contractManager; + this.metrics = metrics; + + this.storage = storage; + this.graph_sidechain = graph_sidechain; + this.graph_mainchain = graph_mainchain; + this.relaySigners = relaySigners; + } + + private get app(): express.Application { + return this.web_service.app; + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + private async getRelaySigner(provider?: ethers.providers.Provider): Promise { + if (provider === undefined) provider = this.contractManager.sideChainProvider; + return this.relaySigners.getSigner(provider); + } + + /*** + * 트팬잭션을 중계할 때 사용될 서명자 + * @private + */ + private releaseRelaySigner(signer: ISignerItem) { + signer.using = false; + } + + /** + * Make the response data + * @param code The result code + * @param data The result data + * @param error The error + * @private + */ + private makeResponseData(code: number, data: any, error?: any): any { + return { + code, + data, + error, + }; + } + + public async registerRoutes() { + this.app.post( + "/v1/agent/provision", + [ + body("account").exists().trim().isEthereumAddress(), + body("agent").exists().trim().isEthereumAddress(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.post_agent_provision.bind(this) + ); + + this.app.get( + "/v1/agent/provision/:account", + [param("account").exists().trim().isEthereumAddress()], + this.get_agent_provision.bind(this) + ); + + this.app.post( + "/v1/agent/refund", + [ + body("account").exists().trim().isEthereumAddress(), + body("agent").exists().trim().isEthereumAddress(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.post_agent_refund.bind(this) + ); + + this.app.get( + "/v1/agent/refund/:account", + [param("account").exists().trim().isEthereumAddress()], + this.get_agent_refund.bind(this) + ); + + this.app.post( + "/v1/agent/withdrawal", + [ + body("account").exists().trim().isEthereumAddress(), + body("agent").exists().trim().isEthereumAddress(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.post_agent_withdrawal.bind(this) + ); + + this.app.get( + "/v1/agent/withdrawal/:account", + [param("account").exists().trim().isEthereumAddress()], + this.get_agent_withdrawal.bind(this) + ); + } + + private async post_agent_provision(req: express.Request, res: express.Response) { + logger.http(`POST /v1/agent/provision ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(this.contractManager.sideChainProvider); + try { + const account: string = String(req.body.account).trim(); + const agent: string = String(req.body.agent).trim(); + const signature: string = String(req.body.signature).trim(); + + const nonce = await this.contractManager.sideLedgerContract.nonceOf(account); + const message = ContractUtils.getRegisterAgentMessage( + account, + agent, + nonce, + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + const tx = await this.contractManager.sideLedgerContract + .connect(signerItem.signer) + .registerProvisionAgent(account, agent, signature); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent, txHash: tx.hash })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/agent/provision : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + private async get_agent_provision(req: express.Request, res: express.Response) { + logger.http(`GET /v1/agent/provision/:account ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const account: string = String(req.params.account).trim(); + const agent = await this.contractManager.sideLedgerContract.provisionAgentOf(account); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/agent/provision/:account : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } + + private async post_agent_refund(req: express.Request, res: express.Response) { + logger.http(`POST /v1/agent/refund ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(this.contractManager.sideChainProvider); + try { + const account: string = String(req.body.account).trim(); + const agent: string = String(req.body.agent).trim(); + const signature: string = String(req.body.signature).trim(); + + const nonce = await this.contractManager.sideLedgerContract.nonceOf(account); + const message = ContractUtils.getRegisterAgentMessage( + account, + agent, + nonce, + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + const tx = await this.contractManager.sideLedgerContract + .connect(signerItem.signer) + .registerRefundAgent(account, agent, signature); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent, txHash: tx.hash })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/agent/refund : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + private async get_agent_refund(req: express.Request, res: express.Response) { + logger.http(`GET /v1/agent/refund/:account ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const account: string = String(req.params.account).trim(); + const agent = await this.contractManager.sideLedgerContract.refundAgentOf(account); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/agent/refund/:account : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } + + private async post_agent_withdrawal(req: express.Request, res: express.Response) { + logger.http(`POST /v1/agent/withdrawal/ ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(this.contractManager.sideChainProvider); + try { + const account: string = String(req.body.account).trim(); + const agent: string = String(req.body.agent).trim(); + const signature: string = String(req.body.signature).trim(); + + const nonce = await this.contractManager.sideLedgerContract.nonceOf(account); + const message = ContractUtils.getRegisterAgentMessage( + account, + agent, + nonce, + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + const tx = await this.contractManager.sideLedgerContract + .connect(signerItem.signer) + .registerWithdrawalAgent(account, agent, signature); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent, txHash: tx.hash })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/agent/withdrawal/ : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + private async get_agent_withdrawal(req: express.Request, res: express.Response) { + logger.http(`GET /v1/agent/withdrawal/:account ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const account: string = String(req.params.account).trim(); + const agent = await this.contractManager.sideLedgerContract.withdrawalAgentOf(account); + this.metrics.add("success", 1); + return res.status(200).json(this.makeResponseData(0, { account, agent })); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/agent/withdrawal/:account : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } +} diff --git a/packages/relay/src/routers/BridgeRouter.ts b/packages/relay/src/routers/BridgeRouter.ts index dda46fbe..bd44a5c8 100644 --- a/packages/relay/src/routers/BridgeRouter.ts +++ b/packages/relay/src/routers/BridgeRouter.ts @@ -10,7 +10,9 @@ import { ContractUtils } from "../utils/ContractUtils"; import { ResponseMessage } from "../utils/Errors"; import { Validation } from "../validation"; -import { BigNumber, ethers } from "ethers"; +import { BigNumber } from "@ethersproject/bignumber"; + +import { ethers } from "ethers"; import express from "express"; import { body, validationResult } from "express-validator"; diff --git a/packages/relay/src/routers/HistoryRouter.ts b/packages/relay/src/routers/HistoryRouter.ts index ce222d5f..55713324 100644 --- a/packages/relay/src/routers/HistoryRouter.ts +++ b/packages/relay/src/routers/HistoryRouter.ts @@ -6,16 +6,15 @@ import { Metrics } from "../metrics/Metrics"; import { WebService } from "../service/WebService"; import { GraphStorage } from "../storage/GraphStorage"; import { RelayStorage } from "../storage/RelayStorage"; +import { ActionInLedger, ActionInShop } from "../types"; import { ContractUtils } from "../utils/ContractUtils"; import { ResponseMessage } from "../utils/Errors"; -// tslint:disable-next-line:no-implicit-dependencies import { AddressZero } from "@ethersproject/constants"; import { ethers } from "ethers"; import express from "express"; import { param, query, validationResult } from "express-validator"; import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; -import { ActionInLedger, ActionInShop } from "../types"; export class HistoryRouter { private web_service: WebService; diff --git a/packages/relay/src/routers/LedgerRouter.ts b/packages/relay/src/routers/LedgerRouter.ts index f9679f26..402818fa 100644 --- a/packages/relay/src/routers/LedgerRouter.ts +++ b/packages/relay/src/routers/LedgerRouter.ts @@ -10,9 +10,9 @@ import { ContractUtils } from "../utils/ContractUtils"; import { ResponseMessage } from "../utils/Errors"; import { Validation } from "../validation"; -// tslint:disable-next-line:no-implicit-dependencies +import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; -import { BigNumber, ethers } from "ethers"; +import { ethers } from "ethers"; import express from "express"; import { body, param, query, validationResult } from "express-validator"; import { PhoneNumberFormat, PhoneNumberUtil } from "google-libphonenumber"; @@ -642,17 +642,32 @@ export class LedgerRouter { const balance = await this.contractManager.sideLedgerContract.tokenBalanceOf(account); if (balance.lt(amount)) return res.status(200).json(ResponseMessage.getErrorMessage("1511")); - const nonce = await this.contractManager.sideLedgerContract.nonceOf(account); - const message = ContractUtils.getTransferMessage( + const nonce1 = await this.contractManager.sideLedgerContract.nonceOf(account); + const message1 = ContractUtils.getTransferMessage( this.contractManager.sideChainId, this.contractManager.sideTokenContract.address, account, this.contractManager.sideLoyaltyBridgeContract.address, amount, - nonce, + nonce1, expiry ); - if (!ContractUtils.verifyMessage(account, message, signature)) + const agent = await this.contractManager.sideLedgerContract.withdrawalAgentOf(account); + const nonce2 = await this.contractManager.sideLedgerContract.nonceOf(agent); + const message2 = ContractUtils.getTransferMessage( + this.contractManager.sideChainId, + this.contractManager.sideTokenContract.address, + account, + this.contractManager.sideLoyaltyBridgeContract.address, + amount, + nonce2, + expiry + ); + + if ( + !ContractUtils.verifyMessage(account, message1, signature) && + !ContractUtils.verifyMessage(agent, message2, signature) + ) return res.status(200).json(ResponseMessage.getErrorMessage("1501")); const tokenId = ContractUtils.getTokenId( diff --git a/packages/relay/src/routers/PaymentRouter.ts b/packages/relay/src/routers/PaymentRouter.ts index 934f19d3..896be055 100644 --- a/packages/relay/src/routers/PaymentRouter.ts +++ b/packages/relay/src/routers/PaymentRouter.ts @@ -24,9 +24,10 @@ import { ResponseMessage } from "../utils/Errors"; import { HTTPClient } from "../utils/Utils"; import { Validation } from "../validation"; -// tslint:disable-next-line:no-implicit-dependencies +import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; -import { BigNumber, ContractTransaction, ethers } from "ethers"; +import { ContractTransaction } from "@ethersproject/contracts"; +import { ethers } from "ethers"; import express from "express"; import { body, query, validationResult } from "express-validator"; diff --git a/packages/relay/src/routers/ProviderRouter.ts b/packages/relay/src/routers/ProviderRouter.ts index b286d5ed..24d7aee0 100644 --- a/packages/relay/src/routers/ProviderRouter.ts +++ b/packages/relay/src/routers/ProviderRouter.ts @@ -10,7 +10,6 @@ import { ContractUtils } from "../utils/ContractUtils"; import { ResponseMessage } from "../utils/Errors"; import { Validation } from "../validation"; -// tslint:disable-next-line:no-implicit-dependencies import { AddressZero } from "@ethersproject/constants"; import { BigNumber, ethers } from "ethers"; import express from "express"; @@ -269,7 +268,7 @@ export class ProviderRouter { const amount: BigNumber = BigNumber.from(req.body.amount); const signature: string = String(req.body.signature).trim(); - let assistant = await this.contractManager.sideLedgerContract.assistantOf(provider); + let assistant = await this.contractManager.sideLedgerContract.provisionAgentOf(provider); if (assistant === AddressZero) assistant = provider; const nonce = await this.contractManager.sideLedgerContract.nonceOf(assistant); @@ -312,7 +311,7 @@ export class ProviderRouter { const amount: BigNumber = BigNumber.from(req.body.amount); const signature: string = String(req.body.signature).trim(); - let assistant = await this.contractManager.sideLedgerContract.assistantOf(provider); + let assistant = await this.contractManager.sideLedgerContract.provisionAgentOf(provider); if (assistant === AddressZero) assistant = provider; const nonce = await this.contractManager.sideLedgerContract.nonceOf(assistant); @@ -355,7 +354,7 @@ export class ProviderRouter { const signature: string = String(req.body.signature).trim(); const nonce = await this.contractManager.sideLedgerContract.nonceOf(provider); - const message = ContractUtils.getRegisterAssistanceMessage( + const message = ContractUtils.getRegisterAgentMessage( provider, assistant, nonce, @@ -365,7 +364,7 @@ export class ProviderRouter { return res.status(200).json(ResponseMessage.getErrorMessage("1501")); const tx = await this.contractManager.sideLedgerContract .connect(signerItem.signer) - .registerAssistant(provider, assistant, signature); + .registerProvisionAgent(provider, assistant, signature); this.metrics.add("success", 1); return res.status(200).json(this.makeResponseData(0, { provider, assistant, txHash: tx.hash })); } catch (error: any) { @@ -388,7 +387,7 @@ export class ProviderRouter { try { const provider: string = String(req.params.provider).trim(); - const assistant = await this.contractManager.sideLedgerContract.assistantOf(provider); + const assistant = await this.contractManager.sideLedgerContract.provisionAgentOf(provider); this.metrics.add("success", 1); return res.status(200).json(this.makeResponseData(0, { provider, assistant })); } catch (error: any) { diff --git a/packages/relay/src/routers/ShopRouter.ts b/packages/relay/src/routers/ShopRouter.ts index cf2af1ff..7087cef5 100644 --- a/packages/relay/src/routers/ShopRouter.ts +++ b/packages/relay/src/routers/ShopRouter.ts @@ -16,7 +16,7 @@ import express from "express"; import { body, param, query, validationResult } from "express-validator"; import * as hre from "hardhat"; -// tslint:disable-next-line:no-implicit-dependencies +import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; import { ContractManager } from "../contract/ContractManager"; import { Metrics } from "../metrics/Metrics"; @@ -246,6 +246,100 @@ export class ShopRouter { ], this.shop_refundable.bind(this) ); + this.app.post( + "/v1/shop/settlement/manager/set", + [ + body("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + body("account").exists().trim().isEthereumAddress(), + body("managerId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.shop_settlement_manager_set.bind(this) + ); + this.app.post( + "/v1/shop/settlement/manager/remove", + [ + body("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + body("account").exists().trim().isEthereumAddress(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.shop_settlement_manager_remove.bind(this) + ); + this.app.get( + "/v1/shop/settlement/manager/get/:shopId", + [ + param("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + ], + this.shop_settlement_manager_get.bind(this) + ); + this.app.get( + "/v1/shop/settlement/client/length/:shopId", + [ + param("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + ], + this.shop_settlement_client_length.bind(this) + ); + this.app.get( + "/v1/shop/settlement/client/list/:shopId", + [ + param("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + query("startIndex").exists().trim().isNumeric(), + query("endIndex").exists().trim().isNumeric(), + ], + this.shop_settlement_client_list.bind(this) + ); + this.app.post( + "/v1/shop/settlement/collect", + [ + body("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + body("account").exists().trim().isEthereumAddress(), + body("clients").exists(), + body("signature") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{130}$/i), + ], + this.shop_settlement_collect.bind(this) + ); + this.app.get( + "/v1/shop/settlement/collect/history/:shopId", + [ + param("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + query("pageNumber").exists().trim().isNumeric(), + query("pageSize").exists().trim().isNumeric(), + ], + this.shop_settlement_collect_history.bind(this) + ); } private async getNonce(req: express.Request, res: express.Response) { @@ -1139,19 +1233,13 @@ export class ShopRouter { // 서명검증 const nonce = await this.contractManager.sideShopContract.nonceOf(account); - const message = ContractUtils.getShopRefundMessage( - shopId, - account, - amount, - nonce, - this.contractManager.sideChainId - ); + const message = ContractUtils.getShopRefundMessage(shopId, amount, nonce, this.contractManager.sideChainId); if (!ContractUtils.verifyMessage(account, message, signature)) return res.status(200).json(ResponseMessage.getErrorMessage("1501")); const tx = await this.contractManager.sideShopContract .connect(signerItem.signer) - .refund(shopId, account, amount, signature); + .refund(shopId, amount, signature); logger.http(`TxHash(/v1/shop/refund): ${tx.hash}`); this.metrics.add("success", 1); @@ -1264,6 +1352,7 @@ export class ShopRouter { delegator: info.delegator, providedAmount: info.providedAmount.toString(), usedAmount: info.usedAmount.toString(), + collectedAmount: info.collectedAmount.toString(), refundedAmount: info.refundedAmount.toString(), }; this.metrics.add("success", 1); @@ -1306,4 +1395,296 @@ export class ShopRouter { return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } } + + /** + * POST /v1/shop/settlement/manager/set + * @private + */ + private async shop_settlement_manager_set(req: express.Request, res: express.Response) { + logger.http(`POST /v1/shop/settlement/manager/set ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(); + try { + const shopId: string = String(req.body.shopId).trim(); + const account: string = String(req.body.account).trim(); + const managerId: string = String(req.body.managerId).trim(); + const signature: string = String(req.body.signature).trim(); + + const contract = this.contractManager.sideShopContract; + const message = ContractUtils.getSetSettlementManagerMessage( + shopId, + managerId, + await contract.nonceOf(account), + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + + const tx = await contract.connect(signerItem.signer).setSettlementManager(shopId, managerId, signature); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + managerId, + txHash: tx.hash, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/shop/settlement/manager/set : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + /** + * POST /v1/shop/settlement/manager/remove + * @private + */ + private async shop_settlement_manager_remove(req: express.Request, res: express.Response) { + logger.http(`POST /v1/shop/settlement/manager/remove ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(); + try { + const shopId: string = String(req.body.shopId).trim(); + const account: string = String(req.body.account).trim(); + const signature: string = String(req.body.signature).trim(); + + const contract = this.contractManager.sideShopContract; + const message = ContractUtils.getRemoveSettlementManagerMessage( + shopId, + await contract.nonceOf(account), + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + + const tx = await contract.connect(signerItem.signer).removeSettlementManager(shopId, signature); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + txHash: tx.hash, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/shop/settlement/manager/remove : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + /** + * GET /v1/shop/settlement/manager/get + * @private + */ + private async shop_settlement_manager_get(req: express.Request, res: express.Response) { + logger.http(`GET /v1/shop/settlement/manager/get ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const shopId: string = String(req.params.shopId).trim(); + + const contract = this.contractManager.sideShopContract; + const managerId = await contract.settlementManagerOf(shopId); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + managerId, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/shop/settlement/manager/get : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } + } + + /** + * GET /v1/shop/settlement/client/length + * @private + */ + private async shop_settlement_client_length(req: express.Request, res: express.Response) { + logger.http(`GET /v1/shop/settlement/client/length ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const shopId: string = String(req.params.shopId).trim(); + + const contract = this.contractManager.sideShopContract; + const length = await contract.getSettlementClientLength(shopId); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + length: length.toNumber(), + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/shop/settlement/client/length : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } + } + + /** + * GET /v1/shop/settlement/client/list + * @private + */ + private async shop_settlement_client_list(req: express.Request, res: express.Response) { + logger.http(`GET /v1/shop/settlement/client/list ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const shopId: string = String(req.params.shopId).trim(); + const startIndex = BigNumber.from(String(req.query.startIndex).trim()); + const endIndex = BigNumber.from(String(req.query.endIndex).trim()); + + const contract = this.contractManager.sideShopContract; + const clients = await contract.getSettlementClientList(shopId, startIndex, endIndex); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + clients, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/shop/settlement/client/list : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } + } + + /** + * POST /v1/shop/settlement/collect + * @private + */ + private async shop_settlement_collect(req: express.Request, res: express.Response) { + logger.http(`POST /v1/shop/settlement/collect ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + const signerItem = await this.getRelaySigner(); + try { + const shopId: string = String(req.body.shopId).trim(); + const account: string = String(req.body.account).trim(); + const clients: string[] = String(req.body.clients).trim().split(","); + const signature: string = String(req.body.signature).trim(); + + const contract = this.contractManager.sideShopContract; + const message = ContractUtils.getCollectSettlementAmountMultiClientMessage( + shopId, + clients, + await contract.nonceOf(account), + this.contractManager.sideChainId + ); + if (!ContractUtils.verifyMessage(account, message, signature)) + return res.status(200).json(ResponseMessage.getErrorMessage("1501")); + + const tx = await contract + .connect(signerItem.signer) + .collectSettlementAmountMultiClient(shopId, clients, signature); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopId, + txHash: tx.hash, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`POST /v1/shop/settlement/collect : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(msg); + } finally { + this.releaseRelaySigner(signerItem); + } + } + + /** + * POST /v1/shop/settlement/collect/history + * @private + */ + private async shop_settlement_collect_history(req: express.Request, res: express.Response) { + logger.http(`POST /v1/shop/settlement/collect/history/:shopId ${req.ip}:${JSON.stringify(req.body)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const shopId: string = String(req.params.shopId).trim(); + let pageSize = Number(req.query.pageSize); + if (pageSize > 50) pageSize = 50; + let pageNumber = Number(req.query.pageNumber); + if (pageNumber < 1) pageNumber = 1; + + const records = await this.graph_sidechain.getSettlementAmountList(shopId, pageNumber, pageSize); + const pageInfo = await this.graph_sidechain.getSettlementAmountPageInfo(shopId, pageSize); + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + pageInfo, + items: records.map((m) => { + return { + clientId: m.clientId, + clientAccount: m.clientAccount, + clientCurrency: m.clientCurrency, + clientAmount: m.clientAmount.toString(), + clientTotal: m.clientTotal.toString(), + managerId: m.managerId, + managerAccount: m.managerAccount, + managerCurrency: m.managerCurrency, + managerAmount: m.managerAmount.toString(), + managerTotal: m.managerTotal.toString(), + blockNumber: m.blockNumber.toString(), + blockTimestamp: m.blockTimestamp.toString(), + transactionHash: m.transactionHash, + }; + }), + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v1/shop/settlement/collect/history/:shopId : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } } diff --git a/packages/relay/src/routers/StorePurchaseRouter.ts b/packages/relay/src/routers/StorePurchaseRouter.ts index 7275619e..b5fb1c6b 100644 --- a/packages/relay/src/routers/StorePurchaseRouter.ts +++ b/packages/relay/src/routers/StorePurchaseRouter.ts @@ -8,9 +8,7 @@ import { RelayStorage } from "../storage/RelayStorage"; import { IStorePurchaseData, PHONE_NULL } from "../types"; import { ResponseMessage } from "../utils/Errors"; -// tslint:disable-next-line:no-implicit-dependencies import { BigNumber } from "@ethersproject/bignumber"; -// tslint:disable-next-line:no-implicit-dependencies import { AddressZero } from "@ethersproject/constants"; import express from "express"; import { body, param, validationResult } from "express-validator"; diff --git a/packages/relay/src/routers/TaskRouter.ts b/packages/relay/src/routers/TaskRouter.ts index c0e5093b..fe067313 100644 --- a/packages/relay/src/routers/TaskRouter.ts +++ b/packages/relay/src/routers/TaskRouter.ts @@ -6,7 +6,6 @@ import { WebService } from "../service/WebService"; import { RelayStorage } from "../storage/RelayStorage"; import { ResponseMessage } from "../utils/Errors"; -// tslint:disable-next-line:no-implicit-dependencies import express from "express"; import { param, validationResult } from "express-validator"; diff --git a/packages/relay/src/routers/TokenRouter.ts b/packages/relay/src/routers/TokenRouter.ts index 1581d2b9..e7bb148a 100644 --- a/packages/relay/src/routers/TokenRouter.ts +++ b/packages/relay/src/routers/TokenRouter.ts @@ -10,9 +10,9 @@ import { ContractUtils } from "../utils/ContractUtils"; import { ResponseMessage } from "../utils/Errors"; import { Validation } from "../validation"; -// tslint:disable-next-line:no-implicit-dependencies +import { BigNumber } from "@ethersproject/bignumber"; import { AddressZero } from "@ethersproject/constants"; -import { BigNumber, ethers } from "ethers"; +import { ethers } from "ethers"; import express from "express"; import { body, param, validationResult } from "express-validator"; import { BOACoin } from "../common/Amount"; @@ -158,6 +158,21 @@ export class TokenRouter { ], this.summary_shop.bind(this) ); + this.app.get( + "/v2/summary/account/:account", + [param("account").exists().trim().isEthereumAddress()], + this.v2_summary_account.bind(this) + ); + this.app.get( + "/v2/summary/shop/:shopId", + [ + param("shopId") + .exists() + .trim() + .matches(/^(0x)[0-9a-f]{64}$/i), + ], + this.v2_summary_shop.bind(this) + ); } private async token_main_nonce(req: express.Request, res: express.Response) { @@ -510,7 +525,7 @@ export class TokenRouter { } const isProvider = await this.contractManager.sideLedgerContract.isProvider(account); - const assistant = await this.contractManager.sideLedgerContract.assistantOf(account); + const assistant = await this.contractManager.sideLedgerContract.provisionAgentOf(account); const symbol = await this.contractManager.sideTokenContract.symbol(); const name = await this.contractManager.sideTokenContract.name(); @@ -699,4 +714,236 @@ export class TokenRouter { return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } } + + private async v2_summary_account(req: express.Request, res: express.Response) { + logger.http(`GET /v2/summary/account/:account ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + let account: string = String(req.params.account).trim(); + if (ContractUtils.isTemporaryAccount(account)) { + const realAccount = await this.storage.getRealAccountOnTemporary(account); + if (realAccount === undefined) { + return res.status(200).json(ResponseMessage.getErrorMessage("2004")); + } else { + account = realAccount; + } + } + + const isProvider = await this.contractManager.sideLedgerContract.isProvider(account); + const provisionAgent = await this.contractManager.sideLedgerContract.provisionAgentOf(account); + const refundAgent = await this.contractManager.sideLedgerContract.refundAgentOf(account); + const withdrawalAgent = await this.contractManager.sideLedgerContract.withdrawalAgentOf(account); + + const symbol = await this.contractManager.sideTokenContract.symbol(); + const name = await this.contractManager.sideTokenContract.name(); + const tokenAmount = BOACoin.make(1).value; + const pointAmount = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint(tokenAmount); + const decimals = await this.contractManager.sideTokenContract.decimals(); + + const pointBalance = await this.contractManager.sideLedgerContract.pointBalanceOf(account); + const pointValue = BigNumber.from(pointBalance); + + const tokenBalanceInLedger = await this.contractManager.sideLedgerContract.tokenBalanceOf(account); + const tokenValueInLedger = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInLedger + ); + const tokenBalanceInMainChain = await this.contractManager.mainTokenContract.balanceOf(account); + const tokenValueInMainChain = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInMainChain + ); + const tokenBalanceInSideChain = await this.contractManager.sideTokenContract.balanceOf(account); + const tokenValueInSideChain = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInSideChain + ); + + const defaultCurrencySymbol = await this.contractManager.sideCurrencyRateContract.defaultSymbol(); + + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + account, + tokenInfo: { + symbol, + name, + decimals, + }, + exchangeRate: { + token: { + symbol, + value: tokenAmount.toString(), + }, + currency: { + symbol: defaultCurrencySymbol, + value: pointAmount.toString(), + }, + }, + provider: { + enable: isProvider, + assistant: provisionAgent, + }, + agent: { + provision: provisionAgent, + refund: refundAgent, + withdrawal: withdrawalAgent, + }, + ledger: { + point: { balance: pointBalance.toString(), value: pointValue.toString() }, + token: { balance: tokenBalanceInLedger.toString(), value: tokenValueInLedger.toString() }, + }, + mainChain: { + point: { balance: "0", value: "0" }, + token: { balance: tokenBalanceInMainChain.toString(), value: tokenValueInMainChain.toString() }, + }, + sideChain: { + point: { balance: "0", value: "0" }, + token: { balance: tokenBalanceInSideChain.toString(), value: tokenValueInSideChain.toString() }, + }, + protocolFees: { + transfer: (await this.contractManager.mainTokenContract.getProtocolFee()).toString(), + withdraw: ( + await this.contractManager.mainChainBridgeContract.getProtocolFee( + this.contractManager.mainTokenId + ) + ).toString(), + deposit: ( + await this.contractManager.sideLoyaltyBridgeContract.getProtocolFee( + this.contractManager.sideTokenId + ) + ).toString(), + }, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v2/summary/account/:account : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } + + private async v2_summary_shop(req: express.Request, res: express.Response) { + logger.http(`GET /v2/summary/shop/:shopId ${req.ip}:${JSON.stringify(req.params)}`); + + const errors = validationResult(req); + if (!errors.isEmpty()) { + return res.status(200).json(ResponseMessage.getErrorMessage("2001", { validation: errors.array() })); + } + + try { + const shopId: string = String(req.params.shopId).trim(); + const info = await this.contractManager.sideShopContract.shopOf(shopId); + const info2 = await this.contractManager.sideShopContract.refundableOf(shopId); + + const shopInfo = { + shopId: info.shopId, + name: info.name, + currency: info.currency, + status: info.status, + account: info.account, + delegator: info.delegator, + providedAmount: info.providedAmount.toString(), + usedAmount: info.usedAmount.toString(), + collectedAmount: info.collectedAmount.toString(), + refundedAmount: info.refundedAmount.toString(), + refundableAmount: info2.refundableAmount.toString(), + refundableToken: info2.refundableToken.toString(), + }; + + const account: string = shopInfo.account; + + const symbol = await this.contractManager.sideTokenContract.symbol(); + const name = await this.contractManager.sideTokenContract.name(); + const tokenAmount = BOACoin.make(1).value; + const pointAmount = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint(tokenAmount); + const decimals = await this.contractManager.sideTokenContract.decimals(); + + const pointBalance = await this.contractManager.sideLedgerContract.pointBalanceOf(account); + const pointValue = BigNumber.from(pointBalance); + + const tokenBalanceInLedger = await this.contractManager.sideLedgerContract.tokenBalanceOf(account); + const tokenValueInLedger = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInLedger + ); + const tokenBalanceInMainChain = await this.contractManager.mainTokenContract.balanceOf(account); + const tokenValueInMainChain = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInMainChain + ); + const tokenBalanceInSideChain = await this.contractManager.sideTokenContract.balanceOf(account); + const tokenValueInSideChain = await this.contractManager.sideCurrencyRateContract.convertTokenToPoint( + tokenBalanceInSideChain + ); + const defaultCurrencySymbol = await this.contractManager.sideCurrencyRateContract.defaultSymbol(); + + const provisionAgent = await this.contractManager.sideLedgerContract.provisionAgentOf(account); + const refundAgent = await this.contractManager.sideLedgerContract.refundAgentOf(account); + const withdrawalAgent = await this.contractManager.sideLedgerContract.withdrawalAgentOf(account); + + const settlementManager = await this.contractManager.sideShopContract.settlementManagerOf(shopId); + + this.metrics.add("success", 1); + return res.status(200).json( + this.makeResponseData(0, { + shopInfo, + tokenInfo: { + symbol, + name, + decimals, + }, + exchangeRate: { + token: { + symbol, + value: tokenAmount.toString(), + }, + currency: { + symbol: defaultCurrencySymbol, + value: pointAmount.toString(), + }, + }, + settlement: { + manager: settlementManager, + }, + agent: { + provision: provisionAgent, + refund: refundAgent, + withdrawal: withdrawalAgent, + }, + ledger: { + point: { balance: pointBalance.toString(), value: pointValue.toString() }, + token: { balance: tokenBalanceInLedger.toString(), value: tokenValueInLedger.toString() }, + }, + mainChain: { + point: { balance: "0", value: "0" }, + token: { balance: tokenBalanceInMainChain.toString(), value: tokenValueInMainChain.toString() }, + }, + sideChain: { + point: { balance: "0", value: "0" }, + token: { balance: tokenBalanceInSideChain.toString(), value: tokenValueInSideChain.toString() }, + }, + protocolFees: { + transfer: (await this.contractManager.mainTokenContract.getProtocolFee()).toString(), + withdraw: ( + await this.contractManager.mainChainBridgeContract.getProtocolFee( + this.contractManager.mainTokenId + ) + ).toString(), + deposit: ( + await this.contractManager.sideLoyaltyBridgeContract.getProtocolFee( + this.contractManager.sideTokenId + ) + ).toString(), + }, + }) + ); + } catch (error: any) { + const msg = ResponseMessage.getEVMErrorMessage(error); + logger.error(`GET /v2/summary/shop/:shopId : ${msg.error.message}`); + this.metrics.add("failure", 1); + return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); + } + } } diff --git a/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts b/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts index ee5479d2..4bf25d03 100644 --- a/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts +++ b/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts @@ -10,7 +10,6 @@ import { Scheduler } from "./Scheduler"; import axios from "axios"; import URI from "urijs"; -// tslint:disable-next-line:no-implicit-dependencies import { AddressZero } from "@ethersproject/constants"; export interface IWalletData { diff --git a/packages/relay/src/scheduler/WatchScheduler.ts b/packages/relay/src/scheduler/WatchScheduler.ts index 14affa29..f69167f2 100644 --- a/packages/relay/src/scheduler/WatchScheduler.ts +++ b/packages/relay/src/scheduler/WatchScheduler.ts @@ -22,7 +22,6 @@ import { ContractUtils } from "../utils/ContractUtils"; import { HTTPClient } from "../utils/Utils"; import { Scheduler } from "./Scheduler"; -// tslint:disable-next-line:no-implicit-dependencies import { ContractTransaction } from "@ethersproject/contracts"; import { BigNumber, ethers } from "ethers"; diff --git a/packages/relay/src/storage/GraphStorage.ts b/packages/relay/src/storage/GraphStorage.ts index 3014df69..2d9dbccb 100644 --- a/packages/relay/src/storage/GraphStorage.ts +++ b/packages/relay/src/storage/GraphStorage.ts @@ -6,6 +6,7 @@ import { IGraphShopData, IGraphShopHistoryData, IGraphTokenTransferHistoryData, + ISettlementAmountListData, IStatisticsAccountInfo, IStatisticsShopInfo, } from "../types"; @@ -452,4 +453,65 @@ export class GraphStorage extends Storage { }); }); } + + public getSettlementAmountList( + managerId: string, + pageNumber: number, + pageSize: number + ): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("shop", "getSettlementAmountList", { + scheme: this.scheme, + pageNumber, + pageSize, + managerId, + }) + .then((result) => { + return resolve( + result.rows.map((m) => { + return { + clientId: "0x" + m.clientId.toString("hex"), + clientAccount: "0x" + m.clientAccount.toString("hex"), + clientCurrency: m.clientCurrency, + clientAmount: BigNumber.from(m.clientAmount), + clientTotal: BigNumber.from(m.clientTotal), + managerId: "0x" + m.managerId.toString("hex"), + managerAccount: "0x" + m.managerAccount.toString("hex"), + managerCurrency: m.managerCurrency, + managerAmount: BigNumber.from(m.managerAmount), + managerTotal: BigNumber.from(m.managerTotal), + blockNumber: BigNumber.from(m.blockNumber), + blockTimestamp: BigNumber.from(m.blockTimestamp), + transactionHash: "0x" + m.transactionHash.toString("hex"), + }; + }) + ); + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } + + public getSettlementAmountPageInfo(managerId: string, pageSize: number): Promise { + return new Promise(async (resolve, reject) => { + this.queryForMapper("shop", "getSettlementAmountPageInfo", { scheme: this.scheme, pageSize, managerId }) + .then((result) => { + if (result.rows.length > 0) { + const m = result.rows[0]; + return resolve({ + totalCount: Number(m.totalCount), + totalPages: Number(m.totalPages), + }); + } else { + return reject(new Error("")); + } + }) + .catch((reason) => { + if (reason instanceof Error) return reject(reason); + return reject(new Error(reason)); + }); + }); + } } diff --git a/packages/relay/src/storage/graph/shop.xml b/packages/relay/src/storage/graph/shop.xml index a148b36e..0654d718 100644 --- a/packages/relay/src/storage/graph/shop.xml +++ b/packages/relay/src/storage/graph/shop.xml @@ -37,6 +37,48 @@ FROM RowInfo; + + + +