From d582b134ae88769a5386eb3dc0f2570744bd0f8c 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 | 4 +- packages/relay/src/routers/PaymentRouter.ts | 5 +- packages/relay/src/routers/ProviderRouter.ts | 11 +- packages/relay/src/routers/ShopRouter.ts | 335 +++++++++- .../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/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 | 620 +++++++++++++++++- 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 +++++++++ yarn.lock | 14 +- 39 files changed, 4184 insertions(+), 230 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..dd985659 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"; 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..389d20a5 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,88 @@ 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) + ); } private async getNonce(req: express.Request, res: express.Response) { @@ -1139,19 +1221,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 +1340,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 +1383,244 @@ 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); + } + } } 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/utils/ContractUtils.ts b/packages/relay/src/utils/ContractUtils.ts index 99dba007..0f5e9e7d 100644 --- a/packages/relay/src/utils/ContractUtils.ts +++ b/packages/relay/src/utils/ContractUtils.ts @@ -9,7 +9,7 @@ import { BigNumber, BigNumberish } from "@ethersproject/bignumber"; // tslint:disable-next-line:no-implicit-dependencies import { arrayify, BytesLike } from "@ethersproject/bytes"; // tslint:disable-next-line:no-implicit-dependencies -import { AddressZero } from "@ethersproject/constants"; +import { AddressZero, HashZero } from "@ethersproject/constants"; // tslint:disable-next-line:no-implicit-dependencies import { ContractReceipt, ContractTransaction } from "@ethersproject/contracts"; // tslint:disable-next-line:no-implicit-dependencies @@ -248,14 +248,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, nonce] + ["bytes32", "uint256", "uint256", "uint256"], + [shopId, amount, chainId, nonce] ); return arrayify(keccak256(encodedResult)); } @@ -775,15 +774,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)); } @@ -832,6 +831,57 @@ export class ContractUtils { return arrayify(keccak256(encodedResult)); } + public static getSetSettlementManagerMessage( + shopId: BytesLike, + managerId: BytesLike, + nonce: BigNumberish, + chainId: BigNumberish + ): Uint8Array { + const encodedResult = defaultAbiCoder.encode( + ["string", "bytes32", "bytes32", "uint256", "uint256"], + ["SetSettlementManager", shopId, managerId, chainId, nonce] + ); + return arrayify(keccak256(encodedResult)); + } + + public static getRemoveSettlementManagerMessage( + shopId: BytesLike, + nonce: BigNumberish, + chainId: BigNumberish + ): Uint8Array { + const encodedResult = defaultAbiCoder.encode( + ["string", "bytes32", "bytes32", "uint256", "uint256"], + ["RemoveSettlementManager", shopId, HashZero, chainId, nonce] + ); + return arrayify(keccak256(encodedResult)); + } + + public static getCollectSettlementAmountMessage( + managerShopId: BytesLike, + clientShopId: BytesLike, + nonce: BigNumberish, + chainId: BigNumberish + ): Uint8Array { + const encodedResult = defaultAbiCoder.encode( + ["string", "bytes32", "bytes32", "uint256", "uint256"], + ["CollectSettlementAmount", managerShopId, clientShopId, chainId, nonce] + ); + return arrayify(keccak256(encodedResult)); + } + + public static getCollectSettlementAmountMultiClientMessage( + managerShopId: BytesLike, + clientShopIds: BytesLike[], + nonce: BigNumberish, + chainId: BigNumberish + ): Uint8Array { + const encodedResult = defaultAbiCoder.encode( + ["string", "bytes32", "bytes32[]", "uint256", "uint256"], + ["CollectSettlementAmountMultiClient", managerShopId, clientShopIds, chainId, nonce] + ); + return arrayify(keccak256(encodedResult)); + } + public static async signMessage(signer: Signer, message: Uint8Array): Promise { return signer.signMessage(message); } diff --git a/packages/relay/test/Agent.test.ts b/packages/relay/test/Agent.test.ts new file mode 100644 index 00000000..a1fe7d42 --- /dev/null +++ b/packages/relay/test/Agent.test.ts @@ -0,0 +1,173 @@ +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; + +import { Config } from "../src/common/Config"; +import { ContractManager } from "../src/contract/ContractManager"; +import { GraphStorage } from "../src/storage/GraphStorage"; +import { RelayStorage } from "../src/storage/RelayStorage"; +import { ContractUtils } from "../src/utils/ContractUtils"; +import { Deployments } from "./helper/Deployments"; +import { TestClient, TestServer } from "./helper/Utility"; + +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; + +import * as path from "path"; +import URI from "urijs"; +import { URL } from "url"; + +import { AddressZero } from "@ethersproject/constants"; + +chai.use(solidity); + +describe("Test of Register Agent", function () { + this.timeout(1000 * 60 * 5); + + const config = new Config(); + config.readFromFile(path.resolve(process.cwd(), "config", "config_test.yaml")); + const contractManager = new ContractManager(config); + const deployments = new Deployments(config); + + let client: TestClient; + let server: TestServer; + let storage: RelayStorage; + let serverURL: URL; + + before("Deploy", async () => { + deployments.setShopData([]); + await deployments.doDeploy(); + }); + + before("Create Config", async () => { + config.contracts.sideChain.tokenAddress = deployments.getContractAddress("TestLYT") || ""; + config.contracts.sideChain.currencyRateAddress = deployments.getContractAddress("CurrencyRate") || ""; + config.contracts.sideChain.phoneLinkerAddress = deployments.getContractAddress("PhoneLinkCollection") || ""; + config.contracts.sideChain.ledgerAddress = deployments.getContractAddress("Ledger") || ""; + config.contracts.sideChain.shopAddress = deployments.getContractAddress("Shop") || ""; + config.contracts.sideChain.loyaltyProviderAddress = deployments.getContractAddress("LoyaltyProvider") || ""; + config.contracts.sideChain.loyaltyConsumerAddress = deployments.getContractAddress("LoyaltyConsumer") || ""; + config.contracts.sideChain.loyaltyExchangerAddress = deployments.getContractAddress("LoyaltyExchanger") || ""; + config.contracts.sideChain.loyaltyTransferAddress = deployments.getContractAddress("LoyaltyTransfer") || ""; + config.contracts.sideChain.loyaltyBridgeAddress = deployments.getContractAddress("LoyaltyBridge") || ""; + config.contracts.sideChain.chainBridgeAddress = deployments.getContractAddress("SideChainBridge") || ""; + + config.contracts.mainChain.tokenAddress = deployments.getContractAddress("MainChainKIOS") || ""; + config.contracts.mainChain.loyaltyBridgeAddress = + deployments.getContractAddress("MainChainLoyaltyBridge") || ""; + config.contracts.mainChain.chainBridgeAddress = deployments.getContractAddress("MainChainBridge") || ""; + + config.relay.certifiers = deployments.accounts.certifiers.map((m) => m.privateKey); + config.relay.relayEndpoint = `http://127.0.0.1:${config.server.port}`; + + client = new TestClient({ + headers: { + Authorization: config.relay.accessKey, + }, + }); + }); + + before("Create TestServer", async () => { + serverURL = new URL(`http://127.0.0.1:${config.server.port}`); + storage = await RelayStorage.make(config.database); + const graph_sidechain = await GraphStorage.make(config.graph_sidechain); + const graph_mainchain = await GraphStorage.make(config.graph_mainchain); + await contractManager.attach(); + server = new TestServer(config, contractManager, storage, graph_sidechain, graph_mainchain); + }); + + before("Start TestServer", async () => { + await server.start(); + }); + + after("Stop TestServer", async () => { + await server.stop(); + await storage.dropTestDB(); + }); + + it("Register agent provision", async () => { + const user = deployments.accounts.users[0]; + const response1 = await client.get(URI(serverURL).directory(`/v1/agent/provision/${user.address}`).toString()); + expect(response1.data.data.account).to.deep.equal(user.address); + expect(response1.data.data.agent).to.deep.equal(AddressZero); + + const agent = deployments.accounts.users[1]; + const nonce = await contractManager.sideLedgerContract.nonceOf(user.address); + const message = ContractUtils.getRegisterAgentMessage( + user.address, + agent.address, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(user, message); + const response2 = await client.post(URI(serverURL).directory(`/v1/agent/provision`).toString(), { + account: user.address, + agent: agent.address, + signature, + }); + expect(response2.data.code).to.equal(0); + expect(response2.data.data).to.not.equal(undefined); + expect(response2.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + const response3 = await client.get(URI(serverURL).directory(`/v1/agent/provision/${user.address}`).toString()); + expect(response3.data.data.account).to.deep.equal(user.address); + expect(response3.data.data.agent).to.deep.equal(agent.address); + }); + + it("Register agent refund", async () => { + const user = deployments.accounts.users[0]; + const response1 = await client.get(URI(serverURL).directory(`/v1/agent/refund/${user.address}`).toString()); + expect(response1.data.data.account).to.deep.equal(user.address); + expect(response1.data.data.agent).to.deep.equal(AddressZero); + + const agent = deployments.accounts.users[2]; + const nonce = await contractManager.sideLedgerContract.nonceOf(user.address); + const message = ContractUtils.getRegisterAgentMessage( + user.address, + agent.address, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(user, message); + const response2 = await client.post(URI(serverURL).directory(`/v1/agent/refund`).toString(), { + account: user.address, + agent: agent.address, + signature, + }); + expect(response2.data.code).to.equal(0); + expect(response2.data.data).to.not.equal(undefined); + expect(response2.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + const response3 = await client.get(URI(serverURL).directory(`/v1/agent/refund/${user.address}`).toString()); + expect(response3.data.data.account).to.deep.equal(user.address); + expect(response3.data.data.agent).to.deep.equal(agent.address); + }); + + it("Register agent withdrawal", async () => { + const user = deployments.accounts.users[0]; + const response1 = await client.get(URI(serverURL).directory(`/v1/agent/withdrawal/${user.address}`).toString()); + expect(response1.data.data.account).to.deep.equal(user.address); + expect(response1.data.data.agent).to.deep.equal(AddressZero); + + const agent = deployments.accounts.users[3]; + const nonce = await contractManager.sideLedgerContract.nonceOf(user.address); + const message = ContractUtils.getRegisterAgentMessage( + user.address, + agent.address, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(user, message); + const response2 = await client.post(URI(serverURL).directory(`/v1/agent/withdrawal`).toString(), { + account: user.address, + agent: agent.address, + signature, + }); + expect(response2.data.code).to.equal(0); + expect(response2.data.data).to.not.equal(undefined); + expect(response2.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + const response3 = await client.get(URI(serverURL).directory(`/v1/agent/withdrawal/${user.address}`).toString()); + expect(response3.data.data.account).to.deep.equal(user.address); + expect(response3.data.data.agent).to.deep.equal(agent.address); + }); +}); diff --git a/packages/relay/test/LoyaltyProvider.test.ts b/packages/relay/test/LoyaltyProvider.test.ts index bf6c86ff..5c7e070d 100644 --- a/packages/relay/test/LoyaltyProvider.test.ts +++ b/packages/relay/test/LoyaltyProvider.test.ts @@ -231,7 +231,7 @@ describe("Test of LoyaltyProvider", function () { const assistant = deployments.accounts.users[1]; const nonce = await contractManager.sideLedgerContract.nonceOf(provider.address); - const message = ContractUtils.getRegisterAssistanceMessage( + const message = ContractUtils.getRegisterAgentMessage( provider.address, assistant.address, nonce, diff --git a/packages/relay/test/LoyaltyProvider2.test.ts b/packages/relay/test/LoyaltyProvider2.test.ts new file mode 100644 index 00000000..8b639eab --- /dev/null +++ b/packages/relay/test/LoyaltyProvider2.test.ts @@ -0,0 +1,341 @@ +import "@nomiclabs/hardhat-ethers"; +import "@nomiclabs/hardhat-waffle"; + +import { Amount } from "../src/common/Amount"; +import { Config } from "../src/common/Config"; +import { ContractManager } from "../src/contract/ContractManager"; +import { GraphStorage } from "../src/storage/GraphStorage"; +import { RelayStorage } from "../src/storage/RelayStorage"; +import { ContractUtils } from "../src/utils/ContractUtils"; +import { Deployments } from "./helper/Deployments"; +import { TestClient, TestServer } from "./helper/Utility"; + +import chai, { expect } from "chai"; +import { solidity } from "ethereum-waffle"; + +import * as path from "path"; +import URI from "urijs"; +import { URL } from "url"; + +import { AddressZero } from "@ethersproject/constants"; + +chai.use(solidity); + +describe("Test of LoyaltyProvider", function () { + this.timeout(1000 * 60 * 5); + + const config = new Config(); + config.readFromFile(path.resolve(process.cwd(), "config", "config_test.yaml")); + const contractManager = new ContractManager(config); + const deployments = new Deployments(config); + + let client: TestClient; + let server: TestServer; + let storage: RelayStorage; + let serverURL: URL; + + before("Deploy", async () => { + deployments.setShopData([]); + await deployments.doDeploy(); + }); + + before("Create Config", async () => { + config.contracts.sideChain.tokenAddress = deployments.getContractAddress("TestLYT") || ""; + config.contracts.sideChain.currencyRateAddress = deployments.getContractAddress("CurrencyRate") || ""; + config.contracts.sideChain.phoneLinkerAddress = deployments.getContractAddress("PhoneLinkCollection") || ""; + config.contracts.sideChain.ledgerAddress = deployments.getContractAddress("Ledger") || ""; + config.contracts.sideChain.shopAddress = deployments.getContractAddress("Shop") || ""; + config.contracts.sideChain.loyaltyProviderAddress = deployments.getContractAddress("LoyaltyProvider") || ""; + config.contracts.sideChain.loyaltyConsumerAddress = deployments.getContractAddress("LoyaltyConsumer") || ""; + config.contracts.sideChain.loyaltyExchangerAddress = deployments.getContractAddress("LoyaltyExchanger") || ""; + config.contracts.sideChain.loyaltyTransferAddress = deployments.getContractAddress("LoyaltyTransfer") || ""; + config.contracts.sideChain.loyaltyBridgeAddress = deployments.getContractAddress("LoyaltyBridge") || ""; + config.contracts.sideChain.chainBridgeAddress = deployments.getContractAddress("SideChainBridge") || ""; + + config.contracts.mainChain.tokenAddress = deployments.getContractAddress("MainChainKIOS") || ""; + config.contracts.mainChain.loyaltyBridgeAddress = + deployments.getContractAddress("MainChainLoyaltyBridge") || ""; + config.contracts.mainChain.chainBridgeAddress = deployments.getContractAddress("MainChainBridge") || ""; + + config.relay.certifiers = deployments.accounts.certifiers.map((m) => m.privateKey); + config.relay.relayEndpoint = `http://127.0.0.1:${config.server.port}`; + + client = new TestClient({ + headers: { + Authorization: config.relay.accessKey, + }, + }); + }); + + before("Create TestServer", async () => { + serverURL = new URL(`http://127.0.0.1:${config.server.port}`); + storage = await RelayStorage.make(config.database); + const graph_sidechain = await GraphStorage.make(config.graph_sidechain); + const graph_mainchain = await GraphStorage.make(config.graph_mainchain); + await contractManager.attach(); + server = new TestServer(config, contractManager, storage, graph_sidechain, graph_mainchain); + }); + + before("Start TestServer", async () => { + await server.start(); + }); + + after("Stop TestServer", async () => { + await server.stop(); + await storage.dropTestDB(); + }); + + it("Deposit token", async () => { + for (const userIndex of [0, 1]) { + const amount = Amount.make(1000, 18).value; + const oldTokenBalance = await contractManager.sideLedgerContract.tokenBalanceOf( + deployments.accounts.users[userIndex].address + ); + await contractManager.sideTokenContract + .connect(deployments.accounts.users[userIndex]) + .approve(contractManager.sideLedgerContract.address, amount); + await expect( + contractManager.sideLedgerContract.connect(deployments.accounts.users[userIndex]).deposit(amount) + ) + .to.emit(contractManager.sideLedgerContract, "Deposited") + .withNamedArgs({ + account: deployments.accounts.users[userIndex].address, + depositedToken: amount, + balanceToken: oldTokenBalance.add(amount), + }); + expect( + await contractManager.sideLedgerContract.tokenBalanceOf(deployments.accounts.users[userIndex].address) + ).to.deep.equal(oldTokenBalance.add(amount)); + } + }); + + it("Check Summary of Account", async () => { + const response = await client.get( + URI(serverURL).directory(`/v2/summary/account/${deployments.accounts.users[0].address}`).toString() + ); + + expect(response.data.data.provider.enable).to.deep.equal(false); + expect(response.data.data.agent.provision).to.deep.equal(AddressZero); + expect(response.data.data.agent.withdrawal).to.deep.equal(AddressZero); + }); + + it("Register Provide", async () => { + expect(await contractManager.sideLedgerContract.isProvider(deployments.accounts.users[0].address)).equal(false); + + await expect( + contractManager.sideLedgerContract + .connect(deployments.accounts.deployer) + .registerProvider(deployments.accounts.users[0].address) + ).emit(contractManager.sideLedgerContract, "RegisteredProvider"); + + expect(await contractManager.sideLedgerContract.isProvider(deployments.accounts.users[0].address)).equal(true); + }); + + it("Check Summary of Account", async () => { + const response = await client.get( + URI(serverURL).directory(`/v2/summary/account/${deployments.accounts.users[0].address}`).toString() + ); + + expect(response.data.data.provider.enable).to.deep.equal(true); + expect(response.data.data.agent.provision).to.deep.equal(AddressZero); + expect(response.data.data.agent.withdrawal).to.deep.equal(AddressZero); + }); + + it("Balance of Provider", async () => { + const tokenAmount = Amount.make(1000, 18).value; + const pointAmount = await contractManager.sideCurrencyRateContract.convertTokenToPoint(tokenAmount); + const response = await client.get( + URI(serverURL).directory(`/v1/provider/balance/${deployments.accounts.users[0].address}`).toString() + ); + expect(response.data.data.provider).to.deep.equal(deployments.accounts.users[0].address); + expect(response.data.data.providable.token).to.deep.equal(tokenAmount); + expect(response.data.data.providable.point).to.deep.equal(pointAmount); + }); + + it("Provide Point for Address", async () => { + const provider = deployments.accounts.users[0]; + const receiver = deployments.accounts.users[1]; + const balance0 = await contractManager.sideLedgerContract.tokenBalanceOf(provider.address); + const balance1 = await contractManager.sideLedgerContract.pointBalanceOf(receiver.address); + const pointAmount = Amount.make(10, 18).value; + const tokenAmount = await contractManager.sideCurrencyRateContract.convertPointToToken(pointAmount); + const nonce = await contractManager.sideLedgerContract.nonceOf(provider.address); + const message = ContractUtils.getProvidePointToAddressMessage( + provider.address, + receiver.address, + pointAmount, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(provider, message); + const response = await client.post(URI(serverURL).directory("/v1/provider/send/account").toString(), { + provider: provider.address, + receiver: receiver.address, + amount: pointAmount.toString(), + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + expect(await contractManager.sideLedgerContract.tokenBalanceOf(provider.address)).to.deep.equal( + balance0.sub(tokenAmount) + ); + expect(await contractManager.sideLedgerContract.pointBalanceOf(receiver.address)).to.deep.equal( + balance1.add(pointAmount) + ); + }); + + it("Provide Point for Phone number", async () => { + const phoneNumber = "+82 10-1000-9000"; + const phoneHash = ContractUtils.getPhoneHash(phoneNumber); + const provider = deployments.accounts.users[0]; + const balance0 = await contractManager.sideLedgerContract.tokenBalanceOf(provider.address); + const balance1 = await contractManager.sideLedgerContract.unPayablePointBalanceOf(phoneHash); + const pointAmount = Amount.make(10, 18).value; + const tokenAmount = await contractManager.sideCurrencyRateContract.convertPointToToken(pointAmount); + const nonce = await contractManager.sideLedgerContract.nonceOf(provider.address); + const message = ContractUtils.getProvidePointToPhoneMessage( + provider.address, + phoneHash, + pointAmount, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(provider, message); + const response = await client.post(URI(serverURL).directory("/v1/provider/send/phoneHash").toString(), { + provider: provider.address, + receiver: phoneHash, + amount: pointAmount.toString(), + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + expect(await contractManager.sideLedgerContract.tokenBalanceOf(provider.address)).to.deep.equal( + balance0.sub(tokenAmount) + ); + expect(await contractManager.sideLedgerContract.unPayablePointBalanceOf(phoneHash)).to.deep.equal( + balance1.add(pointAmount) + ); + }); + + it("Register agent", async () => { + const provider = deployments.accounts.users[0]; + const response1 = await client.get( + URI(serverURL).directory(`/v1/agent/provision/${provider.address}`).toString() + ); + expect(response1.data.data.account).to.deep.equal(provider.address); + expect(response1.data.data.agent).to.deep.equal(AddressZero); + + const agent = deployments.accounts.users[1]; + const nonce = await contractManager.sideLedgerContract.nonceOf(provider.address); + const message = ContractUtils.getRegisterAgentMessage( + provider.address, + agent.address, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(provider, message); + const response2 = await client.post(URI(serverURL).directory(`/v1/agent/provision`).toString(), { + account: provider.address, + agent: agent.address, + signature, + }); + expect(response2.data.code).to.equal(0); + expect(response2.data.data).to.not.equal(undefined); + expect(response2.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + const response3 = await client.get( + URI(serverURL).directory(`/v1/agent/provision/${provider.address}`).toString() + ); + expect(response3.data.data.account).to.deep.equal(provider.address); + expect(response3.data.data.agent).to.deep.equal(agent.address); + }); + + it("Check Summary of Account", async () => { + const response = await client.get( + URI(serverURL).directory(`/v2/summary/account/${deployments.accounts.users[0].address}`).toString() + ); + + expect(response.data.data.provider.enable).to.deep.equal(true); + expect(response.data.data.agent.provision).to.deep.equal(deployments.accounts.users[1].address); + expect(response.data.data.agent.withdrawal).to.deep.equal(AddressZero); + }); + + it("Provide Point for Address by Agent", async () => { + const provider = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const receiver = deployments.accounts.users[2]; + const balance0 = await contractManager.sideLedgerContract.tokenBalanceOf(provider.address); + const balance1 = await contractManager.sideLedgerContract.pointBalanceOf(receiver.address); + const pointAmount = Amount.make(10, 18).value; + const tokenAmount = await contractManager.sideCurrencyRateContract.convertPointToToken(pointAmount); + const nonce = await contractManager.sideLedgerContract.nonceOf(agent.address); + const message = ContractUtils.getProvidePointToAddressMessage( + provider.address, + receiver.address, + pointAmount, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(agent, message); + const response = await client.post(URI(serverURL).directory("/v1/provider/send/account").toString(), { + provider: provider.address, + receiver: receiver.address, + amount: pointAmount.toString(), + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + expect(await contractManager.sideLedgerContract.tokenBalanceOf(provider.address)).to.deep.equal( + balance0.sub(tokenAmount) + ); + expect(await contractManager.sideLedgerContract.pointBalanceOf(receiver.address)).to.deep.equal( + balance1.add(pointAmount) + ); + }); + + it("Provide Point for Phone number by Agent", async () => { + const phoneNumber = "+82 10-1000-9000"; + const phoneHash = ContractUtils.getPhoneHash(phoneNumber); + const provider = deployments.accounts.users[0]; + const agent = deployments.accounts.users[1]; + const balance0 = await contractManager.sideLedgerContract.tokenBalanceOf(provider.address); + const balance1 = await contractManager.sideLedgerContract.unPayablePointBalanceOf(phoneHash); + const pointAmount = Amount.make(10, 18).value; + const tokenAmount = await contractManager.sideCurrencyRateContract.convertPointToToken(pointAmount); + const nonce = await contractManager.sideLedgerContract.nonceOf(agent.address); + const message = ContractUtils.getProvidePointToPhoneMessage( + provider.address, + phoneHash, + pointAmount, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(agent, message); + const response = await client.post(URI(serverURL).directory("/v1/provider/send/phoneHash").toString(), { + provider: provider.address, + receiver: phoneHash, + amount: pointAmount.toString(), + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + + expect(await contractManager.sideLedgerContract.tokenBalanceOf(provider.address)).to.deep.equal( + balance0.sub(tokenAmount) + ); + expect(await contractManager.sideLedgerContract.unPayablePointBalanceOf(phoneHash)).to.deep.equal( + balance1.add(pointAmount) + ); + }); +}); diff --git a/packages/relay/test/ShopWithdraw.test.ts b/packages/relay/test/ShopWithdraw.test.ts index 0e0c63be..113d868a 100644 --- a/packages/relay/test/ShopWithdraw.test.ts +++ b/packages/relay/test/ShopWithdraw.test.ts @@ -16,12 +16,12 @@ import { solidity } from "ethereum-waffle"; import { BigNumber, Wallet } from "ethers"; -import assert from "assert"; import path from "path"; import URI from "urijs"; import { URL } from "url"; -import { AddressZero } from "@ethersproject/constants"; +import { BytesLike } from "@ethersproject/bytes"; +import { AddressZero, HashZero } from "@ethersproject/constants"; chai.use(solidity); @@ -61,11 +61,6 @@ describe("Test for Shop", () => { let providerContract: LoyaltyProvider; let ledgerContract: Ledger; - const multiple = BigNumber.from(1000000000); - const price = BigNumber.from(150).mul(multiple); - - const amount = Amount.make(20_000, 18); - let client: TestClient; let server: TestServer; let storage: RelayStorage; @@ -514,7 +509,7 @@ describe("Test for Shop", () => { const url = URI(serverURL).directory("/v1/shop/info").filename(shop.shopId).toString(); const response = await client.get(url); expect(response.data.code).to.equal(0); - assert.deepStrictEqual(response.data.data, { + expect(response.data.data).to.deep.equal({ shopId: shop.shopId, name: "Shop3", currency: "krw", @@ -523,6 +518,7 @@ describe("Test for Shop", () => { account: shop.wallet.address, providedAmount: "100000000000000000000", usedAmount: "500000000000000000000", + collectedAmount: "0", refundedAmount: "0", }); }); @@ -531,7 +527,6 @@ describe("Test for Shop", () => { const nonce = await shopContract.nonceOf(shop.wallet.address); const message = ContractUtils.getShopRefundMessage( shop.shopId, - shop.wallet.address, amount2, nonce, contractManager.sideChainId @@ -573,7 +568,7 @@ describe("Test for Shop", () => { const url = URI(serverURL).directory("/v1/shop/info").filename(shop.shopId).toString(); const response = await client.get(url); expect(response.data.code).to.equal(0); - assert.deepStrictEqual(response.data.data, { + expect(response.data.data).to.deep.equal({ shopId: shop.shopId, name: "Shop3", currency: "krw", @@ -582,9 +577,614 @@ describe("Test for Shop", () => { delegator: "0x0000000000000000000000000000000000000000", providedAmount: "100000000000000000000", usedAmount: "500000000000000000000", + collectedAmount: "0", refundedAmount: "400000000000000000000", }); }); }); }); + + context("Refunds of shops 2", () => { + const userData: IUserData[] = [ + { + phone: "08201012341001", + address: userWallets[0].address, + privateKey: userWallets[0].privateKey, + }, + { + phone: "08201012341002", + address: userWallets[1].address, + privateKey: userWallets[1].privateKey, + }, + { + phone: "08201012341003", + address: userWallets[2].address, + privateKey: userWallets[2].privateKey, + }, + { + phone: "08201012341004", + address: userWallets[3].address, + privateKey: userWallets[3].privateKey, + }, + { + phone: "08201012341005", + address: userWallets[4].address, + privateKey: userWallets[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: shopWallets[0], + }, + { + shopId: "F000200", + name: "Shop2", + currency: "krw", + wallet: shopWallets[1], + }, + { + shopId: "F000300", + name: "Shop3", + currency: "krw", + wallet: shopWallets[2], + }, + { + shopId: "F000400", + name: "Shop4", + currency: "krw", + wallet: shopWallets[3], + }, + { + shopId: "F000500", + name: "Shop5", + currency: "krw", + wallet: shopWallets[4], + }, + { + shopId: "F000600", + name: "Shop6", + currency: "krw", + wallet: shopWallets[5], + }, + ]; + + before("Set Shop ID", async () => { + for (const elem of shopData) { + elem.shopId = ContractUtils.getShopId(elem.wallet.address, LoyaltyNetworkID.ACC_TESTNET); + } + }); + + before("Deploy", async () => { + deployments.setShopData(shopData); + await deployments.doDeploy(); + + ledgerContract = deployments.getContract("Ledger") as Ledger; + consumerContract = deployments.getContract("LoyaltyConsumer") as LoyaltyConsumer; + providerContract = deployments.getContract("LoyaltyProvider") as LoyaltyProvider; + shopContract = deployments.getContract("Shop") as Shop; + }); + + before("Create Config", async () => { + config.contracts.sideChain.tokenAddress = deployments.getContractAddress("TestLYT") || ""; + config.contracts.sideChain.currencyRateAddress = deployments.getContractAddress("CurrencyRate") || ""; + config.contracts.sideChain.phoneLinkerAddress = deployments.getContractAddress("PhoneLinkCollection") || ""; + config.contracts.sideChain.ledgerAddress = deployments.getContractAddress("Ledger") || ""; + config.contracts.sideChain.shopAddress = deployments.getContractAddress("Shop") || ""; + config.contracts.sideChain.loyaltyProviderAddress = deployments.getContractAddress("LoyaltyProvider") || ""; + config.contracts.sideChain.loyaltyConsumerAddress = deployments.getContractAddress("LoyaltyConsumer") || ""; + config.contracts.sideChain.loyaltyExchangerAddress = + deployments.getContractAddress("LoyaltyExchanger") || ""; + config.contracts.sideChain.loyaltyTransferAddress = deployments.getContractAddress("LoyaltyTransfer") || ""; + config.contracts.sideChain.loyaltyBridgeAddress = deployments.getContractAddress("LoyaltyBridge") || ""; + config.contracts.sideChain.chainBridgeAddress = deployments.getContractAddress("SideChainBridge") || ""; + + config.contracts.mainChain.tokenAddress = deployments.getContractAddress("MainChainKIOS") || ""; + config.contracts.mainChain.loyaltyBridgeAddress = + deployments.getContractAddress("MainChainLoyaltyBridge") || ""; + config.contracts.mainChain.chainBridgeAddress = deployments.getContractAddress("MainChainBridge") || ""; + + config.relay.certifiers = deployments.accounts.certifiers.map((m) => m.privateKey); + config.relay.callbackEndpoint = `http://127.0.0.1:${config.server.port}/callback`; + config.relay.relayEndpoint = `http://127.0.0.1:${config.server.port}`; + + client = new TestClient({ + headers: { + Authorization: config.relay.accessKey, + }, + }); + }); + + before("Create TestServer", async () => { + serverURL = new URL(`http://127.0.0.1:${config.server.port}`); + storage = await RelayStorage.make(config.database); + const graph_sidechain = await GraphStorage.make(config.graph_sidechain); + const graph_mainchain = await GraphStorage.make(config.graph_mainchain); + await contractManager.attach(); + server = new TestServer(config, contractManager, storage, graph_sidechain, graph_mainchain); + }); + + before("Start TestServer", async () => { + await server.start(); + }); + + after("Stop TestServer", async () => { + await server.stop(); + await storage.dropTestDB(); + }); + + 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 = ContractUtils.zeroGWEI(purchaseAmount.mul(purchase.providePercent).div(100)); + const amt = ContractUtils.zeroGWEI(purchaseAmount.mul(purchase.providePercent).div(100)); + const userAccount = + userData[purchase.userIndex].address.trim() !== "" + ? userData[purchase.userIndex].address.trim() + : AddressZero; + const purchaseParam = { + purchaseId: purchase.purchaseId, + 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, + contractManager.sideChainId + ); + const purchaseMessage = ContractUtils.getPurchasesMessage( + 0, + [purchaseParam], + contractManager.sideChainId + ); + const signatures = await Promise.all( + deployments.accounts.validators.map((m) => ContractUtils.signMessage(m, purchaseMessage)) + ); + const proposeMessage = ContractUtils.getPurchasesProposeMessage( + 0, + [purchaseParam], + signatures, + contractManager.sideChainId + ); + 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( + purchase.purchaseId, + purchaseAmount, + loyaltyAmount, + purchase.currency.toLowerCase(), + shopData[purchase.shopIndex].shopId, + userAccount, + phoneHash, + deployments.accounts.system.address + ) + .emit(ledgerContract, "ProvidedPoint") + .withNamedArgs({ + account: userAccount, + providedPoint: amt, + providedValue: amt, + purchaseId: purchase.purchaseId, + }); + } + }); + }); + + 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 nonce = await ledgerContract.nonceOf(userWallets[purchase.userIndex].address); + const paymentId = ContractUtils.getPaymentId(userWallets[purchase.userIndex].address, nonce); + const purchaseAmount = Amount.make(purchase.amount, 18).value; + const shop = shopData[purchase.shopIndex]; + const signature = await ContractUtils.signLoyaltyNewPayment( + userWallets[purchase.userIndex], + paymentId, + purchase.purchaseId, + purchaseAmount, + purchase.currency, + shop.shopId, + nonce, + contractManager.sideChainId + ); + + const [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: userWallets[purchase.userIndex].address, + signature, + secretLock, + }) + ).to.emit(consumerContract, "LoyaltyPaymentEvent"); + + const paymentData = await consumerContract.loyaltyPaymentOf(paymentId); + expect(paymentData.paymentId).to.deep.equal(paymentId); + expect(paymentData.purchaseId).to.deep.equal(purchase.purchaseId); + expect(paymentData.currency).to.deep.equal(purchase.currency); + expect(paymentData.shopId).to.deep.equal(shop.shopId); + expect(paymentData.account).to.deep.equal(userWallets[purchase.userIndex].address); + expect(paymentData.paidPoint).to.deep.equal(purchaseAmount); + expect(paymentData.paidValue).to.deep.equal(purchaseAmount); + + 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, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(shop.wallet, message); + + const uri = URI(serverURL).directory("/v1/shop/settlement/manager").filename("set"); + const url = uri.toString(); + const response = await client.post(url, { + shopId: shop.shopId, + account: shop.wallet.address, + managerId: managerShop.shopId, + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const uri = URI(serverURL).directory("/v1/shop/settlement/manager/get").filename(shop.shopId); + const url = uri.toString(); + const response = await client.get(url); + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.managerId).to.be.equal(managerShop.shopId); + } + }); + + it("check client", async () => { + const uri = URI(serverURL) + .directory("/v1/shop/settlement/client/length") + .filename(managerShop.shopId) + .toString(); + const response = await client.get(uri); + expect(response.data.data.length).to.be.equal(clients.length); + + const response2 = await client.get( + URI(serverURL) + .directory("/v1/shop/settlement/client/list") + .filename(managerShop.shopId) + .addQuery("startIndex", 0) + .addQuery("endIndex", 2) + .toString() + ); + expect(response2.data.data.clients).to.deep.equal(clients.slice(0, 2)); + }); + + 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, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(shop.wallet, message); + + const uri = URI(serverURL).directory("/v1/shop/settlement/manager").filename("remove"); + const url = uri.toString(); + const response = await client.post(url, { + shopId: shop.shopId, + account: shop.wallet.address, + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const uri = URI(serverURL).directory("/v1/shop/settlement/manager/get").filename(shop.shopId); + const url = uri.toString(); + const response = await client.get(url); + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.managerId).to.be.equal(HashZero); + } + }); + + it("check client", async () => { + const uri = URI(serverURL) + .directory("/v1/shop/settlement/client/length") + .filename(managerShop.shopId) + .toString(); + const response = await client.get(uri); + expect(response.data.data.length).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, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(shop.wallet, message); + + const uri = URI(serverURL).directory("/v1/shop/settlement/manager").filename("set"); + const url = uri.toString(); + const response = await client.post(url, { + shopId: shop.shopId, + account: shop.wallet.address, + managerId: managerShop.shopId, + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + } + }); + + it("check manager", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const uri = URI(serverURL).directory("/v1/shop/settlement/manager/get").filename(shop.shopId); + const url = uri.toString(); + const response = await client.get(url); + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.managerId).to.be.equal(managerShop.shopId); + } + }); + + it("check client", async () => { + const uri = URI(serverURL) + .directory("/v1/shop/settlement/client/length") + .filename(managerShop.shopId) + .toString(); + const response = await client.get(uri); + expect(response.data.data.length).to.be.equal(clients.length); + + const response2 = await client.get( + URI(serverURL) + .directory("/v1/shop/settlement/client/list") + .filename(managerShop.shopId) + .addQuery("startIndex", 0) + .addQuery("endIndex", 2) + .toString() + ); + expect(response2.data.data.clients).to.deep.equal(clients.slice(0, 2)); + }); + }); + + 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("Check refundable amount", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const url = URI(serverURL).directory("/v1/shop/refundable/").filename(shop.shopId).toString(); + const response = await client.get(url); + const refundableAmount = BigNumber.from(response.data.data.refundableAmount); + expect(refundableAmount).to.equal(expected[shopIndex]); + } + }); + + it("getCollectSettlementAmountMultiClientMessage", async () => { + const clientLength = await shopContract.getSettlementClientLength(managerShop.shopId); + const clients = await shopContract.getSettlementClientList(managerShop.shopId, 0, clientLength); + const nonce = await shopContract.nonceOf(managerShop.wallet.address); + const message = ContractUtils.getCollectSettlementAmountMultiClientMessage( + managerShop.shopId, + clients, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(managerShop.wallet, message); + + const uri = URI(serverURL).directory("/v1/shop/settlement/collect"); + const url = uri.toString(); + const response = await client.post(url, { + shopId: managerShop.shopId, + account: managerShop.wallet.address, + clients: clients.join(","), + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + }); + + it("Check refundable amount", async () => { + for (let shopIndex = 0; shopIndex < 4; shopIndex++) { + const shop = shopData[shopIndex]; + const url = URI(serverURL).directory("/v1/shop/refundable/").filename(shop.shopId).toString(); + const response = await client.get(url); + const refundableAmount = BigNumber.from(response.data.data.refundableAmount); + expect(refundableAmount).to.equal(0); + } + }); + + it("Check refundable amount of settlement manager", async () => { + const url = URI(serverURL).directory("/v1/shop/refundable/").filename(managerShop.shopId).toString(); + const response = await client.get(url); + const refundableAmount = BigNumber.from(response.data.data.refundableAmount); + expect(refundableAmount).to.equal(sumExpected); + amountToken = BigNumber.from(response.data.data.refundableToken); + }); + + it("refund", async () => { + const nonce = await shopContract.nonceOf(managerShop.wallet.address); + const message = ContractUtils.getShopRefundMessage( + managerShop.shopId, + sumExpected, + nonce, + contractManager.sideChainId + ); + const signature = await ContractUtils.signMessage(managerShop.wallet, message); + + const uri = URI(serverURL).directory("/v1/shop").filename("refund"); + const url = uri.toString(); + const response = await client.post(url, { + shopId: managerShop.shopId, + amount: sumExpected.toString(), + account: managerShop.wallet.address, + signature, + }); + + expect(response.data.code).to.equal(0); + expect(response.data.data).to.not.equal(undefined); + expect(response.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); + }); + + it("Check refundable amount", async () => { + const url = URI(serverURL).directory("/v1/shop/refundable/").filename(managerShop.shopId).toString(); + const response = await client.get(url); + const refundableAmount = BigNumber.from(response.data.data.refundableAmount); + expect(refundableAmount).to.equal(0); + }); + + it("Check balance of ledger", async () => { + const url = URI(serverURL) + .directory("/v1/ledger/balance/account/") + .filename(managerShop.wallet.address) + .toString(); + const response = await client.get(url); + const balance = BigNumber.from(response.data.data.token.balance); + expect(balance).to.equal(amountToken); + }); + }); + }); }); diff --git a/packages/relay/tspec/02_Shop.ts b/packages/relay/tspec/02_Shop.ts index d94338d2..33d5335b 100644 --- a/packages/relay/tspec/02_Shop.ts +++ b/packages/relay/tspec/02_Shop.ts @@ -626,5 +626,320 @@ export type ShopApiSpec = Tspec.DefineApiSpec<{ }; }; }; + "/v1/shop/refund": { + post: { + summary: ""; + body: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Amount for refund (info. decimals are 18) + * @example "100000000000000000000000" + */ + amount: string; + /** + * Address of wallet + * @example "0xafFe745418Ad24c272175e5B58610A8a35e2EcDa" + */ + account: string; + /** + * Signature of shop owner + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Hash of transaction + * @example "0x3798157a3f32c0ed7692f240eb83f3a3c2f6077c5ad7acf7a9a54d426d63632e" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/manager/set": { + post: { + summary: "Set settlement manager of the shop(shopId)"; + body: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Address of wallet + * @example "0xafFe745418Ad24c272175e5B58610A8a35e2EcDa" + */ + account: string; + /** + * ID of settlement manager + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + managerId: string; + /** + * Signature of shop owner + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Hash of transaction + * @example "0x3798157a3f32c0ed7692f240eb83f3a3c2f6077c5ad7acf7a9a54d426d63632e" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/manager/remove": { + post: { + summary: "Remove settlement manager of the shop(shopId)"; + body: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Address of wallet + * @example "0xafFe745418Ad24c272175e5B58610A8a35e2EcDa" + */ + account: string; + /** + * Signature of shop owner + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Hash of transaction + * @example "0x3798157a3f32c0ed7692f240eb83f3a3c2f6077c5ad7acf7a9a54d426d63632e" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/manager/get/{shopId}": { + get: { + summary: "Provide settlement manager"; + path: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * ID of Settlement Manager + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + managerId: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/client/length/{shopId}": { + get: { + summary: "Provide number of registered shops"; + path: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Number of registered shop + * @example 10 + */ + length: number; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/client/list/{shopId}": { + get: { + summary: "Provide registered shops"; + path: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * registered shops + * @example 10 + */ + clients: string[]; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/shop/settlement/collect": { + post: { + summary: "Collect settlement amount"; + body: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Address of wallet + * @example "0xafFe745418Ad24c272175e5B58610A8a35e2EcDa" + */ + account: string; + /** + * Address of wallet + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874,0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + clients: string; + /** + * Signature of shop owner + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Hash of transaction + * @example "0x3798157a3f32c0ed7692f240eb83f3a3c2f6077c5ad7acf7a9a54d426d63632e" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; }; }>; diff --git a/packages/relay/tspec/07_Summary.ts b/packages/relay/tspec/07_Summary.ts index 0b414e42..2f4d5453 100644 --- a/packages/relay/tspec/07_Summary.ts +++ b/packages/relay/tspec/07_Summary.ts @@ -189,6 +189,208 @@ export type Summary1ApiSpec = Tspec.DefineApiSpec<{ }; }; }; + "/v2/summary/account/{account}": { + get: { + summary: "Provide general information corresponding to the user's wallet address"; + path: { + /** + * Address of wallet + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Address of wallet + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + tokenInfo: { + /** + * Symbol of Token + * @example "ACC" + */ + symbol: string; + /** + * Name of Token + * @example "ACC Coin" + */ + name: string; + /** + * Decimals of Token + * @example 18 + */ + decimals: number; + }; + exchangeRate: { + token: { + /** + * Symbol of Token (info. decimals is 18) + * @example "ACC" + */ + symbol: string; + /** + * Amount of Token (info. decimals is 18) + * @example "1000000000000000000" + */ + value: string; + }; + currency: { + /** + * Symbol of Basic Currency (info. decimals is 18) + * @example "php" + */ + symbol: string; + /** + * Basic Currency Value for one token (info. decimals is 18) + * @example "2169736506000000000" + */ + value: string; + }; + }; + provider: { + /** + * If true, this account is a point provider. + * @example false + */ + enable: boolean; + /** + * Technical representatives to provide point + * @example "0x0000000000000000000000000000000000000000" + */ + assistant: string; + }; + agent: { + /** + * Agent of provision + * @example "0x0000000000000000000000000000000000000000" + */ + provision: string; + /** + * Agent of refund + * @example "0x0000000000000000000000000000000000000000" + */ + refund: string; + /** + * Agent of withdrawal + * @example "0x0000000000000000000000000000000000000000" + */ + withdrawal: string; + }; + ledger: { + point: { + /** + * Balance of point in the Ledger (info. decimals is 18) + * @example "3895000000000000000000" + */ + balance: string; + /** + * Basic Currency Value of point in the Ledger (info. decimals is 18) + * @example "3895000000000000000000" + */ + value: string; + }; + token: { + /** + * Balance of token in the Ledger (info. decimals is 18) + * @example "1661929627132000000000" + */ + balance: string; + /** + * Basic Currency Value of token in the Ledger (info. decimals is 18) + * @example "3605949382391000000000" + */ + value: string; + }; + }; + mainChain: { + point: { + /** + * Balance of point in the Main Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of point in the Main Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + token: { + /** + * Balance of token in the Main Chain (info. decimals is 18) + * @example "99900000000000000000" + */ + balance: string; + /** + * Basic Currency Value of token in the Main Chain (info. decimals is 18) + * @example "216756676949000000000" + */ + value: string; + }; + }; + sideChain: { + point: { + /** + * Balance of point in the Side Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of point in the Side Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + token: { + /** + * Balance of token in the Side Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of token in the Side Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + }; + protocolFees: { + /** + * Protocol fees required to use the transfer function (info. decimals is 18) + * @example "100000000000000000" + */ + transfer: string; + /** + * Protocol fees required to use the withdrawal function (info. decimals is 18) + * @example "100000000000000000" + */ + withdraw: string; + /** + * Protocol fees required to use the deposit function (info. decimals is 18) + * @example "100000000000000000" + */ + deposit: string; + }; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; }; }>; @@ -420,5 +622,254 @@ export type Summary2ApiSpec = Tspec.DefineApiSpec<{ }; }; }; + "/v2/summary/shop/{shopId}": { + get: { + summary: "Provide general information corresponding to the ID of shop"; + path: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + shopInfo: { + /** + * ID of Shop + * @example "0x0001be96d74202df38fd21462ffcef10dfe0fcbd7caa3947689a3903e8b6b874" + */ + shopId: string; + /** + * Name of Shop + * @example "Coffee Nine" + */ + name: string; + /** + * Basic currency symbol of Shop + * @example "php" + */ + currency: string; + /** + * Active status of Shop ( 0: None, 1: ACTIVE, 2: INACTIVE ) + * @example 1 + */ + status: number; + /** + * Wallet address of the shop owner + * @example "0xafFe745418Ad24c272175e5B58610A8a35e2EcDa" + */ + account: string; + /** + * Wallet address that authorizes cancellation on behalf of the shop owner + * @example "0xD10ADf251463A260242c216c8c7D3e736eBdB398" + */ + delegator: string; + /** + * Amount of provided + * @example "0" + */ + providedAmount: string; + /** + * Amount of used + * @example "0" + */ + usedAmount: string; + /** + * Amount of refunded + * @example "0" + */ + refundedAmount: string; + /** + * Basic currency amount of refundable + * @example "0" + */ + refundableAmount: string; + /** + * Token amount of refundable + * @example "0" + */ + refundableToken: string; + }; + tokenInfo: { + /** + * Symbol of Token + * @example "ACC" + */ + symbol: string; + /** + * Name of Token + * @example "ACC Coin" + */ + name: string; + /** + * Decimals of Token + * @example 18 + */ + decimals: number; + }; + exchangeRate: { + token: { + /** + * Symbol of Token (info. decimals is 18) + * @example "ACC" + */ + symbol: string; + /** + * Amount of Token (info. decimals is 18) + * @example "1000000000000000000" + */ + value: string; + }; + currency: { + /** + * Symbol of Basic Currency (info. decimals is 18) + * @example "php" + */ + symbol: string; + /** + * Basic Currency Value for one token (info. decimals is 18) + * @example "2169736506000000000" + */ + value: string; + }; + }; + settlement: { + /** + * ID of manager shop for settlement + * @example "0x0000000000000000000000000000000000000000000000000000000000000000" + */ + manager: string; + }; + agent: { + /** + * Agent of provision + * @example "0x0000000000000000000000000000000000000000" + */ + provision: string; + /** + * Agent of refund + * @example "0x0000000000000000000000000000000000000000" + */ + refund: string; + /** + * Agent of withdrawal + * @example "0x0000000000000000000000000000000000000000" + */ + withdrawal: string; + }; + ledger: { + point: { + /** + * Balance of point in the Ledger (info. decimals is 18) + * @example "3895000000000000000000" + */ + balance: string; + /** + * Basic Currency Value of point in the Ledger (info. decimals is 18) + * @example "3895000000000000000000" + */ + value: string; + }; + token: { + /** + * Balance of token in the Ledger (info. decimals is 18) + * @example "1661929627132000000000" + */ + balance: string; + /** + * Basic Currency Value of token in the Ledger (info. decimals is 18) + * @example "3605949382391000000000" + */ + value: string; + }; + }; + mainChain: { + point: { + /** + * Balance of point in the Main Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of point in the Main Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + token: { + /** + * Balance of token in the Main Chain (info. decimals is 18) + * @example "99900000000000000000" + */ + balance: string; + /** + * Basic Currency Value of token in the Main Chain (info. decimals is 18) + * @example "216756676949000000000" + */ + value: string; + }; + }; + sideChain: { + point: { + /** + * Balance of point in the Side Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of point in the Side Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + token: { + /** + * Balance of token in the Side Chain (info. decimals is 18) + * @example "0" + */ + balance: string; + /** + * Basic Currency Value of token in the Side Chain (info. decimals is 18) + * @example "0" + */ + value: string; + }; + }; + protocolFees: { + /** + * Protocol fees required to use the transfer function (info. decimals is 18) + * @example "100000000000000000" + */ + transfer: string; + /** + * Protocol fees required to use the withdrawal function (info. decimals is 18) + * @example "100000000000000000" + */ + withdraw: string; + /** + * Protocol fees required to use the deposit function (info. decimals is 18) + * @example "100000000000000000" + */ + deposit: string; + }; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; }; }>; diff --git a/packages/relay/tspec/10_Provider.ts b/packages/relay/tspec/10_Provider.ts index 5d436b73..5c650b78 100644 --- a/packages/relay/tspec/10_Provider.ts +++ b/packages/relay/tspec/10_Provider.ts @@ -32,11 +32,6 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" */ assistant: string; - /** - * Hash of transaction - * @example "0xe5502185d39057bd82e6dde675821b87313570df77d3e23d8e5712bd5f3fa6b6" - */ - txHash: string; }; error?: { /** @@ -87,6 +82,11 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" */ assistant: string; + /** + * Hash of transaction + * @example "0xe5502185d39057bd82e6dde675821b87313570df77d3e23d8e5712bd5f3fa6b6" + */ + txHash: string; }; error?: { /** diff --git a/packages/relay/tspec/11_Agent.ts b/packages/relay/tspec/11_Agent.ts new file mode 100644 index 00000000..986ea6a5 --- /dev/null +++ b/packages/relay/tspec/11_Agent.ts @@ -0,0 +1,293 @@ +import { Tspec } from "tspec"; +import { ResultCode } from "./types"; + +export type AgentApiSpec = Tspec.DefineApiSpec<{ + tags: ["Agent"]; + paths: { + "/v1/agent/provision/{account}": { + get: { + summary: "Provide information on the provider's agent"; + path: { + /** + * Wallet address of the provider + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the provider + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/agent/provision/": { + post: { + summary: "Register information on the provider's agent"; + body: { + /** + * Wallet address of the provider + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Signature of provider or agent + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the provider + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Hash of transaction + * @example "0xe5502185d39057bd82e6dde675821b87313570df77d3e23d8e5712bd5f3fa6b6" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/agent/refund/{account}": { + get: { + summary: "Provide information on the refund's agent"; + path: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/agent/refund/": { + post: { + summary: "Register information on the refund's agent"; + body: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Signature of account's owner or agent + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Hash of transaction + * @example "0xe5502185d39057bd82e6dde675821b87313570df77d3e23d8e5712bd5f3fa6b6" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/agent/withdrawal/{account}": { + get: { + summary: "Provide information on the withdrawal's agent"; + path: { + /** + * Wallet address of the shop's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + "/v1/agent/withdrawal/": { + post: { + summary: "Register information on the withdrawal's agent"; + body: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Signature of account's owner or agent + * @example "0x020d671b80fbd20466d8cb65cef79a24e3bca3fdf82e9dd89d78e7a4c4c045bd72944c20bb1d839e76ee6bb69fed61f64376c37799598b40b8c49148f3cdd88a1b" + */ + signature: string; + }; + responses: { + 200: { + /** + * Result Code + * @example 0 + */ + code: ResultCode; + data: { + /** + * Wallet address of the account's owner + * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" + */ + account: string; + /** + * Wallet address of the agent + * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" + */ + agent: string; + /** + * Hash of transaction + * @example "0xe5502185d39057bd82e6dde675821b87313570df77d3e23d8e5712bd5f3fa6b6" + */ + txHash: string; + }; + error?: { + /** + * Error Message + * @example "Failed to check the validity of parameters" + */ + message: string; + }; + }; + }; + }; + }; + }; +}>; diff --git a/yarn.lock b/yarn.lock index e40232ee..90302d61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -349,7 +349,7 @@ dependencies: "@ethersproject/bignumber" "^5.7.0" -"@ethersproject/contracts@5.7.0": +"@ethersproject/contracts@5.7.0", "@ethersproject/contracts@^5.7.0": version "5.7.0" resolved "https://registry.npmjs.org/@ethersproject/contracts/-/contracts-5.7.0.tgz" integrity sha512-5GJbzEU3X+d33CdfPhcyS+z8MzsTrBGk/sc+G+59+tPa9yFkl6HQ9D6L0QMgNTA9q8dT0XKxxkyp883XsQvbbg== @@ -1932,18 +1932,6 @@ acc-bridge-contracts-v2@~2.5.0: "@openzeppelin/hardhat-upgrades" "^1.28.0" loyalty-tokens "~2.1.1" -acc-contracts-v2@~2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/acc-contracts-v2/-/acc-contracts-v2-2.6.0.tgz#eb71aaae439ef3096314f83de9c244f69366d310" - integrity sha512-Cnyh3XGRcN8m+PTD/3O/nr28IWlPu9tszkX9lAytmQO1w/yi/NEt5C0Bu0vcuLo5DdR6K4q7FGSgC3RIq5vqAA== - dependencies: - "@openzeppelin/contracts" "^4.9.5" - "@openzeppelin/contracts-upgradeable" "^4.9.5" - "@openzeppelin/hardhat-upgrades" "^1.28.0" - acc-bridge-contracts-v2 "~2.5.0" - loyalty-tokens "~2.1.1" - multisig-wallet-contracts "~2.0.0" - accepts@~1.3.8: version "1.3.8" resolved "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz"