diff --git a/.github/workflows/pr-test.yml b/.github/workflows/pr-test.yml index 7f03e50f..341fd0e6 100644 --- a/.github/workflows/pr-test.yml +++ b/.github/workflows/pr-test.yml @@ -1,16 +1,16 @@ -name: pr-test - -on: - pull_request: - branches: - - '*' - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build optimized jar - run: ./gradlew clean build optimizedJar \ No newline at end of file +#name: pr-test +# +#on: +# pull_request: +# branches: +# - '*' +# +#jobs: +# docker: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v2 +# +# - name: Build optimized jar +# run: ./gradlew clean build optimizedJar \ No newline at end of file diff --git a/.github/workflows/push-main.yml b/.github/workflows/push-main.yml index 1dc36eff..1a5bbab4 100644 --- a/.github/workflows/push-main.yml +++ b/.github/workflows/push-main.yml @@ -1,16 +1,16 @@ -name: push-main - -on: - push: - branches: - - main - -jobs: - docker: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@v2 - - - name: Build optimized jar - run: ./gradlew clean build optimizedJar \ No newline at end of file +#name: push-main +# +#on: +# push: +# branches: +# - main +# +#jobs: +# docker: +# runs-on: ubuntu-latest +# steps: +# - name: Checkout +# uses: actions/checkout@v2 +# +# - name: Build optimized jar +# run: ./gradlew clean build optimizedJar \ No newline at end of file diff --git a/.gitignore b/.gitignore index 1dff633f..6151cd4b 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,38 @@ -keystore.json -keystore -gradle.properties -.gradle +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* .idea -keystore.zip -lib -CPSTreasury.iml -CPFTreasury/build/* -CPSTreasury/build/* + +.gradle +**/build/ +!gradle-wrapper.jar + +gradle.properties +*.json + +.deployment + +.project +.classpath +.settings +**/bin/ diff --git a/CPFTreasury/build.gradle b/CPFTreasury/build.gradle index f0d0d9f6..c3acc381 100644 --- a/CPFTreasury/build.gradle +++ b/CPFTreasury/build.gradle @@ -2,6 +2,8 @@ version = '0.9.1' dependencies { compileOnly 'foundation.icon:javaee-api:0.9.1' + implementation project(':score-lib') + implementation 'com.github.sink772:javaee-tokens:0.6.1' implementation 'com.github.sink772:minimal-json:0.9.6' @@ -26,6 +28,7 @@ deployJar { lisbon { uri = 'https://lisbon.net.solidwallet.io/api/v3' nid = 0x2 + to = 'cx3d4182c0e783b7ef97ba63409c4bdf808853bd9f' } local { uri = 'http://localhost:9082/api/v3' @@ -45,6 +48,7 @@ deployJar { keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' parameters { + arg('score', 'cx0355e153f269c05f64100d34d3c48a002b404dc5') } } diff --git a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/CPFTreasury.java b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/CPFTreasury.java index dd2b8692..9d7d8f63 100644 --- a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/CPFTreasury.java +++ b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/CPFTreasury.java @@ -16,38 +16,41 @@ import static community.icon.cps.score.cpftreasury.Constants.*; import static community.icon.cps.score.cpftreasury.Validations.validateAdmins; import static community.icon.cps.score.cpftreasury.Validations.validateCpsScore; +import community.icon.cps.score.lib.interfaces.CPFTreasuryInterface; -public class CPFTreasury extends SetterGetter { - - private final ArrayDB proposalsKeys = Context.newArrayDB(PROPOSALS_KEYS, String.class); - private final DictDB proposalBudgets = Context.newDictDB(PROPOSAL_BUDGETS, BigInteger.class); - private final VarDB treasuryFund = Context.newVarDB(TREASURY_FUND, BigInteger.class); - private final VarDB treasuryFundbnUSD = Context.newVarDB(TREASURY_FUND_BNUSD, BigInteger.class); - +public class CPFTreasury extends SetterGetter implements CPFTreasuryInterface { public static final VarDB
cpsTreasuryScore = Context.newVarDB(CPS_TREASURY_SCORE, Address.class); public static final VarDB
cpsScore = Context.newVarDB(CPS_SCORE, Address.class); public static final VarDB
balancedDollar = Context.newVarDB(BALANCED_DOLLAR, Address.class); public static final VarDB
dexScore = Context.newVarDB(DEX_SCORE, Address.class); public static final VarDB
sICXScore = Context.newVarDB(SICX_SCORE, Address.class); public static final VarDB
routerScore = Context.newVarDB(ROUTER_SCORE, Address.class); + private final ArrayDB proposalsKeys = Context.newArrayDB(PROPOSALS_KEYS, String.class); + private final DictDB proposalBudgets = Context.newDictDB(PROPOSAL_BUDGETS, BigInteger.class); + private final VarDB treasuryFund = Context.newVarDB(TREASURY_FUND, BigInteger.class); + private final VarDB treasuryFundbnUSD = Context.newVarDB(TREASURY_FUND_BNUSD, BigInteger.class); private final VarDB swapState = Context.newVarDB(SWAP_STATE, Integer.class); private final VarDB swapCount = Context.newVarDB(SWAP_COUNT, Integer.class); - public CPFTreasury() { + private final VarDB swapFlag = Context.newVarDB(SWAP_FLAG, Boolean.class); + public CPFTreasury() { + swapFlag.set(true); } private boolean proposalExists(String ipfsKey) { return proposalBudgets.get(ipfsKey) != null; } + @Override @External(readonly = true) public String name() { return TAG; } + @Override @External public void setMaximumTreasuryFundIcx(BigInteger _value) { validateAdmins(); @@ -59,12 +62,24 @@ public void setMaximumTreasuryFundIcx(BigInteger _value) { * * @param _value: value in loop */ + @Override @External public void setMaximumTreasuryFundBnusd(BigInteger _value) { validateAdmins(); treasuryFundbnUSD.set(_value); } + @External + public void toggleSwapFlag() { + validateAdmins(); + swapFlag.set(!swapFlag.getOrDefault(false)); + } + + @External(readonly = true) + public boolean getSwapFlag() { + return swapFlag.getOrDefault(false); + } + private void burn(BigInteger amount) { Context.call(amount, SYSTEM_ADDRESS, "burn"); @@ -75,6 +90,7 @@ private void burn(BigInteger amount) { * * @return map of ICX and bnUSD amount */ + @Override @External(readonly = true) public Map get_total_funds() { return Map.of(ICX, Context.getBalance(Context.getAddress()), @@ -85,6 +101,7 @@ private BigInteger getTotalFundBNUSD() { return (BigInteger) Context.call(balancedDollar.get(), "balanceOf", Context.getAddress()); } + @Override @External(readonly = true) public Map get_remaining_swap_amount() { BigInteger maxCap = treasuryFundbnUSD.get(); @@ -98,6 +115,7 @@ private void returnFundAmount(Address address, BigInteger value) { FundReturned(address, "Sponsor Bond amount " + value + " " + bnUSD + " Returned to CPF Treasury."); } + @Override @External public void transfer_proposal_fund_to_cps_treasury(String _ipfs_key, int _total_installment_count, Address _sponsor_address, Address _contributor_address, @@ -131,6 +149,7 @@ public void transfer_proposal_fund_to_cps_treasury(String _ipfs_key, int _total_ ProposalFundTransferred(_ipfs_key, "Successfully transferred " + totalTransfer + " " + token_flag + " to CPS Treasury " + cpsTreasuryScore.get()); } + @Override @External public void update_proposal_fund(String _ipfs_key, @Optional String _flag, @Optional BigInteger _added_budget, @Optional int _total_installment_count) { @@ -174,6 +193,7 @@ private void disqualifyProposalFund(String ipfsKey, BigInteger value) { ProposalDisqualified(ipfsKey, "Proposal disqualified. " + value + " " + bnUSD + " is returned back to Treasury."); } + @Override @External @Payable public void add_fund() { @@ -206,13 +226,17 @@ private void swapTokens(Address _from, Address _to, BigInteger _amount) { Context.call(_from, "transfer", dexScore.get(), _amount, swapData.toString().getBytes()); } + @Override @External public void swapIcxBnusd(BigInteger amount) { - Address[] path = new Address[]{sICXScore.get(), balancedDollar.get()}; - Object[] params = new Object[]{path}; - Context.call(amount, routerScore.get(), "route", params); + if (getSwapFlag()) { + Address[] path = new Address[]{sICXScore.get(), balancedDollar.get()}; + Object[] params = new Object[]{path}; + Context.call(amount, routerScore.get(), "route", params); + } } + @Override @External public void swap_tokens(int _count) { validateCpsScore(); @@ -234,35 +258,38 @@ public void swap_tokens(int _count) { } else { BigInteger remainingICXToSwap = bnUSDRemainingToSwap.multiply(EXA).divide(icxbnUSDPrice.multiply(BigInteger.valueOf(count))); BigInteger icxBalance = Context.getBalance(Context.getAddress()); + swapCount.set(swapCountValue + 1); if (remainingICXToSwap.compareTo(icxBalance) > 0) { remainingICXToSwap = icxBalance; } if (remainingICXToSwap.compareTo(BigInteger.valueOf(5).multiply(EXA)) > 0) { swapIcxBnusd(remainingICXToSwap); - swapCount.set(swapCountValue + 1); } } } } } + @Override @External(readonly = true) public Map get_swap_state_status() { return Map.of("state", swapState.getOrDefault(0), "count", swapCount.getOrDefault(0)); } + @Override @External public void reset_swap_state() { Address cpsScoreAddress = cpsScore.get(); Address caller = Context.getCaller(); - boolean checkCaller = caller.equals(cpsScoreAddress) || (Boolean) Context.call(cpsScoreAddress, "is_admin", caller); + boolean checkCaller = caller.equals(cpsScoreAddress) || (Boolean) Context.call(cpsScoreAddress, "isAdmin", caller); Context.require(checkCaller, TAG + ": Only admin can call this method."); swapState.set(SwapContinue); swapCount.set(SwapReset); } + @Override @External(readonly = true) public Map get_proposal_details(@Optional int _start_index, @Optional int _end_index) { if (_end_index == 0) { @@ -294,6 +321,7 @@ public Map get_proposal_details(@Optional int _start_index, @Opt return Map.of("data", proposalsList, "count", count); } + @Override @External public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { Address bnUSDScore = balancedDollar.get(); @@ -340,6 +368,7 @@ public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { } } + @Override @Payable public void fallback() { if (Context.getCaller().equals(dexScore.get())) { @@ -351,18 +380,22 @@ public void fallback() { //EventLogs + @Override @EventLog(indexed = 1) public void FundReturned(Address _sponsor_address, String note) { } + @Override @EventLog(indexed = 1) public void ProposalFundTransferred(String _ipfs_key, String note) { } + @Override @EventLog(indexed = 1) public void ProposalDisqualified(String _ipfs_key, String note) { } + @Override @EventLog(indexed = 1) public void FundReceived(Address _sponsor_address, String note) { } diff --git a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Constants.java b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Constants.java index 7c92451a..a603c678 100644 --- a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Constants.java +++ b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Constants.java @@ -26,6 +26,7 @@ public class Constants { public static final String SWAP_STATE = "swap_state"; public static final String SWAP_COUNT = "swap_count"; + public static final String SWAP_FLAG = "swap_flag"; public static final Address SYSTEM_ADDRESS = Address.fromString("cx0000000000000000000000000000000000000000"); public static final int sICXICXPoolID = 1; diff --git a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Validations.java b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Validations.java index 967dbc33..77d39709 100644 --- a/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Validations.java +++ b/CPFTreasury/src/main/java/community/icon/cps/score/cpftreasury/Validations.java @@ -7,7 +7,7 @@ public class Validations { public static void validateAdmins() { - Context.require((Boolean) Context.call(CPFTreasury.cpsScore.get(), "is_admin", Context.getCaller()), + Context.require((Boolean) Context.call(CPFTreasury.cpsScore.get(), "isAdmin", Context.getCaller()), TAG + ": Only Admins can call this method"); } diff --git a/CPFTreasury/src/test/java/community/icon/cps/score/cpftreasury/CPFTTreasuryTest.java b/CPFTreasury/src/test/java/community/icon/cps/score/cpftreasury/CPFTTreasuryTest.java index 1dccddc2..5c6a3c06 100644 --- a/CPFTreasury/src/test/java/community/icon/cps/score/cpftreasury/CPFTTreasuryTest.java +++ b/CPFTreasury/src/test/java/community/icon/cps/score/cpftreasury/CPFTTreasuryTest.java @@ -85,7 +85,7 @@ public void setMaximumTreasuryFundBNUSDExceptions(Account address) { void setCPSScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setCpsScore", score_address); } assertEquals(score_address, tokenScore.call("getCpsScore")); @@ -95,7 +95,7 @@ void setCPSScore() { void setCPSTreasuryScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setCpsTreasuryScore", score_address); } assertEquals(score_address, tokenScore.call("getCpsTreasuryScore")); @@ -105,7 +105,7 @@ void setCPSTreasuryScore() { void setBMUSDScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setBnUSDScore", score_address); } assertEquals(score_address, tokenScore.call("getBnUSDScore")); @@ -115,7 +115,7 @@ void setBMUSDScore() { void setSICXScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setSicxScore", score_address); } assertEquals(score_address, tokenScore.call("getSicxScore")); @@ -125,7 +125,7 @@ void setSICXScore() { void setDEXScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setDexScore", score_address); } assertEquals(score_address, tokenScore.call("getDexScore")); @@ -135,7 +135,7 @@ void setDEXScore() { void setRouterScore() { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setRouterScore", score_address); } assertEquals(score_address, tokenScore.call("getRouterScore")); @@ -184,7 +184,7 @@ void setDEXScoreExceptions(Account address, Address _score) { void setRouterScoreExceptions(Account address, Address _score) { VarDB
cpsScore = mock(VarDB.class); try (MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(false); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(false); tokenScore.invoke(address, "setRouterScore", _score); } catch (Exception e) { throw e; @@ -294,7 +294,7 @@ void updateProposalFund() { void setMaxCapIcxAndBnusd(){ VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setMaximumTreasuryFundIcx", BigInteger.valueOf(2000).multiply(MULTIPLIER)); tokenScore.invoke(owner, "setMaximumTreasuryFundBnusd", BigInteger.valueOf(2000).multiply(MULTIPLIER)); @@ -305,7 +305,7 @@ void setMaxCapIcxAndBnusd(){ void setMaxCapIcxAndBnusdTest(){ VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setMaximumTreasuryFundIcx", BigInteger.valueOf(2000).multiply(MULTIPLIER)); tokenScore.invoke(owner, "setMaximumTreasuryFundBnusd", BigInteger.valueOf(2000).multiply(MULTIPLIER)); } @@ -389,7 +389,7 @@ void resetSwapState() { setCPSScoreMethod(score_address); try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { theMock.when(() -> Context.getCaller()).thenReturn(testing_account.getAddress()); - theMock.when(() -> Context.call(score_address, "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(score_address, "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "reset_swap_state"); assertEquals(tokenScore.call("get_swap_state_status"), Map.of("state", 0, "count", 0)); } @@ -695,7 +695,7 @@ public void expectErrorMessage(Executable contractCall, String errorMessage) { void setCPSScoreMethod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setCpsScore", scoreAddress); } } @@ -703,7 +703,7 @@ void setCPSScoreMethod(Address scoreAddress) { void setCPSTreasuryScoreMetod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setCpsTreasuryScore", scoreAddress); } } @@ -711,7 +711,7 @@ void setCPSTreasuryScoreMetod(Address scoreAddress) { void setBMUSDScoreMethod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setBnUSDScore", scoreAddress); } } @@ -719,7 +719,7 @@ void setBMUSDScoreMethod(Address scoreAddress) { void setSICXScoreMethod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setSicxScore", scoreAddress); } } @@ -727,7 +727,7 @@ void setSICXScoreMethod(Address scoreAddress) { void setDEXScoreMethod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setDexScore", scoreAddress); } } @@ -735,7 +735,7 @@ void setDEXScoreMethod(Address scoreAddress) { void setRouterScoreMethod(Address scoreAddress) { VarDB
cpsScore = mock(VarDB.class); try(MockedStatic theMock = Mockito.mockStatic(Context.class)){ - theMock.when(() -> Context.call(cpsScore.get(), "is_admin", Context.getCaller())).thenReturn(true); + theMock.when(() -> Context.call(cpsScore.get(), "isAdmin", Context.getCaller())).thenReturn(true); tokenScore.invoke(owner, "setRouterScore", score_address); } } diff --git a/CPSCore/build.gradle b/CPSCore/build.gradle new file mode 100644 index 00000000..c8896a42 --- /dev/null +++ b/CPSCore/build.gradle @@ -0,0 +1,67 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.2' + implementation 'com.github.sink772:minimal-json:0.9.7' + implementation 'foundation.icon:javaee-scorex:0.5.3' + + implementation project(':score-lib') + testImplementation project(':test-lib') + + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testImplementation('org.mockito:mockito-inline:4.8.0') + testImplementation 'foundation.icon:javaee-unittest:0.9.7' + intTestImplementation 'foundation.icon:icon-sdk:2.2.0' + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' +} + +optimizedJar { + mainClassName = 'community.icon.cps.score.cpscore.CPSCore' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + to = 'cxd82e5f0c1090f55f9b2f727ad35fb3a9e1de95c4' + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + } + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + to = 'cx2b214d56805531b9c878c4d4ff0e53d961c60834' + } + mainnet { + uri = 'https://ctz.solidwallet.io/api/v3' + nid = 0x1 + to = 'cx9f4ab72f854d3ccdc59aa6f2c3e2215dd62e879f' + } + + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + } +} + +test { + useJUnitPlatform() +} + +repositories { + mavenCentral() +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/CPSCore.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/CPSCore.java new file mode 100644 index 00000000..c9bf3a7c --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/CPSCore.java @@ -0,0 +1,2406 @@ +package community.icon.cps.score.cpscore; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import community.icon.cps.score.cpscore.db.ProgressReportDataDb; +import community.icon.cps.score.cpscore.db.ProposalDataDb; +import community.icon.cps.score.cpscore.utils.ArrayDBUtils; +import community.icon.cps.score.lib.interfaces.CPSCoreInterface; +import score.*; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; +import scorex.util.ArrayList; +import scorex.util.HashMap; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static community.icon.cps.score.cpscore.db.ProgressReportDataDb.*; +import static community.icon.cps.score.cpscore.db.ProposalDataDb.*; +import static community.icon.cps.score.cpscore.utils.ArrayDBUtils.*; +import static community.icon.cps.score.cpscore.utils.Checkers.checkMaintenance; +import static community.icon.cps.score.cpscore.utils.Checkers.onlyOwner; +import static community.icon.cps.score.cpscore.utils.Constants.*; + +public class CPSCore implements CPSCoreInterface { + + private final ArrayDB proposalsKeyList = Context.newArrayDB(PROPOSALS_KEY_LIST, String.class); + private final DictDB proposalsKeyListIndex = Context.newDictDB(PROPOSALS_KEY_LIST_INDEX, Integer.class); + private final ArrayDB progressKeyList = Context.newArrayDB(PROGRESS_KEY_LIST, String.class); + private final DictDB progressKeyListIndex = Context.newDictDB(PROGRESS_KEY_LIST_INDEX, Integer.class); + private final ArrayDB budgetApprovalsList = Context.newArrayDB(BUDGET_APPROVALS_LIST, String.class); + + private final ArrayDB activeProposals = Context.newArrayDB(ACTIVE_PROPOSALS, String.class); + + private final ArrayDB
contributors = Context.newArrayDB(CONTRIBUTORS, Address.class); + private final ArrayDB
sponsors = Context.newArrayDB(SPONSORS, Address.class); + private static final ArrayDB
admins = Context.newArrayDB(ADMINS, Address.class); + private final ArrayDB sponsorPending = Context.newArrayDB(SPONSOR_PENDING, String.class); + private final ArrayDB pending = Context.newArrayDB(PENDING, String.class); + private final ArrayDB active = Context.newArrayDB(ACTIVE, String.class); + private final ArrayDB paused = Context.newArrayDB(PAUSED, String.class); + private final ArrayDB completed = Context.newArrayDB(COMPLETED, String.class); + private final ArrayDB rejected = Context.newArrayDB(REJECTED, String.class); + private final ArrayDB disqualified = Context.newArrayDB(DISQUALIFIED, String.class); + public final Map> proposalStatus = Map.of(SPONSOR_PENDING, sponsorPending, + PENDING, pending, + ACTIVE, active, + PAUSED, paused, + COMPLETED, completed, + REJECTED, rejected, + DISQUALIFIED, disqualified); + + + private final ArrayDB waitingProgressReports = Context.newArrayDB(WAITING, String.class); + private final ArrayDB approvedProgressReports = Context.newArrayDB(APPROVED, String.class); + private final ArrayDB progressRejected = Context.newArrayDB(PROGRESS_REPORT_REJECTED, String.class); + public final Map> progressReportStatus = Map.of(WAITING, waitingProgressReports, + APPROVED, approvedProgressReports, + PROGRESS_REPORT_REJECTED, progressRejected + ); + + private final BranchDB> sponsorBondReturn = Context.newBranchDB(SPONSOR_BOND_RETURN, BigInteger.class); + private final DictDB delegationSnapshot = Context.newDictDB(DELEGATION_SNAPSHOT, BigInteger.class); + private final VarDB maxDelegation = Context.newVarDB(MAX_DELEGATION, BigInteger.class); + private final VarDB proposalFees = Context.newVarDB(PROPOSAL_FEES, BigInteger.class); + private final VarDB swapBlockHeight = Context.newVarDB(SWAP_BLOCK_HEIGHT, BigInteger.class); + private final VarDB swapCount = Context.newVarDB(SWAP_COUNT, Integer.class); + private final DictDB proposalRank = Context.newDictDB(PROPOSAL_RANK, Integer.class); + private final ArrayDB
priorityVotedPreps = Context.newArrayDB(PRIORITY_VOTED_PREPS, Address.class); + private final BranchDB> sponsorProjects = Context.newBranchDB(SPONSOR_PROJECTS, String.class); + private final BranchDB> contributorProjects = Context.newBranchDB(CONTRIBUTOR_PROJECTS, String.class); + + public CPSCore() { + PeriodController periodController = new PeriodController(); + } + + @Override + @External(readonly = true) + public String name() { + return TAG; + } + + @Override + public String proposalPrefix(String proposalKey) { + return PROPOSAL_DB_PREFIX + "|" + "|" + proposalKey; + } + + @Override + public String progressReportPrefix(String progressKey) { + return PROGRESS_REPORT_DB_PREFIX + "|" + "|" + progressKey; + } + + /*** + * Deprecated because JAVA convention will be used in the future versions for method name and parameter name + * i.e. set_cps_treasury_score -> setCpsTreasuryScore and _score -> score + * @param _score: Address of CPS Treasury Score + */ + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void set_cps_treasury_score(Address _score) { + setCpsTreasuryScore(_score); + } + + @Override + @External + public void setCpsTreasuryScore(Address score) { + validateAdminScore(score); + SetterGetter setterGetter = new SetterGetter(); + setterGetter.cpsTreasuryScore.set(score); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Address get_cps_treasury_score() { + return getCpsTreasuryScore(); + } + + @Override + @External(readonly = true) + public Address getCpsTreasuryScore() { + SetterGetter setterGetter = new SetterGetter(); + return setterGetter.cpsTreasuryScore.get(); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void set_cpf_treasury_score(Address _score) { + setCpfTreasuryScore(_score); + } + + @Override + @External + public void setCpfTreasuryScore(Address score) { + validateAdminScore(score); + SetterGetter setterGetter = new SetterGetter(); + setterGetter.cpfScore.set(score); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Address get_cpf_treasury_score() { + return getCpfTreasuryScore(); + } + + @Override + @External(readonly = true) + public Address getCpfTreasuryScore() { + SetterGetter setterGetter = new SetterGetter(); + return setterGetter.cpfScore.get(); + } + + + @Override + @External + public void setBnusdScore(Address score) { + validateAdminScore(score); + SetterGetter setterGetter = new SetterGetter(); + setterGetter.balancedDollar.set(score); + } + + + @Override + @External(readonly = true) + public Address getBnusdScore() { + SetterGetter setterGetter = new SetterGetter(); + return setterGetter.balancedDollar.get(); + } + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Address get_bnUSD_score() { + return getBnusdScore(); + } + + private boolean proposalKeyExists(String key) { + return proposalsKeyListIndex.get(key) != null; + } + + private boolean progressKeyExists(String key) { + return progressKeyListIndex.get(key) != null; + } + + @External(readonly = true) + public boolean isAdmin(Address address) { + return ArrayDBUtils.containsInArrayDb(address, admins); + } + + @Deprecated + @External + public boolean is_admin(Address _address) { + return isAdmin(_address); + } + + @Override + @External + public void toggleBudgetAdjustmentFeature() { + validateAdmins(); + SetterGetter setterGetter = new SetterGetter(); + setterGetter.budgetAdjustment.set(!setterGetter.budgetAdjustment.getOrDefault(false)); + } + + @Override + @External(readonly = true) + public boolean getBudgetAdjustmentFeature() { + SetterGetter setterGetter = new SetterGetter(); + return setterGetter.budgetAdjustment.getOrDefault(true); + } + + @External + public void toggleMaintenance() { + validateAdmins(); + SetterGetter setterGetter = new SetterGetter(); + setterGetter.maintenance.set(!setterGetter.maintenance.getOrDefault(Boolean.TRUE)); + } + + + @Override + @External(readonly = true) + public boolean getMaintenanceMode() { + SetterGetter setterGetter = new SetterGetter(); + return setterGetter.maintenance.getOrDefault(false); + } + + @External(readonly = true) + public boolean get_maintenance_mode() { + return getMaintenanceMode(); + } + + @Override + @Payable + public void fallback() { + Context.revert(TAG + ": ICX can only be sent while submitting a proposal or paying the penalty."); + } + + private void burn(BigInteger amount, @Optional Address token) { + if (token == null) { + token = SYSTEM_ADDRESS; + } + SetterGetter setterGetter = new SetterGetter(); + if (token.equals(SYSTEM_ADDRESS)) { + callScore(amount, SYSTEM_ADDRESS, "burn"); + } else { + Address bnUSDScore = setterGetter.balancedDollar.get(); + if (token.equals(bnUSDScore)) { + JsonObject burnTokens = new JsonObject(); + burnTokens.add("method", "burn_amount"); + callScore(bnUSDScore, "transfer", setterGetter.cpfScore.get(), amount, burnTokens.toString().getBytes()); + } else { + Context.revert(TAG + ": Not a supported token."); + } + } + } + + @Override + @External(readonly = true) + public int getPeriodCount() { + PeriodController periodController = new PeriodController(); + return periodController.periodCount.getOrDefault(0); + } + + @External + public void addAdmin(Address address) { +// TODO check governance contract + onlyOwner(); + boolean check = ArrayDBUtils.containsInArrayDb(address, admins); + if (!check) { + admins.add(address); + } + } + + + @External + public void removeAdmin(Address address) { // change made + onlyOwner(); + Context.require(address != Context.getOwner(), "Owner cannot be removed from admin list."); + boolean check = ArrayDBUtils.containsInArrayDb(address, admins); + Context.require(check, TAG + ": Address not registered as admin."); + ArrayDBUtils.removeArrayItem(admins, address); + } + + + @External + public void unregisterPrep() { + checkMaintenance(); + updatePeriod(); + Address caller = Context.getCaller(); + PReps pReps = new PReps(); + PeriodController period = new PeriodController(); + Context.require(ArrayDBUtils.containsInArrayDb(caller, pReps.validPreps) && + ArrayDBUtils.containsInArrayDb(caller, pReps.registeredPreps), "P-Rep is not registered yet."); + Context.require(period.periodName.get().equals(APPLICATION_PERIOD), + "P-Reps can only be unregister on Application Period"); + ArrayDBUtils.removeArrayItem(pReps.validPreps, caller); + ArrayDBUtils.removeArrayItem(pReps.registeredPreps, caller); + pReps.unregisteredPreps.add(caller); + UnRegisterPRep(caller, "P-Rep has ben unregistered successfully."); + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void unregister_prep() { + unregisterPrep(); + } + + @External + public void registerPrep() { + checkMaintenance(); + updatePeriod(); + Address caller = Context.getCaller(); + List
prepList = getPrepsAddress(); + PReps pReps = new PReps(); + + Context.require(prepList.contains(caller), + TAG + ": Not a P-Rep."); + Context.require(!ArrayDBUtils.containsInArrayDb(caller, pReps.registeredPreps), + TAG + ": P-Rep is already registered."); + Context.require(!ArrayDBUtils.containsInArrayDb(caller, pReps.denylist), + TAG + ": You are in denylist. To register, You've to pay Penalty."); + + if (ArrayDBUtils.containsInArrayDb(caller, pReps.unregisteredPreps)) { + ArrayDBUtils.removeArrayItem(pReps.unregisteredPreps, caller); + } + pReps.registeredPreps.add(caller); + RegisterPRep(caller, "P-Rep Registered."); + PeriodController period = new PeriodController(); + if (period.periodName.get().equals(APPLICATION_PERIOD)) { + pReps.validPreps.add(caller); + } + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void register_prep() { + registerPrep(); + } + + @SuppressWarnings("unchecked") + private List> getPrepTerm() { + Map prepDict = callScore(Map.class, SYSTEM_ADDRESS, "getPRepTerm"); + return (List>) prepDict.get("preps"); + } + + private Map getPRepInfo(Address address) { + return callScore(Map.class, SYSTEM_ADDRESS, "getPRep", address); + } + + + private List
getPrepsAddress() { + List
prepsList = new ArrayList<>(); + for (Map preps : getPrepTerm()) { + Address prepAddress = (Address) preps.get("address"); + prepsList.add(prepAddress); + } + return prepsList; + } + + private String getPrepName(Address address) { + return (String) getPRepInfo(address).get("name"); + } + + private BigInteger getStake(Address address) { + return (BigInteger) getPRepInfo(address).get("power"); + } + + private void setPreps() { + PReps pReps = new PReps(); + ArrayDBUtils.clearArrayDb(pReps.validPreps); + List
prepsList = getPrepsAddress(); + + for (Address prep : prepsList) { + if (!ArrayDBUtils.containsInArrayDb(prep, pReps.denylist) && + !ArrayDBUtils.containsInArrayDb(prep, pReps.unregisteredPreps)) { + if (ArrayDBUtils.containsInArrayDb(prep, pReps.registeredPreps)) { + pReps.validPreps.add(prep); + } + } + } + + } + + private void removeSponsor(Address address, String ipfsHash) { + Context.require(ArrayDBUtils.containsInArrayDb(address, sponsors), + address + " not on sponsor list."); + ArrayDBUtils.removeArrayItem(sponsors, address); + ArrayDBUtils.removeArrayItem(sponsorProjects.at(address), ipfsHash); + } + + private void removeContributor(Address address, String ipfsHash) { + Context.require(ArrayDBUtils.containsInArrayDb(address, contributors), + address + " not on contributor list."); + ArrayDBUtils.removeArrayItem(contributors, address); + ArrayDBUtils.removeArrayItem(contributorProjects.at(address), ipfsHash); + } + + @External(readonly = true) + public List getProposalKeys() { + List proposalKeys = new ArrayList<>(); + for (int i = 0; i < proposalsKeyList.size(); i++) { + proposalKeys.add(proposalsKeyList.get(i)); + + } + return proposalKeys; + + } + + @External(readonly = true) + public List getProgressKeys() { + List progressKeys = new ArrayList<>(); + for (int i = 0; i < progressKeyList.size(); i++) { + progressKeys.add(progressKeyList.get(i)); + + } + return progressKeys; + + } + + private BigInteger getPenaltyAmount(Address address) { + PReps pReps = new PReps(); + Integer count = pReps.prepsDenylistStatus.getOrDefault(address.toString(), 0); + Context.require(count != 0, address + " doesn't need to pay any penalty."); + + int idx = count < 3 ? count - 1 : 2; + BigInteger amount = pReps.penaltyAmount.get(idx); + BigInteger delegationAmount = getStake(address); + return delegationAmount.multiply(amount).divide(maxDelegation.get()); + } + + @Override + @External(readonly = true) + public boolean checkPriorityVoting(Address _prep) { + return ArrayDBUtils.containsInArrayDb(_prep, priorityVotedPreps); + } + + @Override + @External(readonly = true) + public List sortPriorityProposals() { + String[] pendingProposals = new String[pending.size()]; + for (int i = 0; i < pending.size(); i++) { + pendingProposals[i] = pending.get(i); + } + mergeSort(pendingProposals, 0, pending.size() - 1, getPriorityVoteResult()); + return arrayToList(pendingProposals); + } + + @Override + @External(readonly = true) + public Map getPriorityVoteResult() { + Map priorityVoteResult = new HashMap<>(); + + for (int i = 0; i < pending.size(); i++) { + String prop = pending.get(i); + priorityVoteResult.put(prop, proposalRank.getOrDefault(prop, 0)); + + } + return priorityVoteResult; + } + + + @Override + @External + public void votePriority(String[] _proposals) { + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(VOTING_PERIOD), TAG + ": Voting can only be done in Voting Period."); + Address caller = Context.getCaller(); + PReps pReps = new PReps(); + Context.require(ArrayDBUtils.containsInArrayDb(caller, pReps.validPreps), "Voting can only be done by registered P-Reps"); + Context.require(!checkPriorityVoting(caller), "Already voted for Priority Ranking."); + + priorityVotedPreps.add(caller); + int size = _proposals.length; + for (int i = 0; i < size; i++) { + String proposal = _proposals[i]; + Context.require(ArrayDBUtils.containsInArrayDb(proposal, pending), + proposal + " not in pending state."); + proposalRank.set(proposal, proposalRank.getOrDefault(proposal, 0) + size - i); + } + PriorityVote(caller, "Priority voting done successfully."); + } + + @Override + @External + public void setPrepPenaltyAmount(BigInteger[] penalty) { + checkMaintenance(); + validateAdmins(); + Context.require(penalty.length == PENALTY_LEVELS, TAG + ": Exactly 3 Penalty amount Required."); + PReps pReps = new PReps(); + for (int i = 0; i < PENALTY_LEVELS; i++) { + BigInteger amount = penalty[i]; + Context.require(amount.compareTo(BigInteger.ZERO) >= 0, "Invalid amount" + amount); + pReps.penaltyAmount.add(amount.multiply(EXA)); + } + + } + + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void set_prep_penalty_amount(BigInteger[] _penalty) { + setPrepPenaltyAmount(_penalty); + } + + + @Override + @External + public void setInitialBlock() { + validateAdmins(); + setPreps(); + PeriodController period = new PeriodController(); + period.initialBlock.set(BigInteger.valueOf(Context.getBlockHeight())); + period.nextBlock.set(BigInteger.valueOf(Context.getBlockHeight()).add(BLOCKS_DAY_COUNT.multiply(DAY_COUNT))); + period.periodName.set(APPLICATION_PERIOD); + period.previousPeriodName.set("None"); + } + + @External(readonly = true) + public Map loginPrep(Address address) { + Map loginData = new HashMap<>(); + List
allPreps = getPrepsAddress(); + PReps pReps = new PReps(); + if (allPreps.contains(address)) { + loginData.put("isPRep", BigInteger.ONE); + if (ArrayDBUtils.containsInArrayDb(address, pReps.unregisteredPreps)) { + loginData.put("isRegistered", BigInteger.ZERO); + loginData.put("payPenalty", BigInteger.ZERO); + loginData.put("votingPRep", BigInteger.ZERO); + + } else if (ArrayDBUtils.containsInArrayDb(address, pReps.denylist)) { + loginData.put("isRegistered", BigInteger.ZERO); + loginData.put("payPenalty", BigInteger.ONE); + loginData.put("votingPRep", BigInteger.ZERO); + loginData.put("penaltyAmount", getPenaltyAmount(address)); + } else if (ArrayDBUtils.containsInArrayDb(address, pReps.registeredPreps)) { + loginData.put("isRegistered", BigInteger.ONE); + loginData.put("payPenalty", BigInteger.ZERO); + loginData.put("votingPRep", BigInteger.ZERO); + + if (ArrayDBUtils.containsInArrayDb(address, pReps.validPreps)) { + loginData.put("votingPRep", BigInteger.ONE); + } + } else { + loginData.put("isRegistered", BigInteger.ZERO); + loginData.put("payPenalty", BigInteger.ZERO); + loginData.put("votingPRep", BigInteger.ZERO); + } + + } else { + loginData.put("isPRep", BigInteger.ZERO); + loginData.put("isRegistered", BigInteger.ZERO); + loginData.put("payPenalty", BigInteger.ZERO); + loginData.put("votingPRep", BigInteger.ZERO); + + } + return loginData; + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map login_prep(Address _address) { + return loginPrep(_address); + } + + @External(readonly = true) + public List
getAdmins() { + return ArrayDBUtils.arrayDBtoList(admins); + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List
get_admins() { + return getAdmins(); + } + + @Override + @External(readonly = true) + public Map getRemainingFund() { + SetterGetter setterGetter = new SetterGetter(); + //noinspection unchecked + return callScore(Map.class, setterGetter.cpfScore.get(), "get_total_funds"); + } + + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_remaining_fund() { + return getRemainingFund(); + } + + @External(readonly = true) + public List> getPReps() { + List> prepsList = new ArrayList<>(); + PReps pReps = new PReps(); + for (int i = 0; i < pReps.validPreps.size(); i++) { + Address prep = pReps.validPreps.get(i); + Map prepData = new HashMap<>(); + prepData.put("name", getPrepName(prep)); + prepData.put("address", prep.toString()); + prepData.put("delegated", getStake(prep).toString()); + prepsList.add(prepData); + } + return prepsList; + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List> get_PReps() { + return getPReps(); + } + + @External(readonly = true) + public List
getDenylist() { + List
denyList = new ArrayList<>(); + PReps pReps = new PReps(); + for (int i = 0; i < pReps.denylist.size(); i++) { + denyList.add(pReps.denylist.get(i)); + } + return denyList; + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List
get_denylist() { + return getDenylist(); + } + + @External(readonly = true) + public Map getPeriodStatus() { + PeriodController period = new PeriodController(); + BigInteger remainingTime = period.nextBlock.getOrDefault(BigInteger.ZERO).subtract(BigInteger.valueOf(Context.getBlockHeight())).multiply(BigInteger.valueOf(2)); + if (remainingTime.compareTo(BigInteger.ZERO) < 0) { + remainingTime = BigInteger.ZERO; + } + return Map.of(CURRENTBLOCK, Context.getBlockHeight(), + NEXTBLOCK, period.nextBlock.getOrDefault(BigInteger.valueOf(0)), + REMAINING_TIME, remainingTime, + PERIOD_NAME, period.periodName.getOrDefault("None"), + PREVIOUS_PERIOD_NAME, period.previousPeriodName.getOrDefault("None"), + PERIOD_SPAN, BLOCKS_DAY_COUNT.multiply(DAY_COUNT).multiply(BigInteger.valueOf(2))); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_period_status() { + return getPeriodStatus(); + } + + @External(readonly = true) + public List
getContributors() { + return ArrayDBUtils.arrayDBtoList(this.contributors); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List
get_contributors() { + return getContributors(); + } + + + @External(readonly = true) + public Map checkClaimableSponsorBond(Address address) { + DictDB userAmounts = sponsorBondReturn.at(address.toString()); + return Map.of(ICX, userAmounts.getOrDefault(ICX, BigInteger.ZERO), + bnUSD, userAmounts.getOrDefault(bnUSD, BigInteger.ZERO)); + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map check_claimable_sponsor_bond(Address _address) { + return checkClaimableSponsorBond(_address); + } + + @SuppressWarnings("unchecked") + private BigInteger getMaxCapBNUsd() { + SetterGetter setterGetter = new SetterGetter(); + Map cpfAmount = callScore(Map.class, setterGetter.cpfScore.get(), "get_remaining_swap_amount"); + return cpfAmount.get("maxCap"); + } + + @Payable + @External + public void submitProposal(ProposalAttributes proposals) { + checkMaintenance(); + updatePeriod(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(APPLICATION_PERIOD), + TAG + ": Proposals can only be submitted on Application Period "); + Context.require(!proposalKeyExists(proposals.ipfs_hash), TAG + ": Proposal key already exists."); + Context.require(!Context.getCaller().isContract(), TAG + ": Contract Address not supported."); + Context.require(proposals.project_duration <= MAX_PROJECT_PERIOD, + TAG + ": Maximum Project Duration exceeds " + MAX_PROJECT_PERIOD + " months."); + BigInteger projectBudget = proposals.total_budget.multiply(EXA); + BigInteger maxCapBNUsd = getMaxCapBNUsd(); + Context.require(projectBudget.compareTo(maxCapBNUsd) < 0, + TAG + ": " + projectBudget + "is greater than MAX CAP " + maxCapBNUsd); + PReps pReps = new PReps(); + Context.require(ArrayDBUtils.containsInArrayDb(proposals.sponsor_address, pReps.validPreps), + TAG + ": Sponsor P-Rep not a Top 100 P-Rep."); + Context.require(Context.getValue().equals(BigInteger.valueOf(APPLICATION_FEE).multiply(EXA)), + TAG + ": Deposit " + APPLICATION_FEE + " ICX to submit a proposal."); + String tokenFlag = proposals.token; + Context.require(tokenFlag.equals(bnUSD), TAG + ": " + tokenFlag + " Not a supported token."); + + String ipfsHash = proposals.ipfs_hash; + String ipfsHashPrefix = proposalPrefix(ipfsHash); + + addDataToProposalDB(proposals, ipfsHashPrefix); + proposalsKeyList.add(proposals.ipfs_hash); + proposalsKeyListIndex.set(ipfsHash, proposalsKeyList.size() - 1); + sponsorPending.add(ipfsHash); + contributors.add(Context.getCaller()); + contributorProjects.at(Context.getCaller()).add(ipfsHash); + ProposalSubmitted(Context.getCaller(), "Successfully submitted a Proposal."); + + BigInteger totalFund = proposalFees.getOrDefault(BigInteger.ZERO); + BigInteger halfProposalFee = Context.getValue().divide(BigInteger.TWO); + proposalFees.set(totalFund.add(halfProposalFee)); + burn(halfProposalFee, null); + swapBNUsdToken(); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @Payable + @External + public void submit_proposal(ProposalAttributes _proposals) { + submitProposal(_proposals); + } + + @External + public void voteProposal(String ipfsKey, String vote, String voteReason, @Optional boolean voteChange) { + checkMaintenance(); + updatePeriod(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(VOTING_PERIOD), + TAG + ": Proposals can be voted only on Voting Period."); + Address caller = Context.getCaller(); + PReps pReps = new PReps(); + Context.require(ArrayDBUtils.containsInArrayDb(caller, pReps.validPreps), + TAG + ": Voting can only be done by registered P-Reps."); + Context.require(List.of(APPROVE, REJECT, ABSTAIN).contains(vote), + TAG + ": Vote should be either _approve, _reject or _abstain"); + + Map proposalDetails = getProposalDetails(ipfsKey); + String proposalPrefix = proposalPrefix(ipfsKey); + String status = (String) proposalDetails.get(STATUS); + + ArrayDB
voterList = ProposalDataDb.votersList.at(proposalPrefix); + + if (!voteChange) { + if (ArrayDBUtils.containsInArrayDb(caller, voterList)) { + Context.revert(TAG + ": Already Voted"); + } + } + Context.require(status.equals(PENDING), TAG + ": Proposal must be done in Voting state."); + + BigInteger voterStake = delegationSnapshot.get(caller); + BigInteger totalVotes = (BigInteger) proposalDetails.get(TOTAL_VOTES); + BigInteger approvedVotes = (BigInteger) proposalDetails.get(APPROVED_VOTES); + BigInteger rejectedVotes = (BigInteger) proposalDetails.get(REJECTED_VOTES); + BigInteger abstainedVotes = (BigInteger) proposalDetails.get(ABSTAINED_VOTES); + Integer totalVoter = (Integer) proposalDetails.get(TOTAL_VOTERS); + if (totalVoter == 0) { + ProposalDataDb.totalVoters.at(proposalPrefix).set(pReps.validPreps.size()); + } + + DictDB votersIndexDb = votersListIndex.at(proposalPrefix).at(caller); + + if (!voteChange) { + ProposalDataDb.totalVotes.at(proposalPrefix).set(totalVotes.add(voterStake)); + ProposalDataDb.votersList.at(proposalPrefix).add(caller); + votersIndexDb.set(INDEX, ProposalDataDb.votersList.at(proposalPrefix).size()); + ProposalDataDb.votersReasons.at(proposalPrefix).add(voteReason); + } else { + Context.require(votersIndexDb.getOrDefault(CHANGE_VOTE, 0) == 0, + TAG + ": Vote change can be done only once."); + votersIndexDb.set(CHANGE_VOTE, VOTED); + int index = votersIndexDb.getOrDefault(INDEX, 0); + int voteIndex = votersIndexDb.getOrDefault(VOTE, 0); + ProposalDataDb.votersReasons.at(proposalPrefix).set(index - 1, voteReason); + if (voteIndex == APPROVE_) { + ArrayDBUtils.removeArrayItem(ProposalDataDb.approveVoters.at(proposalPrefix), caller); + ProposalDataDb.approvedVotes.at(proposalPrefix).set(approvedVotes.subtract(voterStake)); + } else if (voteIndex == REJECT_) { + ArrayDBUtils.removeArrayItem(ProposalDataDb.rejectVoters.at(proposalPrefix), caller); + ProposalDataDb.rejectedVotes.at(proposalPrefix).set(rejectedVotes.subtract(voterStake)); + } else { + ArrayDBUtils.removeArrayItem(abstainVoters.at(proposalPrefix), caller); + ProposalDataDb.abstainedVotes.at(proposalPrefix).set(abstainedVotes.subtract(voterStake)); + } + approvedVotes = ProposalDataDb.approvedVotes.at(proposalPrefix).getOrDefault(BigInteger.ZERO); + rejectedVotes = ProposalDataDb.rejectedVotes.at(proposalPrefix).getOrDefault(BigInteger.ZERO); + abstainedVotes = ProposalDataDb.abstainedVotes.at(proposalPrefix).getOrDefault(BigInteger.ZERO); + + } + if (vote.equals(APPROVE)) { + ProposalDataDb.approveVoters.at(proposalPrefix).add(caller); + votersIndexDb.set(VOTE, APPROVE_); + ProposalDataDb.approvedVotes.at(proposalPrefix).set(approvedVotes.add(voterStake)); + } else if (vote.equals(REJECT)) { + ProposalDataDb.rejectVoters.at(proposalPrefix).add(caller); + votersIndexDb.set(VOTE, REJECT_); + ProposalDataDb.rejectedVotes.at(proposalPrefix).set(rejectedVotes.add(voterStake)); + + } else { + abstainVoters.at(proposalPrefix).add(caller); + votersIndexDb.set(VOTE, ABSTAIN_); + ProposalDataDb.abstainedVotes.at(proposalPrefix).set(abstainedVotes.add(voterStake)); + } + VotedSuccessfully(caller, "Proposal Vote for " + proposalDetails.get(PROJECT_TITLE) + " Successful."); + swapBNUsdToken(); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void vote_proposal(String _ipfs_key, String _vote, String _vote_reason, @Optional boolean _vote_change) { + voteProposal(_ipfs_key, _vote, _vote_reason, _vote_change); + } + + @External + public void submitProgressReport(ProgressReportAttributes progressReport) { + checkMaintenance(); + updatePeriod(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(APPLICATION_PERIOD), + TAG + ": Proposals can only be submitted on Application Period "); + + Address caller = Context.getCaller(); + Context.require(!caller.isContract(), TAG + ": Contract Address not supported."); + + String ipfsHashPrefix = proposalPrefix(progressReport.ipfs_hash); + String tokenFlag = ProposalDataDb.token.at(ipfsHashPrefix).getOrDefault(""); + Context.require(tokenFlag.equals(bnUSD), TAG + ": " + tokenFlag + " Not a supported token."); + + Address contributorAddress = ProposalDataDb.contributorAddress.at(ipfsHashPrefix).get(); + Context.require(caller.equals(contributorAddress), + TAG + ": Sorry, You are not the contributor for this project."); + + String status = ProposalDataDb.status.at(ipfsHashPrefix).get(); + Context.require(List.of(ACTIVE, PAUSED).contains(status), + TAG + ": Sorry, This project is not found on active state."); + + Boolean progressSubmitted = ProposalDataDb.submitProgressReport.at(ipfsHashPrefix).get(); + Context.require(!progressSubmitted, TAG + ": Progress Report is already submitted this cycle."); + + String reportHash = progressReport.report_hash; + String ipfsHash = progressReport.ipfs_hash; + Context.require(!progressKeyExists(reportHash), TAG + ": Report key already exists."); + Context.require(proposalKeyExists(ipfsHash), TAG + ": Invalid proposal key"); + addNewProgressReportKey(ipfsHash, reportHash); + String reportHashPrefix = progressReportPrefix(reportHash); + addDataToProgressReportDB(progressReport, reportHashPrefix); + int percentageCompleted = progressReport.percentage_completed; + + if (percentageCompleted >= 0 && percentageCompleted <= 100) { + ProposalDataDb.percentageCompleted.at(ipfsHashPrefix).set(percentageCompleted); + } else { + Context.revert(TAG + ": Percentage completed should be between 0 and 100"); + } + + if (progressReport.budget_adjustment) { + Context.require(getBudgetAdjustmentFeature(), + TAG + ": Budget Adjustment feature is disabled for the moment."); + + Boolean budgetAdjustment = ProposalDataDb.budgetAdjustment.at(ipfsHashPrefix).get(); + Context.require(!budgetAdjustment, + TAG + ": Budget Adjustment Already submitted for this proposal."); + + int projectDuration = ProposalDataDb.projectDuration.at(ipfsHashPrefix).get(); + Context.require(progressReport.additional_month + projectDuration <= MAX_PROJECT_PERIOD, + TAG + ": Maximum period for a project is " + MAX_PROJECT_PERIOD + " months."); + + budgetApprovalsList.add(reportHash); + ProgressReportDataDb.budgetAdjustmentStatus.at(reportHashPrefix).set(PENDING); + ProposalDataDb.budgetAdjustment.at(ipfsHashPrefix).set(true); + } + progressKeyList.add(reportHash); + progressKeyListIndex.set(reportHash, progressKeyList.size() - 1); + + submitProgressReport.at(ipfsHashPrefix).set(true); + waitingProgressReports.add(reportHash); + swapBNUsdToken(); + ProgressReportSubmitted(caller, progressReport.progress_report_title + + " --> Progress Report Submitted Successfully."); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void submit_progress_report(ProgressReportAttributes _progress_report) { + submitProgressReport(_progress_report); + } + + @External + public void voteProgressReport(String ipfsKey, String reportKey, String vote, String voteReason, @Optional String budgetAdjustmentVote, @Optional boolean voteChange) { + if (budgetAdjustmentVote == null) { + budgetAdjustmentVote = ""; + + } + + checkMaintenance(); + updatePeriod(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(VOTING_PERIOD), + TAG + ": Progress Reports can be voted only on Voting Period."); + Address caller = Context.getCaller(); + PReps pReps = new PReps(); + Context.require(ArrayDBUtils.containsInArrayDb(caller, pReps.validPreps), + TAG + ": Voting can only be done by registered P-Reps."); + Context.require(List.of(APPROVE, REJECT).contains(vote), + TAG + ": Vote should be either _approve or _reject"); + + Map progressReportDetails = getProgressReportDetails(reportKey); + String progressReportPrefix = progressReportPrefix(reportKey); + String status = (String) progressReportDetails.get(STATUS); + + ArrayDB
voterList = ProgressReportDataDb.votersList.at(progressReportPrefix); + + if (!voteChange) { + if (ArrayDBUtils.containsInArrayDb(caller, voterList)) { + Context.revert(TAG + ": Already Voted"); + } + } + + if (status.equals(WAITING)) { + BigInteger voterStake = delegationSnapshot.get(caller); + BigInteger totalVotes = (BigInteger) progressReportDetails.get(TOTAL_VOTES); + BigInteger approvedVotes = (BigInteger) progressReportDetails.get(APPROVED_VOTES); + BigInteger rejectedVotes = (BigInteger) progressReportDetails.get(REJECTED_VOTES); + Integer totalVoter = (Integer) progressReportDetails.get(TOTAL_VOTERS); + if (totalVoter == 0) { + ProgressReportDataDb.totalVoters.at(progressReportPrefix).set(pReps.validPreps.size()); + } + DictDB votersIndexDb = votersListIndices.at(progressReportPrefix).at(caller); + + if (!voteChange) { + ProgressReportDataDb.totalVotes.at(progressReportPrefix).set(totalVotes.add(voterStake)); + ProgressReportDataDb.votersList.at(progressReportPrefix).add(caller); + votersIndexDb.set(INDEX, ProgressReportDataDb.votersList.at(progressReportPrefix).size()); + ProgressReportDataDb.votersReasons.at(progressReportPrefix).add(voteReason); + } else { + Context.require(votersIndexDb.getOrDefault(CHANGE_VOTE, 0) == 0, + TAG + ": Progress Report Vote change can be done only once."); + votersIndexDb.set(CHANGE_VOTE, VOTED); + int index = votersIndexDb.getOrDefault(INDEX, 0); + int voteIndex = votersIndexDb.getOrDefault(VOTE, 0); + ProgressReportDataDb.votersReasons.at(progressReportPrefix).set(index - 1, voteReason); + if (voteIndex == APPROVE_) { + ArrayDBUtils.removeArrayItem(ProgressReportDataDb.approveVoters.at(progressReportPrefix), caller); + ProgressReportDataDb.approvedVotes.at(progressReportPrefix).set(approvedVotes.subtract(voterStake)); + } else { + ArrayDBUtils.removeArrayItem(ProgressReportDataDb.rejectVoters.at(progressReportPrefix), caller); + ProgressReportDataDb.rejectedVotes.at(progressReportPrefix).set(rejectedVotes.subtract(voterStake)); + } + + if (ArrayDBUtils.containsInArrayDb(reportKey, budgetApprovalsList)) { + BigInteger budgetApprovedVotes = (BigInteger) progressReportDetails.get(BUDGET_APPROVED_VOTES); + BigInteger budgetRejectedVotes = (BigInteger) progressReportDetails.get(BUDGET_REJECTED_VOTES); + int budgetVoteIndex = budgetVotersListIndices.at(progressReportPrefix).at(caller).getOrDefault(VOTE, 0); + if (budgetVoteIndex == APPROVE_) { + ArrayDBUtils.removeArrayItem(budgetApproveVoters.at(progressReportPrefix), caller); + ProgressReportDataDb.budgetApprovedVotes.at(progressReportPrefix).set(budgetApprovedVotes.subtract(voterStake)); + } else if (budgetVoteIndex == REJECT_) { + ArrayDBUtils.removeArrayItem(budgetRejectVoters.at(progressReportPrefix), caller); + ProgressReportDataDb.budgetRejectedVotes.at(progressReportPrefix).set(budgetRejectedVotes.subtract(voterStake)); + } else { + Context.revert(TAG + ": Choose option " + APPROVE + " or " + REJECT + " for budget adjustment"); + } + + } + approvedVotes = ProgressReportDataDb.approvedVotes.at(progressReportPrefix).getOrDefault(BigInteger.ZERO); + rejectedVotes = ProgressReportDataDb.rejectedVotes.at(progressReportPrefix).getOrDefault(BigInteger.ZERO); + + } + if (vote.equals(APPROVE)) { + ProgressReportDataDb.approveVoters.at(progressReportPrefix).add(caller); + votersIndexDb.set(VOTE, APPROVE_); + ProgressReportDataDb.approvedVotes.at(progressReportPrefix).set(approvedVotes.add(voterStake)); + } else if (vote.equals(REJECT)) { + ProgressReportDataDb.rejectVoters.at(progressReportPrefix).add(caller); + votersIndexDb.set(VOTE, REJECT_); + ProgressReportDataDb.rejectedVotes.at(progressReportPrefix).set(rejectedVotes.add(voterStake)); + + } else { + Context.revert(TAG + ": Choose option " + APPROVE + " or " + REJECT + " for budget adjustment"); + } + + if (ArrayDBUtils.containsInArrayDb(reportKey, budgetApprovalsList)) { + BigInteger budgetApprovedVotes = ProgressReportDataDb.budgetApprovedVotes.at(progressReportPrefix).getOrDefault(BigInteger.ZERO); + BigInteger budgetRejectedVotes = ProgressReportDataDb.budgetRejectedVotes.at(progressReportPrefix).getOrDefault(BigInteger.ZERO); + DictDB budgetVoteIndex = budgetVotersListIndices.at(progressReportPrefix).at(caller); + if (budgetAdjustmentVote.equals(APPROVE)) { + ProgressReportDataDb.budgetApproveVoters.at(progressReportPrefix).add(caller); + ProgressReportDataDb.budgetApprovedVotes.at(progressReportPrefix).set(budgetApprovedVotes.add(voterStake)); + budgetVoteIndex.set(VOTE, APPROVE_); + } else if (budgetAdjustmentVote.equals(REJECT)) { + ProgressReportDataDb.budgetRejectVoters.at(progressReportPrefix).add(caller); + ProgressReportDataDb.budgetRejectedVotes.at(progressReportPrefix).set(budgetRejectedVotes.add(voterStake)); + budgetVoteIndex.set(VOTE, REJECT_); + } else { + Context.revert(TAG + ": Choose option " + APPROVE + " or " + REJECT + " for budget adjustment"); + } + } + VotedSuccessfully(caller, "Proposal Vote for " + progressReportDetails.get(PROGRESS_REPORT_TITLE) + " Successful."); + + + swapBNUsdToken(); + } + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void vote_progress_report(String _ipfs_key, String _report_key, String _vote, String _vote_reason, @Optional String _budget_adjustment_vote, @Optional boolean _vote_change) { + voteProgressReport(_ipfs_key, _report_key, _vote, _vote_reason, _budget_adjustment_vote, _vote_change); + } + + @External(readonly = true) + public List getProposalsKeysByStatus(String status) { + Context.require(STATUS_TYPE.contains(status), TAG + ": Not a valid status"); + ArrayDB proposalStatus = this.proposalStatus.get(status); + return ArrayDBUtils.arrayDBtoList(proposalStatus); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List get_proposals_keys_by_status(String _status) { + return getProposalsKeysByStatus(_status); + } + + @External(readonly = true) + public int checkChangeVote(Address address, String ipfsHash, String proposalType) { + if (proposalType.equals(PROPOSAL)) { + String proposalPrefix = proposalPrefix(ipfsHash); + return ProposalDataDb.votersListIndex.at(proposalPrefix).at(address).getOrDefault(CHANGE_VOTE, NOT_VOTED); + } else if (proposalType.equals(PROGRESS_REPORTS)) { + String progressReportPrefix = progressReportPrefix(ipfsHash); + return votersListIndices.at(progressReportPrefix).at(address).getOrDefault(CHANGE_VOTE, NOT_VOTED); + } else { + return 0; + } + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public int check_change_vote(Address _address, String _ipfs_hash, String _proposal_type) { + return checkChangeVote(_address, _ipfs_hash, _proposal_type); + } + + @External(readonly = true) + public Map getProjectAmounts() { + List statusList = List.of(PENDING, ACTIVE, PAUSED, COMPLETED, DISQUALIFIED); + BigInteger pendingAmountIcx = BigInteger.ZERO; + BigInteger activeAmountIcx = BigInteger.ZERO; + BigInteger pausedAmountIcx = BigInteger.ZERO; + BigInteger completedAmountIcx = BigInteger.ZERO; + BigInteger disqualifiedAmountIcx = BigInteger.ZERO; + + BigInteger pendingAmountBnusd = BigInteger.ZERO; + BigInteger activeAmountBnusd = BigInteger.ZERO; + BigInteger pausedAmountBnusd = BigInteger.ZERO; + BigInteger completedAmountBnusd = BigInteger.ZERO; + BigInteger disqualifiedAmountBnusd = BigInteger.ZERO; + + for (int statusId = 0; statusId < statusList.size(); statusId++) { + BigInteger amountICX = BigInteger.ZERO; + BigInteger amountBnusd = BigInteger.ZERO; + + List proposalsKeysByStatus = this.getProposalsKeysByStatus(statusList.get(statusId)); + for (String proposalKey : proposalsKeysByStatus) { + String proposalPrefix = proposalPrefix(proposalKey); + BigInteger projectBudget = ProposalDataDb.totalBudget.at(proposalPrefix).getOrDefault(BigInteger.ZERO); + String token = ProposalDataDb.token.at(proposalPrefix).getOrDefault(""); + if (token.equals(ICX)) { + amountICX = amountICX.add(projectBudget); + } else { + amountBnusd = amountBnusd.add(projectBudget); + } + } + if (statusId == 0) { + pendingAmountIcx = amountICX; + pendingAmountBnusd = amountBnusd; + } else if (statusId == 1) { + activeAmountIcx = amountICX; + activeAmountBnusd = amountBnusd; + } else if (statusId == 2) { + pausedAmountIcx = amountICX; + pausedAmountBnusd = amountBnusd; + } else if (statusId == 3) { + completedAmountIcx = amountICX; + completedAmountBnusd = amountBnusd; + } else { + disqualifiedAmountIcx = amountICX; + disqualifiedAmountBnusd = amountBnusd; + } + + + } + return Map.of(statusList.get(0), Map.of(AMOUNT, Map.of(ICX, pendingAmountIcx, bnUSD, pendingAmountBnusd), "_count", proposalStatus.get(statusList.get(0)).size()), + statusList.get(1), Map.of(AMOUNT, Map.of(ICX, activeAmountIcx, bnUSD, activeAmountBnusd), "_count", proposalStatus.get(statusList.get(1)).size()), + statusList.get(2), Map.of(AMOUNT, Map.of(ICX, pausedAmountIcx, bnUSD, pausedAmountBnusd), "_count", proposalStatus.get(statusList.get(2)).size()), + statusList.get(3), Map.of(AMOUNT, Map.of(ICX, completedAmountIcx, bnUSD, completedAmountBnusd), "_count", proposalStatus.get(statusList.get(3)).size()), + statusList.get(4), Map.of(AMOUNT, Map.of(ICX, disqualifiedAmountIcx, bnUSD, disqualifiedAmountBnusd), "_count", proposalStatus.get(statusList.get(4)).size())); + + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_project_amounts() { + return getProjectAmounts(); + } + + @External(readonly = true) + public Map getSponsorsRecord() { + Map sponsorsDict = new HashMap<>(); + int size = sponsors.size(); + for (int i = 0; i < size; i++) { + Address sponsorAddress = sponsors.get(i); + sponsorsDict.put(sponsorAddress.toString(), sponsorProjects.at(sponsorAddress).size()); + } + return sponsorsDict; + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_sponsors_record() { + return getSponsorsRecord(); + } + + @External + public void updatePeriod() { + checkMaintenance(); + BigInteger currentBlock = BigInteger.valueOf(Context.getBlockHeight()); + PeriodController period = new PeriodController(); + BigInteger nextBlock = period.nextBlock.get(); + PReps pReps = new PReps(); + if (currentBlock.compareTo(nextBlock) >= 0) { + if (period.periodName.get().equals(APPLICATION_PERIOD)) { + period.periodName.set(VOTING_PERIOD); + period.periodCount.set(period.periodCount.get() + 1); + period.previousPeriodName.set(APPLICATION_PERIOD); + period.nextBlock.set(nextBlock.add(BLOCKS_DAY_COUNT.multiply(DAY_COUNT))); + updateApplicationResult(); + + period.updatePeriodIndex.set(0); + setPreps(); + snapshotDelegation(); + + int activeProposalCount = pending.size() + waitingProgressReports.size(); + swapCount.set(activeProposalCount + pReps.validPreps.size()); + + } else { + Integer updateIndex = period.updatePeriodIndex.get(); + if (updateIndex == 0) { + period.periodName.set(TRANSITION_PERIOD); + period.previousPeriodName.set(APPLICATION_PERIOD); + period.updatePeriodIndex.set(updateIndex + 1); + updateProposalsResult(); + + PeriodUpdate("Period Update State 1/4. Period Updated to Transition Period. " + + "After all the calculations are completed, " + + "Period will change to " + APPLICATION_PERIOD); + } else if (updateIndex == 1) { + checkProgressReportSubmission(); + period.updatePeriodIndex.set(updateIndex + 1); + PeriodUpdate("Period Update State 2/4. Progress Reports Checks Completed."); + } else if (updateIndex == 2) { + updateProgressReportResult(); + period.updatePeriodIndex.set(updateIndex + 1); + PeriodUpdate("Period Update State 3/4. Progress Reports Calculations Completed."); + } else { + SetterGetter setterGetter = new SetterGetter(); + updateDenylistPreps(); + period.nextBlock.set(nextBlock.add(BLOCKS_DAY_COUNT.multiply(DAY_COUNT))); + period.periodName.set(APPLICATION_PERIOD); + period.previousPeriodName.set(VOTING_PERIOD); + PeriodUpdate("Period Update State 4/4. Period Successfully Updated to Application Period."); + setPreps(); + + int activeProposalCount = active.size() + paused.size(); + swapCount.set(activeProposalCount + activeProposalCount * pReps.validPreps.size()); + callScore(setterGetter.cpfScore.get(), "reset_swap_state"); + + ArrayDBUtils.clearArrayDb(budgetApprovalsList); + ArrayDBUtils.clearArrayDb(activeProposals); + ArrayDBUtils.clearArrayDb(priorityVotedPreps); + + burn(proposalFees.get(), null); + proposalFees.set(BigInteger.ZERO); + } + + } + } + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External + public void update_period() { + updatePeriod(); + } + + private void updateDenylistPreps() { + PReps pReps = new PReps(); + for (int i = 0; i < pReps.inactivePreps.size(); i++) { + Address prep = pReps.inactivePreps.get(i); + ArrayDBUtils.removeArrayItem(pReps.registeredPreps, prep); + pReps.denylist.add(prep); + int count = pReps.prepsDenylistStatus.getOrDefault(prep.toString(), 0) + 1; + pReps.prepsDenylistStatus.set(prep.toString(), Math.min(count, 3)); + PRepPenalty(prep, "P-Rep added to Denylist."); + + } + ArrayDBUtils.clearArrayDb(pReps.inactivePreps); + } + + /*** + Calculate votes for the progress reports and update the status and get the Installment and Sponsor + Reward is the progress report is accepted. + :return: + ***/ + + private void updateProgressReportResult() { + List waiting_progress_reports = arrayDBtoList(this.waitingProgressReports); + PReps pReps = new PReps(); + List
_main_preps_list = arrayDBtoList(pReps.validPreps); + + for (String _reports : waiting_progress_reports) { + Map _report_result = getProgressReportDetails(_reports); + + String _ipfs_hash = (String) _report_result.get(IPFS_HASH); + String proposal_prefix = proposalPrefix(_ipfs_hash); + String progressPrefix = progressReportPrefix(_reports); + Map _proposal_details = getProposalDetails(_ipfs_hash); + + submitProgressReport.at(proposal_prefix).set(Boolean.FALSE); + + String _proposal_status = (String) _proposal_details.get(STATUS); + int _approved_reports_count = (int) _proposal_details.get(APPROVED_REPORTS); + Address _sponsor_address = (Address) _proposal_details.get(SPONSOR_ADDRESS); + Address _contributor_address = (Address) _proposal_details.get(CONTRIBUTOR_ADDRESS); + boolean _budget_adjustment = (boolean) _report_result.get(BUDGET_ADJUSTMENT); + BigInteger _sponsor_deposit_amount = (BigInteger) _proposal_details.get(SPONSOR_DEPOSIT_AMOUNT); + String flag = (String) _proposal_details.get("token"); + + int _approve_voters = (int) _report_result.get(APPROVE_VOTERS); + BigInteger _approved_votes = (BigInteger) _report_result.get(APPROVED_VOTES); + BigInteger _total_votes = (BigInteger) _report_result.get(TOTAL_VOTES); + int _total_voters = (int) _report_result.get(TOTAL_VOTERS); + +// checking which prep(s) did not vote the progress report + checkInactivePreps(ProgressReportDataDb.votersList.at(progressPrefix)); + +// If a progress report have any budget_adjustment, then it checks the budget adjustment first + if (_budget_adjustment) { + updateBudgetAdjustments(_reports); + } + + int _project_duration = projectDuration.at(proposal_prefix).getOrDefault(0); + String updated_status; + double votersRatio = (double) _approve_voters / _total_voters; + if (_total_voters == 0 || _total_votes.equals(BigInteger.ZERO) || _main_preps_list.size() < MINIMUM_PREPS) { + updateProgressReportStatus(_reports, PROGRESS_REPORT_REJECTED); + updated_status = PROGRESS_REPORT_REJECTED; + } else if (votersRatio >= MAJORITY && (_approved_votes.doubleValue() / _total_votes.doubleValue()) >= MAJORITY) { + + updateProgressReportStatus(_reports, APPROVED); + updated_status = APPROVED; + _approved_reports_count += 1; + + if (_approved_reports_count == _project_duration) { + updateProposalStatus(_ipfs_hash, COMPLETED); +// Transfer the Sponsor - Bond back to the Sponsor P - Rep after the project is completed. + this.sponsorBondReturn.at(_sponsor_address.toString()).set(flag, this.sponsorBondReturn.at(_sponsor_address.toString()).getOrDefault(flag, BigInteger.ZERO).add(_sponsor_deposit_amount)); + sponsorDepositStatus.at(proposal_prefix).set(BOND_RETURNED); + SponsorBondReturned(_sponsor_address, + _sponsor_deposit_amount + " " + flag + " returned to sponsor address."); + } else if (_proposal_status.equals(PAUSED)) { + updateProposalStatus(_ipfs_hash, ACTIVE); + } + ProposalDataDb.approvedReports.at(proposal_prefix).set(_approved_reports_count); +// Request CPS Treasury to add some installments amount to the contributor address + callScore(getCpsTreasuryScore(), "send_installment_to_contributor", _ipfs_hash); +// Request CPS Treasury to add some sponsor reward amount to the sponsor address + callScore(getCpsTreasuryScore(), "send_reward_to_sponsor", _ipfs_hash); + + } else { + updateProgressReportStatus(_reports, PROGRESS_REPORT_REJECTED); + updated_status = PROGRESS_REPORT_REJECTED; + } + + if (updated_status.equals(PROGRESS_REPORT_REJECTED)) { + if (_proposal_status.equals(ACTIVE)) { + updateProposalStatus(_ipfs_hash, PAUSED); + } else if (_proposal_status.equals(PAUSED)) { + updateProposalStatus(_ipfs_hash, DISQUALIFIED); + callScore(getCpsTreasuryScore(), "disqualify_project", _ipfs_hash); + + removeContributor(_contributor_address, _ipfs_hash); + removeSponsor(_sponsor_address, _ipfs_hash); + + sponsorDepositStatus.at(proposal_prefix).set(BOND_CANCELLED); + +// Transferring the sponsor bond deposit to CPF after the project being disqualified + disqualifyProject(_sponsor_address, _sponsor_deposit_amount, flag); + } + } + } + } + + /*** + Check if all active and paused proposals submits the progress report + :return: + ***/ + private void checkProgressReportSubmission() { + for (int i = 0; i < activeProposals.size(); i++) { + String _ipfs_hash = activeProposals.get(i); + String proposalPrefix = proposalPrefix(_ipfs_hash); + + Map _proposal_details = getProposalDetails(_ipfs_hash); + String _proposal_status = (String) _proposal_details.get(STATUS); + Address _sponsor_address = (Address) _proposal_details.get(SPONSOR_ADDRESS); + Address _contributor_address = (Address) _proposal_details.get(CONTRIBUTOR_ADDRESS); + String flag = (String) _proposal_details.get("token"); + + if (!ProposalDataDb.submitProgressReport.at(proposalPrefix).getOrDefault(Boolean.FALSE)) { + if (_proposal_status.equals(ACTIVE)) { + updateProposalStatus(_ipfs_hash, PAUSED); + } else if (_proposal_status.equals(PAUSED)) { + updateProposalStatus(_ipfs_hash, DISQUALIFIED); + callScore(getCpsTreasuryScore(), "disqualify_project", _ipfs_hash); + + + removeContributor(_contributor_address, _ipfs_hash); + removeSponsor(_sponsor_address, _ipfs_hash); + + sponsorDepositStatus.at(proposalPrefix).set(BOND_CANCELLED); + BigInteger _sponsor_deposit_amount = (BigInteger) _proposal_details.get(SPONSOR_DEPOSIT_AMOUNT); + +// Transferring the sponsor bond deposit to CPF after the project being disqualified + disqualifyProject(_sponsor_address, _sponsor_deposit_amount, flag); + } + } + } + } + + /*** + Update the budget amount and added month time if the budget adjustment application is approved by majority. + :param _budget_key: report hash of the progress report + :type _budget_key: str + :return: + ***/ + private void updateBudgetAdjustments(String _budget_key) { + Map _report_result = getProgressReportDetails(_budget_key); + String _prefix = progressReportPrefix(_budget_key); + + Map _vote_result = getBudgetAdjustmentVoteResult(_budget_key); + int _approve_voters = (int) _vote_result.get(APPROVE_VOTERS); + int _total_voters = (int) _vote_result.get(TOTAL_VOTERS); + BigInteger _approved_votes = (BigInteger) _vote_result.get(APPROVED_VOTES); + BigInteger _total_votes = (BigInteger) _vote_result.get(TOTAL_VOTES); + PReps pReps = new PReps(); + double votersRatio = (double) _approve_voters / _total_voters; + if (_total_voters == 0 || _total_votes.equals(BigInteger.ZERO) || pReps.validPreps.size() < MINIMUM_PREPS) { + budgetAdjustmentStatus.at(_prefix).set(REJECTED); + } else if (votersRatio >= MAJORITY && (_approved_votes.doubleValue() / _total_votes.doubleValue()) >= MAJORITY) { + String _ipfs_hash = (String) _report_result.get(IPFS_HASH); + String proposal_prefix = proposalPrefix(_ipfs_hash); + String token_flag = token.at(proposal_prefix).getOrDefault(""); + + int _period_count = projectDuration.at(proposal_prefix).getOrDefault(0); + BigInteger _total_budget = totalBudget.at(proposal_prefix).getOrDefault(BigInteger.ZERO); + int _additional_duration = (int) _report_result.get(ADDITIONAL_DURATION); + BigInteger _additional_budget = (BigInteger) _report_result.get(ADDITIONAL_BUDGET); + + projectDuration.at(proposal_prefix).set(_period_count + _additional_duration); + totalBudget.at(proposal_prefix).set(_total_budget.add(_additional_budget)); + budgetAdjustmentStatus.at(_prefix).set(APPROVED); + + +// After the budget adjustment is approved, Request new added fund to CPF + callScore(getCpfTreasuryScore(), "update_proposal_fund", _ipfs_hash, token_flag, _additional_budget, _additional_duration); + } else { + budgetAdjustmentStatus.at(_prefix).set(REJECTED); + } + } + + private void updateProposalsResult() { + BigInteger distributionAmount = getRemainingFund().get(bnUSD); + List proposals = sortPriorityProposals(); + PReps pReps = new PReps(); + + for (String proposal : proposals) { + Map proposalDetails = getProposalDetails(proposal); + Address sponsorAddress = (Address) proposalDetails.get(SPONSOR_ADDRESS); + Address contributorAddress = (Address) proposalDetails.get(CONTRIBUTOR_ADDRESS); + BigInteger totalBudget = (BigInteger) proposalDetails.get(TOTAL_BUDGET); + int projectDuration = (int) proposalDetails.get(PROJECT_DURATION); + BigInteger sponsorDepositAmount = (BigInteger) proposalDetails.get(SPONSOR_DEPOSIT_AMOUNT); + int approvedVoters = (int) proposalDetails.get(APPROVE_VOTERS); + BigInteger approvedVotes = (BigInteger) proposalDetails.get(APPROVED_VOTES); + BigInteger totalVotes = (BigInteger) proposalDetails.get(TOTAL_VOTES); + int totalVoters = (int) proposalDetails.get(TOTAL_VOTERS); + String flag = (String) proposalDetails.get(TOKEN); + String updatedStatus; + String proposalPrefix = proposalPrefix(proposal); + + checkInactivePreps(ProposalDataDb.votersList.at(proposalPrefix)); + + double voters_ratio = 0; + if (totalVoters != 0) { + voters_ratio = (double) approvedVoters / totalVoters; + } + if (totalVoters == 0 || totalVotes.equals(BigInteger.ZERO) || pReps.validPreps.size() < MINIMUM_PREPS) { + updateProposalStatus(proposal, REJECTED); + updatedStatus = REJECTED; + } else if ((voters_ratio) >= MAJORITY && + (approvedVotes.doubleValue() / totalVotes.doubleValue()) >= MAJORITY) { + if (totalBudget.multiply(BigInteger.valueOf(102)).divide(BigInteger.valueOf(100)). + compareTo(distributionAmount) < 0) { + updateProposalStatus(proposal, ACTIVE); + updatedStatus = ACTIVE; + sponsors.add(sponsorAddress); + sponsorProjects.at(sponsorAddress).add(proposal); + ProposalDataDb.sponsorDepositStatus.at(proposalPrefix).set(BOND_APPROVED); + callScore(getCpfTreasuryScore(), "transfer_proposal_fund_to_cps_treasury", + proposal, projectDuration, sponsorAddress, contributorAddress, flag, totalBudget); + distributionAmount = distributionAmount.subtract(totalBudget); + + } else { + updatedStatus = PENDING; + ProposalDataDb.totalVoters.at(proposalPrefix).set(0); + ProposalDataDb.totalVotes.at(proposalPrefix).set(BigInteger.ZERO); + ProposalDataDb.approvedVotes.at(proposalPrefix).set(BigInteger.ZERO); + ProposalDataDb.rejectedVotes.at(proposalPrefix).set(BigInteger.ZERO); + + ArrayDBUtils.clearArrayDb(ProposalDataDb.rejectVoters.at(proposalPrefix)); + ArrayDBUtils.clearArrayDb(ProposalDataDb.approveVoters.at(proposalPrefix)); + ArrayDBUtils.clearArrayDb(ProposalDataDb.votersList.at(proposalPrefix)); + ArrayDBUtils.clearArrayDb(ProposalDataDb.votersReasons.at(proposalPrefix)); + + for (int i = 0; i < pReps.validPreps.size(); i++) { + Address prep = pReps.validPreps.get(i); + BranchDB> prepVoteChange = ProposalDataDb.votersListIndex.at(proposalPrefix); + prepVoteChange.at(prep).set(VOTE, 0); + prepVoteChange.at(prep).set(INDEX, 0); + prepVoteChange.at(prep).set(CHANGE_VOTE, 0); + } + + } + } else { + updateProposalStatus(proposal, REJECTED); + updatedStatus = REJECTED; + } + + proposalRank.set(proposal, null); + if (updatedStatus.equals(REJECTED)) { + removeContributor(contributorAddress, proposal); + sponsorDepositStatus.at(proposalPrefix).set(BOND_RETURNED); + BigInteger halfSubmissionFee = BigInteger.valueOf(APPLICATION_FEE / 2).multiply(EXA); + Context.transfer(contributorAddress, halfSubmissionFee); + proposalFees.set(proposalFees.get().subtract(halfSubmissionFee)); + + BigInteger sponsorBondAmount = sponsorBondReturn.at(sponsorAddress.toString()).getOrDefault(flag, BigInteger.ZERO); + sponsorBondReturn.at(sponsorAddress.toString()).set(flag, sponsorBondAmount.add(sponsorDepositAmount)); + SponsorBondReturned(sponsorAddress, + sponsorDepositAmount + " returned to sponsor address."); + } + if (proposals.size() > 0) { + checkInactivePreps(priorityVotedPreps); + } + } + } + + private void updateProposalStatus(String proposalHash, String propStatus) { + String proposalPrefix = proposalPrefix(proposalHash); + String currentStatus = ProposalDataDb.status.at(proposalPrefix).get(); + ProposalDataDb.timestamp.at(proposalPrefix).set(BigInteger.valueOf(Context.getBlockTimestamp())); + ProposalDataDb.status.at(proposalPrefix).set(propStatus); + + ArrayDB proposalStatus = this.proposalStatus.get(currentStatus); + ArrayDBUtils.removeArrayItem(proposalStatus, proposalHash); + this.proposalStatus.get(propStatus).add(proposalHash); + } + + private void updateProgressReportStatus(String progressHash, String progressStatus) { + String progressPrefix = progressReportPrefix(progressHash); + String currentStatus = ProgressReportDataDb.status.at(progressPrefix).get(); + ProgressReportDataDb.timestamp.at(progressPrefix).set(BigInteger.valueOf(Context.getBlockTimestamp())); + ProgressReportDataDb.status.at(progressPrefix).set(progressStatus); + + ArrayDB progressReportStatus = this.progressReportStatus.get(currentStatus); + ArrayDBUtils.removeArrayItem(progressReportStatus, progressHash); + this.progressReportStatus.get(progressStatus).add(progressHash); + } + + private void checkInactivePreps(ArrayDB
prepList) { + PReps pReps = new PReps(); + for (int i = 0; i < pReps.validPreps.size(); i++) { + Address prep = pReps.validPreps.get(i); + if (!containsInArrayDb(prep, prepList) && !containsInArrayDb(prep, pReps.inactivePreps)) { + pReps.inactivePreps.add(prep); + } + } + } + + private Map getProposalDetails(String proposal) { + if (proposalKeyExists(proposal)) { + return getDataFromProposalDB(proposalPrefix(proposal)); + } + return Map.of(); + + } + + private void addNewProgressReportKey(String ipfsHash, String reportHash) { + String prefix = proposalPrefix(ipfsHash); + if (!containsInArrayDb(reportHash, progressReports.at(prefix))) { + progressReports.at(prefix).add(reportHash); + } + } + + @Override + @External(readonly = true) + public Map getProposalDetails(String status, @Optional Address walletAddress, @Optional int startIndex) { + if (!STATUS_TYPE.contains(status)) { + return Map.of(MESSAGE, "Not a valid _status."); + } + List proposalsList = new ArrayList<>(); + List proposalKeys = getProposalsKeysByStatus(status); + + if (startIndex < 0) { + startIndex = 0; + } + int endIndex = startIndex + 10; + + int count = proposalKeys.size(); + if (endIndex > count) { + endIndex = count; + } + + for (int i = startIndex; i < endIndex; i++) { + String proposalKey = proposalKeys.get(i); + Map proposalDetails = getProposalDetails(proposalKey); + String propStatus = (String) proposalDetails.get(STATUS); + + if (status.equals(SPONSOR_PENDING)) { + Address wallet = (Address) proposalDetails.get(CONTRIBUTOR_ADDRESS); + if (wallet.equals(walletAddress)) { + proposalsList.add(proposalDetails); + } + } else if (propStatus.equals(status)) { + proposalsList.add(proposalDetails); + } + + } + return Map.of(DATA, proposalsList, COUNT, count); + + } + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_proposal_details(String _status, @Optional Address _wallet_address, @Optional int _start_index) { + return getProposalDetails(_status, _wallet_address, _start_index); + } + + @External(readonly = true) + public Map getProposalDetailsByHash(String ipfsKey) { + Map proposalDetails = new HashMap<>(); + String prefix = proposalPrefix(ipfsKey); + proposalDetails.putAll(getProposalDetails(ipfsKey)); + proposalDetails.put(SPONSOR_VOTE_REASON, ProposalDataDb.sponsorVoteReason.at(prefix).getOrDefault("")); + return proposalDetails; + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_proposal_details_by_hash(String _ipfs_key) { + return getProposalDetailsByHash(_ipfs_key); + } + + private Map getProgressReportDetails(String progressKey) { + return getDataFromProgressReportDB(progressReportPrefix(progressKey)); + } + + @Override + @External(readonly = true) + public Map getProgressReports(String status, @Optional int startIndex) { + if (!PROGRESS_REPORT_STATUS_TYPE.contains(status)) { + return Map.of(MESSAGE, "Not a valid _status."); + } + + List progressReportList = new ArrayList<>(); + ArrayDB progressReportKeys = this.progressReportStatus.get(status); + + if (startIndex < 0) { + startIndex = 0; + } + int endIndex = startIndex + 10; + + int count = progressReportKeys.size(); + if (endIndex > count) { + endIndex = count; + } + + for (int i = startIndex; i < endIndex; i++) { + String progressReportKey = progressReportKeys.get(i); + Map progressReportDetails = this.getProgressReportDetails(progressReportKey); + String progressStatus = (String) progressReportDetails.get(STATUS); + + if (progressStatus.equals(status)) { + progressReportList.add(progressReportDetails); + } + } + return Map.of(DATA, progressReportList, COUNT, progressReportList.size()); + } + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_progress_reports(String _status, @Optional int _start_index) { + return getProgressReports(_status, _start_index); + } + + /*** + Returns all the progress reports for a specific project + :param _report_key : project key i.e. progress report ipfs hash + :type _report_key : str + :return : List of all progress report with status + :rtype : dict + ***/ + @External(readonly = true) + public Map getProgressReportsByHash(String reportKey) { + if (progressKeyExists(reportKey)) { + return getProgressReportDetails(reportKey); + } + return Map.of(); + + + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_progress_reports_by_hash(String _report_key) { + return getProgressReportsByHash(_report_key); + } + + @External(readonly = true) + public Map getProgressReportsByProposal(String ipfsKey) { + String proposalPrefix = proposalPrefix(ipfsKey); + ArrayDB reportKeys = ProposalDataDb.progressReports.at(proposalPrefix); + + List> progressReportList = new ArrayList<>(); + + for (int i = 0; i < reportKeys.size(); i++) { + Map progressReportDetails = this.getProgressReportDetails(reportKeys.get(i)); + progressReportList.add(progressReportDetails); + } + return Map.of(DATA, progressReportList, COUNT, progressReportList.size()); + } + + @Override + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_progress_reports_by_proposal(String _ipfs_key) { + return getProgressReportsByProposal(_ipfs_key); + } + + @Override + @External(readonly = true) + public Map getSponsorsRequests(String status, Address sponsorAddress, @Optional int startIndex) { + if (startIndex < 0) { + startIndex = 0; + } + int endIndex = startIndex + 10; + if (!ArrayDBUtils.containsInList(status, List.of(APPROVED, SPONSOR_PENDING, REJECTED, DISQUALIFIED))) { + return Map.of(MESSAGE, "Not a valid _status."); + } + List proposalKeys = new ArrayList<>(); + List sponsorRequests = new ArrayList<>(); + String prefix; + + if (status.equals(APPROVED)) { + proposalKeys = arrayDBtoList(this.sponsorProjects.at(sponsorAddress)); + List pendingProposals = getProposalsKeysByStatus(PENDING); + for (String ipfsKey : pendingProposals) { + prefix = proposalPrefix(ipfsKey); + if (ProposalDataDb.sponsorAddress.at(prefix).get().equals(sponsorAddress)) { + proposalKeys.add(ipfsKey); + } + } + } else { + List proposalKeysList = getProposalsKeysByStatus(status); + for (String ipfsKey : proposalKeysList) { + prefix = proposalPrefix(ipfsKey); + if (ProposalDataDb.sponsorAddress.at(prefix).get().equals(sponsorAddress)) { + proposalKeys.add(ipfsKey); + } + } + } + int count = proposalKeys.size(); + if (endIndex > count) { + endIndex = count; + } + + BigInteger sponsorAmountICX = BigInteger.ZERO, sponsorAmountBnusd = BigInteger.ZERO; + + for (int i = startIndex; i < endIndex; i++) { + String proposalKey = proposalKeys.get(i); + String proposalPrefix = proposalPrefix(proposalKey); + String sponsorDepositStatus = ProposalDataDb.sponsorDepositStatus.at(proposalPrefix).getOrDefault(""); + Map proposalDetails = getProposalDetails(proposalKey); + sponsorRequests.add(proposalDetails); + if (sponsorDepositStatus.equals(BOND_APPROVED)) { + String token = (String) proposalDetails.get(TOKEN); + BigInteger sponsorDepositAmount = (BigInteger) proposalDetails.get(SPONSOR_DEPOSIT_AMOUNT); + if (token.equals(ICX)) { + sponsorAmountICX = sponsorAmountICX.add(sponsorDepositAmount); + } else if (token.equals(bnUSD)) { + sponsorAmountBnusd = sponsorAmountBnusd.add(sponsorDepositAmount); + } + } + } + + return Map.of(DATA, sponsorRequests, COUNT, proposalKeys.size(), + SPONSOR_DEPOSIT_AMOUNT, Map.of(ICX, sponsorAmountICX, bnUSD, sponsorAmountBnusd)); + } + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public Map get_sponsors_requests(String _status, Address _sponsor_address, @Optional int _start_index) { + return getSponsorsRequests(_status, _sponsor_address, _start_index); + } + + /*** + Returns remaining projects and progress reports to vote on the current voting period + :param _project_type: "proposal" or "progress_report" which type, remaining votes need to be checked + :type _project_type: str + :param _wallet_address: Wallet Address of the P-Rep + :type _wallet_address: str + :return: list of details of proposal or progress report what they need to vote on the same voting period + ***/ + @External(readonly = true) + public List> get_remaining_project(String _project_type, Address _wallet_address) { + List> _remaining_proposals = new ArrayList<>(); + List> _remaining_progress_report = new ArrayList<>(); + if (_project_type.equals(PROPOSAL)) { + List _proposal_keys = getProposalsKeysByStatus(PENDING); + for (String _ipfs_key : _proposal_keys) { + String prefix = proposalPrefix(_ipfs_key); + + if (!containsInArrayDb(_wallet_address, ProposalDataDb.votersList.at(prefix))) { + Map _proposal_details = getProposalDetails(_ipfs_key); + _remaining_proposals.add(_proposal_details); + } + } + return _remaining_proposals; + } + + if (_project_type.equals(PROGRESS_REPORTS)) { + for (int i = 0; i < waitingProgressReports.size(); i++) { + String reportHash = waitingProgressReports.get(i); + String prefix = progressReportPrefix(reportHash); + + if (!containsInArrayDb(_wallet_address, ProgressReportDataDb.votersList.at(prefix))) { + Map progressReportDetails = getProgressReportDetails(reportHash); + _remaining_progress_report.add(progressReportDetails); + } + } + return _remaining_progress_report; + } + return List.of(Map.of("", "")); + } + + + /*** + Get vote results by proposal + :param _ipfs_key : proposal ipfs key + :type _ipfs_key : str + :return: Vote status of given _ipfs_key + :rtype : dict + ***/ + @External(readonly = true) + public Map getVoteResult(String ipfsKey) { + String prefix = proposalPrefix(ipfsKey); + + ArrayDB
_voters_list = ProposalDataDb.votersList.at(prefix); + ArrayDB
approve_voters = ProposalDataDb.approveVoters.at(prefix); + ArrayDB
reject_voters = ProposalDataDb.rejectVoters.at(prefix); + List> _vote_status = new ArrayList<>(); + String vote; + for (int i = 0; i < _voters_list.size(); i++) { + Address voter = _voters_list.get(i); + if (containsInArrayDb(voter, approve_voters)) { + vote = APPROVE; + } else if (containsInArrayDb(voter, reject_voters)) { + vote = REJECT; + } else { + vote = ABSTAIN; + } + + String reason = ProposalDataDb.votersReasons.at(prefix).get(i); + if (reason == null) { + reason = ""; + } + + + Map _voters = Map.of(ADDRESS, voter, + PREP_NAME, getPrepName(voter), + VOTE_REASON, reason, + VOTE, vote); + _vote_status.add(_voters); + } + + return Map.of(DATA, _vote_status, APPROVE_VOTERS, approve_voters.size(), + REJECT_VOTERS, reject_voters.size(), + TOTAL_VOTERS, ProposalDataDb.totalVoters.at(prefix).getOrDefault(0), + APPROVED_VOTES, ProposalDataDb.approvedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + REJECTED_VOTES, ProposalDataDb.rejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + TOTAL_VOTES, ProposalDataDb.totalVotes.at(prefix).getOrDefault(BigInteger.ZERO)); + } + + @Override + @External(readonly = true) + @Deprecated(since = "JAVA translation", forRemoval = true) + public Map get_vote_result(String _ipfs_key) { + return getVoteResult(_ipfs_key); + } + + /*** + Get vote results by progress report + :param reportKey : progress report ipfs key + :type reportKey : str + :return: Vote status of given reportKey + :rtype : dict + ***/ + @External(readonly = true) + public Map getProgressReportResult(String reportKey) { + String prefix = progressReportPrefix(reportKey); + + + ArrayDB
_voters_list = ProgressReportDataDb.votersList.at(prefix); + ArrayDB
_approved_voters_list = ProgressReportDataDb.approveVoters.at(prefix); + ArrayDB
_rejected_voters_list = ProgressReportDataDb.rejectVoters.at(prefix); + List> _vote_status = new ArrayList<>(); + String vote; +// Differentiating the P-Rep(s) votes according to their votes + for (int i = 0; i < _voters_list.size(); i++) { + Address voter = _voters_list.get(i); + if (containsInArrayDb(voter, _approved_voters_list)) { + vote = APPROVE; + } else if (containsInArrayDb(voter, _rejected_voters_list)) { + vote = REJECT; + } else { + vote = "not voted"; + } + String reason = ProgressReportDataDb.votersReasons.at(prefix).get(i); + if (reason == null) { + reason = ""; + } + Map _voters = Map.of(ADDRESS, voter, + PREP_NAME, getPrepName(voter), + VOTE_REASON, reason, + VOTE, vote); + _vote_status.add(_voters); + } + + return Map.of(DATA, _vote_status, APPROVE_VOTERS, _approved_voters_list.size(), + REJECT_VOTERS, _rejected_voters_list.size(), + TOTAL_VOTERS, ProgressReportDataDb.totalVoters.at(prefix).getOrDefault(0), + APPROVED_VOTES, ProgressReportDataDb.approvedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + REJECTED_VOTES, ProgressReportDataDb.rejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + TOTAL_VOTES, ProgressReportDataDb.totalVotes.at(prefix).getOrDefault(BigInteger.ZERO)); + } + + @Override + @External(readonly = true) + @Deprecated(since = "JAVA translation", forRemoval = true) + public Map get_progress_report_result(String _report_key) { + return getProgressReportResult(_report_key); + } + + /*** + Get budget adjustment vote results + :param _report_key : progress report ipfs key + :type _report_key : str + :return: Vote status of given _report_key + :rtype : dict + ***/ + + @External(readonly = true) + public Map getBudgetAdjustmentVoteResult(String reportKey) { + String prefix = progressReportPrefix(reportKey); + + ArrayDB
_voters_list = ProgressReportDataDb.votersList.at(prefix); + ArrayDB
_approved_voters_list = budgetApproveVoters.at(prefix); + ArrayDB
_rejected_voters_list = budgetRejectVoters.at(prefix); + List> _vote_status = new ArrayList<>(); + String vote; + for (int i = 0; i < _voters_list.size(); i++) { + Address voter = _voters_list.get(i); + if (containsInArrayDb(voter, _approved_voters_list)) { + vote = APPROVE; + } else if (containsInArrayDb(voter, _rejected_voters_list)) { + vote = REJECT; + } else { + vote = "not voted"; + } + Map _voters = Map.of( + ADDRESS, voter, + PREP_NAME, getPrepName(voter), + VOTE, vote); + _vote_status.add(_voters); + } + + return Map.of(DATA, _vote_status, APPROVE_VOTERS, _approved_voters_list.size(), + REJECT_VOTERS, _rejected_voters_list.size(), + TOTAL_VOTERS, ProgressReportDataDb.totalVoters.at(prefix).getOrDefault(0), + APPROVED_VOTES, budgetApprovedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + REJECTED_VOTES, budgetRejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO), + TOTAL_VOTES, ProgressReportDataDb.totalVotes.at(prefix).getOrDefault(BigInteger.ZERO)); + } + + @Override + @External(readonly = true) + @Deprecated(since = "JAVA translation", forRemoval = true) + public Map get_budget_adjustment_vote_result(String _report_key) { + return getBudgetAdjustmentVoteResult(_report_key); + } + + + private void snapshotDelegation() { + BigInteger maxDelegation = BigInteger.ZERO; + PReps pReps = new PReps(); + for (int i = 0; i < pReps.validPreps.size(); i++) { + Address prep = pReps.validPreps.get(i); + BigInteger stake = getStake(prep); + delegationSnapshot.set(prep, stake); + if (stake.compareTo(maxDelegation) > 0) { + maxDelegation = stake; + } + } + this.maxDelegation.set(maxDelegation); + } + + private void updateApplicationResult() { + PReps pReps = new PReps(); + PeriodController period = new PeriodController(); + if (pReps.validPreps.size() < MINIMUM_PREPS) { + period.periodName.set(APPLICATION_PERIOD); + PeriodUpdate("Period Updated back to Application Period due to less Registered P-Reps Count"); + + } else if (getProposalsKeysByStatus(PENDING).size() == 0 && this.progressReportStatus.get(WAITING).size() == 0 && activeProposals.size() + paused.size() == 0) { + createActiveProposalDb(); + checkProgressReportSubmission(); + period.periodName.set(APPLICATION_PERIOD); + PeriodUpdate("Period Updated back to Application Period due not enough " + + "Voting Proposals or Progress Reports."); + } else if (pending.size() == 0 && waitingProgressReports.size() == 0 && activeProposals.size() + paused.size() > 0) { + createActiveProposalDb(); + checkProgressReportSubmission(); + period.periodName.set(APPLICATION_PERIOD); + PeriodUpdate("Period Updated back to Application Period due not enough " + + "Voting Proposals or Progress Reports."); + + } else { + createActiveProposalDb(); + PeriodUpdate("Period Updated to Voting Period"); + } + } + + private void createActiveProposalDb() { + for (int i = 0; i < active.size(); i++) { + String proposal = active.get(i); + if (!ArrayDBUtils.containsInArrayDb(proposal, activeProposals)) { + activeProposals.add(proposal); + } + } + for (int i = 0; i < paused.size(); i++) { + String proposal = paused.get(i); + if (!ArrayDBUtils.containsInArrayDb(proposal, activeProposals)) { + activeProposals.add(proposal); + } + } + } + + private void payPrepPenalty(Address from, BigInteger _value) { + checkMaintenance(); + updatePeriod(); + PReps pReps = new PReps(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(APPLICATION_PERIOD), + TAG + " Penalty can only be paid on Application Period"); + Context.require(ArrayDBUtils.containsInArrayDb(from, pReps.denylist), + TAG + " " + from + " not in denylist."); + + BigInteger penaltyAmount = getPenaltyAmount(from); + Context.require(penaltyAmount.equals(_value), + "Please pay Penalty amount of" + penaltyAmount + " to register as a P-Rep."); + SetterGetter setterGetter = new SetterGetter(); + ArrayDBUtils.removeArrayItem(pReps.denylist, from); + pReps.registeredPreps.add(from); + pReps.validPreps.add(from); + burn(_value, setterGetter.balancedDollar.get()); + PRepPenalty(from, _value + " bnUSD Penalty Received. P-Rep removed from Denylist."); + + } + + @Override + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + Context.require(Context.getCaller().equals(getBnusdScore()), TAG + " Only bnUSD token accepted."); + + String unpacked_data = new String(_data); + JsonObject transferData = Json.parse(unpacked_data).asObject(); + String methodName = transferData.get("method").asString(); + JsonObject paramsName = transferData.get("params").asObject(); + + if (methodName.equals("sponsor_vote")) { + String ipfsKey = paramsName.get(IPFS_HASH).asString(); + String vote = paramsName.get(VOTE).asString(); + String voteReason = paramsName.get(VOTE_REASON).asString(); + sponsorVote(ipfsKey, vote, voteReason, _from, _value); + } else if (methodName.equals("pay_prep_penalty")) { + payPrepPenalty(_from, _value); + + } else { + Context.revert(TAG + " Not supported method. Token transfer fails."); + } + + + } + + private void sponsorVote(String ipfsKey, String vote, String voteReason, Address from, BigInteger value) { + checkMaintenance(); + updatePeriod(); + PeriodController period = new PeriodController(); + Context.require(period.periodName.get().equals(APPLICATION_PERIOD), + TAG + " Sponsor Vote can only be done on Application Period"); + PReps pReps = new PReps(); + Context.require(ArrayDBUtils.containsInArrayDb(from, pReps.validPreps), TAG + ": Not a P-Rep"); + + swapBNUsdToken(); + Map proposalDetails = getProposalDetails(ipfsKey); + String status = (String) proposalDetails.get(STATUS); + Address sponsorAddress = (Address) proposalDetails.get(SPONSOR_ADDRESS); + Address contributorAddress = (Address) proposalDetails.get(CONTRIBUTOR_ADDRESS); + String token = (String) proposalDetails.get(TOKEN); + + Context.require(from.equals(sponsorAddress), TAG + ": Not a valid sponsor"); + Context.require(List.of(ACCEPT, REJECT).contains(vote), TAG + ": Not valid vote"); + + if (vote.equals(ACCEPT)) { + Context.require(status.equals(SPONSOR_PENDING), + TAG + ": Sponsor can be only approve sponsorship for Pending proposals."); + Context.require(token.equals(bnUSD), TAG + ": " + token + " Not a supported token."); + + BigInteger projectBudget = (BigInteger) proposalDetails.get(TOTAL_BUDGET); + + Context.require(value.equals(projectBudget.divide(BigInteger.TEN)), + TAG + ": Deposit 10% of the total budget of the project."); + + updateProposalStatus(ipfsKey, PENDING); + String proposalPrefix = proposalPrefix(ipfsKey); + sponsorDepositAmount.at(proposalPrefix).set(value); + sponsoredTimestamp.at(proposalPrefix).set(BigInteger.valueOf(Context.getBlockTimestamp())); + sponsorDepositStatus.at(proposalPrefix).set(BOND_RECEIVED); + sponsorVoteReason.at(proposalPrefix).set(voteReason); + + SponsorBondReceived(from, "Sponsor Bond " + value + " " + token + " Received."); + } else { + removeContributor(contributorAddress, ipfsKey); + updateProposalStatus(ipfsKey, REJECTED); + BigInteger halfSubmissionFee = BigInteger.valueOf(APPLICATION_FEE / 2).multiply(EXA); + Context.transfer(contributorAddress, halfSubmissionFee); + proposalFees.set(proposalFees.get().subtract(halfSubmissionFee)); + SponsorBondRejected(from, + "Sponsor Bond Rejected for project " + proposalDetails.get(PROJECT_TITLE)); + + } + } + + @External + public void removeDenylistPreps() { + validateAdmins(); + PReps pReps = new PReps(); + for (int i = 0; i < pReps.denylist.size(); i++) { + Address prep = pReps.denylist.pop(); + pReps.prepsDenylistStatus.set(prep.toString(), 0); + } + } + + @Override + @External + @Deprecated(since = "JAVA translation", forRemoval = true) + public void remove_denylist_preps() { + removeDenylistPreps(); + } + + @External + public void claimSponsorBond() { + Address caller = Context.getCaller(); + DictDB userAmounts = sponsorBondReturn.at(caller.toString()); + BigInteger amountIcx = userAmounts.getOrDefault(ICX, BigInteger.ZERO); + BigInteger amountBNUsd = userAmounts.getOrDefault(bnUSD, BigInteger.ZERO); + + if (amountIcx.compareTo(BigInteger.ZERO) > 0) { + userAmounts.set(ICX, BigInteger.ZERO); + Context.transfer(caller, amountIcx); + SponsorBondClaimed(caller, amountIcx, amountIcx + " " + ICX + " withdrawn to " + caller); + + } else if (amountBNUsd.compareTo(BigInteger.ZERO) > 0) { + userAmounts.set(bnUSD, BigInteger.ZERO); + callScore(getBnusdScore(), "transfer", caller, amountBNUsd); + SponsorBondClaimed(caller, amountIcx, amountBNUsd + " " + bnUSD + " withdrawn to " + caller); + } else { + Context.revert(TAG + " Claim Reward Fails. Available Amounts are " + amountIcx + " " + ICX + " and" + amountBNUsd + " " + bnUSD); + } + } + + @Override + @External + @Deprecated(since = "JAVA translation", forRemoval = true) + public void claim_sponsor_bond() { + claimSponsorBond(); + } + + private void swapBNUsdToken() { + BigInteger sbh = swapBlockHeight.getOrDefault(BigInteger.ZERO); + BigInteger currentBlock = BigInteger.valueOf(Context.getBlockHeight()); + if (sbh.compareTo(currentBlock) < 0) { + swapBlockHeight.set(currentBlock.add(SWAP_BLOCK_DIFF)); + callScore(getCpfTreasuryScore(), "swap_tokens", swapCount.getOrDefault(0)); + } + } + + @External + public void setSwapCount(int value) { + validateAdmins(); + if (value > 0) { + swapCount.set(value); + } else { + Context.revert(value + " must be greater than 0"); + } + } + + @Override + @External + @Deprecated(since = "JAVA translation", forRemoval = true) + public void set_swap_count(int value) { + setSwapCount(value); + } + + @External(readonly = true) + public int getSwapCount() { + return swapCount.getOrDefault(0); + } + + @Override + @External + public void updateNextBlock(int blockCount) { + validateAdmins(); + PeriodController period = new PeriodController(); + period.nextBlock.set(BigInteger.valueOf(Context.getBlockHeight() + blockCount)); + } + + private void disqualifyProject(Address sponsorAddress, BigInteger sponsorDepositAmount, String flag) { + Context.require(flag.equals(bnUSD), TAG + " Not supported Token"); + JsonObject disqualifyProject = new JsonObject(); + disqualifyProject.add("method", "burn_amount"); + JsonObject params = new JsonObject(); + params.add(SPONSOR_ADDRESS, sponsorAddress.toString()); + disqualifyProject.add("params", params); + Address cpfScore = getCpfTreasuryScore(); + callScore(getBnusdScore(), "transfer", cpfScore, + sponsorDepositAmount, disqualifyProject.toString().getBytes()); + SponsorBondReturned(cpfScore, "Project Disqualified. " + sponsorDepositAmount + " " + flag + + " returned to CPF Treasury Address."); + } + + @External(readonly = true) + public Map getActiveProposalsList(@Optional int startIndex) { + List proposalKeys = new ArrayList<>(); + List> activeProposalsList = new ArrayList<>(); + + List activeProposals = getProposalsKeysByStatus(ACTIVE); + proposalKeys.addAll(activeProposals); + List pausedProposals = getProposalsKeysByStatus(PAUSED); + proposalKeys.addAll(pausedProposals); + + int endIndex = startIndex + 10; + int size = proposalKeys.size(); + if (endIndex > size) { + endIndex = size; + } + + for (int i = startIndex; i < endIndex; i++) { + Map proposalDetails = getProposalDetails(proposalKeys.get(i)); + activeProposalsList.add(proposalDetails); + } + return Map.of(DATA, activeProposalsList, COUNT, size); + } + + /*** + Returns the list of all all active or paused proposal from that address + :param walletAddress : wallet address of the contributor + :type walletAddress: Address + :return: list of active proposals of a contributor + ***/ + @External(readonly = true) + public List> getActiveProposals(Address walletAddress) { + List> _proposal_titles = new ArrayList<>(); + + ArrayDB contributorsAddress = contributorProjects.at(walletAddress); + for (int i = 0; i < contributorsAddress.size(); i++) { + String proposals = contributorsAddress.get(i); + String prefix = proposalPrefix(proposals); + String status = ProposalDataDb.status.at(prefix).getOrDefault(""); + if (ArrayDBUtils.containsInList(status, List.of(ACTIVE, PAUSED))) { + int _project_duration = projectDuration.at(prefix).getOrDefault(0); + int _approved_reports_count = approvedReports.at(prefix).getOrDefault(0); + boolean _last_progress_report = _project_duration - _approved_reports_count == 1; + Map _proposals_details = Map.of(PROJECT_TITLE, projectTitle.at(prefix).getOrDefault(""), + IPFS_HASH, proposals, + NEW_PROGRESS_REPORT, submitProgressReport.at(prefix).getOrDefault(false), + "last_progress_report", _last_progress_report); + _proposal_titles.add(_proposals_details); + } + } + + return _proposal_titles; + } + + @Deprecated(since = "JAVA translation", forRemoval = true) + @External(readonly = true) + public List> get_active_proposals(Address _wallet_address) { + return getActiveProposals(_wallet_address); + } + + + /*** + Returns a dict of proposals of provided status + :param walletAddress : user Signing in + :type walletAddress : "iconservice.base.address" + :return: List of all proposals_details + ***/ + @Override + @External(readonly = true) + public Map getProposalDetailByWallet(Address walletAddress, @Optional int startIndex) { + List> _proposals_list = new ArrayList<>(); + ArrayDB projects = contributorProjects.at(walletAddress); + int endIndex = startIndex + 5; + int size = projects.size(); + if (endIndex > size) { + endIndex = size; + } + for (int i = startIndex; i < endIndex; i++) { + Map _proposal_details = getProposalDetails(projects.get(i)); + _proposals_list.add(_proposal_details); + } + return Map.of(DATA, _proposals_list, COUNT, size); + } + + @External(readonly = true) + public Map get_proposal_detail_by_wallet(Address _wallet_address, @Optional int startIndex) { + return getProposalDetailByWallet(_wallet_address, startIndex); + } + + @Override + @External(readonly = true) + public Map getProposalsHistory(@Optional int startIndex) { + List proposalKeys = new ArrayList<>(); + List> proposalHistory = new ArrayList<>(); + + List completedProjects = getProposalsKeysByStatus(COMPLETED); + proposalKeys.addAll(completedProjects); + List rejectedProposals = getProposalsKeysByStatus(REJECTED); + proposalKeys.addAll(rejectedProposals); + List disqualifiedProjects = getProposalsKeysByStatus(DISQUALIFIED); + proposalKeys.addAll(disqualifiedProjects); + + int endIndex = startIndex + 10; + int size = proposalKeys.size(); + if (endIndex > size) { + endIndex = size; + } + + for (int i = startIndex; i < endIndex; i++) { + Map proposalDetails = getProposalDetails(proposalKeys.get(i)); + proposalHistory.add(proposalDetails); + } + return Map.of(DATA, proposalHistory, COUNT, size); + } + + + // EventLogs + @Override + @EventLog(indexed = 1) + public void ProposalSubmitted(Address _sender_address, String note) { + } + + @Override + @EventLog(indexed = 1) + public void ProgressReportSubmitted(Address _sender_address, String _project_title) { + } + + @Override + @EventLog(indexed = 1) + public void SponsorBondReceived(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void SponsorBondRejected(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void VotedSuccessfully(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void PRepPenalty(Address _prep_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void UnRegisterPRep(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void RegisterPRep(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void SponsorBondReturned(Address _sender_address, String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void PeriodUpdate(String _notes) { + } + + @Override + @EventLog(indexed = 1) + public void SponsorBondClaimed(Address _receiver_address, BigInteger _fund, String note) { + } + + @Override + @EventLog(indexed = 1) + public void PriorityVote(Address _address, String note) { + } + + public T callScore(Class t, Address address, String method, Object... params) { + return Context.call(t, address, method, params); + } + + public void callScore(Address address, String method, Object... params) { + Context.call(address, method, params); + } + + public void callScore(BigInteger amount, Address address, String method, Object... params) { + Context.call(amount, address, method, params); + } + + + public void validateAdmins() { + Context.require(isAdmin(Context.getCaller()), + TAG + ": Only Admins can call this method"); + + } + + public void validateAdminScore(Address scoreAddress) { + validateAdmins(); + Context.require(scoreAddress.isContract(), scoreAddress + " is not a SCORE Address"); + + } +} \ No newline at end of file diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/PReps.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/PReps.java new file mode 100644 index 00000000..64ed58bb --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/PReps.java @@ -0,0 +1,28 @@ +package community.icon.cps.score.cpscore; + +import score.Address; +import score.ArrayDB; +import score.Context; +import score.DictDB; + +import java.math.BigInteger; + +import static community.icon.cps.score.cpscore.utils.Constants.DENYLIST; +import static community.icon.cps.score.cpscore.utils.Constants.INACTIVE_PREPS; +import static community.icon.cps.score.cpscore.utils.Constants.MAIN_PREPS; +import static community.icon.cps.score.cpscore.utils.Constants.PENALTY_AMOUNT; +import static community.icon.cps.score.cpscore.utils.Constants.PREPS_DENYLIST_STATUS; +import static community.icon.cps.score.cpscore.utils.Constants.REGISTERED_PREPS; +import static community.icon.cps.score.cpscore.utils.Constants.UNREGISTERED_PREPS; + +public class PReps { + public final ArrayDB
validPreps = Context.newArrayDB(MAIN_PREPS, Address.class); + public final ArrayDB
unregisteredPreps = Context.newArrayDB(UNREGISTERED_PREPS, Address.class); + public final ArrayDB
registeredPreps = Context.newArrayDB(REGISTERED_PREPS, Address.class); + public final ArrayDB
inactivePreps = Context.newArrayDB(INACTIVE_PREPS, Address.class); + public final ArrayDB
denylist = Context.newArrayDB(DENYLIST, Address.class); + + public final ArrayDB penaltyAmount = Context.newArrayDB(PENALTY_AMOUNT, BigInteger.class); + + public final DictDB prepsDenylistStatus = Context.newDictDB(PREPS_DENYLIST_STATUS, Integer.class); +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/PeriodController.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/PeriodController.java new file mode 100644 index 00000000..21003a24 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/PeriodController.java @@ -0,0 +1,17 @@ +package community.icon.cps.score.cpscore; + +import score.Context; +import score.VarDB; + +import java.math.BigInteger; + +import static community.icon.cps.score.cpscore.utils.Constants.*; + +public class PeriodController { + public final VarDB initialBlock = Context.newVarDB(INITIAL_BLOCK, BigInteger.class); + public final VarDB periodName = Context.newVarDB(PERIOD_NAME, String.class); + public final VarDB previousPeriodName = Context.newVarDB(PREVIOUS_PERIOD_NAME, String.class); + public final VarDB nextBlock = Context.newVarDB(NEXTBLOCK, BigInteger.class); + public final VarDB updatePeriodIndex = Context.newVarDB(UPDATE_PERIOD_INDEX, Integer.class); + public final VarDB periodCount = Context.newVarDB(PERIOD_COUNT, Integer.class); +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/SetterGetter.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/SetterGetter.java new file mode 100644 index 00000000..7dde67c8 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/SetterGetter.java @@ -0,0 +1,22 @@ +package community.icon.cps.score.cpscore; + +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.External; + +import java.math.BigInteger; + +//import static community.icon.cps.score.cpscore.utils.Checkers.validateAdminScore; +//import static community.icon.cps.score.cpscore.utils.Checkers.validateAdmins; +import static community.icon.cps.score.cpscore.utils.Constants.*; + +public class SetterGetter { + public final VarDB
cpsTreasuryScore = Context.newVarDB(CPS_TREASURY_SCORE, Address.class); + public final VarDB
cpfScore = Context.newVarDB(CPF_SCORE, Address.class); + public final VarDB
balancedDollar = Context.newVarDB(BALANCED_DOLLAR, Address.class); + public final VarDB budgetAdjustment = Context.newVarDB(BUDGETADJUSTMENT, Boolean.class); + public final VarDB maintenance = Context.newVarDB(MAINTENANCE, Boolean.class); + + +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProgressReportDataDb.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProgressReportDataDb.java new file mode 100644 index 00000000..0640ccd1 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProgressReportDataDb.java @@ -0,0 +1,94 @@ +package community.icon.cps.score.cpscore.db; + +import score.*; + +import java.math.BigInteger; +import java.util.Map; + +import static community.icon.cps.score.cpscore.utils.ArrayDBUtils.recordTxHash; +import static community.icon.cps.score.cpscore.utils.Constants.*; +import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProgressReportAttributes; + +public class ProgressReportDataDb { + private static final BranchDB> ipfsHash = Context.newBranchDB(IPFS_HASH, String.class); + private static final BranchDB> reportHash = Context.newBranchDB(REPORT_HASH, String.class); + private static final BranchDB> progressReportTitle = Context.newBranchDB(PROGRESS_REPORT_TITLE, String.class); + public static final BranchDB> timestamp = Context.newBranchDB(TIMESTAMP, BigInteger.class); + public static final BranchDB> status = Context.newBranchDB(STATUS, String.class); + private static final BranchDB> txHash = Context.newBranchDB(TX_HASH, String.class); + private static final BranchDB> budgetAdjustment = Context.newBranchDB(BUDGET_ADJUSTMENT, Boolean.class); + private static final BranchDB> additionalBudget = Context.newBranchDB(ADDITIONAL_BUDGET, BigInteger.class); + private static final BranchDB> additionalMonth = Context.newBranchDB(ADDITIONAL_DURATION, Integer.class); + + public static final BranchDB> votersReasons = Context.newBranchDB(VOTERS_REASON, String.class); + public static final BranchDB> totalVotes = Context.newBranchDB(TOTAL_VOTES, BigInteger.class); + public static final BranchDB> approvedVotes = Context.newBranchDB(APPROVED_VOTES, BigInteger.class); + public static final BranchDB> rejectedVotes = Context.newBranchDB(REJECTED_VOTES, BigInteger.class); + public static final BranchDB> votersList = Context.newBranchDB(VOTERS_LIST, Address.class); + public static final BranchDB> approveVoters = Context.newBranchDB(APPROVE_VOTERS, Address.class); + public static final BranchDB> rejectVoters = Context.newBranchDB(REJECT_VOTERS, Address.class); + public static final BranchDB> totalVoters = Context.newBranchDB(TOTAL_VOTERS, Integer.class); + + public static final BranchDB>> votersListIndices = Context.newBranchDB(VOTERS_LIST_INDEXES, Integer.class); + public static final BranchDB> budgetApprovedVotes = Context.newBranchDB(BUDGET_APPROVED_VOTES, BigInteger.class); + public static final BranchDB> budgetRejectedVotes = Context.newBranchDB(BUDGET_REJECTED_VOTES, BigInteger.class); + public static final BranchDB> budgetApproveVoters = Context.newBranchDB(BUDGET_APPROVE_VOTERS, Address.class); + public static final BranchDB> budgetRejectVoters = Context.newBranchDB(BUDGET_REJECT_VOTERS, Address.class); + public static final BranchDB> budgetAdjustmentStatus = Context.newBranchDB(BUDGET_ADJUSTMENT_STATUS, String.class); + public static final BranchDB> ipfsLink = Context.newBranchDB(IPFS_LINK, String.class); + public static final BranchDB>> budgetVotersListIndices = Context.newBranchDB(BUDGET_VOTERS_LIST_INDICES, Integer.class); + + public static void addDataToProgressReportDB(ProgressReportAttributes progressData, String prefix) { + ipfsHash.at(prefix).set(progressData.ipfs_hash); + reportHash.at(prefix).set(progressData.report_hash); + progressReportTitle.at(prefix).set(progressData.progress_report_title); + timestamp.at(prefix).set(BigInteger.valueOf(Context.getBlockTimestamp())); + additionalBudget.at(prefix).set(progressData.additional_budget.multiply(EXA)); + additionalMonth.at(prefix).set(progressData.additional_month); + status.at(prefix).set(WAITING); + txHash.at(prefix).set(recordTxHash(Context.getTransactionHash())); + budgetAdjustment.at(prefix).set(progressData.budget_adjustment); + budgetAdjustmentStatus.at(prefix).set("N/A"); + totalVotes.at(prefix).set(BigInteger.ZERO); + totalVoters.at(prefix).set(0); + approvedVotes.at(prefix).set(BigInteger.ZERO); + rejectedVotes.at(prefix).set(BigInteger.ZERO); + budgetApprovedVotes.at(prefix).set(BigInteger.ZERO); + budgetRejectedVotes.at(prefix).set(BigInteger.ZERO); + ipfsLink.at(prefix).set(progressData.ipfs_link); + } + + public static Map getDataFromProgressReportDB(String prefix) { + String proposalHash = ipfsHash.at(prefix).getOrDefault(""); + Map entryMap = + Map.ofEntries(Map.entry(IPFS_HASH, proposalHash), + Map.entry(REPORT_HASH, reportHash.at(prefix).getOrDefault("")), + Map.entry(PROGRESS_REPORT_TITLE, progressReportTitle.at(prefix).getOrDefault("")), + Map.entry(TIMESTAMP, timestamp.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(ADDITIONAL_BUDGET, additionalBudget.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(ADDITIONAL_DURATION, additionalMonth.at(prefix).getOrDefault(0)), + Map.entry(STATUS, status.at(prefix).getOrDefault("")), + Map.entry(TX_HASH, txHash.at(prefix).getOrDefault("")), + Map.entry(TOTAL_VOTES, totalVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(APPROVED_VOTES, approvedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(REJECTED_VOTES, rejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(TOTAL_VOTERS, totalVoters.at(prefix).getOrDefault(0)), + Map.entry(APPROVE_VOTERS, approveVoters.at(prefix).size()), + Map.entry(REJECT_VOTERS, rejectVoters.at(prefix).size()), + Map.entry(BUDGET_APPROVED_VOTES, budgetApprovedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(BUDGET_REJECTED_VOTES, budgetRejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(BUDGET_APPROVE_VOTERS, budgetApproveVoters.at(prefix).size()), + Map.entry(BUDGET_REJECT_VOTERS, budgetRejectVoters.at(prefix).size()), + Map.entry(BUDGET_ADJUSTMENT_STATUS, budgetAdjustmentStatus.at(prefix).getOrDefault("")), + Map.entry(IPFS_LINK, ipfsLink.at(prefix).getOrDefault("")), + Map.entry(BUDGET_ADJUSTMENT, budgetAdjustment.at(prefix).getOrDefault(false)), + Map.entry(PROJECT_TITLE, ProposalDataDb.projectTitle.at(proposalPrefix(proposalHash)).getOrDefault("")), + Map.entry(CONTRIBUTOR_ADDRESS, ProposalDataDb.contributorAddress.at(proposalPrefix(proposalHash)).get())); + + return entryMap; + } + + public static String proposalPrefix(String proposalKey) { + return PROPOSAL_DB_PREFIX + "|" + "|" + proposalKey; + } +} \ No newline at end of file diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProposalDataDb.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProposalDataDb.java new file mode 100644 index 00000000..9425118b --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/db/ProposalDataDb.java @@ -0,0 +1,105 @@ +package community.icon.cps.score.cpscore.db; + +import score.*; + +import java.math.BigInteger; +import java.util.Map; + +import static community.icon.cps.score.cpscore.utils.ArrayDBUtils.recordTxHash; +import static community.icon.cps.score.cpscore.utils.Constants.*; +import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProposalAttributes; + +public class ProposalDataDb { + public static final BranchDB> ipfsHash = Context.newBranchDB(IPFS_HASH, String.class); + public static final BranchDB> projectTitle = Context.newBranchDB(PROJECT_TITLE, String.class); + public static final BranchDB> timestamp = Context.newBranchDB(TIMESTAMP, BigInteger.class); + public static final BranchDB> totalBudget = Context.newBranchDB(TOTAL_BUDGET, BigInteger.class); + public static final BranchDB> projectDuration = Context.newBranchDB(PROJECT_DURATION, Integer.class); + public static final BranchDB> approvedReports = Context.newBranchDB(APPROVED_REPORTS, Integer.class); + public static final BranchDB> sponsorAddress = Context.newBranchDB(SPONSOR_ADDRESS, Address.class); + public static final BranchDB> contributorAddress = Context.newBranchDB(CONTRIBUTOR_ADDRESS, Address.class); + public static final BranchDB> token = Context.newBranchDB(TOKEN, String.class); + public static final BranchDB> ipfsLink = Context.newBranchDB(IPFS_LINK, String.class); + public static final BranchDB> status = Context.newBranchDB(STATUS, String.class); + private static final BranchDB> txHash = Context.newBranchDB(TX_HASH, String.class); + public static final BranchDB> percentageCompleted = Context.newBranchDB(PERCENTAGE_COMPLETED, Integer.class); + public static final BranchDB> votersReasons = Context.newBranchDB(VOTERS_REASON, String.class); + public static final BranchDB> totalVotes = Context.newBranchDB(TOTAL_VOTES, BigInteger.class); + public static final BranchDB> totalVoters = Context.newBranchDB(TOTAL_VOTERS, Integer.class); + public static final BranchDB> approvedVotes = Context.newBranchDB(APPROVED_VOTES, BigInteger.class); + public static final BranchDB> rejectedVotes = Context.newBranchDB(REJECTED_VOTES, BigInteger.class); + public static final BranchDB> abstainedVotes = Context.newBranchDB(ABSTAINED_VOTES, BigInteger.class); + public static final BranchDB> sponsorDepositAmount = Context.newBranchDB(SPONSOR_DEPOSIT, BigInteger.class); + public static final BranchDB> sponsoredTimestamp = Context.newBranchDB(SPONSORED_TIMESTAMP, BigInteger.class); + public static final BranchDB> sponsorDepositStatus = Context.newBranchDB(SPONSOR_DEPOSIT_STATUS, String.class); + public static final BranchDB> sponsorVoteReason = Context.newBranchDB(SPONSOR_VOTE_REASON, String.class); + public static final BranchDB> votersList = Context.newBranchDB(VOTERS_LIST, Address.class); + public static final BranchDB> approveVoters = Context.newBranchDB(APPROVE_VOTERS, Address.class); + public static final BranchDB> rejectVoters = Context.newBranchDB(REJECT_VOTERS, Address.class); + public static final BranchDB> abstainVoters = Context.newBranchDB(ABSTAIN_VOTERS, Address.class); + public static final BranchDB>> votersListIndex = Context.newBranchDB(VOTERS_LIST_INDEXES, Integer.class); + public static final BranchDB> progressReports = Context.newBranchDB(PROGRESS_REPORTS, String.class); + public static final BranchDB> budgetAdjustment = Context.newBranchDB(BUDGET_ADJUSTMENT, Boolean.class); + public static final BranchDB> submitProgressReport = Context.newBranchDB(SUBMIT_PROGRESS_REPORT, Boolean.class); + + public static void addDataToProposalDB(ProposalAttributes proposalData, String prefix) { + ipfsHash.at(prefix).set(proposalData.ipfs_hash); + projectTitle.at(prefix).set(proposalData.project_title); + timestamp.at(prefix).set(BigInteger.valueOf(Context.getBlockTimestamp())); + totalBudget.at(prefix).set(proposalData.total_budget.multiply(EXA)); + projectDuration.at(prefix).set(proposalData.project_duration); + sponsorAddress.at(prefix).set(proposalData.sponsor_address); + ipfsLink.at(prefix).set(proposalData.ipfs_link); + contributorAddress.at(prefix).set(Context.getCaller()); + status.at(prefix).set(SPONSOR_PENDING); + txHash.at(prefix).set(recordTxHash(Context.getTransactionHash())); + percentageCompleted.at(prefix).set(0); + totalVotes.at(prefix).set(BigInteger.ZERO); + totalVoters.at(prefix).set(0); + approvedVotes.at(prefix).set(BigInteger.ZERO); + rejectedVotes.at(prefix).set(BigInteger.ZERO); + approvedReports.at(prefix).set(0); + budgetAdjustment.at(prefix).set(false); + submitProgressReport.at(prefix).set(false); + token.at(prefix).set(proposalData.token); + } + + public static Map getDataFromProposalDB(String prefix) { + String reason = sponsorVoteReason.at(prefix).getOrDefault(""); + if (reason.equalsIgnoreCase("none")) { + reason = ""; + } else { + reason = reason.toString(); + } + + Map entryMap = Map.ofEntries( + Map.entry(IPFS_HASH, ipfsHash.at(prefix).getOrDefault("")), + Map.entry(PROJECT_TITLE, projectTitle.at(prefix).getOrDefault("")), + Map.entry(TIMESTAMP, timestamp.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(TOTAL_BUDGET, totalBudget.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(PROJECT_DURATION, projectDuration.at(prefix).getOrDefault(0)), + Map.entry(APPROVED_REPORTS, approvedReports.at(prefix).getOrDefault(0)), + Map.entry(SPONSOR_ADDRESS, sponsorAddress.at(prefix).get()), + Map.entry(CONTRIBUTOR_ADDRESS, contributorAddress.at(prefix).get()), + Map.entry(STATUS, status.at(prefix).getOrDefault("")), + Map.entry(TX_HASH, txHash.at(prefix).getOrDefault("")), + Map.entry(PERCENTAGE_COMPLETED, percentageCompleted.at(prefix).getOrDefault(0)), + Map.entry(TOKEN, token.at(prefix).getOrDefault("")), + Map.entry(TOTAL_VOTES, totalVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(TOTAL_VOTERS, totalVoters.at(prefix).getOrDefault(0)), + Map.entry(APPROVED_VOTES, approvedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(REJECTED_VOTES, rejectedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(ABSTAINED_VOTES, abstainedVotes.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(SPONSOR_DEPOSIT_AMOUNT, sponsorDepositAmount.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(SPONSORED_TIMESTAMP, sponsoredTimestamp.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(SPONSOR_DEPOSIT_STATUS, sponsorDepositStatus.at(prefix).getOrDefault("")), + Map.entry(APPROVE_VOTERS, approveVoters.at(prefix).size()), + Map.entry(REJECT_VOTERS, rejectVoters.at(prefix).size()), + Map.entry(ABSTAIN_VOTERS, abstainVoters.at(prefix).size()), + Map.entry(BUDGET_ADJUSTMENT, budgetAdjustment.at(prefix).getOrDefault(false)), + Map.entry(SUBMIT_PROGRESS_REPORT, submitProgressReport.at(prefix).getOrDefault(false))); + return entryMap; + } + + +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/ArrayDBUtils.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/ArrayDBUtils.java new file mode 100644 index 00000000..af556c7c --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/ArrayDBUtils.java @@ -0,0 +1,156 @@ +package community.icon.cps.score.cpscore.utils; + +import score.ArrayDB; +import scorex.util.ArrayList; + +import java.util.List; +import java.util.Map; + +public final class ArrayDBUtils { + public static void clearArrayDb(ArrayDB array_db) { + int size = array_db.size(); + for (int i = 0; i < size; i++) { + array_db.pop(); + } + + } + + public static void removeArrayItem(ArrayDB array_db, Object target) { + int size = array_db.size(); + T _out = array_db.get(size - 1); + if (_out.equals(target)) { + array_db.pop(); + return; + } + for (int i = 0; i < size - 1; i++) { + if (array_db.get(i).equals(target)) { + array_db.set(i, _out); + array_db.pop(); + return; + } + } + } + + + public static boolean containsInArrayDb(T value, ArrayDB array) { + boolean contains = false; + if (array == null || value == null) { + return contains; + } + + for (int i = 0; i < array.size(); i++) { + if (array.get(i) != null && array.get(i).equals(value)) { + contains = true; + break; + } + } + return contains; + } + + public static boolean containsInList(T value, List array){ + boolean contains = false; + if (array == null || value == null) { + return contains; + } + + for (int i = 0; i < array.size(); i++) { + if (array.get(i) != null && array.get(i).equals(value)) { + contains = true; + break; + } + } + return contains; + } + + public static String recordTxHash(byte[] tx_hash) { + String tx_hash_string = encodeHexString(tx_hash); + return "0x" + tx_hash_string; + } + + public static String encodeHexString(byte[] byteArray) { + StringBuffer hexStringBuffer = new StringBuffer(); + for (byte b : byteArray) { + hexStringBuffer.append(byteToHex(b)); + } + return hexStringBuffer.toString(); + } + + public static String byteToHex(byte num) { + char[] hexDigits = new char[2]; + hexDigits[0] = Character.forDigit((num >> 4) & 0xF, 16); + hexDigits[1] = Character.forDigit((num & 0xF), 16); + return new String(hexDigits); + } + + public static List arrayDBtoList(ArrayDB arraydb) { + List list = new ArrayList<>(); + for (int i = 0; i < arraydb.size(); i++) { + list.add(arraydb.get(i)); + } + return list; + } + + public static List arrayToList(T[] array){ + List list = new ArrayList<>(); + for (int i = 0; i < array.length; i++){ + list.add(array[i]); + } + return list; + } + + public static void mergeSort(String[] array, int left, int right, Map priorityVoteResult) { + if (left < right) { + + int mid = (left + right) / 2; + + mergeSort(array, left, mid, priorityVoteResult); + mergeSort(array, mid + 1, right, priorityVoteResult); + + merge(array, left, mid, right, priorityVoteResult); + } + } + + private static void merge(String[] array, int p, int q, int r, Map priorityVoteResult) { + + int n1 = q - p + 1; + int n2 = r - q; + + String[] L = new String[n1]; + String[] M = new String[n2]; + + for (int i = 0; i < n1; i++) + L[i] = array[p + i]; + for (int j = 0; j < n2; j++) + M[j] = array[q + 1 + j]; + + int i, j, k; + i = 0; + j = 0; + k = p; + + while (i < n1 && j < n2) { + if ((priorityVoteResult.get(L[i])) > (priorityVoteResult.get(M[j]))) { + array[k] = L[i]; + i++; + } else { + array[k] = M[j]; + j++; + } + k++; + } + + while (i < n1) { + array[k] = L[i]; + i++; + k++; + } + + while (j < n2) { + array[k] = M[j]; + j++; + k++; + } + } +} + + diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Checkers.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Checkers.java new file mode 100644 index 00000000..8f82c211 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Checkers.java @@ -0,0 +1,19 @@ +package community.icon.cps.score.cpscore.utils; + +import community.icon.cps.score.cpscore.SetterGetter; +import score.Address; +import score.Context; + +public class Checkers { + public static void onlyOwner() { + Address caller = Context.getCaller(); + Address owner = Context.getOwner(); + Context.require(caller.equals(owner), "SenderNotScoreOwner: Sender=" + caller + " Owner=" + owner); + } + + + public static void checkMaintenance() { + SetterGetter setterGetter = new SetterGetter(); + Context.require(!setterGetter.maintenance.get(), "Maintenance mode is on. Will resume soon."); + } +} diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Constants.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Constants.java new file mode 100644 index 00000000..45c4e364 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Constants.java @@ -0,0 +1,195 @@ +package community.icon.cps.score.cpscore.utils; + +import score.Address; + +import java.math.BigInteger; +import java.util.List; + +public class Constants { + public static final String TAG = "CPS Score"; + + + public static final BigInteger EXA = new BigInteger("1000000000000000000"); + public static final Integer MINIMUM_PREPS = 7; + + public static final Integer MAX_PROJECT_PERIOD = 12; + public static final double MAJORITY = 0.67; + public static final BigInteger DAY_COUNT = BigInteger.valueOf(15); + public static final BigInteger BLOCKS_DAY_COUNT = BigInteger.valueOf(43120); + + public static final String PROPOSAL_DB_PREFIX = "proposal"; + public static final String PROGRESS_REPORT_DB_PREFIX = "progressReport"; + + public static final String APPLICATION_PERIOD = "Application Period"; + public static final String VOTING_PERIOD = "Voting Period"; + public static final String TRANSITION_PERIOD = "Transition Period"; + + // Bond Status + public static final String BOND_RECEIVED = "bond_received"; + public static final String BOND_APPROVED = "bond_approved"; + public static final String BOND_RETURNED = "bond_returned"; + public static final String BOND_CANCELLED = "bond_cancelled"; + + // SCOREs Constants + public static final String CPS_TREASURY_SCORE = "_cps_treasury_score"; + public static final String CPF_SCORE = "_cpf_score"; + public static final String BALANCED_DOLLAR = "balanced_dollar"; + + // PERIOD CONSTANTS + public static final String INITIAL_BLOCK = "initial_block"; + public static final String PERIOD_DETAILS = "_period_details"; + public static final String PERIOD_NAME = "period_name"; + public static final String PREVIOUS_PERIOD_NAME = "previous_period_name"; + public static final String PERIOD_SPAN = "period_span"; + public static final String LASTBLOCK = "last_block"; + public static final String CURRENTBLOCK = "current_block"; + public static final String NEXTBLOCK = "next_block"; + public static final String REMAINING_TIME = "remaining_time"; + public static final String UPDATE_PERIOD_INDEX = "update_period_index"; + + // PREPS Related Constants + public static final String MAIN_PREPS = "main_preps"; + public static final String ALL_PREPS = "_all_preps"; + public static final String ADMINS = "admins"; + public static final String UNREGISTERED_PREPS = "unregistered_preps"; + public static final String REGISTERED_PREPS = "registered_preps"; + public static final String INACTIVE_PREPS = "inactive_preps"; + public static final String PREP_NAME = "prep_name"; + + public static final String ICX = "ICX"; + public static final String bnUSD = "bnUSD"; + public static final List SUPPORTED_TOKENS = List.of(ICX, bnUSD); + + public static final String MAINTENANCE = "maintenance"; + public static final String MESSAGE = "message"; + + //VarDB/ArrayDB Params + public static final String PROPOSALS_KEY_LIST = "proposals_key_list"; + public static final String PROPOSALS_KEY_LIST_INDEX = "proposals_key_list_index"; + public static final String PROGRESS_KEY_LIST = "progress_key_list"; + public static final String PROGRESS_KEY_LIST_INDEX = "progress_key_list_index"; + public static final String CONTRIBUTORS = "contributors"; + public static final String SPONSORS = "sponsors"; + public static final String BUDGET_APPROVALS_LIST = "budget_approvals_list"; + public static final String SPONSOR_ADDRESS = "sponsor_address"; + public static final String TOTAL_BUDGET = "total_budget"; + public static final String ACTIVE_PROPOSALS = "active_proposals"; + public static final String VOTING_PROPOSALS = "voting_proposals"; + public static final String VOTING_PROGRESS_REPORTS = "voting_progress_reports"; + public static final String AMOUNT = "_total_amount"; + public static final String ADDRESS = "address"; + + // Proposals and Progress reports keys + public static final String PROPOSAL = "proposal"; + public static final String PROGRESS_REPORTS = "progress_reports"; + public static final String NEW_PROGRESS_REPORT = "new_progress_report"; + public static final String PROJECT_TITLE = "project_title"; + public static final String PROGRESS_REPORT_TITLE = "progress_report_title"; + public static final String TOTAL_VOTES = "total_votes"; + public static final String TOTAL_VOTERS = "total_voters"; + public static final String REJECTED_VOTES = "rejected_votes"; + public static final String APPROVED_VOTES = "approved_votes"; + public static final String ABSTAINED_VOTES = "abstained_votes"; + public static final String REJECT_VOTERS = "reject_voters"; + public static final String APPROVE_VOTERS = "approve_voters"; + public static final String ABSTAIN_VOTERS = "abstain_voters"; + public static final String VOTERS_REASON = "voters_reasons"; + public static final String SUBMIT_PROGRESS_REPORT = "submit_progress_report"; + public static final String SPONSORED_TIMESTAMP = "sponsored_timestamp"; + public static final String SPONSOR_DEPOSIT_STATUS = "sponsor_deposit_status"; + public static final String SPONSOR_VOTE_REASON = "sponsor_vote_reason"; + public static final String VOTERS_LIST = "voters_list"; + public static final String VOTERS_LIST_INDEXES = "voters_list_indexes"; + public static final String BUDGET_VOTERS_LIST_INDICES = "budget_voters_list_indexes"; + + + public static final String TIMESTAMP = "timestamp"; + public static final String TOKEN = "token"; + public static final String CONTRIBUTOR_ADDRESS = "contributor_address"; + public static final String TX_HASH = "tx_hash"; + public static final String IPFS_HASH = "ipfs_hash"; + public static final String REPORT_KEY = "report_key"; + public static final String REPORT_HASH = "report_hash"; + public static final String PROJECT_DURATION = "project_duration"; + public static final String APPROVED_REPORTS = "approved_reports"; + public static final String IPFS_LINK = "ipfs_link"; + public static final String PERCENTAGE_COMPLETED = "percentage_completed"; + public static final String ADDITIONAL_BUDGET = "additional_budget"; + public static final String ADDITIONAL_DURATION = "additional_month"; + public static final String BUDGET_ADJUSTMENT = "budget_adjustment"; + public static final String BUDGETADJUSTMENT = "budgetAdjustment"; + public static final String BUDGET_ADJUSTMENT_STATUS = "budget_adjustment_status"; + public static final String BUDGET_APPROVED_VOTES = "budget_approved_votes"; + public static final String BUDGET_REJECTED_VOTES = "budget_rejected_votes"; + public static final String BUDGET_APPROVE_VOTERS = "budget_approve_voters"; + public static final String BUDGET_REJECT_VOTERS = "budget_reject_voters"; + public static final String SPONSOR_DEPOSIT_AMOUNT = "sponsor_deposit_amount"; + public static final String SPONSOR_DEPOSIT = "sponsor_deposit"; + public static final String PREPS_DENYLIST_STATUS = "preps_denylist_status"; + public static final String DENYLIST = "denylist"; + public static final String PENALTY_AMOUNT = "penalty_amount"; + public static final String STATUS = "status"; + public static final String DATA = "data"; + public static final String COUNT = "count"; + + public static final String DELEGATION_SNAPSHOT = "delegation_snapshot"; + public static final String MAX_DELEGATION = "max_delegation"; + public static final String PROPOSAL_FUND = "proposal_fund"; + public static final String PROPOSAL_FEES = "proposal_fees"; + public static final String SWAP_BLOCK_HEIGHT = "swap_block_height"; + public static final String SWAP_COUNT = "swap_count"; + public static final String PERIOD_COUNT = "period_count"; + + public static final String PROPOSAL_RANK = "proposal_rank"; + public static final String PRIORITY_VOTED_PREPS = "priority_voted_preps"; + public static final String SPONSOR_PROJECTS = "sponsor_projects"; + public static final String CONTRIBUTOR_PROJECTS = "contributor_projects"; + + + // VOTE KEYS + public static final String VOTE = "vote"; + public static final String INDEX = "index"; + public static final String CHANGE_VOTE = "change_vote"; + public static final String VOTE_REASON = "vote_reason"; + public static final String APPROVE = "_approve"; + public static final String REJECT = "_reject"; + public static final String ABSTAIN = "_abstain"; + public static final String ACCEPT = "_accept"; + + public static final Integer APPROVE_ = 1; + public static final Integer REJECT_ = 2; + public static final Integer ABSTAIN_ = 3; + + public static final Integer VOTED = 1; + public static final Integer NOT_VOTED = 0; + + // Sponsor Fee + public static final Integer APPLICATION_FEE = 50; + public static final BigInteger SWAP_BLOCK_DIFF = BigInteger.valueOf(5); + + + public static final String SPONSOR_PENDING = "_sponsor_pending"; + public static final String PENDING = "_pending"; + public static final String ACTIVE = "_active"; + public static final String PAUSED = "_paused"; + public static final String DISQUALIFIED = "_disqualified"; + public static final String REJECTED = "_rejected"; + public static final String COMPLETED = "_completed"; + public static final List STATUS_TYPE = List.of(SPONSOR_PENDING, PENDING, ACTIVE, PAUSED, DISQUALIFIED, REJECTED, COMPLETED); + + public static final String WAITING = "_waiting"; + public static final String APPROVED = "_approved"; + public static final String PROGRESS_REPORT_REJECTED = "_progress_report_rejected"; + public static final List PROGRESS_REPORT_STATUS_TYPE = List.of(APPROVED, WAITING, PROGRESS_REPORT_REJECTED); + + public static final String SPONSOR_BOND_RETURN = "sponsor_bond_return"; + + public static final Address SYSTEM_ADDRESS = Address.fromString("cx0000000000000000000000000000000000000000"); + + public static final Integer PENALTY_LEVELS = 3; + + // migration + public static final int MIGRATION_BATCH = 10; + + public static final String BATCH_SIZE = "batch_size"; +} \ No newline at end of file diff --git a/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Migration.java b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Migration.java new file mode 100644 index 00000000..61c28494 --- /dev/null +++ b/CPSCore/src/main/java/community/icon/cps/score/cpscore/utils/Migration.java @@ -0,0 +1,11 @@ +package community.icon.cps.score.cpscore.utils; + +import score.Address; + +import java.math.BigInteger; + +public class Migration { + public String ipfsHash; + public Address[] abstainVoters; + public BigInteger abstainVotes; +} \ No newline at end of file diff --git a/CPSCore/src/test/java/community/icon/cps/score/cpscore/CPSScoreTest.java b/CPSCore/src/test/java/community/icon/cps/score/cpscore/CPSScoreTest.java new file mode 100644 index 00000000..90daab31 --- /dev/null +++ b/CPSCore/src/test/java/community/icon/cps/score/cpscore/CPSScoreTest.java @@ -0,0 +1,1432 @@ +package community.icon.cps.score.cpscore; + +import com.eclipsesource.json.JsonObject; +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; +import community.icon.cps.score.cpscore.utils.Constants; +import org.junit.jupiter.api.*; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; +import score.Address; +import score.ArrayDB; +import score.Context; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import static community.icon.cps.score.cpscore.utils.Constants.*; +import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProgressReportAttributes; +import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProposalAttributes; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class CPSScoreTest extends TestBase{ + public static final Address SYSTEM_ADDRESS = Address.fromString("cx0000000000000000000000000000000000000000"); + private static final Address cpsTreasury = Address.fromString("cx0000000000000000000000000000000000000002"); + private static final Address cpfTreasury = Address.fromString("cx0000000000000000000000000000000000000003"); + private static final Address bnUSDScore = Address.fromString("cx0000000000000000000000000000000000000004"); + public static final String TAG = "CPS Score"; + public static final BigInteger MULTIPLIER = new BigInteger("1000000000000000000"); + + private static final ServiceManager sm = getServiceManager(); + private static final Account owner = sm.createAccount(); + private static final Account testingAccount = sm.createAccount(); + private static final Account testingAccount1 = sm.createAccount(); + private static final Account testingAccount2 = sm.createAccount(); + private static final Account testingAccount3 = sm.createAccount(); + private static final Account testingAccount4 = sm.createAccount(); + private static final Account testingAccount5 = sm.createAccount(); + private static final Account testingAccount6 = sm.createAccount(); + private Score cpsScore; + private static MockedStatic contextMock; + + CPSCore scoreSpy; + + @BeforeEach + public void setup() throws Exception { + cpsScore = sm.deploy(owner, CPSCore.class); + CPSCore instance = (CPSCore) cpsScore.getInstance(); + scoreSpy = spy(instance); + cpsScore.setInstance(scoreSpy); + long currentTime = System.currentTimeMillis() / 1000L; + sm.getBlock().increase(currentTime / 2); + contextMock.reset(); + } + + @BeforeAll + public static void init(){ + contextMock = Mockito.mockStatic(Context.class, CALLS_REAL_METHODS); + } + + public void expectErrorMessage(Executable contractCall, String errorMessage) { + AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); + assertEquals(errorMessage, e.getMessage()); + } + + @Test + void name(){ + assertEquals(cpsScore.call("name"), TAG); + } + + @Test + void addAdmin(){ + cpsScore.invoke(owner, "addAdmin", testingAccount.getAddress()); + assertEquals(List.of(testingAccount.getAddress()), cpsScore.call("get_admins")); + } + + @Test + void addAdminNotOwner(){ + Executable addAdminNotOwner = () -> cpsScore.invoke(testingAccount, "addAdmin", testingAccount.getAddress()); + expectErrorMessage(addAdminNotOwner, "Reverted(0): SenderNotScoreOwner: Sender=" + testingAccount.getAddress() + " Owner=" + owner.getAddress()); + } + + @Test + void removeAdmin(){ + cpsScore.invoke(owner, "addAdmin", testingAccount.getAddress()); + assertEquals(List.of(testingAccount.getAddress()), cpsScore.call("getAdmins")); + cpsScore.invoke(owner, "removeAdmin", testingAccount.getAddress()); + assertEquals(0, ((List) cpsScore.call("getAdmins")).size()); + } + + @Test + void removeAdminNotOwner(){ + cpsScore.invoke(owner, "addAdmin", testingAccount.getAddress()); + assertEquals(List.of(testingAccount.getAddress()), cpsScore.call("getAdmins")); + Executable removeAdminNotOwner = () -> cpsScore.invoke(testingAccount, "removeAdmin", testingAccount.getAddress()); + expectErrorMessage(removeAdminNotOwner, "Reverted(0): SenderNotScoreOwner: Sender=" + testingAccount.getAddress() + " Owner=" + owner.getAddress()); + } + + @Test + void removeAdminAddressNotAdmin(){ + cpsScore.invoke(owner, "addAdmin", testingAccount.getAddress()); + assertEquals(List.of(testingAccount.getAddress()), cpsScore.call("getAdmins")); + Executable removeAdminNotOwner = () -> cpsScore.invoke(owner, "removeAdmin", testingAccount1.getAddress()); + expectErrorMessage(removeAdminNotOwner, "Reverted(0): CPS Score: Address not registered as admin."); + } + + void addAdminMethod(){ + cpsScore.invoke(owner, "addAdmin", owner.getAddress()); + } + + @Test + void registerPRep(){ + registerPrepsMethod(); + assertEquals(7, ((List)(cpsScore.call("get_PReps"))).size()); + } + + @Test + void registerPrepAlreadyRegistered(){ + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + cpsScore.invoke(owner, "register_prep"); + Executable register = () -> cpsScore.invoke(owner, "register_prep"); + expectErrorMessage(register, "Reverted(0): CPS Score: P-Rep is already registered."); + } + + @Test + void registerPrepNotAPrep(){ + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + Executable register = () -> cpsScore.invoke(testingAccount6, "register_prep"); + expectErrorMessage(register, "Reverted(0): CPS Score: Not a P-Rep."); + } + + @Test + void registerPrepPrepInDenyList(){ + ArrayDB
denylist = mock(ArrayDB.class); + denylist.add(owner.getAddress()); + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); +// contextMock.when() + cpsScore.invoke(owner, "register_prep"); + // todo check when voiting logic is achecked + } + + @Test + void unregisterPrep(){ + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + + cpsScore.invoke(owner, "register_prep"); + assertEquals(1, ((List)(cpsScore.call("getPReps"))).size()); + + Map loginPrep = (Map) cpsScore.call("login_prep", owner.getAddress()); + System.out.println(loginPrep); + assertEquals(BigInteger.ONE, loginPrep.get("isRegistered")); + + cpsScore.invoke(owner, "unregister_prep"); + assertEquals(0, ((List)(cpsScore.call("getPReps"))).size()); + + loginPrep = (Map) cpsScore.call("loginPrep", owner.getAddress()); + System.out.println(loginPrep); + assertEquals(BigInteger.ZERO, loginPrep.get("isRegistered")); + } + + @Test + void unregisterPrepNotInValidPrep(){ + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + cpsScore.invoke(owner, "register_prep"); + + Executable unregister = () -> cpsScore.invoke(testingAccount1, "unregister_prep"); + expectErrorMessage(unregister, "Reverted(0): P-Rep is not registered yet."); + } + + @Test + void loginPrep(){ + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + cpsScore.invoke(owner, "register_prep"); + + Map loginPrep = (Map) cpsScore.call("loginPrep", owner.getAddress()); + assertEquals(BigInteger.ONE, loginPrep.get("isPRep")); + assertEquals(BigInteger.ONE, loginPrep.get("isRegistered")); + assertEquals(BigInteger.ZERO, loginPrep.get("payPenalty")); + assertEquals(BigInteger.ONE, loginPrep.get("votingPRep")); + + loginPrep = (Map) cpsScore.call("loginPrep", testingAccount.getAddress()); + assertEquals(BigInteger.ONE, loginPrep.get("isPRep")); + assertEquals(BigInteger.ZERO, loginPrep.get("isRegistered")); + assertEquals(BigInteger.ZERO, loginPrep.get("payPenalty")); + assertEquals(BigInteger.ZERO, loginPrep.get("votingPRep")); + + loginPrep = (Map) cpsScore.call("loginPrep", testingAccount6.getAddress()); + assertEquals(BigInteger.ZERO, loginPrep.get("isPRep")); + assertEquals(BigInteger.ZERO, loginPrep.get("isRegistered")); + assertEquals(BigInteger.ZERO, loginPrep.get("payPenalty")); + assertEquals(BigInteger.ZERO, loginPrep.get("votingPRep")); + + } + + @Test + void getPeriodStatus(){ + Map periodStatus = (Map) cpsScore.call("getPeriodStatus"); + assertEquals("None", periodStatus.get("previous_period_name")); + assertEquals("None", periodStatus.get("period_name")); + assertEquals(BigInteger.ZERO, periodStatus.get("next_block")); + assertEquals(BigInteger.ZERO, periodStatus.get("remaining_time")); + assertEquals(BigInteger.valueOf(1293600), periodStatus.get("period_span")); + + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + + periodStatus = (Map) cpsScore.call("getPeriodStatus"); + assertEquals("None", periodStatus.get("previous_period_name")); + assertEquals(APPLICATION_PERIOD, periodStatus.get("period_name")); + assertEquals(BigInteger.valueOf(1293600), periodStatus.get("period_span")); + System.out.println(periodStatus); + assertEquals(BigInteger.valueOf(Context.getBlockHeight()).add(BLOCKS_DAY_COUNT.multiply(DAY_COUNT)), periodStatus.get("next_block")); + } + + void setScoresMethod(){ + cpsScore.invoke(owner, "addAdmin", owner.getAddress()); + cpsScore.invoke(owner, "setCpsTreasuryScore", cpsTreasury); + cpsScore.invoke(owner, "setCpfTreasuryScore", cpfTreasury); + cpsScore.invoke(owner, "setBnusdScore", bnUSDScore); + + } + private void registerPrepsMethod(){ + setScoresMethod(); + List> prepDict = + List.of(Map.of("name", "owner", "address", owner.getAddress(), "power", BigInteger.valueOf(1000)), + Map.of("name", "testingAccount", "address", testingAccount.getAddress(), "power", BigInteger.valueOf(850)), + Map.of("name", "testingAccount1", "address", testingAccount1.getAddress(), "power", BigInteger.valueOf(770)), + Map.of("name", "testingAccount2" , "address", testingAccount2.getAddress(), "power", BigInteger.valueOf(800)), + Map.of("name", "testingAccount3", "address", testingAccount3.getAddress(), "power", BigInteger.valueOf(990)), + Map.of("name", "testingAccount4", "address", testingAccount4.getAddress(), "power", BigInteger.valueOf(500)), + Map.of("name", "testingAccount5", "address", testingAccount5.getAddress(), "power", BigInteger.valueOf(250)) + ); + Map preps = Map.of("preps", prepDict); + + addAdminMethod(); + doReturn(preps).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRepTerm")); + doReturn(prepDict.get(0)).when(scoreSpy).callScore(eq(Map.class), eq(SYSTEM_ADDRESS), eq("getPRep"), any()); + cpsScore.invoke(owner, "toggleMaintenance"); + cpsScore.invoke(owner, "setInitialBlock"); + cpsScore.invoke(owner, "register_prep"); + cpsScore.invoke(testingAccount, "register_prep"); + cpsScore.invoke(testingAccount1, "register_prep"); + cpsScore.invoke(testingAccount2, "register_prep"); + cpsScore.invoke(testingAccount3, "register_prep"); + cpsScore.invoke(testingAccount4, "register_prep"); + cpsScore.invoke(testingAccount5, "register_prep"); + } + + + @Test + void submitProposal(){ + submitProposalMethod(); + Map proposalDetails = (Map) cpsScore.call("getProposalDetailsByHash", "Proposal 1"); + System.out.println(proposalDetails); + assertEquals("Proposal 1", proposalDetails.get("ipfs_hash")); + assertEquals("Title", proposalDetails.get("project_title")); + assertEquals(2, proposalDetails.get("project_duration")); + assertEquals(testingAccount.getAddress(), proposalDetails.get("sponsor_address")); + assertEquals(owner.getAddress(), proposalDetails.get("contributor_address")); + assertEquals("_sponsor_pending", proposalDetails.get("status")); + assertEquals(BigInteger.valueOf(100).multiply(MULTIPLIER), proposalDetails.get("total_budget")); + Map proposalDetailsOfStatus = (Map) cpsScore.call("getProposalDetails", SPONSOR_PENDING, owner.getAddress(), 0, 10); + assertEquals(1, proposalDetailsOfStatus.get(COUNT)); + assertEquals(proposalDetails, ((List>)proposalDetailsOfStatus.get(DATA)).get(0)); + + Map proposalDetailsOfWallet = (Map) cpsScore.call("get_proposal_detail_by_wallet", owner.getAddress()); + assertEquals(proposalDetails, ((List>)proposalDetailsOfWallet.get(DATA)).get(0)); + assertEquals(1, proposalDetailsOfWallet.get(COUNT)); + + List proposalKeys = (List) cpsScore.call("getProposalKeys"); + assertEquals(List.of("Proposal 1"), proposalKeys); + } + + void submitProposalMethod(){ + registerPrepsMethod(); + ProposalAttributes proposalAttributes = new ProposalAttributes(); + proposalAttributes.ipfs_hash = "Proposal 1"; + proposalAttributes.project_title = "Title"; + proposalAttributes.project_duration = 2; + proposalAttributes.total_budget = BigInteger.valueOf(100); + proposalAttributes.token = bnUSD; + proposalAttributes.sponsor_address = testingAccount.getAddress(); + proposalAttributes.ipfs_link = "link"; + + Map remainingSwapAmount = Map.of( + "remaining_swap_amount", BigInteger.valueOf(1000).multiply(MULTIPLIER), + "maxCap", BigInteger.valueOf(1000).multiply(MULTIPLIER)); + doReturn(remainingSwapAmount).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_remaining_swap_amount")); + contextMock.when(() -> Context.getValue()).thenReturn(BigInteger.valueOf(50).multiply(MULTIPLIER)); + byte [] tx_hash = "transaction".getBytes(); + contextMock.when(() -> Context.getTransactionHash()).thenReturn(tx_hash); + doNothing().when(scoreSpy).callScore(eq(BigInteger.valueOf(25).multiply(MULTIPLIER)), eq(SYSTEM_ADDRESS), eq("burn")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(0)); + cpsScore.invoke(owner, "submitProposal", proposalAttributes); + + } + + @Test + void sponsorVote(){ + submitAndSponsorVote(); + Map proposalDetails = (Map) cpsScore.call("getProposalDetailsByHash", "Proposal 1"); + System.out.println(proposalDetails); + assertEquals(PENDING, proposalDetails.get("status")); + assertEquals(BOND_RECEIVED, proposalDetails.get("sponsor_deposit_status")); + Map>> projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + Map> amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(PENDING))); + + Map sponosrsRequest = (Map) cpsScore.call("getSponsorsRequests", APPROVED, testingAccount.getAddress(), 0, 10); + System.out.println("Sponsors request" + sponosrsRequest); + } + + void submitAndSponsorVote(){ + submitProposalMethod(); + contextMock.when(caller()).thenReturn(bnUSDScore); + JsonObject sponsorVoteParams = new JsonObject(); + sponsorVoteParams.add("method", "sponsor_vote"); + JsonObject params = new JsonObject(); + params.add(IPFS_HASH, "Proposal 1"); + params.add(VOTE, ACCEPT); + params.add(VOTE_REASON, "reason"); + sponsorVoteParams.add("params", params); + + cpsScore.invoke(testingAccount, "tokenFallback", testingAccount.getAddress(), BigInteger.valueOf(10).multiply(MULTIPLIER), sponsorVoteParams.toString().getBytes()); + } + + void updateNextBlock(){ + cpsScore.invoke(owner, "updateNextBlock", 0); + } + + @Test + @DisplayName("vote approve then change it to reject") + void voteProposal(){ + submitAndSponsorVote(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + cpsScore.invoke(owner, "voteProposal", "Proposal 1", APPROVE, "reason", false); + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("approved_votes")); + assertEquals(1, proposalDetails.get("approve_voters")); + assertEquals(BigInteger.ZERO, proposalDetails.get("rejected_votes")); + assertEquals(0, proposalDetails.get("reject_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("total_votes")); + + cpsScore.invoke(owner, "voteProposal", "Proposal 1", REJECT, "reason", true); + + proposalDetails = getProposalDetailsByHash("Proposal 1"); + + assertEquals(BigInteger.valueOf(0), proposalDetails.get("approved_votes")); + assertEquals(0, proposalDetails.get("approve_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("rejected_votes")); + assertEquals(1, proposalDetails.get("reject_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("total_votes")); + + assertEquals(1, cpsScore.call("checkChangeVote", owner.getAddress(), "Proposal 1", "proposal")); + + Map voteResult = (Map) cpsScore.call("getVoteResult", "Proposal 1"); + System.out.println(voteResult); + } + + @Test + @DisplayName("vote reject then change it to approve") + void voteProposal2(){ + submitAndSponsorVote(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + cpsScore.invoke(owner, "voteProposal", "Proposal 1", REJECT, "reason", false); + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + + assertEquals(BigInteger.valueOf(0), proposalDetails.get("approved_votes")); + assertEquals(0, proposalDetails.get("approve_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("rejected_votes")); + assertEquals(1, proposalDetails.get("reject_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("total_votes")); + + + cpsScore.invoke(owner, "voteProposal", "Proposal 1", APPROVE, "reason", true); + proposalDetails = getProposalDetailsByHash("Proposal 1"); + + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("approved_votes")); + assertEquals(1, proposalDetails.get("approve_voters")); + assertEquals(BigInteger.ZERO, proposalDetails.get("rejected_votes")); + assertEquals(0, proposalDetails.get("reject_voters")); + assertEquals(BigInteger.valueOf(1000), proposalDetails.get("total_votes")); + + assertEquals(1, cpsScore.call("checkChangeVote", owner.getAddress(), "Proposal 1", "proposal")); + } + + void voteProposalMethod(){ + submitAndSponsorVote(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + String[] proposal = new String[1]; + proposal[0] = "Proposal 1"; + cpsScore.invoke(owner, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + } + + @Test + void submitMultipleProposals(){ + registerPrepsMethod(); + Map remainingSwapAmount = Map.of( + "remaining_swap_amount", BigInteger.valueOf(1000).multiply(MULTIPLIER), + "maxCap", BigInteger.valueOf(1000).multiply(MULTIPLIER)); + doReturn(remainingSwapAmount).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_remaining_swap_amount")); + contextMock.when(() -> Context.getValue()).thenReturn(BigInteger.valueOf(50).multiply(MULTIPLIER)); + byte [] tx_hash = "transaction".getBytes(); + contextMock.when(() -> Context.getTransactionHash()).thenReturn(tx_hash); + doNothing().when(scoreSpy).callScore(eq(BigInteger.valueOf(25).multiply(MULTIPLIER)), eq(SYSTEM_ADDRESS), eq("burn")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(0)); + for (int i = 0; i < 10; i++) { + ProposalAttributes proposalAttributes = new ProposalAttributes(); + proposalAttributes.ipfs_hash = "Proposal " + i; + proposalAttributes.project_title = "Title"; + proposalAttributes.project_duration = 2; + proposalAttributes.total_budget = BigInteger.valueOf(100); + proposalAttributes.token = bnUSD; + proposalAttributes.sponsor_address = testingAccount.getAddress(); + proposalAttributes.ipfs_link = "link"; + cpsScore.invoke(owner, "submitProposal", proposalAttributes); + } + contextMock.when(caller()).thenReturn(bnUSDScore); + JsonObject sponsorVoteParams = new JsonObject(); + sponsorVoteParams.add("method", "sponsor_vote"); + JsonObject params = new JsonObject(); + for (int i = 0; i < 10; i++) { + params.add(IPFS_HASH, "Proposal " + i); + params.add(VOTE, ACCEPT); + params.add(VOTE_REASON, "reason"); + sponsorVoteParams.add("params", params); + + cpsScore.invoke(testingAccount, "tokenFallback", testingAccount.getAddress(), BigInteger.valueOf(10).multiply(MULTIPLIER), sponsorVoteParams.toString().getBytes()); + } + } + + @Test + void voteMultipleProposals(){ + submitMultipleProposals(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), any()); + String[] proposal = new String[]{"Proposal 0","Proposal 1","Proposal 2","Proposal 3","Proposal 4","Proposal 5", + "Proposal 6","Proposal 7","Proposal 8","Proposal 9"}; + + for (int i = 0; i < 10; i++) { + contextMock.when(caller()).thenReturn(owner.getAddress()); + cpsScore.invoke(owner, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProposal", "Proposal " + i, APPROVE, "reason", false); + + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProposal", "Proposal " + i, APPROVE, "reason", false); +} + + contextMock.when(caller()).thenReturn(owner.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + } + + void voteProposalMethodReject(){ + submitAndSponsorVote(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + String[] proposal = new String[1]; + proposal[0] = "Proposal 1"; + cpsScore.invoke(owner, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProposal", "Proposal 1", REJECT, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + } + + void updatePeriods(){ + // 1/4 + cpsScore.invoke(owner, "update_period"); + // 2/4 + cpsScore.invoke(owner, "update_period"); + // 3/4 + cpsScore.invoke(owner, "update_period"); + // 4/4 + cpsScore.invoke(owner, "update_period"); + } + + @Test + void updatePeriodAfterProposalVoting(){ + voteProposalMethod(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + updatePeriods(); + + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + Map activeProposals = (Map) cpsScore.call("getActiveProposals", 0); + assertEquals(ACTIVE, proposalDetails.get("status")); + assertEquals(List.of(proposalDetails), activeProposals.get(DATA)); + + Map sponosrsRequest = (Map) cpsScore.call("getSponsorsRequests", APPROVED, testingAccount.getAddress(), 0, 10); + System.out.println("Sponsors request" + sponosrsRequest); + + Map sponsorsRecord = (Map) cpsScore.call("getSponsorsRecord"); + assertEquals(1, sponsorsRecord.get(testingAccount.getAddress().toString())); + + Map>> projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + Map> amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(ACTIVE))); + + Map voteResult = (Map) cpsScore.call("getVoteResult", "Proposal 1"); + System.out.println("voteResult: " + voteResult); + } + + @Test + void rejectProposal(){ + voteProposalMethodReject(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + contextMock.when(() -> Context.transfer(any(), any())).thenAnswer((Answer) invocation -> null); + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + assertEquals(REJECTED, proposalDetails.get("status")); + + Map claimableSponsorBond = (Map) cpsScore.call("checkClaimableSponsorBond", testingAccount.getAddress()); + assertEquals(BigInteger.valueOf(10).multiply(MULTIPLIER), claimableSponsorBond.get(bnUSD)); + + Map voteResult = (Map) cpsScore.call("getVoteResult", "Proposal 1"); + System.out.println("voteResult: " + voteResult); + + Map proposalsHistory = (Map) cpsScore.call("getProposalsHistory", 0); + assertEquals(List.of(proposalDetails), proposalsHistory.get(DATA)); + } + + @Test + void submitProgressReport(){ + ProgressReportAttributes progressReport = new ProgressReportAttributes(); + progressReport.ipfs_hash = "Proposal 1"; + progressReport.report_hash = "Report 1"; + progressReport.ipfs_link = "Link"; + progressReport.progress_report_title = "Progress Report Title"; + progressReport.budget_adjustment = true; + progressReport.additional_budget = BigInteger.valueOf(10); + progressReport.additional_month = 1; + progressReport.percentage_completed = 50; + updatePeriodAfterProposalVoting(); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), any()); + cpsScore.invoke(owner, "toggleBudgetAdjustmentFeature"); + cpsScore.invoke(owner, "submitProgressReport", progressReport); + + @SuppressWarnings("unchecked") + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(progressReport.ipfs_hash, progressReportDetails.get("ipfs_hash")); + assertEquals(progressReport.report_hash, progressReportDetails.get("report_hash")); + assertEquals(progressReport.ipfs_link, progressReportDetails.get("ipfs_link")); + assertEquals(progressReport.progress_report_title, progressReportDetails.get("progress_report_title")); + assertEquals(progressReport.budget_adjustment, progressReportDetails.get("budget_adjustment")); + assertEquals(progressReport.additional_budget.multiply(MULTIPLIER), progressReportDetails.get("additional_budget")); + assertEquals(progressReport.additional_month, progressReportDetails.get("additional_month")); + assertEquals(progressReport.percentage_completed, progressReportDetails.get("percentage_completed")); + + List progressKeys = (List) cpsScore.call("getProgressKeys"); + assertEquals(List.of("Report 1"), progressKeys); + + Map progressReports = (Map) cpsScore.call("getProgressReports", WAITING, 0, 10); + System.out.println("Progerss reports: " + progressReports); + assertEquals(List.of(progressReportDetails), progressReports.get(DATA)); + assertEquals(1, progressReports.get(COUNT)); + + Map progressReportsByProposal = (Map) cpsScore.call("getProgressReportsByProposal", "Proposal 1"); + assertEquals(List.of(progressReportDetails), progressReportsByProposal.get(DATA)); + assertEquals(1, progressReportsByProposal.get(COUNT)); + } + + @Test + void voteProgressReport(){ + submitProgressReport(); + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + + @SuppressWarnings("unchecked") + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(7, progressReportDetails.get(TOTAL_VOTERS)); + assertEquals(7, progressReportDetails.get(BUDGET_APPROVE_VOTERS)); + assertEquals(7, progressReportDetails.get(APPROVE_VOTERS)); + + Map voteResult = (Map) cpsScore.call("getProgressReportResult", "Report 1"); + System.out.println("progress report vote Result: " + voteResult); + } + + @Test + void voteProgressReportVoteChangeFromApproveToReject(){ + submitProgressReport(); + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(BigInteger.valueOf(1000), progressReportDetails.get(APPROVED_VOTES)); + assertEquals(BigInteger.valueOf(0), progressReportDetails.get(REJECTED_VOTES)); + assertEquals(BigInteger.valueOf(1000), progressReportDetails.get(BUDGET_APPROVED_VOTES)); + assertEquals(BigInteger.valueOf(0), progressReportDetails.get(BUDGET_REJECTED_VOTES)); + assertEquals(1, progressReportDetails.get(APPROVE_VOTERS)); + assertEquals(0, progressReportDetails.get(REJECT_VOTERS)); + assertEquals(1, progressReportDetails.get(BUDGET_APPROVE_VOTERS)); + assertEquals(0, progressReportDetails.get(BUDGET_REJECT_VOTERS)); + + + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", REJECT, true); + + progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + assertEquals(BigInteger.valueOf(0), progressReportDetails.get(APPROVED_VOTES)); + assertEquals(BigInteger.valueOf(1000), progressReportDetails.get(REJECTED_VOTES)); + assertEquals(BigInteger.valueOf(0), progressReportDetails.get(BUDGET_APPROVED_VOTES)); + assertEquals(BigInteger.valueOf(1000), progressReportDetails.get(BUDGET_REJECTED_VOTES)); + assertEquals(0, progressReportDetails.get(APPROVE_VOTERS)); + assertEquals(1, progressReportDetails.get(REJECT_VOTERS)); + assertEquals(0, progressReportDetails.get(BUDGET_APPROVE_VOTERS)); + assertEquals(1, progressReportDetails.get(BUDGET_REJECT_VOTERS)); + + assertEquals(1, cpsScore.call("checkChangeVote", owner.getAddress(), "Report 1", "progress_reports")); + } + + @Test + void updatePeriodAfterProgressReportSubmission(){ + voteProgressReport(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("update_proposal_fund"), eq("Proposal 1"), eq(bnUSD), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(1)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_installment_to_contributor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_reward_to_sponsor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + @SuppressWarnings("unchecked") + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + System.out.println(progressReportDetails); + + assertEquals(APPROVED, progressReportDetails.get(STATUS)); + assertEquals(APPROVED, progressReportDetails.get(BUDGET_ADJUSTMENT_STATUS)); + } + + @Test + void submitProgressReportWithoutBudgetAdjustment(){ + ProgressReportAttributes progressReport = new ProgressReportAttributes(); + progressReport.ipfs_hash = "Proposal 1"; + progressReport.report_hash = "Report 1"; + progressReport.ipfs_link = "Link"; + progressReport.progress_report_title = "Progress Report Title"; + progressReport.budget_adjustment = false; + progressReport.additional_budget = BigInteger.valueOf(0); + progressReport.additional_month = 0; + progressReport.percentage_completed = 50; + updatePeriodAfterProposalVoting(); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), any()); + cpsScore.invoke(owner, "toggleBudgetAdjustmentFeature"); + cpsScore.invoke(owner, "submitProgressReport", progressReport); + } + + @Test + void voteProgressReportAfterSubmittingProposalWithoutBudgetAdjustment(){ + submitProgressReportWithoutBudgetAdjustment(); + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProgressReport", "Proposal 1", "Report 1", APPROVE, "reason", APPROVE, false); + + @SuppressWarnings("unchecked") + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(7, progressReportDetails.get(TOTAL_VOTERS)); + assertEquals(7, progressReportDetails.get(APPROVE_VOTERS)); + + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("update_proposal_fund"), eq("Proposal 1"), eq(bnUSD), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(1)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_installment_to_contributor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_reward_to_sponsor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + System.out.println(progressReportDetails); + + ProgressReportAttributes progressReport = new ProgressReportAttributes(); + progressReport.ipfs_hash = "Proposal 1"; + progressReport.report_hash = "Report 2"; + progressReport.ipfs_link = "Link"; + progressReport.progress_report_title = "Progress Report Title"; + progressReport.budget_adjustment = false; + progressReport.additional_budget = BigInteger.valueOf(0); + progressReport.additional_month = 0; + progressReport.percentage_completed = 50; + + cpsScore.invoke(owner, "submitProgressReport", progressReport); + + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProgressReport", "Proposal 1", "Report 2", APPROVE, "reason", APPROVE, false); + + progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(7, progressReportDetails.get(TOTAL_VOTERS)); + assertEquals(7, progressReportDetails.get(APPROVE_VOTERS)); + + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("update_proposal_fund"), eq("Proposal 1"), eq(bnUSD), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(1)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_installment_to_contributor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_reward_to_sponsor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + Map proposalDetails = (Map) cpsScore.call("getProposalDetailsByHash", "Proposal 1"); + assertEquals(COMPLETED, proposalDetails.get("status")); + + Map sponsorsRecord = (Map) cpsScore.call("getSponsorsRecord"); + assertEquals(1, sponsorsRecord.get(testingAccount.getAddress().toString())); + + Map>> projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + System.out.println("Project Amount: " + projectAmounts); + Map> amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(COMPLETED))); + + Map progressReportsByProposal = (Map) cpsScore.call("getProgressReportsByProposal", "Proposal 1"); + assertEquals(2, progressReportsByProposal.get(COUNT)); + + Map claimableSponsorBond = (Map) cpsScore.call("checkClaimableSponsorBond", testingAccount.getAddress()); + assertEquals(BigInteger.valueOf(10).multiply(MULTIPLIER), claimableSponsorBond.get(bnUSD)); + + Map proposalsHistory = (Map) cpsScore.call("getProposalsHistory", 0); + assertEquals(List.of(proposalDetails), proposalsHistory.get(DATA)); + } + + @Test + void claimSponsorBond(){ + voteProgressReportAfterSubmittingProposalWithoutBudgetAdjustment(); + doNothing().when(scoreSpy).callScore(eq(bnUSDScore), eq("transfer"), eq(testingAccount.getAddress()), eq(BigInteger.valueOf(10).multiply(MULTIPLIER))); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "claimSponsorBond"); + Map claimableSponsorBond = (Map) cpsScore.call("checkClaimableSponsorBond", testingAccount.getAddress()); + assertEquals(BigInteger.valueOf(0), claimableSponsorBond.get(bnUSD)); + + } + + @Test + void sortPriorityProposals(){ + voteMultipleProposals(); + List proposal = List.of("Proposal 0","Proposal 1","Proposal 2","Proposal 3","Proposal 4","Proposal 5", + "Proposal 6","Proposal 7","Proposal 8","Proposal 9"); +// @SuppressWarnings("unchecked") + List proposalList = (List) cpsScore.call("sortPriorityProposals"); + assertEquals(proposal, proposalList); + + @SuppressWarnings("unchecked") + Map priorityVoteResult = (Map) cpsScore.call("getPriorityVoteResult"); + System.out.println(priorityVoteResult); + } + + + @Test + void setCpsTreasury(){ + contextMock.when(caller()).thenReturn(owner.getAddress()); + addAdminMethod(); + cpsScore.invoke(owner, "set_cps_treasury_score", cpsTreasury); + assertEquals(cpsTreasury, cpsScore.call("get_cps_treasury_score")); + } + + @Test + void setCpfTreasury(){ + addAdminMethod(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + cpsScore.invoke(owner, "set_cpf_treasury_score", cpfTreasury); + assertEquals(cpfTreasury, cpsScore.call("get_cpf_treasury_score")); + } + + @Test + void setbnUSDScore(){ + addAdminMethod(); + contextMock.when(caller()).thenReturn(owner.getAddress()); + cpsScore.invoke(owner, "setBnusdScore", bnUSDScore); + assertEquals(bnUSDScore, cpsScore.call("getBnusdScore")); + } + + @Test + void toggleMaintenanceMode(){ + addAdminMethod(); + cpsScore.invoke(owner, "toggleMaintenance"); + assertEquals(Boolean.FALSE, cpsScore.call("getMaintenanceMode")); + } + + @Test + void payPrepPenalty(){ + submitAndSponsorVote(); + BigInteger[] penaltyAmount = new BigInteger[3]; + penaltyAmount[0] = BigInteger.valueOf(5); + penaltyAmount[1] = BigInteger.valueOf(10); + penaltyAmount[2] = BigInteger.valueOf(15); + contextMock.when(caller()).thenReturn(owner.getAddress()); + cpsScore.invoke(owner, "set_prep_penalty_amount", (Object) penaltyAmount); + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + String[] proposal = new String[1]; + proposal[0] = "Proposal 1"; + cpsScore.invoke(owner, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProposal", "Proposal 1", APPROVE, "reason", false); + cpsScore.invoke(owner, "votePriority", (Object) proposal); + + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + updatePeriods(); + + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + + + @SuppressWarnings("unchecked") + List
denyList = (List
) cpsScore.call("get_denylist"); + assertEquals(List.of(testingAccount5.getAddress()), denyList); + + @SuppressWarnings("unchecked") + Map loginPrep = (Map) cpsScore.call("loginPrep", testingAccount5.getAddress()); + System.out.println(loginPrep); + + JsonObject payPenalty = new JsonObject(); + payPenalty.add("method", "pay_prep_penalty"); + JsonObject params = new JsonObject(); + payPenalty.add("params", params); + + JsonObject burnTokens = new JsonObject(); + burnTokens.add("method", "burn_amount"); + doNothing().when(scoreSpy).callScore(eq(bnUSDScore), eq("transfer"), eq(cpfTreasury), eq(new BigInteger("5000000000000000000")), eq(burnTokens.toString().getBytes())); + contextMock.when(caller()).thenReturn(bnUSDScore); + cpsScore.invoke(owner, "tokenFallback", testingAccount5.getAddress(), new BigInteger("5000000000000000000"), payPenalty.toString().getBytes()); + } + + @Test + void disqualifyProposal(){ + updatePeriodAfterProposalVoting(); + updateNextBlock(); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("disqualify_project"), eq("Proposal 1")); + + JsonObject disqualifyProject = new JsonObject(); + disqualifyProject.add("method", "burn_amount"); + JsonObject params = new JsonObject(); + params.add(SPONSOR_ADDRESS, testingAccount.getAddress().toString()); + disqualifyProject.add("params", params); + + doNothing().when(scoreSpy).callScore(eq(bnUSDScore), eq("transfer"), eq(cpfTreasury), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(disqualifyProject.toString().getBytes())); + cpsScore.invoke(owner, "update_period"); + Map proposalDetails = getProposalDetailsByHash("Proposal 1"); + assertEquals(PAUSED, proposalDetails.get(STATUS)); + + Map sponosrsRequest = (Map) cpsScore.call("getSponsorsRequests", APPROVED, testingAccount.getAddress(), 0, 10); + System.out.println("Sponsors request::" + sponosrsRequest); + + Map sponsorsRecord = (Map) cpsScore.call("getSponsorsRecord"); + assertEquals(1, sponsorsRecord.get(testingAccount.getAddress().toString())); + + Map>> projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + Map> amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(PAUSED))); + + updateNextBlock(); + cpsScore.invoke(owner, "update_period"); + proposalDetails = getProposalDetailsByHash("Proposal 1"); + assertEquals(DISQUALIFIED, proposalDetails.get(STATUS)); + assertEquals(BOND_CANCELLED, proposalDetails.get(SPONSOR_DEPOSIT_STATUS)); + + projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(DISQUALIFIED))); + + Map proposalsHistory = (Map) cpsScore.call("getProposalsHistory", 0); + assertEquals(List.of(proposalDetails), proposalsHistory.get(DATA)); + } + + @Test + void getContributors(){ + submitAndSponsorVote(); + @SuppressWarnings("unchecked") + List
contributors = (List
) cpsScore.call("getContributors"); + assertEquals(List.of(owner.getAddress()), contributors); + } + + @Test + void rejectSponsorVote(){ + submitProposalMethod(); + contextMock.when(caller()).thenReturn(bnUSDScore); + contextMock.when(() -> Context.transfer(any(), any())).thenAnswer((Answer) invocation -> null); + JsonObject sponsorVoteParams = new JsonObject(); + sponsorVoteParams.add("method", "sponsor_vote"); + JsonObject params = new JsonObject(); + params.add(IPFS_HASH, "Proposal 1"); + params.add(VOTE, REJECT); + params.add(VOTE_REASON, "reason"); + sponsorVoteParams.add("params", params); + + cpsScore.invoke(testingAccount, "tokenFallback", testingAccount.getAddress(), BigInteger.valueOf(0), sponsorVoteParams.toString().getBytes()); + } + + @Test + void disqualifyProjectByRejectingProgressReport(){ + submitProgressReportWithoutBudgetAdjustment(); + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + + JsonObject disqualifyProject = new JsonObject(); + disqualifyProject.add("method", "burn_amount"); + JsonObject params = new JsonObject(); + params.add(SPONSOR_ADDRESS, testingAccount.getAddress().toString()); + disqualifyProject.add("params", params); + + doNothing().when(scoreSpy).callScore(eq(bnUSDScore), eq("transfer"), eq(cpfTreasury), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(disqualifyProject.toString().getBytes())); + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProgressReport", "Proposal 1", "Report 1", REJECT, "reason", APPROVE, false); + + @SuppressWarnings("unchecked") + Map progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(7, progressReportDetails.get(TOTAL_VOTERS)); + assertEquals(7, progressReportDetails.get(REJECT_VOTERS)); + + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + Map totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("update_proposal_fund"), eq("Proposal 1"), eq(bnUSD), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(1)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_installment_to_contributor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_reward_to_sponsor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + System.out.println(progressReportDetails); + + ProgressReportAttributes progressReport = new ProgressReportAttributes(); + progressReport.ipfs_hash = "Proposal 1"; + progressReport.report_hash = "Report 2"; + progressReport.ipfs_link = "Link"; + progressReport.progress_report_title = "Progress Report Title"; + progressReport.budget_adjustment = false; + progressReport.additional_budget = BigInteger.valueOf(0); + progressReport.additional_month = 0; + progressReport.percentage_completed = 50; + + cpsScore.invoke(owner, "submitProgressReport", progressReport); + + updateNextBlock(); + cpsScore.invoke(owner, "updatePeriod"); + getPeriodStatusMethod(); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("swap_tokens"), eq(8)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("disqualify_project"), eq("Proposal 1")); + cpsScore.invoke(owner, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount.getAddress()); + cpsScore.invoke(testingAccount, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount1.getAddress()); + cpsScore.invoke(testingAccount1, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount2.getAddress()); + cpsScore.invoke(testingAccount2, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount3.getAddress()); + cpsScore.invoke(testingAccount3, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount4.getAddress()); + cpsScore.invoke(testingAccount4, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + contextMock.when(caller()).thenReturn(testingAccount5.getAddress()); + cpsScore.invoke(testingAccount5, "voteProgressReport", "Proposal 1", "Report 2", REJECT, "reason", APPROVE, false); + + progressReportDetails = (Map) cpsScore.call("getProgressReportsByHash", "Report 1"); + + assertEquals(7, progressReportDetails.get(TOTAL_VOTERS)); + assertEquals(7, progressReportDetails.get(REJECT_VOTERS)); + + contextMock.when(caller()).thenReturn(owner.getAddress()); + updateNextBlock(); + + totalFunds = Map.of( + Constants.ICX, BigInteger.valueOf(1000).multiply(MULTIPLIER), + bnUSD, BigInteger.valueOf(1000).multiply(MULTIPLIER) + ); + + doReturn(totalFunds).when(scoreSpy).callScore(eq(Map.class), eq(cpfTreasury), eq("get_total_funds")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("transfer_proposal_fund_to_cps_treasury"), + eq("Proposal 1"), eq(2), eq(testingAccount.getAddress()), eq(owner.getAddress()), + eq(bnUSD), eq(BigInteger.valueOf(100).multiply(MULTIPLIER))); + + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("reset_swap_state")); + doNothing().when(scoreSpy).callScore(eq(cpfTreasury), eq("update_proposal_fund"), eq("Proposal 1"), eq(bnUSD), eq(BigInteger.valueOf(10).multiply(MULTIPLIER)), eq(1)); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_installment_to_contributor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(cpsTreasury), eq("send_reward_to_sponsor"), eq("Proposal 1")); + doNothing().when(scoreSpy).callScore(eq(BigInteger.ZERO), eq(SYSTEM_ADDRESS), eq("burn")); + updatePeriods(); + + Map proposalDetails = (Map) cpsScore.call("getProposalDetailsByHash", "Proposal 1"); + assertEquals(DISQUALIFIED, proposalDetails.get("status")); + + Map>> projectAmounts = (Map>>) cpsScore.call("get_project_amounts"); + System.out.println("Project Amount: " + projectAmounts); + Map> amount = Map.of( + AMOUNT, Map.of( + Constants.ICX, BigInteger.ZERO, + bnUSD, BigInteger.valueOf(100).multiply(MULTIPLIER) + ) + ); + assertEquals(amount, (projectAmounts.get(DISQUALIFIED))); + + Map voteResult = (Map) cpsScore.call("getProgressReportResult", "Report 1"); + System.out.println("progress report vote Result of Report 1: " + voteResult); + + voteResult = (Map) cpsScore.call("getProgressReportResult", "Report 2"); + System.out.println("progress report vote Result of Report 2: " + voteResult); + } + + @Test + void setSwapCount(){ + addAdminMethod(); + cpsScore.invoke(owner, "set_swap_count", 10); + assertEquals(10, cpsScore.call("getSwapCount")); + } + + @Test + void fallback(){ + Executable fallback = () -> cpsScore.invoke(owner, "fallback"); + expectErrorMessage(fallback, "Reverted(0): " + TAG +": ICX can only be sent while submitting a proposal or paying the penalty."); + } + + @Test + void getPeriodCount(){ + assertEquals(20, cpsScore.call("getPeriodCount")); + } + + + void getPeriodStatusMethod(){ + Map periodStatus = (Map) cpsScore.call("getPeriodStatus"); + + System.out.println(periodStatus); + } + + Map getProposalDetailsByHash(String ipfs_hash){ + return (Map) cpsScore.call("getProposalDetailsByHash", ipfs_hash); + } + public MockedStatic.Verification caller(){ + return () -> Context.getCaller(); + } +} diff --git a/CPSTreasury/build.gradle b/CPSTreasury/build.gradle new file mode 100644 index 00000000..0fd824d8 --- /dev/null +++ b/CPSTreasury/build.gradle @@ -0,0 +1,87 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.2' + + implementation project(':score-lib') + implementation 'com.github.sink772:minimal-json:0.9.7' + implementation 'foundation.icon:javaee-scorex:0.5.3' + testImplementation project(':test-lib') + testImplementation 'foundation.icon:javaee-unittest:0.9.7' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.9.0' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.0' + testImplementation('org.mockito:mockito-inline:4.8.0') + intTestImplementation 'foundation.icon:icon-sdk:2.2.0' + intTestImplementation project(":score-client") + intTestAnnotationProcessor project(":score-client") +} + +optimizedJar { + mainClassName = 'community.icon.cps.score.cpstreasury.CPSTreasury' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + to = 'cxabc97ed26cb147eeef55b1728102aea25af8f62f' + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + sejong{ + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + to = 'cxb33f0187a22a41c3ecb05a9756af320e98badb7d' + } + berlin{ + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + to = 'cx3e2a9648e4365added55bfa6a84e515d99f1bb67' + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + } +} + +test { + useJUnitPlatform { + excludeTags("integration") + } +} + +sourceSets { + intTest { + java.srcDirs = [file('src/intTest/java')] + + compileClasspath += sourceSets.main.output + configurations.testRuntime + runtimeClasspath += output + compileClasspath + } +} + +task integrationTest(type: Test) { + useJUnitPlatform() + + + rootProject.allprojects { + if (it.getTasks().findByName('optimizedJar')) { + dependsOn(it.getTasks().getByName('optimizedJar')) + } + } + + options { + testLogging.showStandardStreams = true + description = 'Runs integration tests.' + group = 'verification' + + testClassesDirs = sourceSets.intTest.output.classesDirs + classpath = sourceSets.intTest.runtimeClasspath + } +} \ No newline at end of file diff --git a/CPSTreasury/src/intTest/java/community/icon/cps/score/cpstreasury/CPSTreasuryIntTest.java b/CPSTreasury/src/intTest/java/community/icon/cps/score/cpstreasury/CPSTreasuryIntTest.java new file mode 100644 index 00000000..cc64d126 --- /dev/null +++ b/CPSTreasury/src/intTest/java/community/icon/cps/score/cpstreasury/CPSTreasuryIntTest.java @@ -0,0 +1,393 @@ +//package community.icon.cps.score.cpstreasury; +// +//import com.eclipsesource.json.JsonObject; +//import community.icon.cps.score.lib.interfaces.*; +// +//import community.icon.cps.score.test.integration.CPS; +//import foundation.icon.icx.KeyWallet; +//import foundation.icon.icx.Wallet; +//import static org.junit.jupiter.api.Assertions.assertEquals; +// +//import foundation.icon.score.client.DefaultScoreClient; +//import org.junit.jupiter.api.*; +//import score.Address; +// +//import java.math.BigInteger; +//import java.util.Map; +// +//import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProposalAttributes; +//import static community.icon.cps.score.lib.interfaces.CPSCoreInterface.ProgressReportAttributes; +// +// +//@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +//public class CPSTreasuryIntTest { +// private static CPS cps; +// private final KeyWallet tester = cps.user; +// private final KeyWallet tester2 = cps.testUser; +// +// private final KeyWallet prepWallet1 = cps.prepWallet1; +// private final KeyWallet prepWallet2 = cps.prepWallet2; +// private final KeyWallet prepWallet3 = cps.prepWallet3; +// private final KeyWallet prepWallet4 = cps.prepWallet4; +// private final KeyWallet prepWallet5 = cps.prepWallet5; +// private final KeyWallet prepWallet6 = cps.prepWallet6; +// private final KeyWallet prepWallet7 = cps.prepWallet7; +// public static final BigInteger EXA = BigInteger.valueOf(1_000_000_000_000_000_000L); +// +// private static Wallet owner; +// +// private static CPSTreasuryInterfaceScoreClient cpsTreasury; +// private static CPFTreasuryInterfaceScoreClient cpfTreasury; +// private static CPSCoreInterfaceScoreClient cpsMain; +// +// private static DexInterfaceScoreClient dex; +// private static RouterInterfaceScoreClient router; +// private static bnUSDInterfaceScoreClient bnusd; +// private static sICXInterfaceScoreClient sicx; +// +// DefaultScoreClient clientWithTester = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , tester, cps.cpsCore._address()); +// +// DefaultScoreClient clientWithTester2 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , tester2, cps.cpsCore._address()); +// +// DefaultScoreClient prepClient1 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet1, cps.cpsCore._address()); +// +// DefaultScoreClient bnUSDClient2 = new DefaultScoreClient(cps.bnusd.endpoint(), cps.bnusd._nid() +// , prepWallet2, cps.bnusd._address()); +// +// +// DefaultScoreClient prepClient2 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet2, cps.cpsCore._address()); +// DefaultScoreClient prepClient3 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet3, cps.cpsCore._address()); +// DefaultScoreClient prepClient4 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet4, cps.cpsCore._address()); +// DefaultScoreClient prepClient5 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet5, cps.cpsCore._address()); +// DefaultScoreClient prepClient6 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet6, cps.cpsCore._address()); +// DefaultScoreClient prepClient7 = new DefaultScoreClient(cps.cpsCore.endpoint(), cps.cpsCore._nid() +// , prepWallet7, cps.cpsCore._address()); +// +// @BeforeAll +// static void setup() throws Exception { +// cps = new CPS(); +// cps.setupCPS(); +// owner = cps.owner; +// cpsMain = new CPSCoreInterfaceScoreClient(cps.cpsCore); +// cpsTreasury = new CPSTreasuryInterfaceScoreClient(cps.cpsTreasury); +// cpfTreasury = new CPFTreasuryInterfaceScoreClient(cps.cpfTreasury); +// +// dex = new DexInterfaceScoreClient(cps.dex); +// bnusd = new bnUSDInterfaceScoreClient(cps.bnusd); +// sicx = new sICXInterfaceScoreClient(cps.sicx); +// router = new RouterInterfaceScoreClient(cps.router); +// +// } +// +// CPSCoreInterfaceScoreClient cpsMainTestClient1 = new CPSCoreInterfaceScoreClient(clientWithTester); +// CPSCoreInterfaceScoreClient cpsMainTestClient2 = new CPSCoreInterfaceScoreClient(clientWithTester2); +// CPSCoreInterfaceScoreClient prepSender1 = new CPSCoreInterfaceScoreClient(prepClient1); +// bnUSDInterfaceScoreClient bnUSDSender2 = new bnUSDInterfaceScoreClient(bnUSDClient2); +// CPSCoreInterfaceScoreClient prepSender2 = new CPSCoreInterfaceScoreClient(prepClient2); +// CPSCoreInterfaceScoreClient prepSender3 = new CPSCoreInterfaceScoreClient(prepClient3); +// CPSCoreInterfaceScoreClient prepSender4 = new CPSCoreInterfaceScoreClient(prepClient4); +// CPSCoreInterfaceScoreClient prepSender5 = new CPSCoreInterfaceScoreClient(prepClient5); +// CPSCoreInterfaceScoreClient prepSender6 = new CPSCoreInterfaceScoreClient(prepClient6); +// CPSCoreInterfaceScoreClient prepSender7 = new CPSCoreInterfaceScoreClient(prepClient7); +// +// @Test +// @Order(1) +// void name(){ +// assertEquals(cpsTreasury.name(), "CPS_TREASURY"); +// assertEquals(cpfTreasury.name(), "CPF_TREASURY"); +// assertEquals(cpsMain.name(), "CPS Score"); +// } +// +// void addAdminMethod(){ +// cpsMain.add_admin(Address.fromString(tester.getAddress().toString())); +// cpsMain.add_admin(Address.fromString(owner.getAddress().toString())); +// } +// +// private void setInitialBlockMethod(){ +// cpsMain.set_initialBlock(); +// } +// +// void setScores(){ +// addAdminMethod(); +// cpsMain.set_cps_treasury_score(cpsTreasury._address()); +// assertEquals(cpsMain.getCpsTreasuryScore(), cpsTreasury._address()); +// cpsMain.set_cpf_treasury_score(cpfTreasury._address()); +// assertEquals(cpsMain.get_cpf_treasury_score(), cpfTreasury._address()); +// cpsMain.set_bnUSD_score(bnusd._address()); +// assertEquals(cpsMain.get_bnUSD_score(), bnusd._address()); +// +// cpsTreasury.setCpsScore(cpsMain._address()); +// assertEquals(cpsTreasury.getCpsScore(), cpsMain._address()); +// cpsTreasury.setCpfTreasuryScore(cpfTreasury._address()); +// assertEquals(cpsTreasury.getCpfTreasuryScore(), cpfTreasury._address()); +// cpsTreasury.setBnUSDScore(bnusd._address()); +// assertEquals(cpsTreasury.getBnUSDScore(), bnusd._address()); +// +// cpfTreasury.setCpsScore(cpsMain._address()); +// assertEquals(cpfTreasury.getCpsScore(), cpsMain._address()); +// cpfTreasury.setCpsTreasuryScore(cpsTreasury._address()); +// assertEquals(cpfTreasury.getCpsTreasuryScore(), cpsTreasury._address()); +// cpfTreasury.setBnUSDScore(bnusd._address()); +// assertEquals(cpfTreasury.getBnUSDScore(), bnusd._address()); +// cpfTreasury.setRouterScore(router._address()); +// assertEquals(cpfTreasury.getRouterScore(), router._address()); +// cpfTreasury.setDexScore(dex._address()); +// assertEquals(cpfTreasury.getDexScore(), dex._address()); +// cpfTreasury.setSicxScore(sicx._address()); +// assertEquals(cpfTreasury.getSicxScore(), sicx._address()); +// +// dex.setSicxScore(sicx._address()); +// } +// +// void setCPFTreasuryContract(){ +// setScores(); +// cpfTreasury.setMaximumTreasuryFundBnusd(BigInteger.valueOf(1000).multiply(EXA)); +// cpfTreasury.setMaximumTreasuryFundIcx(BigInteger.valueOf(1000).multiply(EXA)); +// +// cpfTreasury.add_fund(BigInteger.valueOf(1000).multiply(EXA)); +// System.out.println(cpfTreasury.get_remaining_swap_amount()); +// +// setRouterScore(); +// setDexScore(); +// cpfTreasury.swapIcxBnusd(BigInteger.valueOf(200).multiply(EXA)); +// System.out.println(bnusd.balanceOf(cpfTreasury._address())); +// } +// +// void setRouterScore(){ +//// setScores(); +// bnusd.transfer(router._address(), BigInteger.valueOf(100000).multiply(EXA), null); +// bnusd.transfer(Address.fromString(prepWallet2.getAddress().toString()), BigInteger.valueOf(1000).multiply(EXA), null); +// System.out.println(bnusd.balanceOf(router._address())); +// } +// +// void setDexScore(){ +//// setScores(); +// sicx.transfer(dex._address(), BigInteger.valueOf(100000).multiply(EXA), null); +// System.out.println(sicx.balanceOf(dex._address())); +// } +// +// private void registerPrepMethod(){ +// setInitialBlockMethod(); +// cpsMain.toggle_maintenance(); +// prepSender1.register_prep(); +// prepSender2.register_prep(); +// prepSender3.register_prep(); +// prepSender4.register_prep(); +// prepSender5.register_prep(); +// prepSender6.register_prep(); +// prepSender7.register_prep(); +// } +// +// @Test +// @Order(2) +// void submitProposal(){ +// setCPFTreasuryContract(); +// registerPrepMethod(); +// ProposalAttributes proposalAttributes = new ProposalAttributes(); +// proposalAttributes.ipfs_hash = "Proposal 1"; +// proposalAttributes.project_title = "Proposal 1 title"; +// proposalAttributes.ipfs_link = "Link"; +// proposalAttributes.project_duration = 3; +// proposalAttributes.token = "bnUSD"; +// proposalAttributes.sponsor_address = Address.fromString(prepWallet2.getAddress().toString()); +// proposalAttributes.total_budget = BigInteger.valueOf(100); +// +// cpsMain.submit_proposal(BigInteger.valueOf(50).multiply(EXA), proposalAttributes); +// +// Map proposalDetails = cpsMain.get_proposal_details_by_hash("Proposal 1"); +// assertEquals(proposalDetails.get("ipfs_hash"), "Proposal 1"); +// assertEquals(proposalDetails.get("project_duration"), "0x" + Integer.toHexString(3)); +// assertEquals(proposalDetails.get("project_title"), "Proposal 1 title"); +// assertEquals(proposalDetails.get("status"), "_sponsor_pending"); +// assertEquals(proposalDetails.get("token"), "bnUSD"); +// String totalBudgetString = ((String) proposalDetails.get("total_budget")).substring(2); +// assertEquals(new BigInteger(totalBudgetString, 16), BigInteger.valueOf(100).multiply(EXA)); +// } +// +// @Test +// @Order(3) +// void voteProposal(){ +// JsonObject sponsorVoteParams = new JsonObject(); +// sponsorVoteParams.add("method", "sponsor_vote"); +// JsonObject params = new JsonObject(); +// params.add("ipfs_hash", "Proposal 1"); +// params.add("vote", "_accept"); +// params.add("vote_reason", "sponsor_reason"); +// sponsorVoteParams.add("params", params); +// logger("sponsor vote start"); +// bnUSDSender2.transfer(cpsMain._address(), BigInteger.valueOf(10).multiply(EXA), sponsorVoteParams.toString().getBytes()); +// logger("sponosr vote complete"); +// +// cpsMain.update_next_block(0); +// logger("period ended"); +// +// cpsMain.update_period(); +// logger("updated to next period"); +// +// prepSender1.vote_proposal("Proposal 1", "_approve", "reason", false); +// prepSender2.vote_proposal("Proposal 1", "_approve", "reason", false); +// prepSender3.vote_proposal("Proposal 1", "_approve", "reason", false); +// prepSender4.vote_proposal("Proposal 1", "_approve", "reason", false); +// prepSender5.vote_proposal("Proposal 1", "_approve", "reason", false); +// prepSender6.vote_proposal("Proposal 1", "_reject", "reason", false); +// prepSender7.vote_proposal("Proposal 1", "_reject", "reason", false); +// +// logger("completed vote"); +// +// Map proposalDetails = cpsMain.get_proposal_details_by_hash("Proposal 1"); +// logger(proposalDetails); +// +// assertEquals("0x" + Integer.toHexString(5), proposalDetails.get("approve_voters")); +// assertEquals("0x" + Integer.toHexString(2), proposalDetails.get("reject_voters")); +// assertEquals("0x" + Integer.toHexString(7), proposalDetails.get("total_voters")); +// assertEquals(BigInteger.valueOf(4000).multiply(EXA), new BigInteger(((String) proposalDetails.get("approved_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(1600).multiply(EXA), new BigInteger(((String) proposalDetails.get("rejected_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(5600).multiply(EXA), new BigInteger(((String) proposalDetails.get("total_votes")).substring(2), 16)); +// } +// +// @Test +// @Order(4) +// void votePriority(){ +// String[] priorityVote = {"Proposal 1"}; +// logger("priority voting started"); +// prepSender1.votePriority(priorityVote); +// prepSender2.votePriority(priorityVote); +// prepSender3.votePriority(priorityVote); +// prepSender4.votePriority(priorityVote); +// prepSender5.votePriority(priorityVote); +// prepSender6.votePriority(priorityVote); +// prepSender7.votePriority(priorityVote); +// logger("priority vote complete"); +// Map priorityVoteResult = cpsMain.getPriorityVoteResult(); +// logger(priorityVoteResult); +// assertEquals(7, priorityVoteResult.get("Proposal 1")); +// } +// +// @Test +// @Order(5) +// void updatePeriodAfterVoting(){ +// cpsMain.update_next_block(0); +// logger("end period"); +// +// cpsMain.update_period(); +// logger("end first update period period"); +// +// cpsMain.update_period(); +// logger("end second update period period"); +// +// cpsMain.update_period(); +// logger("end third update period period"); +// +// cpsMain.update_period(); +// logger("end fourth update period period"); +// +// Map proposalDetails = cpsMain.get_proposal_details_by_hash("Proposal 1"); +// logger(proposalDetails); +// +// assertEquals("bond_approved", proposalDetails.get("sponsor_deposit_status")); +// assertEquals("_active", proposalDetails.get("status")); +// } +// +// @Test +// @Order(6) +// void submitProgressReport(){ +// ProgressReportAttributes progress = new ProgressReportAttributes(); +// progress.ipfs_hash = "Proposal 1"; +// progress.report_hash = "Report 1"; +// progress.progress_report_title = "Progress Report Title"; +// progress.ipfs_link = "Link"; +// progress.additional_budget = BigInteger.valueOf(50); +// progress.budget_adjustment = Boolean.TRUE; +// progress.percentage_completed = 10; +// progress.additional_month = 2; +// cpsMain.toggleBudgetAdjustmentFeature(); +// cpsMain.submit_progress_report(progress); +// +// Map progressReports = cpsMain.get_progress_reports_by_hash("Report 1"); +// logger(progressReports); +// +// assertEquals("Report 1", progressReports.get("report_hash")); +// assertEquals("_waiting", progressReports.get("status")); +// assertEquals("Progress Report Title", progressReports.get("progress_report_title")); +// assertEquals("Proposal 1", progressReports.get("ipfs_hash")); +// assertEquals("_pending", progressReports.get("budget_adjustment_status")); +// assertEquals("0x" + Integer.toHexString(2), progressReports.get("additional_month")); +// assertEquals(BigInteger.valueOf(50).multiply(EXA), new BigInteger(((String)progressReports.get("additional_budget")).substring(2), 16)); +// } +// +// @Test +// @Order(7) +// void vote_progress_report(){ +// cpsMain.update_next_block(0); +// logger("period ended"); +// +// logger(cpsMain.get_PReps()); +// +// cpsMain.update_period(); +// logger("updated to next period"); +// +// logger(cpsMain.get_period_status()); +// +// prepSender1.vote_progress_report("Proposal 1", "Report 1", "_approve", "vote reason", "_approve", false); +// prepSender2.vote_progress_report("Proposal 1", "Report 1", "_approve", "vote reason", "_approve", false); +// prepSender3.vote_progress_report("Proposal 1", "Report 1", "_approve", "vote reason", "_approve", false); +// prepSender4.vote_progress_report("Proposal 1", "Report 1", "_approve", "vote reason", "_approve", false); +// prepSender5.vote_progress_report("Proposal 1", "Report 1", "_reject", "vote reason", "_reject", false); +// prepSender6.vote_progress_report("Proposal 1", "Report 1", "_reject", "vote reason", "_reject", false); +// prepSender7.vote_progress_report("Proposal 1", "Report 1", "_approve", "vote reason", "_approve", false); +// +// logger("completed vote"); +// +// Map progressReports = cpsMain.get_progress_reports_by_hash("Report 1"); +// logger(progressReports); +// +// assertEquals(BigInteger.valueOf(5600).multiply(EXA), new BigInteger(((String)progressReports.get("total_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(4000).multiply(EXA), new BigInteger(((String)progressReports.get("approved_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(1600).multiply(EXA), new BigInteger(((String)progressReports.get("rejected_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(4000).multiply(EXA), new BigInteger(((String)progressReports.get("budget_approved_votes")).substring(2), 16)); +// assertEquals(BigInteger.valueOf(1600).multiply(EXA), new BigInteger(((String)progressReports.get("budget_rejected_votes")).substring(2), 16)); +// assertEquals("0x" + Integer.toHexString(7), progressReports.get("total_voters")); +// assertEquals("0x" + Integer.toHexString(5), progressReports.get("approve_voters")); +// assertEquals("0x" + Integer.toHexString(2), progressReports.get("reject_voters")); +// assertEquals("0x" + Integer.toHexString(5), progressReports.get("budget_approve_voters")); +// assertEquals("0x" + Integer.toHexString(2), progressReports.get("budget_reject_voters")); +// } +// +// @Test +// @Order(8) +// void update_period_after_voting_progress_reports(){ +// cpsMain.update_next_block(0); +// logger("end period"); +// +// cpsMain.update_period(); +// logger("end first update period period"); +// +// cpsMain.update_period(); +// logger("end second update period period"); +// +// cpsMain.update_period(); +// logger("end third update period period"); +// +// cpsMain.update_period(); +// logger("end fourth update period period"); +// +// Map proposalDetails = cpsMain.get_proposal_details_by_hash("Proposal 1"); +// logger(proposalDetails); +// +// Map progressDetails = cpsMain.get_progress_reports_by_hash("Report 1"); +// logger(progressDetails); +// } +// void logger(T log){ +// System.out.println(log); +// } +// +// +//} diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/CPSTreasury.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/CPSTreasury.java new file mode 100644 index 00000000..6560c517 --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/CPSTreasury.java @@ -0,0 +1,535 @@ +package community.icon.cps.score.cpstreasury; + +import community.icon.cps.score.cpstreasury.db.ProposalData; +import community.icon.cps.score.cpstreasury.utils.ArrayDBUtils; +import score.*; +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Payable; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +import community.icon.cps.score.lib.interfaces.CPSTreasuryInterface; + +import community.icon.cps.score.cpstreasury.utils.consts; + +public class CPSTreasury extends ProposalData implements CPSTreasuryInterface { + private static final String TAG = "CPS_TREASURY"; + private static final String PROPOSAL_DB_PREFIX = "proposal"; + private static final String PROPOSALS_KEYS = "_proposals_keys"; + private static final String PROPOSALS_KEY_LIST_INDEX = "proposals_key_list_index"; + private static final String INSTALLMENT_FUND_RECORD = "installment_fund_record"; + + private static final String CPS_SCORE = "_cps_score"; + private static final String CPF_TREASURY_SCORE = "_cpf_treasury_score"; + private static final String BALANCED_DOLLAR = "balanced_dollar"; + + private static final String ACTIVE = "active"; + private static final String DISQUALIFIED = "disqualified"; + private static final String COMPLETED = "completed"; + private static final String CONTRIBUTOR_PROJECTS = "contributor_projects"; + private static final String SPONSOR_PROJECTS = "sponsor_projects"; + + private final ArrayDB proposalsKeys = Context.newArrayDB(PROPOSALS_KEYS, String.class); + private final DictDB proposalsKeyListIndex = Context.newDictDB(PROPOSALS_KEY_LIST_INDEX, Integer.class); + private final BranchDB> installmentFundRecord = Context.newBranchDB(INSTALLMENT_FUND_RECORD, BigInteger.class); + + private final VarDB
cpfTreasuryScore = Context.newVarDB(CPF_TREASURY_SCORE, Address.class); + private final VarDB
cpsScore = Context.newVarDB(CPS_SCORE, Address.class); + private final VarDB
balancedDollar = Context.newVarDB(BALANCED_DOLLAR, Address.class); + private final BranchDB> contributorProjects = Context.newBranchDB(CONTRIBUTOR_PROJECTS, String.class); + private final BranchDB> sponsorProjects = Context.newBranchDB(SPONSOR_PROJECTS, String.class); + private final VarDB batchSize = Context.newVarDB("batch_size", Integer.class); + + public CPSTreasury() { + } + + @Override + @External(readonly = true) + public String name() { + return TAG; + } + + @Override + @Payable + public void fallback() { + Context.revert(TAG + ": ICX can only be send by CPF Treasury Score"); + } + + private String proposalPrefix(String _proposal_key) { + return PROPOSAL_DB_PREFIX + "|" + "|" + _proposal_key; + } + + private Boolean proposalExists(String _ipfs_key) { + return proposalsKeyListIndex.getOrDefault(_ipfs_key, null) != null; + } + + private void validateAdmins() { + Boolean isAdmin = callScore(Boolean.class, cpsScore.get(), "is_admin", Context.getCaller()); + Context.require(isAdmin, TAG + ": Only admins can call this method"); + + } + + private void validateAdminScore(Address _score) { + validateAdmins(); + Context.require(_score.isContract(), TAG + "Target " + _score + " is not a score."); + } + + private void validateCpsScore() { + Context.require(Context.getCaller().equals(cpsScore.get()), TAG + ": Only CPS score " + cpsScore.get() + " can send fund using this method."); + } + + private void addRecord(ProposalAttributes _proposal) { + String ipfs_hash = _proposal.ipfs_hash; + Context.require(!proposalExists(ipfs_hash), TAG + ": Already have this project"); + proposalsKeys.add(ipfs_hash); + sponsorProjects.at(_proposal.sponsor_address).add(ipfs_hash); + contributorProjects.at(_proposal.contributor_address).add(ipfs_hash); + String proposalPrefix = proposalPrefix(ipfs_hash); + addDataToProposalDB(_proposal, proposalPrefix); + proposalsKeyListIndex.set(ipfs_hash, proposalsKeys.size() - 1); + } + + @Override + @External + public void setCpsScore(Address _score) { + validateAdminScore(_score); + cpsScore.set(_score); + } + + @Override + @External(readonly = true) + public Address getCpsScore() { + return cpsScore.get(); + } + + @Override + @External + public void setCpfTreasuryScore(Address _score) { + validateAdminScore(_score); + cpfTreasuryScore.set(_score); + } + + @Override + @External(readonly = true) + public Address getCpfTreasuryScore() { + return cpfTreasuryScore.get(); + } + + @Override + @External + public void setBnUSDScore(Address _score) { + validateAdminScore(_score); + balancedDollar.set(_score); + } + + @Override + @External + public Address getBnUSDScore() { + return balancedDollar.get(); + } + + @Override + @External(readonly = true) + public Map get_contributor_projected_fund(Address _wallet_address) { + BigInteger totalAmountToBePaidICX = BigInteger.ZERO; + BigInteger totalAmountToBePaidbnUSD = BigInteger.ZERO; + List> projectDetails = new ArrayList<>(); + ArrayDB proposalKeysArray = contributorProjects.at(_wallet_address.toString()); + int proposalKeysSize = proposalKeysArray.size(); + for (int i = 0; i < proposalKeysSize; i++) { + String _ipfs_key = proposalKeysArray.get(i); + String proposalPrefix = proposalPrefix(_ipfs_key); + Map proposal_details = getDataFromProposalDB(proposalPrefix); + if (!proposal_details.get(consts.STATUS).equals(DISQUALIFIED)) { + if (proposal_details.get(consts.CONTRIBUTOR_ADDRESS).equals(_wallet_address)) { + int totalInstallment = (int) proposal_details.get(consts.PROJECT_DURATION); + int totalPaidCount = totalInstallment - (int) proposal_details.get(consts.INSTALLMENT_COUNT); + + if (totalPaidCount < totalInstallment) { + String flag = (String) proposal_details.get(consts.TOKEN); + BigInteger totalBudget = (BigInteger) proposal_details.get(consts.TOTAL_BUDGET); + BigInteger totalPaidAmount = (BigInteger) proposal_details.get(consts.WITHDRAW_AMOUNT); + + Map project_details = Map.of( + consts.IPFS_HASH, _ipfs_key, + consts.TOKEN, flag, + consts.TOTAL_BUDGET, totalBudget, + consts.TOTAL_INSTALLMENT_PAID, totalPaidAmount, + consts.TOTAL_INSTALLMENT_COUNT, totalInstallment, + consts.TOTAL_TIMES_INSTALLMENT_PAID, totalPaidCount, + consts.INSTALLMENT_AMOUNT, totalBudget.divide(BigInteger.valueOf(totalInstallment))); + + projectDetails.add(project_details); + if (flag.equals(consts.ICX)) { + totalAmountToBePaidICX = totalAmountToBePaidICX.add(totalBudget.divide(BigInteger.valueOf(totalInstallment))); + } else { + totalAmountToBePaidbnUSD = totalAmountToBePaidbnUSD.add(totalBudget.divide(BigInteger.valueOf(totalInstallment))); + } + } + } + } + } + DictDB installmentRecord = installmentFundRecord.at(_wallet_address.toString()); + return Map.of( + "data", projectDetails, + "project_count", projectDetails.size(), + "total_amount", Map.of("ICX", totalAmountToBePaidICX, "bnUSD", totalAmountToBePaidbnUSD), + "withdraw_amount_icx", installmentRecord.getOrDefault(consts.ICX, BigInteger.ZERO), + "withdraw_amount_bnUSD", installmentRecord.getOrDefault(consts.bnUSD, BigInteger.ZERO)); + } + + @Override + @External(readonly = true) + public List getContributorProjects(Address address) { + List contributorProjects = new ArrayList<>(); + for (int i = 0; i < this.contributorProjects.at(address.toString()).size(); i++) { + contributorProjects.add(this.contributorProjects.at(address.toString()).get(i)); + } + return contributorProjects; + } + + @Override + @External(readonly = true) + public List getSponsorProjects(Address address) { + List sponsorProjects = new ArrayList<>(); + for (int i = 0; i < this.sponsorProjects.at(address.toString()).size(); i++) { + sponsorProjects.add(this.sponsorProjects.at(address.toString()).get(i)); + } + return sponsorProjects; + } + + @Override + @External(readonly = true) + public Map get_sponsor_projected_fund(Address _wallet_address) { + ProposalData proposalData = new ProposalData(); + BigInteger totalAmountToBePaidICX = BigInteger.ZERO; + BigInteger totalAmountToBePaidbnUSD = BigInteger.ZERO; + BigInteger totalSponsorBondICX = BigInteger.ZERO; + BigInteger totalSponsorBondbnUSD = BigInteger.ZERO; + List> projectDetails = new ArrayList<>(); + ArrayDB proposalKeysArray = sponsorProjects.at(_wallet_address.toString()); + int proposalKeysSize = proposalKeysArray.size(); + for (int i = 0; i < proposalKeysSize; i++) { + String _ipfs_key = proposalKeysArray.get(i); + String proposalPrefix = proposalPrefix(_ipfs_key); + Map proposal_details = proposalData.getDataFromProposalDB(proposalPrefix); + if (!proposal_details.get(consts.STATUS).equals(DISQUALIFIED)) { + if (proposal_details.get(consts.SPONSOR_ADDRESS).equals(_wallet_address)) { + int totalInstallment = (int) proposal_details.get(consts.PROJECT_DURATION); + int totalPaidCount = totalInstallment - (int) proposal_details.get(consts.SPONSOR_REWARD_COUNT); + if (totalPaidCount < totalInstallment) { + String flag = (String) proposal_details.get(consts.TOKEN); + BigInteger totalBudget = (BigInteger) proposal_details.get(consts.SPONSOR_REWARD); + BigInteger totalPaidAmount = (BigInteger) proposal_details.get(consts.WITHDRAW_AMOUNT); + BigInteger depositedSponsorBond = ((BigInteger) proposal_details.get(consts.TOTAL_BUDGET)).divide(BigInteger.TEN); + + Map project_details = Map.of( + consts.IPFS_HASH, _ipfs_key, + consts.TOKEN, flag, + consts.TOTAL_BUDGET, totalBudget, + consts.TOTAL_INSTALLMENT_PAID, totalPaidAmount, + consts.TOTAL_INSTALLMENT_COUNT, totalInstallment, + consts.TOTAL_TIMES_INSTALLMENT_PAID, totalPaidCount, + consts.INSTALLMENT_AMOUNT, totalBudget.divide(BigInteger.valueOf(totalInstallment)), + consts.SPONSOR_BOND_AMOUNT, depositedSponsorBond); + + projectDetails.add(project_details); + if (flag.equals(consts.ICX)) { + totalAmountToBePaidICX = totalAmountToBePaidICX.add(totalBudget.divide(BigInteger.valueOf(totalInstallment))); + totalSponsorBondICX = totalSponsorBondICX.add(depositedSponsorBond); + } else { + totalAmountToBePaidbnUSD = totalAmountToBePaidbnUSD.add(totalBudget.divide(BigInteger.valueOf(totalInstallment))); + totalSponsorBondbnUSD = totalSponsorBondbnUSD.add(depositedSponsorBond); + } + } + } + } + } + DictDB installmentRecord = installmentFundRecord.at(_wallet_address.toString()); + return Map.of( + "data", projectDetails, + "project_count", projectDetails.size(), + "total_amount", Map.of("ICX", totalAmountToBePaidICX, "bnUSD", totalAmountToBePaidbnUSD), + "withdraw_amount_icx", installmentRecord.getOrDefault(consts.ICX, BigInteger.ZERO), + "withdraw_amount_bnUSD", installmentRecord.getOrDefault(consts.bnUSD, BigInteger.ZERO), + "total_sponsor_bond", Map.of("ICX", totalSponsorBondICX, "bnUSD", totalSponsorBondbnUSD) + ); + } + + private void depositProposalFund(ProposalData.ProposalAttributes _proposals, BigInteger _value) { + addRecord(_proposals); + ProposalFundDeposited(_proposals.ipfs_hash, "Received " + _proposals.ipfs_hash + " " + _value + " " + + consts.bnUSD + " fund from CPF"); + } + + @Override + @External + @Payable + public void update_proposal_fund(String _ipfs_key, BigInteger _added_budget, BigInteger _added_sponsor_reward, + int _added_installment_count) { + ProposalData proposalData = new ProposalData(); + Context.require(proposalExists(_ipfs_key), TAG + ": Invalid IPFS hash."); + String proposalPrefix = proposalPrefix(_ipfs_key); + Map proposalDetails = proposalData.getDataFromProposalDB(proposalPrefix); + BigInteger totalBudget = (BigInteger) proposalDetails.get(consts.TOTAL_BUDGET); + BigInteger sponsorReward = (BigInteger) proposalDetails.get(consts.SPONSOR_REWARD); + int totalDuration = (int) proposalDetails.get(consts.PROJECT_DURATION); + BigInteger remainingAmount = (BigInteger) proposalDetails.get(consts.REMAINING_AMOUNT); + BigInteger sponsorRemainingAmount = (BigInteger) proposalDetails.get(consts.SPONSOR_REMAINING_AMOUNT); + int installmentCount = (int) proposalDetails.get(consts.INSTALLMENT_COUNT); + int sponsorRewardCount = (int) proposalDetails.get(consts.SPONSOR_REWARD_COUNT); + String flag = (String) proposalDetails.get(consts.TOKEN); + + setTotalBudget(proposalPrefix, totalBudget.add(_added_budget)); + setSponsorReward(proposalPrefix, sponsorReward.add(_added_sponsor_reward)); + setProjectDuration(proposalPrefix, totalDuration + _added_installment_count); + setRemainingAmount(proposalPrefix, remainingAmount.add(_added_budget)); + setSponsorRemainingAmount(proposalPrefix, sponsorRemainingAmount.add(_added_sponsor_reward)); + setInstallmentCount(proposalPrefix, installmentCount + _added_installment_count); + setSponsorRewardCount(proposalPrefix, sponsorRewardCount + _added_installment_count); + + ProposalFundDeposited(_ipfs_key, _ipfs_key + ": Added Budget: " + _added_budget + " " + + flag + "and Added time: " + _added_installment_count + " Successfully"); + } + + @Override + @External + public void send_installment_to_contributor(String _ipfs_key) { + validateCpsScore(); + Context.require(proposalExists(_ipfs_key), TAG + ": Invalid IPFS Hash."); + BigInteger installmentAmount; + String prefix = proposalPrefix(_ipfs_key); + Map proposalData = getDataFromProposalDB(prefix); + + int installmentCount = (int) proposalData.get(consts.INSTALLMENT_COUNT); + BigInteger withdrawAmount = (BigInteger) proposalData.get(consts.WITHDRAW_AMOUNT); + BigInteger remainingAmount = (BigInteger) proposalData.get(consts.REMAINING_AMOUNT); + Address contributorAddress = (Address) proposalData.get(consts.CONTRIBUTOR_ADDRESS); + String flag = (String) proposalData.get(consts.TOKEN); + + if (installmentCount == 1) { + installmentAmount = remainingAmount; + } else { + installmentAmount = remainingAmount.divide(BigInteger.valueOf(installmentCount)); + } + int newInstallmentCount = installmentCount - 1; + + setInstallmentCount(prefix, newInstallmentCount); + setRemainingAmount(prefix, remainingAmount.subtract(installmentAmount)); + setWithdrawAmount(prefix, withdrawAmount.add(installmentAmount)); + DictDB installmentFund = this.installmentFundRecord.at(contributorAddress.toString()); + BigInteger installmentFundAmount = installmentFund.getOrDefault(flag, BigInteger.ZERO); + installmentFund.set(flag, installmentFundAmount.add(installmentAmount)); + ProposalFundSent(contributorAddress, "new installment " + installmentAmount + " " + flag + " sent to contributors address."); + + if (newInstallmentCount == 0) { + setStatus(prefix, COMPLETED); + } + } + + @Override + @External + public void send_reward_to_sponsor(String _ipfs_key) { + validateCpsScore(); + + Context.require(proposalExists(_ipfs_key), TAG + ": Invalid IPFS Hash."); + BigInteger installmentAmount; + String prefix = proposalPrefix(_ipfs_key); + + int sponsorRewardCount = getSponsorRewardCount(prefix); + BigInteger sponsorWithdrawAmount = getSponsorWithdrawAmount(prefix); + BigInteger sponsorRemainingAmount = getSponsorRemainingAmount(prefix); + Address sponsorAddress = getSponsorAddress(prefix); + String flag = getToken(prefix); + + if (sponsorRewardCount == 1) { + installmentAmount = sponsorRemainingAmount; + } else { + installmentAmount = sponsorRemainingAmount.divide(BigInteger.valueOf(sponsorRewardCount)); + } + int newSponsorRewardCount = sponsorRewardCount - 1; + + setSponsorRewardCount(prefix, newSponsorRewardCount); + setSponsorWithdrawAmount(prefix, sponsorWithdrawAmount.add(installmentAmount)); + setSponsorRemainingAmount(prefix, sponsorRemainingAmount.subtract(installmentAmount)); + DictDB installmentFunds = installmentFundRecord.at(sponsorAddress.toString()); + installmentFunds.set(flag, installmentFunds.getOrDefault(flag, BigInteger.ZERO).add(installmentAmount)); + ProposalFundSent(sponsorAddress, "New installment " + installmentAmount + " " + + flag + " sent to sponsor address."); + } + + @Override + @External + public void disqualify_project(String _ipfs_key) { + validateCpsScore(); + Context.require(proposalExists(_ipfs_key), TAG + ": Project not found. Invalid IPFS hash."); + String prefix = proposalPrefix(_ipfs_key); + setStatus(prefix, DISQUALIFIED); + + BigInteger totalBudget = getTotalBudget(prefix); + BigInteger withdrawAmount = getWithdrawAmount(prefix); + BigInteger sponsorReward = getSponsorReward(prefix); + BigInteger sponsorWithdrawAmount = getSponsorWithdrawAmount(prefix); + String flag = getToken(prefix); + + BigInteger remainingBudget = totalBudget.subtract(withdrawAmount); + BigInteger remainingReward = sponsorReward.subtract(sponsorWithdrawAmount); + BigInteger totalReturnAmount = remainingBudget.add(remainingReward); + + Address cpfTreasuryAddres = cpfTreasuryScore.get(); + if (flag.equals(consts.ICX)) { + callScore(totalReturnAmount, cpfTreasuryAddres, "disqualify_proposal_fund", _ipfs_key); + } else if (flag.equals(consts.bnUSD)) { + JsonObject disqualifyProjectParams = new JsonObject(); + disqualifyProjectParams.add("method", "disqualify_project"); + JsonObject params = new JsonObject(); + params.add("ipfs_key", _ipfs_key); + disqualifyProjectParams.add("params", params); + + callScore(balancedDollar.get(), "transfer", cpfTreasuryAddres, totalReturnAmount, disqualifyProjectParams.toString().getBytes()); + } else { + Context.revert(TAG + ": Not supported token."); + } + ProposalDisqualified(_ipfs_key, _ipfs_key + ", Proposal disqualified"); + } + + + @Override + @External + public void claim_reward() { + Address caller = Context.getCaller(); + DictDB installmentFundRecord = this.installmentFundRecord.at(caller.toString()); + BigInteger availableAmountICX = installmentFundRecord.getOrDefault(consts.ICX, BigInteger.ZERO); + BigInteger availableAmountbnUSD = installmentFundRecord.getOrDefault(consts.bnUSD, BigInteger.ZERO); + if (availableAmountICX.compareTo(BigInteger.ZERO) > 0) { + installmentFundRecord.set(consts.ICX, BigInteger.ZERO); + Context.transfer(caller, availableAmountICX); + ProposalFundWithdrawn(caller, availableAmountICX + " " + consts.ICX + + " withdrawn to " + caller); + } else if (availableAmountbnUSD.compareTo(BigInteger.ZERO) > 0) { + installmentFundRecord.set(consts.bnUSD, BigInteger.ZERO); + callScore(balancedDollar.get(), "transfer", caller, availableAmountbnUSD); + } else { + Context.revert(TAG + ": Claim Reward Fails. Available amount(ICX) = " + availableAmountICX + + " and Available amount(bnUSD) = " + availableAmountbnUSD); + } + } + + @Override + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + Context.require(_from.equals(cpfTreasuryScore.get()), TAG + "Only receiving from " + cpfTreasuryScore.get()); + String unpacked_data = new String(_data); + JsonObject jsonObject = Json.parse(unpacked_data).asObject(); + JsonObject params = jsonObject.get("params").asObject(); + String methodName = jsonObject.get("method").asString(); + if (methodName.equals("deposit_proposal_fund")) { + String ipfs_hash = params.get("ipfs_hash").asString(); + int project_duration = params.get("project_duration").asInt(); + BigInteger total_budget = new BigInteger(params.get("total_budget").asString(), 16); + BigInteger sponsor_reward = new BigInteger(params.get("sponsor_reward").asString(), 16); + String token = params.get("token").asString(); + String contributor_address = params.get("contributor_address").asString(); + String sponsor_address = params.get("sponsor_address").asString(); + ProposalAttributes proposalAttributes = new ProposalAttributes(); + proposalAttributes.ipfs_hash = ipfs_hash; + proposalAttributes.project_duration = project_duration; + proposalAttributes.total_budget = total_budget; + proposalAttributes.sponsor_reward = sponsor_reward; + proposalAttributes.token = token; + proposalAttributes.contributor_address = contributor_address; + proposalAttributes.sponsor_address = sponsor_address; + proposalAttributes.status = ACTIVE; + depositProposalFund(proposalAttributes, _value); + } else if (methodName.equals("budget_adjustment")) { + String ipfs_key = params.get("_ipfs_key").asString(); + BigInteger added_budget = new BigInteger(params.get("_added_budget").asString(), 16); + BigInteger added_sponsor_reward = new BigInteger(params.get("_added_sponsor_reward").asString(), 16); + int added_installment_count = params.get("_added_installment_count").asInt(); + + update_proposal_fund(ipfs_key, added_budget, added_sponsor_reward, added_installment_count); + } else { + Context.revert(TAG + methodName + " Not a valid method."); + } + } + + // for migration into java contract + @Override + @External + public void updateSponsorAndContributorProjects() { + validateAdmins(); + int startIndex = batchSize.getOrDefault(0); + int size = proposalsKeys.size(); + int endIndex = startIndex + 10; + if (endIndex > size) { + endIndex = size; + } + for (int i = startIndex; i < endIndex; i++) { + String proposalKey = proposalsKeys.get(i); + String proposalPrefix = proposalPrefix(proposalKey); + Address contributorAddress = getContributorAddress(proposalPrefix); + Address sponsorAddress = getSponsorAddress(proposalPrefix); + contributorProjects.at(contributorAddress.toString()).add(proposalKey); + sponsorProjects.at(sponsorAddress.toString()).add(proposalKey); + batchSize.set(endIndex); + } + } + + + public T callScore(Class t, Address address, String method, Object... params) { + return Context.call(t, address, method, params); + } + + public void callScore(Address address, String method, Object... params) { + Context.call(address, method, params); + } + + public void callScore(BigInteger amount, Address address, String method, Object... params) { + Context.call(amount, address, method, params); + } + + @Override + @EventLog(indexed = 1) + public void ProposalDisqualified(String _ipfs_key, String note) { + } + + @Override + @EventLog(indexed = 1) + public void ProposalFundDeposited(String _ipfs_key, String note) { + } + + @Override + @EventLog(indexed = 1) + public void ProposalFundSent(Address _receiver_address, String note) { + } + + @Override + @EventLog(indexed = 1) + public void ProposalFundWithdrawn(Address _receiver_address, String note) { + } + + /*----------------------------------------------------------------------------------------------------------------- + ***************************************************** to be removed in production********************************** + -----------------------------------------------------------------------------------------------------------------*/ + + @External + public void depositProposalFunds(ProposalData.ProposalAttributes proposals){ + addRecord(proposals); + } + + @External(readonly = true) + public String returnString(){ + return ""; + } + +// @External +// public void removeArrayItems(){ +// ArrayDBUtils.remove_array_item_address(contributorProjects); +// } +} diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/db/ProposalData.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/db/ProposalData.java new file mode 100644 index 00000000..1eb43b95 --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/db/ProposalData.java @@ -0,0 +1,148 @@ +package community.icon.cps.score.cpstreasury.db; + +import score.*; +import community.icon.cps.score.cpstreasury.utils.consts; + +import java.math.BigInteger; +import java.util.Map; + +public class ProposalData { + public static class ProposalAttributes{ + public String ipfs_hash; + public int project_duration; + public BigInteger total_budget; + public BigInteger sponsor_reward; + public String token; + public String contributor_address; + public String sponsor_address; + public String status; + } + private final BranchDB> ipfsHash = Context.newBranchDB(consts.IPFS_HASH, String.class); + private final BranchDB> totalBudget = Context.newBranchDB(consts.TOTAL_BUDGET, BigInteger.class); + private final BranchDB> sponsorReward = Context.newBranchDB(consts.SPONSOR_REWARD, BigInteger.class); + private final BranchDB> projectDuration = Context.newBranchDB(consts.PROJECT_DURATION, Integer.class); + private final BranchDB> sponsorAddress = Context.newBranchDB(consts.SPONSOR_ADDRESS, Address.class); + private final BranchDB> contributorAddress = Context.newBranchDB(consts.CONTRIBUTOR_ADDRESS, Address.class); + private final BranchDB> token = Context.newBranchDB(consts.TOKEN, String.class); + private final BranchDB> withdrawAmount = Context.newBranchDB(consts.WITHDRAW_AMOUNT, BigInteger.class); + private final BranchDB> sponsorWithdrawAmount = Context.newBranchDB(consts.SPONSOR_WITHDRAW_AMOUNT, BigInteger.class); + private final BranchDB> remainingAmount = Context.newBranchDB(consts.REMAINING_AMOUNT, BigInteger.class); + private final BranchDB> sponsorRemainingAmount = Context.newBranchDB(consts.SPONSOR_REMAINING_AMOUNT, BigInteger.class); + private final BranchDB> installmentCount = Context.newBranchDB(consts.INSTALLMENT_COUNT, Integer.class); + private final BranchDB> sponsorRewardCount = Context.newBranchDB(consts.SPONSOR_REWARD_COUNT, Integer.class); + private final BranchDB> status = Context.newBranchDB(consts.STATUS, String.class); + + + + + public void addDataToProposalDB(ProposalAttributes _proposals, String prefix){ + ipfsHash.at(prefix).set(_proposals.ipfs_hash); + totalBudget.at(prefix).set(_proposals.total_budget); + sponsorReward.at(prefix).set(_proposals.sponsor_reward); + projectDuration.at(prefix).set(_proposals.project_duration); + sponsorAddress.at(prefix).set(Address.fromString(_proposals.sponsor_address)); + contributorAddress.at(prefix).set(Address.fromString(_proposals.contributor_address)); + withdrawAmount.at(prefix).set(BigInteger.ZERO); + sponsorWithdrawAmount.at(prefix).set(BigInteger.ZERO); + remainingAmount.at(prefix).set(_proposals.total_budget); + sponsorRemainingAmount.at(prefix).set(_proposals.sponsor_reward); + installmentCount.at(prefix).set(_proposals.project_duration); + sponsorRewardCount.at(prefix).set(_proposals.project_duration); + token.at(prefix).set(_proposals.token); + status.at(prefix).set(_proposals.status); + } + + public Map getDataFromProposalDB(String prefix){ + return Map.ofEntries( + Map.entry(consts.IPFS_HASH, ipfsHash.at(prefix).getOrDefault("")), + Map.entry(consts.TOTAL_BUDGET, totalBudget.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.SPONSOR_REWARD, sponsorReward.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.PROJECT_DURATION, projectDuration.at(prefix).getOrDefault(0)), + Map.entry(consts.SPONSOR_ADDRESS, sponsorAddress.at(prefix).get()), + Map.entry(consts.CONTRIBUTOR_ADDRESS, contributorAddress.at(prefix).get()), + Map.entry(consts.WITHDRAW_AMOUNT, withdrawAmount.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.INSTALLMENT_COUNT, installmentCount.at(prefix).getOrDefault(0)), + Map.entry(consts.SPONSOR_REWARD_COUNT, sponsorRewardCount.at(prefix).getOrDefault(0)), + Map.entry(consts.SPONSOR_WITHDRAW_AMOUNT, sponsorWithdrawAmount.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.REMAINING_AMOUNT, remainingAmount.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.SPONSOR_REMAINING_AMOUNT, sponsorRemainingAmount.at(prefix).getOrDefault(BigInteger.ZERO)), + Map.entry(consts.TOKEN, token.at(prefix).getOrDefault("")), + Map.entry(consts.STATUS, status.at(prefix).getOrDefault("")) + ); + } + + public Address getSponsorAddress(String prefix){ + return sponsorAddress.at(prefix).get(); + } + + public Address getContributorAddress(String prefix){ + return contributorAddress.at(prefix).get(); + } + + public void setTotalBudget(String prefix, BigInteger totalBudget){ + this.totalBudget.at(prefix).set(totalBudget); + } + + public BigInteger getTotalBudget(String prefix){ + return totalBudget.at(prefix).getOrDefault(BigInteger.ZERO); + } + + public void setSponsorReward(String prefix, BigInteger sponsorReward){ + this.sponsorReward.at(prefix).set(sponsorReward); + } + + public BigInteger getSponsorReward(String prefix){ + return sponsorReward.at(prefix).getOrDefault(BigInteger.ZERO); + } + + public void setProjectDuration(String prefix, int projectDuration){ + this.projectDuration.at(prefix).set(projectDuration); + } + + public void setWithdrawAmount(String prefix, BigInteger withdrawAmount){ + this.withdrawAmount.at(prefix).set(withdrawAmount); + } + + public BigInteger getWithdrawAmount(String prefix){ + return withdrawAmount.at(prefix).getOrDefault(BigInteger.ZERO); + } + + public void setInstallmentCount(String prefix, int installmentCount){ + this.installmentCount.at(prefix).set(installmentCount); + } + + public void setSponsorRewardCount(String prefix, int sponsorRewardCount){ + this.sponsorRewardCount.at(prefix).set(sponsorRewardCount); + } + + public int getSponsorRewardCount(String prefix){ + return sponsorRewardCount.at(prefix).getOrDefault(0); + } + + public void setSponsorWithdrawAmount(String prefix, BigInteger sponsorWithdrawAmount){ + this.sponsorWithdrawAmount.at(prefix).set(sponsorWithdrawAmount); + } + + public BigInteger getSponsorWithdrawAmount(String prefix){ + return sponsorWithdrawAmount.at(prefix).getOrDefault(BigInteger.ZERO); + } + + public void setRemainingAmount(String prefix, BigInteger remainingAmount){ + this.remainingAmount.at(prefix).set(remainingAmount); + } + public void setSponsorRemainingAmount(String prefix, BigInteger sponsorRemainingAmount){ + this.sponsorRemainingAmount.at(prefix).set(sponsorRemainingAmount); + } + + public BigInteger getSponsorRemainingAmount(String prefix){ + return sponsorRemainingAmount.at(prefix).getOrDefault(BigInteger.ZERO); + } + + public void setStatus(String prefix, String status){ + this.status.at(prefix).set(status); + } + + public String getToken(String prefix){ + return token.at(prefix).get(); + } +} diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/ArrayDBUtils.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/ArrayDBUtils.java new file mode 100644 index 00000000..8f09e223 --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/ArrayDBUtils.java @@ -0,0 +1,112 @@ +package community.icon.cps.score.cpstreasury.utils; + +import score.Address; +import score.ArrayDB; +import score.Context; + +import java.util.ArrayList; +import java.util.List; + +public final class ArrayDBUtils { + + ArrayDBUtils() {} + + @SuppressWarnings("unchecked") + public static List removeElement(List list, E element){ + E[] array = (E[])list.toArray(); + + boolean found = false; + for(int i = 0; i < array.length; i++) { + if(array[i].equals(element)) { + int numMoved = array.length - i - 1; + System.arraycopy(array, i+1, array, i, numMoved); + found = true; + break; + } + } + if(!found) { + return list; + } + + array[array.length-1] = null; + + Object[] result = new Object[array.length-1]; + + System.arraycopy(array, 0, result, 0, result.length); + + return List.of((E[])result); + } + + @SuppressWarnings("unchecked") + public static List removeElementIndex(List list, int index){ + E[] array = (E[])list.toArray(); + + if(index >= list.size()) { + return list; + } + + int numMoved = array.length - index - 1; + System.arraycopy(array, index+1, array, index, numMoved); + + array[array.length-1] = null; + + Object[] result = new Object[array.length-1]; + + System.arraycopy(array, 0, result, 0, result.length); + + return List.of((E[])result); + } + + public static void removeElementIndexFromArrayDB(ArrayDB arrayDB, int index){ + List username_list = new ArrayList<>(); + if (index > arrayDB.size()-1){ + Context.revert("ArrayDB out of index"); + } + if (index == arrayDB.size() - 1) + for(int j = index + 1; j > arrayDB.size() - 1; j++){ + username_list.add(arrayDB.get(j)); + } + arrayDB.removeLast(); + + for (int i = 0; i > username_list.size() - 1; i++){ + arrayDB.add(username_list.get(i)); + } + } + + public static boolean remove_array_item_address(ArrayDB
array_db, Object target){ + int size = array_db.size(); + Address _out = array_db.get(size - 1); + if (_out.equals(target)){ + array_db.pop(); + return true; + } + for (int i = 0; i < size - 1; i++){ + if (array_db.get(i).equals(target)){ + array_db.set(i, _out); + array_db.pop(); + return true; + } + } + return false; + } + + public static boolean remove_array_item_string(ArrayDB array_db, Object target){ + int size = array_db.size(); + String _out = array_db.get(size - 1); + if (_out.equals(target)){ + array_db.pop(); + return true; + } + for (int i = 0; i < size - 1; i++){ + if (array_db.get(i).equals(target)){ + array_db.set(i, _out); + array_db.pop(); + return true; + } + } + return false; + } + +} + + diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Checks.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Checks.java new file mode 100644 index 00000000..da8a3fa8 --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Checks.java @@ -0,0 +1,4 @@ +package community.icon.cps.score.cpstreasury.utils; + +public class Checks { +} diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Eventlogs.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Eventlogs.java new file mode 100644 index 00000000..37ecd221 --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/Eventlogs.java @@ -0,0 +1,18 @@ +package community.icon.cps.score.cpstreasury.utils; +import score.Address; +import score.annotation.EventLog; + + +public class Eventlogs { + @EventLog(indexed = 1) + public static void ProposalSubmitted(Address _sender_address, String note){} + + @EventLog(indexed = 1) + public static void VotedSuccessfully(Address _sender_address, String note){} + + @EventLog(indexed = 1) + public static void ProgressReportSubmitted(Address _sender_address, String note){} + + @EventLog(indexed = 1) + public static void PeriodUpdate(String note){} +} diff --git a/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/consts.java b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/consts.java new file mode 100644 index 00000000..a84965ef --- /dev/null +++ b/CPSTreasury/src/main/java/community/icon/cps/score/cpstreasury/utils/consts.java @@ -0,0 +1,34 @@ +package community.icon.cps.score.cpstreasury.utils; + +import java.math.BigInteger; + +public class consts { + public consts() { + } + + public static final String TOTAL_BUDGET = "total_budget"; + public static final String IPFS_HASH = "ipfs_hash"; + public static final String PROJECT_DURATION = "project_duration"; + public static final String STATUS = "status"; + + public static final String ICX = "ICX"; + public static final String bnUSD = "bnUSD"; + + public static final String SPONSOR_REWARD = "sponsor_reward"; + public static final String SPONSOR_ADDRESS = "sponsor_address"; + public static final String CONTRIBUTOR_ADDRESS = "contributor_address"; + public static final String WITHDRAW_AMOUNT = "withdraw_amount"; + public static final String SPONSOR_WITHDRAW_AMOUNT = "sponsor_withdraw_amount"; + public static final String REMAINING_AMOUNT = "remaining_amount"; + public static final String SPONSOR_REMAINING_AMOUNT = "sponsor_remaining_amount"; + public static final String INSTALLMENT_COUNT = "installment_count"; + public static final String SPONSOR_REWARD_COUNT = "sponsor_reward_count"; + public static final String TOKEN = "token"; + public static final String TOTAL_INSTALLMENT_PAID = "total_installment_paid"; + public static final String TOTAL_TIMES_INSTALLMENT_PAID = "total_times_installment_paid"; + public static final String INSTALLMENT_AMOUNT = "installment_amount"; + public static final String TOTAL_INSTALLMENT_COUNT = "total_installment_count"; + public static final String SPONSOR_BOND_AMOUNT = "sponsor_bond_amount"; + + +} diff --git a/CPSTreasury/src/test/java/community/icon/cps/score/cpstreasury/CPSTreasuryTest.java b/CPSTreasury/src/test/java/community/icon/cps/score/cpstreasury/CPSTreasuryTest.java new file mode 100644 index 00000000..dc4b17e3 --- /dev/null +++ b/CPSTreasury/src/test/java/community/icon/cps/score/cpstreasury/CPSTreasuryTest.java @@ -0,0 +1,354 @@ +package community.icon.cps.score.cpstreasury; + +import com.eclipsesource.json.JsonObject; +import com.iconloop.score.test.Account; +import com.iconloop.score.test.Score; +import com.iconloop.score.test.ServiceManager; +import com.iconloop.score.test.TestBase; +import community.icon.cps.score.cpstreasury.utils.consts; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; + +import java.math.BigInteger; +import java.security.SecureRandom; +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.*; + +public class CPSTreasuryTest extends TestBase { + private static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + private static final Address treasury_score = new Address(new byte[Address.LENGTH]); + private static final Address score_address = Address.fromString("cx0000000000000000000000000000000000000000"); + private static final Address cpfTreasury = Address.fromString("cx0000000000000000000000000000000000000001"); + private static final Address bnUSDScore = Address.fromString("cx0000000000000000000000000000000000000002"); + private static final Address dexScore = Address.fromString("cx0000000000000000000000000000000000000003"); + private static final Address cpsTreasuryScore = Address.fromString("cx0000000000000000000000000000000000000004"); + + private static final String name = "CPS_TREASURY"; + public static final String TAG = "CPS_TREASURY"; + CPSTreasury cpsTreasury; + private static final BigInteger MULTIPLIER = new BigInteger("1000000000000000000"); + + + private static final ServiceManager sm = getServiceManager(); + private static final Account owner = sm.createAccount(); + private static final Account testing_account = sm.createAccount(); + private static final Account testing_account2 = sm.createAccount(); + + private Score tokenScore; + private final SecureRandom secureRandom = new SecureRandom(); + + DictDB proposalBudgets = Mockito.mock(DictDB.class); + VarDB swapState = Mockito.mock(VarDB.class); + VarDB swapCount = Mockito.mock(VarDB.class); + + CPSTreasury scoreSpy; + + @BeforeEach + public void setup() throws Exception { + tokenScore = sm.deploy(owner, CPSTreasury.class); + CPSTreasury instance = (CPSTreasury) tokenScore.getInstance(); + scoreSpy = spy(instance); + tokenScore.setInstance(scoreSpy); + } + + @Test + void name() { + assertEquals(tokenScore.call("name"), name); + } + + + public void expectErrorMessage(Executable contractCall, String errorMessage) { + AssertionError e = Assertions.assertThrows(AssertionError.class, contractCall); + assertEquals(errorMessage, e.getMessage()); + } + + void fallbackExceptions(Account address) { + try { + sm.call(address, BigInteger.valueOf(1000).multiply(MULTIPLIER), tokenScore.getAddress(), "fallback"); + } catch (Exception e) { + throw e; + } + } + + @Test + void fallback() { + Executable fallback = () -> fallbackExceptions(owner); + expectErrorMessage(fallback, "Reverted(0):" + " " + TAG + ": ICX can only be send by CPF Treasury Score"); + } + + @Test + void setCpsScore() { + setCpsScoreMethod(); + } + + private void setCpsScoreMethod() { + doReturn(Boolean.TRUE).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setCpsScore", score_address); + assertEquals(score_address, tokenScore.call("getCpsScore")); + } + + @Test + void setCPFTreasuryScore() { + setCPFTreasuryScoreMethod(); + } + + private void setCPFTreasuryScoreMethod() { + doReturn(Boolean.TRUE).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setCpfTreasuryScore", cpfTreasury); + assertEquals(cpfTreasury, tokenScore.call("getCpfTreasuryScore")); + } + + @Test + void setBnUSDScore() { + setBnUSDScoreMethod(); + } + + private void setBnUSDScoreMethod() { + doReturn(Boolean.TRUE).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setBnUSDScore", bnUSDScore); + assertEquals(bnUSDScore, tokenScore.call("getBnUSDScore")); + } + + void setCpsScoreExceptions(Boolean is_admin, Address score_address) { + doReturn(is_admin).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setCpsScore", score_address); + } + + void setCpfTreasuryScoreExceptions(Boolean is_admin, Address score_address) { + doReturn(is_admin).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setCpfTreasuryScore", score_address); + } + + void setBnUSDScoreExceptions(Boolean is_admin, Address score_address) { + doReturn(is_admin).when(scoreSpy).callScore(eq(Boolean.class), any(), eq("is_admin"), eq(owner.getAddress())); + tokenScore.invoke(owner, "setBnUSDScore", score_address); + } + + @Test + void setCpsScoreNotAdmin() { + Executable setCpsScoreNotAdmin = () -> setCpsScoreExceptions(false, score_address); + expectErrorMessage(setCpsScoreNotAdmin, "Reverted(0): " + TAG + ": Only admins can call this method"); + } + + @Test + void setCpfTreasuryScoreNotAdmin() { + Executable setCpfTreasuryScoreNotAdmin = () -> setCpfTreasuryScoreExceptions(false, score_address); + expectErrorMessage(setCpfTreasuryScoreNotAdmin, "Reverted(0): " + TAG + ": Only admins can call this method"); + } + + @Test + void setBnUSDScoreNotAdmin() { + Executable setBnUSDScoreNotAdmin = () -> setBnUSDScoreExceptions(false, score_address); + expectErrorMessage(setBnUSDScoreNotAdmin, "Reverted(0): " + TAG + ": Only admins can call this method"); + } + + @Test + void setCPSScoreNotContract() { + Executable setCpsScoreNotAdmin = () -> setCpsScoreExceptions(true, testing_account.getAddress()); + expectErrorMessage(setCpsScoreNotAdmin, "Reverted(0): " + TAG + "Target " + testing_account.getAddress() + " is not a score."); + } + + @Test + void setCPFTreasuryScoreNotContract() { + Executable setCpfTreasuryScoreNotContract = () -> setCpfTreasuryScoreExceptions(true, testing_account.getAddress()); + expectErrorMessage(setCpfTreasuryScoreNotContract, "Reverted(0): " + TAG + "Target " + testing_account.getAddress() + " is not a score."); + } + + @Test + void setBnUSDScoreNotContract() { + Executable setBnUSDScoreContract = () -> setBnUSDScoreExceptions(true, testing_account.getAddress()); + expectErrorMessage(setBnUSDScoreContract, "Reverted(0): " + TAG + "Target " + testing_account.getAddress() + " is not a score."); + } + + @Test + void depositProposalFund() { + depositProposalFundMethod(); + @SuppressWarnings("unchecked") + Map proposalDataDetails = (Map) tokenScore.call("get_contributor_projected_fund", testing_account2.getAddress()); + @SuppressWarnings("unchecked") + List> proposalDetails = (List>) proposalDataDetails.get("data"); + Map expectedData = Map.of( + consts.IPFS_HASH, "Proposal 1", + consts.TOKEN, "bnUSD", + consts.TOTAL_BUDGET, BigInteger.valueOf(100).multiply(MULTIPLIER), + consts.TOTAL_INSTALLMENT_PAID, BigInteger.ZERO, + consts.TOTAL_INSTALLMENT_COUNT, 2, + consts.TOTAL_TIMES_INSTALLMENT_PAID, 0, + consts.INSTALLMENT_AMOUNT, BigInteger.valueOf(50).multiply(MULTIPLIER)); + assertEquals(proposalDetails.get(0), expectedData); + } + + private void depositProposalFundMethod() { + JsonObject depositProposal = new JsonObject(); + depositProposal.add("method", "deposit_proposal_fund"); + JsonObject params = new JsonObject(); + params.add("ipfs_hash", "Proposal 1"); + params.add("project_duration", 2); + params.add("sponsor_address", testing_account.getAddress().toString()); + params.add("contributor_address", testing_account2.getAddress().toString()); + params.add("total_budget", BigInteger.valueOf(100).multiply(MULTIPLIER).toString(16)); + params.add("sponsor_reward", BigInteger.valueOf(2).multiply(MULTIPLIER).toString(16)); + params.add("token", "bnUSD"); + depositProposal.add("params", params); + setCPFTreasuryScoreMethod(); + tokenScore.invoke(owner, "tokenFallback", cpfTreasury, BigInteger.valueOf(102).multiply(MULTIPLIER), depositProposal.toString().getBytes()); + } + + void depositProposalFundExceptions(){ + try{ + depositProposalFundMethod(); + } + catch (Exception e){ + throw e; + } + } + @Test + void depositProposalFundProposalAlreadyExists(){ + depositProposalFundMethod(); + Executable depositProposalFundProposalAlreadyExists = () -> depositProposalFundExceptions(); + expectErrorMessage(depositProposalFundProposalAlreadyExists, "Reverted(0): " + "CPS_TREASURY: Already have this project"); + } + + @Test + void updateProposalFund() { + depositProposalFundMethod(); + JsonObject budgetAdjustmentData = new JsonObject(); + budgetAdjustmentData.add("method", "budget_adjustment"); + JsonObject params = new JsonObject(); + params.add("_ipfs_key", "Proposal 1"); + params.add("_added_budget", BigInteger.valueOf(100).multiply(MULTIPLIER).toString(16)); + params.add("_added_sponsor_reward", BigInteger.valueOf(2).multiply(MULTIPLIER).toString(16)); + params.add("_added_installment_count", 1); + budgetAdjustmentData.add("params", params); + + tokenScore.invoke(owner, "tokenFallback", cpfTreasury, BigInteger.valueOf(102).multiply(MULTIPLIER), budgetAdjustmentData.toString().getBytes()); + + @SuppressWarnings("unchecked") + Map proposalDataDetails = (Map) tokenScore.call("get_contributor_projected_fund", testing_account2.getAddress()); + @SuppressWarnings("unchecked") + List> proposalDetails = (List>) proposalDataDetails.get("data"); + Map expectedData = Map.of( + consts.IPFS_HASH, "Proposal 1", + consts.TOKEN, "bnUSD", + consts.TOTAL_BUDGET, BigInteger.valueOf(200).multiply(MULTIPLIER), + consts.TOTAL_INSTALLMENT_PAID, BigInteger.ZERO, + consts.TOTAL_INSTALLMENT_COUNT, 3, + consts.TOTAL_TIMES_INSTALLMENT_PAID, 0, + consts.INSTALLMENT_AMOUNT, new BigInteger("66666666666666666666")); + assertEquals(proposalDetails.get(0), expectedData); + } + + void updateProposalFundProposalException(){ + try { + setCPFTreasuryScoreMethod(); + JsonObject budgetAdjustmentData = new JsonObject(); + budgetAdjustmentData.add("method", "budget_adjustment"); + JsonObject params = new JsonObject(); + params.add("_ipfs_key", "Proposal 1"); + params.add("_added_budget", BigInteger.valueOf(100).multiply(MULTIPLIER).toString(16)); + params.add("_added_sponsor_reward", BigInteger.valueOf(2).multiply(MULTIPLIER).toString(16)); + params.add("_added_installment_count", 1); + budgetAdjustmentData.add("params", params); + + tokenScore.invoke(owner, "tokenFallback", cpfTreasury, BigInteger.valueOf(102).multiply(MULTIPLIER), budgetAdjustmentData.toString().getBytes()); + } + catch (Exception e){ + throw e; + } + } + + @Test + void updateProposalFundProposalDoesnotExist(){ + Executable updateProposalFundProposalDoesnotExist = () -> updateProposalFundProposalException(); + expectErrorMessage(updateProposalFundProposalDoesnotExist, "Reverted(0): " + "CPS_TREASURY: Invalid IPFS hash."); + } + + @Test + void sendInstallmentToContributor() { + depositProposalFundMethod(); + setCpsScoreMethod(); + try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(score_address); + tokenScore.invoke(owner, "send_installment_to_contributor", "Proposal 1"); + } + @SuppressWarnings("unchecked") + Map proposalDataDetails = (Map) tokenScore.call("get_contributor_projected_fund", testing_account2.getAddress()); + assertEquals(proposalDataDetails.get("withdraw_amount_bnUSD"), BigInteger.valueOf(50).multiply(MULTIPLIER)); + + try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(score_address); + tokenScore.invoke(owner, "send_installment_to_contributor", "Proposal 1"); + } + @SuppressWarnings("unchecked") + Map proposalDataDetails2 = (Map) tokenScore.call("get_contributor_projected_fund", testing_account2.getAddress()); + assertEquals(proposalDataDetails2.get("withdraw_amount_bnUSD"), BigInteger.valueOf(100).multiply(MULTIPLIER)); + } + + @Test + void sendRewardToSponsor() { + depositProposalFundMethod(); + setCpsScoreMethod(); + sendRewardToSponsorMethod(); + @SuppressWarnings("unchecked") + Map proposalDataDetails = (Map) tokenScore.call("get_sponsor_projected_fund", testing_account.getAddress()); + assertEquals(proposalDataDetails.get("withdraw_amount_bnUSD"), BigInteger.valueOf(1).multiply(MULTIPLIER)); + + try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(score_address); + tokenScore.invoke(owner, "send_reward_to_sponsor", "Proposal 1"); + } + @SuppressWarnings("unchecked") + Map proposalDataDetails2 = (Map) tokenScore.call("get_sponsor_projected_fund", testing_account.getAddress()); + assertEquals(proposalDataDetails2.get("withdraw_amount_bnUSD"), BigInteger.valueOf(2).multiply(MULTIPLIER)); + } + + private void sendRewardToSponsorMethod() { + try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(score_address); + tokenScore.invoke(owner, "send_reward_to_sponsor", "Proposal 1"); + } + } + + @Test + void disqualifyProject(){ + depositProposalFundMethod(); + setCPFTreasuryScoreMethod(); + setCpsScoreMethod(); + setBnUSDScoreMethod(); + try (MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(score_address); + tokenScore.invoke(owner, "disqualify_project", "Proposal 1"); + JsonObject disqualifyProjectParams = new JsonObject(); + disqualifyProjectParams.add("method", "disqualify_project"); + JsonObject params = new JsonObject(); + params.add("ipfs_key", "Proposal 1"); + disqualifyProjectParams.add("params", params); + theMock.verify(() -> Context.call(bnUSDScore, "transfer", cpfTreasury, BigInteger.valueOf(102).multiply(MULTIPLIER), disqualifyProjectParams.toString().getBytes()), times(1)); + } + } + + @Test + void claimReward(){ + depositProposalFundMethod(); + sendRewardToSponsorMethod(); + setBnUSDScoreMethod(); + try(MockedStatic theMock = Mockito.mockStatic(Context.class)) { + theMock.when(() -> Context.getCaller()).thenReturn(testing_account.getAddress()); + tokenScore.invoke(testing_account, "claim_reward"); + theMock.verify(() -> Context.call(bnUSDScore, "transfer", testing_account.getAddress(), BigInteger.valueOf(1).multiply(MULTIPLIER)), times(1)); + } + } +} \ No newline at end of file diff --git a/build.gradle b/build.gradle index 8fa0cddd..1e59de7f 100644 --- a/build.gradle +++ b/build.gradle @@ -3,18 +3,41 @@ buildscript { mavenCentral() } dependencies { - classpath 'foundation.icon:gradle-javaee-plugin:0.8.0' + classpath "foundation.icon:gradle-javaee-plugin:0.8.1" } } +def scoreIntegrationTest = new HashMap() + +task buildContracts(type: Exec) { + workingDir "$projectDir/" + commandLine './gradlew', 'optimizedJar' +} + subprojects { repositories { mavenCentral() } apply plugin: 'java' + apply plugin: 'jacoco' apply plugin: 'foundation.icon.javaee' + sourceSets { + intTest { + compileClasspath += sourceSets.main.output + runtimeClasspath += sourceSets.main.output + java { + } + } + } + + configurations { + intTestImplementation.extendsFrom testImplementation + intTestAnnotationProcessor.extendsFrom testAnnotationProcessor + intTestRuntimeOnly.extendsFrom testRuntimeOnly + } + java { sourceCompatibility = JavaVersion.VERSION_11 targetCompatibility = JavaVersion.VERSION_11 @@ -24,4 +47,29 @@ subprojects { compileJava { options.compilerArgs += ['-parameters'] } + + tasks.named('test') { + // Use JUnit Platform for unit tests. + useJUnitPlatform() + } + + scoreIntegrationTest.put('env.props', new File(project(':test-lib').projectDir, 'conf/env.props')) + + afterEvaluate { project -> + scoreIntegrationTest.put(project.name, project.getTasks().getByName("optimizedJar").outputJarName) + } + + tasks.named("assemble") { + if (project.tasks.findByName("compileIntTestJava")) { + finalizedBy(project.tasks.getByName("compileIntTestJava")) + } + } + + gradle.taskGraph.whenReady { taskGraph -> + taskGraph.getAllTasks().eachWithIndex { task, n -> + if (task.name == 'integrationTest') { + scoreIntegrationTest.each { key, val -> task.systemProperty key, val } + } + } + } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..9375516f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +version: "3.7" +services: + gochain-iconee: + container_name: gochain-iconee + image: public.ecr.aws/f5d2t1f5/goloop-gochain-icon:latest + env_file: + - ./gochain-local/data/single/iconee.env + volumes: + - ./gochain-local/data/single:/goloop/data + - ./gochain-local/data/governance:/goloop/data/gov + - ./gochain-local/chain:/testsuite/chain + ports: + - "9082:9082" \ No newline at end of file diff --git a/dummy/Dex/build.gradle b/dummy/Dex/build.gradle new file mode 100644 index 00000000..bed00959 --- /dev/null +++ b/dummy/Dex/build.gradle @@ -0,0 +1,55 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.1' + implementation project(':score-lib') + + implementation 'com.github.sink772:javaee-tokens:0.6.1' + implementation 'com.github.sink772:minimal-json:0.9.6' + + testImplementation 'foundation.icon:javaee-unittest:0.9.2' + implementation 'org.mockito:mockito-core:4.3.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation('org.mockito:mockito-inline:4.3.1') + implementation 'foundation.icon:javaee-scorex:0.5.2' + +} + +optimizedJar { + mainClassName = 'dummy.contract.dex.Dex' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + + } + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + to = 'cx28ae7ed3b07ed5247a3d2f97680f8555ce7c0a92' + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + } +} + +test { + useJUnitPlatform() +} diff --git a/dummy/Dex/src/main/java/dummy/contract/dex/Dex.java b/dummy/Dex/src/main/java/dummy/contract/dex/Dex.java new file mode 100644 index 00000000..439f8006 --- /dev/null +++ b/dummy/Dex/src/main/java/dummy/contract/dex/Dex.java @@ -0,0 +1,126 @@ +package dummy.contract.dex; + +import com.eclipsesource.json.Json; +import com.eclipsesource.json.JsonObject; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import scorex.util.ArrayList; + +import java.math.BigInteger; +import java.util.Arrays; +import java.util.List; + +public class Dex implements community.icon.cps.score.lib.interfaces.DexInterface { + private static final String TAG = "Balanced DEX"; + private final VarDB
sicx = Context.newVarDB("sicx", Address.class); + public static final BigInteger EXA = BigInteger.valueOf(1_000_000_000_000_000_000L); + + public Dex(){} + + @Override + @External + public void setSicxScore(Address _score){ + this.sicx.set(_score); + } + + @External(readonly = true) + public BigInteger getPrice(int poolId){ + return BigInteger.ONE; + } + + @Override + @EventLog + public void Deposit(Address from_token, Address from, BigInteger value){} + + @Override + @EventLog(indexed = 2) + public void Swap(BigInteger _id, Address _baseToken, Address _fromToken, Address _toToken, + Address _sender, Address _receiver, BigInteger _fromValue, BigInteger _toValue, + BigInteger _timestamp, BigInteger _lpFees, BigInteger _balnFees, BigInteger _poolBase, + BigInteger _poolQuote, BigInteger _endingPrice, BigInteger _effectiveFillPrice) { + } + + @Override + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data) { + // Parse the transaction data submitted by the user + String unpackedData = new String(_data); + Context.require(!unpackedData.equals(""), "Token Fallback: Data can't be empty"); + if (Arrays.equals(_data, "None".getBytes())){ + return; + } + JsonObject json = Json.parse(unpackedData).asObject(); + + String method = json.get("method").asString(); + Address fromToken = Context.getCaller(); + + Context.require(_value.compareTo(BigInteger.ZERO) > 0, TAG + ": Invalid token transfer value"); + + // Call an internal method based on the "method" param sent in tokenFallBack + switch (method) { + case "_swap_icx": { + Context.require(fromToken.equals(sicx.get()), + TAG + ": InvalidAsset: _swap_icx can only be called with sICX"); + swapIcx(_from, _value); + break; + + } + case "_swap": { + + // Parse the slippage sent by the user in minimumReceive. + // If none is sent, use the maximum. + JsonObject params = json.get("params").asObject(); + BigInteger minimumReceive = BigInteger.ZERO; + if (params.contains("minimumReceive")) { + minimumReceive = BigInteger.valueOf(1000).multiply(EXA); + Context.require(minimumReceive.signum() >= 0, + TAG + ": Must specify a positive number for minimum to receive"); + } + + // Check if an alternative recipient of the swap is set. + Address receiver; + if (params.contains("receiver")) { + receiver = Address.fromString(params.get("receiver").asString()); + } else { + receiver = _from; + } + + // Get destination coin from the swap + Context.require(params.contains("toToken"), TAG + ": No toToken specified in swap"); + Address toToken = Address.fromString(params.get("toToken").asString()); + + // Perform the swap + exchange(fromToken, toToken, _from, receiver, _value, minimumReceive); + + break; + } + default: + // If no supported method was sent, revert the transaction + Context.revert(100, TAG + ": Unsupported method supplied"); + break; + } + } + + void swapIcx(Address sender, BigInteger value) { + Context.transfer(sender, value); + } + + void exchange(Address fromToken, Address toToken, Address sender, + Address receiver, BigInteger value, BigInteger minimumReceive) { + + if (minimumReceive == null) { + minimumReceive = BigInteger.ZERO; + } + + // Send the trader their funds + Context.call(toToken, "transfer", receiver, value); + + Swap(BigInteger.valueOf(0), fromToken, fromToken, toToken, sender, receiver, value, value, + BigInteger.valueOf(Context.getBlockTimestamp()), BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO, BigInteger.ZERO + , BigInteger.ZERO); + } +} diff --git a/dummy/Router/build.gradle b/dummy/Router/build.gradle new file mode 100644 index 00000000..3a5641d7 --- /dev/null +++ b/dummy/Router/build.gradle @@ -0,0 +1,55 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.1' + implementation project(':score-lib') + + implementation 'com.github.sink772:javaee-tokens:0.6.1' + implementation 'com.github.sink772:minimal-json:0.9.6' + + testImplementation 'foundation.icon:javaee-unittest:0.9.2' + implementation 'org.mockito:mockito-core:4.3.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation('org.mockito:mockito-inline:4.3.1') + implementation 'foundation.icon:javaee-scorex:0.5.2' + +} + +optimizedJar { + mainClassName = 'dummy.contract.router.Router' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + sejong { + uri = 'https://sejong.net.solidwallet.io/api/v3' + nid = 0x53 + + } + berlin { + uri = 'https://berlin.net.solidwallet.io/api/v3' + nid = 0x7 + to = 'cx28ae7ed3b07ed5247a3d2f97680f8555ce7c0a92' + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + } +} + +test { + useJUnitPlatform() +} diff --git a/dummy/Router/src/main/java/dummy/contract/router/Router.java b/dummy/Router/src/main/java/dummy/contract/router/Router.java new file mode 100644 index 00000000..a8bcd4f7 --- /dev/null +++ b/dummy/Router/src/main/java/dummy/contract/router/Router.java @@ -0,0 +1,46 @@ +package dummy.contract.router; + +import score.Address; +import score.Context; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; +import community.icon.cps.score.lib.interfaces.RouterInterface; + +public class Router implements RouterInterface { + private static final String TAG = "Router"; + + public Router(){} + private void route(Address from, Address startToken, Address[] _path, BigInteger _minReceive) { + Address currentToken = _path[1]; + + + BigInteger balance = (BigInteger) Context.call(currentToken, "balanceOf", Context.getAddress()); + Context.require(balance.compareTo(_minReceive) >= 0, + TAG + ": Below minimum receive amount of " + _minReceive); + Context.call(currentToken, "transfer", from, _minReceive); + + } + + @Override + @Payable + @External + public void route(Address[] _path, @Optional BigInteger _minReceive) { + if (_minReceive == null) { + _minReceive = BigInteger.ZERO; + } + + Context.require(_minReceive.signum() >= 0, TAG + ": Must specify a positive number for minimum to receive"); + + Context.require(_path.length <= 2, + TAG + ": Passed max swaps of " + 2); + + route(Context.getCaller(), null, _path, Context.getValue()); + } + + @External + public void tokenFallback(Address _from, BigInteger _value, byte[] _data){ + } +} diff --git a/dummy/bnUSD/build.gradle b/dummy/bnUSD/build.gradle new file mode 100644 index 00000000..f752b4aa --- /dev/null +++ b/dummy/bnUSD/build.gradle @@ -0,0 +1,43 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.2' + implementation 'com.github.sink772:javaee-tokens:0.6.1' + implementation project(':score-lib') + testImplementation 'foundation.icon:javaee-unittest:0.9.4' + testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +optimizedJar { + mainClassName = 'dummy.contract.bnusd.bnUSD' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_name', 'Balanced Dollar') + arg('_symbol', 'bnUSD') + arg('_decimals', '0x12') + arg('_initialSupply', '0x989680') + } +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/dummy/bnUSD/src/main/java/dummy/contract/bnusd/bnUSD.java b/dummy/bnUSD/src/main/java/dummy/contract/bnusd/bnUSD.java new file mode 100644 index 00000000..0c1560d2 --- /dev/null +++ b/dummy/bnUSD/src/main/java/dummy/contract/bnusd/bnUSD.java @@ -0,0 +1,28 @@ +package dummy.contract.bnusd; + +import community.icon.cps.score.lib.interfaces.bnUSDInterface; +import community.icon.cps.score.lib.tokens.IRC2Base; +import score.Context; + +import java.math.BigInteger; + +public class bnUSD extends IRC2Base implements bnUSDInterface { + public bnUSD(String _name, String _symbol, int _decimals, BigInteger _initialSupply) { + super(_name, _symbol, BigInteger.valueOf(_decimals)); + + // mint the initial token supply here + Context.require(_initialSupply.compareTo(BigInteger.ZERO) >= 0); + mint(Context.getCaller(), _initialSupply.multiply(pow10(_decimals))); + } + + private static BigInteger pow10(int exponent) { + BigInteger result = BigInteger.ONE; + for (int i = 0; i < exponent; i++) { + result = result.multiply(BigInteger.TEN); + } + return result; + } + + +} + diff --git a/dummy/sICX/build.gradle b/dummy/sICX/build.gradle new file mode 100644 index 00000000..4f993073 --- /dev/null +++ b/dummy/sICX/build.gradle @@ -0,0 +1,43 @@ +version = '0.9.1' + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.2' + implementation 'com.github.sink772:javaee-tokens:0.6.1' + implementation project(':score-lib') + testImplementation 'foundation.icon:javaee-unittest:0.9.4' + testImplementation 'org.mockito:mockito-core:4.6.1' + testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.2' + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' +} + +optimizedJar { + mainClassName = 'dummy.contract.sicx.sICX' + from { + configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } + } +} + +deployJar { + endpoints { + lisbon { + uri = 'https://lisbon.net.solidwallet.io/api/v3' + nid = 0x2 + } + local { + uri = 'http://localhost:9082/api/v3' + nid = 0x3 + } + } + keystore = rootProject.hasProperty('keystoreName') ? "$keystoreName" : '' + password = rootProject.hasProperty('keystorePass') ? "$keystorePass" : '' + parameters { + arg('_name', 'Staked ICX') + arg('_symbol', 'sICX') + arg('_decimals', '0x12') + arg('_initialSupply', '0x989680') + } +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/dummy/sICX/src/main/java/dummy/contract/sicx/sICX.java b/dummy/sICX/src/main/java/dummy/contract/sicx/sICX.java new file mode 100644 index 00000000..993e5c54 --- /dev/null +++ b/dummy/sICX/src/main/java/dummy/contract/sicx/sICX.java @@ -0,0 +1,26 @@ +package dummy.contract.sicx; + +import com.iconloop.score.token.irc2.IRC2Basic; +import community.icon.cps.score.lib.tokens.IRC2Base; +import score.Context; + +import java.math.BigInteger; + +public class sICX extends IRC2Base implements community.icon.cps.score.lib.interfaces.sICXInterface { + public sICX(String _name, String _symbol, int _decimals, BigInteger _initialSupply) { + super(_name, _symbol, BigInteger.valueOf(_decimals)); + + // mint the initial token supply here + Context.require(_initialSupply.compareTo(BigInteger.ZERO) >= 0); + mint(Context.getCaller(), _initialSupply.multiply(pow10(_decimals))); + } + + private static BigInteger pow10(int exponent) { + BigInteger result = BigInteger.ONE; + for (int i = 0; i < exponent; i++) { + result = result.multiply(BigInteger.TEN); + } + return result; + } +} + diff --git a/score-client/build.gradle b/score-client/build.gradle new file mode 100644 index 00000000..edbc0eda --- /dev/null +++ b/score-client/build.gradle @@ -0,0 +1,22 @@ +apply plugin: 'java-library' + +optimizedJar.enabled = false + + +dependencies { + implementation("foundation.icon:javaee-annotation-processor:0.9.0") + implementation("com.squareup:javapoet:1.12.1") + implementation("foundation.icon:javaee-api:0.9.0") + implementation("foundation.icon:icon-sdk:2.0.0") + implementation("com.fasterxml.jackson.core:jackson-databind:2.9.6") + implementation("com.squareup.okhttp3:okhttp:3.11.0") + implementation("org.bouncycastle:bcprov-jdk15on:1.60") + + testImplementation("org.junit.jupiter:junit-jupiter-api:5.7.2") + testImplementation("org.junit.jupiter:junit-jupiter-params:5.7.2") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.7.2") +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/Address.java b/score-client/src/main/java/foundation/icon/jsonrpc/Address.java new file mode 100644 index 00000000..48bea738 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/Address.java @@ -0,0 +1,134 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import foundation.icon.icx.Wallet; + +import java.util.Arrays; + +public class Address extends score.Address { + public enum Type { + EOA("hx", 0x0), + CONTRACT("cx", 0x1); + String str; + byte value; + Type(String str, int value) { + this.str = str; + this.value = (byte)value; + } + String str() { + return str; + } + byte value() { + return value; + } + static Type of(byte value) { + for(Type type : values()) { + if (type.value == value) { + return type; + } + } + throw new IllegalArgumentException(); + } + static Type of(String str) { + for(Type type : values()) { + if (type.str.equals(str)) { + return type; + } + } + throw new IllegalArgumentException(); + } + } + public static final int LENGTH = 21; + public static final int BODY_LENGTH = LENGTH - 1; + private final Type type; + private final String str; + private final byte[] bytes; + + @JsonCreator + public Address(String str) { + this(parse(str)); + } + + public Address(byte[] bytes) throws IllegalArgumentException { + super(bytes); + if (bytes == null) { + throw new IllegalArgumentException("raw could not be null"); + } + if (bytes.length != LENGTH) { + throw new IllegalArgumentException("invalid length"); + } + type = Type.of(bytes[0]); + this.bytes = bytes; + this.str = type.str() + IconJsonModule.bytesToHex(bytes).substring(2); + } + + public Address(Type type, byte[] body) throws IllegalArgumentException { + this(concat(type.value(), body)); + } + + public boolean isContract() { + return Type.CONTRACT.equals(type); + } + + public static byte[] concat(byte type, byte[] body) { + byte[] copy = new byte[LENGTH]; + copy[0] = type; + System.arraycopy(body, 0, copy, 1, BODY_LENGTH); + return copy; + } + + public byte[] toByteArray() { + return Arrays.copyOf(bytes, LENGTH); + } + + @Override + public boolean equals(Object obj) { + return this == obj || + (obj instanceof score.Address && toString().equals(obj.toString())); + } + + @JsonValue + @Override + public String toString() { + return str; + } + + public static byte[] parse(String str) { + if (str == null) { + throw new IllegalArgumentException("string could not be null"); + } + if (str.length() != LENGTH * 2) { + throw new IllegalArgumentException("invalid length"); + } + byte[] bytes = new byte[LENGTH]; + bytes[0] = Type.of(str.substring(0, 2)).value(); + System.arraycopy(IconJsonModule.hexToBytes(str.substring(2)), 0, bytes, 1, BODY_LENGTH); + return bytes; + } + + public static Address of(Wallet wallet) { + return of(wallet.getAddress()); + } + + private static Address of(foundation.icon.icx.data.Address address) { + return new Address(address.toString()); + } + +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/IconJsonModule.java b/score-client/src/main/java/foundation/icon/jsonrpc/IconJsonModule.java new file mode 100644 index 00000000..fc81edc8 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/IconJsonModule.java @@ -0,0 +1,454 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.Version; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.core.util.VersionUtil; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.type.TypeFactory; +import com.fasterxml.jackson.databind.util.Converter; + +import java.io.IOException; +import java.math.BigInteger; +import java.util.function.Function; + +public class IconJsonModule extends SimpleModule { + public final static Version VERSION = VersionUtil.parseVersion( + "0.1.0", "foundation.icon", "javaee-score-client" + ); + + public static final String BOOLEAN_TRUE = "0x1"; + public static final String BOOLEAN_FALSE = "0x0"; + public static final String HEX_PREFIX = "0x"; + public static final String NEG_HEX_PREFIX = "-0x"; + private final boolean isIncludeNonNull; + + public static final char[] HEX_CODES = "0123456789abcdef".toCharArray(); + + public static String bytesToHex(byte[] bytes) { + if (bytes == null) { + return ""; + } + StringBuilder r = new StringBuilder(bytes.length * 2); + for (byte b : bytes) { + r.append(HEX_CODES[(b >> 4) & 0xF]); + r.append(HEX_CODES[(b & 0xF)]); + } + return r.toString(); + } + + public static byte[] hexToBytes(String hexString) { + if (hexString == null) { + return null; + } + if (hexString.length() % 2 > 0) { + throw new IllegalArgumentException("hex cannot has odd length"); + } + int l = hexString.length()/2; + int j = 0; + byte[] bytes = new byte[l]; + for (int i = 0; i < l; i++) { + bytes[i] = (byte)((Character.digit(hexString.charAt(j++), 16) << 4) | + Character.digit(hexString.charAt(j++), 16) & 0xFF); + } + return bytes; + } + + public IconJsonModule() { + super(VERSION); + this.isIncludeNonNull = true; + init(); + } + + public IconJsonModule(boolean isIncludeNonNull) { + super(VERSION); + this.isIncludeNonNull = isIncludeNonNull; + init(); + } + + private void init() { + addSerializer(char.class, CharSerializer.CHAR); + addSerializer(Character.class, CharSerializer.CHAR); + addSerializer(byte.class, NumberSerializer.BYTE); + addSerializer(Byte.class, NumberSerializer.BYTE); + addSerializer(long.class, NumberSerializer.LONG); + addSerializer(Long.class, NumberSerializer.LONG); + addSerializer(int.class, NumberSerializer.INTEGER); + addSerializer(Integer.class, NumberSerializer.INTEGER); + addSerializer(short.class, NumberSerializer.SHORT); + addSerializer(Short.class, NumberSerializer.SHORT); + addSerializer(BigInteger.class, NumberSerializer.BIG_INTEGER); + addSerializer(boolean.class, BooleanSerializer.BOOLEAN); + addSerializer(Boolean.class, BooleanSerializer.BOOLEAN); + addSerializer(byte[].class, ByteArraySerializer.BYTE_ARRAY); + addSerializer(score.Address.class, AddressSerializer.SCORE_ADDRESS); +// addSerializer(foundation.icon.icx.data.Address.class, AddressSerializer.SDK_ADDRESS); + + addDeserializer(char.class, CharDeserializer.CHAR); + addDeserializer(Character.class, CharDeserializer.CHAR); + addDeserializer(byte.class, NumberDeserializer.BYTE); + addDeserializer(Byte.class, NumberDeserializer.BYTE); + addDeserializer(long.class, NumberDeserializer.LONG); + addDeserializer(Long.class, NumberDeserializer.LONG); + addDeserializer(int.class, NumberDeserializer.INTEGER); + addDeserializer(Integer.class, NumberDeserializer.INTEGER); + addDeserializer(short.class, NumberDeserializer.SHORT); + addDeserializer(Short.class, NumberDeserializer.SHORT); + addDeserializer(BigInteger.class, NumberDeserializer.BIG_INTEGER); + addDeserializer(boolean.class, BooleanDeserializer.BOOLEAN); + addDeserializer(Boolean.class, BooleanDeserializer.BOOLEAN); + addDeserializer(byte[].class, ByteArrayDeserializer.BYTE_ARRAY); + addDeserializer(score.Address.class, AddressDeserializer.SCORE_ADDRESS); +// addDeserializer(foundation.icon.icx.data.Address.class, AddressDeserializer.SDK_ADDRESS); + } + + @Override + public void setupModule(SetupContext context) { + super.setupModule(context); + if (isIncludeNonNull) { + JsonInclude.Value value = JsonInclude.Value.construct(JsonInclude.Include.NON_NULL, JsonInclude.Include.NON_NULL); + context.configOverride(Long.class).setInclude(value); + context.configOverride(Integer.class).setInclude(value); + context.configOverride(Short.class).setInclude(value); + context.configOverride(BigInteger.class).setInclude(value); + context.configOverride(Boolean.class).setInclude(value); + context.configOverride(byte[].class).setInclude(value); + } + } + + public static class NumberSerializer extends JsonSerializer implements Converter { + public static final NumberSerializer BYTE = new NumberSerializer<>(); + public static final NumberSerializer SHORT = new NumberSerializer<>(); + public static final NumberSerializer INTEGER = new NumberSerializer<>(); + public static final NumberSerializer LONG = new NumberSerializer<>(); + public static final NumberSerializer BIG_INTEGER = new NumberSerializer<>(); + + @Override + public String convert(T t) { + BigInteger bi; + if (t instanceof BigInteger) { + bi = (BigInteger) t; + } else { + bi = BigInteger.valueOf(t.longValue()); + } + String prefix = (bi.signum() == -1) ? NEG_HEX_PREFIX : HEX_PREFIX; + return prefix + bi.abs().toString(16); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(new TypeReference(){}); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public void serialize(T value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(convert(value)); + } + } + + public static class CharSerializer extends JsonSerializer implements Converter { + public static final CharSerializer CHAR = new CharSerializer(); + + @Override + public String convert(Character value) { + return NumberSerializer.INTEGER.convert((int)value); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Character.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public void serialize(Character value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(convert(value)); + } + } + + public static class BooleanSerializer extends JsonSerializer implements Converter { + public static final BooleanSerializer BOOLEAN = new BooleanSerializer(); + + @Override + public String convert(Boolean value) { + return value ? BOOLEAN_TRUE : BOOLEAN_FALSE; + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Boolean.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + if (value != null) { + gen.writeString(convert(value)); + } + } + } + + public static class ByteArraySerializer extends JsonSerializer implements Converter { + public static final ByteArraySerializer BYTE_ARRAY = new ByteArraySerializer(); + + @Override + public void serialize(byte[] value, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(convert(value)); + } + + @Override + public String convert(byte[] value) { + return HEX_PREFIX + bytesToHex(value); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructArrayType(byte.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + } + + public static class AddressSerializer extends JsonSerializer implements Converter { + public static final AddressSerializer SCORE_ADDRESS = new AddressSerializer<>(); + public static final AddressSerializer SDK_ADDRESS = new AddressSerializer<>(); + + @Override + public String convert(T value) { + return value.toString(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(new TypeReference(){}); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public void serialize(T address, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeString(convert(address)); + } + } + + public static class NumberDeserializer extends JsonDeserializer implements Converter{ + public static final NumberDeserializer BYTE = new NumberDeserializer<>(BigInteger::byteValue); + public static final NumberDeserializer SHORT = new NumberDeserializer<>(BigInteger::shortValue); + public static final NumberDeserializer INTEGER = new NumberDeserializer<>(BigInteger::intValue); + public static final NumberDeserializer LONG = new NumberDeserializer<>(BigInteger::longValue); + public static final NumberDeserializer BIG_INTEGER = new NumberDeserializer<>(bi -> bi); + + private final Function parseFunc; + + public NumberDeserializer(Function parseFunc) { + this.parseFunc = parseFunc; + } + + @Override + public T convert(String s) { + if (s.startsWith(HEX_PREFIX)) { + return parseFunc.apply(new BigInteger(s.substring(2), 16)); + } else if (s.startsWith(NEG_HEX_PREFIX)) { + return parseFunc.apply(new BigInteger(s.substring(3), 16).negate()); + } else { +// throw new IllegalArgumentException(String.format("invalid prefix loc:%s", p.getCurrentLocation().toString())); + return parseFunc.apply(new BigInteger(s, 16)); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(new TypeReference(){}); + } + + @SuppressWarnings("unchecked") + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (p.currentToken().isNumeric()) { + return parseFunc.apply(p.getBigIntegerValue()); + } else { + return convert(p.getValueAsString()); + } + } + } + + public static class CharDeserializer extends JsonDeserializer implements Converter { + public static final CharDeserializer CHAR = new CharDeserializer(); + + @Override + public Character convert(String value) { + return (char) NumberDeserializer.INTEGER.convert(value).intValue(); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Boolean.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public Character deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + if (p.currentToken().isNumeric()) { + return (char)p.getIntValue(); + } else { + return convert(p.getValueAsString()); + } + } + } + + public static class BooleanDeserializer extends JsonDeserializer implements Converter { + public static final BooleanDeserializer BOOLEAN = new BooleanDeserializer(); + + @Override + public Boolean convert(String value) { + if (BOOLEAN_TRUE.equals(value)) { + return Boolean.TRUE; + } else if (BOOLEAN_FALSE.equals(value)) { + return Boolean.FALSE; + } + throw new IllegalArgumentException("invalid value:"+value); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(Boolean.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try{ + return convert(p.getValueAsString()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format("fail to deserialize loc:%s err:%s", + p.getCurrentLocation().toString(), e.getMessage()),e); + } + } + } + + public static class ByteArrayDeserializer extends JsonDeserializer implements Converter{ + public static final ByteArrayDeserializer BYTE_ARRAY = new ByteArrayDeserializer(); + + @Override + public byte[] deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + try{ + return convert(p.getValueAsString()); + } catch (IllegalArgumentException e) { + throw new IllegalArgumentException( + String.format("fail to deserialize loc:%s err:%s", + p.getCurrentLocation().toString(), e.getMessage()),e); + } + } + + @Override + public byte[] convert(String s) { + if (s.length() % 2 == 0) { + if (s.startsWith(HEX_PREFIX)) { + s = s.substring(2); + } + return hexToBytes(s); + } else { + throw new IllegalArgumentException("hex string length must be even"); + } + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructArrayType(byte.class); + } + } + + public static class AddressDeserializer extends JsonDeserializer implements Converter { + public static final AddressDeserializer SCORE_ADDRESS = new AddressDeserializer<>(Address::new); + public static final AddressDeserializer SDK_ADDRESS = new AddressDeserializer<>(foundation.icon.icx.data.Address::new); + + private final Function parseFunc; + + public AddressDeserializer(Function parseFunc) { + this.parseFunc = parseFunc; + } + + @Override + public T convert(String value) { + return parseFunc.apply(value); + } + + @Override + public JavaType getInputType(TypeFactory typeFactory) { + return typeFactory.constructType(String.class); + } + + @Override + public JavaType getOutputType(TypeFactory typeFactory) { + return typeFactory.constructType(new TypeReference(){}); + } + + @Override + public T deserialize(JsonParser p, DeserializationContext ctxt) throws IOException { + return convert(p.getValueAsString()); + } + } + +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/JsonrpcClient.java b/score-client/src/main/java/foundation/icon/jsonrpc/JsonrpcClient.java new file mode 100644 index 00000000..4975e581 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/JsonrpcClient.java @@ -0,0 +1,271 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc; + +import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.fasterxml.jackson.databind.deser.std.NumberDeserializers; +import okhttp3.Headers; +import okhttp3.MediaType; +import okhttp3.OkHttpClient; +import okhttp3.Request; +import okhttp3.RequestBody; +import okhttp3.Response; +import okhttp3.ResponseBody; +import okio.BufferedSink; + +import java.io.IOException; + +public class JsonrpcClient { + public static MediaType APPLICATION_JSON = MediaType.parse("application/json"); + + protected final String endpoint; + protected final OkHttpClient httpClient; + protected final ObjectMapper mapper; + protected Headers customHeaders; + protected boolean dumpJson; + + public JsonrpcClient(String endpoint) { + this(endpoint, new OkHttpClient.Builder().build()); + } + + public JsonrpcClient(String endpoint, OkHttpClient httpClient) { + this(endpoint, httpClient, new ObjectMapper()); + } + + public JsonrpcClient(String endpoint, ObjectMapper mapper) { + this(endpoint, new OkHttpClient.Builder().build(), mapper); + } + + public JsonrpcClient(String endpoint, OkHttpClient httpClient, ObjectMapper mapper) { + this.endpoint = endpoint; + this.httpClient = httpClient; + this.mapper = mapper; + } + + public String endpoint() { + return endpoint; + } + + public OkHttpClient httpClient() { + return httpClient; + } + + public ObjectMapper mapper() { + return mapper; + } + + public boolean isDumpJson() { + return dumpJson; + } + + public void setDumpJson(boolean dumpJson) { + this.dumpJson = dumpJson; + } + + public Headers getCustomHeaders() { + return customHeaders; + } + + public void setCustomHeaders(Headers customHeaders) { + this.customHeaders = customHeaders; + } + + private Headers.Builder customHeadersBuilder() { + return customHeaders == null ? new Headers.Builder() : customHeaders.newBuilder(); + } + + public void addCustomHeader(String name) { + customHeaders = customHeadersBuilder().add(name).build(); + } + + public void addCustomHeader(String name, String value) { + customHeaders = customHeadersBuilder().add(name, value).build(); + } + + public void setCustomHeader(String name, String value) { + customHeaders = customHeadersBuilder().set(name, value).build(); + } + + public Object request(String method, Object param) { + return request(Object.class, method, param); + } + + public T request(Class resultType, String method, Object params) { + return request(mapper.getTypeFactory().constructType(resultType), method, params); + } + + public T request(TypeReference resultType, String method, Object params) { + return request(mapper.getTypeFactory().constructType(resultType), method, params); + } + + public T request(JavaType resultType, String method, Object params) { + Request.Builder builder = new Request.Builder() + .url(endpoint) + .post(new JsonrpcRequest(method, params, mapper, dumpJson)); + if (customHeaders != null) { + builder.headers(customHeaders); + } + return request(builder.build(), resultType); + } + + protected T request(Request request, JavaType resultType) { + Response response = null; + try { + response = httpClient.newCall(request).execute(); + } catch (IOException e) { + throw new RuntimeException(e); + } + ResponseBody responseBody = response.body(); + if (responseBody != null) { + JsonrpcResponse jsonrpcResponse = null; + try { + String json = responseBody.string(); + if (dumpJson) { + System.out.println(json); + } + jsonrpcResponse = mapper.readValue(json, + mapper.getTypeFactory().constructParametricType(JsonrpcResponse.class, resultType)); + } catch (IOException e) { + throw new RuntimeException(e); + } + JsonrpcError error = jsonrpcResponse.getError(); + if (error != null) { + throw error; + } + return jsonrpcResponse.result; + } else { + throw new RuntimeException("empty body"); + } + } + + public static class JsonrpcRequest extends RequestBody { + @JsonIgnore + ObjectMapper mapper; + @JsonIgnore + boolean dumpJson; + String jsonrpc = "2.0"; + @JsonSerialize(using = LongLikeSerializer.class) + @JsonDeserialize(using = NumberDeserializers.NumberDeserializer.class) + long id; + String method; + @JsonInclude(JsonInclude.Include.NON_NULL) + Object params; + + JsonrpcRequest(String method, Object params, ObjectMapper mapper, boolean dumpJson) { + this.id = System.currentTimeMillis(); + this.method = method; + this.params = params; + this.mapper = mapper; + this.dumpJson = dumpJson; + } + + @Override + public MediaType contentType() { + return APPLICATION_JSON; + } + + @Override + public void writeTo(BufferedSink bufferedSink) throws IOException { + if (dumpJson) { + byte[] bytes = mapper.writeValueAsBytes(this); + bufferedSink.write(bytes); + System.out.println(new String(bytes)); + } else { + mapper.writeValue(bufferedSink.outputStream(), this); + } + } + + public String getJsonrpc() { + return jsonrpc; + } + + public long getId() { + return id; + } + + public String getMethod() { + return method; + } + + public Object getParams() { + return params; + } + } + + @JsonIgnoreProperties(ignoreUnknown = true) + public static class JsonrpcResponse { + String jsonrpc = "2.0"; + @JsonSerialize(using = LongLikeSerializer.class) + @JsonDeserialize(using = NumberDeserializers.NumberDeserializer.class) + long id; + T result; + JsonrpcError error; + + public String getJsonrpc() { + return jsonrpc; + } + + public long getId() { + return id; + } + + public T getResult() { + return result; + } + + public JsonrpcError getError() { + return error; + } + } + + public static class JsonrpcError extends RuntimeException { + @JsonSerialize(using = LongLikeSerializer.class) + @JsonDeserialize(using = NumberDeserializers.NumberDeserializer.class) + private long code; + private String message; + private byte[] data; + + public long getCode() { + return code; + } + + public String getMessage() { + return message; + } + + public byte[] getData() { + return data; + } + } + + public static class LongLikeSerializer extends JsonSerializer { + @Override + public void serialize(Long aLong, JsonGenerator gen, SerializerProvider serializers) throws IOException { + gen.writeNumber(((Number) aLong).intValue()); + } + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/SendTransactionParamSerializer.java b/score-client/src/main/java/foundation/icon/jsonrpc/SendTransactionParamSerializer.java new file mode 100644 index 00000000..898ea823 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/SendTransactionParamSerializer.java @@ -0,0 +1,95 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import foundation.icon.jsonrpc.model.SendTransactionParam; + +import java.io.IOException; +import java.util.Collection; +import java.util.Map; +import java.util.StringJoiner; +import java.util.TreeSet; + +public class SendTransactionParamSerializer { + static final String PREFIX = "icx_sendTransaction."; + + static ObjectMapper iconMapper = new ObjectMapper(); + static { + iconMapper.registerModule(new IconJsonModule()); + iconMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + static ObjectMapper mapper = new ObjectMapper(); + + public static String serialize(SendTransactionParam sendTransactionParam) throws IOException { + return serialize(sendTransactionParam, null); + } + + public static String serialize(SendTransactionParam sendTransactionParam, Map buffer) throws IOException { + String json = iconMapper.writeValueAsString(sendTransactionParam); + Map params = mapper.readValue(json, new TypeReference>() {}); + if (buffer != null) { + buffer.putAll(params); + } + return PREFIX + serializeMap(params); + } + + public static String serializeObject(Object object) { + if (object == null) { + return "\\0"; + } else if (object instanceof Map) { + // noinspection unchecked + return "{" + serializeMap((Map) object) + "}"; + } else if (object instanceof Collection) { + return "[" + serializeArray(object) + "]"; + } else if (object instanceof String) { + return escape((String) object); + } else { + throw new RuntimeException(String.format("not supported class:%s", object.getClass().getName())); + } + } + + public static String serializeMap(Map map) { + StringJoiner joiner = new StringJoiner("."); + TreeSet keys = new TreeSet<>(map.keySet()); + for (String key : keys) { + joiner.add(key); + joiner.add(serializeObject(map.get(key))); + } + return joiner.toString(); + } + + public static String serializeArray(Object arrayOrCollection) { + StringJoiner joiner = new StringJoiner("."); + if (arrayOrCollection instanceof Collection) { + Collection collection = ((Collection) arrayOrCollection); + for (Object element : collection) { + joiner.add(serializeObject(element)); + } + } else { + throw new RuntimeException(String.format("not supported class:%s", arrayOrCollection.getClass().getName())); + } + return joiner.toString(); + } + + public static String escape(String string) { + return string.replaceAll("([\\\\.{}\\[\\]])", "\\\\$1"); + } + +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/AbstractTransaction.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/AbstractTransaction.java new file mode 100644 index 00000000..a86f2f75 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/AbstractTransaction.java @@ -0,0 +1,76 @@ +package foundation.icon.jsonrpc.model; + +import com.fasterxml.jackson.annotation.JsonInclude; +import foundation.icon.jsonrpc.Address; + +import java.math.BigInteger; + +public abstract class AbstractTransaction { + protected BigInteger version = new BigInteger("3"); + protected Address from; + protected Address to; + @JsonInclude(JsonInclude.Include.NON_NULL) + protected BigInteger value; + protected BigInteger timestamp; + protected BigInteger nid; + @JsonInclude(JsonInclude.Include.NON_NULL) + protected BigInteger nonce; + @JsonInclude(JsonInclude.Include.NON_NULL) + protected String dataType; + @JsonInclude(JsonInclude.Include.NON_NULL) + protected Object data; + + public BigInteger getVersion() { + return version; + } + + public Address getFrom() { + return from; + } + + public Address getTo() { + return to; + } + + public BigInteger getValue() { + return value; + } + + public BigInteger getTimestamp() { + return timestamp; + } + + public BigInteger getNid() { + return nid; + } + + public BigInteger getNonce() { + return nonce; + } + + public String getDataType() { + return dataType; + } + + public Object getData() { + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("AbstractTransaction{"); + sb.append("version=").append(version); + sb.append(", from=").append(from); + sb.append(", to=").append(to); + sb.append(", value=").append(value); + sb.append(", timestamp=").append(timestamp); + sb.append(", nid=").append(nid); + sb.append(", nonce=").append(nonce); + sb.append(", dataType='").append(dataType).append('\''); + sb.append(", data=").append(data); + sb.append('}'); + return sb.toString(); + } + +} + diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/CallData.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/CallData.java new file mode 100644 index 00000000..27e909a2 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/CallData.java @@ -0,0 +1,37 @@ +package foundation.icon.jsonrpc.model; + +import java.util.Map; +import java.util.Objects; + +public class CallData { + private String method; + private Map params; + + public CallData(String method, Map params) { + Objects.requireNonNull(method, "method required not null"); + if (method.isEmpty()) { + throw new IllegalArgumentException("method required not empty"); + } + this.method = method; + this.params = params; + } + + public String getMethod() { + return method; + } + + public Map getParams() { + return params; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CallData{"); + sb.append("method='").append(method).append('\''); + sb.append(", params=").append(params); + sb.append('}'); + return sb.toString(); + } +} + + diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/CallParam.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/CallParam.java new file mode 100644 index 00000000..fe44e131 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/CallParam.java @@ -0,0 +1,52 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import foundation.icon.jsonrpc.Address; + +public class CallParam { + private Address to; + private String dataType = "call"; + private CallData data; + + public CallParam(Address to, CallData data) { + this.to = to; + this.data = data; + } + + public Address getTo() { + return to; + } + + public String getDataType() { + return dataType; + } + + public CallData getData() { + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CallParam{"); + sb.append("to=").append(to); + sb.append(", dataType='").append(dataType).append('\''); + sb.append(", data=").append(data); + sb.append('}'); + return sb.toString(); + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/DeployData.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/DeployData.java new file mode 100644 index 00000000..c3bd4aa5 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/DeployData.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import foundation.icon.jsonrpc.IconJsonModule; + +import java.util.Map; +import java.util.Objects; + +public class DeployData { + private String contentType; + private byte[] content; + private Map params; + + public DeployData(String contentType, byte[] content, Map params) { + Objects.requireNonNull(contentType, "contentType required not null"); + if (contentType.isEmpty()) { + throw new IllegalArgumentException("contentType required not empty"); + } + Objects.requireNonNull(content, "content required not null"); + if (content.length == 0) { + throw new IllegalArgumentException("content required not empty"); + } + this.contentType = contentType; + this.content = content; + this.params = params; + } + + public String getContentType() { + return contentType; + } + + public byte[] getContent() { + return content; + } + + public Map getParams() { + return params; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("DeployData{"); + sb.append("contentType='").append(contentType).append('\''); + sb.append(", content=").append(IconJsonModule.bytesToHex(content)); + sb.append(", params=").append(params); + sb.append('}'); + return sb.toString(); + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/Hash.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/Hash.java new file mode 100644 index 00000000..d7544148 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/Hash.java @@ -0,0 +1,56 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; +import foundation.icon.jsonrpc.IconJsonModule; + + +public class Hash { + public static final String HEX_PREFIX = "0x"; + + private final byte[] bytes; + + public Hash(byte[] bytes) { + if (bytes == null) { + throw new IllegalArgumentException("bytes could not be null"); + } + this.bytes = bytes; + } + + @JsonCreator + public Hash(String string) { + if (string == null) { + throw new IllegalArgumentException("string could not be null"); + } + if (string.startsWith(HEX_PREFIX)) { + string = string.substring(2); + } + this.bytes = IconJsonModule.hexToBytes(string); + } + + public byte[] toBytes() { + return bytes; + } + + @JsonValue + @Override + public String toString() { + return HEX_PREFIX + IconJsonModule.bytesToHex(bytes); + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/SendTransactionParam.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/SendTransactionParam.java new file mode 100644 index 00000000..31482694 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/SendTransactionParam.java @@ -0,0 +1,64 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import foundation.icon.jsonrpc.Address; + +import java.math.BigInteger; +import java.util.Objects; + +public class SendTransactionParam extends TransactionParam { + private BigInteger stepLimit; + + public SendTransactionParam(BigInteger nid, Address to, BigInteger value, String dataType, Object data) { + super(nid, to, value, dataType, data); + Objects.requireNonNull(nid, "nid required not null"); + Objects.requireNonNull(to, "to Address required not null"); + if (value != null && value.signum() == -1) { + throw new IllegalArgumentException("nid must be positive"); + } + } + + public BigInteger getStepLimit() { + return stepLimit; + } + + public void setStepLimit(BigInteger stepLimit) { + this.stepLimit = stepLimit; + } + + public void setFrom(Address from) { + this.from = from; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("SendTransactionParam{"); + sb.append("stepLimit=").append(stepLimit); + sb.append(", version=").append(version); + sb.append(", from=").append(from); + sb.append(", to=").append(to); + sb.append(", value=").append(value); + sb.append(", timestamp=").append(timestamp); + sb.append(", nid=").append(nid); + sb.append(", nonce=").append(nonce); + sb.append(", dataType='").append(dataType).append('\''); + sb.append(", data=").append(data); + sb.append('}'); + return sb.toString(); + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionParam.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionParam.java new file mode 100644 index 00000000..28ae9e75 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionParam.java @@ -0,0 +1,61 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import foundation.icon.jsonrpc.Address; + +import java.math.BigInteger; + +public class TransactionParam extends AbstractTransaction { + public static final BigInteger TIMESTAMP_MSEC_SCALE = BigInteger.valueOf(1000); + public static BigInteger currentTimestamp() { + return TIMESTAMP_MSEC_SCALE.multiply(BigInteger.valueOf(System.currentTimeMillis())); + } + + public TransactionParam(BigInteger nid, Address to, BigInteger value, String dataType, Object data) { + super(); + this.nid = nid; + this.to = to; + this.value = value; + this.dataType = dataType; + this.data = data; + } + + public void setFrom(Address from) { + this.from = from; + } + + public void setTimestamp(BigInteger timestamp) { + this.timestamp = timestamp; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TransactionParam{"); + sb.append("version=").append(version); + sb.append(", from=").append(from); + sb.append(", to=").append(to); + sb.append(", value=").append(value); + sb.append(", timestamp=").append(timestamp); + sb.append(", nid=").append(nid); + sb.append(", nonce=").append(nonce); + sb.append(", dataType='").append(dataType).append('\''); + sb.append(", data=").append(data); + sb.append('}'); + return sb.toString(); + } +} diff --git a/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionResult.java b/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionResult.java new file mode 100644 index 00000000..c9adb3d6 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/jsonrpc/model/TransactionResult.java @@ -0,0 +1,173 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.jsonrpc.model; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import foundation.icon.jsonrpc.Address; +import foundation.icon.jsonrpc.IconJsonModule; + +import java.math.BigInteger; +import java.util.List; + +@JsonIgnoreProperties(ignoreUnknown = true) +public class TransactionResult { + private Address to; + private BigInteger cumulativeStepUsed; + private BigInteger stepUsed; + private BigInteger stepPrice; + private List eventLogs; + private byte[] logsBloom; + private BigInteger status; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Failure failure; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Address scoreAddress; + private Hash blockHash; + private BigInteger blockHeight; + private BigInteger txIndex; + private Hash txHash; + @JsonInclude(JsonInclude.Include.NON_NULL) + private Object stepUsedDetails; //[]feePayment + + public Address getTo() { + return to; + } + + public BigInteger getCumulativeStepUsed() { + return cumulativeStepUsed; + } + + public BigInteger getStepUsed() { + return stepUsed; + } + + public BigInteger getStepPrice() { + return stepPrice; + } + + public List getEventLogs() { + return eventLogs; + } + + public byte[] getLogsBloom() { + return logsBloom; + } + + public BigInteger getStatus() { + return status; + } + + public Failure getFailure() { + return failure; + } + + public Address getScoreAddress() { + return scoreAddress; + } + + public Hash getBlockHash() { + return blockHash; + } + + public BigInteger getBlockHeight() { + return blockHeight; + } + + public BigInteger getTxIndex() { + return txIndex; + } + + public Hash getTxHash() { + return txHash; + } + + public Object getStepUsedDetails() { + return stepUsedDetails; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("TransactionResult{"); + sb.append("to=").append(to); + sb.append(", cumulativeStepUsed=").append(cumulativeStepUsed); + sb.append(", stepUsed=").append(stepUsed); + sb.append(", stepPrice=").append(stepPrice); + sb.append(", eventLogs=").append(eventLogs); + sb.append(", logsBloom=").append(IconJsonModule.bytesToHex(logsBloom)); + sb.append(", status=").append(status); + sb.append(", failure=").append(failure); + sb.append(", scoreAddress=").append(scoreAddress); + sb.append(", blockHash=").append(blockHash); + sb.append(", blockHeight=").append(blockHeight); + sb.append(", txIndex=").append(txIndex); + sb.append(", txHash=").append(txHash); + sb.append(", stepUsedDetails=").append(stepUsedDetails); + sb.append('}'); + return sb.toString(); + } + + public static class EventLog { + private Address scoreAddress; + private List indexed; + private List data; + + public Address getScoreAddress() { + return scoreAddress; + } + + public List getIndexed() { + return indexed; + } + + public List getData() { + return data; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("EventLog{"); + sb.append("scoreAddress=").append(scoreAddress); + sb.append(", indexed=").append(indexed); + sb.append(", data=").append(data); + sb.append('}'); + return sb.toString(); + } + } + + public static class Failure { + private BigInteger code; + private String message; + + public BigInteger getCode() { + return code; + } + + public String getMessage() { + return message; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("Failure{"); + sb.append("code=").append(code); + sb.append(", message='").append(message).append('\''); + sb.append('}'); + return sb.toString(); + } + } +} diff --git a/score-client/src/main/java/foundation/icon/score/client/DefaultScoreClient.java b/score-client/src/main/java/foundation/icon/score/client/DefaultScoreClient.java new file mode 100644 index 00000000..34593d68 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/DefaultScoreClient.java @@ -0,0 +1,510 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.type.TypeReference; +import foundation.icon.icx.KeyWallet; +import foundation.icon.icx.Wallet; +import foundation.icon.icx.crypto.KeystoreException; +import foundation.icon.jsonrpc.Address; +import foundation.icon.jsonrpc.IconJsonModule; +import foundation.icon.jsonrpc.JsonrpcClient; +import foundation.icon.jsonrpc.SendTransactionParamSerializer; +import foundation.icon.jsonrpc.model.CallData; +import foundation.icon.jsonrpc.model.CallParam; +import foundation.icon.jsonrpc.model.DeployData; +import foundation.icon.jsonrpc.model.Hash; +import foundation.icon.jsonrpc.model.SendTransactionParam; +import foundation.icon.jsonrpc.model.TransactionParam; +import foundation.icon.jsonrpc.model.TransactionResult; +import org.bouncycastle.jcajce.provider.digest.SHA3; +import org.bouncycastle.util.encoders.Base64; +import score.UserRevertedException; + +import java.io.File; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Properties; + +public class DefaultScoreClient extends JsonrpcClient { + public static final Address ZERO_ADDRESS = new Address("cx0000000000000000000000000000000000000000"); + public static final BigInteger DEFAULT_STEP_LIMIT = new BigInteger("9502f900", 16); + public static final long BLOCK_INTERVAL = 1; + public static final long DEFAULT_RESULT_RETRY_WAIT = 0; + public static final long DEFAULT_RESULT_TIMEOUT = 10000; + + protected final BigInteger nid; + protected final Wallet wallet; + protected final Address address; + protected BigInteger stepLimit = DEFAULT_STEP_LIMIT; + protected long resultTimeout = DEFAULT_RESULT_TIMEOUT; + + public DefaultScoreClient(String url, String nid, String keyStorePath, String keyStorePassword, String address) { + this(url, nid(nid), wallet(keyStorePath, keyStorePassword), new Address(address)); + } + + public DefaultScoreClient(String url, BigInteger nid, Wallet wallet, Address address) { + super(url); + initialize(this); + + this.nid = nid; + this.wallet = wallet; + this.address = address; + } + + public DefaultScoreClient(DefaultScoreClient client) { + super(client.endpoint); + initialize(this); + + this.nid = client._nid(); + this.wallet = client._wallet(); + this.address = client._address(); + this.stepLimit = client._stepLimit(); + this.resultTimeout = client._resultTimeout(); + } + + static void initialize(JsonrpcClient client) { + client.mapper().registerModule(new IconJsonModule()); + client.mapper().setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + public static DefaultScoreClient _deploy(String url, String nid, String keyStorePath, String keyStorePassword, String scoreFilePath, Map params) { + return _deploy(url, nid(nid), wallet(keyStorePath, keyStorePassword), scoreFilePath, params); + } + + public static DefaultScoreClient _deploy(String url, BigInteger nid, Wallet wallet, String scoreFilePath, Map params) { + JsonrpcClient client = new JsonrpcClient(url); + initialize(client); + Address address = deploy(client, nid, wallet, DEFAULT_STEP_LIMIT, ZERO_ADDRESS, scoreFilePath, params, DEFAULT_RESULT_TIMEOUT); + return new DefaultScoreClient(url, nid, wallet, address); + } + + public static Hash _deployAsync(String url, BigInteger nid, Wallet wallet, String scoreFilePath, Map params) { + JsonrpcClient client = new JsonrpcClient(url); + initialize(client); + return deployAsync(client, nid, wallet, DEFAULT_STEP_LIMIT, ZERO_ADDRESS, scoreFilePath, params); + } + + public static DefaultScoreClient getDeploymentResult(String url, BigInteger nid, Wallet wallet, Hash hash) { + JsonrpcClient client = new JsonrpcClient(url); + initialize(client); + TransactionResult txr = result(client, hash, DEFAULT_RESULT_TIMEOUT); + Address address = txr.getScoreAddress(); + return new DefaultScoreClient(url, nid, wallet, address); + } + + public void _update(String scoreFilePath, Map params) { + deploy(this, nid, wallet, DEFAULT_STEP_LIMIT, address, scoreFilePath, params, DEFAULT_RESULT_TIMEOUT); + } + + public BigInteger _nid() { + return nid; + } + + public Wallet _wallet() { + return wallet; + } + + public Address _address() { + return address; + } + + public BigInteger _stepLimit() { + return stepLimit; + } + + public void _stepLimit(BigInteger stepLimit) { + this.stepLimit = stepLimit; + } + + public long _resultTimeout() { + return resultTimeout; + } + + public void _resultTimeout(long resultTimeout) { + this.resultTimeout = resultTimeout; + } + + public T _call(Class responseType, String method, Map params) { + return call(this, responseType, address, method, params); + } + + public T _call(TypeReference responseType, String method, Map params) { + return call(this, responseType, address, method, params); + } + + public TransactionResult _send(String method, Map params) { + return send(this, nid, wallet, stepLimit, address, null, method, params, resultTimeout); + } + + public TransactionResult _send(BigInteger valueForPayable, String method, Map params) { + return send(this, nid, wallet, stepLimit, address, valueForPayable, method, params, resultTimeout); + } + + public TransactionResult _transfer(Address to, BigInteger value, String message) { + return transfer(this, nid, wallet, stepLimit, to, value, message, resultTimeout); + } + + public BigInteger _balance() { + return balance(this, address); + } + + public BigInteger _balance(Address address) { + return balance(this, address); + } + + @JsonIgnoreProperties(ignoreUnknown = true) + static class BlockHeight { + BigInteger height; + + public BigInteger getHeight() { + return height; + } + } + + public BigInteger _lastBlockHeight() { + return lastBlock(this, BlockHeight.class).height; + } + + + public static DefaultScoreClient of(Properties properties) { + return of("", properties); + } + + public static DefaultScoreClient of(Properties properties, Map params) { + return of("", properties, params); + } + + public static DefaultScoreClient of(String prefix, Properties properties) { + return of(prefix, properties, null); + } + + public static DefaultScoreClient of(String prefix, Properties properties, Map params) { + String url = url(prefix, properties); + BigInteger nid = nid(prefix, properties); + Wallet wallet = wallet(prefix, properties); + Address address = address(prefix, properties); + String scoreFilePath = scoreFilePath(prefix, properties); + Map deployParams = params(prefix, properties, params); + if (address == null) { + System.out.printf("deploy prefix: %s, url: %s, nid: %s, keyStorePath: %s, scoreFilePath: %s, params: %s%n", + prefix, url, nid, wallet != null ? wallet.getAddress() : wallet, scoreFilePath, deployParams); + return _deploy(url, nid, wallet, scoreFilePath, deployParams); + } else { + System.out.printf("prefix: %s, url: %s, nid: %s, wallet: %s, address: %s%n", + prefix, url, nid, wallet != null ? wallet.getAddress() : wallet, address); + DefaultScoreClient client = new DefaultScoreClient(url, nid, wallet, address); + if (isUpdate(prefix, properties) && scoreFilePath != null && !scoreFilePath.isEmpty()) { + System.out.printf("update scoreFilePath: %s, params: %s%n", scoreFilePath, deployParams); + client._update(scoreFilePath, deployParams); + } + return client; + } + } + + public static String url(Properties properties) { + return url("", properties); + } + + public static String url(String prefix, Properties properties) { + return properties.getProperty(prefix+"url"); + } + + public static BigInteger nid(Properties properties) { + return nid("", properties); + } + + public static BigInteger nid(String prefix, Properties properties) { + return nid(properties.getProperty(prefix+"nid")); + } + + public static BigInteger nid(String nid) { + if (nid.startsWith("0x")) { + return new BigInteger(nid.substring(2), 16); + } else { + return new BigInteger(nid); + } + } + + public static Wallet wallet(Properties properties) { + return wallet("", properties); + } + + public static Wallet wallet(String prefix, Properties properties) { + String keyStore = properties.getProperty(prefix+"keyStore"); + if (keyStore == null || keyStore.isEmpty()) { + return null; + } + String keyPassword = properties.getProperty(prefix+"keyPassword"); + if (keyPassword == null || keyPassword.isEmpty()) { + String keySecret = properties.getProperty(prefix+"keySecret"); + try { + System.out.println("using keySecret "+keySecret); + keyPassword = Files.readString(Path.of(keySecret)); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + return wallet(keyStore, keyPassword); + } + + public static Wallet wallet(String keyStorePath, String keyStorePassword) { + try { + System.out.println("load wallet "+keyStorePath); + return KeyWallet.load(keyStorePassword, new File(keyStorePath)); + } catch (IOException | KeystoreException e) { + throw new RuntimeException(e); + } + } + + public static Address address(Properties properties) { + return address("", properties); + } + + public static Address address(String prefix, Properties properties) { + String address = properties.getProperty(prefix+"address"); + if (address == null || address.isEmpty()) { + return null; + } + return address(address); + } + + public static Address address(String address) { + return new Address(address); + } + + public static boolean isUpdate(Properties properties) { + return isUpdate("", properties); + } + + public static boolean isUpdate(String prefix, Properties properties) { + return Boolean.parseBoolean( + (String)properties.getOrDefault(prefix+"isUpdate", + Boolean.FALSE.toString())); + } + + public static String scoreFilePath(Properties properties) { + return scoreFilePath("", properties); + } + + public static String scoreFilePath(String prefix, Properties properties) { + return properties.getProperty(prefix+"scoreFilePath"); + } + + public static Map params(Properties properties) { + return params("", properties); + } + + public static Map params(String prefix, Properties properties) { + return params(prefix, properties, null); + } + + public static Map params(String prefix, Properties properties, Map overwrite) { + String paramsKey = prefix+"params."; + Map params = new HashMap<>(); + for(Map.Entry entry : properties.entrySet()) { + String key = ((String)entry.getKey()); + if (key.startsWith(paramsKey)) { + params.put(key.substring(paramsKey.length()), entry.getValue()); + } + } + if (overwrite != null) { + for(Map.Entry entry : overwrite.entrySet()) { + params.put(entry.getKey(), entry.getValue()); + } + } + return params.isEmpty() ? null : params; + } + + + public static CallData callData(String method, Map params) { + return new CallData(method, params != null && !params.isEmpty() ? params : null); + } + + public static T call( + JsonrpcClient client, Class responseType, Address address, + String method, Map params) { + return client.request(responseType, "icx_call", new CallParam(address, callData(method, params))); + } + + public static T call( + JsonrpcClient client, TypeReference responseType, Address address, + String method, Map params) { + return client.request(responseType, "icx_call", new CallParam(address, callData(method, params))); + } + + static Hash sendTransaction(JsonrpcClient client, Wallet wallet, SendTransactionParam sendTransactionParam) { + Objects.requireNonNull(client, "client required not null"); + Objects.requireNonNull(wallet, "wallet required not null"); + Objects.requireNonNull(wallet, "sendTransactionParam required not null"); + + sendTransactionParam.setFrom(Address.of(wallet)); + if (sendTransactionParam.getTimestamp() == null) { + sendTransactionParam.setTimestamp(TransactionParam.currentTimestamp()); + } + if (sendTransactionParam.getStepLimit() == null) { + sendTransactionParam.setStepLimit(DEFAULT_STEP_LIMIT); + } + if (sendTransactionParam.getNid() == null) { + throw new IllegalArgumentException("nid could not be null"); + } + + Map params = new HashMap<>(); + String serialized; + try { + serialized = SendTransactionParamSerializer.serialize(sendTransactionParam, params); + } catch (IOException e) { + throw new RuntimeException(e); + } + byte[] digest = new SHA3.Digest256().digest(serialized.getBytes(StandardCharsets.UTF_8)); + String signature = Base64.toBase64String(wallet.sign(digest)); + params.put("signature", signature); + return client.request(Hash.class, "icx_sendTransaction", params); + } + + static void waitBlockInterval() { + // System.out.printf("wait block interval %d msec%n", BLOCK_INTERVAL); + // try { + // Thread.sleep(BLOCK_INTERVAL); + // } catch (InterruptedException ie) { + // ie.printStackTrace(); + // } + } + + public static TransactionResult send( + JsonrpcClient client, BigInteger nid, Wallet wallet, BigInteger stepLimit, Address address, + BigInteger valueForPayable, String method, Map params, + long timeout) { + SendTransactionParam tx = new SendTransactionParam(nid, address, valueForPayable, "call", callData(method, params)); + Hash txh = sendTransaction(client, wallet, tx); + waitBlockInterval(); + System.out.println("tx hash is: "+txh); + return result(client, txh, timeout); + } + + public static Address deploy( + JsonrpcClient client, BigInteger nid, Wallet wallet, BigInteger stepLimit, Address address, + String scoreFilePath, Map params, + long timeout) { + + byte[] content; + try { + content = Files.readAllBytes(Path.of(scoreFilePath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + String contentType; + if (scoreFilePath.endsWith(".jar")) { + contentType = "application/java"; + } else if (scoreFilePath.endsWith(".zip")) { + contentType = "application/zip"; + } else { + throw new RuntimeException("not supported score file"); + } + SendTransactionParam tx = new SendTransactionParam(nid, address,null,"deploy", new DeployData(contentType, content, params)); + Hash txh = sendTransaction(client, wallet, tx); + waitBlockInterval(); + TransactionResult txr = result(client, txh, timeout); + System.out.println("SCORE address: "+txr.getScoreAddress()); + return txr.getScoreAddress(); + } + + public static Hash deployAsync( + JsonrpcClient client, BigInteger nid, Wallet wallet, BigInteger stepLimit, Address address, + String scoreFilePath, Map params) { + byte[] content; + try { + content = Files.readAllBytes(Path.of(scoreFilePath)); + } catch (IOException e) { + throw new RuntimeException(e); + } + String contentType; + if (scoreFilePath.endsWith(".jar")) { + contentType = "application/java"; + } else if (scoreFilePath.endsWith(".zip")) { + contentType = "application/zip"; + } else { + throw new RuntimeException("not supported score file"); + } + + SendTransactionParam tx = new SendTransactionParam(nid, address,null,"deploy", new DeployData(contentType, content, params)); + return sendTransaction(client, wallet, tx); + } + + public static TransactionResult transfer( + JsonrpcClient client, BigInteger nid, Wallet wallet, BigInteger stepLimit, Address address, + BigInteger value, String message, long timeout) { + SendTransactionParam tx; + if (message != null) { + tx = new SendTransactionParam(nid, address, value, "message", message.getBytes(StandardCharsets.UTF_8)); + } else { + tx = new SendTransactionParam(nid, address, value, null, null); + } + Hash txh = sendTransaction(client, wallet, tx); + waitBlockInterval(); + return result(client, txh, timeout); + } + + public static TransactionResult result(JsonrpcClient client, Hash txh, long timeout) { + Map params = Map.of("txHash", txh); + long etime = System.currentTimeMillis() + timeout; + TransactionResult txr = null; + while(txr == null) { + try { + txr = client.request(TransactionResult.class, "icx_getTransactionResult", params); + } catch (JsonrpcError e) { + if (e.getCode() == -31002 /* pending */ + || e.getCode() == -31003 /* executing */ + || e.getCode() == -31004 /* not found */) { + if (timeout > 0 && System.currentTimeMillis() >= etime) { + throw new RuntimeException("timeout"); + } + } else { + throw new RuntimeException(e); + } + } + } + if (!BigInteger.ONE.equals(txr.getStatus())) { + TransactionResult.Failure failure = txr.getFailure(); + int revertCode = failure.getCode().intValue(); + String revertMessage = failure.getMessage(); + if (revertCode >= 32) { + throw new UserRevertedException(revertCode - 32, revertMessage); + } else { + throw new RevertedException(revertCode, revertMessage); + } + } + return txr; + } + + public static BigInteger balance(JsonrpcClient client, Address address) { + return client.request(BigInteger.class, "icx_getBalance", Map.of("address", address)); + } + + public static T lastBlock(JsonrpcClient client, Class blockType) { + return client.request(blockType, "icx_getLastBlock", null); + } + +} diff --git a/score-client/src/main/java/foundation/icon/score/client/RevertedException.java b/score-client/src/main/java/foundation/icon/score/client/RevertedException.java new file mode 100644 index 00000000..b6af28d5 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/RevertedException.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +public class RevertedException extends score.RevertedException { + private int code; + public RevertedException(int code, String message) { + super(message); + this.code = code; + } + + public int getCode() { + return code; + } +} diff --git a/score-client/src/main/java/foundation/icon/score/client/ScoreClient.java b/score-client/src/main/java/foundation/icon/score/client/ScoreClient.java new file mode 100644 index 00000000..7ab6d42b --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/ScoreClient.java @@ -0,0 +1,28 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE, ElementType.FIELD}) +@Retention(RetentionPolicy.SOURCE) +public @interface ScoreClient { + String suffix() default "ScoreClient"; +} diff --git a/score-client/src/main/java/foundation/icon/score/client/ScoreClientProcessor.java b/score-client/src/main/java/foundation/icon/score/client/ScoreClientProcessor.java new file mode 100644 index 00000000..d185410b --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/ScoreClientProcessor.java @@ -0,0 +1,425 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.CodeBlock; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.ParameterizedTypeName; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import foundation.icon.annotation_processor.AbstractProcessor; +import foundation.icon.annotation_processor.ProcessorUtil; +import foundation.icon.icx.Wallet; +import foundation.icon.jsonrpc.Address; +import foundation.icon.jsonrpc.model.TransactionResult; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Payable; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.Set; +import java.util.function.Consumer; + +public class ScoreClientProcessor extends AbstractProcessor { + static final String METHOD_OF = "_of"; + static final String PARAM_PROPERTEIS = "properties"; + static final String PARAM_PREFIX = "prefix"; + static final String METHOD_DEPLOY = "_deploy"; + static final String PARAM_URL = "url"; + static final String PARAM_NID = "nid"; + static final String PARAM_WALLET = "wallet"; + static final String PARAM_ADDRESS = "address"; + static final String PARAM_CLIENT = "client"; + static final String PARAM_SCORE_FILE_PATH = "scoreFilePath"; + static final String PARAM_PARAMS = "params"; + // + static final String PARAM_PAYABLE_VALUE = "valueForPayable"; + static final String PARAM_CONSUMER = "consumerFunc"; + static final String PARAM_STEP_LIMIT = "stepLimit"; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set s = new HashSet<>(); + s.add(ScoreClient.class.getCanonicalName()); + return s; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + boolean ret = false; + for (TypeElement annotation : annotations) { + Set annotationElements = roundEnv.getElementsAnnotatedWith(annotation); + for (Element element : annotationElements) { + if (element.getKind().isInterface() || element.getKind().isClass() || element.getKind().isField()) { + messager.noteMessage("process %s %s", element.getKind(), element.asType(), element.getSimpleName()); + generateImplementClass(processingEnv.getFiler(), element); + ret = true; + } else { + throw new RuntimeException("not support"); + } + } + } + return ret; + } + + private void generateImplementClass(Filer filer, Element element) { + TypeElement typeElement; + if (element instanceof TypeElement) { + typeElement = (TypeElement) element; + } else if (element instanceof VariableElement) { + typeElement = super.getTypeElement(element.asType()); + } else { + throw new RuntimeException("not support"); + } + + ClassName elementClassName = ClassName.get(typeElement); + ScoreClient ann = element.getAnnotation(ScoreClient.class); + ClassName className = ClassName.get(elementClassName.packageName(), elementClassName.simpleName() + ann.suffix()); + TypeSpec typeSpec = typeSpec(className, typeElement); + JavaFile javaFile = JavaFile.builder(className.packageName(), typeSpec).build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + messager.warningMessage("create javaFile error : %s", e.getMessage()); + } + } + + private TypeSpec typeSpec(ClassName className, TypeElement element) { + TypeSpec.Builder builder = TypeSpec + .classBuilder(className) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL) + .superclass(DefaultScoreClient.class) + .addSuperinterfaces(ProcessorUtil.getSuperinterfaces(element)); + + if (element.getKind().isInterface()) { + builder.addSuperinterface(element.asType()); + builder.addMethod(deployMethodSpec(className, null)); + } + + //Constructor + builder.addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterSpec.builder(String.class, PARAM_URL).build()) + .addParameter(ParameterSpec.builder(BigInteger.class, PARAM_NID).build()) + .addParameter(ParameterSpec.builder(Wallet.class, PARAM_WALLET).build()) + .addParameter(ParameterSpec.builder(Address.class, PARAM_ADDRESS).build()) + .addStatement("super($L, $L, $L, $L)", + PARAM_URL, PARAM_NID, PARAM_WALLET, PARAM_ADDRESS).build()); + builder.addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterSpec.builder(DefaultScoreClient.class, PARAM_CLIENT).build()) + .addStatement("super($L)", PARAM_CLIENT).build()); + + //_of(Properties) + builder.addMethod(MethodSpec.methodBuilder(METHOD_OF) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(ParameterSpec.builder(Properties.class, PARAM_PROPERTEIS).build()) + .addStatement("return _of(\"\", $L)", PARAM_PROPERTEIS) + .returns(className) + .build()); + //_of(String prefix, Properties) + builder.addMethod(MethodSpec.methodBuilder(METHOD_OF) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(ParameterSpec.builder(String.class, PARAM_PREFIX).build()) + .addParameter(ParameterSpec.builder(Properties.class, PARAM_PROPERTEIS).build()) + .addStatement("return new $T($T.of($L, $L))", className, DefaultScoreClient.class, PARAM_PREFIX, PARAM_PROPERTEIS) + .returns(className) + .build()); + + builder.addMethods(overrideMethods(element)); + builder.addMethods(deployMethods(className, element)); + return builder.build(); + } + + private List overrideMethods(TypeElement element) { + List methods = new ArrayList<>(); + TypeMirror superClass = element.getSuperclass(); + if (!superClass.getKind().equals(TypeKind.NONE) && !superClass.toString().equals(Object.class.getName())) { + messager.noteMessage("superClass[kind:%s, name:%s]", superClass.getKind().name(), superClass.toString()); + List superMethods = overrideMethods(super.getTypeElement(element.getSuperclass())); + methods.addAll(superMethods); + } + + for (TypeMirror inf : element.getInterfaces()) { + TypeElement infElement = super.getTypeElement(inf); + List infMethods = overrideMethods(infElement); + methods.addAll(infMethods); + } + + boolean mustGenerate = element.getKind().isInterface(); + for (Element enclosedElement : element.getEnclosedElements()) { + if (ElementKind.METHOD.equals(enclosedElement.getKind()) && + ProcessorUtil.hasModifier(enclosedElement, Modifier.PUBLIC) && + !ProcessorUtil.hasModifier(enclosedElement, Modifier.STATIC)) { + ExecutableElement ee = (ExecutableElement) enclosedElement; + External external = ee.getAnnotation(External.class); + + if (external != null || mustGenerate) { + CodeBlock paramsCodeblock = paramsCodeblock(ee); + MethodSpec methodSpec = methodSpec(ee, paramsCodeblock); + addMethod(methods, methodSpec, element); + boolean isExternal = external != null ? + !external.readonly() : + methodSpec.returnType.equals(TypeName.VOID); + if (isExternal) { + addMethod(methods, consumerMethodSpec(methodSpec, paramsCodeblock, false), element); + if (ee.getAnnotation(Payable.class) != null) { + addMethod(methods, payableMethodSpec(methodSpec, paramsCodeblock), element); + addMethod(methods, consumerMethodSpec(methodSpec, paramsCodeblock, true), element); + } + } + } + } + } + return methods; + } + + private void addMethod(List methods, MethodSpec methodSpec, TypeElement element) { + if (methodSpec != null) { + MethodSpec conflictMethod = ProcessorUtil.getConflictMethod(methods, methodSpec); + if (conflictMethod != null) { + methods.remove(conflictMethod); + if (element.getKind().isInterface()) { + messager.warningMessage( + "Redeclare '%s %s(%s)' in %s", + conflictMethod.returnType.toString(), + conflictMethod.name, + ProcessorUtil.parameterSpecToString(conflictMethod.parameters), + element.getQualifiedName()); + } + } + methods.add(methodSpec); + } + } + + private CodeBlock paramsCodeblock(ExecutableElement element) { + if (element == null || element.getParameters() == null || element.getParameters().size() == 0) { + return null; + } + CodeBlock.Builder builder = CodeBlock.builder(); + builder.addStatement("$T<$T,$T> $L = new $T<>()", + Map.class, String.class, Object.class, PARAM_PARAMS, HashMap.class); + for (VariableElement ve : element.getParameters()) { + ParameterSpec ps = ParameterSpec.get(ve); + builder.addStatement("$L.put(\"$L\",$L)", PARAM_PARAMS, ps.name, ps.name); + } + return builder.build(); + } + + static Map wrapperTypeNames = Map.of( + TypeKind.BOOLEAN, TypeName.get(Boolean.class), + TypeKind.BYTE, TypeName.get(Boolean.class), + TypeKind.SHORT, TypeName.get(Byte.class), + TypeKind.INT, TypeName.get(Integer.class), + TypeKind.LONG, TypeName.get(Long.class), + TypeKind.CHAR, TypeName.get(Character.class), + TypeKind.FLOAT, TypeName.get(Float.class), + TypeKind.DOUBLE, TypeName.get(Double.class)); + + private MethodSpec methodSpec(ExecutableElement ee, CodeBlock paramsCodeblock) { + if (ee.getAnnotation(EventLog.class) != null) { + return notSupportedMethod(ee, "not supported EventLog method", null); + } + + String methodName = ee.getSimpleName().toString(); + TypeMirror returnType = ee.getReturnType(); + TypeName returnTypeName = TypeName.get(returnType); + External external = ee.getAnnotation(External.class); + + MethodSpec.Builder builder = MethodSpec + .methodBuilder(methodName) + .addModifiers(ProcessorUtil.getModifiers(ee, Modifier.ABSTRACT)) + .addParameters(ProcessorUtil.getParameterSpecs(ee)) + .returns(returnTypeName); +// .addAnnotation(Override.class); + + String params = "null"; + if (paramsCodeblock != null) { + builder.addCode(paramsCodeblock); + params = PARAM_PARAMS; + } + + boolean isVoid = returnTypeName.equals(TypeName.VOID); + boolean isExternal = external != null ? !external.readonly() : isVoid; + if (isExternal) { + if (isVoid) { + builder.addStatement("super._send(\"$L\", $L)", methodName, params); + if (ee.getAnnotation(Payable.class) != null) { + builder.addJavadoc("To payable, use $L($T $L, ...)", methodName, BigInteger.class, PARAM_PAYABLE_VALUE); + } + } else { + return notSupportedMethod(ee, "not supported response of writable method in ScoreClient", + CodeBlock.builder().add("$L($T<$T> $L, ...)", + methodName, Consumer.class, TransactionResult.class, PARAM_CONSUMER).build()); + } + } else { + if (!isVoid) { + if (returnType.getKind().isPrimitive()) { + builder.addStatement("return super._call($T.class, \"$L\", $L)", + wrapperTypeNames.get(returnType.getKind()), methodName, params); + } else { + if (returnType.getKind().equals(TypeKind.DECLARED) && + ((DeclaredType)returnType).getTypeArguments().size() > 0) { + builder.addStatement("return super._call(new $T<$T>(){}, \"$L\", $L)", + TypeReference.class, returnTypeName, methodName, params); + } else { + builder.addStatement("return super._call($T.class, \"$L\", $L)", + returnTypeName, methodName, params); + } + } + } else { + return notSupportedMethod(ee, "not supported, void of readonly method in ScoreClient", null); + } + } + return builder.build(); + } + + private MethodSpec payableMethodSpec(MethodSpec methodSpec, CodeBlock paramsCodeblock) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodSpec.name) + .addModifiers(methodSpec.modifiers) + .addParameter(BigInteger.class, PARAM_PAYABLE_VALUE) + .addParameters(methodSpec.parameters) + .returns(TypeName.VOID); + + String params = "null"; + if (paramsCodeblock != null) { + builder.addCode(paramsCodeblock); + params = PARAM_PARAMS; + } + builder.addStatement("super._send($L, \"$L\", $L)", PARAM_PAYABLE_VALUE, methodSpec.name, params); + return builder.build(); + } + + private MethodSpec consumerMethodSpec(MethodSpec methodSpec, CodeBlock paramsCodeblock, boolean isPayable) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodSpec.name) + .addModifiers(methodSpec.modifiers) + .addParameter(ParameterSpec.builder( + ParameterizedTypeName.get(Consumer.class, TransactionResult.class), PARAM_CONSUMER).build()) + .returns(TypeName.VOID); + + String params = "null"; + if (paramsCodeblock != null) { + builder.addCode(paramsCodeblock); + params = PARAM_PARAMS; + } + if (isPayable) { + builder.addParameter(BigInteger.class, PARAM_PAYABLE_VALUE) + .addStatement("$L.accept(super._send($L, \"$L\", $L))", + PARAM_CONSUMER, PARAM_PAYABLE_VALUE, methodSpec.name, params); + } else { + builder.addStatement("$L.accept(super._send(\"$L\", $L))", PARAM_CONSUMER, methodSpec.name, params); + } + return builder.addParameters(methodSpec.parameters).build(); + } + + private MethodSpec notSupportedMethod(ExecutableElement ee, String msg, CodeBlock instead) { + String methodName = ee.getSimpleName().toString(); + TypeName returnTypeName = TypeName.get(ee.getReturnType()); + return MethodSpec.methodBuilder(methodName) + .addModifiers(ProcessorUtil.getModifiers(ee, Modifier.ABSTRACT)) + .addParameters(ProcessorUtil.getParameterSpecs(ee)) + .returns(returnTypeName) + .addStatement("throw new $T(\"$L\")", RuntimeException.class, msg) + .addJavadoc("@deprecated Do not use this method, this is generated only for preventing compile error.\n Instead, use $L\n", + instead != null ? instead : "N/A") + .addJavadoc("@throws $L(\"$L\")", RuntimeException.class.getName(), msg) + .addAnnotation(Deprecated.class) + .build(); + } + + private List deployMethods(ClassName className, TypeElement element) { + List methods = new ArrayList<>(); + TypeMirror superClass = element.getSuperclass(); + if (!superClass.getKind().equals(TypeKind.NONE) && !superClass.toString().equals(Object.class.getName())) { + messager.noteMessage("superClass[kind:%s, name:%s]", superClass.getKind().name(), superClass.toString()); + List superMethods = deployMethods(className, super.getTypeElement(element.getSuperclass())); + methods.addAll(superMethods); + } + + for (Element enclosedElement : element.getEnclosedElements()) { + if (ElementKind.CONSTRUCTOR.equals(enclosedElement.getKind()) && + ProcessorUtil.hasModifier(enclosedElement, Modifier.PUBLIC)) { + methods.add(deployMethodSpec(className, (ExecutableElement) enclosedElement)); + } + } + return methods; + } + + private MethodSpec deployMethodSpec(ClassName className, ExecutableElement element) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(METHOD_DEPLOY) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(ParameterSpec.builder(String.class, PARAM_URL).build()) + .addParameter(ParameterSpec.builder(BigInteger.class, PARAM_NID).build()) + .addParameter(ParameterSpec.builder(Wallet.class, PARAM_WALLET).build()) + .addParameter(ParameterSpec.builder(String.class, PARAM_SCORE_FILE_PATH).build()) + .returns(className); + + if (element != null) { + builder.addParameters(ProcessorUtil.getParameterSpecs(element)); + } else { + builder.addParameter(ParameterSpec.builder( + ParameterizedTypeName.get(Map.class, String.class, Object.class), PARAM_PARAMS).build()); + } + + CodeBlock paramsCodeblock = paramsCodeblock(element); + if (paramsCodeblock != null) { + builder.addCode(paramsCodeblock); + } + builder + .addStatement("return new $T($T._deploy($L,$L,$L,$L,$L))", + className, DefaultScoreClient.class, + PARAM_URL, PARAM_NID, PARAM_WALLET, PARAM_SCORE_FILE_PATH, + paramsCodeblock != null || element == null ? PARAM_PARAMS : "null") + .build(); + return builder.build(); + } + +} diff --git a/score-client/src/main/java/foundation/icon/score/client/ScoreInterface.java b/score-client/src/main/java/foundation/icon/score/client/ScoreInterface.java new file mode 100644 index 00000000..7bfa3b1a --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/ScoreInterface.java @@ -0,0 +1,29 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +@Target({ElementType.TYPE}) +@Retention(RetentionPolicy.SOURCE) +public @interface ScoreInterface { + String suffix() default "ScoreInterface"; + String addressGetter() default "_address"; +} diff --git a/score-client/src/main/java/foundation/icon/score/client/ScoreInterfaceProcessor.java b/score-client/src/main/java/foundation/icon/score/client/ScoreInterfaceProcessor.java new file mode 100644 index 00000000..70aa69c2 --- /dev/null +++ b/score-client/src/main/java/foundation/icon/score/client/ScoreInterfaceProcessor.java @@ -0,0 +1,282 @@ +/* + * Copyright 2021 ICON Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package foundation.icon.score.client; + +import com.squareup.javapoet.ClassName; +import com.squareup.javapoet.JavaFile; +import com.squareup.javapoet.MethodSpec; +import com.squareup.javapoet.ParameterSpec; +import com.squareup.javapoet.TypeName; +import com.squareup.javapoet.TypeSpec; +import foundation.icon.annotation_processor.AbstractProcessor; +import foundation.icon.annotation_processor.ProcessorUtil; +import score.Address; +import score.Context; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Payable; + +import javax.annotation.processing.Filer; +import javax.annotation.processing.ProcessingEnvironment; +import javax.annotation.processing.RoundEnvironment; +import javax.lang.model.SourceVersion; +import javax.lang.model.element.Element; +import javax.lang.model.element.ElementKind; +import javax.lang.model.element.ExecutableElement; +import javax.lang.model.element.Modifier; +import javax.lang.model.element.TypeElement; +import javax.lang.model.element.VariableElement; +import javax.lang.model.type.DeclaredType; +import javax.lang.model.type.TypeKind; +import javax.lang.model.type.TypeMirror; +import java.io.IOException; +import java.math.BigInteger; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.StringJoiner; + +/* + * TODO generate exception each Method + * GenericPredefinedException + * - Status.OutOfStep + * - Status.StackOverflow + * handle IllegalArgumentException + * - Status.ContractNotFound + * - Status.MethodNotFound + * - Status.MethodNotPayable + * - Status.InvalidParameter + * - Status.OutOfBalance + * - Status.PackageError + * handle RevertedException + * - Status.UnknownFailure + * - s < Status.UserReversionStart + * handle UserRevertedException + * - Status.UserReversionStart <= s < Status.UserReversionEnd + * + * Exception extends score.UserRevertException + * Exception extends Exception + */ +public class ScoreInterfaceProcessor extends AbstractProcessor { + static final String MEMBER_ADDRESS = "address"; + static final String PARAM_PAYABLE_VALUE = "valueForPayable"; + + @Override + public synchronized void init(ProcessingEnvironment processingEnv) { + super.init(processingEnv); + } + + @Override + public Set getSupportedAnnotationTypes() { + Set s = new HashSet<>(); + s.add(ScoreInterface.class.getCanonicalName()); + return s; + } + + @Override + public SourceVersion getSupportedSourceVersion() { + return SourceVersion.latestSupported(); + } + + @Override + public boolean process(Set annotations, RoundEnvironment roundEnv) { + boolean ret = false; + for (TypeElement annotation : annotations) { + Set annotationElements = roundEnv.getElementsAnnotatedWith(annotation); + for (Element element : annotationElements) { + if (element.getKind().isInterface() || element.getKind().isClass()) { + messager.noteMessage("process %s %s", element.getKind(), element.asType(), element.getSimpleName()); + generateImplementClass(processingEnv.getFiler(), (TypeElement) element); + ret = true; + } else { + throw new RuntimeException("not support"); + } + } + } + return ret; + } + + private void generateImplementClass(Filer filer, TypeElement element) { + ClassName interfaceClassName = ClassName.get(element); + TypeSpec typeSpec = typeSpec(element); + JavaFile javaFile = JavaFile.builder(interfaceClassName.packageName(), typeSpec).build(); + try { + javaFile.writeTo(filer); + } catch (IOException e) { + e.printStackTrace(); + } + } + + private TypeSpec typeSpec(TypeElement element) { + ClassName interfaceClassName = ClassName.get(element); + ScoreInterface scoreInterface = element.getAnnotation(ScoreInterface.class); + ClassName className = ClassName.get(interfaceClassName.packageName(), interfaceClassName.simpleName() + scoreInterface.suffix()); + TypeSpec.Builder builder = TypeSpec + .classBuilder(ClassName.get(interfaceClassName.packageName(), className.simpleName())) + .addModifiers(Modifier.PUBLIC, Modifier.FINAL); + + if (element.getKind().isInterface()) { + builder.addSuperinterface(element.asType()); + } + + //Fields + builder.addField(Address.class, MEMBER_ADDRESS, Modifier.PROTECTED, Modifier.FINAL); + + //Constructor + builder.addMethod(MethodSpec.constructorBuilder() + .addModifiers(Modifier.PUBLIC) + .addParameter(ParameterSpec.builder(Address.class, MEMBER_ADDRESS).build()) + .addStatement("this.$L = $L", MEMBER_ADDRESS, MEMBER_ADDRESS).build()); + + //addressGetter + builder.addMethod(MethodSpec.methodBuilder(scoreInterface.addressGetter()) + .addModifiers(Modifier.PUBLIC) + .returns(Address.class) + .addStatement("return this.$L", MEMBER_ADDRESS).build()); + + List methods = overrideMethods(element); + builder.addMethods(methods); + return builder.build(); + } + + private List overrideMethods(TypeElement element) { + List methods = new ArrayList<>(); + TypeMirror superClass = element.getSuperclass(); + if (!superClass.getKind().equals(TypeKind.NONE) && !superClass.toString().equals(Object.class.getName())) { + messager.noteMessage("superClass[kind:%s, name:%s]", superClass.getKind().name(), superClass.toString()); + List superMethods = overrideMethods(super.getTypeElement(element.getSuperclass())); + methods.addAll(superMethods); + } + + for (TypeMirror inf : element.getInterfaces()) { + TypeElement infElement = super.getTypeElement(inf); + List infMethods = overrideMethods(infElement); + methods.addAll(infMethods); + } + + boolean mustGenerate = element.getKind().isInterface(); + for (Element enclosedElement : element.getEnclosedElements()) { + if (ElementKind.METHOD.equals(enclosedElement.getKind()) && + ProcessorUtil.hasModifier(enclosedElement, Modifier.PUBLIC) && + !ProcessorUtil.hasModifier(enclosedElement, Modifier.STATIC)) { + ExecutableElement ee = (ExecutableElement) enclosedElement; + External external = ee.getAnnotation(External.class); + if (external != null || mustGenerate) { + MethodSpec methodSpec = methodSpec(ee, mustGenerate); + addMethod(methods, methodSpec, element); + if (external != null && !external.readonly() && ee.getAnnotation(Payable.class) != null) { + addMethod(methods, payableMethodSpec(ee, methodSpec), element); + } + } + } + } + return methods; + } + + private void addMethod(List methods, MethodSpec methodSpec, TypeElement element) { + if (methodSpec != null) { + MethodSpec conflictMethod = ProcessorUtil.getConflictMethod(methods, methodSpec); + if (conflictMethod != null) { + methods.remove(conflictMethod); + messager.warningMessage( + "Redeclare '%s %s(%s)' in %s", + conflictMethod.returnType.toString(), + conflictMethod.name, + ProcessorUtil.parameterSpecToString(conflictMethod.parameters), + element.getQualifiedName()); + } + methods.add(methodSpec); + } + } + + private String callParameters(ExecutableElement element) { + StringJoiner variables = new StringJoiner(", "); + variables.add(String.format("this.%s", MEMBER_ADDRESS)); + variables.add(String.format("\"%s\"", element.getSimpleName().toString())); + for (VariableElement variableElement : element.getParameters()) { + variables.add(variableElement.getSimpleName().toString()); + } + return variables.toString(); + } + + private MethodSpec methodSpec(ExecutableElement ee, boolean mustGenerate) { + if (ee.getAnnotation(EventLog.class) != null) { + return notSupportedMethod(ee, "not supported EventLog method"); + } + + String methodName = ee.getSimpleName().toString(); + TypeMirror returnType = ee.getReturnType(); + TypeName returnTypeName = TypeName.get(returnType); + + MethodSpec.Builder builder = MethodSpec + .methodBuilder(methodName) +// .addAnnotation(Override.class) + .addModifiers(ProcessorUtil.getModifiers(ee, Modifier.ABSTRACT)) + .addParameters(ProcessorUtil.getParameterSpecs(ee)) + .returns(returnTypeName); + + String callParameters = callParameters(ee); + if (returnTypeName.equals(TypeName.VOID)) { + builder.addStatement("$T.call($L)", Context.class, callParameters); + } else { + if (returnType.getKind().equals(TypeKind.DECLARED) && + ((DeclaredType)returnType).getTypeArguments().size() > 0) { + builder.addStatement("return ($T)$T.call($L)", returnTypeName, Context.class, callParameters); + } else { + builder.addStatement("return $T.call($T.class, $L)", Context.class, returnTypeName, callParameters); + } + } + return builder.build(); + } + + private MethodSpec payableMethodSpec(ExecutableElement ee, MethodSpec methodSpec) { + MethodSpec.Builder builder = MethodSpec.methodBuilder(methodSpec.name) + .addModifiers(methodSpec.modifiers) + .addParameter(BigInteger.class, PARAM_PAYABLE_VALUE) + .addParameters(methodSpec.parameters) + .returns(methodSpec.returnType); + + String callParameters = callParameters(ee); + TypeMirror returnType = ee.getReturnType(); + if (methodSpec.returnType.equals(TypeName.VOID)) { + builder.addStatement("$T.call($L, $L)", Context.class, PARAM_PAYABLE_VALUE, callParameters); + } else { + if (returnType.getKind().equals(TypeKind.DECLARED) && + ((DeclaredType)returnType).getTypeArguments().size() > 0) { + builder.addStatement("return ($T)$T.call($L, $L)", methodSpec.returnType, Context.class, PARAM_PAYABLE_VALUE, callParameters); + } else { + builder.addStatement("return $T.call($T.class, $L, $L)", Context.class, methodSpec.returnType, PARAM_PAYABLE_VALUE, callParameters); + } + } + return builder.build(); + } + + private MethodSpec notSupportedMethod(ExecutableElement ee, String msg) { + String methodName = ee.getSimpleName().toString(); + TypeName returnTypeName = TypeName.get(ee.getReturnType()); + return MethodSpec.methodBuilder(methodName) + .addModifiers(ProcessorUtil.getModifiers(ee, Modifier.ABSTRACT)) + .addParameters(ProcessorUtil.getParameterSpecs(ee)) + .returns(returnTypeName) + .addStatement("throw new $T(\"$L\")", RuntimeException.class, msg) + .addJavadoc("@deprecated Do not use this method, this is generated only for preventing compile error. $L\n", msg) + .addJavadoc("@throws $L", RuntimeException.class.getName()) + .addAnnotation(Deprecated.class) + .build(); + } +} diff --git a/score-lib/build.gradle b/score-lib/build.gradle new file mode 100644 index 00000000..28f2129d --- /dev/null +++ b/score-lib/build.gradle @@ -0,0 +1,34 @@ +plugins { + id 'java' +} + +version '0.1.0' + +repositories { + mavenCentral() +} + +optimizedJar.enabled = false + +dependencies { + compileOnly 'foundation.icon:javaee-api:0.9.1' + implementation 'foundation.icon:javaee-scorex:0.5.2' + implementation 'com.github.sink772:minimal-json:0.9.7' + + compileOnly 'foundation.icon:javaee-score-client:0.9.0' + annotationProcessor 'foundation.icon:javaee-score-client:0.9.0' +// + implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.3' + implementation 'foundation.icon:icon-sdk:2.1.0' +// +// + testImplementation 'foundation.icon:javaee-unittest:0.9.4' + testImplementation "org.junit.jupiter:junit-jupiter:5.8.2" + testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.2' + testImplementation 'org.mockito:mockito-core:4.5.1' + testRuntimeOnly 'org.mockito:mockito-inline:4.5.1' +} + +test { + useJUnitPlatform() +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPFTreasuryInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPFTreasuryInterface.java new file mode 100644 index 00000000..347aaf15 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPFTreasuryInterface.java @@ -0,0 +1,118 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; + +import score.Address; +import score.Context; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +@ScoreClient +@ScoreInterface +public interface CPFTreasuryInterface { + @External(readonly = true) + String name(); + + @External + void setCpsScore(Address _score); + + @External(readonly = true) + Address getCpsScore(); + + @External + void setCpsTreasuryScore(Address _score); + + @External(readonly = true) + Address getCpsTreasuryScore(); + + @External + void setBnUSDScore(Address _score); + + @External(readonly = true) + Address getBnUSDScore(); + + @External + void setSicxScore(Address _score); + + @External(readonly = true) + Address getSicxScore(); + + @External + void setDexScore(Address _score); + + @External(readonly = true) + Address getDexScore(); + + @External + void setRouterScore(Address _score); + + @External(readonly = true) + Address getRouterScore(); + + @External + void setMaximumTreasuryFundIcx(BigInteger _value); + + @External + void setMaximumTreasuryFundBnusd(BigInteger _value); + + @External(readonly = true) + Map get_total_funds(); + + @External(readonly = true) + Map get_remaining_swap_amount(); + + @External + void transfer_proposal_fund_to_cps_treasury(String ipfs_key, int total_installment_count, + Address sponsor_address, Address contributor_address, + String token_flag, BigInteger _total_budget); + + @External + void update_proposal_fund(String ipfs_key, @Optional String flag, @Optional BigInteger _added_budget, + @Optional int _total_installment_count); + + @External + @Payable + void add_fund(); + + @External + void swapIcxBnusd(BigInteger amount); + + @External + void swap_tokens(int _count); + + @External(readonly = true) + Map get_swap_state_status(); + + @External + void reset_swap_state(); + + @External(readonly = true) + Map get_proposal_details(@Optional int start_index, @Optional int end_index); + + @External + void tokenFallback(Address from, BigInteger value, byte[] _data); + + @Payable + void fallback(); + + //EventLogs + @EventLog(indexed = 1) + void FundReturned(Address _sponsor_address, String note); + + @EventLog(indexed = 1) + void ProposalFundTransferred(String _ipfs_key, String note); + + @EventLog(indexed = 1) + void ProposalDisqualified(String _ipfs_key, String note); + + @EventLog(indexed = 1) + void FundReceived(Address _sponsor_address, String note); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSCoreInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSCoreInterface.java new file mode 100644 index 00000000..c029e777 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSCoreInterface.java @@ -0,0 +1,304 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; +import score.Address; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +@ScoreClient +@ScoreInterface +public interface CPSCoreInterface { + + public static class ProposalAttributes { + public String ipfs_hash; + public String project_title; + public int project_duration; + public BigInteger total_budget; + public String token; + public Address sponsor_address; + public String ipfs_link; + } + + public static class ProgressReportAttributes { + public String ipfs_hash; + public String report_hash; + public String ipfs_link; + public String progress_report_title; + public Boolean budget_adjustment; + public BigInteger additional_budget; + public int additional_month; + public int percentage_completed; + } + + @External(readonly = true) + String name(); + + String proposalPrefix(String proposalKey); + + String progressReportPrefix(String progressKey); + + + @External + void set_cps_treasury_score(Address _score); + + @External + void setCpsTreasuryScore(Address score); + + + @External(readonly = true) + Address get_cps_treasury_score(); + + @External(readonly = true) + Address getCpsTreasuryScore(); + + + @External + void set_cpf_treasury_score(Address _score); + + @External + void setCpfTreasuryScore(Address score); + + + @External(readonly = true) + Address get_cpf_treasury_score(); + + @External(readonly = true) + Address getCpfTreasuryScore(); + + + @External + void setBnusdScore(Address _score); + + + @External(readonly = true) + Address getBnusdScore(); + + + @External(readonly = true) + boolean isAdmin(Address address); + + @External + void toggleBudgetAdjustmentFeature(); + + @External(readonly = true) + boolean getBudgetAdjustmentFeature(); + + + @External + void toggleMaintenance(); + + @External(readonly = true) + boolean getMaintenanceMode(); + + @Payable + void fallback(); + + + @External + void addAdmin(Address address); + + + @External + void removeAdmin(Address address); + + + @External + void unregister_prep(); + + + @External + void register_prep(); + + @External(readonly = true) + boolean checkPriorityVoting(Address prep); + + @External(readonly = true) + List sortPriorityProposals(); + + @External(readonly = true) + Map getPriorityVoteResult(); + + @External + void votePriority(String[] proposals); + + + @External + void setPrepPenaltyAmount(BigInteger[] penalty); + + + @External + void setInitialBlock(); + + + @External(readonly = true) + Map login_prep(Address _address); + + + @External(readonly = true) + List
get_admins(); + + @SuppressWarnings("unchecked") + + @External(readonly = true) + Map getRemainingFund(); + + + @External(readonly = true) + List> get_PReps(); + + + @External(readonly = true) + List
get_denylist(); + + + @External(readonly = true) + Map get_period_status(); + + + @External(readonly = true) + List
get_contributors(); + + + @External(readonly = true) + Map check_claimable_sponsor_bond(Address _address); + + + @Payable + @External + void submit_proposal(ProposalAttributes _proposals); + + + @External + void vote_proposal(String _ipfs_key, String _vote, String _vote_reason, @Optional boolean _vote_change); + + + @External + void submit_progress_report(ProgressReportAttributes _progress_report); + + + @External + void vote_progress_report(String _ipfs_key, String _report_key, String _vote, String _vote_reason, @Optional String _budget_adjustment_vote, @Optional boolean _vote_change); + + + @External(readonly = true) + List get_proposals_keys_by_status(String _status); + + + @External(readonly = true) + int check_change_vote(Address _address, String _ipfs_hash, String _proposal_type); + + + @External(readonly = true) + Map get_project_amounts(); + + + @External(readonly = true) + Map get_sponsors_record(); + + @External + void update_period(); + + + @External(readonly = true) + Map getProposalDetails(String status, @Optional Address walletAddress, @Optional int startIndex); + + @External(readonly = true) + Map get_proposal_details_by_hash(String _ipfs_key); + + + @External(readonly = true) + Map getProgressReports(String status, @Optional int startIndex); + + + @External(readonly = true) + Map get_progress_reports_by_hash(String _report_key); + + + @External(readonly = true) + Map get_progress_reports_by_proposal(String _ipfs_key); + + + @External(readonly = true) + Map getSponsorsRequests(String status, Address sponsorAddress, @Optional int startIndex); + + @External(readonly = true) + Map get_vote_result(String _ipfs_key); + + @External(readonly = true) + Map get_progress_report_result(String _report_key); + + @External(readonly = true) + Map get_budget_adjustment_vote_result(String _report_key); + + @External + void tokenFallback(Address _from, BigInteger _value, byte[] _data); + + @External + void remove_denylist_preps(); + + @External + void claim_sponsor_bond(); + + @External + void set_swap_count(int value); + + @External + void updateNextBlock(int blockCount); + + + @External(readonly = true) + Map getActiveProposalsList(@Optional int startIndex); + + @External(readonly = true) + Map getProposalDetailByWallet(Address walletAddress, @Optional int startIndex); + + @External(readonly = true) + Map getProposalsHistory(@Optional int startIndex); + + // EventLogs + @EventLog(indexed = 1) + void ProposalSubmitted(Address _sender_address, String note); + + @EventLog(indexed = 1) + void ProgressReportSubmitted(Address _sender_address, String _project_title); + + @EventLog(indexed = 1) + void SponsorBondReceived(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void SponsorBondRejected(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void VotedSuccessfully(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void PRepPenalty(Address _prep_address, String _notes); + + @EventLog(indexed = 1) + void UnRegisterPRep(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void RegisterPRep(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void SponsorBondReturned(Address _sender_address, String _notes); + + @EventLog(indexed = 1) + void PeriodUpdate(String _notes); + + @EventLog(indexed = 1) + void SponsorBondClaimed(Address _receiver_address, BigInteger _fund, String note); + + @EventLog(indexed = 1) + void PriorityVote(Address _address, String note); + + @External(readonly = true) + int getPeriodCount(); +} \ No newline at end of file diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSTreasuryInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSTreasuryInterface.java new file mode 100644 index 00000000..0959dc03 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/CPSTreasuryInterface.java @@ -0,0 +1,91 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; + +import score.Address; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; +import java.util.List; +import java.util.Map; + +@ScoreClient +@ScoreInterface +public interface CPSTreasuryInterface { + @External(readonly = true) + String name(); + + @Payable + void fallback(); + + @External + void setCpsScore(Address _score); + + @External(readonly = true) + //Todo java convention in get methods?? + Address getCpsScore(); + + @External + void setCpfTreasuryScore(Address _score); + + @External(readonly = true) + Address getCpfTreasuryScore(); + + @External + void setBnUSDScore(Address _score); + + @External(readonly = true) + Address getBnUSDScore(); + + @External(readonly = true) + Map get_contributor_projected_fund(Address _wallet_address); + + @External(readonly = true) + List getContributorProjects(Address address); + + @External(readonly = true) + List getSponsorProjects(Address address); + + @External(readonly = true) + Map get_sponsor_projected_fund(Address _wallet_address); + + @External + @Payable + void update_proposal_fund(String ipfs_key, BigInteger added_budget, BigInteger _added_sponsor_reward, + int _added_installment_count); + + @External + void send_installment_to_contributor(String _ipfs_key); + + @External + void send_reward_to_sponsor(String _ipfs_key); + + @External + void disqualify_project(String _ipfs_key); + + @External + void claim_reward(); + + @External + void tokenFallback(Address from, BigInteger value, byte[] _data); + + // for migration into java contract + @External + void updateSponsorAndContributorProjects(); + + @EventLog(indexed = 1) + void ProposalDisqualified(String _ipfs_key, String note); + + @EventLog(indexed = 1) + void ProposalFundDeposited(String _ipfs_key, String note); + + @EventLog(indexed = 1) + void ProposalFundSent(Address _receiver_address, String note); + + @EventLog(indexed = 1) + void ProposalFundWithdrawn(Address _receiver_address, String note); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/DexInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/DexInterface.java new file mode 100644 index 00000000..1be7d1a4 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/DexInterface.java @@ -0,0 +1,28 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; +import score.Address; +import score.annotation.EventLog; +import score.annotation.External; + +import java.math.BigInteger; + +@ScoreInterface +@ScoreClient +public interface DexInterface { + + @External + void setSicxScore(Address _score); + @EventLog + void Deposit(Address from_token, Address from, BigInteger value); + + @EventLog(indexed = 2) + void Swap(BigInteger _id, Address _baseToken, Address _fromToken, Address _toToken, + Address _sender, Address _receiver, BigInteger _fromValue, BigInteger _toValue, + BigInteger _timestamp, BigInteger _lpFees, BigInteger _balnFees, BigInteger _poolBase, + BigInteger _poolQuote, BigInteger _endingPrice, BigInteger _effectiveFillPrice); + + @External + void tokenFallback(Address _from, BigInteger _value, byte[] _data); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/RouterInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/RouterInterface.java new file mode 100644 index 00000000..efb9b81b --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/RouterInterface.java @@ -0,0 +1,18 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; +import score.Address; +import score.annotation.External; +import score.annotation.Optional; +import score.annotation.Payable; + +import java.math.BigInteger; + +@ScoreInterface +@ScoreClient +public interface RouterInterface { + @Payable + @External + void route(Address[] _path, @Optional BigInteger _minReceive); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/SystemInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/SystemInterface.java new file mode 100644 index 00000000..76d46074 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/SystemInterface.java @@ -0,0 +1,45 @@ +package community.icon.cps.score.lib.interfaces; + +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; +import score.Address; +import score.annotation.Payable; + +import javax.management.MBeanServerInvocationHandler; +import java.math.BigInteger; +import java.util.Map; + +@ScoreClient +@ScoreInterface +public interface SystemInterface { + public static class Delegation{ + public Address address; + public BigInteger value; + } + + public static class Bond{ + public Address address; + public BigInteger value; + } + Map getIISSInfo(); + + Map queryIScore(Address address); + + Map getStake(Address address); + + Map getDelegation(Address address); + + Map getPReps(BigInteger startRanking, BigInteger endRanking); + + void setStake(BigInteger value); + + void setDelegation(Delegation[] delegations); + Map getPRepTerm(); + + void setBond(Bond[] bonds); + + void setBonderList(Address[] bonderList); + + @Payable + void registerPRep(String name, String email, String country, String city, String website, String details, String p2pEndpoint); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/bnUSDInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/bnUSDInterface.java new file mode 100644 index 00000000..21c472a5 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/bnUSDInterface.java @@ -0,0 +1,10 @@ +package community.icon.cps.score.lib.interfaces; + +import community.icon.cps.score.lib.interfaces.tokens.IRC2; +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; + +@ScoreInterface +@ScoreClient +public interface bnUSDInterface extends IRC2 { +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/sICXInterface.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/sICXInterface.java new file mode 100644 index 00000000..9433f219 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/sICXInterface.java @@ -0,0 +1,10 @@ +package community.icon.cps.score.lib.interfaces; + +import community.icon.cps.score.lib.interfaces.tokens.IRC2; +import foundation.icon.score.client.ScoreClient; +import foundation.icon.score.client.ScoreInterface; + +@ScoreInterface +@ScoreClient +public interface sICXInterface extends IRC2 { +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/tokens/IRC2.java b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/tokens/IRC2.java new file mode 100644 index 00000000..15a81ce2 --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/interfaces/tokens/IRC2.java @@ -0,0 +1,52 @@ +package community.icon.cps.score.lib.interfaces.tokens; + +import foundation.icon.score.client.ScoreInterface; +import score.Address; +import score.annotation.Optional; + +import java.math.BigInteger; + +@ScoreInterface +public interface IRC2 { + /** + * Returns the name of the token. (e.g. "MySampleToken") + */ + String name(); + + /** + * Returns the symbol of the token. (e.g. "MST") + */ + String symbol(); + + /** + * Returns the number of decimals the token uses. (e.g. 18) + */ + BigInteger decimals(); + + /** + * Returns the total token supply. + */ + BigInteger totalSupply(); + + /** + * Returns the account balance of another account with address {@code _owner}. + */ + BigInteger balanceOf(Address _owner); + + /** + * Transfers {@code _value} amount of tokens to address {@code _to}, and MUST fire the {@code Transfer} event. + * This function SHOULD throw if the caller account balance does not have enough tokens to spend. + * If {@code _to} is a contract, this function MUST invoke the function {@code tokenFallback(Address, int, bytes)} + * in {@code _to}. If the {@code tokenFallback} function is not implemented in {@code _to} (receiver contract), + * then the transaction must fail and the transfer of tokens should not occur. + * If {@code _to} is an externally owned address, then the transaction must be sent without trying to execute + * {@code tokenFallback} in {@code _to}. {@code _data} can be attached to this token transaction. + * {@code _data} can be empty. + */ + void transfer(Address _to, BigInteger _value, @Optional byte[] _data); + + /** + * (EventLog) Must trigger on any successful token transfers. + */ + void Transfer(Address _from, Address _to, BigInteger _value, byte[] _data); +} diff --git a/score-lib/src/main/java/community/icon/cps/score/lib/tokens/IRC2Base.java b/score-lib/src/main/java/community/icon/cps/score/lib/tokens/IRC2Base.java new file mode 100644 index 00000000..c837942d --- /dev/null +++ b/score-lib/src/main/java/community/icon/cps/score/lib/tokens/IRC2Base.java @@ -0,0 +1,130 @@ +/* + * Copyright (c) 2022-2022 Balanced.network. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package community.icon.cps.score.lib.tokens; + +import community.icon.cps.score.lib.interfaces.tokens.IRC2; +import score.Address; +import score.Context; +import score.DictDB; +import score.VarDB; +import score.annotation.EventLog; +import score.annotation.External; +import score.annotation.Optional; + +import java.math.BigInteger; + +public class IRC2Base implements IRC2 { + + private final static String NAME = "name"; + private final static String SYMBOL = "symbol"; + private final static String DECIMALS = "decimals"; + private final static String TOTAL_SUPPLY = "total_supply"; + private final static String BALANCES = "balances"; + + static final Address ZERO_ADDRESS = new Address(new byte[Address.LENGTH]); + + private final VarDB name = Context.newVarDB(NAME, String.class); + private final VarDB symbol = Context.newVarDB(SYMBOL, String.class); + private final VarDB decimals = Context.newVarDB(DECIMALS, BigInteger.class); + private final VarDB totalSupply = Context.newVarDB(TOTAL_SUPPLY, BigInteger.class); + protected final DictDB balances = Context.newDictDB(BALANCES, BigInteger.class); + + protected IRC2Base(String _tokenName, String _symbolName, @Optional BigInteger _decimals) { + if (this.name.get() == null) { + _decimals = _decimals == null ? BigInteger.valueOf(18L) : _decimals; + Context.require(_decimals.compareTo(BigInteger.ZERO) >= 0, "Decimals cannot be less than zero"); + + this.name.set(ensureNotEmpty(_tokenName)); + this.symbol.set(ensureNotEmpty(_symbolName)); + this.decimals.set(_decimals); + } + } + + @EventLog(indexed = 3) + public void Transfer(Address _from, Address _to, BigInteger _value, byte[] _data) { + } + + private String ensureNotEmpty(String str) { + Context.require(str != null && !str.trim().isEmpty(), "str is null or empty"); + assert str != null; + return str.trim(); + } + + @External(readonly = true) + public String name() { + return name.get(); + } + + @External(readonly = true) + public String symbol() { + return symbol.get(); + } + + @External(readonly = true) + public BigInteger decimals() { + return decimals.get(); + } + + @External(readonly = true) + public BigInteger totalSupply() { + return totalSupply.getOrDefault(BigInteger.ZERO); + } + + @External(readonly = true) + public BigInteger balanceOf(Address _owner) { + return balances.getOrDefault(_owner, BigInteger.ZERO); + } + + @External + public void transfer(Address _to, BigInteger _value, @Optional byte[] _data) { + transfer(Context.getCaller(), _to, _value, _data); + } + + protected void transfer(Address _from, Address _to, BigInteger _value, byte[] _data) { + Context.require(_value.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": _value needs to be positive"); + Context.require(balanceOf(_from).compareTo(_value) >= 0, this.name.get() + ": Insufficient balance"); + + this.balances.set(_from, balanceOf(_from).subtract(_value)); + this.balances.set(_to, balanceOf(_to).add(_value)); + + byte[] dataBytes = (_data == null) ? "None".getBytes() : _data; + Transfer(_from, _to, _value, dataBytes); + + if (_to.isContract()) { + Context.call(_to, "tokenFallback", _from, _value, dataBytes); + } + } + + protected void mint(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner), this.name.get() + ": Owner address cannot be zero address"); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": Amount needs to be positive"); + + totalSupply.set(totalSupply().add(amount)); + balances.set(owner, balanceOf(owner).add(amount)); + Transfer(ZERO_ADDRESS, owner, amount, "mint".getBytes()); + } + + protected void burn(Address owner, BigInteger amount) { + Context.require(!ZERO_ADDRESS.equals(owner), this.name.get() + ": Owner address cannot be zero address"); + Context.require(amount.compareTo(BigInteger.ZERO) >= 0, this.name.get() + ": Amount needs to be positive"); + Context.require(balanceOf(owner).compareTo(amount) >= 0, this.name.get() + ": Insufficient Balance"); + + balances.set(owner, balanceOf(owner).subtract(amount)); + totalSupply.set(totalSupply().subtract(amount)); + Transfer(owner, ZERO_ADDRESS, amount, "burn".getBytes()); + } +} diff --git a/settings.gradle b/settings.gradle index 9949ac10..69e2e90a 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1,2 +1,28 @@ rootProject.name = 'cps_java_contracts' -include ('CPFTreasury') + +include (':CPFTreasury') +project(':CPFTreasury').projectDir = file("CPFTreasury") + +include (':CPSTreasury') +project(':CPSTreasury').projectDir = file("CPSTreasury") + +include (':CPSCore') +project(':CPSCore').projectDir = file("CPSCore") + +include ('score-client') + +include ('test-lib') + +include ('score-lib') + +include (':Router') +project(':Router').projectDir = file("dummy/Router") + +include (':Dex') +project(':Dex').projectDir = file("dummy/Dex") + +include (':sICX') +project(':sICX').projectDir = file("dummy/sICX") + +include (':bnUSD') +project(':bnUSD').projectDir = file("dummy/bnUSD") diff --git a/test-lib/build.gradle b/test-lib/build.gradle new file mode 100644 index 00000000..bb9b7c53 --- /dev/null +++ b/test-lib/build.gradle @@ -0,0 +1,33 @@ +plugins { + id 'java' +} + +version '0.1.0' + +repositories { + mavenCentral() +} + +optimizedJar.enabled = false + +dependencies { + implementation "foundation.icon:icon-sdk:2.1.0" + implementation project(':score-lib') + annotationProcessor project(':score-client') + implementation project(':score-client') + implementation "org.junit.jupiter:junit-jupiter-api:5.8.2" + implementation "com.fasterxml.jackson.core:jackson-databind:2.13.3" + implementation "foundation.icon:javaee-unittest:0.9.4" + implementation "org.junit.jupiter:junit-jupiter:5.8.2" + runtimeOnly "org.junit.jupiter:junit-jupiter-engine:5.8.2" + implementation "com.github.sink772:minimal-json:0.9.7" + implementation "org.mockito:mockito-core:4.5.1" + implementation "org.json:json:20220320" + implementation "foundation.icon:javaee-scorex:0.5.2" + + implementation "com.github.javafaker:javafaker:1.0.2" +} + +test { + useJUnitPlatform() +} \ No newline at end of file diff --git a/test-lib/conf/env.props b/test-lib/conf/env.props new file mode 100644 index 00000000..6d1b17d9 --- /dev/null +++ b/test-lib/conf/env.props @@ -0,0 +1,6 @@ +node.url=http://localhost:9082 +node.apiVersion=3 + +chain.nid=0x3 +chain.godWallet=godWallet.json +chain.godPassword=gochain diff --git a/test-lib/conf/godWallet.json b/test-lib/conf/godWallet.json new file mode 100644 index 00000000..2a611aa3 --- /dev/null +++ b/test-lib/conf/godWallet.json @@ -0,0 +1,22 @@ +{ + "address": "hxb6b5791be0b5ef67063b3c10b840fb81514db2fd", + "id": "87323a66-289a-4ce2-88e4-00278deb5b84", + "version": 3, + "coinType": "icx", + "crypto": { + "cipher": "aes-128-ctr", + "cipherparams": { + "iv": "069e46aaefae8f1c1f840d6b09144999" + }, + "ciphertext": "f35ff7cf4f5759cb0878088d0887574a896f7f0fc2a73898d88be1fe52977dbd", + "kdf": "scrypt", + "kdfparams": { + "dklen": 32, + "n": 65536, + "r": 8, + "p": 1, + "salt": "0fc9c3b24cdb8175" + }, + "mac": "1ef4ff51fdee8d4de9cf59e160da049eb0099eb691510994f5eca492f56c817a" + } +} diff --git a/test-lib/src/main/java/community/icon/cps/score/test/integration/CPS.java b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPS.java new file mode 100644 index 00000000..ee46165d --- /dev/null +++ b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPS.java @@ -0,0 +1,156 @@ +package community.icon.cps.score.test.integration; + +import foundation.icon.icx.KeyWallet; +import foundation.icon.icx.data.Bytes; +import foundation.icon.score.client.DefaultScoreClient; + +import community.icon.cps.score.lib.interfaces.CPSTreasuryInterfaceScoreClient; +import community.icon.cps.score.lib.interfaces.CPFTreasuryInterfaceScoreClient; +import community.icon.cps.score.lib.interfaces.CPSCoreInterfaceScoreInterface; + +import community.icon.cps.score.lib.interfaces.DexInterfaceScoreClient; +import community.icon.cps.score.lib.interfaces.sICXInterfaceScoreClient; +import community.icon.cps.score.lib.interfaces.bnUSDInterfaceScoreClient; +import community.icon.cps.score.lib.interfaces.RouterInterfaceScoreClient; +import score.Address; + + +import java.io.BufferedReader; +import java.io.File; +import java.io.FileReader; +import java.math.BigInteger; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static community.icon.cps.score.test.integration.ScoreIntegrationTest.*; + +public class CPS { + public KeyWallet user; + public KeyWallet testUser; + + public KeyWallet owner; + + /* + Prep wallets loaded from test-lib/src/main/java/community/icon/cps/score/test/wallets + */ + + public KeyWallet prepWallet1; + public KeyWallet prepWallet2; + public KeyWallet prepWallet3; + public KeyWallet prepWallet4; + public KeyWallet prepWallet5; + public KeyWallet prepWallet6; + public KeyWallet prepWallet7; + + public CPSClient ownerClient; + public DefaultScoreClient cpsTreasury; + public DefaultScoreClient cpfTreasury; + public DefaultScoreClient cpsCore; + + public DefaultScoreClient dex; + public DefaultScoreClient router; + public DefaultScoreClient bnusd; + public DefaultScoreClient sicx; + + public DefaultScoreClient governanceBalanced; + + public CPSTreasuryInterfaceScoreClient cpsTreasuryScore; + public CPFTreasuryInterfaceScoreClient cpfTreasuryScore; + public CPSCoreInterfaceScoreInterface cpsMainScore; + + + Map cpsClients; + + public List prepList; + + public CPS() throws Exception{ + cpsClients = new HashMap<>(); + owner = createWalletWithBalance(BigInteger.TEN.pow(24)); + user = createWalletWithBalance(BigInteger.TEN.pow(24)); + testUser = createWalletWithBalance(BigInteger.TEN.pow(24)); + BufferedReader br = new BufferedReader(new FileReader("privateKey.txt")); +// System.out.println("Reading the content of the file in cpsMain: " + br.readLine()); + String longPrivateKey = br.readLine(); + String privateKey0 = longPrivateKey.substring(0, 66); + String privateKey1 = longPrivateKey.substring(66, 132); + String privateKey2 = longPrivateKey.substring(132, 198); + String privateKey3 = longPrivateKey.substring(198, 264); + String privateKey4 = longPrivateKey.substring(264, 330); + String privateKey5 = longPrivateKey.substring(330, 396); + String privateKey6 = longPrivateKey.substring(396, 462); + + prepWallet1 = KeyWallet.load(new Bytes(privateKey1)); + prepWallet2 = KeyWallet.load(new Bytes(privateKey2)); + prepWallet3 = KeyWallet.load(new Bytes(privateKey3)); + prepWallet4 = KeyWallet.load(new Bytes(privateKey4)); + prepWallet5 = KeyWallet.load(new Bytes(privateKey5)); + prepWallet6 = KeyWallet.load(new Bytes(privateKey6)); + prepWallet7 = KeyWallet.load(new Bytes(privateKey0)); + } + + public void setupCPS() throws Exception{ + registerPreps(); + deployContracts(); +// setStakeOfPreps(); +// setDelegationOfPreps(); +// setBonderListOfPReps(); +// setBondOfPreps(); +// registerGodPrep(); +// setGodStake(); + } + + public void registerGodPrep(){ + registerGodClient(); + } + + public void setGodPrep(){ + setGodStake(); + } + + public void setStakeOfPreps(){ + KeyWallet[] keyWallets = {prepWallet1, prepWallet2, prepWallet3, prepWallet4, prepWallet5, prepWallet6, prepWallet7}; + setStake(keyWallets); + } + + public void setDelegationOfPreps(){ + KeyWallet[] keyWallets = {prepWallet1, prepWallet2, prepWallet3, prepWallet4, prepWallet5, prepWallet6, prepWallet7}; + setDelegation(keyWallets); + } + + public void setBondOfPreps(){ + KeyWallet[] keyWallets = {prepWallet1, prepWallet2, prepWallet3, prepWallet4, prepWallet5, prepWallet6, prepWallet7}; + setBond(keyWallets); + } + + public void setBonderListOfPReps(){ + KeyWallet[] keyWallets = {prepWallet1, prepWallet2, prepWallet3, prepWallet4, prepWallet5, prepWallet6, prepWallet7}; + setBonderList(keyWallets); + } + + public void deployContracts(){ + cpsCore = deploy(owner, "CPSCore", null); + + Map cpsScoreAddress = Map.of("cps_score", cpsCore._address()); + cpsTreasury = deploy(owner, "CPSTreasury", cpsScoreAddress); + cpfTreasury = deploy(owner, "CPFTreasury", cpsScoreAddress); + + + dex = deploy(owner, "Dex", null); + + Map bnUSDParams = Map.of( + "_name", "Balanced Dollar", + "_symbol", "bnUSD", + "_decimals", BigInteger.valueOf(18), + "_initialSupply", BigInteger.valueOf(100000000)); + bnusd = deploy(owner, "bnUSD", bnUSDParams); + + sicx = deploy(owner, "sICX", bnUSDParams); + + router = deploy(owner, "Router", null); + } + + public void setScoreAddresses(){ + // CPS Main + } +} diff --git a/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSClient.java b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSClient.java new file mode 100644 index 00000000..14a22959 --- /dev/null +++ b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSClient.java @@ -0,0 +1,25 @@ +package community.icon.cps.score.test.integration; + +import foundation.icon.icx.KeyWallet; +import foundation.icon.score.client.DefaultScoreClient; + +import community.icon.cps.score.lib.interfaces.*; + +import static community.icon.cps.score.test.integration.ScoreIntegrationTest.chain; + +public class CPSClient { + private final KeyWallet wallet; + public CPSTreasuryInterfaceScoreClient cpsTreasury; + public CPFTreasuryInterfaceScoreClient cpfTreasury; + + public CPSClient(CPS cps, KeyWallet wallet){ + this.wallet = wallet; + cpsTreasury = new CPSTreasuryInterfaceScoreClient(chain.getEndpointURL(), chain.networkId, wallet, + cps.cpsTreasury._address()); + cpfTreasury = new CPFTreasuryInterfaceScoreClient(chain.getEndpointURL(), chain.networkId, wallet, cps.cpfTreasury._address()); + } + + public score.Address getAddress() { + return score.Address.fromString(wallet.getAddress().toString()); + } +} diff --git a/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSUtils.java b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSUtils.java new file mode 100644 index 00000000..e4a3ea2b --- /dev/null +++ b/test-lib/src/main/java/community/icon/cps/score/test/integration/CPSUtils.java @@ -0,0 +1,4 @@ +package community.icon.cps.score.test.integration; + +public class CPSUtils { +} diff --git a/test-lib/src/main/java/community/icon/cps/score/test/integration/Env.java b/test-lib/src/main/java/community/icon/cps/score/test/integration/Env.java new file mode 100644 index 00000000..d2fbadbf --- /dev/null +++ b/test-lib/src/main/java/community/icon/cps/score/test/integration/Env.java @@ -0,0 +1,93 @@ +package community.icon.cps.score.test.integration; + +import foundation.icon.icx.KeyWallet; +import foundation.icon.icx.Wallet; +import foundation.icon.icx.crypto.KeystoreException; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.math.BigInteger; +import java.nio.file.Path; +import java.util.Properties; + +public class Env { + private static Chain chain; + + static { + String envFile = System.getProperty("env.props", "/Users/ibriz/icon-foundation-cps/cps_java_contracts/test-lib/conf/env.props"); + Properties props = new Properties(); + try { + FileInputStream fis = new FileInputStream(envFile); + props.load(fis); + fis.close(); + } catch (IOException e) { + System.err.printf("'%s' does not exist\n", envFile); + throw new IllegalArgumentException(e.getMessage()); + } + String confPath = Path.of(envFile).getParent().toString() + "/"; + readProperties(props, confPath); + } + + private static void readProperties(Properties props, String confPath) { + String chainName = "chain"; + String nid = props.getProperty(chainName + ".nid"); + if (nid == null) { + throw new IllegalArgumentException("nid not found"); + } + String godWalletPath = confPath + props.getProperty(chainName + ".godWallet"); + String godPassword = props.getProperty(chainName + ".godPassword"); + KeyWallet godWallet; + try { + godWallet = readWalletFromFile(godWalletPath, godPassword); + } catch (IOException e) { + throw new IllegalArgumentException(e.getMessage()); + } + String nodeName = "node"; + String url = props.getProperty(nodeName + ".url"); + if (url == null) { + throw new IllegalArgumentException("node url not found"); + } + + String apiVersion = props.getProperty(nodeName + ".apiVersion"); + if (apiVersion == null) { + throw new IllegalArgumentException("apiVersion not found"); + } + chain = new Chain(BigInteger.valueOf(Integer.parseInt(nid.substring(2), 16)), godWallet, url, apiVersion); + } + + private static KeyWallet readWalletFromFile(String path, String password) throws IOException { + try { + File file = new File(path); + return KeyWallet.load(password, file); + } catch (KeystoreException e) { + e.printStackTrace(); + throw new IOException("Key load failed!"); + } + } + + public static Chain getDefaultChain() { + if (chain == null) { + throw new AssertionError("Chain not found"); + } + return chain; + } + + public static class Chain { + public final BigInteger networkId; + public final Wallet godWallet; + private final String nodeUrl; + private final String apiVersion; + + public Chain(BigInteger networkId, Wallet godWallet, String url, String apiVersion) { + this.networkId = networkId; + this.godWallet = godWallet; + this.nodeUrl = url; + this.apiVersion = apiVersion; + } + + public String getEndpointURL() { + return this.nodeUrl + "/api/v" + apiVersion; + } + } +} diff --git a/test-lib/src/main/java/community/icon/cps/score/test/integration/ScoreIntegrationTest.java b/test-lib/src/main/java/community/icon/cps/score/test/integration/ScoreIntegrationTest.java new file mode 100644 index 00000000..4390c289 --- /dev/null +++ b/test-lib/src/main/java/community/icon/cps/score/test/integration/ScoreIntegrationTest.java @@ -0,0 +1,326 @@ +package community.icon.cps.score.test.integration; + +import community.icon.cps.score.lib.interfaces.SystemInterface; +import foundation.icon.icx.KeyWallet; +import foundation.icon.icx.Wallet; +import foundation.icon.icx.data.Bytes; +import foundation.icon.jsonrpc.Address; +import foundation.icon.jsonrpc.model.Hash; +import foundation.icon.jsonrpc.model.TransactionResult; +import foundation.icon.score.client.DefaultScoreClient; +import foundation.icon.score.client.RevertedException; + +import community.icon.cps.score.lib.interfaces.SystemInterfaceScoreClient; +import net.bytebuddy.pool.TypePool; +import org.junit.jupiter.api.MethodOrderer; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.TestMethodOrder; +import org.junit.jupiter.api.function.Executable; +import score.UserRevertedException; + +import java.io.BufferedReader; +import java.io.FileOutputStream; +import java.io.FileReader; +import java.io.PrintStream; +import java.math.BigInteger; +import java.security.InvalidAlgorithmParameterException; +import java.security.Key; +import java.security.NoSuchAlgorithmException; +import java.security.NoSuchProviderException; +import java.util.List; +import java.util.Map; +import java.util.Properties; +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Predicate; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import static community.icon.cps.score.test.integration.Env.Chain; +import static community.icon.cps.score.test.integration.Env.getDefaultChain; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static community.icon.cps.score.lib.interfaces.SystemInterface.Delegation; +import static community.icon.cps.score.lib.interfaces.SystemInterface.Bond; + +@Tag("integration") +@TestMethodOrder(value = MethodOrderer.OrderAnnotation.class) +public interface ScoreIntegrationTest { + Chain chain = getDefaultChain(); + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, chain.godWallet, + DefaultScoreClient.ZERO_ADDRESS); + DefaultScoreClient client = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, null, null); + + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + public static final BigInteger EXA = BigInteger.valueOf(1_000_000_000_000_000_000L); + + @SuppressWarnings("unchecked") + static void registerPreps() throws Exception { + + Map getPreps; + String privateKey = ""; + + try { + getPreps = systemScore.getPReps(BigInteger.ONE, BigInteger.valueOf(7)); + } catch (Exception e) { + privateKey = registerPrep(); + getPreps = systemScore.getPReps(BigInteger.ONE, BigInteger.valueOf(7)); + } + + List> prepList = (List>) getPreps.get("preps"); + int prepCount = prepList.size(); + if (prepCount >= 7) { + return; + } + int remainingPrepsToRegister = 7 - prepCount; + for (int i = 0; i < remainingPrepsToRegister; i++) { + privateKey = privateKey + registerPrep(); + } + PrintStream out = new PrintStream(new FileOutputStream("privateKey.txt")); + System.out.println("private keys: " + privateKey); + out.println(privateKey); + BufferedReader br = new BufferedReader(new FileReader("privateKey.txt")); + System.out.println("Reading the content of the file: " + br.readLine()); + } + + private static String registerPrep() throws Exception { + KeyWallet owner = createWalletWithBalance(BigInteger.TEN.pow(24)); + String privateKey = String.valueOf(owner.getPrivateKey()); +// PrintStream out = new PrintStream(new FileOutputStream("privateKey.txt")); +// System.out.println("private keys: " + privateKey); +// out.println(privateKey); + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner, + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.registerPRep(BigInteger.valueOf(2000).multiply(BigInteger.TEN.pow(18)), "test", + "kokoa@example.com", "USA", "New York", "https://icon.kokoa.com", + "https://icon.kokoa.com/json/details.json", "localhost:9082"); + return privateKey; + } + + public static void registerGodClient() { + systemScore.registerPRep(BigInteger.valueOf(2000).multiply(BigInteger.TEN.pow(18)), "test", + "kokoa@example.com", "USA", "New York", "https://icon.kokoa.com", + "https://icon.kokoa.com/json/details.json", "localhost:9082"); + + } + + public static void registerPrepByPrivateKey(KeyWallet owner){ + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner, + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.registerPRep(BigInteger.valueOf(2000).multiply(BigInteger.TEN.pow(18)), "test", + "kokoa@example.com", "USA", "New York", "https://icon.kokoa.com", + "https://icon.kokoa.com/json/details.json", "localhost:9082"); + } + + public static void setStake(KeyWallet[] owner) { + for (int i = 0; i < owner.length; i++) { + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner[i], + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.setStake(BigInteger.valueOf(1000).multiply(EXA)); + } + } + + public static void setGodStake(){ + systemScore.setStake(BigInteger.valueOf(9000000).multiply(EXA)); + Delegation[] delegation = new Delegation[1]; + delegation[0] = new Delegation(); + delegation[0].address = score.Address.fromString(chain.godWallet.getAddress().toString()); + delegation[0].value = BigInteger.valueOf(8000000).multiply(EXA); + systemScore.setDelegation(delegation); + + score.Address[] addresses = new score.Address[1]; + addresses[0] = score.Address.fromString(chain.godWallet.getAddress().toString()); + systemScore.setBonderList(addresses); + + Bond[] bonds = new Bond[1]; + bonds[0] = new Bond(); + bonds[0].address = score.Address.fromString(chain.godWallet.getAddress().toString()); + bonds[0].value = BigInteger.valueOf(1000000).multiply(EXA); + systemScore.setBond(bonds); + } + public static void setDelegation(KeyWallet[] owner){ + for (int i = 0; i < owner.length; i++) { + Delegation[] delegations = new Delegation[1]; + delegations[0] = new Delegation(); + delegations[0].address = score.Address.fromString(owner[i].getAddress().toString()); + delegations[0].value = BigInteger.valueOf(400).multiply(EXA); + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner[i], + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.setDelegation(delegations); + } + } + + public static void setBond(KeyWallet[] owner) { + for (int i = 0; i < owner.length; i++) { + Bond[] bond = new Bond[1]; + bond[0] = new Bond(); + bond[0].address = score.Address.fromString(owner[i].getAddress().toString()); + bond[0].value = BigInteger.valueOf(400).multiply(EXA); + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner[i], + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.setBond(bond); + } + } + + public static void setBonderList(KeyWallet[] owner){ + for (int i = 0; i < owner.length; i++) { + score.Address[] bonderList = new score.Address[1]; + bonderList[0] = score.Address.fromString(owner[i].getAddress().toString()); + DefaultScoreClient godClient = new DefaultScoreClient(chain.getEndpointURL(), chain.networkId, owner[i], + DefaultScoreClient.ZERO_ADDRESS); + SystemInterfaceScoreClient systemScore = new SystemInterfaceScoreClient(godClient); + systemScore.setBonderList(bonderList); + } + } + + static KeyWallet createWalletWithBalance(BigInteger amount) throws Exception { + KeyWallet wallet = KeyWallet.create(); + Address address = DefaultScoreClient.address(wallet.getAddress().toString()); + transfer(address, amount); + return wallet; + } + + static void transfer(Address address, BigInteger amount) { + godClient._transfer(address, amount, null); + } + + static DefaultScoreClient deploy(Wallet wallet, String name, Map params) { + String path = getFilePath(name); + return DefaultScoreClient._deploy(chain.getEndpointURL(), chain.networkId, wallet, path, params); + } + + static Hash deployAsync(Wallet wallet, String name, Map params) { + String path = getFilePath(name); + return DefaultScoreClient._deployAsync(chain.getEndpointURL(), chain.networkId, wallet, path, params); + } + + static DefaultScoreClient getDeploymentResult(Wallet wallet, Hash hash) { + return DefaultScoreClient.getDeploymentResult(chain.getEndpointURL(), chain.networkId, wallet, hash); + } + + static String getFilePath(String key) { + String path = System.getProperty(key); + if (path == null) { + throw new IllegalArgumentException("No such property: " + key); + } + return path; + } + + static int indexOf(T[] array, T value) { + return indexOf(array, value::equals); + } + + static int indexOf(T[] array, Predicate predicate) { + for (int i = 0; i < array.length; i++) { + if (predicate.test(array[i])) { + return i; + } + } + return -1; + } + + static boolean contains(Map map, String key, Object value) { + return contains(map, key, value::equals); + } + + static boolean contains(Map map, String key, Predicate predicate) { + return map.containsKey(key) && predicate.test(map.get(key)); + } + + static List eventLogs(TransactionResult txr, String signature, Address scoreAddress, + Function mapperFunc, Predicate filter) { + Predicate predicate = (el) -> el.getIndexed().get(0).equals(signature); + if (scoreAddress != null) { + predicate = predicate.and((el) -> el.getScoreAddress().toString().equals(scoreAddress.toString())); + } + Stream stream = txr.getEventLogs().stream().filter(predicate).map(mapperFunc); + if (filter != null) { + stream = stream.filter(filter); + } + return stream.collect(Collectors.toList()); + } + + static void waitByNumOfBlock(long numOfBlock) { + waitByHeight(client._lastBlockHeight().add(BigInteger.valueOf(numOfBlock))); + } + + static void waitByHeight(long waitHeight) { + waitByHeight(BigInteger.valueOf(waitHeight)); + } + + static void waitByHeight(BigInteger waitHeight) { + BigInteger height = client._lastBlockHeight(); + while (height.compareTo(waitHeight) < 0) { + System.out.println("height: " + height + ", waitHeight: " + waitHeight); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + height = client._lastBlockHeight(); + } + } + + static void balanceCheck(Address address, BigInteger value, Executable executable) { + BigInteger balance = client._balance(address); + try { + executable.execute(); + } catch (UserRevertedException | RevertedException e) { + throw e; + } catch (Throwable e) { + throw new RuntimeException(e); + } + assertEquals(balance.add(value), client._balance(address)); + } + + @FunctionalInterface + interface EventLogsSupplier { + List apply(TransactionResult txr, Address address, Predicate filter); + } + + static Consumer eventLogChecker( + Address address, EventLogsSupplier supplier, Consumer consumer) { + return (txr) -> { + List eventLogs = supplier.apply(txr, address, null); + assertEquals(1, eventLogs.size()); + if (consumer != null) { + consumer.accept(eventLogs.get(0)); + } + }; + } + + static Consumer eventLogsChecker(Address address, EventLogsSupplier supplier, + Consumer> consumer) { + return (txr) -> { + List eventLogs = supplier.apply(txr, address, null); + if (consumer != null) { + consumer.accept(eventLogs); + } + }; + } + + static Consumer dummyConsumer() { + return (txr) -> { + + }; + } + + static Wallet getOrGenerateWallet(String prefix, Properties properties) { + Wallet wallet = DefaultScoreClient.wallet(prefix, properties); + return wallet == null ? generateWallet() : wallet; + } + + static KeyWallet generateWallet() { + try { + return KeyWallet.create(); + } catch (InvalidAlgorithmParameterException | NoSuchAlgorithmException | NoSuchProviderException e) { + throw new RuntimeException(e); + } + } +} + +