diff --git a/packages/contracts/contracts/controllers/LoyaltyBridge.sol b/packages/contracts/contracts/controllers/LoyaltyBridge.sol index 0d52a53..fd9ec6c 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 581db06..0cfd8be 100644 --- a/packages/contracts/contracts/controllers/LoyaltyProvider.sol +++ b/packages/contracts/contracts/controllers/LoyaltyProvider.sol @@ -190,7 +190,7 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad ) ); address recover = ECDSA.recover(ECDSA.toEthSignedMessageHash(purchaseDataHash), data.signature); - address agent = ledgerContract.provisioningAgentOf(data.sender); + address agent = ledgerContract.provisionAgentOf(data.sender); if ((agent == address(0x0)) && (recover != data.sender)) continue; if ((agent != address(0x0)) && (recover != agent)) continue; @@ -285,7 +285,7 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad if (recurve1 == _provider) { sender = _provider; } else { - address agent = ledgerContract.provisioningAgentOf(_provider); + address agent = ledgerContract.provisionAgentOf(_provider); require(agent != address(0x0), "1501"); address recurve2 = ECDSA.recover( @@ -331,7 +331,7 @@ contract LoyaltyProvider is LoyaltyProviderStorage, Initializable, OwnableUpgrad if (recurve1 == _provider) { sender = _provider; } else { - address agent = ledgerContract.provisioningAgentOf(_provider); + address agent = ledgerContract.provisionAgentOf(_provider); require(agent != address(0x0), "1501"); address recurve2 = ECDSA.recover( diff --git a/packages/contracts/contracts/interfaces/ILedger.sol b/packages/contracts/contracts/interfaces/ILedger.sol index d2f5fe2..3b59304 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 provisioningAgentOf(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 7164814..00c77f5 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 RegisteredProvisioningAgent(address account, address agent); + event RegisteredProvisionAgent(address account, address agent); + event RegisteredRefundAgent(address account, address agent); + event RegisteredWithdrawalAgent(address account, address agent); struct ManagementAddresses { address system; @@ -513,17 +515,42 @@ contract Ledger is LedgerStorage, Initializable, OwnableUpgradeable, UUPSUpgrade return providers[_account]; } - function registerProvisioningAgent(address _account, address _agent, bytes calldata _signature) external { - require(providers[_account], "1054"); + 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"); - provisioningAgents[_account] = _agent; + provisionAgents[_account] = _agent; nonce[_account]++; - emit RegisteredProvisioningAgent(_account, _agent); + emit RegisteredProvisionAgent(_account, _agent); } - function provisioningAgentOf(address _account) external view override returns (address) { - return provisioningAgents[_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 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 1a586cf..8dda710 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 provisioningAgents; + 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 81b30cc..106b309 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 7505a44..d4421f0 100644 --- a/packages/contracts/deploy/side_chain_devnet/deploy.ts +++ b/packages/contracts/deploy/side_chain_devnet/deploy.ts @@ -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]) - .registerProvisioningAgent(accounts.system.address, accounts.publisher.address, signature); + .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.provisioningAgentOf(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 78fdff7..bf76779 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 2e9857c..1644943 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)); } diff --git a/packages/contracts/test/04-Ledger.test.ts b/packages/contracts/test/04-Ledger.test.ts index a89f82f..365e1a9 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 cca520e..c87b141 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 bcb3947..ceeb1a7 100644 --- a/packages/contracts/test/08-Ledger-Provider.test.ts +++ b/packages/contracts/test/08-Ledger-Provider.test.ts @@ -356,7 +356,7 @@ describe("Test for Ledger", () => { }); it("Register Assistance", async () => { - expect(await ledgerContract.provisioningAgentOf(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.getRegisterAgentMessage( deployments.accounts.users[0].address, @@ -368,19 +368,19 @@ describe("Test for Ledger", () => { await expect( ledgerContract .connect(deployments.accounts.deployer) - .registerProvisioningAgent( + .registerProvisionAgent( deployments.accounts.users[0].address, deployments.accounts.users[2].address, signature ) ) - .emit(ledgerContract, "RegisteredProvisioningAgent") + .emit(ledgerContract, "RegisteredProvisionAgent") .withNamedArgs({ account: deployments.accounts.users[0].address, agent: deployments.accounts.users[2].address, }); - expect(await ledgerContract.provisioningAgentOf(deployments.accounts.users[0].address)).equal( + expect(await ledgerContract.provisionAgentOf(deployments.accounts.users[0].address)).equal( deployments.accounts.users[2].address ); }); diff --git a/packages/library/package.json b/packages/library/package.json index ba13998..dfc387e 100644 --- a/packages/library/package.json +++ b/packages/library/package.json @@ -1,6 +1,6 @@ { "name": "acc-contracts-lib-v2", - "version": "2.9.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 414546c..2bc29d0 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", @@ -61,6 +62,7 @@ "@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", @@ -71,7 +73,7 @@ "@typechain/ethers-v5": "^10.1.0", "@typechain/hardhat": "^6.1.2", "acc-bridge-contracts-v2": "~2.5.0", - "acc-contracts-v2": "~2.9.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 da7f4bb..2fb435b 100644 --- a/packages/relay/scripts/provider/send.ts +++ b/packages/relay/scripts/provider/send.ts @@ -35,7 +35,7 @@ async function main() { const provider = new Wallet("0x70438bc3ed02b5e4b76d496625cb7c06d6b7bf4362295b16fdfe91a046d4586c"); // 0x64D111eA9763c93a003cef491941A011B8df5a49 const receiver = new Wallet("0x595f911dcf0845cb1f2d0e5cec9f1ccfd62fa199ebeae215a72aa56014edbb32"); // 0xB6f69F0e9e70034ba0578C542476cC13eF739269 - const agent = await sideLedgerContract.provisioningAgentOf(provider.address); + const agent = await sideLedgerContract.provisionAgentOf(provider.address); console.log(`agent: ${agent}`); const balance1 = await sideLedgerContract.pointBalanceOf(receiver.address); diff --git a/packages/relay/scripts/provider/send_to_phone.ts b/packages/relay/scripts/provider/send_to_phone.ts index cc0b7a9..4670357 100644 --- a/packages/relay/scripts/provider/send_to_phone.ts +++ b/packages/relay/scripts/provider/send_to_phone.ts @@ -35,7 +35,7 @@ async function main() { const provider = new Wallet("0x70438bc3ed02b5e4b76d496625cb7c06d6b7bf4362295b16fdfe91a046d4586c"); // 0x64D111eA9763c93a003cef491941A011B8df5a49 const receiver = ContractUtils.getPhoneHash("+82 10-9000-2000"); - const agent = await sideLedgerContract.provisioningAgentOf(provider.address); + const agent = await sideLedgerContract.provisionAgentOf(provider.address); console.log(`agent: ${agent}`); const balance1 = await sideLedgerContract.unPayablePointBalanceOf(receiver); diff --git a/packages/relay/src/DefaultServer.ts b/packages/relay/src/DefaultServer.ts index 0eaee81..d4577a1 100644 --- a/packages/relay/src/DefaultServer.ts +++ b/packages/relay/src/DefaultServer.ts @@ -14,10 +14,11 @@ 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"; -import { ProviderRouter } from "./routers/ProviderRouter"; +import { ProvisionRouter } from "./routers/ProvisionRouter"; import { StorePurchaseRouter } from "./routers/StorePurchaseRouter"; import { TaskRouter } from "./routers/TaskRouter"; import { TokenRouter } from "./routers/TokenRouter"; @@ -42,8 +43,9 @@ 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 provisionRouter: ProvisionRouter; public readonly bridgeRouter: BridgeRouter; public readonly historyRouter: HistoryRouter; @@ -159,7 +161,17 @@ export class DefaultServer extends WebService { this.graph_mainchain, this.relaySigners ); - this.providerRouter = new ProviderRouter( + this.provisionRouter = new ProvisionRouter( + this, + this.config, + this.contractManager, + this.metrics, + this.storage, + this.graph_sidechain, + this.graph_mainchain, + this.relaySigners + ); + this.agentRouter = new AgentRouter( this, this.config, this.contractManager, @@ -232,7 +244,8 @@ export class DefaultServer extends WebService { await this.purchaseRouter.registerRoutes(); await this.tokenRouter.registerRoutes(); await this.phoneLinkRouter.registerRoutes(); - await this.providerRouter.registerRoutes(); + await this.provisionRouter.registerRoutes(); + await this.agentRouter.registerRoutes(); await this.bridgeRouter.registerRoutes(); await this.historyRouter.registerRoutes(); await this.taskRouter.registerRoutes(); diff --git a/packages/relay/src/routers/AgentRouter.ts b/packages/relay/src/routers/AgentRouter.ts new file mode 100644 index 0000000..1f1d9ab --- /dev/null +++ b/packages/relay/src/routers/AgentRouter.ts @@ -0,0 +1,321 @@ +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 { Validation } from "../validation"; + +import { AddressZero } from "@ethersproject/constants"; +import { BigNumber, ethers } from "ethers"; +import express from "express"; +import { body, param, validationResult } from "express-validator"; +import { BOACoin } from "../common/Amount"; + +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 dda46fb..bd44a5c 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 ce222d5..5571332 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 f9679f2..dd98565 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 934f19d..896be05 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/ProvisionRouter.ts similarity index 68% rename from packages/relay/src/routers/ProviderRouter.ts rename to packages/relay/src/routers/ProvisionRouter.ts index 82a5ea8..c73b25f 100644 --- a/packages/relay/src/routers/ProviderRouter.ts +++ b/packages/relay/src/routers/ProvisionRouter.ts @@ -10,14 +10,13 @@ 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"; import { body, param, validationResult } from "express-validator"; import { BOACoin } from "../common/Amount"; -export class ProviderRouter { +export class ProvisionRouter { private web_service: WebService; private readonly config: Config; private readonly contractManager: ContractManager; @@ -86,7 +85,7 @@ export class ProviderRouter { public async registerRoutes() { this.app.post( - "/v1/provider/register", + "/v1/provision/register", [ body("provider").exists().trim().isEthereumAddress(), body("signature") @@ -94,23 +93,23 @@ export class ProviderRouter { .trim() .matches(/^(0x)[0-9a-f]{130}$/i), ], - this.provider_register.bind(this) + this.provision_register.bind(this) ); this.app.get( - "/v1/provider/balance/:provider", + "/v1/provision/balance/:provider", [param("provider").exists().trim().isEthereumAddress()], - this.provider_balance.bind(this) + this.provision_balance.bind(this) ); this.app.get( - "/v1/provider/status/:provider", + "/v1/provision/status/:provider", [param("provider").exists().trim().isEthereumAddress()], - this.provider_status.bind(this) + this.provision_status.bind(this) ); this.app.post( - "/v1/provider/send/account", + "/v1/provision/send/account", [ body("provider").exists().trim().isEthereumAddress(), body("receiver").exists().trim().isEthereumAddress(), @@ -120,11 +119,11 @@ export class ProviderRouter { .trim() .matches(/^(0x)[0-9a-f]{130}$/i), ], - this.provider_send_account.bind(this) + this.provision_send_account.bind(this) ); this.app.post( - "/v1/provider/send/phoneHash", + "/v1/provision/send/phoneHash", [ body("provider").exists().trim().isEthereumAddress(), body("receiver") @@ -137,31 +136,12 @@ export class ProviderRouter { .trim() .matches(/^(0x)[0-9a-f]{130}$/i), ], - this.provider_send_phone_hash.bind(this) - ); - - this.app.post( - "/v1/provider/agent/register", - [ - body("provider").exists().trim().isEthereumAddress(), - body("agent").exists().trim().isEthereumAddress(), - body("signature") - .exists() - .trim() - .matches(/^(0x)[0-9a-f]{130}$/i), - ], - this.provider_agent_register.bind(this) - ); - - this.app.get( - "/v1/provider/agent/:provider", - [param("provider").exists().trim().isEthereumAddress()], - this.provider_agent.bind(this) + this.provision_send_phone_hash.bind(this) ); } - private async provider_register(req: express.Request, res: express.Response) { - logger.http(`POST /v1/provider/register ${req.ip}:${JSON.stringify(req.body)}`); + private async provision_register(req: express.Request, res: express.Response) { + logger.http(`POST /v1/provision/register ${req.ip}:${JSON.stringify(req.body)}`); const errors = validationResult(req); if (!errors.isEmpty()) { @@ -190,7 +170,7 @@ export class ProviderRouter { return res.status(200).json(this.makeResponseData(0, { provider })); } catch (error: any) { const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`POST /v1/provider/register : ${msg.error.message}`); + logger.error(`POST /v1/provision/register : ${msg.error.message}`); this.metrics.add("failure", 1); return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } finally { @@ -198,8 +178,8 @@ export class ProviderRouter { } } - private async provider_balance(req: express.Request, res: express.Response) { - logger.http(`GET /v1/provider/balance/:provider ${req.ip}:${JSON.stringify(req.params)}`); + private async provision_balance(req: express.Request, res: express.Response) { + logger.http(`GET /v1/provision/balance/:provider ${req.ip}:${JSON.stringify(req.params)}`); const errors = validationResult(req); if (!errors.isEmpty()) { @@ -222,14 +202,14 @@ export class ProviderRouter { ); } catch (error: any) { const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`GET /v1/provider/balance/:provider : ${msg.error.message}`); + logger.error(`GET /v1/provision/balance/:provider : ${msg.error.message}`); this.metrics.add("failure", 1); return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } } - private async provider_status(req: express.Request, res: express.Response) { - logger.http(`GET /v1/provider/status/:provider ${req.ip}:${JSON.stringify(req.params)}`); + private async provision_status(req: express.Request, res: express.Response) { + logger.http(`GET /v1/provision/status/:provider ${req.ip}:${JSON.stringify(req.params)}`); const errors = validationResult(req); if (!errors.isEmpty()) { @@ -248,14 +228,14 @@ export class ProviderRouter { ); } catch (error: any) { const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`GET /v1/provider/status/:provider : ${msg.error.message}`); + logger.error(`GET /v1/provision/status/:provider : ${msg.error.message}`); this.metrics.add("failure", 1); return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } } - private async provider_send_account(req: express.Request, res: express.Response) { - logger.http(`POST /v1/provider/send/account ${req.ip}:${JSON.stringify(req.body)}`); + private async provision_send_account(req: express.Request, res: express.Response) { + logger.http(`POST /v1/provision/send/account ${req.ip}:${JSON.stringify(req.body)}`); const errors = validationResult(req); if (!errors.isEmpty()) { @@ -269,7 +249,7 @@ export class ProviderRouter { const amount: BigNumber = BigNumber.from(req.body.amount); const signature: string = String(req.body.signature).trim(); - let agent = await this.contractManager.sideLedgerContract.provisioningAgentOf(provider); + let agent = await this.contractManager.sideLedgerContract.provisionAgentOf(provider); if (agent === AddressZero) agent = provider; const nonce = await this.contractManager.sideLedgerContract.nonceOf(agent); @@ -289,7 +269,7 @@ export class ProviderRouter { return res.status(200).json(this.makeResponseData(0, { provider, receiver, amount, txHash: tx.hash })); } catch (error: any) { const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`POST /v1/provider/send/account : ${msg.error.message}`); + logger.error(`POST /v1/provision/send/account : ${msg.error.message}`); this.metrics.add("failure", 1); return res.status(200).json(this.makeResponseData(msg.code, undefined, msg.error)); } finally { @@ -297,8 +277,8 @@ export class ProviderRouter { } } - private async provider_send_phone_hash(req: express.Request, res: express.Response) { - logger.http(`POST /v1/provider/send/phoneHash ${req.ip}:${JSON.stringify(req.body)}`); + private async provision_send_phone_hash(req: express.Request, res: express.Response) { + logger.http(`POST /v1/provision/send/phoneHash ${req.ip}:${JSON.stringify(req.body)}`); const errors = validationResult(req); if (!errors.isEmpty()) { @@ -312,7 +292,7 @@ export class ProviderRouter { const amount: BigNumber = BigNumber.from(req.body.amount); const signature: string = String(req.body.signature).trim(); - let agent = await this.contractManager.sideLedgerContract.provisioningAgentOf(provider); + let agent = await this.contractManager.sideLedgerContract.provisionAgentOf(provider); if (agent === AddressZero) agent = provider; const nonce = await this.contractManager.sideLedgerContract.nonceOf(agent); @@ -332,70 +312,11 @@ export class ProviderRouter { return res.status(200).json(this.makeResponseData(0, { provider, receiver, amount, txHash: tx.hash })); } catch (error: any) { const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`POST /v1/provider/send/phoneHash : ${msg.error.message}`); + logger.error(`POST /v1/provision/send/phoneHash : ${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 provider_agent_register(req: express.Request, res: express.Response) { - logger.http(`POST /v1/provider/agent/register ${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 provider: string = String(req.body.provider).trim(); - const agent: string = String(req.body.agent).trim(); - const signature: string = String(req.body.signature).trim(); - - const nonce = await this.contractManager.sideLedgerContract.nonceOf(provider); - const message = ContractUtils.getRegisterAgentMessage( - provider, - agent, - nonce, - this.contractManager.sideChainId - ); - if (!ContractUtils.verifyMessage(provider, message, signature)) - return res.status(200).json(ResponseMessage.getErrorMessage("1501")); - const tx = await this.contractManager.sideLedgerContract - .connect(signerItem.signer) - .registerProvisioningAgent(provider, agent, signature); - this.metrics.add("success", 1); - return res.status(200).json(this.makeResponseData(0, { provider, agent, txHash: tx.hash })); - } catch (error: any) { - const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`POST /v1/provider/agent/register : ${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 provider_agent(req: express.Request, res: express.Response) { - logger.http(`GET /v1/provider/agent/:provider ${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 provider: string = String(req.params.provider).trim(); - const agent = await this.contractManager.sideLedgerContract.provisioningAgentOf(provider); - this.metrics.add("success", 1); - return res.status(200).json(this.makeResponseData(0, { provider, agent })); - } catch (error: any) { - const msg = ResponseMessage.getEVMErrorMessage(error); - logger.error(`GET /v1/provider/agent/: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/ShopRouter.ts b/packages/relay/src/routers/ShopRouter.ts index 78a9df2..946a3e4 100644 --- a/packages/relay/src/routers/ShopRouter.ts +++ b/packages/relay/src/routers/ShopRouter.ts @@ -16,7 +16,6 @@ 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"; @@ -1222,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); diff --git a/packages/relay/src/routers/StorePurchaseRouter.ts b/packages/relay/src/routers/StorePurchaseRouter.ts index 7275619..b5fb1c6 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 c0e5093..fe06731 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 4fdf3a4..18ef6b9 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"; @@ -510,7 +510,8 @@ export class TokenRouter { } const isProvider = await this.contractManager.sideLedgerContract.isProvider(account); - const agent = await this.contractManager.sideLedgerContract.provisioningAgentOf(account); + const provisionAgent = await this.contractManager.sideLedgerContract.provisionAgentOf(account); + const withdrawalAgent = await this.contractManager.sideLedgerContract.withdrawalAgentOf(account); const symbol = await this.contractManager.sideTokenContract.symbol(); const name = await this.contractManager.sideTokenContract.name(); @@ -557,7 +558,10 @@ export class TokenRouter { }, provider: { enable: isProvider, - agent, + }, + agent: { + provision: provisionAgent, + withdrawal: withdrawalAgent, }, ledger: { point: { balance: pointBalance.toString(), value: pointValue.toString() }, @@ -646,6 +650,11 @@ export class TokenRouter { ); const defaultCurrencySymbol = await this.contractManager.sideCurrencyRateContract.defaultSymbol(); + 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, { @@ -665,6 +674,13 @@ export class TokenRouter { value: pointAmount.toString(), }, }, + settlement: { + manager: settlementManager, + }, + agent: { + refund: refundAgent, + withdrawal: withdrawalAgent, + }, ledger: { point: { balance: pointBalance.toString(), value: pointValue.toString() }, token: { balance: tokenBalanceInLedger.toString(), value: tokenValueInLedger.toString() }, diff --git a/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts b/packages/relay/src/scheduler/DelegatorApprovalScheduler.ts index ee5479d..4bf25d0 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 14affa2..f69167f 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 547ed16..0f5e9e7 100644 --- a/packages/relay/src/utils/ContractUtils.ts +++ b/packages/relay/src/utils/ContractUtils.ts @@ -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)); } diff --git a/packages/relay/test/Agent.test.ts b/packages/relay/test/Agent.test.ts new file mode 100644 index 0000000..9cbc9e0 --- /dev/null +++ b/packages/relay/test/Agent.test.ts @@ -0,0 +1,174 @@ +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 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 430f605..42cf535 100644 --- a/packages/relay/test/LoyaltyProvider.test.ts +++ b/packages/relay/test/LoyaltyProvider.test.ts @@ -115,7 +115,8 @@ describe("Test of LoyaltyProvider", function () { ); expect(response.data.data.provider.enable).to.deep.equal(false); - expect(response.data.data.provider.agent).to.deep.equal(AddressZero); + expect(response.data.data.agent.provision).to.deep.equal(AddressZero); + expect(response.data.data.agent.withdrawal).to.deep.equal(AddressZero); }); it("Register Provide", async () => { @@ -136,14 +137,15 @@ describe("Test of LoyaltyProvider", function () { ); expect(response.data.data.provider.enable).to.deep.equal(true); - expect(response.data.data.provider.agent).to.deep.equal(AddressZero); + 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() + URI(serverURL).directory(`/v1/provision/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); @@ -166,7 +168,7 @@ describe("Test of LoyaltyProvider", function () { contractManager.sideChainId ); const signature = await ContractUtils.signMessage(provider, message); - const response = await client.post(URI(serverURL).directory("/v1/provider/send/account").toString(), { + const response = await client.post(URI(serverURL).directory("/v1/provision/send/account").toString(), { provider: provider.address, receiver: receiver.address, amount: pointAmount.toString(), @@ -202,7 +204,7 @@ describe("Test of LoyaltyProvider", function () { contractManager.sideChainId ); const signature = await ContractUtils.signMessage(provider, message); - const response = await client.post(URI(serverURL).directory("/v1/provider/send/phoneHash").toString(), { + const response = await client.post(URI(serverURL).directory("/v1/provision/send/phoneHash").toString(), { provider: provider.address, receiver: phoneHash, amount: pointAmount.toString(), @@ -224,9 +226,9 @@ describe("Test of LoyaltyProvider", function () { it("Register agent", async () => { const provider = deployments.accounts.users[0]; const response1 = await client.get( - URI(serverURL).directory(`/v1/provider/agent/${provider.address}`).toString() + URI(serverURL).directory(`/v1/agent/provision/${provider.address}`).toString() ); - expect(response1.data.data.provider).to.deep.equal(provider.address); + 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]; @@ -238,8 +240,8 @@ describe("Test of LoyaltyProvider", function () { contractManager.sideChainId ); const signature = await ContractUtils.signMessage(provider, message); - const response2 = await client.post(URI(serverURL).directory(`/v1/provider/agent/register`).toString(), { - provider: provider.address, + const response2 = await client.post(URI(serverURL).directory(`/v1/agent/provision`).toString(), { + account: provider.address, agent: agent.address, signature, }); @@ -248,9 +250,9 @@ describe("Test of LoyaltyProvider", function () { expect(response2.data.data.txHash).to.match(/^0x[A-Fa-f0-9]{64}$/i); const response3 = await client.get( - URI(serverURL).directory(`/v1/provider/agent/${provider.address}`).toString() + URI(serverURL).directory(`/v1/agent/provision/${provider.address}`).toString() ); - expect(response3.data.data.provider).to.deep.equal(provider.address); + expect(response3.data.data.account).to.deep.equal(provider.address); expect(response3.data.data.agent).to.deep.equal(agent.address); }); @@ -260,10 +262,11 @@ describe("Test of LoyaltyProvider", function () { ); expect(response.data.data.provider.enable).to.deep.equal(true); - expect(response.data.data.provider.agent).to.deep.equal(deployments.accounts.users[1].address); + 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 Assistant", async () => { + 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]; @@ -280,7 +283,7 @@ describe("Test of LoyaltyProvider", function () { contractManager.sideChainId ); const signature = await ContractUtils.signMessage(agent, message); - const response = await client.post(URI(serverURL).directory("/v1/provider/send/account").toString(), { + const response = await client.post(URI(serverURL).directory("/v1/provision/send/account").toString(), { provider: provider.address, receiver: receiver.address, amount: pointAmount.toString(), @@ -299,7 +302,7 @@ describe("Test of LoyaltyProvider", function () { ); }); - it("Provide Point for Phone number by Assistant", async () => { + 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]; @@ -317,7 +320,7 @@ describe("Test of LoyaltyProvider", function () { contractManager.sideChainId ); const signature = await ContractUtils.signMessage(agent, message); - const response = await client.post(URI(serverURL).directory("/v1/provider/send/phoneHash").toString(), { + const response = await client.post(URI(serverURL).directory("/v1/provision/send/phoneHash").toString(), { provider: provider.address, receiver: phoneHash, amount: pointAmount.toString(), diff --git a/packages/relay/test/ShopWithdraw.test.ts b/packages/relay/test/ShopWithdraw.test.ts index 9197d40..8c69cc1 100644 --- a/packages/relay/test/ShopWithdraw.test.ts +++ b/packages/relay/test/ShopWithdraw.test.ts @@ -61,9 +61,6 @@ describe("Test for Shop", () => { let providerContract: LoyaltyProvider; let ledgerContract: Ledger; - const multiple = BigNumber.from(1000000000); - const price = BigNumber.from(150).mul(multiple); - let client: TestClient; let server: TestServer; let storage: RelayStorage; @@ -529,7 +526,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 @@ -1151,7 +1147,6 @@ describe("Test for Shop", () => { const nonce = await shopContract.nonceOf(managerShop.wallet.address); const message = ContractUtils.getShopRefundMessage( managerShop.shopId, - managerShop.wallet.address, sumExpected, nonce, contractManager.sideChainId diff --git a/packages/relay/tspec/10_Provider.ts b/packages/relay/tspec/10_Provision.ts similarity index 72% rename from packages/relay/tspec/10_Provider.ts rename to packages/relay/tspec/10_Provision.ts index 7a11cb8..ccd8aa4 100644 --- a/packages/relay/tspec/10_Provider.ts +++ b/packages/relay/tspec/10_Provision.ts @@ -1,105 +1,10 @@ import { Tspec } from "tspec"; import { ResultCode } from "./types"; -export type ProviderApiSpec = Tspec.DefineApiSpec<{ +export type ProvisionApiSpec = Tspec.DefineApiSpec<{ tags: ["Loyalty Point Provider"]; paths: { - "/v1/provider/agent/{provider}": { - get: { - summary: "Provides the agent's information"; - path: { - /** - * Wallet address of the provider - * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" - */ - provider: string; - }; - responses: { - 200: { - /** - * Result Code - * @example 0 - */ - code: ResultCode; - data: { - /** - * Wallet address of the provider - * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" - */ - provider: string; - /** - * Wallet address of the agent - * @example "0x3FE8D00143bd0eAd2397D48ba0E31E5E1268dBfb" - */ - agent: string; - }; - error?: { - /** - * Error Message - * @example "Failed to check the validity of parameters" - */ - message: string; - }; - }; - }; - }; - }; - "/v1/provider/agent/register": { - post: { - summary: "Register the provider's agent. The agent can only process the instructions of the point transfer"; - body: { - /** - * Wallet address of the provider - * @example "0x5650CD3E6E8963B43D21FAE60EE7A03BCEFCE766" - */ - provider: 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" - */ - provider: 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/provider/balance/{provider}": { + "/v1/provision/balance/{provider}": { get: { summary: "Provide a point provider's assets"; path: { @@ -146,7 +51,7 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ }; }; }; - "/v1/provider/register": { + "/v1/provision/register": { post: { summary: "Register the provider. To become a point provider, you must first deposit 50,000 tokens"; body: { @@ -186,7 +91,7 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ }; }; }; - "/v1/provider/send/account": { + "/v1/provision/send/account": { post: { summary: "Request the ability to provide points to the recipient's wallet address"; body: { @@ -251,7 +156,7 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ }; }; }; - "/v1/provider/send/phoneHash": { + "/v1/provision/send/phoneHash": { post: { summary: "Request the ability to provide points to the recipient's phone number hash"; body: { @@ -316,7 +221,7 @@ export type ProviderApiSpec = Tspec.DefineApiSpec<{ }; }; }; - "/v1/provider/status/{provider}": { + "/v1/provision/status/{provider}": { get: { summary: "Provides the status value of the point provider"; path: { diff --git a/packages/relay/tspec/11_Agent.ts b/packages/relay/tspec/11_Agent.ts new file mode 100644 index 0000000..986ea6a --- /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 f1c7a51..90302d6 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==